提交 09b9add8 编写于 作者: X Xavier Noria

Merge commit 'docrails/master'

......@@ -58,9 +58,9 @@ end
Here is a quick explanation of the items presented in the preceding method. For a full list of all available options, please have a look further down at the Complete List of ActionMailer user-settable attributes section.
* <tt>default Hash</tt> - This is a hash of default values for any email you send, in this case we are setting the <tt>:from</tt> header to a value for all messages in this class, this can be overridden on a per email basis
* +mail+ - The actual email message, we are passing the <tt>:to</tt> and <tt>:subject</tt> headers in|
* +mail+ - The actual email message, we are passing the <tt>:to</tt> and <tt>:subject</tt> headers in.
And instance variables we define in the method become available for use in the view.
Just like controllers, any instance variables we define in the method become available for use in the views.
h5. Create a Mailer View
......@@ -104,9 +104,9 @@ When you call the +mail+ method now, Action Mailer will detect the two templates
h5. Wire It Up So That the System Sends the Email When a User Signs Up
There are three ways to achieve this. One is to send the email from the controller that sends the email, another is to put it in a +before_create+ callback in the user model, and the last one is to use an observer on the user model. Whether you use the second or third methods is up to you, but staying away from the first is recommended. Not because it's wrong, but because it keeps your controller clean, and keeps all logic related to the user model within the user model. This way, whichever way a user is created (from a web form, or from an API call, for example), we are guaranteed that the email will be sent.
There are several ways to do this, some people create Rails Observers to fire off emails, others do it inside of the User Model. However, in Rails 3, mailers are really just another way to render a view. Instead of rendering a view and sending out the HTTP protocol, they are just sending it out through the Email protocols instead. Due to this, it makes sense to just have your controller tell the mailer to send an email when a user is successfully created.
Let's see how we would go about wiring it up using an observer.
Setting this up is painfully simple.
First off, we need to create a simple +User+ scaffold:
......@@ -115,35 +115,38 @@ $ rails generate scaffold user name:string email:string login:string
$ rake db:migrate
</shell>
Now that we have a user model to play with, edit +config/application.rb+ and register the observer:
Now that we have a user model to play with, we will just edit the +app/controllers/users_controller.rb+ make it instruct the UserMailer to deliver an email to the newly created user by editing the create action and inserting a call to <tt>UserMailer.welcome_email</tt> right after the user is successfully saved:
<ruby>
module MailerGuideCode
class Application < Rails::Application
# ...
config.active_record.observers = :user_observer
class UsersController < ApplicationController
# POST /users
# POST /users.xml
def create
@user = User.new(params[:user])
respond_to do |format|
if @user.save
# Tell the UserMailer to send a welcome Email after save
UserMailer.welcome_email(@user).deliver
format.html { redirect_to(@user, :notice => 'User was successfully created.') }
format.xml { render :xml => @user, :status => :created, :location => @user }
else
format.html { render :action => "new" }
format.xml { render :xml => @user.errors, :status => :unprocessable_entity }
end
end
end
end
</ruby>
You can make a +app/observers+ directory and Rails will automatically load it for you (Rails will automatically load anything in the +app+ directory as of version 3.0)
Now create a file called +user_observer.rb+ in +app/observers+ and make it look like:
<ruby>
class UserObserver < ActiveRecord::Observer
def after_create(user)
UserMailer.welcome_email(user).deliver
end
end
</ruby>
This provides a much simpler implementation that does not require the registering of observers and the like.
Notice how we call <tt>UserMailer.welcome_email(user)</tt>? Even though in the <tt>user_mailer.rb</tt> file we defined an instance method, we are calling the method_name +welcome_email(user)+ on the class. This is a peculiarity of Action Mailer.
The method +welcome_email+ returns a Mail::Message object which can then just be told +deliver+ to send itself out.
NOTE: In previous versions of Rails, you would call +deliver_welcome_email+ or +create_welcome_email+ however in Rails 3.0 this has been deprecated in favour of just calling the method name itself.
The method +welcome_email+ returns a Mail::Message object which can then just be told +deliver+ to send itself out.
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.
h4. Complete List of Action Mailer Methods
......@@ -160,21 +163,23 @@ Defining custom headers are simple, you can do it one of three ways:
* Defining a header field as a parameter to the +mail+ method:
<ruby>
mail(:x_spam => value)
mail("X-Spam" => value)
</ruby>
* Passing in a key value assignment to the +headers+ method:
<ruby>
headers[:x_spam] = value
headers["X-Spam"] = value
</ruby>
* Passing a hash of key value pairs to the +headers+ method:
<ruby>
headers {:x_spam => value, :x_special => another_value}
headers {"X-Spam" => value, "X-Special" => another_value}
</ruby>
TIP: All <tt>X-Value</tt> headers per the RFC2822 can appear more than one time. If you want to delete an <tt>X-Value</tt> header, you need to assign it a value of <tt>nil</tt>.
h5. Adding Attachments
Adding attachments has been simplified in Action Mailer 3.0.
......@@ -325,7 +330,7 @@ class UserMailer < ActionMailer::Base
end
</ruby>
The above will send a multipart email with an attachment, properly nested with the top level being <tt>mixed/multipart</tt> and the first part being a <tt>mixed/alternative</tt> containing the plain text and HTML email messages.
The above will send a multipart email with an attachment, properly nested with the top level being <tt>multipart/mixed</tt> and the first part being a <tt>multipart/alternative</tt> containing the plain text and HTML email messages.
h3. Receiving Emails
......
......@@ -752,6 +752,7 @@ h3. Building Complex Forms
Many apps grow beyond simple forms editing a single object. For example when creating a Person you might want to allow the user to (on the same form) create multiple address records (home, work, etc.). When later editing that person the user should be able to add, remove or amend addresses as necessary. While this guide has shown you all the pieces necessary to handle this, Rails does not yet have a standard end-to-end way of accomplishing this, but many have come up with viable approaches. These include:
* As of Rails 2.3, Rails includes "Nested Attributes":./2_3_release_notes.html#nested-attributes and "Nested Object Forms":./2_3_release_notes.html#nested-object-forms
* Ryan Bates' series of Railscasts on "complex forms":http://railscasts.com/episodes/75
* Handle Multiple Models in One Form from "Advanced Rails Recipes":http://media.pragprog.com/titles/fr_arr/multiple_models_one_form.pdf
* Eloy Duran's "complex-forms-examples":http://github.com/alloy/complex-form-examples/ application
......
......@@ -129,7 +129,7 @@ However, you would probably like to *provide support for more locales* in your a
WARNING: You may be tempted to store the chosen locale in a _session_ or a _cookie_. *Do not do so*. The locale should be transparent and a part of the URL. This way you don't break people's basic assumptions about the web itself: if you send a URL of some page to a friend, she should see the same page, same content. A fancy word for this would be that you're being "_RESTful_":http://en.wikipedia.org/wiki/Representational_State_Transfer. Read more about the RESTful approach in "Stefan Tilkov's articles":http://www.infoq.com/articles/rest-introduction. There may be some exceptions to this rule, which are discussed below.
The _setting part_ is easy. You can set the locale in a +before_filter+ in the ApplicationController like this:
The _setting part_ is easy. You can set the locale in a +before_filter+ in the +ApplicationController+ like this:
<ruby>
before_filter :set_locale
......@@ -158,7 +158,7 @@ You can implement it like this in your +ApplicationController+:
before_filter :set_locale
def set_locale
I18n.locale = extract_locale_from_uri
I18n.locale = extract_locale_from_tld
end
# Get locale from top-level domain or return nil if such locale is not available
......@@ -206,7 +206,7 @@ Getting the locale from +params+ and setting it accordingly is not hard; includi
Rails contains infrastructure for "centralizing dynamic decisions about the URLs" in its "+ApplicationController#default_url_options+":http://api.rubyonrails.org/classes/ActionController/Base.html#M000515, which is useful precisely in this scenario: it enables us to set "defaults" for "+url_for+":http://api.rubyonrails.org/classes/ActionController/Base.html#M000503 and helper methods dependent on it (by implementing/overriding this method).
We can include something like this in our ApplicationController then:
We can include something like this in our +ApplicationController+ then:
<ruby>
# app/controllers/application_controller.rb
......
......@@ -1792,487 +1792,53 @@ Now that Rails has finished loading all the Railties by way of +require 'rails/a
NOTE: It is worth mentioning here that you are not tied to using Bundler with Rails 3, but it is (of course) advised that you do. To "turn off" Bundler, comment out or remove the corresponding lines in _config/application.rb_ and _config/boot.rb_.
Bundler was +require+'d back in _config/boot.rb_ and now we'll dive into the internals of Bundler to determine precisely what this line accomplishes.
Bundler was +require+'d back in _config/boot.rb_, and so that is what makes it available here. This guide does not dive into the internals of Bundler; it's really it's own separate guide.
h4. +Bundler.require+
The +Bundler.require+ method adds all the gems not specified inside a +group+ in the +Gemfile+ and the ones specified in groups for the +Rails.env+ (in this case, _development_), to the load path. This is how an application is able to find them.
+Bundler.require+ is defined in _lib/bundler.rb_:
The rest of this file is spent defining your application's main class. This is it without the comments:
<ruby>
def require(*groups)
gemfile = default_gemfile
load(gemfile).require(*groups)
end
</ruby>
The +groups+ variable here would be a two-element array of the arguments passed to +Bundler.require+. In this case we're going to assume, +Rails.env+ is +"development"+.
h4. Locating the Gemfile
+default_gemfile+ is defined in _lib/bundler.rb_ and makes a call out to the +SharedHelpers+ module:
<ruby>
def default_gemfile
SharedHelpers.default_gemfile
end
</ruby>
+SharedHelpers+ defines +default_gemfile+ like this:
<ruby>
def default_gemfile
gemfile = find_gemfile
gemfile or raise GemfileNotFound, "The default Gemfile was not found"
Pathname.new(gemfile)
end
</ruby>
+find_gemfile+ is defined like this:
<ruby>
def find_gemfile
return ENV['BUNDLE_GEMFILE'] if ENV['BUNDLE_GEMFILE']
previous = nil
current = File.expand_path(Dir.pwd)
until !File.directory?(current) || current == previous
filename = File.join(current, 'Gemfile')
return filename if File.file?(filename)
current, previous = File.expand_path("..", current), current
end
end
</ruby>
The first line of course means if you define the environment variable +BUNDLE_GEMFILE+ this is the name of the file that will be used and returned. If not, then Bundler will look for a file called _Gemfile_ in the current directory and if it can find it then it will return the filename. If it cannot, it will recurse up the directory structure until it does. Once the file is found a +Pathname+ is made from the expanded path to _Gemfile_.
If the file cannot be found at all then +GemfileNotFound+ will be raised back in +default_gemfile+.
h4. Loading the Gemfile
Now that Bundler has determined what the _Gemfile_ is, it goes about loading it:
<ruby>
def require(*groups)
gemfile = default_gemfile
load(gemfile).require(*groups)
end
</ruby>
+load+ is defined like this in _lib/bundler.rb_:
<ruby>
def load(gemfile = default_gemfile)
root = Pathname.new(gemfile).dirname
Runtime.new root, definition(gemfile)
end
</ruby>
The next method to be called here would be +definition+ and it is defined like this:
<ruby>
def definition(gemfile = default_gemfile)
configure
root = Pathname.new(gemfile).dirname
lockfile = root.join("Gemfile.lock")
if lockfile.exist?
Definition.from_lock(lockfile)
else
Definition.from_gemfile(gemfile)
end
end
</ruby>
+configure+ is responsible for setting up the path to gem home and gem path:
<ruby>
def configure
@configured ||= begin
configure_gem_home_and_path
true
end
end
</ruby>
+configure_gem_home_and_path+ defined like this:
<ruby>
def configure_gem_home_and_path
if settings[:disable_shared_gems]
ENV['GEM_HOME'] = File.expand_path(bundle_path, root)
ENV['GEM_PATH'] = ''
else
gem_home, gem_path = Gem.dir, Gem.path
ENV["GEM_PATH"] = [gem_home, gem_path].flatten.compact.join(File::PATH_SEPARATOR)
ENV["GEM_HOME"] = bundle_path.to_s
end
Gem.clear_paths
end
</ruby>
We do not have +settings[:disabled_shared_gems]+ set to true so this will execute the code under the +else+. The +ENV["GEM_PATH"]+ will resemble +/usr/local/lib/ruby/gems/1.9.1:/home/you/.gem/ruby/1.9.1+
And +ENV["GEM_HOME"]+ will be the path to the gems installed into your home directory by Bundler, something resembling +/home/you/.bundle/ruby/1.9.1+.
After +configure_gem_home_and_path+ is done the +definition+ method goes about creating a +Definition+ from either +Gemfile.lock+ if it exists, or the +gemfile+ previously located. +Gemfile.lock+ only exists if +bundle lock+ has been ran and so far it has not.
+Definition.from_gemfile+ is defined in _lib/bundler/definition.rb_:
<ruby>
def self.from_gemfile(gemfile)
gemfile = Pathname.new(gemfile).expand_path
unless gemfile.file?
raise GemfileNotFound, "#{gemfile} not found"
end
Dsl.evaluate(gemfile)
end
</ruby>
Now that the +gemfile+ is located +Dsl.evaluate+ goes about loading it. The code for this can be found in _lib/bundler/dsl.rb_:
<ruby>
def self.evaluate(gemfile)
builder = new
builder.instance_eval(File.read(gemfile.to_s), gemfile.to_s, 1)
builder.to_definition
end
</ruby>
+new+ here will, of course, call +initialize+ which sets up a couple of variables:
<ruby>
def initialize
@source = nil
@sources = []
@dependencies = []
@group = nil
end
</ruby>
When Bundler calls +instance_eval+ on the new +Bundler::Dsl+ object it evaluates the content of the +gemfile+ file within the context of this instance. The Gemfile for a default Rails 3 project with all the comments stripped out looks like this:
<ruby>
source 'http://rubygems.org'
gem 'rails', '3.0.0.beta1'
# Bundle edge Rails instead:
# gem 'rails', :git => 'git://github.com/rails/rails.git'
gem 'sqlite3-ruby', :require => 'sqlite3'
</ruby>
When Bundler loads this file it firstly calls the +source+ method on the +Bundler::Dsl+ object:
<ruby>
def source(source, options = {})
@source = case source
when :gemcutter, :rubygems, :rubyforge then Source::Rubygems.new("uri" => "http://gemcutter.org")
when String then Source::Rubygems.new("uri" => source)
else source
end
options[:prepend] ? @sources.unshift(@source) : @sources << @source
yield if block_given?
@source
ensure
@source = nil
end
</ruby>
TODO: Perhaps make this a side-note. However you do that.
The interesting thing to note about this method is that it takes a block, so you may do:
<ruby>
source 'http://someothergemhost.com' do
gem 'your_favourite_gem'
end
</ruby>
if you wish to install _your_favourite_gem_ from _http://someothergemhost.com_.
In this instance however a block is not specified so this sets up the +@source+ instance variable to be +'http://rubygems.org'+.
The next method that is called is +gem+:
<ruby>
def gem(name, *args)
options = Hash === args.last ? args.pop : {}
version = args.last || ">= 0"
if options[:group]
options[:group] = options[:group].to_sym
module YourApp
class Application < Rails::Application
config.encoding = "utf-8"
config.filter_parameters += [:password]
end
_deprecated_options(options)
_normalize_options(name, version, options)
@dependencies << Dependency.new(name, version, options)
end
</ruby>
This sets up a couple of important things initially. If you specify a gem like the following:
<ruby>
gem 'rails', "2.3.4"
</ruby>
This sets +options+ to be an empty hash, but +version+ to be +"2.3.4"+. TODO: How does one pass options and versions at the same time?
h3. Return to Rails
In the Gemfile for a default Rails project, the first +gem+ line is:
On the surface, this looks like a simple class inheritance. There's more underneath though. back in +Rails::Application+, the +inherited+ method is defined:
<ruby>
gem 'rails', '3.0.0.beta2'
</ruby>
TODO: change version number.
This line will check that +options+ contains no deprecated options by using the +_deprecated_options+ method, but the +options+ hash is empty. This is of course until +_normalize_options+ has its way:
<ruby>
def _normalize_options(name, version, opts)
_normalize_hash(opts)
group = opts.delete("group") || @group
# Normalize git and path options
["git", "path"].each do |type|
if param = opts[type]
options = _version?(version) ? opts.merge("name" => name, "version" => version) : opts.dup
source = send(type, param, options, :prepend => true)
opts["source"] = source
end
end
opts["source"] ||= @source
opts["group"] = group
def inherited(base)
raise "You cannot have more than one Rails::Application" if Rails.application
super
Rails.application = base.instance
end
</ruby>
+_normalize_hash+ will convert all the keys in the +opts+ hash to strings. There is neither a +git+ or a +path+ key in the +opts+ hash so the next couple of lines are ignored, then the +source+ and +group+ keys are set up.
TODO: Maybe it is best to cover what would happen in the case these lines did exist?
The next line goes about defining a dependency for this gem:
<ruby>
@dependencies << Dependency.new(name, version, options)
</ruby>
This class is defined like this:
We do not already have a +Rails.application+, so instead this resorts to calling +super+. +Rails::Application+ descends from +Rails::Engine+ and so will call the +inherited+ method in +Rails::Engine+, but before that it's important to note that +called_from+ is defined an +attr_accessor+ on +Rails::Engine+:
<ruby>
module Bundler
class Dependency < Gem::Dependency
attr_reader :autorequire
attr_reader :groups
def initialize(name, version, options = {}, &blk)
super(name, version)
@autorequire = nil
@groups = Array(options["group"] || :default).map { |g| g.to_sym }
@source = options["source"]
if options.key?('require')
@autorequire = Array(options['require'] || [])
end
def inherited(base)
unless base.abstract_railtie?
base.called_from = begin
# Remove the line number from backtraces making sure we don't leave anything behind
call_stack = caller.map { |p| p.split(':')[0..-2].join(':') }
File.dirname(call_stack.detect { |p| p !~ %r[railties[\w\-\.]*/lib/rails|rack[\w\-\.]*/lib/rack] })
end
end
end
</ruby>
The +initialize+ method in +Gem::Dependency+ is defined:
<ruby>
def initialize(name, version_requirements, type=:runtime)
@name = name
unless TYPES.include? type
raise ArgumentError, "Valid types are #{TYPES.inspect}, not #{@type.inspect}"
end
@type = type
@version_requirements = Gem::Requirement.create version_requirements
@version_requirement = nil # Avoid warnings.
end
</ruby>
The +version_requirements+ that was passed in here will be inspected by +Gem::Requirement.create+ and return, for our +3.0.0beta2+ version string a +Gem::Requirement+ object:
<ruby>
#<Gem::Requirement:0x101dd8c20 @requirements=[["=", #<Gem::Version "3.0.0beta2">]]>
</ruby>
Going back to +Bundler::Dependency+, the next line simply sets +@autorequire+ to +nil+ and the next line is a little more interesting:
<ruby>
@autorequire = nil
@groups = Array(options["group"] || :default).map { |g| g.to_sym }
</ruby>
Here, bundler sets the +groups+ variable to be whatever +group+ we've set for this gem and also demonstrates through code that the +group+ option allows for multiple groups, so in the _Gemfile_ we can specify the same gem for multiple groups:
<ruby>
group :test, :cucumber do
gem 'faker'
end
</ruby>
The final lines in +initialize+ work on the +require+ option which is not passed:
<ruby>
if options.key?('require')
@autorequire = Array(options['require'] || [])
end
</ruby>
If it were to be used in the _Gemfile_, it would look like this:
<ruby>
gem 'thinking-sphinx', :require => "thinking_sphinx"
</ruby>
So far, this is what simply loading the _Gemfile_ does.
h3. Bring forth the gems
Now that the _Gemfile_ has finished being parsed, the next line is:
<ruby>
builder.to_definition
</ruby>
This method is defined in _lib/bundler/dsl.rb_ and does this:
<ruby>
def to_definition
Definition.new(@dependencies, @sources)
end
</ruby>
The +Bundler::Definition#initialize+ method is this:
<ruby>
def initialize(dependencies, sources)
@dependencies = dependencies
@sources = sources
end
</ruby>
Now Bundler has a +Bundler::Definition+ object to be passed back to the +load+ method from _lib/bundler.rb_:
<ruby>
def load(gemfile = default_gemfile)
root = Pathname.new(gemfile).dirname
Runtime.new root, definition(gemfile)
end
</ruby>
The +Bundler::Runtime+ class inherits from +Bundler::Environment+ and the reason this is pointed out is because +super+ is used in the +initialize+ method in +Bundler::Runtime+:
<ruby>
super
if locked?
write_rb_lock
end
</ruby>
Thankfully, the +Bundler::Environment#initialize+ method is nothing too complex:
<ruby>
def initialize(root, definition)
@root = root
@definition = definition
end
</ruby>
The +locked?+ method checks if the _Gemfile.lock_ or _.bundler/environment.rb_ files exist:
<ruby>
def locked?
File.exist?("#{root}/Gemfile.lock") || File.exist?("#{root}/.bundle/environment.rb")
end
</ruby>
And if they do will call +write_rb_lock+:
<ruby>
def write_rb_lock
shared_helpers = File.read(File.expand_path("../shared_helpers.rb", __FILE__))
template = File.read(File.expand_path("../templates/environment.erb", __FILE__))
erb = ERB.new(template, nil, '-')
FileUtils.mkdir_p(rb_lock_file.dirname)
File.open(rb_lock_file, 'w') do |f|
f.puts erb.result(binding)
end
end
</ruby>
This will write out to _.bundler/environment.rb_ the state of the current environment.
Now a quick refresher. Bundler is still evaulating the code for the +require+ in _lib/bundler.rb_, and the +groups+ variable here is an +Array+ containing two elements: +:default+ and the current Rails environment: +development+:
<ruby>
def require(*groups)
gemfile = default_gemfile
load(gemfile).require(*groups)
end
</ruby>
The second +require+ method here:
<ruby>
load(gemfile).require(*groups)
</ruby>
Is defined on _bundler/runtime.rb_:
<ruby>
def require(*groups)
groups.map! { |g| g.to_sym }
groups = [:default] if groups.empty?
autorequires = autorequires_for_groups(*groups)
groups.each do |group|
(autorequires[group] || [[]]).each do |path, explicit|
if explicit
Kernel.require(path)
else
begin
Kernel.require(path)
rescue LoadError
end
end
end
end
super
end
</ruby>
This method does TODO: Describe what magic this undertakes.
The first method to be called here is +autorequires_for_groups+:
<ruby>
def autorequires_for_groups(*groups)
groups.map! { |g| g.to_sym }
autorequires = Hash.new { |h,k| h[k] = [] }
ordered_deps = []
specs_for(*groups).each do |g|
dep = @definition.dependencies.find{|d| d.name == g.name }
ordered_deps << dep if dep && !ordered_deps.include?(dep)
end
</ruby>
This +called_from+ setting looks a little overwhelming to begin with, but the short end of it is that it returns the route to your application's config directory, something like: _/home/you/yourapp/config_. After +called_from+ has been set, +super+ is again called and this means the +Rails::Railtie#inherited+ method.
The +specs_for+ method here:
<ruby>
</ruby>
h3. Firing it up!
......@@ -2642,7 +2208,7 @@ The method +find_with_root_flag+ is defined on +Rails::Engine+ (the superclass o
root = File.exist?("#{root_path}/#{flag}") ? root_path : default
raise "Could not find root path for #{self}" unless root
RUBY_PLATFORM =~ /mswin|mingw/ ?
RUBY_PLATFORM =~ /(:?mswin|mingw)/ ?
Pathname.new(root).expand_path : Pathname.new(root).realpath
end
</ruby>
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册