mirror of https://github.com/rails/rails
Initial
git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@4 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
This commit is contained in:
commit
db045dbbf6
|
@ -0,0 +1,19 @@
|
|||
*0.4* (5)
|
||||
|
||||
* Consolidated the server configuration options into Base#server_settings= and expanded that with controls for authentication and more [Marten]
|
||||
NOTE: This is an API change that could potentially break your application if you used the old application form. Please do change!
|
||||
|
||||
* Added Base#deliveries as an accessor for an array of emails sent out through that ActionMailer class when using the :test delivery option. [bitsweat]
|
||||
|
||||
* Added Base#perform_deliveries= which can be set to false to turn off the actual delivery of the email through smtp or sendmail.
|
||||
This is especially useful for functional testing that shouldn't send off real emails, but still trigger delivery_* methods.
|
||||
|
||||
* Added option to specify delivery method with Base#delivery_method=. Default is :smtp and :sendmail is currently the only other option.
|
||||
Sendmail is assumed to be present at "/usr/sbin/sendmail" if that option is used. [Kent Sibilev]
|
||||
|
||||
* Dropped "include TMail" as it added to much baggage into the default namespace (like Version) [Chad Fowler]
|
||||
|
||||
|
||||
*0.3*
|
||||
|
||||
* First release
|
|
@ -0,0 +1,21 @@
|
|||
Copyright (c) 2004 David Heinemeier Hansson
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
|
@ -0,0 +1,102 @@
|
|||
= Action Mailer -- Easy email delivery and testing
|
||||
|
||||
Action Mailer is framework for designing email-service layers. These layers
|
||||
are used to consolidate code for sending out forgotten passwords, welcoming
|
||||
wishes on signup, invoices for billing, and any other use case that requires
|
||||
a written notification to either a person or another system.
|
||||
|
||||
The framework works by setting up all the email details, except the body,
|
||||
in methods on the service layer. Subject, recipients, sender, and timestamp
|
||||
are all set up this way. An example of such a method:
|
||||
|
||||
def signed_up(recipient)
|
||||
@recipients = recipient
|
||||
@subject = "[Signed up] Welcome #{recipient}"
|
||||
@from = "system@loudthinking.com"
|
||||
@sent_on = Time.local(2004, 12, 12)
|
||||
|
||||
@body["recipient"] = recipient
|
||||
end
|
||||
|
||||
The body of the email is created by using an Action View template (regular
|
||||
ERb) that has the content of the @body hash available as instance variables.
|
||||
So the corresponding body template for the method above could look like this:
|
||||
|
||||
Hello there,
|
||||
|
||||
Mr. <%= @recipient %>
|
||||
|
||||
And if the recipient was given as "david@loudthinking.com", the email
|
||||
generated would look like this:
|
||||
|
||||
Date: Sun, 12 Dec 2004 00:00:00 +0100
|
||||
From: system@loudthinking.com
|
||||
To: david@loudthinking.com
|
||||
Subject: [Signed up] Welcome david@loudthinking.com
|
||||
|
||||
Hello there,
|
||||
|
||||
Mr. david@loudthinking.com
|
||||
|
||||
You never actually call the instance methods like signed_up directly. Instead,
|
||||
you call class methods like deliver_* and create_* that are automatically
|
||||
created for each instance method. So if the signed_up method sat on
|
||||
ApplicationMailer, it would look like this:
|
||||
|
||||
ApplicationMailer.create_signed_up("david@loudthinking.com") # => tmail object for testing
|
||||
ApplicationMailer.deliver_signed_up("david@loudthinking.com") # sends the email
|
||||
ApplicationMailer.new.signed_up("david@loudthinking.com") # won't work!
|
||||
|
||||
|
||||
== Dependencies
|
||||
|
||||
Action Mailer requires that the Action Pack is either available to be required immediately
|
||||
or is accessible as a GEM.
|
||||
|
||||
|
||||
== Bundled software
|
||||
|
||||
* tmail 0.10.8 by Minero Aoki released under LGPL
|
||||
Read more on http://i.loveruby.net/en/prog/tmail.html
|
||||
|
||||
* Text::Format 0.63 by Austin Ziegler released under OpenSource
|
||||
Read more on http://www.halostatue.ca/ruby/Text__Format.html
|
||||
|
||||
|
||||
== Download
|
||||
|
||||
The latest version of Action Mailer can be found at
|
||||
|
||||
* http://rubyforge.org/project/showfiles.php?group_id=361
|
||||
|
||||
Documentation can be found at
|
||||
|
||||
* http://actionmailer.rubyonrails.org
|
||||
|
||||
|
||||
== Installation
|
||||
|
||||
You can install Action Mailer with the following command.
|
||||
|
||||
% [sudo] ruby install.rb
|
||||
|
||||
from its distribution directory.
|
||||
|
||||
|
||||
== License
|
||||
|
||||
Action Mailer is released under the MIT license.
|
||||
|
||||
|
||||
== Support
|
||||
|
||||
The Action Mailer homepage is http://actionmailer.rubyonrails.org. You can find
|
||||
the Action Mailer RubyForge page at http://rubyforge.org/projects/actionmailer.
|
||||
And as Jim from Rake says:
|
||||
|
||||
Feel free to submit commits or feature requests. If you send a patch,
|
||||
remember to update the corresponding unit tests. If fact, I prefer
|
||||
new feature to be submitted in the form of new unit tests.
|
||||
|
||||
For other information, feel free to ask on the ruby-talk mailing list (which
|
||||
is mirrored to comp.lang.ruby) or contact mailto:david@loudthinking.com.
|
|
@ -0,0 +1,107 @@
|
|||
require 'rubygems'
|
||||
require 'rake'
|
||||
require 'rake/testtask'
|
||||
require 'rake/rdoctask'
|
||||
require 'rake/packagetask'
|
||||
require 'rake/gempackagetask'
|
||||
require 'rake/contrib/rubyforgepublisher'
|
||||
|
||||
PKG_BUILD = ENV['PKG_BUILD'] ? '.' + ENV['PKG_BUILD'] : ''
|
||||
PKG_NAME = 'actionmailer'
|
||||
PKG_VERSION = '0.4.0' + PKG_BUILD
|
||||
PKG_FILE_NAME = "#{PKG_NAME}-#{PKG_VERSION}"
|
||||
|
||||
desc "Default Task"
|
||||
task :default => [ :test ]
|
||||
|
||||
# Run the unit tests
|
||||
|
||||
Rake::TestTask.new { |t|
|
||||
t.libs << "test"
|
||||
t.pattern = 'test/*_test.rb'
|
||||
t.verbose = true
|
||||
}
|
||||
|
||||
|
||||
# Genereate the RDoc documentation
|
||||
|
||||
Rake::RDocTask.new { |rdoc|
|
||||
rdoc.rdoc_dir = 'doc'
|
||||
rdoc.title = "Action Mailer -- Easy email delivery and testing"
|
||||
rdoc.options << '--line-numbers --inline-source --main README'
|
||||
rdoc.rdoc_files.include('README', 'CHANGELOG')
|
||||
rdoc.rdoc_files.include('lib/action_mailer.rb')
|
||||
rdoc.rdoc_files.include('lib/action_mailer/*.rb')
|
||||
}
|
||||
|
||||
|
||||
# Create compressed packages
|
||||
|
||||
|
||||
spec = Gem::Specification.new do |s|
|
||||
s.platform = Gem::Platform::RUBY
|
||||
s.name = PKG_NAME
|
||||
s.summary = "Service layer for easy email delivery and testing."
|
||||
s.description = %q{Makes it trivial to test and deliver emails sent from a single service layer.}
|
||||
s.version = PKG_VERSION
|
||||
|
||||
s.author = "David Heinemeier Hansson"
|
||||
s.email = "david@loudthinking.com"
|
||||
s.rubyforge_project = "actionmailer"
|
||||
s.homepage = "http://actionmailer.rubyonrails.org"
|
||||
|
||||
s.add_dependency('actionpack', '>= 0.9.5')
|
||||
|
||||
s.has_rdoc = true
|
||||
s.requirements << 'none'
|
||||
s.require_path = 'lib'
|
||||
s.autorequire = 'action_mailer'
|
||||
|
||||
s.files = [ "rakefile", "install.rb", "README", "CHANGELOG", "MIT-LICENSE" ]
|
||||
s.files = s.files + Dir.glob( "lib/**/*" ).delete_if { |item| item.include?( "CVS" ) }
|
||||
s.files = s.files + Dir.glob( "test/**/*" ).delete_if { |item| item.include?( "CVS" ) }
|
||||
end
|
||||
|
||||
Rake::GemPackageTask.new(spec) do |p|
|
||||
p.gem_spec = spec
|
||||
p.need_tar = true
|
||||
p.need_zip = true
|
||||
end
|
||||
|
||||
|
||||
# Publish beta gem
|
||||
desc "Publish the API documentation"
|
||||
task :pgem => [:package] do
|
||||
Rake::SshFilePublisher.new("davidhh@one.textdrive.com", "domains/rubyonrails.org/gems/gems", "pkg", "#{PKG_FILE_NAME}.gem").upload
|
||||
end
|
||||
|
||||
# Publish documentation
|
||||
desc "Publish the API documentation"
|
||||
task :pdoc => [:rdoc] do
|
||||
Rake::SshDirPublisher.new("davidhh@one.textdrive.com", "domains/rubyonrails.org/am", "doc").upload
|
||||
end
|
||||
|
||||
desc "Publish to RubyForge"
|
||||
task :rubyforge do
|
||||
Rake::RubyForgePublisher.new('actionmailer', 'webster132').upload
|
||||
end
|
||||
|
||||
|
||||
desc "Count lines in the main rake file"
|
||||
task :lines do
|
||||
lines = 0
|
||||
codelines = 0
|
||||
Dir.foreach("lib/action_mailer") { |file_name|
|
||||
next unless file_name =~ /.*rb/
|
||||
|
||||
f = File.open("lib/action_mailer/" + file_name)
|
||||
|
||||
while line = f.gets
|
||||
lines += 1
|
||||
next if line =~ /^\s*$/
|
||||
next if line =~ /^\s*#/
|
||||
codelines += 1
|
||||
end
|
||||
}
|
||||
puts "Lines #{lines}, LOC #{codelines}"
|
||||
end
|
|
@ -0,0 +1,61 @@
|
|||
require 'rbconfig'
|
||||
require 'find'
|
||||
require 'ftools'
|
||||
|
||||
include Config
|
||||
|
||||
# this was adapted from rdoc's install.rb by ways of Log4r
|
||||
|
||||
$sitedir = CONFIG["sitelibdir"]
|
||||
unless $sitedir
|
||||
version = CONFIG["MAJOR"] + "." + CONFIG["MINOR"]
|
||||
$libdir = File.join(CONFIG["libdir"], "ruby", version)
|
||||
$sitedir = $:.find {|x| x =~ /site_ruby/ }
|
||||
if !$sitedir
|
||||
$sitedir = File.join($libdir, "site_ruby")
|
||||
elsif $sitedir !~ Regexp.quote(version)
|
||||
$sitedir = File.join($sitedir, version)
|
||||
end
|
||||
end
|
||||
|
||||
makedirs = %w{ action_mailer/vendor action_mailer/vendor/text action_mailer/vendor/tmail }
|
||||
makedirs.each {|f| File::makedirs(File.join($sitedir, *f.split(/\//)))}
|
||||
|
||||
# deprecated files that should be removed
|
||||
# deprecated = %w{ }
|
||||
|
||||
# files to install in library path
|
||||
files = %w-
|
||||
action_mailer.rb
|
||||
action_mailer/base.rb
|
||||
action_mailer/mail_helper.rb
|
||||
action_mailer/vendor/text/format.rb
|
||||
action_mailer/vendor/tmail.rb
|
||||
action_mailer/vendor/tmail/address.rb
|
||||
action_mailer/vendor/tmail/base64.rb
|
||||
action_mailer/vendor/tmail/config.rb
|
||||
action_mailer/vendor/tmail/encode.rb
|
||||
action_mailer/vendor/tmail/facade.rb
|
||||
action_mailer/vendor/tmail/header.rb
|
||||
action_mailer/vendor/tmail/info.rb
|
||||
action_mailer/vendor/tmail/loader.rb
|
||||
action_mailer/vendor/tmail/mail.rb
|
||||
action_mailer/vendor/tmail/mailbox.rb
|
||||
action_mailer/vendor/tmail/mbox.rb
|
||||
action_mailer/vendor/tmail/net.rb
|
||||
action_mailer/vendor/tmail/obsolete.rb
|
||||
action_mailer/vendor/tmail/parser.rb
|
||||
action_mailer/vendor/tmail/port.rb
|
||||
action_mailer/vendor/tmail/scanner.rb
|
||||
action_mailer/vendor/tmail/scanner_r.rb
|
||||
action_mailer/vendor/tmail/stringio.rb
|
||||
action_mailer/vendor/tmail/tmail.rb
|
||||
action_mailer/vendor/tmail/utils.rb
|
||||
-
|
||||
|
||||
# the acual gruntwork
|
||||
Dir.chdir("lib")
|
||||
# File::safe_unlink *deprecated.collect{|f| File.join($sitedir, f.split(/\//))}
|
||||
files.each {|f|
|
||||
File::install(f, File.join($sitedir, *f.split(/\//)), 0644, true)
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
#--
|
||||
# Copyright (c) 2004 David Heinemeier Hansson
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining
|
||||
# a copy of this software and associated documentation files (the
|
||||
# "Software"), to deal in the Software without restriction, including
|
||||
# without limitation the rights to use, copy, modify, merge, publish,
|
||||
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||
# permit persons to whom the Software is furnished to do so, subject to
|
||||
# the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
#++
|
||||
|
||||
begin
|
||||
require 'action_controller'
|
||||
rescue LoadError
|
||||
# Action Pack is not already available, try RubyGems
|
||||
require 'rubygems'
|
||||
require_gem 'actionpack', '>= 0.9.0'
|
||||
end
|
||||
|
||||
$:.unshift(File.dirname(__FILE__) + "/action_mailer/vendor/")
|
||||
|
||||
require 'action_mailer/base'
|
||||
require 'action_mailer/mail_helper'
|
||||
require 'action_mailer/vendor/tmail'
|
||||
require 'net/smtp'
|
||||
|
||||
ActionView::Base.class_eval { include MailHelper }
|
||||
|
||||
old_verbose, $VERBOSE = $VERBOSE, nil
|
||||
TMail::Encoder.const_set("MAX_LINE_LEN", 200)
|
||||
$VERBOSE = old_verbose
|
|
@ -0,0 +1,152 @@
|
|||
module ActionMailer #:nodoc:
|
||||
# Usage:
|
||||
#
|
||||
# class ApplicationMailer < ActionMailer::Base
|
||||
# def post_notification(recipients, post)
|
||||
# @recipients = recipients
|
||||
# @subject = "[#{post.account.name} #{post.title}]"
|
||||
# @body["post"] = post
|
||||
# @from = post.author.email_address_with_name
|
||||
# end
|
||||
#
|
||||
# def comment_notification(recipient, comment)
|
||||
# @recipients = recipient.email_address_with_name
|
||||
# @subject = "[#{comment.post.project.client.firm.account.name}]" +
|
||||
# " Re: #{comment.post.title}"
|
||||
# @body["comment"] = comment
|
||||
# @from = comment.author.email_address_with_name
|
||||
# @sent_on = comment.posted_on
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# # After this post_notification will look for "templates/application_mailer/post_notification.rhtml"
|
||||
# ApplicationMailer.template_root = "templates"
|
||||
#
|
||||
# ApplicationMailer.create_comment_notification(david, hello_world) # => a tmail object
|
||||
# ApplicationMailer.deliver_comment_notification(david, hello_world) # sends the email
|
||||
class Base
|
||||
private_class_method :new
|
||||
|
||||
# Template root determines the base from which template references will be made.
|
||||
cattr_accessor :template_root
|
||||
|
||||
# The logger is used for generating information on the mailing run if available.
|
||||
# Can be set to nil for no logging. Compatible with both Ruby's own Logger and Log4r loggers.
|
||||
cattr_accessor :logger
|
||||
|
||||
# Allows detailed configuration of the server:
|
||||
# * <tt>:address</tt> Allows you to use a remote mail server. Just change it away from it's default "localhost" setting.
|
||||
# * <tt>:port</tt> On the off change that your mail server doesn't run on port 25, you can change it.
|
||||
# * <tt>:domain</tt> If you need to specify a HELO domain, you can do it here.
|
||||
# * <tt>:user_name</tt> If your mail server requires authentication, set the username and password in these two settings.
|
||||
# * <tt>:password</tt> If your mail server requires authentication, set the username and password in these two settings.
|
||||
# * <tt>:authentication</tt> 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
|
||||
@@server_settings = {
|
||||
:address => "localhost",
|
||||
:port => 25,
|
||||
:domain => 'localhost.localdomain',
|
||||
:user_name => nil,
|
||||
:password => nil,
|
||||
:authentication => nil
|
||||
}
|
||||
cattr_accessor :server_settings
|
||||
|
||||
|
||||
# Whether or not errors should be raised if the email fails to be delivered
|
||||
@@raise_delivery_errors = true
|
||||
cattr_accessor :raise_delivery_errors
|
||||
|
||||
# Defines a delivery method. Possible values are :smtp (default), :sendmail, and :test.
|
||||
# Sendmail is assumed to be present at "/usr/sbin/sendmail".
|
||||
@@delivery_method = :smtp
|
||||
cattr_accessor :delivery_method
|
||||
|
||||
# Determines whether deliver_* methods are actually carried out. By default they are,
|
||||
# but this can be turned off to help functional testing.
|
||||
@@perform_deliveries = true
|
||||
cattr_accessor :perform_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.
|
||||
@@deliveries = []
|
||||
cattr_accessor :deliveries
|
||||
|
||||
attr_accessor :recipients, :subject, :body, :from, :sent_on, :bcc, :cc
|
||||
|
||||
class << self
|
||||
def method_missing(method_symbol, *parameters)#:nodoc:
|
||||
case method_symbol.id2name
|
||||
when /^create_([_a-z]*)/
|
||||
create_from_action($1, *parameters)
|
||||
when /^deliver_([_a-z]*)/
|
||||
begin
|
||||
deliver(send("create_" + $1, *parameters))
|
||||
rescue Object => e
|
||||
raise e if raise_delivery_errors
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def mail(to, subject, body, from, timestamp = nil) #:nodoc:
|
||||
deliver(create(to, subject, body, from, timestamp))
|
||||
end
|
||||
|
||||
def create(to, subject, body, from, timestamp = nil) #:nodoc:
|
||||
m = TMail::Mail.new
|
||||
m.to, m.subject, m.body, m.from = to, subject, body, from
|
||||
m.date = timestamp.respond_to?("to_time") ? timestamp.to_time : (timestamp || Time.now)
|
||||
return m
|
||||
end
|
||||
|
||||
def deliver(mail) #:nodoc:
|
||||
logger.info "Sent mail:\n #{mail.encoded}" unless logger.nil?
|
||||
send("perform_delivery_#{delivery_method}", mail) if perform_deliveries
|
||||
end
|
||||
|
||||
private
|
||||
def perform_delivery_smtp(mail)
|
||||
Net::SMTP.start(server_settings[:address], server_settings[:port], server_settings[:domain],
|
||||
server_settings[:user_name], server_settings[:password], server_settings[:authentication]) do |smtp|
|
||||
smtp.sendmail(mail.encoded, mail.from_address, mail.destinations)
|
||||
end
|
||||
end
|
||||
|
||||
def perform_delivery_sendmail(mail)
|
||||
IO.popen("/usr/sbin/sendmail -i -t","w+") do |sm|
|
||||
sm.print(mail.encoded)
|
||||
sm.flush
|
||||
end
|
||||
end
|
||||
|
||||
def perform_delivery_test(mail)
|
||||
deliveries << mail
|
||||
end
|
||||
|
||||
def create_from_action(method_name, *parameters)
|
||||
mailer = new
|
||||
mailer.body = {}
|
||||
mailer.send(method_name, *parameters)
|
||||
|
||||
if String === mailer.body
|
||||
mail = create(mailer.recipients, mailer.subject, mailer.body, mailer.from, mailer.sent_on)
|
||||
else
|
||||
mail = create(mailer.recipients, mailer.subject, render_body(mailer, method_name), mailer.from, mailer.sent_on)
|
||||
end
|
||||
|
||||
mail.bcc = @bcc if @bcc
|
||||
mail.cc = @cc if @cc
|
||||
|
||||
return mail
|
||||
end
|
||||
|
||||
def render_body(mailer, method_name)
|
||||
ActionView::Base.new(template_path, mailer.body).render_file(method_name)
|
||||
end
|
||||
|
||||
def template_path
|
||||
template_root + "/" + Inflector.underscore(self.to_s)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,17 @@
|
|||
require 'action_mailer/vendor/text/format'
|
||||
|
||||
module MailHelper#:nodoc:
|
||||
def block_format(text)
|
||||
formatted = text.split(/\n\r\n/).collect { |paragraph|
|
||||
Text::Format.new(
|
||||
:columns => 72, :first_indent => 2, :body_indent => 2, :text => paragraph
|
||||
).format
|
||||
}.join("\n")
|
||||
|
||||
# Make list points stand on their own line
|
||||
formatted.gsub!(/[ ]*([*]+) ([^*]*)/) { |s| " #{$1} #{$2.strip}\n" }
|
||||
formatted.gsub!(/[ ]*([#]+) ([^#]*)/) { |s| " #{$1} #{$2.strip}\n" }
|
||||
|
||||
formatted
|
||||
end
|
||||
end
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,4 @@
|
|||
require 'tmail/info'
|
||||
require 'tmail/mail'
|
||||
require 'tmail/mailbox'
|
||||
require 'tmail/obsolete'
|
|
@ -0,0 +1,223 @@
|
|||
#
|
||||
# address.rb
|
||||
#
|
||||
# Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
|
||||
#
|
||||
# This program is free software.
|
||||
# You can distribute/modify this program under the terms of
|
||||
# the GNU Lesser General Public License version 2 or later.
|
||||
#
|
||||
|
||||
require 'tmail/encode'
|
||||
require 'tmail/parser'
|
||||
|
||||
|
||||
module TMail
|
||||
|
||||
class Address
|
||||
|
||||
include TextUtils
|
||||
|
||||
def Address.parse( str )
|
||||
Parser.parse :ADDRESS, str
|
||||
end
|
||||
|
||||
def address_group?
|
||||
false
|
||||
end
|
||||
|
||||
def initialize( local, domain )
|
||||
if domain
|
||||
domain.each do |s|
|
||||
raise SyntaxError, 'empty word in domain' if s.empty?
|
||||
end
|
||||
end
|
||||
@local = local
|
||||
@domain = domain
|
||||
@name = nil
|
||||
@routes = []
|
||||
end
|
||||
|
||||
attr_reader :name
|
||||
|
||||
def name=( str )
|
||||
@name = str
|
||||
@name = nil if str and str.empty?
|
||||
end
|
||||
|
||||
alias phrase name
|
||||
alias phrase= name=
|
||||
|
||||
attr_reader :routes
|
||||
|
||||
def inspect
|
||||
"#<#{self.class} #{address()}>"
|
||||
end
|
||||
|
||||
def local
|
||||
return nil unless @local
|
||||
return '""' if @local.size == 1 and @local[0].empty?
|
||||
@local.map {|i| quote_atom(i) }.join('.')
|
||||
end
|
||||
|
||||
def domain
|
||||
return nil unless @domain
|
||||
join_domain(@domain)
|
||||
end
|
||||
|
||||
def spec
|
||||
s = self.local
|
||||
d = self.domain
|
||||
if s and d
|
||||
s + '@' + d
|
||||
else
|
||||
s
|
||||
end
|
||||
end
|
||||
|
||||
alias address spec
|
||||
|
||||
|
||||
def ==( other )
|
||||
other.respond_to? :spec and self.spec == other.spec
|
||||
end
|
||||
|
||||
alias eql? ==
|
||||
|
||||
def hash
|
||||
@local.hash ^ @domain.hash
|
||||
end
|
||||
|
||||
def dup
|
||||
obj = self.class.new(@local.dup, @domain.dup)
|
||||
obj.name = @name.dup if @name
|
||||
obj.routes.replace @routes
|
||||
obj
|
||||
end
|
||||
|
||||
include StrategyInterface
|
||||
|
||||
def accept( strategy, dummy1 = nil, dummy2 = nil )
|
||||
unless @local
|
||||
strategy.meta '<>' # empty return-path
|
||||
return
|
||||
end
|
||||
|
||||
spec_p = (not @name and @routes.empty?)
|
||||
if @name
|
||||
strategy.phrase @name
|
||||
strategy.space
|
||||
end
|
||||
tmp = spec_p ? '' : '<'
|
||||
unless @routes.empty?
|
||||
tmp << @routes.map {|i| '@' + i }.join(',') << ':'
|
||||
end
|
||||
tmp << self.spec
|
||||
tmp << '>' unless spec_p
|
||||
strategy.meta tmp
|
||||
strategy.lwsp ''
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
class AddressGroup
|
||||
|
||||
include Enumerable
|
||||
|
||||
def address_group?
|
||||
true
|
||||
end
|
||||
|
||||
def initialize( name, addrs )
|
||||
@name = name
|
||||
@addresses = addrs
|
||||
end
|
||||
|
||||
attr_reader :name
|
||||
|
||||
def ==( other )
|
||||
other.respond_to? :to_a and @addresses == other.to_a
|
||||
end
|
||||
|
||||
alias eql? ==
|
||||
|
||||
def hash
|
||||
map {|i| i.hash }.hash
|
||||
end
|
||||
|
||||
def []( idx )
|
||||
@addresses[idx]
|
||||
end
|
||||
|
||||
def size
|
||||
@addresses.size
|
||||
end
|
||||
|
||||
def empty?
|
||||
@addresses.empty?
|
||||
end
|
||||
|
||||
def each( &block )
|
||||
@addresses.each(&block)
|
||||
end
|
||||
|
||||
def to_a
|
||||
@addresses.dup
|
||||
end
|
||||
|
||||
alias to_ary to_a
|
||||
|
||||
def include?( a )
|
||||
@addresses.include? a
|
||||
end
|
||||
|
||||
def flatten
|
||||
set = []
|
||||
@addresses.each do |a|
|
||||
if a.respond_to? :flatten
|
||||
set.concat a.flatten
|
||||
else
|
||||
set.push a
|
||||
end
|
||||
end
|
||||
set
|
||||
end
|
||||
|
||||
def each_address( &block )
|
||||
flatten.each(&block)
|
||||
end
|
||||
|
||||
def add( a )
|
||||
@addresses.push a
|
||||
end
|
||||
|
||||
alias push add
|
||||
|
||||
def delete( a )
|
||||
@addresses.delete a
|
||||
end
|
||||
|
||||
include StrategyInterface
|
||||
|
||||
def accept( strategy, dummy1 = nil, dummy2 = nil )
|
||||
strategy.phrase @name
|
||||
strategy.meta ':'
|
||||
strategy.space
|
||||
first = true
|
||||
each do |mbox|
|
||||
if first
|
||||
first = false
|
||||
else
|
||||
strategy.meta ','
|
||||
end
|
||||
strategy.space
|
||||
mbox.accept strategy
|
||||
end
|
||||
strategy.meta ';'
|
||||
strategy.lwsp ''
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end # module TMail
|
|
@ -0,0 +1,52 @@
|
|||
#
|
||||
# base64.rb
|
||||
#
|
||||
# Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
|
||||
#
|
||||
# This program is free software.
|
||||
# You can distribute/modify this program under the terms of
|
||||
# the GNU Lesser General Public License version 2 or later.
|
||||
#
|
||||
|
||||
module TMail
|
||||
|
||||
module Base64
|
||||
|
||||
module_function
|
||||
|
||||
def rb_folding_encode( str, eol = "\n", limit = 60 )
|
||||
[str].pack('m')
|
||||
end
|
||||
|
||||
def rb_encode( str )
|
||||
[str].pack('m').tr( "\r\n", '' )
|
||||
end
|
||||
|
||||
def rb_decode( str, strict = false )
|
||||
str.unpack('m')
|
||||
end
|
||||
|
||||
begin
|
||||
require 'tmail/base64.so'
|
||||
alias folding_encode c_folding_encode
|
||||
alias encode c_encode
|
||||
alias decode c_decode
|
||||
class << self
|
||||
alias folding_encode c_folding_encode
|
||||
alias encode c_encode
|
||||
alias decode c_decode
|
||||
end
|
||||
rescue LoadError
|
||||
alias folding_encode rb_folding_encode
|
||||
alias encode rb_encode
|
||||
alias decode rb_decode
|
||||
class << self
|
||||
alias folding_encode rb_folding_encode
|
||||
alias encode rb_encode
|
||||
alias decode rb_decode
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
|
@ -0,0 +1,50 @@
|
|||
#
|
||||
# config.rb
|
||||
#
|
||||
# Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
|
||||
#
|
||||
# This program is free software.
|
||||
# You can distribute/modify this program under the terms of
|
||||
# the GNU Lesser General Public License version 2 or later.
|
||||
#
|
||||
|
||||
module TMail
|
||||
|
||||
class Config
|
||||
|
||||
def initialize( strict )
|
||||
@strict_parse = strict
|
||||
@strict_base64decode = strict
|
||||
end
|
||||
|
||||
def strict_parse?
|
||||
@strict_parse
|
||||
end
|
||||
|
||||
attr_writer :strict_parse
|
||||
|
||||
def strict_base64decode?
|
||||
@strict_base64decode
|
||||
end
|
||||
|
||||
attr_writer :strict_base64decode
|
||||
|
||||
def new_body_port( mail )
|
||||
StringPort.new
|
||||
end
|
||||
|
||||
alias new_preamble_port new_body_port
|
||||
alias new_part_port new_body_port
|
||||
|
||||
end
|
||||
|
||||
DEFAULT_CONFIG = Config.new(false)
|
||||
DEFAULT_STRICT_CONFIG = Config.new(true)
|
||||
|
||||
def Config.to_config( arg )
|
||||
return DEFAULT_STRICT_CONFIG if arg == true
|
||||
return DEFAULT_CONFIG if arg == false
|
||||
arg or DEFAULT_CONFIG
|
||||
end
|
||||
|
||||
end
|
|
@ -0,0 +1,447 @@
|
|||
#
|
||||
# encode.rb
|
||||
#
|
||||
# Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
|
||||
#
|
||||
# This program is free software.
|
||||
# You can distribute/modify this program under the terms of
|
||||
# the GNU Lesser General Public License version 2 or later.
|
||||
#
|
||||
|
||||
require 'nkf'
|
||||
require 'tmail/base64.rb'
|
||||
require 'tmail/stringio'
|
||||
require 'tmail/utils'
|
||||
|
||||
|
||||
module TMail
|
||||
|
||||
module StrategyInterface
|
||||
|
||||
def create_dest( obj )
|
||||
case obj
|
||||
when nil
|
||||
StringOutput.new
|
||||
when String
|
||||
StringOutput.new(obj)
|
||||
when IO, StringOutput
|
||||
obj
|
||||
else
|
||||
raise TypeError, 'cannot handle this type of object for dest'
|
||||
end
|
||||
end
|
||||
module_function :create_dest
|
||||
|
||||
def encoded( eol = "\r\n", charset = 'j', dest = nil )
|
||||
accept_strategy Encoder, eol, charset, dest
|
||||
end
|
||||
|
||||
def decoded( eol = "\n", charset = 'e', dest = nil )
|
||||
accept_strategy Decoder, eol, charset, dest
|
||||
end
|
||||
|
||||
alias to_s decoded
|
||||
|
||||
def accept_strategy( klass, eol, charset, dest = nil )
|
||||
dest ||= ''
|
||||
accept klass.new(create_dest(dest), charset, eol)
|
||||
dest
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
###
|
||||
### MIME B encoding decoder
|
||||
###
|
||||
|
||||
class Decoder
|
||||
|
||||
include TextUtils
|
||||
|
||||
encoded = '=\?(?:iso-2022-jp|euc-jp|shift_jis)\?[QB]\?[a-z0-9+/=]+\?='
|
||||
ENCODED_WORDS = /#{encoded}(?:\s+#{encoded})*/i
|
||||
|
||||
OUTPUT_ENCODING = {
|
||||
'EUC' => 'e',
|
||||
'SJIS' => 's',
|
||||
}
|
||||
|
||||
def self.decode( str, encoding = nil )
|
||||
encoding ||= (OUTPUT_ENCODING[$KCODE] || 'j')
|
||||
opt = '-m' + encoding
|
||||
str.gsub(ENCODED_WORDS) {|s| NKF.nkf(opt, s) }
|
||||
end
|
||||
|
||||
def initialize( dest, encoding = nil, eol = "\n" )
|
||||
@f = StrategyInterface.create_dest(dest)
|
||||
@encoding = (/\A[ejs]/ === encoding) ? encoding[0,1] : nil
|
||||
@eol = eol
|
||||
end
|
||||
|
||||
def decode( str )
|
||||
self.class.decode(str, @encoding)
|
||||
end
|
||||
private :decode
|
||||
|
||||
def terminate
|
||||
end
|
||||
|
||||
def header_line( str )
|
||||
@f << decode(str)
|
||||
end
|
||||
|
||||
def header_name( nm )
|
||||
@f << nm << ': '
|
||||
end
|
||||
|
||||
def header_body( str )
|
||||
@f << decode(str)
|
||||
end
|
||||
|
||||
def space
|
||||
@f << ' '
|
||||
end
|
||||
|
||||
alias spc space
|
||||
|
||||
def lwsp( str )
|
||||
@f << str
|
||||
end
|
||||
|
||||
def meta( str )
|
||||
@f << str
|
||||
end
|
||||
|
||||
def text( str )
|
||||
@f << decode(str)
|
||||
end
|
||||
|
||||
def phrase( str )
|
||||
@f << quote_phrase(decode(str))
|
||||
end
|
||||
|
||||
def kv_pair( k, v )
|
||||
@f << k << '=' << v
|
||||
end
|
||||
|
||||
def puts( str = nil )
|
||||
@f << str if str
|
||||
@f << @eol
|
||||
end
|
||||
|
||||
def write( str )
|
||||
@f << str
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
###
|
||||
### MIME B-encoding encoder
|
||||
###
|
||||
|
||||
#
|
||||
# FIXME: This class can handle only (euc-jp/shift_jis -> iso-2022-jp).
|
||||
#
|
||||
class Encoder
|
||||
|
||||
include TextUtils
|
||||
|
||||
BENCODE_DEBUG = false unless defined?(BENCODE_DEBUG)
|
||||
|
||||
def Encoder.encode( str )
|
||||
e = new()
|
||||
e.header_body str
|
||||
e.terminate
|
||||
e.dest.string
|
||||
end
|
||||
|
||||
SPACER = "\t"
|
||||
MAX_LINE_LEN = 70
|
||||
|
||||
OPTIONS = {
|
||||
'EUC' => '-Ej -m0',
|
||||
'SJIS' => '-Sj -m0',
|
||||
'UTF8' => nil, # FIXME
|
||||
'NONE' => nil
|
||||
}
|
||||
|
||||
def initialize( dest = nil, encoding = nil, eol = "\r\n", limit = nil )
|
||||
@f = StrategyInterface.create_dest(dest)
|
||||
@opt = OPTIONS[$KCODE]
|
||||
@eol = eol
|
||||
reset
|
||||
end
|
||||
|
||||
def normalize_encoding( str )
|
||||
if @opt
|
||||
then NKF.nkf(@opt, str)
|
||||
else str
|
||||
end
|
||||
end
|
||||
|
||||
def reset
|
||||
@text = ''
|
||||
@lwsp = ''
|
||||
@curlen = 0
|
||||
end
|
||||
|
||||
def terminate
|
||||
add_lwsp ''
|
||||
reset
|
||||
end
|
||||
|
||||
def dest
|
||||
@f
|
||||
end
|
||||
|
||||
def puts( str = nil )
|
||||
@f << str if str
|
||||
@f << @eol
|
||||
end
|
||||
|
||||
def write( str )
|
||||
@f << str
|
||||
end
|
||||
|
||||
#
|
||||
# add
|
||||
#
|
||||
|
||||
def header_line( line )
|
||||
scanadd line
|
||||
end
|
||||
|
||||
def header_name( name )
|
||||
add_text name.split(/-/).map {|i| i.capitalize }.join('-')
|
||||
add_text ':'
|
||||
add_lwsp ' '
|
||||
end
|
||||
|
||||
def header_body( str )
|
||||
scanadd normalize_encoding(str)
|
||||
end
|
||||
|
||||
def space
|
||||
add_lwsp ' '
|
||||
end
|
||||
|
||||
alias spc space
|
||||
|
||||
def lwsp( str )
|
||||
add_lwsp str.sub(/[\r\n]+[^\r\n]*\z/, '')
|
||||
end
|
||||
|
||||
def meta( str )
|
||||
add_text str
|
||||
end
|
||||
|
||||
def text( str )
|
||||
scanadd normalize_encoding(str)
|
||||
end
|
||||
|
||||
def phrase( str )
|
||||
str = normalize_encoding(str)
|
||||
if CONTROL_CHAR === str
|
||||
scanadd str
|
||||
else
|
||||
add_text quote_phrase(str)
|
||||
end
|
||||
end
|
||||
|
||||
# FIXME: implement line folding
|
||||
#
|
||||
def kv_pair( k, v )
|
||||
v = normalize_encoding(v)
|
||||
if token_safe?(v)
|
||||
add_text k + '=' + v
|
||||
elsif not CONTROL_CHAR === v
|
||||
add_text k + '=' + quote_token(v)
|
||||
else
|
||||
# apply RFC2231 encoding
|
||||
kv = k + '*=' + "iso-2022-jp'ja'" + encode_value(v)
|
||||
add_text kv
|
||||
end
|
||||
end
|
||||
|
||||
def encode_value( str )
|
||||
str.gsub(TOKEN_UNSAFE) {|s| '%%%02x' % s[0] }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def scanadd( str, force = false )
|
||||
types = ''
|
||||
strs = []
|
||||
|
||||
until str.empty?
|
||||
if m = /\A[^\e\t\r\n ]+/.match(str)
|
||||
types << (force ? 'j' : 'a')
|
||||
strs.push m[0]
|
||||
|
||||
elsif m = /\A[\t\r\n ]+/.match(str)
|
||||
types << 's'
|
||||
strs.push m[0]
|
||||
|
||||
elsif m = /\A\e../.match(str)
|
||||
esc = m[0]
|
||||
str = m.post_match
|
||||
if esc != "\e(B" and m = /\A[^\e]+/.match(str)
|
||||
types << 'j'
|
||||
strs.push m[0]
|
||||
end
|
||||
|
||||
else
|
||||
raise 'TMail FATAL: encoder scan fail'
|
||||
end
|
||||
str = m.post_match
|
||||
end
|
||||
|
||||
do_encode types, strs
|
||||
end
|
||||
|
||||
def do_encode( types, strs )
|
||||
#
|
||||
# result : (A|E)(S(A|E))*
|
||||
# E : W(SW)*
|
||||
# W : (J|A)+ but must contain J # (J|A)*J(J|A)*
|
||||
# A : <<A character string not to be encoded>>
|
||||
# J : <<A character string to be encoded>>
|
||||
# S : <<LWSP>>
|
||||
#
|
||||
# An encoding unit is `E'.
|
||||
# Input (parameter `types') is (J|A)(J|A|S)*(J|A)
|
||||
#
|
||||
if BENCODE_DEBUG
|
||||
puts
|
||||
puts '-- do_encode ------------'
|
||||
puts types.split(//).join(' ')
|
||||
p strs
|
||||
end
|
||||
|
||||
e = /[ja]*j[ja]*(?:s[ja]*j[ja]*)*/
|
||||
|
||||
while m = e.match(types)
|
||||
pre = m.pre_match
|
||||
concat_A_S pre, strs[0, pre.size] unless pre.empty?
|
||||
concat_E m[0], strs[m.begin(0) ... m.end(0)]
|
||||
types = m.post_match
|
||||
strs.slice! 0, m.end(0)
|
||||
end
|
||||
concat_A_S types, strs
|
||||
end
|
||||
|
||||
def concat_A_S( types, strs )
|
||||
i = 0
|
||||
types.each_byte do |t|
|
||||
case t
|
||||
when ?a then add_text strs[i]
|
||||
when ?s then add_lwsp strs[i]
|
||||
else
|
||||
raise "TMail FATAL: unknown flag: #{t.chr}"
|
||||
end
|
||||
i += 1
|
||||
end
|
||||
end
|
||||
|
||||
METHOD_ID = {
|
||||
?j => :extract_J,
|
||||
?e => :extract_E,
|
||||
?a => :extract_A,
|
||||
?s => :extract_S
|
||||
}
|
||||
|
||||
def concat_E( types, strs )
|
||||
if BENCODE_DEBUG
|
||||
puts '---- concat_E'
|
||||
puts "types=#{types.split(//).join(' ')}"
|
||||
puts "strs =#{strs.inspect}"
|
||||
end
|
||||
|
||||
flush() unless @text.empty?
|
||||
|
||||
chunk = ''
|
||||
strs.each_with_index do |s,i|
|
||||
mid = METHOD_ID[types[i]]
|
||||
until s.empty?
|
||||
unless c = __send__(mid, chunk.size, s)
|
||||
add_with_encode chunk unless chunk.empty?
|
||||
flush
|
||||
chunk = ''
|
||||
fold
|
||||
c = __send__(mid, 0, s)
|
||||
raise 'TMail FATAL: extract fail' unless c
|
||||
end
|
||||
chunk << c
|
||||
end
|
||||
end
|
||||
add_with_encode chunk unless chunk.empty?
|
||||
end
|
||||
|
||||
def extract_J( chunksize, str )
|
||||
size = max_bytes(chunksize, str.size) - 6
|
||||
size = (size % 2 == 0) ? (size) : (size - 1)
|
||||
return nil if size <= 0
|
||||
"\e$B#{str.slice!(0, size)}\e(B"
|
||||
end
|
||||
|
||||
def extract_A( chunksize, str )
|
||||
size = max_bytes(chunksize, str.size)
|
||||
return nil if size <= 0
|
||||
str.slice!(0, size)
|
||||
end
|
||||
|
||||
alias extract_S extract_A
|
||||
|
||||
def max_bytes( chunksize, ssize )
|
||||
(restsize() - '=?iso-2022-jp?B??='.size) / 4 * 3 - chunksize
|
||||
end
|
||||
|
||||
#
|
||||
# free length buffer
|
||||
#
|
||||
|
||||
def add_text( str )
|
||||
@text << str
|
||||
# puts '---- text -------------------------------------'
|
||||
# puts "+ #{str.inspect}"
|
||||
# puts "txt >>>#{@text.inspect}<<<"
|
||||
end
|
||||
|
||||
def add_with_encode( str )
|
||||
@text << "=?iso-2022-jp?B?#{Base64.encode(str)}?="
|
||||
end
|
||||
|
||||
def add_lwsp( lwsp )
|
||||
# puts '---- lwsp -------------------------------------'
|
||||
# puts "+ #{lwsp.inspect}"
|
||||
fold if restsize() <= 0
|
||||
flush
|
||||
@lwsp = lwsp
|
||||
end
|
||||
|
||||
def flush
|
||||
# puts '---- flush ----'
|
||||
# puts "spc >>>#{@lwsp.inspect}<<<"
|
||||
# puts "txt >>>#{@text.inspect}<<<"
|
||||
@f << @lwsp << @text
|
||||
@curlen += (@lwsp.size + @text.size)
|
||||
@text = ''
|
||||
@lwsp = ''
|
||||
end
|
||||
|
||||
def fold
|
||||
# puts '---- fold ----'
|
||||
@f << @eol
|
||||
@curlen = 0
|
||||
@lwsp = SPACER
|
||||
end
|
||||
|
||||
def restsize
|
||||
MAX_LINE_LEN - (@curlen + @lwsp.size + @text.size)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end # module TMail
|
|
@ -0,0 +1,531 @@
|
|||
#
|
||||
# facade.rb
|
||||
#
|
||||
# Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
|
||||
#
|
||||
# This program is free software.
|
||||
# You can distribute/modify this program under the terms of
|
||||
# the GNU Lesser General Public License version 2 or later.
|
||||
#
|
||||
|
||||
require 'tmail/utils'
|
||||
|
||||
|
||||
module TMail
|
||||
|
||||
class Mail
|
||||
|
||||
def header_string( name, default = nil )
|
||||
h = @header[name.downcase] or return default
|
||||
h.to_s
|
||||
end
|
||||
|
||||
###
|
||||
### attributes
|
||||
###
|
||||
|
||||
include TextUtils
|
||||
|
||||
def set_string_array_attr( key, strs )
|
||||
strs.flatten!
|
||||
if strs.empty?
|
||||
@header.delete key.downcase
|
||||
else
|
||||
store key, strs.join(', ')
|
||||
end
|
||||
strs
|
||||
end
|
||||
private :set_string_array_attr
|
||||
|
||||
def set_string_attr( key, str )
|
||||
if str
|
||||
store key, str
|
||||
else
|
||||
@header.delete key.downcase
|
||||
end
|
||||
str
|
||||
end
|
||||
private :set_string_attr
|
||||
|
||||
def set_addrfield( name, arg )
|
||||
if arg
|
||||
h = HeaderField.internal_new(name, @config)
|
||||
h.addrs.replace [arg].flatten
|
||||
@header[name] = h
|
||||
else
|
||||
@header.delete name
|
||||
end
|
||||
arg
|
||||
end
|
||||
private :set_addrfield
|
||||
|
||||
def addrs2specs( addrs )
|
||||
return nil unless addrs
|
||||
list = addrs.map {|addr|
|
||||
if addr.address_group?
|
||||
then addr.map {|a| a.spec }
|
||||
else addr.spec
|
||||
end
|
||||
}.flatten
|
||||
return nil if list.empty?
|
||||
list
|
||||
end
|
||||
private :addrs2specs
|
||||
|
||||
|
||||
#
|
||||
# date time
|
||||
#
|
||||
|
||||
def date( default = nil )
|
||||
if h = @header['date']
|
||||
h.date
|
||||
else
|
||||
default
|
||||
end
|
||||
end
|
||||
|
||||
def date=( time )
|
||||
if time
|
||||
store 'Date', time2str(time)
|
||||
else
|
||||
@header.delete 'date'
|
||||
end
|
||||
time
|
||||
end
|
||||
|
||||
def strftime( fmt, default = nil )
|
||||
if t = date
|
||||
t.strftime(fmt)
|
||||
else
|
||||
default
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
#
|
||||
# destination
|
||||
#
|
||||
|
||||
def to_addrs( default = nil )
|
||||
if h = @header['to']
|
||||
h.addrs
|
||||
else
|
||||
default
|
||||
end
|
||||
end
|
||||
|
||||
def cc_addrs( default = nil )
|
||||
if h = @header['cc']
|
||||
h.addrs
|
||||
else
|
||||
default
|
||||
end
|
||||
end
|
||||
|
||||
def bcc_addrs( default = nil )
|
||||
if h = @header['bcc']
|
||||
h.addrs
|
||||
else
|
||||
default
|
||||
end
|
||||
end
|
||||
|
||||
def to_addrs=( arg )
|
||||
set_addrfield 'to', arg
|
||||
end
|
||||
|
||||
def cc_addrs=( arg )
|
||||
set_addrfield 'cc', arg
|
||||
end
|
||||
|
||||
def bcc_addrs=( arg )
|
||||
set_addrfield 'bcc', arg
|
||||
end
|
||||
|
||||
def to( default = nil )
|
||||
addrs2specs(to_addrs(nil)) || default
|
||||
end
|
||||
|
||||
def cc( default = nil )
|
||||
addrs2specs(cc_addrs(nil)) || default
|
||||
end
|
||||
|
||||
def bcc( default = nil )
|
||||
addrs2specs(bcc_addrs(nil)) || default
|
||||
end
|
||||
|
||||
def to=( *strs )
|
||||
set_string_array_attr 'To', strs
|
||||
end
|
||||
|
||||
def cc=( *strs )
|
||||
set_string_array_attr 'Cc', strs
|
||||
end
|
||||
|
||||
def bcc=( *strs )
|
||||
set_string_array_attr 'Bcc', strs
|
||||
end
|
||||
|
||||
|
||||
#
|
||||
# originator
|
||||
#
|
||||
|
||||
def from_addrs( default = nil )
|
||||
if h = @header['from']
|
||||
h.addrs
|
||||
else
|
||||
default
|
||||
end
|
||||
end
|
||||
|
||||
def from_addrs=( arg )
|
||||
set_addrfield 'from', arg
|
||||
end
|
||||
|
||||
def from( default = nil )
|
||||
addrs2specs(from_addrs(nil)) || default
|
||||
end
|
||||
|
||||
def from=( *strs )
|
||||
set_string_array_attr 'From', strs
|
||||
end
|
||||
|
||||
def friendly_from( default = nil )
|
||||
h = @header['from']
|
||||
a, = h.addrs
|
||||
return default unless a
|
||||
return a.phrase if a.phrase
|
||||
return h.comments.join(' ') unless h.comments.empty?
|
||||
a.spec
|
||||
end
|
||||
|
||||
|
||||
def reply_to_addrs( default = nil )
|
||||
if h = @header['reply-to']
|
||||
h.addrs
|
||||
else
|
||||
default
|
||||
end
|
||||
end
|
||||
|
||||
def reply_to_addrs=( arg )
|
||||
set_addrfield 'reply-to', arg
|
||||
end
|
||||
|
||||
def reply_to( default = nil )
|
||||
addrs2specs(reply_to_addrs(nil)) || default
|
||||
end
|
||||
|
||||
def reply_to=( *strs )
|
||||
set_string_array_attr 'Reply-To', strs
|
||||
end
|
||||
|
||||
|
||||
def sender_addr( default = nil )
|
||||
f = @header['sender'] or return default
|
||||
f.addr or return default
|
||||
end
|
||||
|
||||
def sender_addr=( addr )
|
||||
if addr
|
||||
h = HeaderField.internal_new('sender', @config)
|
||||
h.addr = addr
|
||||
@header['sender'] = h
|
||||
else
|
||||
@header.delete 'sender'
|
||||
end
|
||||
addr
|
||||
end
|
||||
|
||||
def sender( default )
|
||||
f = @header['sender'] or return default
|
||||
a = f.addr or return default
|
||||
a.spec
|
||||
end
|
||||
|
||||
def sender=( str )
|
||||
set_string_attr 'Sender', str
|
||||
end
|
||||
|
||||
|
||||
#
|
||||
# subject
|
||||
#
|
||||
|
||||
def subject( default = nil )
|
||||
if h = @header['subject']
|
||||
h.body
|
||||
else
|
||||
default
|
||||
end
|
||||
end
|
||||
|
||||
def subject=( str )
|
||||
set_string_attr 'Subject', str
|
||||
end
|
||||
|
||||
|
||||
#
|
||||
# identity & threading
|
||||
#
|
||||
|
||||
def message_id( default = nil )
|
||||
if h = @header['message-id']
|
||||
h.id || default
|
||||
else
|
||||
default
|
||||
end
|
||||
end
|
||||
|
||||
def message_id=( str )
|
||||
set_string_attr 'Message-Id', str
|
||||
end
|
||||
|
||||
def in_reply_to( default = nil )
|
||||
if h = @header['in-reply-to']
|
||||
h.ids
|
||||
else
|
||||
default
|
||||
end
|
||||
end
|
||||
|
||||
def in_reply_to=( *idstrs )
|
||||
set_string_array_attr 'In-Reply-To', idstrs
|
||||
end
|
||||
|
||||
def references( default = nil )
|
||||
if h = @header['references']
|
||||
h.refs
|
||||
else
|
||||
default
|
||||
end
|
||||
end
|
||||
|
||||
def references=( *strs )
|
||||
set_string_array_attr 'References', strs
|
||||
end
|
||||
|
||||
|
||||
#
|
||||
# MIME headers
|
||||
#
|
||||
|
||||
def mime_version( default = nil )
|
||||
if h = @header['mime-version']
|
||||
h.version || default
|
||||
else
|
||||
default
|
||||
end
|
||||
end
|
||||
|
||||
def mime_version=( m, opt = nil )
|
||||
if opt
|
||||
if h = @header['mime-version']
|
||||
h.major = m
|
||||
h.minor = opt
|
||||
else
|
||||
store 'Mime-Version', "#{m}.#{opt}"
|
||||
end
|
||||
else
|
||||
store 'Mime-Version', m
|
||||
end
|
||||
m
|
||||
end
|
||||
|
||||
|
||||
def content_type( default = nil )
|
||||
if h = @header['content-type']
|
||||
h.content_type || default
|
||||
else
|
||||
default
|
||||
end
|
||||
end
|
||||
|
||||
def main_type( default = nil )
|
||||
if h = @header['content-type']
|
||||
h.main_type || default
|
||||
else
|
||||
default
|
||||
end
|
||||
end
|
||||
|
||||
def sub_type( default = nil )
|
||||
if h = @header['content-type']
|
||||
h.sub_type || default
|
||||
else
|
||||
default
|
||||
end
|
||||
end
|
||||
|
||||
def set_content_type( str, sub = nil, param = nil )
|
||||
if sub
|
||||
main, sub = str, sub
|
||||
else
|
||||
main, sub = str.split(%r</>, 2)
|
||||
raise ArgumentError, "sub type missing: #{str.inspect}" unless sub
|
||||
end
|
||||
if h = @header['content-type']
|
||||
h.main_type = main
|
||||
h.sub_type = sub
|
||||
h.params.clear
|
||||
else
|
||||
store 'Content-Type', "#{main}/#{sub}"
|
||||
end
|
||||
@header['content-type'].params.replace param if param
|
||||
|
||||
str
|
||||
end
|
||||
|
||||
alias content_type= set_content_type
|
||||
|
||||
def type_param( name, default = nil )
|
||||
if h = @header['content-type']
|
||||
h[name] || default
|
||||
else
|
||||
default
|
||||
end
|
||||
end
|
||||
|
||||
def charset( default = nil )
|
||||
if h = @header['content-type']
|
||||
h['charset'] or default
|
||||
else
|
||||
default
|
||||
end
|
||||
end
|
||||
|
||||
def charset=( str )
|
||||
if str
|
||||
if h = @header[ 'content-type' ]
|
||||
h['charset'] = str
|
||||
else
|
||||
store 'Content-Type', "text/plain; charset=#{str}"
|
||||
end
|
||||
end
|
||||
str
|
||||
end
|
||||
|
||||
|
||||
def transfer_encoding( default = nil )
|
||||
if h = @header['content-transfer-encoding']
|
||||
h.encoding || default
|
||||
else
|
||||
default
|
||||
end
|
||||
end
|
||||
|
||||
def transfer_encoding=( str )
|
||||
set_string_attr 'Content-Transfer-Encoding', str
|
||||
end
|
||||
|
||||
alias encoding transfer_encoding
|
||||
alias encoding= transfer_encoding=
|
||||
alias content_transfer_encoding transfer_encoding
|
||||
alias content_transfer_encoding= transfer_encoding=
|
||||
|
||||
|
||||
def disposition( default = nil )
|
||||
if h = @header['content-disposition']
|
||||
h.disposition || default
|
||||
else
|
||||
default
|
||||
end
|
||||
end
|
||||
|
||||
alias content_disposition disposition
|
||||
|
||||
def set_disposition( str, params = nil )
|
||||
if h = @header['content-disposition']
|
||||
h.disposition = str
|
||||
h.params.clear
|
||||
else
|
||||
h = store('Content-Disposition', str)
|
||||
end
|
||||
h.params.replace params if params
|
||||
end
|
||||
|
||||
alias disposition= set_disposition
|
||||
alias set_content_disposition set_disposition
|
||||
alias content_disposition= set_disposition
|
||||
|
||||
def disposition_param( name, default = nil )
|
||||
if h = @header['content-disposition']
|
||||
h[name] || default
|
||||
else
|
||||
default
|
||||
end
|
||||
end
|
||||
|
||||
###
|
||||
### utils
|
||||
###
|
||||
|
||||
def create_reply
|
||||
mail = TMail::Mail.parse('')
|
||||
mail.subject = 'Re: ' + subject('').sub(/\A(?:\[[^\]]+\])?(?:\s*Re:)*\s*/i, '')
|
||||
mail.to_addrs = reply_addresses([])
|
||||
mail.in_reply_to = [message_id(nil)].compact
|
||||
mail.references = references([]) + [message_id(nil)].compact
|
||||
mail.mime_version = '1.0'
|
||||
mail
|
||||
end
|
||||
|
||||
|
||||
def base64_encode
|
||||
store 'Content-Transfer-Encoding', 'Base64'
|
||||
self.body = Base64.folding_encode(self.body)
|
||||
end
|
||||
|
||||
def base64_decode
|
||||
if /base64/i === self.transfer_encoding('')
|
||||
store 'Content-Transfer-Encoding', '8bit'
|
||||
self.body = Base64.decode(self.body, @config.strict_base64decode?)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
def destinations( default = nil )
|
||||
ret = []
|
||||
%w( to cc bcc ).each do |nm|
|
||||
if h = @header[nm]
|
||||
h.addrs.each {|i| ret.push i.address }
|
||||
end
|
||||
end
|
||||
ret.empty? ? default : ret
|
||||
end
|
||||
|
||||
def each_destination( &block )
|
||||
destinations([]).each do |i|
|
||||
if Address === i
|
||||
yield i
|
||||
else
|
||||
i.each(&block)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
alias each_dest each_destination
|
||||
|
||||
|
||||
def reply_addresses( default = nil )
|
||||
reply_to_addrs(nil) or from_addrs(nil) or default
|
||||
end
|
||||
|
||||
def error_reply_addresses( default = nil )
|
||||
if s = sender(nil)
|
||||
[s]
|
||||
else
|
||||
from_addrs(default)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
def multipart?
|
||||
main_type('').downcase == 'multipart'
|
||||
end
|
||||
|
||||
end # class Mail
|
||||
|
||||
end # module TMail
|
|
@ -0,0 +1,893 @@
|
|||
#
|
||||
# header.rb
|
||||
#
|
||||
# Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
|
||||
#
|
||||
# This program is free software.
|
||||
# You can distribute/modify this program under the terms of
|
||||
# the GNU Lesser General Public License version 2 or later.
|
||||
#
|
||||
|
||||
require 'tmail/encode'
|
||||
require 'tmail/address'
|
||||
require 'tmail/parser'
|
||||
require 'tmail/config'
|
||||
require 'tmail/utils'
|
||||
|
||||
|
||||
module TMail
|
||||
|
||||
class HeaderField
|
||||
|
||||
include TextUtils
|
||||
|
||||
class << self
|
||||
|
||||
alias newobj new
|
||||
|
||||
def new( name, body, conf = DEFAULT_CONFIG )
|
||||
klass = FNAME_TO_CLASS[name.downcase] || UnstructuredHeader
|
||||
klass.newobj body, conf
|
||||
end
|
||||
|
||||
def new_from_port( port, name, conf = DEFAULT_CONFIG )
|
||||
re = Regep.new('\A(' + Regexp.quote(name) + '):', 'i')
|
||||
str = nil
|
||||
port.ropen {|f|
|
||||
f.each do |line|
|
||||
if m = re.match(line) then str = m.post_match.strip
|
||||
elsif str and /\A[\t ]/ === line then str << ' ' << line.strip
|
||||
elsif /\A-*\s*\z/ === line then break
|
||||
elsif str then break
|
||||
end
|
||||
end
|
||||
}
|
||||
new(name, str, Config.to_config(conf))
|
||||
end
|
||||
|
||||
def internal_new( name, conf )
|
||||
FNAME_TO_CLASS[name].newobj('', conf, true)
|
||||
end
|
||||
|
||||
end # class << self
|
||||
|
||||
def initialize( body, conf, intern = false )
|
||||
@body = body
|
||||
@config = conf
|
||||
|
||||
@illegal = false
|
||||
@parsed = false
|
||||
if intern
|
||||
@parsed = true
|
||||
parse_init
|
||||
end
|
||||
end
|
||||
|
||||
def inspect
|
||||
"#<#{self.class} #{@body.inspect}>"
|
||||
end
|
||||
|
||||
def illegal?
|
||||
@illegal
|
||||
end
|
||||
|
||||
def empty?
|
||||
ensure_parsed
|
||||
return true if @illegal
|
||||
isempty?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def ensure_parsed
|
||||
return if @parsed
|
||||
@parsed = true
|
||||
parse
|
||||
end
|
||||
|
||||
# defabstract parse
|
||||
# end
|
||||
|
||||
def clear_parse_status
|
||||
@parsed = false
|
||||
@illegal = false
|
||||
end
|
||||
|
||||
public
|
||||
|
||||
def body
|
||||
ensure_parsed
|
||||
v = Decoder.new(s = '')
|
||||
do_accept v
|
||||
v.terminate
|
||||
s
|
||||
end
|
||||
|
||||
def body=( str )
|
||||
@body = str
|
||||
clear_parse_status
|
||||
end
|
||||
|
||||
include StrategyInterface
|
||||
|
||||
def accept( strategy, dummy1 = nil, dummy2 = nil )
|
||||
ensure_parsed
|
||||
do_accept strategy
|
||||
strategy.terminate
|
||||
end
|
||||
|
||||
# abstract do_accept
|
||||
|
||||
end
|
||||
|
||||
|
||||
class UnstructuredHeader < HeaderField
|
||||
|
||||
def body
|
||||
ensure_parsed
|
||||
@body
|
||||
end
|
||||
|
||||
def body=( arg )
|
||||
ensure_parsed
|
||||
@body = arg
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def parse_init
|
||||
end
|
||||
|
||||
def parse
|
||||
@body = Decoder.decode(@body.gsub(/\n|\r\n|\r/, ''))
|
||||
end
|
||||
|
||||
def isempty?
|
||||
not @body
|
||||
end
|
||||
|
||||
def do_accept( strategy )
|
||||
strategy.text @body
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
class StructuredHeader < HeaderField
|
||||
|
||||
def comments
|
||||
ensure_parsed
|
||||
@comments
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def parse
|
||||
save = nil
|
||||
|
||||
begin
|
||||
parse_init
|
||||
do_parse
|
||||
rescue SyntaxError
|
||||
if not save and mime_encoded? @body
|
||||
save = @body
|
||||
@body = Decoder.decode(save)
|
||||
retry
|
||||
elsif save
|
||||
@body = save
|
||||
end
|
||||
|
||||
@illegal = true
|
||||
raise if @config.strict_parse?
|
||||
end
|
||||
end
|
||||
|
||||
def parse_init
|
||||
@comments = []
|
||||
init
|
||||
end
|
||||
|
||||
def do_parse
|
||||
obj = Parser.parse(self.class::PARSE_TYPE, @body, @comments)
|
||||
set obj if obj
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
class DateTimeHeader < StructuredHeader
|
||||
|
||||
PARSE_TYPE = :DATETIME
|
||||
|
||||
def date
|
||||
ensure_parsed
|
||||
@date
|
||||
end
|
||||
|
||||
def date=( arg )
|
||||
ensure_parsed
|
||||
@date = arg
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def init
|
||||
@date = nil
|
||||
end
|
||||
|
||||
def set( t )
|
||||
@date = t
|
||||
end
|
||||
|
||||
def isempty?
|
||||
not @date
|
||||
end
|
||||
|
||||
def do_accept( strategy )
|
||||
strategy.meta time2str(@date)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
class AddressHeader < StructuredHeader
|
||||
|
||||
PARSE_TYPE = :MADDRESS
|
||||
|
||||
def addrs
|
||||
ensure_parsed
|
||||
@addrs
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def init
|
||||
@addrs = []
|
||||
end
|
||||
|
||||
def set( a )
|
||||
@addrs = a
|
||||
end
|
||||
|
||||
def isempty?
|
||||
@addrs.empty?
|
||||
end
|
||||
|
||||
def do_accept( strategy )
|
||||
first = true
|
||||
@addrs.each do |a|
|
||||
if first
|
||||
first = false
|
||||
else
|
||||
strategy.meta ','
|
||||
strategy.space
|
||||
end
|
||||
a.accept strategy
|
||||
end
|
||||
|
||||
@comments.each do |c|
|
||||
strategy.space
|
||||
strategy.meta '('
|
||||
strategy.text c
|
||||
strategy.meta ')'
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
class ReturnPathHeader < AddressHeader
|
||||
|
||||
PARSE_TYPE = :RETPATH
|
||||
|
||||
def addr
|
||||
addrs()[0]
|
||||
end
|
||||
|
||||
def spec
|
||||
a = addr() or return nil
|
||||
a.spec
|
||||
end
|
||||
|
||||
def routes
|
||||
a = addr() or return nil
|
||||
a.routes
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def do_accept( strategy )
|
||||
a = addr()
|
||||
|
||||
strategy.meta '<'
|
||||
unless a.routes.empty?
|
||||
strategy.meta a.routes.map {|i| '@' + i }.join(',')
|
||||
strategy.meta ':'
|
||||
end
|
||||
spec = a.spec
|
||||
strategy.meta spec if spec
|
||||
strategy.meta '>'
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
class SingleAddressHeader < AddressHeader
|
||||
|
||||
def addr
|
||||
addrs()[0]
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def do_accept( strategy )
|
||||
a = addr()
|
||||
a.accept strategy
|
||||
@comments.each do |c|
|
||||
strategy.space
|
||||
strategy.meta '('
|
||||
strategy.text c
|
||||
strategy.meta ')'
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
class MessageIdHeader < StructuredHeader
|
||||
|
||||
def id
|
||||
ensure_parsed
|
||||
@id
|
||||
end
|
||||
|
||||
def id=( arg )
|
||||
ensure_parsed
|
||||
@id = arg
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def init
|
||||
@id = nil
|
||||
end
|
||||
|
||||
def isempty?
|
||||
not @id
|
||||
end
|
||||
|
||||
def do_parse
|
||||
@id = @body.slice(MESSAGE_ID) or
|
||||
raise SyntaxError, "wrong Message-ID format: #{@body}"
|
||||
end
|
||||
|
||||
def do_accept( strategy )
|
||||
strategy.meta @id
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
class ReferencesHeader < StructuredHeader
|
||||
|
||||
def refs
|
||||
ensure_parsed
|
||||
@refs
|
||||
end
|
||||
|
||||
def each_id
|
||||
self.refs.each do |i|
|
||||
yield i if MESSAGE_ID === i
|
||||
end
|
||||
end
|
||||
|
||||
def ids
|
||||
ensure_parsed
|
||||
@ids
|
||||
end
|
||||
|
||||
def each_phrase
|
||||
self.refs.each do |i|
|
||||
yield i unless MESSAGE_ID === i
|
||||
end
|
||||
end
|
||||
|
||||
def phrases
|
||||
ret = []
|
||||
each_phrase {|i| ret.push i }
|
||||
ret
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def init
|
||||
@refs = []
|
||||
@ids = []
|
||||
end
|
||||
|
||||
def isempty?
|
||||
@ids.empty?
|
||||
end
|
||||
|
||||
def do_parse
|
||||
str = @body
|
||||
while m = MESSAGE_ID.match(str)
|
||||
pre = m.pre_match.strip
|
||||
@refs.push pre unless pre.empty?
|
||||
@refs.push s = m[0]
|
||||
@ids.push s
|
||||
str = m.post_match
|
||||
end
|
||||
str = str.strip
|
||||
@refs.push str unless str.empty?
|
||||
end
|
||||
|
||||
def do_accept( strategy )
|
||||
first = true
|
||||
@ids.each do |i|
|
||||
if first
|
||||
first = false
|
||||
else
|
||||
strategy.space
|
||||
end
|
||||
strategy.meta i
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
class ReceivedHeader < StructuredHeader
|
||||
|
||||
PARSE_TYPE = :RECEIVED
|
||||
|
||||
def from
|
||||
ensure_parsed
|
||||
@from
|
||||
end
|
||||
|
||||
def from=( arg )
|
||||
ensure_parsed
|
||||
@from = arg
|
||||
end
|
||||
|
||||
def by
|
||||
ensure_parsed
|
||||
@by
|
||||
end
|
||||
|
||||
def by=( arg )
|
||||
ensure_parsed
|
||||
@by = arg
|
||||
end
|
||||
|
||||
def via
|
||||
ensure_parsed
|
||||
@via
|
||||
end
|
||||
|
||||
def via=( arg )
|
||||
ensure_parsed
|
||||
@via = arg
|
||||
end
|
||||
|
||||
def with
|
||||
ensure_parsed
|
||||
@with
|
||||
end
|
||||
|
||||
def id
|
||||
ensure_parsed
|
||||
@id
|
||||
end
|
||||
|
||||
def id=( arg )
|
||||
ensure_parsed
|
||||
@id = arg
|
||||
end
|
||||
|
||||
def _for
|
||||
ensure_parsed
|
||||
@_for
|
||||
end
|
||||
|
||||
def _for=( arg )
|
||||
ensure_parsed
|
||||
@_for = arg
|
||||
end
|
||||
|
||||
def date
|
||||
ensure_parsed
|
||||
@date
|
||||
end
|
||||
|
||||
def date=( arg )
|
||||
ensure_parsed
|
||||
@date = arg
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def init
|
||||
@from = @by = @via = @with = @id = @_for = nil
|
||||
@with = []
|
||||
@date = nil
|
||||
end
|
||||
|
||||
def set( args )
|
||||
@from, @by, @via, @with, @id, @_for, @date = *args
|
||||
end
|
||||
|
||||
def isempty?
|
||||
@with.empty? and not (@from or @by or @via or @id or @_for or @date)
|
||||
end
|
||||
|
||||
def do_accept( strategy )
|
||||
list = []
|
||||
list.push 'from ' + @from if @from
|
||||
list.push 'by ' + @by if @by
|
||||
list.push 'via ' + @via if @via
|
||||
@with.each do |i|
|
||||
list.push 'with ' + i
|
||||
end
|
||||
list.push 'id ' + @id if @id
|
||||
list.push 'for <' + @_for + '>' if @_for
|
||||
|
||||
first = true
|
||||
list.each do |i|
|
||||
strategy.space unless first
|
||||
strategy.meta i
|
||||
first = false
|
||||
end
|
||||
if @date
|
||||
strategy.meta ';'
|
||||
strategy.space
|
||||
strategy.meta time2str(@date)
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
class KeywordsHeader < StructuredHeader
|
||||
|
||||
PARSE_TYPE = :KEYWORDS
|
||||
|
||||
def keys
|
||||
ensure_parsed
|
||||
@keys
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def init
|
||||
@keys = []
|
||||
end
|
||||
|
||||
def set( a )
|
||||
@keys = a
|
||||
end
|
||||
|
||||
def isempty?
|
||||
@keys.empty?
|
||||
end
|
||||
|
||||
def do_accept( strategy )
|
||||
first = true
|
||||
@keys.each do |i|
|
||||
if first
|
||||
first = false
|
||||
else
|
||||
strategy.meta ','
|
||||
end
|
||||
strategy.meta i
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
class EncryptedHeader < StructuredHeader
|
||||
|
||||
PARSE_TYPE = :ENCRYPTED
|
||||
|
||||
def encrypter
|
||||
ensure_parsed
|
||||
@encrypter
|
||||
end
|
||||
|
||||
def encrypter=( arg )
|
||||
ensure_parsed
|
||||
@encrypter = arg
|
||||
end
|
||||
|
||||
def keyword
|
||||
ensure_parsed
|
||||
@keyword
|
||||
end
|
||||
|
||||
def keyword=( arg )
|
||||
ensure_parsed
|
||||
@keyword = arg
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def init
|
||||
@encrypter = nil
|
||||
@keyword = nil
|
||||
end
|
||||
|
||||
def set( args )
|
||||
@encrypter, @keyword = args
|
||||
end
|
||||
|
||||
def isempty?
|
||||
not (@encrypter or @keyword)
|
||||
end
|
||||
|
||||
def do_accept( strategy )
|
||||
if @key
|
||||
strategy.meta @encrypter + ','
|
||||
strategy.space
|
||||
strategy.meta @keyword
|
||||
else
|
||||
strategy.meta @encrypter
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
class MimeVersionHeader < StructuredHeader
|
||||
|
||||
PARSE_TYPE = :MIMEVERSION
|
||||
|
||||
def major
|
||||
ensure_parsed
|
||||
@major
|
||||
end
|
||||
|
||||
def major=( arg )
|
||||
ensure_parsed
|
||||
@major = arg
|
||||
end
|
||||
|
||||
def minor
|
||||
ensure_parsed
|
||||
@minor
|
||||
end
|
||||
|
||||
def minor=( arg )
|
||||
ensure_parsed
|
||||
@minor = arg
|
||||
end
|
||||
|
||||
def version
|
||||
sprintf('%d.%d', major, minor)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def init
|
||||
@major = nil
|
||||
@minor = nil
|
||||
end
|
||||
|
||||
def set( args )
|
||||
@major, @minor = *args
|
||||
end
|
||||
|
||||
def isempty?
|
||||
not (@major or @minor)
|
||||
end
|
||||
|
||||
def do_accept( strategy )
|
||||
strategy.meta sprintf('%d.%d', @major, @minor)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
class ContentTypeHeader < StructuredHeader
|
||||
|
||||
PARSE_TYPE = :CTYPE
|
||||
|
||||
def main_type
|
||||
ensure_parsed
|
||||
@main
|
||||
end
|
||||
|
||||
def main_type=( arg )
|
||||
ensure_parsed
|
||||
@main = arg.downcase
|
||||
end
|
||||
|
||||
def sub_type
|
||||
ensure_parsed
|
||||
@sub
|
||||
end
|
||||
|
||||
def sub_type=( arg )
|
||||
ensure_parsed
|
||||
@sub = arg.downcase
|
||||
end
|
||||
|
||||
def content_type
|
||||
ensure_parsed
|
||||
@sub ? sprintf('%s/%s', @main, @sub) : @main
|
||||
end
|
||||
|
||||
def params
|
||||
ensure_parsed
|
||||
@params
|
||||
end
|
||||
|
||||
def []( key )
|
||||
ensure_parsed
|
||||
@params and @params[key]
|
||||
end
|
||||
|
||||
def []=( key, val )
|
||||
ensure_parsed
|
||||
(@params ||= {})[key] = val
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def init
|
||||
@main = @sub = @params = nil
|
||||
end
|
||||
|
||||
def set( args )
|
||||
@main, @sub, @params = *args
|
||||
end
|
||||
|
||||
def isempty?
|
||||
not (@main or @sub)
|
||||
end
|
||||
|
||||
def do_accept( strategy )
|
||||
if @sub
|
||||
strategy.meta sprintf('%s/%s', @main, @sub)
|
||||
else
|
||||
strategy.meta @main
|
||||
end
|
||||
@params.each do |k,v|
|
||||
strategy.meta ';'
|
||||
strategy.space
|
||||
strategy.kv_pair k, v
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
class ContentTransferEncodingHeader < StructuredHeader
|
||||
|
||||
PARSE_TYPE = :CENCODING
|
||||
|
||||
def encoding
|
||||
ensure_parsed
|
||||
@encoding
|
||||
end
|
||||
|
||||
def encoding=( arg )
|
||||
ensure_parsed
|
||||
@encoding = arg
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def init
|
||||
@encoding = nil
|
||||
end
|
||||
|
||||
def set( s )
|
||||
@encoding = s
|
||||
end
|
||||
|
||||
def isempty?
|
||||
not @encoding
|
||||
end
|
||||
|
||||
def do_accept( strategy )
|
||||
strategy.meta @encoding.capitalize
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
class ContentDispositionHeader < StructuredHeader
|
||||
|
||||
PARSE_TYPE = :CDISPOSITION
|
||||
|
||||
def disposition
|
||||
ensure_parsed
|
||||
@disposition
|
||||
end
|
||||
|
||||
def disposition=( str )
|
||||
ensure_parsed
|
||||
@disposition = str.downcase
|
||||
end
|
||||
|
||||
def params
|
||||
ensure_parsed
|
||||
@params
|
||||
end
|
||||
|
||||
def []( key )
|
||||
ensure_parsed
|
||||
@params and @params[key]
|
||||
end
|
||||
|
||||
def []=( key, val )
|
||||
ensure_parsed
|
||||
(@params ||= {})[key] = val
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def init
|
||||
@disposition = @params = nil
|
||||
end
|
||||
|
||||
def set( args )
|
||||
@disposition, @params = *args
|
||||
end
|
||||
|
||||
def isempty?
|
||||
not @disposition and (not @params or @params.empty?)
|
||||
end
|
||||
|
||||
def do_accept( strategy )
|
||||
strategy.meta @disposition
|
||||
@params.each do |k,v|
|
||||
strategy.meta ';'
|
||||
strategy.space
|
||||
strategy.kv_pair k, v
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
class HeaderField # redefine
|
||||
|
||||
FNAME_TO_CLASS = {
|
||||
'date' => DateTimeHeader,
|
||||
'resent-date' => DateTimeHeader,
|
||||
'to' => AddressHeader,
|
||||
'cc' => AddressHeader,
|
||||
'bcc' => AddressHeader,
|
||||
'from' => AddressHeader,
|
||||
'reply-to' => AddressHeader,
|
||||
'resent-to' => AddressHeader,
|
||||
'resent-cc' => AddressHeader,
|
||||
'resent-bcc' => AddressHeader,
|
||||
'resent-from' => AddressHeader,
|
||||
'resent-reply-to' => AddressHeader,
|
||||
'sender' => SingleAddressHeader,
|
||||
'resent-sender' => SingleAddressHeader,
|
||||
'return-path' => ReturnPathHeader,
|
||||
'message-id' => MessageIdHeader,
|
||||
'resent-message-id' => MessageIdHeader,
|
||||
'in-reply-to' => ReferencesHeader,
|
||||
'received' => ReceivedHeader,
|
||||
'references' => ReferencesHeader,
|
||||
'keywords' => KeywordsHeader,
|
||||
'encrypted' => EncryptedHeader,
|
||||
'mime-version' => MimeVersionHeader,
|
||||
'content-type' => ContentTypeHeader,
|
||||
'content-transfer-encoding' => ContentTransferEncodingHeader,
|
||||
'content-disposition' => ContentDispositionHeader,
|
||||
'content-id' => MessageIdHeader,
|
||||
'subject' => UnstructuredHeader,
|
||||
'comments' => UnstructuredHeader,
|
||||
'content-description' => UnstructuredHeader
|
||||
}
|
||||
|
||||
end
|
||||
|
||||
end # module TMail
|
|
@ -0,0 +1,16 @@
|
|||
#
|
||||
# info.rb
|
||||
#
|
||||
# Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
|
||||
#
|
||||
# This program is free software.
|
||||
# You can distribute/modify this program under the terms of
|
||||
# the GNU Lesser General Public License version 2 or later.
|
||||
#
|
||||
|
||||
module TMail
|
||||
|
||||
Version = '0.10.7'
|
||||
Copyright = 'Copyright (c) 1998-2002 Minero Aoki'
|
||||
|
||||
end
|
|
@ -0,0 +1 @@
|
|||
require 'tmail/mailbox'
|
|
@ -0,0 +1,420 @@
|
|||
#
|
||||
# mail.rb
|
||||
#
|
||||
# Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
|
||||
#
|
||||
# This program is free software.
|
||||
# You can distribute/modify this program under the terms of
|
||||
# the GNU Lesser General Public License version 2 or later.
|
||||
#
|
||||
|
||||
require 'tmail/facade'
|
||||
require 'tmail/encode'
|
||||
require 'tmail/header'
|
||||
require 'tmail/port'
|
||||
require 'tmail/config'
|
||||
require 'tmail/utils'
|
||||
require 'socket'
|
||||
|
||||
|
||||
module TMail
|
||||
|
||||
class Mail
|
||||
|
||||
class << self
|
||||
def load( fname )
|
||||
new(FilePort.new(fname))
|
||||
end
|
||||
|
||||
alias load_from load
|
||||
alias loadfrom load
|
||||
|
||||
def parse( str )
|
||||
new(StringPort.new(str))
|
||||
end
|
||||
end
|
||||
|
||||
def initialize( port = nil, conf = DEFAULT_CONFIG )
|
||||
@port = port || StringPort.new
|
||||
@config = Config.to_config(conf)
|
||||
|
||||
@header = {}
|
||||
@body_port = nil
|
||||
@body_parsed = false
|
||||
@epilogue = ''
|
||||
@parts = []
|
||||
|
||||
@port.ropen {|f|
|
||||
parse_header f
|
||||
parse_body f unless @port.reproducible?
|
||||
}
|
||||
end
|
||||
|
||||
attr_reader :port
|
||||
|
||||
def inspect
|
||||
"\#<#{self.class} port=#{@port.inspect} bodyport=#{@body_port.inspect}>"
|
||||
end
|
||||
|
||||
#
|
||||
# to_s interfaces
|
||||
#
|
||||
|
||||
public
|
||||
|
||||
include StrategyInterface
|
||||
|
||||
def write_back( eol = "\n", charset = 'e' )
|
||||
parse_body
|
||||
@port.wopen {|stream| encoded eol, charset, stream }
|
||||
end
|
||||
|
||||
def accept( strategy )
|
||||
with_multipart_encoding(strategy) {
|
||||
ordered_each do |name, field|
|
||||
next if field.empty?
|
||||
strategy.header_name canonical(name)
|
||||
field.accept strategy
|
||||
strategy.puts
|
||||
end
|
||||
strategy.puts
|
||||
body_port().ropen {|r|
|
||||
strategy.write r.read
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def canonical( name )
|
||||
name.split(/-/).map {|s| s.capitalize }.join('-')
|
||||
end
|
||||
|
||||
def with_multipart_encoding( strategy )
|
||||
if parts().empty? # DO NOT USE @parts
|
||||
yield
|
||||
|
||||
else
|
||||
bound = ::TMail.new_boundary
|
||||
if @header.key? 'content-type'
|
||||
@header['content-type'].params['boundary'] = bound
|
||||
else
|
||||
store 'Content-Type', %<multipart/mixed; boundary="#{bound}">
|
||||
end
|
||||
|
||||
yield
|
||||
|
||||
parts().each do |tm|
|
||||
strategy.puts
|
||||
strategy.puts '--' + bound
|
||||
tm.accept strategy
|
||||
end
|
||||
strategy.puts
|
||||
strategy.puts '--' + bound + '--'
|
||||
strategy.write epilogue()
|
||||
end
|
||||
end
|
||||
|
||||
###
|
||||
### header
|
||||
###
|
||||
|
||||
public
|
||||
|
||||
ALLOW_MULTIPLE = {
|
||||
'received' => true,
|
||||
'resent-date' => true,
|
||||
'resent-from' => true,
|
||||
'resent-sender' => true,
|
||||
'resent-to' => true,
|
||||
'resent-cc' => true,
|
||||
'resent-bcc' => true,
|
||||
'resent-message-id' => true,
|
||||
'comments' => true,
|
||||
'keywords' => true
|
||||
}
|
||||
USE_ARRAY = ALLOW_MULTIPLE
|
||||
|
||||
def header
|
||||
@header.dup
|
||||
end
|
||||
|
||||
def []( key )
|
||||
@header[key.downcase]
|
||||
end
|
||||
|
||||
alias fetch []
|
||||
|
||||
def []=( key, val )
|
||||
dkey = key.downcase
|
||||
|
||||
if val.nil?
|
||||
@header.delete dkey
|
||||
return nil
|
||||
end
|
||||
|
||||
case val
|
||||
when String
|
||||
header = new_hf(key, val)
|
||||
when HeaderField
|
||||
;
|
||||
when Array
|
||||
ALLOW_MULTIPLE.include? dkey or
|
||||
raise ArgumentError, "#{key}: Header must not be multiple"
|
||||
@header[dkey] = val
|
||||
return val
|
||||
else
|
||||
header = new_hf(key, val.to_s)
|
||||
end
|
||||
if ALLOW_MULTIPLE.include? dkey
|
||||
(@header[dkey] ||= []).push header
|
||||
else
|
||||
@header[dkey] = header
|
||||
end
|
||||
|
||||
val
|
||||
end
|
||||
|
||||
alias store []=
|
||||
|
||||
def each_header
|
||||
@header.each do |key, val|
|
||||
[val].flatten.each {|v| yield key, v }
|
||||
end
|
||||
end
|
||||
|
||||
alias each_pair each_header
|
||||
|
||||
def each_header_name( &block )
|
||||
@header.each_key(&block)
|
||||
end
|
||||
|
||||
alias each_key each_header_name
|
||||
|
||||
def each_field( &block )
|
||||
@header.values.flatten.each(&block)
|
||||
end
|
||||
|
||||
alias each_value each_field
|
||||
|
||||
FIELD_ORDER = %w(
|
||||
return-path received
|
||||
resent-date resent-from resent-sender resent-to
|
||||
resent-cc resent-bcc resent-message-id
|
||||
date from sender reply-to to cc bcc
|
||||
message-id in-reply-to references
|
||||
subject comments keywords
|
||||
mime-version content-type content-transfer-encoding
|
||||
content-disposition content-description
|
||||
)
|
||||
|
||||
def ordered_each
|
||||
list = @header.keys
|
||||
FIELD_ORDER.each do |name|
|
||||
if list.delete(name)
|
||||
[@header[name]].flatten.each {|v| yield name, v }
|
||||
end
|
||||
end
|
||||
list.each do |name|
|
||||
[@header[name]].flatten.each {|v| yield name, v }
|
||||
end
|
||||
end
|
||||
|
||||
def clear
|
||||
@header.clear
|
||||
end
|
||||
|
||||
def delete( key )
|
||||
@header.delete key.downcase
|
||||
end
|
||||
|
||||
def delete_if
|
||||
@header.delete_if do |key,val|
|
||||
if Array === val
|
||||
val.delete_if {|v| yield key, v }
|
||||
val.empty?
|
||||
else
|
||||
yield key, val
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def keys
|
||||
@header.keys
|
||||
end
|
||||
|
||||
def key?( key )
|
||||
@header.key? key.downcase
|
||||
end
|
||||
|
||||
def values_at( *args )
|
||||
args.map {|k| @header[k.downcase] }.flatten
|
||||
end
|
||||
|
||||
alias indexes values_at
|
||||
alias indices values_at
|
||||
|
||||
private
|
||||
|
||||
def parse_header( f )
|
||||
name = field = nil
|
||||
unixfrom = nil
|
||||
|
||||
while line = f.gets
|
||||
case line
|
||||
when /\A[ \t]/ # continue from prev line
|
||||
raise SyntaxError, 'mail is began by space' unless field
|
||||
field << ' ' << line.strip
|
||||
|
||||
when /\A([^\: \t]+):\s*/ # new header line
|
||||
add_hf name, field if field
|
||||
name = $1
|
||||
field = $' #.strip
|
||||
|
||||
when /\A\-*\s*\z/ # end of header
|
||||
add_hf name, field if field
|
||||
name = field = nil
|
||||
break
|
||||
|
||||
when /\AFrom (\S+)/
|
||||
unixfrom = $1
|
||||
|
||||
else
|
||||
raise SyntaxError, "wrong mail header: '#{line.inspect}'"
|
||||
end
|
||||
end
|
||||
add_hf name, field if name
|
||||
|
||||
if unixfrom
|
||||
add_hf 'Return-Path', "<#{unixfrom}>" unless @header['return-path']
|
||||
end
|
||||
end
|
||||
|
||||
def add_hf( name, field )
|
||||
key = name.downcase
|
||||
field = new_hf(name, field)
|
||||
|
||||
if ALLOW_MULTIPLE.include? key
|
||||
(@header[key] ||= []).push field
|
||||
else
|
||||
@header[key] = field
|
||||
end
|
||||
end
|
||||
|
||||
def new_hf( name, field )
|
||||
HeaderField.new(name, field, @config)
|
||||
end
|
||||
|
||||
###
|
||||
### body
|
||||
###
|
||||
|
||||
public
|
||||
|
||||
def body_port
|
||||
parse_body
|
||||
@body_port
|
||||
end
|
||||
|
||||
def each( &block )
|
||||
body_port().ropen {|f| f.each(&block) }
|
||||
end
|
||||
|
||||
def body
|
||||
parse_body
|
||||
@body_port.ropen {|f|
|
||||
return f.read
|
||||
}
|
||||
end
|
||||
|
||||
def body=( str )
|
||||
parse_body
|
||||
@body_port.wopen {|f| f.write str }
|
||||
str
|
||||
end
|
||||
|
||||
alias preamble body
|
||||
alias preamble= body=
|
||||
|
||||
def epilogue
|
||||
parse_body
|
||||
@epilogue.dup
|
||||
end
|
||||
|
||||
def epilogue=( str )
|
||||
parse_body
|
||||
@epilogue = str
|
||||
str
|
||||
end
|
||||
|
||||
def parts
|
||||
parse_body
|
||||
@parts
|
||||
end
|
||||
|
||||
def each_part( &block )
|
||||
parts().each(&block)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def parse_body( f = nil )
|
||||
return if @body_parsed
|
||||
if f
|
||||
parse_body_0 f
|
||||
else
|
||||
@port.ropen {|f|
|
||||
skip_header f
|
||||
parse_body_0 f
|
||||
}
|
||||
end
|
||||
@body_parsed = true
|
||||
end
|
||||
|
||||
def skip_header( f )
|
||||
while line = f.gets
|
||||
return if /\A[\r\n]*\z/ === line
|
||||
end
|
||||
end
|
||||
|
||||
def parse_body_0( f )
|
||||
if multipart?
|
||||
read_multipart f
|
||||
else
|
||||
@body_port = @config.new_body_port(self)
|
||||
@body_port.wopen {|w|
|
||||
w.write f.read
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
def read_multipart( src )
|
||||
bound = @header['content-type'].params['boundary']
|
||||
is_sep = /\A--#{Regexp.quote bound}(?:--)?[ \t]*(?:\n|\r\n|\r)/
|
||||
lastbound = "--#{bound}--"
|
||||
|
||||
ports = [ @config.new_preamble_port(self) ]
|
||||
begin
|
||||
f = ports.last.wopen
|
||||
while line = src.gets
|
||||
if is_sep === line
|
||||
f.close
|
||||
break if line.strip == lastbound
|
||||
ports.push @config.new_part_port(self)
|
||||
f = ports.last.wopen
|
||||
else
|
||||
f << line
|
||||
end
|
||||
end
|
||||
@epilogue = (src.read || '')
|
||||
ensure
|
||||
f.close if f and not f.closed?
|
||||
end
|
||||
|
||||
@body_port = ports.shift
|
||||
@parts = ports.map {|p| self.class.new(p, @config) }
|
||||
end
|
||||
|
||||
end # class Mail
|
||||
|
||||
end # module TMail
|
|
@ -0,0 +1,414 @@
|
|||
#
|
||||
# mailbox.rb
|
||||
#
|
||||
# Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
|
||||
#
|
||||
# This program is free software.
|
||||
# You can distribute/modify this program under the terms of
|
||||
# the GNU Lesser General Public License version 2 or later.
|
||||
#
|
||||
|
||||
require 'tmail/port'
|
||||
require 'socket'
|
||||
require 'mutex_m'
|
||||
|
||||
|
||||
unless [].respond_to?(:sort_by)
|
||||
module Enumerable#:nodoc:
|
||||
def sort_by
|
||||
map {|i| [yield(i), i] }.sort {|a,b| a.first <=> b.first }.map {|i| i[1] }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
module TMail
|
||||
|
||||
class MhMailbox
|
||||
|
||||
PORT_CLASS = MhPort
|
||||
|
||||
def initialize( dir )
|
||||
edir = File.expand_path(dir)
|
||||
raise ArgumentError, "not directory: #{dir}"\
|
||||
unless FileTest.directory? edir
|
||||
@dirname = edir
|
||||
@last_file = nil
|
||||
@last_atime = nil
|
||||
end
|
||||
|
||||
def directory
|
||||
@dirname
|
||||
end
|
||||
|
||||
alias dirname directory
|
||||
|
||||
attr_accessor :last_atime
|
||||
|
||||
def inspect
|
||||
"#<#{self.class} #{@dirname}>"
|
||||
end
|
||||
|
||||
def close
|
||||
end
|
||||
|
||||
def new_port
|
||||
PORT_CLASS.new(next_file_name())
|
||||
end
|
||||
|
||||
def each_port
|
||||
mail_files().each do |path|
|
||||
yield PORT_CLASS.new(path)
|
||||
end
|
||||
@last_atime = Time.now
|
||||
end
|
||||
|
||||
alias each each_port
|
||||
|
||||
def reverse_each_port
|
||||
mail_files().reverse_each do |path|
|
||||
yield PORT_CLASS.new(path)
|
||||
end
|
||||
@last_atime = Time.now
|
||||
end
|
||||
|
||||
alias reverse_each reverse_each_port
|
||||
|
||||
# old #each_mail returns Port
|
||||
#def each_mail
|
||||
# each_port do |port|
|
||||
# yield Mail.new(port)
|
||||
# end
|
||||
#end
|
||||
|
||||
def each_new_port( mtime = nil, &block )
|
||||
mtime ||= @last_atime
|
||||
return each_port(&block) unless mtime
|
||||
return unless File.mtime(@dirname) >= mtime
|
||||
|
||||
mail_files().each do |path|
|
||||
yield PORT_CLASS.new(path) if File.mtime(path) > mtime
|
||||
end
|
||||
@last_atime = Time.now
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def mail_files
|
||||
Dir.entries(@dirname)\
|
||||
.select {|s| /\A\d+\z/ === s }\
|
||||
.map {|s| s.to_i }\
|
||||
.sort\
|
||||
.map {|i| "#{@dirname}/#{i}" }\
|
||||
.select {|path| FileTest.file? path }
|
||||
end
|
||||
|
||||
def next_file_name
|
||||
unless n = @last_file
|
||||
n = 0
|
||||
Dir.entries(@dirname)\
|
||||
.select {|s| /\A\d+\z/ === s }\
|
||||
.map {|s| s.to_i }.sort\
|
||||
.each do |i|
|
||||
next unless FileTest.file? "#{@dirname}/#{i}"
|
||||
n = i
|
||||
end
|
||||
end
|
||||
begin
|
||||
n += 1
|
||||
end while FileTest.exist? "#{@dirname}/#{n}"
|
||||
@last_file = n
|
||||
|
||||
"#{@dirname}/#{n}"
|
||||
end
|
||||
|
||||
end # MhMailbox
|
||||
|
||||
MhLoader = MhMailbox
|
||||
|
||||
|
||||
class UNIXMbox
|
||||
|
||||
def UNIXMbox.lock( fname )
|
||||
begin
|
||||
f = File.open(fname)
|
||||
f.flock File::LOCK_EX
|
||||
yield f
|
||||
ensure
|
||||
f.flock File::LOCK_UN
|
||||
f.close if f and not f.closed?
|
||||
end
|
||||
end
|
||||
|
||||
class << self
|
||||
alias newobj new
|
||||
end
|
||||
|
||||
def UNIXMbox.new( fname, tmpdir = nil, readonly = false )
|
||||
tmpdir = ENV['TEMP'] || ENV['TMP'] || '/tmp'
|
||||
newobj(fname, "#{tmpdir}/ruby_tmail_#{$$}_#{rand()}", readonly, false)
|
||||
end
|
||||
|
||||
def UNIXMbox.static_new( fname, dir, readonly = false )
|
||||
newobj(fname, dir, readonly, true)
|
||||
end
|
||||
|
||||
def initialize( fname, mhdir, readonly, static )
|
||||
@filename = fname
|
||||
@readonly = readonly
|
||||
@closed = false
|
||||
|
||||
Dir.mkdir mhdir
|
||||
@real = MhMailbox.new(mhdir)
|
||||
@finalizer = UNIXMbox.mkfinal(@real, @filename, !@readonly, !static)
|
||||
ObjectSpace.define_finalizer self, @finalizer
|
||||
end
|
||||
|
||||
def UNIXMbox.mkfinal( mh, mboxfile, writeback_p, cleanup_p )
|
||||
lambda {
|
||||
if writeback_p
|
||||
lock(mboxfile) {|f|
|
||||
mh.each_port do |port|
|
||||
f.puts create_from_line(port)
|
||||
port.ropen {|r|
|
||||
f.puts r.read
|
||||
}
|
||||
end
|
||||
}
|
||||
end
|
||||
if cleanup_p
|
||||
Dir.foreach(mh.dirname) do |fname|
|
||||
next if /\A\.\.?\z/ === fname
|
||||
File.unlink "#{mh.dirname}/#{fname}"
|
||||
end
|
||||
Dir.rmdir mh.dirname
|
||||
end
|
||||
}
|
||||
end
|
||||
|
||||
# make _From line
|
||||
def UNIXMbox.create_from_line( port )
|
||||
sprintf 'From %s %s',
|
||||
fromaddr(), TextUtils.time2str(File.mtime(port.filename))
|
||||
end
|
||||
|
||||
def UNIXMbox.fromaddr
|
||||
h = HeaderField.new_from_port(port, 'Return-Path') ||
|
||||
HeaderField.new_from_port(port, 'From') or return 'nobody'
|
||||
a = h.addrs[0] or return 'nobody'
|
||||
a.spec
|
||||
end
|
||||
private_class_method :fromaddr
|
||||
|
||||
def close
|
||||
return if @closed
|
||||
|
||||
ObjectSpace.undefine_finalizer self
|
||||
@finalizer.call
|
||||
@finalizer = nil
|
||||
@real = nil
|
||||
@closed = true
|
||||
@updated = nil
|
||||
end
|
||||
|
||||
def each_port( &block )
|
||||
close_check
|
||||
update
|
||||
@real.each_port(&block)
|
||||
end
|
||||
|
||||
alias each each_port
|
||||
|
||||
def reverse_each_port( &block )
|
||||
close_check
|
||||
update
|
||||
@real.reverse_each_port(&block)
|
||||
end
|
||||
|
||||
alias reverse_each reverse_each_port
|
||||
|
||||
# old #each_mail returns Port
|
||||
#def each_mail( &block )
|
||||
# each_port do |port|
|
||||
# yield Mail.new(port)
|
||||
# end
|
||||
#end
|
||||
|
||||
def each_new_port( mtime = nil )
|
||||
close_check
|
||||
update
|
||||
@real.each_new_port(mtime) {|p| yield p }
|
||||
end
|
||||
|
||||
def new_port
|
||||
close_check
|
||||
@real.new_port
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def close_check
|
||||
@closed and raise ArgumentError, 'accessing already closed mbox'
|
||||
end
|
||||
|
||||
def update
|
||||
return if FileTest.zero?(@filename)
|
||||
return if @updated and File.mtime(@filename) < @updated
|
||||
w = nil
|
||||
port = nil
|
||||
time = nil
|
||||
UNIXMbox.lock(@filename) {|f|
|
||||
begin
|
||||
f.each do |line|
|
||||
if /\AFrom / === line
|
||||
w.close if w
|
||||
File.utime time, time, port.filename if time
|
||||
|
||||
port = @real.new_port
|
||||
w = port.wopen
|
||||
time = fromline2time(line)
|
||||
else
|
||||
w.print line if w
|
||||
end
|
||||
end
|
||||
ensure
|
||||
if w and not w.closed?
|
||||
w.close
|
||||
File.utime time, time, port.filename if time
|
||||
end
|
||||
end
|
||||
f.truncate(0) unless @readonly
|
||||
@updated = Time.now
|
||||
}
|
||||
end
|
||||
|
||||
def fromline2time( line )
|
||||
m = /\AFrom \S+ \w+ (\w+) (\d+) (\d+):(\d+):(\d+) (\d+)/.match(line) \
|
||||
or return nil
|
||||
Time.local(m[6].to_i, m[1], m[2].to_i, m[3].to_i, m[4].to_i, m[5].to_i)
|
||||
end
|
||||
|
||||
end # UNIXMbox
|
||||
|
||||
MboxLoader = UNIXMbox
|
||||
|
||||
|
||||
class Maildir
|
||||
|
||||
extend Mutex_m
|
||||
|
||||
PORT_CLASS = MaildirPort
|
||||
|
||||
@seq = 0
|
||||
def Maildir.unique_number
|
||||
synchronize {
|
||||
@seq += 1
|
||||
return @seq
|
||||
}
|
||||
end
|
||||
|
||||
def initialize( dir = nil )
|
||||
@dirname = dir || ENV['MAILDIR']
|
||||
raise ArgumentError, "not directory: #{@dirname}"\
|
||||
unless FileTest.directory? @dirname
|
||||
@new = "#{@dirname}/new"
|
||||
@tmp = "#{@dirname}/tmp"
|
||||
@cur = "#{@dirname}/cur"
|
||||
end
|
||||
|
||||
def directory
|
||||
@dirname
|
||||
end
|
||||
|
||||
def inspect
|
||||
"#<#{self.class} #{@dirname}>"
|
||||
end
|
||||
|
||||
def close
|
||||
end
|
||||
|
||||
def each_port
|
||||
mail_files(@cur).each do |path|
|
||||
yield PORT_CLASS.new(path)
|
||||
end
|
||||
end
|
||||
|
||||
alias each each_port
|
||||
|
||||
def reverse_each_port
|
||||
mail_files(@cur).reverse_each do |path|
|
||||
yield PORT_CLASS.new(path)
|
||||
end
|
||||
end
|
||||
|
||||
alias reverse_each reverse_each_port
|
||||
|
||||
def new_port
|
||||
fname = nil
|
||||
tmpfname = nil
|
||||
newfname = nil
|
||||
|
||||
begin
|
||||
fname = "#{Time.now.to_i}.#{$$}_#{Maildir.unique_number}.#{Socket.gethostname}"
|
||||
|
||||
tmpfname = "#{@tmp}/#{fname}"
|
||||
newfname = "#{@new}/#{fname}"
|
||||
end while FileTest.exist? tmpfname
|
||||
|
||||
if block_given?
|
||||
File.open(tmpfname, 'w') {|f| yield f }
|
||||
File.rename tmpfname, newfname
|
||||
PORT_CLASS.new(newfname)
|
||||
else
|
||||
File.open(tmpfname, 'w') {|f| f.write "\n\n" }
|
||||
PORT_CLASS.new(tmpfname)
|
||||
end
|
||||
end
|
||||
|
||||
def each_new_port
|
||||
mail_files(@new).each do |path|
|
||||
dest = @cur + '/' + File.basename(path)
|
||||
File.rename path, dest
|
||||
yield PORT_CLASS.new(dest)
|
||||
end
|
||||
|
||||
check_tmp
|
||||
end
|
||||
|
||||
TOO_OLD = 60 * 60 * 36 # 36 hour
|
||||
|
||||
def check_tmp
|
||||
old = Time.now.to_i - TOO_OLD
|
||||
|
||||
each_filename(@tmp) do |full, fname|
|
||||
if FileTest.file? full and
|
||||
File.stat(full).mtime.to_i < old
|
||||
File.unlink full
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def mail_files( dir )
|
||||
Dir.entries(dir)\
|
||||
.select {|s| s[0] != ?. }\
|
||||
.sort_by {|s| s.slice(/\A\d+/).to_i }\
|
||||
.map {|s| "#{dir}/#{s}" }\
|
||||
.select {|path| FileTest.file? path }
|
||||
end
|
||||
|
||||
def each_filename( dir )
|
||||
Dir.foreach(dir) do |fname|
|
||||
path = "#{dir}/#{fname}"
|
||||
if fname[0] != ?. and FileTest.file? path
|
||||
yield path, fname
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
end # Maildir
|
||||
|
||||
MaildirLoader = Maildir
|
||||
|
||||
end # module TMail
|
|
@ -0,0 +1 @@
|
|||
require 'tmail/mailbox'
|
|
@ -0,0 +1,261 @@
|
|||
#
|
||||
# net.rb
|
||||
#
|
||||
# Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
|
||||
#
|
||||
# This program is free software.
|
||||
# You can distribute/modify this program under the terms of
|
||||
# the GNU Lesser General Public License version 2 or later.
|
||||
#
|
||||
|
||||
require 'nkf'
|
||||
|
||||
|
||||
module TMail
|
||||
|
||||
class Mail
|
||||
|
||||
def send_to( smtp )
|
||||
do_send_to(smtp) do
|
||||
ready_to_send
|
||||
end
|
||||
end
|
||||
|
||||
def send_text_to( smtp )
|
||||
do_send_to(smtp) do
|
||||
ready_to_send
|
||||
mime_encode
|
||||
end
|
||||
end
|
||||
|
||||
def do_send_to( smtp )
|
||||
from = from_address or raise ArgumentError, 'no from address'
|
||||
(dests = destinations).empty? and raise ArgumentError, 'no receipient'
|
||||
yield
|
||||
send_to_0 smtp, from, dests
|
||||
end
|
||||
private :do_send_to
|
||||
|
||||
def send_to_0( smtp, from, to )
|
||||
smtp.ready(from, to) do |f|
|
||||
encoded "\r\n", 'j', f, ''
|
||||
end
|
||||
end
|
||||
|
||||
def ready_to_send
|
||||
delete_no_send_fields
|
||||
add_message_id
|
||||
add_date
|
||||
end
|
||||
|
||||
NOSEND_FIELDS = %w(
|
||||
received
|
||||
bcc
|
||||
)
|
||||
|
||||
def delete_no_send_fields
|
||||
NOSEND_FIELDS.each do |nm|
|
||||
delete nm
|
||||
end
|
||||
delete_if {|n,v| v.empty? }
|
||||
end
|
||||
|
||||
def add_message_id( fqdn = nil )
|
||||
self.message_id = ::TMail::new_msgid(fqdn)
|
||||
end
|
||||
|
||||
def add_date
|
||||
self.date = Time.now
|
||||
end
|
||||
|
||||
def mime_encode
|
||||
if parts.empty?
|
||||
mime_encode_singlepart
|
||||
else
|
||||
mime_encode_multipart true
|
||||
end
|
||||
end
|
||||
|
||||
def mime_encode_singlepart
|
||||
self.mime_version = '1.0'
|
||||
b = body
|
||||
if NKF.guess(b) != NKF::BINARY
|
||||
mime_encode_text b
|
||||
else
|
||||
mime_encode_binary b
|
||||
end
|
||||
end
|
||||
|
||||
def mime_encode_text( body )
|
||||
self.body = NKF.nkf('-j -m0', body)
|
||||
self.set_content_type 'text', 'plain', {'charset' => 'iso-2022-jp'}
|
||||
self.encoding = '7bit'
|
||||
end
|
||||
|
||||
def mime_encode_binary( body )
|
||||
self.body = [body].pack('m')
|
||||
self.set_content_type 'application', 'octet-stream'
|
||||
self.encoding = 'Base64'
|
||||
end
|
||||
|
||||
def mime_encode_multipart( top = true )
|
||||
self.mime_version = '1.0' if top
|
||||
self.set_content_type 'multipart', 'mixed'
|
||||
e = encoding(nil)
|
||||
if e and not /\A(?:7bit|8bit|binary)\z/i === e
|
||||
raise ArgumentError,
|
||||
'using C.T.Encoding with multipart mail is not permitted'
|
||||
end
|
||||
end
|
||||
|
||||
def create_empty_mail
|
||||
self.class.new(StringPort.new(''), @config)
|
||||
end
|
||||
|
||||
def create_reply
|
||||
setup_reply create_empty_mail()
|
||||
end
|
||||
|
||||
def setup_reply( m )
|
||||
if tmp = reply_addresses(nil)
|
||||
m.to_addrs = tmp
|
||||
end
|
||||
|
||||
mid = message_id(nil)
|
||||
tmp = references(nil) || []
|
||||
tmp.push mid if mid
|
||||
m.in_reply_to = [mid] if mid
|
||||
m.references = tmp unless tmp.empty?
|
||||
m.subject = 'Re: ' + subject('').sub(/\A(?:\s*re:)+/i, '')
|
||||
|
||||
m
|
||||
end
|
||||
|
||||
def create_forward
|
||||
setup_forward create_empty_mail()
|
||||
end
|
||||
|
||||
def setup_forward( mail )
|
||||
m = Mail.new(StringPort.new(''))
|
||||
m.body = decoded
|
||||
m.set_content_type 'message', 'rfc822'
|
||||
m.encoding = encoding('7bit')
|
||||
mail.parts.push m
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
class DeleteFields
|
||||
|
||||
NOSEND_FIELDS = %w(
|
||||
received
|
||||
bcc
|
||||
)
|
||||
|
||||
def initialize( nosend = nil, delempty = true )
|
||||
@no_send_fields = nosend || NOSEND_FIELDS.dup
|
||||
@delete_empty_fields = delempty
|
||||
end
|
||||
|
||||
attr :no_send_fields
|
||||
attr :delete_empty_fields, true
|
||||
|
||||
def exec( mail )
|
||||
@no_send_fields.each do |nm|
|
||||
delete nm
|
||||
end
|
||||
delete_if {|n,v| v.empty? } if @delete_empty_fields
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
class AddMessageId
|
||||
|
||||
def initialize( fqdn = nil )
|
||||
@fqdn = fqdn
|
||||
end
|
||||
|
||||
attr :fqdn, true
|
||||
|
||||
def exec( mail )
|
||||
mail.message_id = ::TMail::new_msgid(@fqdn)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
class AddDate
|
||||
|
||||
def exec( mail )
|
||||
mail.date = Time.now
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
class MimeEncodeAuto
|
||||
|
||||
def initialize( s = nil, m = nil )
|
||||
@singlepart_composer = s || MimeEncodeSingle.new
|
||||
@multipart_composer = m || MimeEncodeMulti.new
|
||||
end
|
||||
|
||||
attr :singlepart_composer
|
||||
attr :multipart_composer
|
||||
|
||||
def exec( mail )
|
||||
if mail._builtin_multipart?
|
||||
then @multipart_composer
|
||||
else @singlepart_composer end.exec mail
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
class MimeEncodeSingle
|
||||
|
||||
def exec( mail )
|
||||
mail.mime_version = '1.0'
|
||||
b = mail.body
|
||||
if NKF.guess(b) != NKF::BINARY
|
||||
on_text b
|
||||
else
|
||||
on_binary b
|
||||
end
|
||||
end
|
||||
|
||||
def on_text( body )
|
||||
mail.body = NKF.nkf('-j -m0', body)
|
||||
mail.set_content_type 'text', 'plain', {'charset' => 'iso-2022-jp'}
|
||||
mail.encoding = '7bit'
|
||||
end
|
||||
|
||||
def on_binary( body )
|
||||
mail.body = [body].pack('m')
|
||||
mail.set_content_type 'application', 'octet-stream'
|
||||
mail.encoding = 'Base64'
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
class MimeEncodeMulti
|
||||
|
||||
def exec( mail, top = true )
|
||||
mail.mime_version = '1.0' if top
|
||||
mail.set_content_type 'multipart', 'mixed'
|
||||
e = encoding(nil)
|
||||
if e and not /\A(?:7bit|8bit|binary)\z/i === e
|
||||
raise ArgumentError,
|
||||
'using C.T.Encoding with multipart mail is not permitted'
|
||||
end
|
||||
mail.parts.each do |m|
|
||||
exec m, false if m._builtin_multipart?
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end # module TMail
|
|
@ -0,0 +1,116 @@
|
|||
#
|
||||
# obsolete.rb
|
||||
#
|
||||
# Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
|
||||
#
|
||||
# This program is free software.
|
||||
# You can distribute/modify this program under the terms of
|
||||
# the GNU Lesser General Public License version 2 or later.
|
||||
#
|
||||
|
||||
module TMail
|
||||
|
||||
# mail.rb
|
||||
class Mail
|
||||
alias include? key?
|
||||
alias has_key? key?
|
||||
|
||||
def values
|
||||
ret = []
|
||||
each_field {|v| ret.push v }
|
||||
ret
|
||||
end
|
||||
|
||||
def value?( val )
|
||||
HeaderField === val or return false
|
||||
|
||||
[ @header[val.name.downcase] ].flatten.include? val
|
||||
end
|
||||
|
||||
alias has_value? value?
|
||||
end
|
||||
|
||||
|
||||
# facade.rb
|
||||
class Mail
|
||||
def from_addr( default = nil )
|
||||
addr, = from_addrs(nil)
|
||||
addr || default
|
||||
end
|
||||
|
||||
def from_address( default = nil )
|
||||
if a = from_addr(nil)
|
||||
a.spec
|
||||
else
|
||||
default
|
||||
end
|
||||
end
|
||||
|
||||
alias from_address= from_addrs=
|
||||
|
||||
def from_phrase( default = nil )
|
||||
if a = from_addr(nil)
|
||||
a.phrase
|
||||
else
|
||||
default
|
||||
end
|
||||
end
|
||||
|
||||
alias msgid message_id
|
||||
alias msgid= message_id=
|
||||
|
||||
alias each_dest each_destination
|
||||
end
|
||||
|
||||
|
||||
# address.rb
|
||||
class Address
|
||||
alias route routes
|
||||
alias addr spec
|
||||
|
||||
def spec=( str )
|
||||
@local, @domain = str.split(/@/,2).map {|s| s.split(/\./) }
|
||||
end
|
||||
|
||||
alias addr= spec=
|
||||
alias address= spec=
|
||||
end
|
||||
|
||||
|
||||
# mbox.rb
|
||||
class MhMailbox
|
||||
alias new_mail new_port
|
||||
alias each_mail each_port
|
||||
alias each_newmail each_new_port
|
||||
end
|
||||
class UNIXMbox
|
||||
alias new_mail new_port
|
||||
alias each_mail each_port
|
||||
alias each_newmail each_new_port
|
||||
end
|
||||
class Maildir
|
||||
alias new_mail new_port
|
||||
alias each_mail each_port
|
||||
alias each_newmail each_new_port
|
||||
end
|
||||
|
||||
|
||||
# utils.rb
|
||||
extend TextUtils
|
||||
|
||||
class << self
|
||||
alias msgid? message_id?
|
||||
alias boundary new_boundary
|
||||
alias msgid new_message_id
|
||||
alias new_msgid new_message_id
|
||||
end
|
||||
|
||||
def Mail.boundary
|
||||
::TMail.new_boundary
|
||||
end
|
||||
|
||||
def Mail.msgid
|
||||
::TMail.new_message_id
|
||||
end
|
||||
|
||||
end # module TMail
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,358 @@
|
|||
#
|
||||
# port.rb
|
||||
#
|
||||
# Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
|
||||
#
|
||||
# This program is free software.
|
||||
# You can distribute/modify this program under the terms of
|
||||
# the GNU Lesser General Public License version 2 or later.
|
||||
#
|
||||
|
||||
require 'tmail/stringio'
|
||||
|
||||
|
||||
module TMail
|
||||
|
||||
class Port
|
||||
def reproducible?
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
###
|
||||
### FilePort
|
||||
###
|
||||
|
||||
class FilePort < Port
|
||||
|
||||
def initialize( fname )
|
||||
@filename = File.expand_path(fname)
|
||||
super()
|
||||
end
|
||||
|
||||
attr_reader :filename
|
||||
|
||||
alias ident filename
|
||||
|
||||
def ==( other )
|
||||
other.respond_to?(:filename) and @filename == other.filename
|
||||
end
|
||||
|
||||
alias eql? ==
|
||||
|
||||
def hash
|
||||
@filename.hash
|
||||
end
|
||||
|
||||
def inspect
|
||||
"#<#{self.class}:#{@filename}>"
|
||||
end
|
||||
|
||||
def reproducible?
|
||||
true
|
||||
end
|
||||
|
||||
def size
|
||||
File.size @filename
|
||||
end
|
||||
|
||||
|
||||
def ropen( &block )
|
||||
File.open(@filename, &block)
|
||||
end
|
||||
|
||||
def wopen( &block )
|
||||
File.open(@filename, 'w', &block)
|
||||
end
|
||||
|
||||
def aopen( &block )
|
||||
File.open(@filename, 'a', &block)
|
||||
end
|
||||
|
||||
|
||||
def read_all
|
||||
ropen {|f|
|
||||
return f.read
|
||||
}
|
||||
end
|
||||
|
||||
|
||||
def remove
|
||||
File.unlink @filename
|
||||
end
|
||||
|
||||
def move_to( port )
|
||||
begin
|
||||
File.link @filename, port.filename
|
||||
rescue Errno::EXDEV
|
||||
copy_to port
|
||||
end
|
||||
File.unlink @filename
|
||||
end
|
||||
|
||||
alias mv move_to
|
||||
|
||||
def copy_to( port )
|
||||
if FilePort === port
|
||||
copy_file @filename, port.filename
|
||||
else
|
||||
File.open(@filename) {|r|
|
||||
port.wopen {|w|
|
||||
while s = r.sysread(4096)
|
||||
w.write << s
|
||||
end
|
||||
} }
|
||||
end
|
||||
end
|
||||
|
||||
alias cp copy_to
|
||||
|
||||
private
|
||||
|
||||
# from fileutils.rb
|
||||
def copy_file( src, dest )
|
||||
st = r = w = nil
|
||||
|
||||
File.open(src, 'rb') {|r|
|
||||
File.open(dest, 'wb') {|w|
|
||||
st = r.stat
|
||||
begin
|
||||
while true
|
||||
w.write r.sysread(st.blksize)
|
||||
end
|
||||
rescue EOFError
|
||||
end
|
||||
} }
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
module MailFlags
|
||||
|
||||
def seen=( b )
|
||||
set_status 'S', b
|
||||
end
|
||||
|
||||
def seen?
|
||||
get_status 'S'
|
||||
end
|
||||
|
||||
def replied=( b )
|
||||
set_status 'R', b
|
||||
end
|
||||
|
||||
def replied?
|
||||
get_status 'R'
|
||||
end
|
||||
|
||||
def flagged=( b )
|
||||
set_status 'F', b
|
||||
end
|
||||
|
||||
def flagged?
|
||||
get_status 'F'
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def procinfostr( str, tag, true_p )
|
||||
a = str.upcase.split(//)
|
||||
a.push true_p ? tag : nil
|
||||
a.delete tag unless true_p
|
||||
a.compact.sort.join('').squeeze
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
class MhPort < FilePort
|
||||
|
||||
include MailFlags
|
||||
|
||||
private
|
||||
|
||||
def set_status( tag, flag )
|
||||
begin
|
||||
tmpfile = @filename + '.tmailtmp.' + $$.to_s
|
||||
File.open(tmpfile, 'w') {|f|
|
||||
write_status f, tag, flag
|
||||
}
|
||||
File.unlink @filename
|
||||
File.link tmpfile, @filename
|
||||
ensure
|
||||
File.unlink tmpfile
|
||||
end
|
||||
end
|
||||
|
||||
def write_status( f, tag, flag )
|
||||
stat = ''
|
||||
File.open(@filename) {|r|
|
||||
while line = r.gets
|
||||
if line.strip.empty?
|
||||
break
|
||||
elsif m = /\AX-TMail-Status:/i.match(line)
|
||||
stat = m.post_match.strip
|
||||
else
|
||||
f.print line
|
||||
end
|
||||
end
|
||||
|
||||
s = procinfostr(stat, tag, flag)
|
||||
f.puts 'X-TMail-Status: ' + s unless s.empty?
|
||||
f.puts
|
||||
|
||||
while s = r.read(2048)
|
||||
f.write s
|
||||
end
|
||||
}
|
||||
end
|
||||
|
||||
def get_status( tag )
|
||||
File.foreach(@filename) {|line|
|
||||
return false if line.strip.empty?
|
||||
if m = /\AX-TMail-Status:/i.match(line)
|
||||
return m.post_match.strip.include?(tag[0])
|
||||
end
|
||||
}
|
||||
false
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
class MaildirPort < FilePort
|
||||
|
||||
def move_to_new
|
||||
new = replace_dir(@filename, 'new')
|
||||
File.rename @filename, new
|
||||
@filename = new
|
||||
end
|
||||
|
||||
def move_to_cur
|
||||
new = replace_dir(@filename, 'cur')
|
||||
File.rename @filename, new
|
||||
@filename = new
|
||||
end
|
||||
|
||||
def replace_dir( path, dir )
|
||||
"#{File.dirname File.dirname(path)}/#{dir}/#{File.basename path}"
|
||||
end
|
||||
private :replace_dir
|
||||
|
||||
|
||||
include MailFlags
|
||||
|
||||
private
|
||||
|
||||
MAIL_FILE = /\A(\d+\.[\d_]+\.[^:]+)(?:\:(\d),(\w+)?)?\z/
|
||||
|
||||
def set_status( tag, flag )
|
||||
if m = MAIL_FILE.match(File.basename(@filename))
|
||||
s, uniq, type, info, = m.to_a
|
||||
return if type and type != '2' # do not change anything
|
||||
newname = File.dirname(@filename) + '/' +
|
||||
uniq + ':2,' + procinfostr(info.to_s, tag, flag)
|
||||
else
|
||||
newname = @filename + ':2,' + tag
|
||||
end
|
||||
|
||||
File.link @filename, newname
|
||||
File.unlink @filename
|
||||
@filename = newname
|
||||
end
|
||||
|
||||
def get_status( tag )
|
||||
m = MAIL_FILE.match(File.basename(@filename)) or return false
|
||||
m[2] == '2' and m[3].to_s.include?(tag[0])
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
###
|
||||
### StringPort
|
||||
###
|
||||
|
||||
class StringPort < Port
|
||||
|
||||
def initialize( str = '' )
|
||||
@buffer = str
|
||||
super()
|
||||
end
|
||||
|
||||
def string
|
||||
@buffer
|
||||
end
|
||||
|
||||
def to_s
|
||||
@buffer.dup
|
||||
end
|
||||
|
||||
alias read_all to_s
|
||||
|
||||
def size
|
||||
@buffer.size
|
||||
end
|
||||
|
||||
def ==( other )
|
||||
StringPort === other and @buffer.equal? other.string
|
||||
end
|
||||
|
||||
alias eql? ==
|
||||
|
||||
def hash
|
||||
@buffer.id.hash
|
||||
end
|
||||
|
||||
def inspect
|
||||
"#<#{self.class}:id=#{sprintf '0x%x', @buffer.id}>"
|
||||
end
|
||||
|
||||
def reproducible?
|
||||
true
|
||||
end
|
||||
|
||||
def ropen( &block )
|
||||
@buffer or raise Errno::ENOENT, "#{inspect} is already removed"
|
||||
StringInput.open(@buffer, &block)
|
||||
end
|
||||
|
||||
def wopen( &block )
|
||||
@buffer = ''
|
||||
StringOutput.new(@buffer, &block)
|
||||
end
|
||||
|
||||
def aopen( &block )
|
||||
@buffer ||= ''
|
||||
StringOutput.new(@buffer, &block)
|
||||
end
|
||||
|
||||
def remove
|
||||
@buffer = nil
|
||||
end
|
||||
|
||||
alias rm remove
|
||||
|
||||
def copy_to( port )
|
||||
port.wopen {|f|
|
||||
f.write @buffer
|
||||
}
|
||||
end
|
||||
|
||||
alias cp copy_to
|
||||
|
||||
def move_to( port )
|
||||
if StringPort === port
|
||||
str = @buffer
|
||||
port.instance_eval { @buffer = str }
|
||||
else
|
||||
copy_to port
|
||||
end
|
||||
remove
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end # module TMail
|
|
@ -0,0 +1,22 @@
|
|||
#
|
||||
# scanner.rb
|
||||
#
|
||||
# Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
|
||||
#
|
||||
# This program is free software.
|
||||
# You can distribute/modify this program under the terms of
|
||||
# the GNU Lesser General Public License version 2 or later.
|
||||
#
|
||||
|
||||
require 'tmail/utils'
|
||||
|
||||
module TMail
|
||||
require 'tmail/scanner_r.rb'
|
||||
begin
|
||||
raise LoadError, 'Turn off Ruby extention by user choice' if ENV['NORUBYEXT']
|
||||
require 'tmail/scanner_c.so'
|
||||
Scanner = Scanner_C
|
||||
rescue LoadError
|
||||
Scanner = Scanner_R
|
||||
end
|
||||
end
|
|
@ -0,0 +1,244 @@
|
|||
#
|
||||
# scanner_r.rb
|
||||
#
|
||||
# Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
|
||||
#
|
||||
# This program is free software.
|
||||
# You can distribute/modify this program under the terms of
|
||||
# the GNU Lesser General Public License version 2 or later.
|
||||
#
|
||||
|
||||
require 'tmail/config'
|
||||
|
||||
|
||||
module TMail
|
||||
|
||||
class Scanner_R
|
||||
|
||||
Version = '0.10.7'
|
||||
Version.freeze
|
||||
|
||||
MIME_HEADERS = {
|
||||
:CTYPE => true,
|
||||
:CENCODING => true,
|
||||
:CDISPOSITION => true
|
||||
}
|
||||
|
||||
alnum = 'a-zA-Z0-9'
|
||||
atomsyms = %q[ _#!$%&`'*+-{|}~^/=? ].strip
|
||||
tokensyms = %q[ _#!$%&`'*+-{|}~^. ].strip
|
||||
|
||||
atomchars = alnum + Regexp.quote(atomsyms)
|
||||
tokenchars = alnum + Regexp.quote(tokensyms)
|
||||
iso2022str = '\e(?!\(B)..(?:[^\e]+|\e(?!\(B)..)*\e\(B'
|
||||
|
||||
eucstr = '(?:[\xa1-\xfe][\xa1-\xfe])+'
|
||||
sjisstr = '(?:[\x81-\x9f\xe0-\xef][\x40-\x7e\x80-\xfc])+'
|
||||
utf8str = '(?:[\xc0-\xdf][\x80-\xbf]|[\xe0-\xef][\x80-\xbf][\x80-\xbf])+'
|
||||
|
||||
quoted_with_iso2022 = /\A(?:[^\\\e"]+|#{iso2022str})+/n
|
||||
domlit_with_iso2022 = /\A(?:[^\\\e\]]+|#{iso2022str})+/n
|
||||
comment_with_iso2022 = /\A(?:[^\\\e()]+|#{iso2022str})+/n
|
||||
|
||||
quoted_without_iso2022 = /\A[^\\"]+/n
|
||||
domlit_without_iso2022 = /\A[^\\\]]+/n
|
||||
comment_without_iso2022 = /\A[^\\()]+/n
|
||||
|
||||
PATTERN_TABLE = {}
|
||||
PATTERN_TABLE['EUC'] =
|
||||
[
|
||||
/\A(?:[#{atomchars}]+|#{iso2022str}|#{eucstr})+/n,
|
||||
/\A(?:[#{tokenchars}]+|#{iso2022str}|#{eucstr})+/n,
|
||||
quoted_with_iso2022,
|
||||
domlit_with_iso2022,
|
||||
comment_with_iso2022
|
||||
]
|
||||
PATTERN_TABLE['SJIS'] =
|
||||
[
|
||||
/\A(?:[#{atomchars}]+|#{iso2022str}|#{sjisstr})+/n,
|
||||
/\A(?:[#{tokenchars}]+|#{iso2022str}|#{sjisstr})+/n,
|
||||
quoted_with_iso2022,
|
||||
domlit_with_iso2022,
|
||||
comment_with_iso2022
|
||||
]
|
||||
PATTERN_TABLE['UTF8'] =
|
||||
[
|
||||
/\A(?:[#{atomchars}]+|#{utf8str})+/n,
|
||||
/\A(?:[#{tokenchars}]+|#{utf8str})+/n,
|
||||
quoted_without_iso2022,
|
||||
domlit_without_iso2022,
|
||||
comment_without_iso2022
|
||||
]
|
||||
PATTERN_TABLE['NONE'] =
|
||||
[
|
||||
/\A[#{atomchars}]+/n,
|
||||
/\A[#{tokenchars}]+/n,
|
||||
quoted_without_iso2022,
|
||||
domlit_without_iso2022,
|
||||
comment_without_iso2022
|
||||
]
|
||||
|
||||
|
||||
def initialize( str, scantype, comments )
|
||||
init_scanner str
|
||||
@comments = comments || []
|
||||
@debug = false
|
||||
|
||||
# fix scanner mode
|
||||
@received = (scantype == :RECEIVED)
|
||||
@is_mime_header = MIME_HEADERS[scantype]
|
||||
|
||||
atom, token, @quoted_re, @domlit_re, @comment_re = PATTERN_TABLE[$KCODE]
|
||||
@word_re = (MIME_HEADERS[scantype] ? token : atom)
|
||||
end
|
||||
|
||||
attr_accessor :debug
|
||||
|
||||
def scan( &block )
|
||||
if @debug
|
||||
scan_main do |arr|
|
||||
s, v = arr
|
||||
printf "%7d %-10s %s\n",
|
||||
rest_size(),
|
||||
s.respond_to?(:id2name) ? s.id2name : s.inspect,
|
||||
v.inspect
|
||||
yield arr
|
||||
end
|
||||
else
|
||||
scan_main(&block)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
RECV_TOKEN = {
|
||||
'from' => :FROM,
|
||||
'by' => :BY,
|
||||
'via' => :VIA,
|
||||
'with' => :WITH,
|
||||
'id' => :ID,
|
||||
'for' => :FOR
|
||||
}
|
||||
|
||||
def scan_main
|
||||
until eof?
|
||||
if skip(/\A[\n\r\t ]+/n) # LWSP
|
||||
break if eof?
|
||||
end
|
||||
|
||||
if s = readstr(@word_re)
|
||||
if @is_mime_header
|
||||
yield :TOKEN, s
|
||||
else
|
||||
# atom
|
||||
if /\A\d+\z/ === s
|
||||
yield :DIGIT, s
|
||||
elsif @received
|
||||
yield RECV_TOKEN[s.downcase] || :ATOM, s
|
||||
else
|
||||
yield :ATOM, s
|
||||
end
|
||||
end
|
||||
|
||||
elsif skip(/\A"/)
|
||||
yield :QUOTED, scan_quoted_word()
|
||||
|
||||
elsif skip(/\A\[/)
|
||||
yield :DOMLIT, scan_domain_literal()
|
||||
|
||||
elsif skip(/\A\(/)
|
||||
@comments.push scan_comment()
|
||||
|
||||
else
|
||||
c = readchar()
|
||||
yield c, c
|
||||
end
|
||||
end
|
||||
|
||||
yield false, '$'
|
||||
end
|
||||
|
||||
def scan_quoted_word
|
||||
scan_qstr(@quoted_re, /\A"/, 'quoted-word')
|
||||
end
|
||||
|
||||
def scan_domain_literal
|
||||
'[' + scan_qstr(@domlit_re, /\A\]/, 'domain-literal') + ']'
|
||||
end
|
||||
|
||||
def scan_qstr( pattern, terminal, type )
|
||||
result = ''
|
||||
until eof?
|
||||
if s = readstr(pattern) then result << s
|
||||
elsif skip(terminal) then return result
|
||||
elsif skip(/\A\\/) then result << readchar()
|
||||
else
|
||||
raise "TMail FATAL: not match in #{type}"
|
||||
end
|
||||
end
|
||||
scan_error! "found unterminated #{type}"
|
||||
end
|
||||
|
||||
def scan_comment
|
||||
result = ''
|
||||
nest = 1
|
||||
content = @comment_re
|
||||
|
||||
until eof?
|
||||
if s = readstr(content) then result << s
|
||||
elsif skip(/\A\)/) then nest -= 1
|
||||
return result if nest == 0
|
||||
result << ')'
|
||||
elsif skip(/\A\(/) then nest += 1
|
||||
result << '('
|
||||
elsif skip(/\A\\/) then result << readchar()
|
||||
else
|
||||
raise 'TMail FATAL: not match in comment'
|
||||
end
|
||||
end
|
||||
scan_error! 'found unterminated comment'
|
||||
end
|
||||
|
||||
# string scanner
|
||||
|
||||
def init_scanner( str )
|
||||
@src = str
|
||||
end
|
||||
|
||||
def eof?
|
||||
@src.empty?
|
||||
end
|
||||
|
||||
def rest_size
|
||||
@src.size
|
||||
end
|
||||
|
||||
def readstr( re )
|
||||
if m = re.match(@src)
|
||||
@src = m.post_match
|
||||
m[0]
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
def readchar
|
||||
readstr(/\A./)
|
||||
end
|
||||
|
||||
def skip( re )
|
||||
if m = re.match(@src)
|
||||
@src = m.post_match
|
||||
true
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
def scan_error!( msg )
|
||||
raise SyntaxError, msg
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end # module TMail
|
|
@ -0,0 +1,260 @@
|
|||
#
|
||||
# stringio.rb
|
||||
#
|
||||
# Copyright (c) 1999-2003 Minero Aoki <aamine@loveruby.net>
|
||||
#
|
||||
# This program is free software.
|
||||
# You can distribute/modify this program under the terms of
|
||||
# the GNU Lesser General Public License version 2 or later.
|
||||
#
|
||||
# Id: stringio.rb,v 1.10 2003/04/27 22:02:14 aamine Exp
|
||||
#
|
||||
|
||||
class StringInput#:nodoc:
|
||||
|
||||
include Enumerable
|
||||
|
||||
class << self
|
||||
|
||||
def new( str )
|
||||
if block_given?
|
||||
begin
|
||||
f = super
|
||||
yield f
|
||||
ensure
|
||||
f.close if f
|
||||
end
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
alias open new
|
||||
|
||||
end
|
||||
|
||||
def initialize( str )
|
||||
@src = str
|
||||
@pos = 0
|
||||
@closed = false
|
||||
@lineno = 0
|
||||
end
|
||||
|
||||
attr_reader :lineno
|
||||
|
||||
def string
|
||||
@src
|
||||
end
|
||||
|
||||
def inspect
|
||||
"#<#{self.class}:#{@closed ? 'closed' : 'open'},src=#{@src[0,30].inspect}>"
|
||||
end
|
||||
|
||||
def close
|
||||
stream_check!
|
||||
@pos = nil
|
||||
@closed = true
|
||||
end
|
||||
|
||||
def closed?
|
||||
@closed
|
||||
end
|
||||
|
||||
def pos
|
||||
stream_check!
|
||||
[@pos, @src.size].min
|
||||
end
|
||||
|
||||
alias tell pos
|
||||
|
||||
def seek( offset, whence = IO::SEEK_SET )
|
||||
stream_check!
|
||||
case whence
|
||||
when IO::SEEK_SET
|
||||
@pos = offset
|
||||
when IO::SEEK_CUR
|
||||
@pos += offset
|
||||
when IO::SEEK_END
|
||||
@pos = @src.size - offset
|
||||
else
|
||||
raise ArgumentError, "unknown seek flag: #{whence}"
|
||||
end
|
||||
@pos = 0 if @pos < 0
|
||||
@pos = [@pos, @src.size + 1].min
|
||||
offset
|
||||
end
|
||||
|
||||
def rewind
|
||||
stream_check!
|
||||
@pos = 0
|
||||
end
|
||||
|
||||
def eof?
|
||||
stream_check!
|
||||
@pos > @src.size
|
||||
end
|
||||
|
||||
def each( &block )
|
||||
stream_check!
|
||||
begin
|
||||
@src.each(&block)
|
||||
ensure
|
||||
@pos = 0
|
||||
end
|
||||
end
|
||||
|
||||
def gets
|
||||
stream_check!
|
||||
if idx = @src.index(?\n, @pos)
|
||||
idx += 1 # "\n".size
|
||||
line = @src[ @pos ... idx ]
|
||||
@pos = idx
|
||||
@pos += 1 if @pos == @src.size
|
||||
else
|
||||
line = @src[ @pos .. -1 ]
|
||||
@pos = @src.size + 1
|
||||
end
|
||||
@lineno += 1
|
||||
|
||||
line
|
||||
end
|
||||
|
||||
def getc
|
||||
stream_check!
|
||||
ch = @src[@pos]
|
||||
@pos += 1
|
||||
@pos += 1 if @pos == @src.size
|
||||
ch
|
||||
end
|
||||
|
||||
def read( len = nil )
|
||||
stream_check!
|
||||
return read_all unless len
|
||||
str = @src[@pos, len]
|
||||
@pos += len
|
||||
@pos += 1 if @pos == @src.size
|
||||
str
|
||||
end
|
||||
|
||||
alias sysread read
|
||||
|
||||
def read_all
|
||||
stream_check!
|
||||
return nil if eof?
|
||||
rest = @src[@pos ... @src.size]
|
||||
@pos = @src.size + 1
|
||||
rest
|
||||
end
|
||||
|
||||
def stream_check!
|
||||
@closed and raise IOError, 'closed stream'
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
|
||||
class StringOutput#:nodoc:
|
||||
|
||||
class << self
|
||||
|
||||
def new( str = '' )
|
||||
if block_given?
|
||||
begin
|
||||
f = super
|
||||
yield f
|
||||
ensure
|
||||
f.close if f
|
||||
end
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
alias open new
|
||||
|
||||
end
|
||||
|
||||
def initialize( str = '' )
|
||||
@dest = str
|
||||
@closed = false
|
||||
end
|
||||
|
||||
def close
|
||||
@closed = true
|
||||
end
|
||||
|
||||
def closed?
|
||||
@closed
|
||||
end
|
||||
|
||||
def string
|
||||
@dest
|
||||
end
|
||||
|
||||
alias value string
|
||||
alias to_str string
|
||||
|
||||
def size
|
||||
@dest.size
|
||||
end
|
||||
|
||||
alias pos size
|
||||
|
||||
def inspect
|
||||
"#<#{self.class}:#{@dest ? 'open' : 'closed'},#{id}>"
|
||||
end
|
||||
|
||||
def print( *args )
|
||||
stream_check!
|
||||
raise ArgumentError, 'wrong # of argument (0 for >1)' if args.empty?
|
||||
args.each do |s|
|
||||
raise ArgumentError, 'nil not allowed' if s.nil?
|
||||
@dest << s.to_s
|
||||
end
|
||||
nil
|
||||
end
|
||||
|
||||
def puts( *args )
|
||||
stream_check!
|
||||
args.each do |str|
|
||||
@dest << (s = str.to_s)
|
||||
@dest << "\n" unless s[-1] == ?\n
|
||||
end
|
||||
@dest << "\n" if args.empty?
|
||||
nil
|
||||
end
|
||||
|
||||
def putc( ch )
|
||||
stream_check!
|
||||
@dest << ch.chr
|
||||
nil
|
||||
end
|
||||
|
||||
def printf( *args )
|
||||
stream_check!
|
||||
@dest << sprintf(*args)
|
||||
nil
|
||||
end
|
||||
|
||||
def write( str )
|
||||
stream_check!
|
||||
s = str.to_s
|
||||
@dest << s
|
||||
s.size
|
||||
end
|
||||
|
||||
alias syswrite write
|
||||
|
||||
def <<( str )
|
||||
stream_check!
|
||||
@dest << str.to_s
|
||||
self
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def stream_check!
|
||||
@closed and raise IOError, 'closed stream'
|
||||
end
|
||||
|
||||
end
|
|
@ -0,0 +1 @@
|
|||
require 'tmail'
|
|
@ -0,0 +1,215 @@
|
|||
#
|
||||
# utils.rb
|
||||
#
|
||||
# Copyright (c) 1998-2003 Minero Aoki <aamine@loveruby.net>
|
||||
#
|
||||
# This program is free software.
|
||||
# You can distribute/modify this program under the terms of
|
||||
# the GNU Lesser General Public License version 2 or later.
|
||||
#
|
||||
|
||||
module TMail
|
||||
|
||||
class SyntaxError < StandardError; end
|
||||
|
||||
|
||||
def TMail.new_boundary
|
||||
'mimepart_' + random_tag
|
||||
end
|
||||
|
||||
def TMail.new_message_id( fqdn = nil )
|
||||
fqdn ||= ::Socket.gethostname
|
||||
"<#{random_tag()}@#{fqdn}.tmail>"
|
||||
end
|
||||
|
||||
def TMail.random_tag
|
||||
@uniq += 1
|
||||
t = Time.now
|
||||
sprintf('%x%x_%x%x%d%x',
|
||||
t.to_i, t.tv_usec,
|
||||
$$, Thread.current.id, @uniq, rand(255))
|
||||
end
|
||||
private_class_method :random_tag
|
||||
|
||||
@uniq = 0
|
||||
|
||||
|
||||
module TextUtils
|
||||
|
||||
aspecial = '()<>[]:;.@\\,"'
|
||||
tspecial = '()<>[];:@\\,"/?='
|
||||
lwsp = " \t\r\n"
|
||||
control = '\x00-\x1f\x7f-\xff'
|
||||
|
||||
ATOM_UNSAFE = /[#{Regexp.quote aspecial}#{control}#{lwsp}]/n
|
||||
PHRASE_UNSAFE = /[#{Regexp.quote aspecial}#{control}]/n
|
||||
TOKEN_UNSAFE = /[#{Regexp.quote tspecial}#{control}#{lwsp}]/n
|
||||
CONTROL_CHAR = /[#{control}]/n
|
||||
|
||||
def atom_safe?( str )
|
||||
not ATOM_UNSAFE === str
|
||||
end
|
||||
|
||||
def quote_atom( str )
|
||||
(ATOM_UNSAFE === str) ? dquote(str) : str
|
||||
end
|
||||
|
||||
def quote_phrase( str )
|
||||
(PHRASE_UNSAFE === str) ? dquote(str) : str
|
||||
end
|
||||
|
||||
def token_safe?( str )
|
||||
not TOKEN_UNSAFE === str
|
||||
end
|
||||
|
||||
def quote_token( str )
|
||||
(TOKEN_UNSAFE === str) ? dquote(str) : str
|
||||
end
|
||||
|
||||
def dquote( str )
|
||||
'"' + str.gsub(/["\\]/n) {|s| '\\' + s } + '"'
|
||||
end
|
||||
private :dquote
|
||||
|
||||
|
||||
def join_domain( arr )
|
||||
arr.map {|i|
|
||||
if /\A\[.*\]\z/ === i
|
||||
i
|
||||
else
|
||||
quote_atom(i)
|
||||
end
|
||||
}.join('.')
|
||||
end
|
||||
|
||||
|
||||
ZONESTR_TABLE = {
|
||||
'jst' => 9 * 60,
|
||||
'eet' => 2 * 60,
|
||||
'bst' => 1 * 60,
|
||||
'met' => 1 * 60,
|
||||
'gmt' => 0,
|
||||
'utc' => 0,
|
||||
'ut' => 0,
|
||||
'nst' => -(3 * 60 + 30),
|
||||
'ast' => -4 * 60,
|
||||
'edt' => -4 * 60,
|
||||
'est' => -5 * 60,
|
||||
'cdt' => -5 * 60,
|
||||
'cst' => -6 * 60,
|
||||
'mdt' => -6 * 60,
|
||||
'mst' => -7 * 60,
|
||||
'pdt' => -7 * 60,
|
||||
'pst' => -8 * 60,
|
||||
'a' => -1 * 60,
|
||||
'b' => -2 * 60,
|
||||
'c' => -3 * 60,
|
||||
'd' => -4 * 60,
|
||||
'e' => -5 * 60,
|
||||
'f' => -6 * 60,
|
||||
'g' => -7 * 60,
|
||||
'h' => -8 * 60,
|
||||
'i' => -9 * 60,
|
||||
# j not use
|
||||
'k' => -10 * 60,
|
||||
'l' => -11 * 60,
|
||||
'm' => -12 * 60,
|
||||
'n' => 1 * 60,
|
||||
'o' => 2 * 60,
|
||||
'p' => 3 * 60,
|
||||
'q' => 4 * 60,
|
||||
'r' => 5 * 60,
|
||||
's' => 6 * 60,
|
||||
't' => 7 * 60,
|
||||
'u' => 8 * 60,
|
||||
'v' => 9 * 60,
|
||||
'w' => 10 * 60,
|
||||
'x' => 11 * 60,
|
||||
'y' => 12 * 60,
|
||||
'z' => 0 * 60
|
||||
}
|
||||
|
||||
def timezone_string_to_unixtime( str )
|
||||
if m = /([\+\-])(\d\d?)(\d\d)/.match(str)
|
||||
sec = (m[2].to_i * 60 + m[3].to_i) * 60
|
||||
m[1] == '-' ? -sec : sec
|
||||
else
|
||||
min = ZONESTR_TABLE[str.downcase] or
|
||||
raise SyntaxError, "wrong timezone format '#{str}'"
|
||||
min * 60
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
WDAY = %w( Sun Mon Tue Wed Thu Fri Sat TMailBUG )
|
||||
MONTH = %w( TMailBUG Jan Feb Mar Apr May Jun
|
||||
Jul Aug Sep Oct Nov Dec TMailBUG )
|
||||
|
||||
def time2str( tm )
|
||||
# [ruby-list:7928]
|
||||
gmt = Time.at(tm.to_i)
|
||||
gmt.gmtime
|
||||
offset = tm.to_i - Time.local(*gmt.to_a[0,6].reverse).to_i
|
||||
|
||||
# DO NOT USE strftime: setlocale() breaks it
|
||||
sprintf '%s, %s %s %d %02d:%02d:%02d %+.2d%.2d',
|
||||
WDAY[tm.wday], tm.mday, MONTH[tm.month],
|
||||
tm.year, tm.hour, tm.min, tm.sec,
|
||||
*(offset / 60).divmod(60)
|
||||
end
|
||||
|
||||
|
||||
MESSAGE_ID = /<[^\@>]+\@[^>\@]+>/
|
||||
|
||||
def message_id?( str )
|
||||
MESSAGE_ID === str
|
||||
end
|
||||
|
||||
|
||||
MIME_ENCODED = /=\?[^\s?=]+\?[QB]\?[^\s?=]+\?=/i
|
||||
|
||||
def mime_encoded?( str )
|
||||
MIME_ENCODED === str
|
||||
end
|
||||
|
||||
|
||||
def decode_params( hash )
|
||||
new = Hash.new
|
||||
encoded = nil
|
||||
hash.each do |key, value|
|
||||
if m = /\*(?:(\d+)\*)?\z/.match(key)
|
||||
((encoded ||= {})[m.pre_match] ||= [])[(m[1] || 0).to_i] = value
|
||||
else
|
||||
new[key] = to_kcode(value)
|
||||
end
|
||||
end
|
||||
if encoded
|
||||
encoded.each do |key, strings|
|
||||
new[key] = decode_RFC2231(strings.join(''))
|
||||
end
|
||||
end
|
||||
|
||||
new
|
||||
end
|
||||
|
||||
NKF_FLAGS = {
|
||||
'EUC' => '-e -m',
|
||||
'SJIS' => '-s -m'
|
||||
}
|
||||
|
||||
def to_kcode( str )
|
||||
flag = NKF_FLAGS[$KCODE] or return str
|
||||
NKF.nkf(flag, str)
|
||||
end
|
||||
|
||||
RFC2231_ENCODED = /\A(?:iso-2022-jp|euc-jp|shift_jis|us-ascii)?'[a-z]*'/in
|
||||
|
||||
def decode_RFC2231( str )
|
||||
m = RFC2231_ENCODED.match(str) or return str
|
||||
NKF.nkf(NKF_FLAGS[$KCODE],
|
||||
m.post_match.gsub(/%[\da-f]{2}/in) {|s| s[1,2].hex.chr })
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
|
@ -0,0 +1,3 @@
|
|||
Hello there,
|
||||
|
||||
Mr. <%= @recipient %>
|
|
@ -0,0 +1,3 @@
|
|||
Hello there,
|
||||
|
||||
Mr. <%= @recipient %>
|
|
@ -0,0 +1,92 @@
|
|||
$:.unshift(File.dirname(__FILE__) + "/../lib/")
|
||||
|
||||
require 'test/unit'
|
||||
require 'action_mailer'
|
||||
|
||||
class TestMailer < ActionMailer::Base
|
||||
def signed_up(recipient)
|
||||
@recipients = recipient
|
||||
@subject = "[Signed up] Welcome #{recipient}"
|
||||
@from = "system@loudthinking.com"
|
||||
@sent_on = Time.local(2004, 12, 12)
|
||||
@body["recipient"] = recipient
|
||||
end
|
||||
|
||||
def cancelled_account(recipient)
|
||||
@recipients = recipient
|
||||
@subject = "[Cancelled] Goodbye #{recipient}"
|
||||
@from = "system@loudthinking.com"
|
||||
@sent_on = Time.local(2004, 12, 12)
|
||||
@body = "Goodbye, Mr. #{recipient}"
|
||||
end
|
||||
end
|
||||
|
||||
TestMailer.template_root = File.dirname(__FILE__) + "/fixtures"
|
||||
|
||||
class ActionMailerTest < Test::Unit::TestCase
|
||||
def setup
|
||||
ActionMailer::Base.delivery_method = :test
|
||||
ActionMailer::Base.perform_deliveries = true
|
||||
ActionMailer::Base.deliveries = []
|
||||
|
||||
@recipient = 'test@localhost'
|
||||
end
|
||||
|
||||
def test_signed_up
|
||||
expected = TMail::Mail.new
|
||||
expected.to = @recipient
|
||||
expected.subject = "[Signed up] Welcome #{@recipient}"
|
||||
expected.body = "Hello there, \n\nMr. #{@recipient}"
|
||||
expected.from = "system@loudthinking.com"
|
||||
expected.date = Time.local(2004, 12, 12)
|
||||
|
||||
created = nil
|
||||
assert_nothing_raised { created = TestMailer.create_signed_up(@recipient) }
|
||||
assert_not_nil created
|
||||
assert_equal expected.encoded, created.encoded
|
||||
|
||||
assert_nothing_raised { TestMailer.deliver_signed_up(@recipient) }
|
||||
assert_not_nil ActionMailer::Base.deliveries.first
|
||||
assert_equal expected.encoded, ActionMailer::Base.deliveries.first.encoded
|
||||
end
|
||||
|
||||
def test_cancelled_account
|
||||
expected = TMail::Mail.new
|
||||
expected.to = @recipient
|
||||
expected.subject = "[Cancelled] Goodbye #{@recipient}"
|
||||
expected.body = "Goodbye, Mr. #{@recipient}"
|
||||
expected.from = "system@loudthinking.com"
|
||||
expected.date = Time.local(2004, 12, 12)
|
||||
|
||||
created = nil
|
||||
assert_nothing_raised { created = TestMailer.create_cancelled_account(@recipient) }
|
||||
assert_not_nil created
|
||||
assert_equal expected.encoded, created.encoded
|
||||
|
||||
assert_nothing_raised { TestMailer.deliver_cancelled_account(@recipient) }
|
||||
assert_not_nil ActionMailer::Base.deliveries.first
|
||||
assert_equal expected.encoded, ActionMailer::Base.deliveries.first.encoded
|
||||
end
|
||||
|
||||
def test_instances_are_nil
|
||||
assert_nil ActionMailer::Base.new
|
||||
assert_nil TestMailer.new
|
||||
end
|
||||
|
||||
def test_deliveries_array
|
||||
assert_not_nil ActionMailer::Base.deliveries
|
||||
assert_equal 0, ActionMailer::Base.deliveries.size
|
||||
TestMailer.deliver_signed_up(@recipient)
|
||||
assert_equal 1, ActionMailer::Base.deliveries.size
|
||||
assert_not_nil ActionMailer::Base.deliveries.first
|
||||
end
|
||||
|
||||
def test_perform_deliveries_flag
|
||||
ActionMailer::Base.perform_deliveries = false
|
||||
TestMailer.deliver_signed_up(@recipient)
|
||||
assert_equal 0, ActionMailer::Base.deliveries.size
|
||||
ActionMailer::Base.perform_deliveries = true
|
||||
TestMailer.deliver_signed_up(@recipient)
|
||||
assert_equal 1, ActionMailer::Base.deliveries.size
|
||||
end
|
||||
end
|
|
@ -0,0 +1,738 @@
|
|||
*CVS*
|
||||
|
||||
* Upgraded to Builder 1.2.1
|
||||
|
||||
* Added :module as an alias for :controller_prefix to url_for and friends, so you can do redirect_to(:module => "shop", :controller => "purchases")
|
||||
and go to /shop/purchases/
|
||||
|
||||
* Added support for controllers in modules through @params["module"].
|
||||
|
||||
* Added reloading for dependencies under cached environments like FastCGI and mod_ruby. This makes it possible to use those environments for development.
|
||||
This is turned on by default, but can be turned off with ActionController::Base.reload_dependencies = false in production environments.
|
||||
|
||||
NOTE: This will only have an effect if you use the new model, service, and observer class methods to mark dependencies. All libraries loaded through
|
||||
require will be "forever" cached. You can, however, use ActionController::Base.load_or_require("library") to get this behavior outside of the new
|
||||
dependency style.
|
||||
|
||||
* Added that controllers will automatically require their own helper if possible. So instead of doing:
|
||||
|
||||
class MsgController < AbstractApplicationController
|
||||
helper :msg
|
||||
end
|
||||
|
||||
...you can just do:
|
||||
|
||||
class MsgController < AbstractApplicationController
|
||||
end
|
||||
|
||||
* Added dependencies_on(layer) to query the dependencies of a controller. Examples:
|
||||
|
||||
MsgController.dependencies_on(:model) # => [ :post, :comment, :attachment ]
|
||||
MsgController.dependencies_on(:service) # => [ :notification_service ]
|
||||
MsgController.dependencies_on(:observer) # => [ :comment_observer ]
|
||||
|
||||
* Added a new dependency model with the class methods model, service, and observer. Example:
|
||||
|
||||
class MsgController < AbstractApplicationController
|
||||
model :post, :comment, :attachment
|
||||
service :notification_service
|
||||
observer :comment_observer
|
||||
end
|
||||
|
||||
These new "keywords" remove the need for explicitly calling 'require' in most cases. The observer method even instantiates the
|
||||
observer as well as requiring it.
|
||||
|
||||
* Fixed that link_to would escape & in the url again after url_for already had done so
|
||||
|
||||
*0.9.5* (28)
|
||||
|
||||
* Added helper_method to designate that a given private or protected method you should available as a helper in the view. [bitsweat]
|
||||
|
||||
* Fixed assert_rendered_file so it actually verifies if that was the rendered file [htonl]
|
||||
|
||||
* Added the option for sharing partial spacer templates just like partials themselves [radsaq]
|
||||
|
||||
* Fixed that Russia was named twice in country_select [alexey]
|
||||
|
||||
* Fixed request_origin to use remote_ip instead of remote_addr [bitsweat]
|
||||
|
||||
* Fixed link_to breakage when nil was passed for html_options [alexey]
|
||||
|
||||
* Fixed redirect_to on a virtual server setup with apache with a port other than the default where it would forget the port number [seanohalpin]
|
||||
|
||||
* Fixed that auto-loading webrick on Windows would cause file uploads to fail [bitsweat]
|
||||
|
||||
* Fixed issues with sending files on WEBrick by setting the proper binmode [bitsweat]
|
||||
|
||||
* Added send_data as an alternative to send_file when the stream is not read off the filesystem but from a database or generated live [bitsweat]
|
||||
|
||||
* Added a new way to include helpers that doesn't require the include hack and can go without the explicit require. [bitsweat]
|
||||
|
||||
Before:
|
||||
|
||||
module WeblogHelper
|
||||
def self.append_features(controller) #:nodoc:
|
||||
controller.ancestors.include?(ActionController::Base) ? controller.add_template_helper(self) : super
|
||||
end
|
||||
end
|
||||
|
||||
require 'weblog_helper'
|
||||
class WeblogController < ActionController::Base
|
||||
include WeblogHelper
|
||||
end
|
||||
|
||||
After:
|
||||
|
||||
module WeblogHelper
|
||||
end
|
||||
|
||||
class WeblogController < ActionController::Base
|
||||
helper :weblog
|
||||
end
|
||||
|
||||
* Added a default content-type of "text/xml" to .rxml renders [Ryan Platte]
|
||||
|
||||
* Fixed that when /controller/index was requested by the browser, url_for would generates wrong URLs [Ryan Platte]
|
||||
|
||||
* Fixed a bug that would share cookies between users when using FastCGI and mod_ruby [The Robot Co-op]
|
||||
|
||||
* Added an optional third hash parameter to the process method in functional tests that takes the session data to be used [alexey]
|
||||
|
||||
* Added UrlHelper#mail_to to make it easier to create mailto: style ahrefs
|
||||
|
||||
* Added better error messages for layouts declared with the .rhtml extension (which they shouldn't) [geech]
|
||||
|
||||
* Added another case to DateHelper#distance_in_minutes to return "less than a minute" instead of "0 minutes" and "1 minute" instead of "1 minutes"
|
||||
|
||||
* Added a hidden field to checkboxes generated with FormHelper#check_box that will make sure that the unchecked value (usually 0)
|
||||
is sent even if the checkbox is not checked. This relieves the controller from doing custom checking if the the checkbox wasn't
|
||||
checked. BEWARE: This might conflict with your run-on-the-mill work-around code. [Tobias Luetke]
|
||||
|
||||
* Fixed error_message_on to just use the first if more than one error had been added [marcel]
|
||||
|
||||
* Fixed that URL rewriting with /controller/ was working but /controller was not and that you couldn't use :id on index [geech]
|
||||
|
||||
* Fixed a bug with link_to where the :confirm option wouldn't be picked up if the link was a straight url instead of an option hash
|
||||
|
||||
* Changed scaffolding of forms to use <label> tags instead of <b> to please W3C [evl]
|
||||
|
||||
* Added DateHelper#distance_of_time_in_words_to_now(from_time) that works like distance_of_time_in_words,
|
||||
but where <tt>to_time</tt> is fixed to <tt>Time.now</tt>.
|
||||
|
||||
* Added assert_flash_equal(expected, key, message), assert_session_equal(expected, key, message),
|
||||
assert_assigned_equal(expected, key, message) to test the contents of flash, session, and template assigns.
|
||||
|
||||
* Improved the failure report on assert_success when the action triggered a redirection [alexey].
|
||||
|
||||
* Added "markdown" to accompany "textilize" as a TextHelper method for converting text to HTML using the Markdown syntax.
|
||||
BlueCloth must be installed in order for this method to become available.
|
||||
|
||||
* Made sure that an active session exists before we attempt to delete it [Samuel]
|
||||
|
||||
* Changed link_to with Javascript confirmation to use onclick instead of onClick for XHTML validity [Scott Barron]
|
||||
|
||||
|
||||
*0.9.0 (43)*
|
||||
|
||||
* Added support for Builder-based templates for files with the .rxml extension. These new templates are an alternative to ERb that
|
||||
are especially useful for generating XML content, such as this RSS example from Basecamp:
|
||||
|
||||
xml.rss("version" => "2.0", "xmlns:dc" => "http://purl.org/dc/elements/1.1/") do
|
||||
xml.channel do
|
||||
xml.title(@feed_title)
|
||||
xml.link(@url)
|
||||
xml.description "Basecamp: Recent items"
|
||||
xml.language "en-us"
|
||||
xml.ttl "40"
|
||||
|
||||
for item in @recent_items
|
||||
xml.item do
|
||||
xml.title(item_title(item))
|
||||
xml.description(item_description(item)) if item_description(item)
|
||||
xml.pubDate(item_pubDate(item))
|
||||
xml.guid(@person.firm.account.url + @recent_items.url(item))
|
||||
xml.link(@person.firm.account.url + @recent_items.url(item))
|
||||
|
||||
xml.tag!("dc:creator", item.author_name) if item_has_creator?(item)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
...which will generate something like:
|
||||
|
||||
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||
<channel>
|
||||
<title>Web Site Redesign</title>
|
||||
<link>http://www.basecamphq.com/clients/travelcenter/1/</link>
|
||||
<description>Basecamp: Recent items</description>
|
||||
<language>en-us</language>
|
||||
<ttl>40</ttl>
|
||||
<item>
|
||||
<title>Post: don't you know</title>
|
||||
<description>&lt;p&gt;deeper and down&lt;/p&gt;</description>
|
||||
<pubDate>Fri, 20 Aug 2004 21:13:50 CEST</pubDate>
|
||||
<guid>http://www.basecamphq.com/clients/travelcenter/1/msg/assets/96976/comments</guid>
|
||||
<link>http://www.basecamphq.com/clients/travelcenter/1/msg/assets/96976/comments</link>
|
||||
<dc:creator>David H. Heinemeier</dc:creator>
|
||||
</item>
|
||||
<item>
|
||||
<title>Milestone completed: Design Comp 2</title>
|
||||
<pubDate>Mon, 9 Aug 2004 14:42:06 CEST</pubDate>
|
||||
<guid>http://www.basecamphq.com/clients/travelcenter/1/milestones/#49</guid>
|
||||
<link>http://www.basecamphq.com/clients/travelcenter/1/milestones/#49</link>
|
||||
</item>
|
||||
</channel>
|
||||
</rss>
|
||||
|
||||
The "xml" local variable is automatically available in .rxml templates. You construct the template by calling a method with the name
|
||||
of the tag you want. Options for the tag can be specified as a hash parameter to that method.
|
||||
|
||||
Builder-based templates can be mixed and matched with the regular ERb ones. The only thing that differentiates them is the extension.
|
||||
No new methods have been added to the public interface to handle them.
|
||||
|
||||
Action Pack ships with a version of Builder, but it will use the RubyGems version if you have one installed.
|
||||
|
||||
Read more about Builder on: http://onestepback.org/index.cgi/Tech/Ruby/StayingSimple.rdoc
|
||||
|
||||
[Builder is created by Jim Weirich]
|
||||
|
||||
* Added much improved support for functional testing [what-a-day].
|
||||
|
||||
# Old style
|
||||
def test_failing_authenticate
|
||||
@request.request_uri = "/login/authenticate"
|
||||
@request.action = "authenticate"
|
||||
@request.request_parameters["user_name"] = "nop"
|
||||
@request.request_parameters["password"] = ""
|
||||
|
||||
response = LoginController.process_test(@request)
|
||||
|
||||
assert_equal "The username and/or password you entered is invalid.", response.session["flash"]["alert"]
|
||||
assert_equal "http://37signals.basecamp.com/login/", response.headers["location"]
|
||||
end
|
||||
|
||||
# New style
|
||||
def test_failing_authenticate
|
||||
process :authenticate, "user_name" => "nop", "password" => ""
|
||||
assert_flash_has 'alert'
|
||||
assert_redirected_to :action => "index"
|
||||
end
|
||||
|
||||
See a full example on http://codepaste.org/view/paste/334
|
||||
|
||||
* Increased performance by up to 100% with a revised cookie class that fixes the performance problems with the
|
||||
default one that ships with 1.8.1 and below. It replaces the inheritance on SimpleDelegator with DelegateClass(Array)
|
||||
following the suggestion from Matz on:
|
||||
http://groups.google.com/groups?th=e3a4e68ba042f842&seekm=c3sioe%241qvm%241%40news.cybercity.dk#link14
|
||||
|
||||
* Added caching for compiled ERb templates. On Basecamp, it gave between 8.5% and 71% increase in performance [Andreas Schwarz].
|
||||
|
||||
* Added implicit counter variable to render_collection_of_partials [Marcel]. From the docs:
|
||||
|
||||
<%= render_collection_of_partials "ad", @advertisements %>
|
||||
|
||||
This will render "advertiser/_ad.rhtml" and pass the local variable +ad+ to the template for display. An iteration counter
|
||||
will automatically be made available to the template with a name of the form +partial_name_counter+. In the case of the
|
||||
example above, the template would be fed +ad_counter+.
|
||||
|
||||
* Fixed problems with two sessions being maintained on reset_session that would particularly screw up ActiveRecordStore.
|
||||
|
||||
* Fixed reset_session to start an entirely new session instead of merely deleting the old. So you can now safely access @session
|
||||
after calling reset_ression and expect it to work.
|
||||
|
||||
* Added @request.get?, @request.post?, @request.put?, @request.delete? as convenience query methods for @request.method [geech]
|
||||
|
||||
* Added @request.method that'll return a symbol representing the HTTP method, such as :get, :post, :put, :delete [geech]
|
||||
|
||||
* Changed @request.remote_ip and @request.host to work properly even when a proxy is in front of the application [geech]
|
||||
|
||||
* Added JavaScript confirm feature to link_to. Documentation:
|
||||
|
||||
The html_options have a special feature for creating javascript confirm alerts where if you pass
|
||||
:confirm => 'Are you sure?', the link will be guarded with a JS popup asking that question.
|
||||
If the user accepts, the link is processed, otherwise not.
|
||||
|
||||
* Added link_to_unless_current as a UrlHelper method [Sam Stephenson]. Documentation:
|
||||
|
||||
Creates a link tag of the given +name+ using an URL created by the set of +options+, unless the current
|
||||
controller, action, and id are the same as the link's, in which case only the name is returned (or the
|
||||
given block is yielded, if one exists). This is useful for creating link bars where you don't want to link
|
||||
to the page currently being viewed.
|
||||
|
||||
* Fixed that UrlRewriter (the driver for url_for, link_to, etc) would blow up when the anchor was an integer [alexey]
|
||||
|
||||
* Added that layouts defined with no directory defaults to layouts. So layout "weblog/standard" will use
|
||||
weblog/standard (as always), but layout "standard" will use layouts/standard.
|
||||
|
||||
* Fixed that partials (or any template starting with an underscore) was publically viewable [Marten]
|
||||
|
||||
* Added HTML escaping to text_area helper.
|
||||
|
||||
* Added :overwrite_params to url_for and friends to keep the parameters as they were passed to the current action and only overwrite a subset.
|
||||
The regular :params will clear the slate so you need to manually add in existing parameters if you want to reuse them. [raphinou]
|
||||
|
||||
* Fixed scaffolding problem with composite named objects [Moo Jester]
|
||||
|
||||
* Added the possibility for shared partials. Example:
|
||||
|
||||
<%= render_partial "advertisement/ad", ad %>
|
||||
|
||||
This will render the partial "advertisement/_ad.rhtml" regardless of which controller this is being called from.
|
||||
|
||||
[Jacob Fugal]
|
||||
|
||||
* Fixed crash when encountering forms that have empty-named fields [James Prudente]
|
||||
|
||||
* Added check_box form helper method now accepts true/false as well as 1/0 [what-a-day]
|
||||
|
||||
* Fixed the lacking creation of all directories with install.rb [Dave Steinberg]
|
||||
|
||||
* Fixed that date_select returns valid XHTML selected options [Andreas Schwarz]
|
||||
|
||||
* Fixed referencing an action with the same name as a controller in url_for [what-a-day]
|
||||
|
||||
* Fixed the destructive nature of Base#attributes= on the argument [Kevin Watt]
|
||||
|
||||
* Changed ActionControllerError to decent from StandardError instead of Exception. It can now be caught by a generic rescue.
|
||||
|
||||
* Added SessionRestoreError that is raised when a session being restored holds objects where there is no class available.
|
||||
|
||||
* Added block as option for inline filters. So what used to be written as:
|
||||
|
||||
before_filter Proc { |controller| return false if controller.params["stop_action"] }
|
||||
|
||||
...can now be as:
|
||||
|
||||
before_filter { |controller| return false if controller.params["stop_action"] }
|
||||
|
||||
[Jeremy Kemper]
|
||||
|
||||
* Made the following methods public (was protected): url_for, controller_class_name, controller_name, action_name
|
||||
This makes it easier to write filters without cheating around the encapsulation with send.
|
||||
|
||||
* ActionController::Base#reset_session now sticks even if you access @session afterwards [Kent Sibilev]
|
||||
|
||||
* Improved the exception logging so the log file gets almost as much as in-browser debugging.
|
||||
|
||||
* Changed base class setup from AbstractTemplate/ERbTemplate to ActionView::Base. This change should be harmless unless you were
|
||||
accessing Action View directly in which case you now need to reference the Base class.\
|
||||
|
||||
* Added that render_collection_of_partials returns nil if the collection is empty. This makes showing a “no items” message easier.
|
||||
For example: <%= render_collection_of_partials("message", @messages) || "No messages found." %> [Sam Stephenson]
|
||||
|
||||
* Added :month_before_year as an option to date_select to get the month select before the year. Especially useful for credit card forms.
|
||||
|
||||
* Added :add_month_numbers to select_month to get options like "3 - March".
|
||||
|
||||
* Removed Base.has_active_layout? as it couldn't answer the question without the instance. Use Base#active_layout instead.
|
||||
|
||||
* Removed redundant call to update on ActionController::Base#close_session [Andreas Schwarz]
|
||||
|
||||
* Fixed that DRb Store accidently started its own server (instead of just client) [Andreas]
|
||||
|
||||
* Fixed strip_links so it now works across multiple lines [Chad Fowler]
|
||||
|
||||
* Fixed the TemplateError exception to show the proper trace on to_s (useful for unit test debugging)
|
||||
|
||||
* Implemented class inheritable attributes without eval [Caio Chassot]
|
||||
|
||||
* Made TextHelper#concat accept binding as it would otherwise not work
|
||||
|
||||
* The FormOptionsHelper will now call to_s on the keys and values used to generate options
|
||||
|
||||
|
||||
*0.8.5*
|
||||
|
||||
* Introduced passing of locally scoped variables between templates:
|
||||
|
||||
You can pass local variables to sub templates by using a hash of with the variable
|
||||
names as keys and the objects as values:
|
||||
|
||||
<%= render "shared/header", { "headline" => "Welcome", "person" => person } %>
|
||||
|
||||
These can now be accessed in shared/header with:
|
||||
|
||||
Headline: <%= headline %>
|
||||
First name: <%= person.first_name %>
|
||||
|
||||
* Introduced the concept of partials as a certain type of sub templates:
|
||||
|
||||
There's also a convenience method for rendering sub templates within the current
|
||||
controller that depends on a single object (we call this kind of sub templates for
|
||||
partials). It relies on the fact that partials should follow the naming convention
|
||||
of being prefixed with an underscore -- as to separate them from regular templates
|
||||
that could be rendered on their own. In the template for Advertiser#buy, we could have:
|
||||
|
||||
<% for ad in @advertisements %>
|
||||
<%= render_partial "ad", ad %>
|
||||
<% end %>
|
||||
|
||||
This would render "advertiser/_ad.rhtml" and pass the local variable +ad+
|
||||
for the template to display.
|
||||
|
||||
== Rendering a collection of partials
|
||||
|
||||
The example of partial use describes a familar pattern where a template needs
|
||||
to iterate over a array and render a sub template for each of the elements.
|
||||
This pattern has been implemented as a single method that accepts an array and
|
||||
renders a partial by the same name of as the elements contained within. So the
|
||||
three-lined example in "Using partials" can be rewritten with a single line:
|
||||
|
||||
<%= render_collection_of_partials "ad", @advertisements %>
|
||||
|
||||
So this will render "advertiser/_ad.rhtml" and pass the local variable +ad+ for
|
||||
the template to display.
|
||||
|
||||
* Improved send_file by allowing a wide range of options to be applied [Jeremy Kemper]:
|
||||
|
||||
Sends the file by streaming it 4096 bytes at a time. This way the
|
||||
whole file doesn't need to be read into memory at once. This makes
|
||||
it feasible to send even large files.
|
||||
|
||||
Be careful to sanitize the path parameter if it coming from a web
|
||||
page. send_file(@params['path'] allows a malicious user to
|
||||
download any file on your server.
|
||||
|
||||
Options:
|
||||
* <tt>:filename</tt> - specifies the filename the browser will see.
|
||||
Defaults to File.basename(path).
|
||||
* <tt>:type</tt> - specifies an HTTP content type.
|
||||
Defaults to 'application/octet-stream'.
|
||||
* <tt>:disposition</tt> - specifies whether the file will be shown inline or downloaded.
|
||||
Valid values are 'inline' and 'attachment' (default).
|
||||
* <tt>:buffer_size</tt> - specifies size (in bytes) of the buffer used to stream
|
||||
the file. Defaults to 4096.
|
||||
|
||||
The default Content-Type and Content-Disposition headers are
|
||||
set to download arbitrary binary files in as many browsers as
|
||||
possible. IE versions 4, 5, 5.5, and 6 are all known to have
|
||||
a variety of quirks (especially when downloading over SSL).
|
||||
|
||||
Simple download:
|
||||
send_file '/path/to.zip'
|
||||
|
||||
Show a JPEG in browser:
|
||||
send_file '/path/to.jpeg', :type => 'image/jpeg', :disposition => 'inline'
|
||||
|
||||
Read about the other Content-* HTTP headers if you'd like to
|
||||
provide the user with more information (such as Content-Description).
|
||||
http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.11
|
||||
|
||||
Also be aware that the document may be cached by proxies and browsers.
|
||||
The Pragma and Cache-Control headers declare how the file may be cached
|
||||
by intermediaries. They default to require clients to validate with
|
||||
the server before releasing cached responses. See
|
||||
http://www.mnot.net/cache_docs/ for an overview of web caching and
|
||||
http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9
|
||||
for the Cache-Control header spec.
|
||||
|
||||
* Added pluralize method to the TextHelper that makes it easy to get strings like "1 message", "3 messages"
|
||||
|
||||
* Added proper escaping for the rescues [Andreas Schwartz]
|
||||
|
||||
* Added proper escaping for the option and collection tags [Andreas Schwartz]
|
||||
|
||||
* Fixed NaN errors on benchmarking [Jim Weirich]
|
||||
|
||||
* Fixed query string parsing for URLs that use the escaped versions of & or ; as part of a key or value
|
||||
|
||||
* Fixed bug with custom Content-Type headers being in addition to rather than instead of the default header.
|
||||
(This bug didn't matter with neither CGI or mod_ruby, but FCGI exploded on it) [With help from Ara T. Howard]
|
||||
|
||||
|
||||
*0.8.0*
|
||||
|
||||
* Added select, collection_select, and country_select to make it easier for Active Records to set attributes through
|
||||
drop-down lists of options. Example:
|
||||
|
||||
<%= select "person", "gender", %w( Male Female ) %>
|
||||
|
||||
...would give the following:
|
||||
|
||||
<select name="person[gender]" id="person_gender"><option>Male</option><option>Female</option></select>
|
||||
|
||||
* Added an option for getting multiple values on a single form name into an array instead of having the last one overwrite.
|
||||
This is especially useful for groups of checkboxes, which can now be written as:
|
||||
|
||||
<input type="checkbox" name="rights[]" value="CREATE" />
|
||||
<input type="checkbox" name="rights[]" value="UPDATE" />
|
||||
<input type="checkbox" name="rights[]" value="DELETE" />
|
||||
|
||||
...and retrieved in the controller action with:
|
||||
|
||||
@params["rights"] # => [ "CREATE", "UPDATE", "DELETE" ]
|
||||
|
||||
The old behavior (where the last one wins, "DELETE" in the example) is still available. Just don't add "[]" to the
|
||||
end of the name. [Scott Baron]
|
||||
|
||||
* Added send_file which uses the new render_text block acceptance to make it feasible to send large files.
|
||||
The files is sent with a bunch of voodoo HTTP headers required to get arbitrary files to download as
|
||||
expected in as many browsers as possible (eg, IE hacks). Example:
|
||||
|
||||
def play_movie
|
||||
send_file "/movies/that_movie.avi"
|
||||
end
|
||||
|
||||
[Jeremy Kemper]
|
||||
|
||||
* render_text now accepts a block for deferred rendering. Useful for streaming large files, displaying
|
||||
a “please wait” message during a complex search, etc. Streaming example:
|
||||
|
||||
render_text do |response|
|
||||
File.open(path, 'rb') do |file|
|
||||
while buf = file.read(1024)
|
||||
print buf
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
[Jeremy Kemper]
|
||||
|
||||
* Added a new Tag Helper that can generate generic tags programmatically insted of through HTML. Example:
|
||||
|
||||
tag("br", "clear" => "all") => <br clear="all" />
|
||||
|
||||
...that's usually not terribly interesting (unless you have a lot of options already in a hash), but it
|
||||
gives way for more specific tags, like the new form tag:
|
||||
|
||||
form_tag({ :controller => "weblog", :action => "update" }, { :multipart => "true", "style" => "width: 200px"}) =>
|
||||
<form action="/weblog/update" enctype="multipart/formdata" style="width: 200px">
|
||||
|
||||
There's even a "pretty" version for people who don't like to open tags in code and close them in HTML:
|
||||
|
||||
<%= start_form_tag :action => "update" %>
|
||||
# all the input fields
|
||||
<%= end_form_tag %>
|
||||
|
||||
(end_form_tag just returns "</form>")
|
||||
|
||||
* The selected parameter in options_for_select may now also an array of values to be selected when
|
||||
using a multiple select. Example:
|
||||
|
||||
options_for_select([ "VISA", "Mastercard", "Discover" ], ["VISA", "Discover"]) =>
|
||||
<option selected>VISA</option>\n<option>Mastercard</option>\n<option selected>Discover</option>
|
||||
|
||||
[Scott Baron]
|
||||
|
||||
* Changed the URL rewriter so controller_prefix and action_prefix can be used in isolation. You can now do:
|
||||
|
||||
url_for(:controller_prefix => "clients")
|
||||
|
||||
...or:
|
||||
|
||||
url_for(:action_prefix => "category/messages")
|
||||
|
||||
Neither would have worked in isolation before (:controller_prefix required a :controller and :action_prefix required an :action)
|
||||
|
||||
* Started process of a cleaner separation between Action Controller and ERb-based Action Views by introducing an
|
||||
abstract base class for views. And Amita adapter could be fitted in more easily now.
|
||||
|
||||
* The date helper methods date_select and datetime_select now also use the field error wrapping
|
||||
(div with class fieldWithErrors by default).
|
||||
|
||||
* The date helper methods date_select and datetime_select can now discard selects
|
||||
|
||||
* Added option on AbstractTemplate to specify a different field error wrapping. Example:
|
||||
|
||||
ActionView::AbstractTemplate.field_error_proc = Proc.new do |html, instance|
|
||||
"<p>#{instance.method_name + instance.error_message}</p><div style='background-color: red'>#{html}</div>"
|
||||
end
|
||||
|
||||
...would give the following on a Post#title (text field) error:
|
||||
|
||||
<p>Title can't be empty</p>
|
||||
<div style='background-color: red'>
|
||||
<input id="post_title" name="post[title]" size="30" type="text" value="Hello World" />
|
||||
</div>
|
||||
|
||||
* The UrlHelper methods url_for and link_to will now by default only return paths, not complete URIs.
|
||||
That should make it easier to fit a Rails application behind a proxy or load-balancer.
|
||||
You can overwrite this by passing :only_path => false as part of the options. [Suggested by U235]
|
||||
|
||||
* Fixed bug with having your own layout for use with scaffolding [Kevin Radloff]
|
||||
|
||||
* Fixed bug where redirect_to_path didn't append the port on non-standard ports [dhawkins]
|
||||
|
||||
* Scaffolding plays nicely with single-table inheritance (LoadErrors are caught) [Jeremy Kemper]
|
||||
|
||||
* Scaffolding plays nice with plural models like Category/categories [Jeremy Kemper]
|
||||
|
||||
* Fixed missing suffix appending in scaffolding [Kevin Radloff]
|
||||
|
||||
|
||||
*0.7.9*
|
||||
|
||||
* The "form" method now present boolean fields from PostgreSQL as drop-down menu. [Scott]
|
||||
|
||||
* Scaffolding now automatically attempts to require the class that's being scaffolded.
|
||||
|
||||
* Scaffolding will use the current active layout, instead of its own, if one has been specified. Example:
|
||||
|
||||
class WeblogController < ActionController::Base
|
||||
layout "layouts/weblog"
|
||||
scaffold :post
|
||||
end
|
||||
|
||||
[Suggested by Scott]
|
||||
|
||||
* Changed url_for (and all the that drives, like redirect_to, link_to, link_for) so you can pass it a symbol instead of a hash.
|
||||
This symbol is a method reference which is then called to calculate the url. Example:
|
||||
|
||||
class WeblogController < ActionController::Base
|
||||
def update
|
||||
# do some update
|
||||
redirect_to :dashboard_url
|
||||
end
|
||||
|
||||
protected
|
||||
def dashboard_url
|
||||
if @project.active?
|
||||
url_for :controller => "project", :action => "dashboard"
|
||||
else
|
||||
url_for :controller => "account", :action => "dashboard"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
* Added default_url_options to specialize behavior for all url_for (and friends) calls:
|
||||
|
||||
Overwrite to implement a number of default options that all url_for-based methods will use.
|
||||
The default options should come in form of a hash, just like the one you would use for
|
||||
url_for directly. Example:
|
||||
|
||||
def default_url_options(options)
|
||||
{ :controller_prefix => @project.active? ? "projects/" : "accounts/" }
|
||||
end
|
||||
|
||||
As you can infer from the example, this is mostly useful for situations where you want to
|
||||
centralize dynamic dissions about the urls as they stem from the business domain. Please note
|
||||
that any individual url_for call can always override the defaults set by this method.
|
||||
|
||||
|
||||
* Changed url_for so that an "id" passed in the :params is not treated special. You need to use the dedicated :id to get
|
||||
the special auto path-params treatment. Considering the url http://localhost:81/friends/list
|
||||
|
||||
url_for(:action => "show", :params => { "id" => 5 })
|
||||
...used to give http://localhost:81/friends/show/5
|
||||
......now gives http://localhost:81/friends/show?id=5
|
||||
|
||||
If you want the automated id behavior, do:
|
||||
|
||||
url_for(:action => "show", :id => 5 )
|
||||
....which gives http://localhost:81/friends/show/5
|
||||
|
||||
|
||||
* Fixed problem with anchor being inserted before path parameters with url_for (and friends)
|
||||
|
||||
|
||||
*0.7.8*
|
||||
|
||||
* Fixed session bug where you couldn't store any objects that didn't exist in the standard library
|
||||
(such as Active Record objects).
|
||||
|
||||
* Added reset_session method for Action Controller objects to clear out all objects in the session.
|
||||
|
||||
* Fixed that exceptions raised during filters are now also caught by the default rescues
|
||||
|
||||
* Added new around_filter for doing before and after filtering with a single object [Florian Weber]:
|
||||
|
||||
class WeblogController < ActionController::Base
|
||||
around_filter BenchmarkingFilter.new
|
||||
|
||||
# Before this action is performed, BenchmarkingFilter#before(controller) is executed
|
||||
def index
|
||||
end
|
||||
# After this action has been performed, BenchmarkingFilter#after(controller) is executed
|
||||
end
|
||||
|
||||
class BenchmarkingFilter
|
||||
def initialize
|
||||
@runtime
|
||||
end
|
||||
|
||||
def before
|
||||
start_timer
|
||||
end
|
||||
|
||||
def after
|
||||
stop_timer
|
||||
report_result
|
||||
end
|
||||
end
|
||||
|
||||
* Added the options for specifying a different name and id for the form helper methods than what is guessed [Florian Weber]:
|
||||
|
||||
text_field "post", "title"
|
||||
...just gives: <input id="post_title" name="post[title]" size="30" type="text" value="" />
|
||||
|
||||
text_field "post", "title", "id" => "title_for_post", "name" => "first_post_title"
|
||||
...can now give: <input id="title_for_post" name="first_post_title" size="30" type="text" value="" />
|
||||
|
||||
* Added DebugHelper with a single "debug" method for doing pretty dumps of objects in the view
|
||||
(now used in the default rescues to better present the contents of session and template variables)
|
||||
|
||||
* Added note to log about the templates rendered within layouts (before just the layout was shown)
|
||||
|
||||
* Fixed redirects on https setups [Andreas]
|
||||
|
||||
* Fixed scaffolding problem on the edit action when using :suffix => true [Scott]
|
||||
|
||||
* Fixed scaffolding problem where implementing list.rhtml wouldn't work for the index action
|
||||
|
||||
* URLs generated now uses & instead of just & so pages using it can validate with W3C [Spotted by Andreas]
|
||||
|
||||
|
||||
*0.7.7*
|
||||
|
||||
* Fixed bug in CGI extension that prevented multipart forms from working
|
||||
|
||||
|
||||
*0.7.6*
|
||||
|
||||
* Included ERB::Util so all templates can easily escape HTML content with <%=h @person.content %>
|
||||
|
||||
* All requests are now considered local by default, so everyone will be exposed to detailed debugging screens on errors.
|
||||
When the application is ready to go public, set ActionController::Base.consider_all_requests_local to false,
|
||||
and implement the protected method local_request? in the controller to determine when debugging screens should be shown.
|
||||
|
||||
* Fixed three bugs with the url_for/redirect_to/link_to handling. Considering the url http://localhost:81/friends/show/1
|
||||
|
||||
url_for(:action => "list")
|
||||
...used to give http://localhost:81/friends/list/1
|
||||
......now gives http://localhost:81/friends/list
|
||||
|
||||
url_for(:controller => "friends", :action => "destroy", :id => 5)
|
||||
...used to give http://localhost:81/friends/destroy
|
||||
......now gives http://localhost:81/friends/destroy/5
|
||||
|
||||
Considering the url http://localhost:81/teachers/show/t
|
||||
|
||||
url_for(:action => "list", :id => 5)
|
||||
...used to give http://localhost:81/5eachers/list/t
|
||||
......now gives http://localhost:81/teachers/list/5
|
||||
|
||||
[Reported by David Morton & Radsaq]
|
||||
|
||||
* Logs exception to logfile in addition to showing them for local requests
|
||||
|
||||
* Protects the eruby load behind a begin/rescue block. eRuby is not required to run ActionController.
|
||||
|
||||
* Fixed install.rb to also install clean_logger and the templates
|
||||
|
||||
* Added ActiveRecordStore as a session option. Read more in lib/action_controller/session/active_record_store.rb [Tim Bates]
|
||||
|
||||
* Change license to MIT License (and included license file in package)
|
||||
|
||||
* Application error page now returns status code 500 instead of 200
|
||||
|
||||
* Fixed using Procs as layout handlers [Florian Weber]
|
||||
|
||||
* Fixed bug with using redirects ports other than 80
|
||||
|
||||
* Added index method that calls list on scaffolding
|
||||
|
||||
|
||||
*0.7.5*
|
||||
|
||||
* First public release
|
|
@ -0,0 +1,21 @@
|
|||
Copyright (c) 2004 David Heinemeier Hansson
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
|
@ -0,0 +1,418 @@
|
|||
= Action Pack -- On rails from request to response
|
||||
|
||||
Action Pack splits the response to a web request into a controller part
|
||||
(performing the logic) and a view part (rendering a template). This two-step
|
||||
approach is known as an action, which will normally create, read, update, or
|
||||
delete (CRUD for short) some sort of model part (often backed by a database)
|
||||
before choosing either to render a template or redirecting to another action.
|
||||
|
||||
Action Pack implements these actions as public methods on Action Controllers
|
||||
and uses Action Views to implement the template rendering. Action Controllers
|
||||
are then responsible for handling all the actions relating to a certain part
|
||||
of an application. This grouping usually consists of actions for lists and for
|
||||
CRUDs revolving around a single (or a few) model objects. So ContactController
|
||||
would be responsible for listing contacts, creating, deleting, and updating
|
||||
contacts. A WeblogController could be responsible for both posts and comments.
|
||||
|
||||
Action View templates are written using embedded Ruby in tags mingled in with
|
||||
the HTML. To avoid cluttering the templates with code, a bunch of helper
|
||||
classes provide common behavior for forms, dates, and strings. And it's easy
|
||||
to add specific helpers to keep the separation as the application evolves.
|
||||
|
||||
Note: Some of the features, such as scaffolding and form building, are tied to
|
||||
ActiveRecord[http://activerecord.rubyonrails.org] (an object-relational
|
||||
mapping package), but that doesn't mean that Action Pack depends on Active
|
||||
Record. Action Pack is an independent package that can be used with any sort
|
||||
of backend (Instiki[http://www.instiki.org], which is based on an older version
|
||||
of Action Pack, uses Madeleine for example). Read more about the role Action
|
||||
Pack can play when used together with Active Record on
|
||||
http://www.rubyonrails.org.
|
||||
|
||||
A short rundown of the major features:
|
||||
|
||||
* Actions grouped in controller as methods instead of separate command objects
|
||||
and can therefore helper share methods.
|
||||
|
||||
BlogController < ActionController::Base
|
||||
def display
|
||||
@customer = find_customer
|
||||
end
|
||||
|
||||
def update
|
||||
@customer = find_customer
|
||||
@customer.attributes = @params["customer"]
|
||||
@customer.save ?
|
||||
redirect_to(:action => "display") :
|
||||
render("customer/edit")
|
||||
end
|
||||
|
||||
private
|
||||
def find_customer() Customer.find(@params["id"]) end
|
||||
end
|
||||
|
||||
Learn more in link:classes/ActionController/Base.html
|
||||
|
||||
|
||||
* Embedded Ruby for templates (no new "easy" template language)
|
||||
|
||||
<% for post in @posts %>
|
||||
Title: <%= post.title %>
|
||||
<% end %>
|
||||
|
||||
All post titles: <%= @post.collect{ |p| p.title }.join ", " %>
|
||||
|
||||
<% unless @person.is_client? %>
|
||||
Not for clients to see...
|
||||
<% end %>
|
||||
|
||||
Learn more in link:classes/ActionView.html
|
||||
|
||||
|
||||
* Builder-based templates (great for XML content, like RSS)
|
||||
|
||||
xml.rss("version" => "2.0") do
|
||||
xml.channel do
|
||||
xml.title(@feed_title)
|
||||
xml.link(@url)
|
||||
xml.description "Basecamp: Recent items"
|
||||
xml.language "en-us"
|
||||
xml.ttl "40"
|
||||
|
||||
for item in @recent_items
|
||||
xml.item do
|
||||
xml.title(item_title(item))
|
||||
xml.description(item_description(item))
|
||||
xml.pubDate(item_pubDate(item))
|
||||
xml.guid(@recent_items.url(item))
|
||||
xml.link(@recent_items.url(item))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
* Filters for pre and post processing of the response (as methods, procs, and classes)
|
||||
|
||||
class WeblogController < ActionController::Base
|
||||
before_filter :authenticate, :cache, :audit
|
||||
after_filter { |c| c.response.body = GZip::compress(c.response.body) }
|
||||
after_filter LocalizeFilter
|
||||
|
||||
def list
|
||||
# Before this action is run, the user will be authenticated, the cache
|
||||
# will be examined to see if a valid copy of the results already
|
||||
# exist, and the action will be logged for auditing.
|
||||
|
||||
# After this action has run, the output will first be localized then
|
||||
# compressed to minimize bandwith usage
|
||||
end
|
||||
|
||||
private
|
||||
def authenticate
|
||||
# Implement the filter will full access to both request and response
|
||||
end
|
||||
end
|
||||
|
||||
Learn more in link:classes/ActionController/Filters/ClassMethods.html
|
||||
|
||||
|
||||
* Helpers for forms, dates, action links, and text
|
||||
|
||||
<%= text_field "post", "title", "size" => 30 %>
|
||||
<%= html_date_select(Date.today) %>
|
||||
<%= link_to "New post", :controller => "post", :action => "new" %>
|
||||
<%= truncate(post.title, 25) %>
|
||||
|
||||
Learn more in link:classes/ActionView/Helpers.html
|
||||
|
||||
|
||||
* Layout sharing for template reuse (think simple version of Struts
|
||||
Tiles[http://jakarta.apache.org/struts/userGuide/dev_tiles.html])
|
||||
|
||||
class WeblogController < ActionController::Base
|
||||
layout "weblog_layout"
|
||||
|
||||
def hello_world
|
||||
end
|
||||
end
|
||||
|
||||
Layout file (called weblog_layout):
|
||||
<html><body><%= @content_for_layout %></body></html>
|
||||
|
||||
Template for hello_world action:
|
||||
<h1>Hello world</h1>
|
||||
|
||||
Result of running hello_world action:
|
||||
<html><body><h1>Hello world</h1></body></html>
|
||||
|
||||
Learn more in link:classes/ActionController/Layout.html
|
||||
|
||||
|
||||
* Advanced redirection that makes pretty urls easy
|
||||
|
||||
RewriteRule ^/library/books/([A-Z]+)([0-9]+)/([-_a-zA-Z0-9]+)$ \
|
||||
/books_controller.cgi?action=$3&type=$1&code=$2 [QSA] [L]
|
||||
|
||||
Accessing /library/books/ISBN/0743536703/show calls BooksController#show
|
||||
|
||||
From that URL, you can rewrite the redirect in a number of ways:
|
||||
|
||||
redirect_to(:action => "edit") =>
|
||||
/library/books/ISBN/0743536703/edit
|
||||
|
||||
redirect_to(:path_params => { "type" => "XTC", "code" => "12354345" }) =>
|
||||
/library/books/XTC/12354345/show
|
||||
|
||||
redirect_to(:controller_prefix => "admin", :controller => "accounts") =>
|
||||
/admin/accounts/
|
||||
|
||||
Learn more in link:classes/ActionController/Base.html
|
||||
|
||||
|
||||
* Easy testing of both controller and template result through TestRequest/Response
|
||||
|
||||
class LoginControllerTest < Test::Unit::TestCase
|
||||
def setup
|
||||
@controller = LoginController.new
|
||||
@request = ActionController::TestRequest.new
|
||||
@response = ActionController::TestResponse.new
|
||||
end
|
||||
|
||||
def test_failing_authenticate
|
||||
process :authenticate, "user_name" => "nop", "password" => ""
|
||||
assert_flash_has 'alert'
|
||||
assert_redirected_to :action => "index"
|
||||
end
|
||||
end
|
||||
|
||||
Learn more in link:classes/ActionController/TestRequest.html
|
||||
|
||||
|
||||
* Automated benchmarking and integrated logging
|
||||
|
||||
Processing WeblogController#index (for 127.0.0.1 at Fri May 28 00:41:55)
|
||||
Parameters: {"action"=>"index", "controller"=>"weblog"}
|
||||
Rendering weblog/index (200 OK)
|
||||
Completed in 0.029281 (34 reqs/sec)
|
||||
|
||||
If Active Record is used as the model, you'll have the database debugging
|
||||
as well:
|
||||
|
||||
Processing WeblogController#create (for 127.0.0.1 at Sat Jun 19 14:04:23)
|
||||
Params: {"controller"=>"weblog", "action"=>"create",
|
||||
"post"=>{"title"=>"this is good"} }
|
||||
SQL (0.000627) INSERT INTO posts (title) VALUES('this is good')
|
||||
Redirected to http://test/weblog/display/5
|
||||
Completed in 0.221764 (4 reqs/sec) | DB: 0.059920 (27%)
|
||||
|
||||
You specify a logger through a class method, such as:
|
||||
|
||||
ActionController::Base.logger = Logger.new("Application Log")
|
||||
ActionController::Base.logger = Log4r::Logger.new("Application Log")
|
||||
|
||||
|
||||
* Powerful debugging mechanism for local requests
|
||||
|
||||
All exceptions raised on actions performed on the request of a local user
|
||||
will be presented with a tailored debugging screen that includes exception
|
||||
message, stack trace, request parameters, session contents, and the
|
||||
half-finished response.
|
||||
|
||||
Learn more in link:classes/ActionController/Rescue.html
|
||||
|
||||
|
||||
* Scaffolding for Action Record model objects
|
||||
|
||||
require 'account' # must be an Active Record class
|
||||
class AccountController < ActionController::Base
|
||||
scaffold :account
|
||||
end
|
||||
|
||||
The AccountController now has the full CRUD range of actions and default
|
||||
templates: list, show, destroy, new, create, edit, update
|
||||
|
||||
Learn more in link:classes/ActionController/Scaffolding/ClassMethods.html
|
||||
|
||||
|
||||
* Form building for Active Record model objects
|
||||
|
||||
The post object has a title (varchar), content (text), and
|
||||
written_on (date)
|
||||
|
||||
<%= form "post" %>
|
||||
|
||||
...will generate something like (the selects will have more options of
|
||||
course):
|
||||
|
||||
<form action="create" method="POST">
|
||||
<p>
|
||||
<b>Title:</b><br/>
|
||||
<input type="text" name="post[title]" value="<%= @post.title %>" />
|
||||
</p>
|
||||
<p>
|
||||
<b>Content:</b><br/>
|
||||
<textarea name="post[content]"><%= @post.title %></textarea>
|
||||
</p>
|
||||
<p>
|
||||
<b>Written on:</b><br/>
|
||||
<select name='post[written_on(3i)]'><option>18</option></select>
|
||||
<select name='post[written_on(2i)]'><option value='7'>July</option></select>
|
||||
<select name='post[written_on(1i)]'><option>2004</option></select>
|
||||
</p>
|
||||
|
||||
<input type="submit" value="Create">
|
||||
</form>
|
||||
|
||||
This form generates a @params["post"] array that can be used directly in a save action:
|
||||
|
||||
class WeblogController < ActionController::Base
|
||||
def save
|
||||
post = Post.create(@params["post"])
|
||||
redirect_to :action => "display", :path_params => { "id" => post.id }
|
||||
end
|
||||
end
|
||||
|
||||
Learn more in link:classes/ActionView/Helpers/ActiveRecordHelper.html
|
||||
|
||||
|
||||
* Automated mapping of URLs to controller/action pairs through Apache's
|
||||
mod_rewrite
|
||||
|
||||
Requesting /blog/display/5 will call BlogController#display and
|
||||
make 5 available as an instance variable through @params["id"]
|
||||
|
||||
|
||||
* Runs on top of CGI, FCGI, and mod_ruby
|
||||
|
||||
See the address_book_controller example for all three forms
|
||||
|
||||
|
||||
== Simple example
|
||||
|
||||
This example will implement a simple weblog system using inline templates and
|
||||
an Active Record model. The first thing we need to do is setup an .htaccess to
|
||||
interpret pretty URLs into something the controller can use. Let's use the
|
||||
simplest form for starters:
|
||||
|
||||
RewriteRule ^weblog/([-_a-zA-Z0-9]+)/([0-9]+)$ \
|
||||
/weblog_controller.cgi?action=$2&id=$3 [QSA]
|
||||
RewriteRule ^weblog/([-_a-zA-Z0-9]+)$ \
|
||||
/weblog_controller.cgi?action=$2 [QSA]
|
||||
RewriteRule ^weblog/$ \
|
||||
/weblog_controller.cgi?action=index [QSA]
|
||||
|
||||
Now we'll be able to access URLs like weblog/display/5 and have
|
||||
WeblogController#display called with { "id" => 5 } in the @params array
|
||||
available for the action. So let's build that WeblogController with just a few
|
||||
methods:
|
||||
|
||||
require 'action_controller'
|
||||
require 'post'
|
||||
class WeblogController < ActionController::Base
|
||||
layout "weblog/layout"
|
||||
|
||||
def index
|
||||
@posts = Post.find_all
|
||||
end
|
||||
|
||||
def display
|
||||
@post = Post.find(@params["id"])
|
||||
end
|
||||
|
||||
def new
|
||||
@post = Post.new
|
||||
end
|
||||
|
||||
def create
|
||||
@post = Post.create(@params["post"])
|
||||
@post.save
|
||||
redirect_to :action => "display", :id => @post.id
|
||||
end
|
||||
end
|
||||
|
||||
WeblogController::Base.template_root = File.dirname(__FILE__)
|
||||
WeblogController.process_cgi if $0 == __FILE__
|
||||
|
||||
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).
|
||||
|
||||
And the templates look like this:
|
||||
|
||||
weblog/layout.rhtml:
|
||||
<html><body>
|
||||
<%= @content_for_layout %>
|
||||
</body></html>
|
||||
|
||||
weblog/index.rhtml:
|
||||
<% for post in @posts %>
|
||||
<p><%= link_to(post.title, :action => "display", :id => post.id %></p>
|
||||
<% end %>
|
||||
|
||||
weblog/display.rhtml:
|
||||
<p>
|
||||
<b><%= post.title %></b><br/>
|
||||
<b><%= post.content %></b>
|
||||
</p>
|
||||
|
||||
weblog/new.rhtml:
|
||||
<%= form "post" %>
|
||||
|
||||
This simple setup will list all the posts in the system on the index page,
|
||||
which is called by accessing /weblog/. It uses the form builder for the Active
|
||||
Record model to make the new screen, which in turns hand everything over to
|
||||
the create action (that's the default target for the form builder when given a
|
||||
new model). After creating the post, it'll redirect to the display page using
|
||||
an URL such as /weblog/display/5 (where 5 is the id of the post.
|
||||
|
||||
|
||||
== Examples
|
||||
|
||||
Action Pack ships with three examples that all demonstrate an increasingly
|
||||
detailed view of the possibilities. First is blog_controller that is just a
|
||||
single file for the whole MVC (but still split into separate parts). Second is
|
||||
the debate_controller that uses separate template files and multiple screens.
|
||||
Third is the address_book_controller that uses the layout feature to separate
|
||||
template casing from content.
|
||||
|
||||
Please note that you might need to change the "shebang" line to
|
||||
#!/usr/local/env ruby, if your Ruby is not placed in /usr/local/bin/ruby
|
||||
|
||||
|
||||
== Download
|
||||
|
||||
The latest version of Action Pack can be found at
|
||||
|
||||
* http://rubyforge.org/project/showfiles.php?group_id=249
|
||||
|
||||
Documentation can be found at
|
||||
|
||||
* http://actionpack.rubyonrails.org
|
||||
|
||||
|
||||
== Installation
|
||||
|
||||
You can install Action Pack with the following command.
|
||||
|
||||
% [sudo] ruby install.rb
|
||||
|
||||
from its distribution directory.
|
||||
|
||||
|
||||
== License
|
||||
|
||||
Action Pack is released under the same license as Ruby.
|
||||
|
||||
|
||||
== Support
|
||||
|
||||
The Action Pack homepage is http://actionpack.rubyonrails.org. You can find
|
||||
the Action Pack RubyForge page at http://rubyforge.org/projects/actionpack.
|
||||
And as Jim from Rake says:
|
||||
|
||||
Feel free to submit commits or feature requests. If you send a patch,
|
||||
remember to update the corresponding unit tests. If fact, I prefer
|
||||
new feature to be submitted in the form of new unit tests.
|
||||
|
||||
For other information, feel free to ask on the ruby-talk mailing list (which
|
||||
is mirrored to comp.lang.ruby) or contact mailto:david@loudthinking.com.
|
|
@ -0,0 +1,25 @@
|
|||
== Running with Rake
|
||||
|
||||
The easiest way to run the unit tests is through Rake. The default task runs
|
||||
the entire test suite for all classes. For more information, checkout the
|
||||
full array of rake tasks with "rake -T"
|
||||
|
||||
Rake can be found at http://rake.rubyforge.org
|
||||
|
||||
== Running by hand
|
||||
|
||||
If you only want to run a single test suite, or don't want to bother with Rake,
|
||||
you can do so with something like:
|
||||
|
||||
ruby controller/base_test.rb
|
||||
|
||||
== Dependency on ActiveRecord and database setup
|
||||
|
||||
Test cases in test/controller/active_record_assertions.rb depend on having
|
||||
activerecord installed and configured in a particular way. See comment in the
|
||||
test file itself for details. If ActiveRecord is not in
|
||||
actionpack/../activerecord directory, these tests are skipped. If activerecord
|
||||
is installed, but not configured as expected, the tests will fail.
|
||||
|
||||
Other tests are runnable from a fresh copy of actionpack without any configuration.
|
||||
|
|
@ -0,0 +1,105 @@
|
|||
require 'rubygems'
|
||||
require 'rake'
|
||||
require 'rake/testtask'
|
||||
require 'rake/rdoctask'
|
||||
require 'rake/packagetask'
|
||||
require 'rake/gempackagetask'
|
||||
require 'rake/contrib/rubyforgepublisher'
|
||||
|
||||
PKG_BUILD = ENV['PKG_BUILD'] ? '.' + ENV['PKG_BUILD'] : ''
|
||||
PKG_NAME = 'actionpack'
|
||||
PKG_VERSION = '0.9.5' + PKG_BUILD
|
||||
PKG_FILE_NAME = "#{PKG_NAME}-#{PKG_VERSION}"
|
||||
|
||||
desc "Default Task"
|
||||
task :default => [ :test ]
|
||||
|
||||
# Run the unit tests
|
||||
|
||||
Rake::TestTask.new { |t|
|
||||
t.libs << "test"
|
||||
t.pattern = 'test/*/*_test.rb'
|
||||
t.verbose = true
|
||||
}
|
||||
|
||||
|
||||
# Genereate the RDoc documentation
|
||||
|
||||
Rake::RDocTask.new { |rdoc|
|
||||
rdoc.rdoc_dir = 'doc'
|
||||
rdoc.title = "Action Pack -- On rails from request to response"
|
||||
rdoc.options << '--line-numbers --inline-source --main README'
|
||||
rdoc.rdoc_files.include('README', 'RUNNING_UNIT_TESTS', 'CHANGELOG')
|
||||
rdoc.rdoc_files.include('lib/**/*.rb')
|
||||
}
|
||||
|
||||
|
||||
# Create compressed packages
|
||||
|
||||
|
||||
dist_dirs = [ "lib", "test", "examples" ]
|
||||
|
||||
spec = Gem::Specification.new do |s|
|
||||
s.platform = Gem::Platform::RUBY
|
||||
s.name = PKG_NAME
|
||||
s.version = PKG_VERSION
|
||||
s.summary = "Web-flow and rendering framework putting the VC in MVC."
|
||||
s.description = %q{Eases web-request routing, handling, and response as a half-way front, half-way page controller. Implemented with specific emphasis on enabling easy unit/integration testing that doesn't require a browser.}
|
||||
|
||||
s.author = "David Heinemeier Hansson"
|
||||
s.email = "david@loudthinking.com"
|
||||
s.rubyforge_project = "actionpack"
|
||||
s.homepage = "http://actionpack.rubyonrails.org"
|
||||
|
||||
s.has_rdoc = true
|
||||
s.requirements << 'none'
|
||||
s.require_path = 'lib'
|
||||
s.autorequire = 'action_controller'
|
||||
|
||||
s.files = [ "rakefile", "install.rb", "README", "RUNNING_UNIT_TESTS", "CHANGELOG", "MIT-LICENSE", "examples/.htaccess" ]
|
||||
dist_dirs.each do |dir|
|
||||
s.files = s.files + Dir.glob( "#{dir}/**/*" ).delete_if { |item| item.include?( "CVS" ) }
|
||||
end
|
||||
s.files.delete "examples/benchmark.rb"
|
||||
s.files.delete "examples/benchmark_with_ar.fcgi"
|
||||
end
|
||||
|
||||
Rake::GemPackageTask.new(spec) do |p|
|
||||
p.gem_spec = spec
|
||||
p.need_tar = true
|
||||
p.need_zip = true
|
||||
end
|
||||
|
||||
|
||||
# Publish beta gem
|
||||
desc "Publish the API documentation"
|
||||
task :pgem => [:package] do
|
||||
Rake::SshFilePublisher.new("davidhh@one.textdrive.com", "domains/rubyonrails.org/gems/gems", "pkg", "#{PKG_FILE_NAME}.gem").upload
|
||||
`ssh davidhh@one.textdrive.com './gemupdate.sh'`
|
||||
end
|
||||
|
||||
# Publish documentation
|
||||
desc "Publish the API documentation"
|
||||
task :pdoc => [:rdoc] do
|
||||
Rake::SshDirPublisher.new("davidhh@one.textdrive.com", "domains/rubyonrails.org/ap", "doc").upload
|
||||
end
|
||||
|
||||
|
||||
desc "Count lines in the main rake file"
|
||||
task :lines do
|
||||
lines = 0
|
||||
codelines = 0
|
||||
Dir.foreach("lib/action_controller") { |file_name|
|
||||
next unless file_name =~ /.*rb/
|
||||
|
||||
f = File.open("lib/action_controller/" + file_name)
|
||||
|
||||
while line = f.gets
|
||||
lines += 1
|
||||
next if line =~ /^\s*$/
|
||||
next if line =~ /^\s*#/
|
||||
codelines += 1
|
||||
end
|
||||
}
|
||||
puts "Lines #{lines}, LOC #{codelines}"
|
||||
end
|
|
@ -0,0 +1,24 @@
|
|||
<IfModule mod_ruby.c>
|
||||
RubyRequire apache/ruby-run
|
||||
RubySafeLevel 0
|
||||
|
||||
<Files *.rbx>
|
||||
SetHandler ruby-object
|
||||
RubyHandler Apache::RubyRun.instance
|
||||
</Files>
|
||||
</IfModule>
|
||||
|
||||
|
||||
RewriteEngine On
|
||||
RewriteRule ^fcgi/([-_a-zA-Z0-9]+)/([-_a-zA-Z0-9]+)/([0-9]+)$ /$1_controller.fcgi?action=$2&id=$3 [QSA]
|
||||
RewriteRule ^fcgi/([-_a-zA-Z0-9]+)/([-_a-zA-Z0-9]+)$ /$1_controller.fcgi?action=$2 [QSA]
|
||||
RewriteRule ^fcgi/([-_a-zA-Z0-9]+)/$ /$1_controller.fcgi?action=index [QSA]
|
||||
|
||||
RewriteRule ^modruby/([-_a-zA-Z0-9]+)/([-_a-zA-Z0-9]+)/([0-9]+)$ /$1_controller.rbx?action=$2&id=$3 [QSA]
|
||||
RewriteRule ^modruby/([-_a-zA-Z0-9]+)/([-_a-zA-Z0-9]+)$ /$1_controller.rbx?action=$2 [QSA]
|
||||
RewriteRule ^modruby/([-_a-zA-Z0-9]+)/$ /$1_controller.rbx?action=index [QSA]
|
||||
|
||||
RewriteRule ^([-_a-zA-Z0-9]+)/([-_a-zA-Z0-9]+)/([0-9]+)$ /$1_controller.cgi?action=$2&id=$3 [QSA]
|
||||
RewriteRule ^([-_a-zA-Z0-9]+)/([-_a-zA-Z0-9]+)$ /$1_controller.cgi?action=$2 [QSA]
|
||||
RewriteRule ^([-_a-zA-Z0-9]+)/$ /$1_controller.cgi?action=index [QSA]
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
<h1>Address Book</h1>
|
||||
|
||||
<% if @people.empty? %>
|
||||
<p>No people in the address book yet</p>
|
||||
<% else %>
|
||||
<table>
|
||||
<tr><th>Name</th><th>Email Address</th><th>Phone Number</th></tr>
|
||||
<% for person in @people %>
|
||||
<tr><td><%= person.name %></td><td><%= person.email_address %></td><td><%= person.phone_number %></td></tr>
|
||||
<% end %>
|
||||
</table>
|
||||
<% end %>
|
||||
|
||||
<form action="create_person">
|
||||
<p>
|
||||
Name:<br />
|
||||
<input type="text" name="person[name]">
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Email address:<br />
|
||||
<input type="text" name="person[email_address]">
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Phone number:<br />
|
||||
<input type="text" name="person[phone_number]">
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<input type="submit" value="Create Person">
|
||||
</p>
|
||||
</form>
|
|
@ -0,0 +1,8 @@
|
|||
<html>
|
||||
<head>
|
||||
<title><%= @title || "Untitled" %></title>
|
||||
</head>
|
||||
<body>
|
||||
<%= @content_for_layout %>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,9 @@
|
|||
#!/usr/local/bin/ruby
|
||||
|
||||
require "address_book_controller"
|
||||
|
||||
begin
|
||||
AddressBookController.process_cgi(CGI.new)
|
||||
rescue => e
|
||||
CGI.new.out { "#{e.class}: #{e.message}" }
|
||||
end
|
|
@ -0,0 +1,6 @@
|
|||
#!/usr/local/bin/ruby
|
||||
|
||||
require "address_book_controller"
|
||||
require "fcgi"
|
||||
|
||||
FCGI.each_cgi { |cgi| AddressBookController.process_cgi(cgi) }
|
|
@ -0,0 +1,52 @@
|
|||
$:.unshift(File.dirname(__FILE__) + "/../lib")
|
||||
|
||||
require "action_controller"
|
||||
require "action_controller/test_process"
|
||||
|
||||
Person = Struct.new("Person", :id, :name, :email_address, :phone_number)
|
||||
|
||||
class AddressBookService
|
||||
attr_reader :people
|
||||
|
||||
def initialize() @people = [] end
|
||||
def create_person(data) people.unshift(Person.new(next_person_id, data["name"], data["email_address"], data["phone_number"])) end
|
||||
def find_person(topic_id) people.select { |person| person.id == person.to_i }.first end
|
||||
def next_person_id() people.first.id + 1 end
|
||||
end
|
||||
|
||||
class AddressBookController < ActionController::Base
|
||||
layout "address_book/layout"
|
||||
|
||||
before_filter :initialize_session_storage
|
||||
|
||||
# Could also have used a proc
|
||||
# before_filter proc { |c| c.instance_variable_set("@address_book", c.session["address_book"] ||= AddressBookService.new) }
|
||||
|
||||
def index
|
||||
@title = "Address Book"
|
||||
@people = @address_book.people
|
||||
end
|
||||
|
||||
def person
|
||||
@person = @address_book.find_person(@params["id"])
|
||||
end
|
||||
|
||||
def create_person
|
||||
@address_book.create_person(@params["person"])
|
||||
redirect_to :action => "index"
|
||||
end
|
||||
|
||||
private
|
||||
def initialize_session_storage
|
||||
@address_book = @session["address_book"] ||= AddressBookService.new
|
||||
end
|
||||
end
|
||||
|
||||
ActionController::Base.template_root = File.dirname(__FILE__)
|
||||
# ActionController::Base.logger = Logger.new("debug.log") # Remove first comment to turn on logging in current dir
|
||||
|
||||
begin
|
||||
AddressBookController.process_cgi(CGI.new) if $0 == __FILE__
|
||||
rescue => e
|
||||
CGI.new.out { "#{e.class}: #{e.message}" }
|
||||
end
|
|
@ -0,0 +1,4 @@
|
|||
#!/usr/local/bin/ruby
|
||||
|
||||
require "address_book_controller"
|
||||
AddressBookController.process_cgi(CGI.new)
|
|
@ -0,0 +1,52 @@
|
|||
$:.unshift(File.dirname(__FILE__) + "/../lib")
|
||||
|
||||
require "action_controller"
|
||||
require 'action_controller/test_process'
|
||||
|
||||
Person = Struct.new("Person", :name, :address, :age)
|
||||
|
||||
class BenchmarkController < ActionController::Base
|
||||
def message
|
||||
render_text "hello world"
|
||||
end
|
||||
|
||||
def list
|
||||
@people = [ Person.new("David"), Person.new("Mary") ]
|
||||
render_template "hello: <% for person in @people %>Name: <%= person.name %><% end %>"
|
||||
end
|
||||
|
||||
def form_helper
|
||||
@person = Person.new "david", "hyacintvej", 24
|
||||
render_template(
|
||||
"<% person = Person.new 'Mary', 'hyacintvej', 22 %> " +
|
||||
"change the name <%= text_field 'person', 'name' %> and <%= text_field 'person', 'address' %> and <%= text_field 'person', 'age' %>"
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
#ActionController::Base.template_root = File.dirname(__FILE__)
|
||||
|
||||
require "benchmark"
|
||||
|
||||
RUNS = ARGV[0] ? ARGV[0].to_i : 50
|
||||
|
||||
require "profile" if ARGV[1]
|
||||
|
||||
runtime = Benchmark.measure {
|
||||
RUNS.times { BenchmarkController.process_test(ActionController::TestRequest.new({ "action" => "list" })) }
|
||||
}
|
||||
|
||||
puts "List: #{RUNS / runtime.real}"
|
||||
|
||||
|
||||
runtime = Benchmark.measure {
|
||||
RUNS.times { BenchmarkController.process_test(ActionController::TestRequest.new({ "action" => "message" })) }
|
||||
}
|
||||
|
||||
puts "Message: #{RUNS / runtime.real}"
|
||||
|
||||
runtime = Benchmark.measure {
|
||||
RUNS.times { BenchmarkController.process_test(ActionController::TestRequest.new({ "action" => "form_helper" })) }
|
||||
}
|
||||
|
||||
puts "Form helper: #{RUNS / runtime.real}"
|
|
@ -0,0 +1,89 @@
|
|||
#!/usr/local/bin/ruby
|
||||
|
||||
begin
|
||||
|
||||
$:.unshift(File.dirname(__FILE__) + "/../lib")
|
||||
$:.unshift(File.dirname(__FILE__) + "/../../../edge/activerecord/lib")
|
||||
|
||||
require 'fcgi'
|
||||
require 'action_controller'
|
||||
require 'action_controller/test_process'
|
||||
|
||||
require 'active_record'
|
||||
|
||||
class Post < ActiveRecord::Base; end
|
||||
|
||||
ActiveRecord::Base.establish_connection(:adapter => "mysql", :database => "basecamp")
|
||||
|
||||
SESSION_OPTIONS = { "database_manager" => CGI::Session::MemoryStore }
|
||||
|
||||
class TestController < ActionController::Base
|
||||
def index
|
||||
render_template <<-EOT
|
||||
<% for post in Post.find_all(nil,nil,100) %>
|
||||
<%= post.title %>
|
||||
<% end %>
|
||||
EOT
|
||||
end
|
||||
|
||||
def show_one
|
||||
render_template <<-EOT
|
||||
<%= Post.find_first.title %>
|
||||
EOT
|
||||
end
|
||||
|
||||
def text
|
||||
render_text "hello world"
|
||||
end
|
||||
|
||||
def erb_text
|
||||
render_template "hello <%= 'world' %>"
|
||||
end
|
||||
|
||||
def erb_loop
|
||||
render_template <<-EOT
|
||||
<% for post in 1..100 %>
|
||||
<%= post %>
|
||||
<% end %>
|
||||
EOT
|
||||
end
|
||||
|
||||
def rescue_action(e) puts e.message + e.backtrace.join("\n") end
|
||||
end
|
||||
|
||||
if ARGV.empty? && ENV["REQUEST_URI"]
|
||||
FCGI.each_cgi do |cgi|
|
||||
TestController.process(ActionController::CgiRequest.new(cgi, SESSION_OPTIONS), ActionController::CgiResponse.new(cgi)).out
|
||||
end
|
||||
else
|
||||
if ARGV.empty?
|
||||
cgi = CGI.new
|
||||
end
|
||||
|
||||
require 'benchmark'
|
||||
require 'profile' if ARGV[2] == "profile"
|
||||
|
||||
RUNS = ARGV[1] ? ARGV[1].to_i : 50
|
||||
|
||||
runtime = Benchmark::measure {
|
||||
RUNS.times {
|
||||
if ARGV.empty?
|
||||
TestController.process(ActionController::CgiRequest.new(cgi, SESSION_OPTIONS), ActionController::CgiResponse.new(cgi))
|
||||
else
|
||||
response = TestController.process_test(
|
||||
ActionController::TestRequest.new({"action" => ARGV[0]})
|
||||
)
|
||||
puts(response.body) if ARGV[2] == "show"
|
||||
end
|
||||
}
|
||||
}
|
||||
|
||||
puts "Runs: #{RUNS}"
|
||||
puts "Avg. runtime: #{runtime.real / RUNS}"
|
||||
puts "Requests/second: #{RUNS / runtime.real}"
|
||||
end
|
||||
|
||||
rescue Exception => e
|
||||
# CGI.new.out { "<pre>" + e.message + e.backtrace.join("\n") + "</pre>" }
|
||||
$stderr << e.message + e.backtrace.join("\n")
|
||||
end
|
|
@ -0,0 +1,53 @@
|
|||
#!/usr/local/bin/ruby
|
||||
|
||||
$:.unshift(File.dirname(__FILE__) + "/../lib")
|
||||
|
||||
require "action_controller"
|
||||
|
||||
Post = Struct.new("Post", :title, :body)
|
||||
|
||||
class BlogController < ActionController::Base
|
||||
before_filter :initialize_session_storage
|
||||
|
||||
def index
|
||||
@posts = @session["posts"]
|
||||
|
||||
render_template <<-"EOF"
|
||||
<html><body>
|
||||
<%= @flash["alert"] %>
|
||||
<h1>Posts</h1>
|
||||
<% @posts.each do |post| %>
|
||||
<p><b><%= post.title %></b><br /><%= post.body %></p>
|
||||
<% end %>
|
||||
|
||||
<h1>Create post</h1>
|
||||
<form action="create">
|
||||
Title: <input type="text" name="post[title]"><br>
|
||||
Body: <textarea name="post[body]"></textarea><br>
|
||||
<input type="submit" value="save">
|
||||
</form>
|
||||
|
||||
</body></html>
|
||||
EOF
|
||||
end
|
||||
|
||||
def create
|
||||
@session["posts"].unshift(Post.new(@params["post"]["title"], @params["post"]["body"]))
|
||||
flash["alert"] = "New post added!"
|
||||
redirect_to :action => "index"
|
||||
end
|
||||
|
||||
private
|
||||
def initialize_session_storage
|
||||
@session["posts"] = [] if @session["posts"].nil?
|
||||
end
|
||||
end
|
||||
|
||||
ActionController::Base.template_root = File.dirname(__FILE__)
|
||||
# ActionController::Base.logger = Logger.new("debug.log") # Remove first comment to turn on logging in current dir
|
||||
|
||||
begin
|
||||
BlogController.process_cgi(CGI.new) if $0 == __FILE__
|
||||
rescue => e
|
||||
CGI.new.out { "#{e.class}: #{e.message}" }
|
||||
end
|
|
@ -0,0 +1,14 @@
|
|||
<html>
|
||||
<body>
|
||||
<h1>Topics</h1>
|
||||
|
||||
<%= link_to "New topic", :action => "new_topic" %>
|
||||
|
||||
<ul>
|
||||
<% for topic in @topics %>
|
||||
<li><%= link_to "#{topic.title} (#{topic.replies.length} replies)", :action => "topic", :path_params => { "id" => topic.id } %></li>
|
||||
<% end %>
|
||||
</ul>
|
||||
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,22 @@
|
|||
<html>
|
||||
<body>
|
||||
<h1>New topic</h1>
|
||||
|
||||
<form action="<%= url_for(:action => "create_topic") %>" method="post">
|
||||
<p>
|
||||
Title:<br>
|
||||
<input type="text" name="topic[title]">
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Body:<br>
|
||||
<textarea name="topic[body]" style="width: 200px; height: 200px"></textarea>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<input type="submit" value="Create topic">
|
||||
</p>
|
||||
</form>
|
||||
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,32 @@
|
|||
<html>
|
||||
<body>
|
||||
<h1><%= @topic.title %></h1>
|
||||
|
||||
<p><%= @topic.body %></p>
|
||||
|
||||
<%= link_to "Back to topics", :action => "index" %>
|
||||
|
||||
<% unless @topic.replies.empty? %>
|
||||
<h2>Replies</h2>
|
||||
<ol>
|
||||
<% for reply in @topic.replies %>
|
||||
<li><%= reply.body %></li>
|
||||
<% end %>
|
||||
</ol>
|
||||
<% end %>
|
||||
|
||||
<h2>Reply to this topic</h2>
|
||||
|
||||
<form action="<%= url_for(:action => "create_reply") %>" method="post">
|
||||
<input type="hidden" name="reply[topic_id]" value="<%= @topic.id %>">
|
||||
<p>
|
||||
<textarea name="reply[body]" style="width: 200px; height: 200px"></textarea>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<input type="submit" value="Create reply">
|
||||
</p>
|
||||
</form>
|
||||
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,57 @@
|
|||
#!/usr/local/bin/ruby
|
||||
|
||||
$:.unshift(File.dirname(__FILE__) + "/../lib")
|
||||
|
||||
require "action_controller"
|
||||
|
||||
Topic = Struct.new("Topic", :id, :title, :body, :replies)
|
||||
Reply = Struct.new("Reply", :body)
|
||||
|
||||
class DebateService
|
||||
attr_reader :topics
|
||||
|
||||
def initialize() @topics = [] end
|
||||
def create_topic(data) topics.unshift(Topic.new(next_topic_id, data["title"], data["body"], [])) end
|
||||
def create_reply(data) find_topic(data["topic_id"]).replies << Reply.new(data["body"]) end
|
||||
def find_topic(topic_id) topics.select { |topic| topic.id == topic_id.to_i }.first end
|
||||
def next_topic_id() topics.first.id + 1 end
|
||||
end
|
||||
|
||||
class DebateController < ActionController::Base
|
||||
before_filter :initialize_session_storage
|
||||
|
||||
def index
|
||||
@topics = @debate.topics
|
||||
end
|
||||
|
||||
def topic
|
||||
@topic = @debate.find_topic(@params["id"])
|
||||
end
|
||||
|
||||
# def new_topic() end <-- This is not needed as the template doesn't require any assigns
|
||||
|
||||
def create_topic
|
||||
@debate.create_topic(@params["topic"])
|
||||
redirect_to :action => "index"
|
||||
end
|
||||
|
||||
def create_reply
|
||||
@debate.create_reply(@params["reply"])
|
||||
redirect_to :action => "topic", :path_params => { "id" => @params["reply"]["topic_id"] }
|
||||
end
|
||||
|
||||
private
|
||||
def initialize_session_storage
|
||||
@session["debate"] = DebateService.new if @session["debate"].nil?
|
||||
@debate = @session["debate"]
|
||||
end
|
||||
end
|
||||
|
||||
ActionController::Base.template_root = File.dirname(__FILE__)
|
||||
# ActionController::Base.logger = Logger.new("debug.log") # Remove first comment to turn on logging in current dir
|
||||
|
||||
begin
|
||||
DebateController.process_cgi(CGI.new) if $0 == __FILE__
|
||||
rescue => e
|
||||
CGI.new.out { "#{e.class}: #{e.message}" }
|
||||
end
|
|
@ -0,0 +1,97 @@
|
|||
require 'rbconfig'
|
||||
require 'find'
|
||||
require 'ftools'
|
||||
|
||||
include Config
|
||||
|
||||
# this was adapted from rdoc's install.rb by ways of Log4r
|
||||
|
||||
$sitedir = CONFIG["sitelibdir"]
|
||||
unless $sitedir
|
||||
version = CONFIG["MAJOR"] + "." + CONFIG["MINOR"]
|
||||
$libdir = File.join(CONFIG["libdir"], "ruby", version)
|
||||
$sitedir = $:.find {|x| x =~ /site_ruby/ }
|
||||
if !$sitedir
|
||||
$sitedir = File.join($libdir, "site_ruby")
|
||||
elsif $sitedir !~ Regexp.quote(version)
|
||||
$sitedir = File.join($sitedir, version)
|
||||
end
|
||||
end
|
||||
|
||||
makedirs = %w{ action_controller/assertions action_controller/cgi_ext
|
||||
action_controller/session action_controller/support
|
||||
action_controller/templates action_controller/templates/rescues
|
||||
action_controller/templates/scaffolds
|
||||
action_view/helpers action_view/vendor action_view/vendor/builder
|
||||
}
|
||||
|
||||
|
||||
makedirs.each {|f| File::makedirs(File.join($sitedir, *f.split(/\//)))}
|
||||
|
||||
# deprecated files that should be removed
|
||||
# deprecated = %w{ }
|
||||
|
||||
# files to install in library path
|
||||
files = %w-
|
||||
action_controller.rb
|
||||
action_controller/assertions/action_pack_assertions.rb
|
||||
action_controller/assertions/active_record_assertions.rb
|
||||
action_controller/base.rb
|
||||
action_controller/benchmarking.rb
|
||||
action_controller/cgi_ext/cgi_ext.rb
|
||||
action_controller/cgi_ext/cgi_methods.rb
|
||||
action_controller/cgi_process.rb
|
||||
action_controller/filters.rb
|
||||
action_controller/flash.rb
|
||||
action_controller/helpers.rb
|
||||
action_controller/layout.rb
|
||||
action_controller/request.rb
|
||||
action_controller/rescue.rb
|
||||
action_controller/response.rb
|
||||
action_controller/scaffolding.rb
|
||||
action_controller/session/active_record_store.rb
|
||||
action_controller/session/drb_server.rb
|
||||
action_controller/session/drb_store.rb
|
||||
action_controller/support/class_inheritable_attributes.rb
|
||||
action_controller/support/class_attribute_accessors.rb
|
||||
action_controller/support/clean_logger.rb
|
||||
action_controller/support/cookie_performance_fix.rb
|
||||
action_controller/support/inflector.rb
|
||||
action_controller/templates/rescues/_request_and_response.rhtml
|
||||
action_controller/templates/rescues/diagnostics.rhtml
|
||||
action_controller/templates/rescues/layout.rhtml
|
||||
action_controller/templates/rescues/missing_template.rhtml
|
||||
action_controller/templates/rescues/template_error.rhtml
|
||||
action_controller/templates/rescues/unknown_action.rhtml
|
||||
action_controller/templates/scaffolds/edit.rhtml
|
||||
action_controller/templates/scaffolds/layout.rhtml
|
||||
action_controller/templates/scaffolds/list.rhtml
|
||||
action_controller/templates/scaffolds/new.rhtml
|
||||
action_controller/templates/scaffolds/show.rhtml
|
||||
action_controller/test_process.rb
|
||||
action_controller/url_rewriter.rb
|
||||
action_view.rb
|
||||
action_view/base.rb
|
||||
action_view/helpers/active_record_helper.rb
|
||||
action_view/helpers/date_helper.rb
|
||||
action_view/helpers/debug_helper.rb
|
||||
action_view/helpers/form_helper.rb
|
||||
action_view/helpers/form_options_helper.rb
|
||||
action_view/helpers/text_helper.rb
|
||||
action_view/helpers/tag_helper.rb
|
||||
action_view/helpers/url_helper.rb
|
||||
action_view/partials.rb
|
||||
action_view/template_error.rb
|
||||
action_view/vendor/builder.rb
|
||||
action_view/vendor/builder/blankslate.rb
|
||||
action_view/vendor/builder/xmlbase.rb
|
||||
action_view/vendor/builder/xmlevents.rb
|
||||
action_view/vendor/builder/xmlmarkup.rb
|
||||
-
|
||||
|
||||
# the acual gruntwork
|
||||
Dir.chdir("lib")
|
||||
# File::safe_unlink *deprecated.collect{|f| File.join($sitedir, f.split(/\//))}
|
||||
files.each {|f|
|
||||
File::install(f, File.join($sitedir, *f.split(/\//)), 0644, true)
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
#--
|
||||
# Copyright (c) 2004 David Heinemeier Hansson
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining
|
||||
# a copy of this software and associated documentation files (the
|
||||
# "Software"), to deal in the Software without restriction, including
|
||||
# without limitation the rights to use, copy, modify, merge, publish,
|
||||
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||
# permit persons to whom the Software is furnished to do so, subject to
|
||||
# the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
#++
|
||||
|
||||
$:.unshift(File.dirname(__FILE__))
|
||||
|
||||
require 'action_controller/support/clean_logger'
|
||||
|
||||
require 'action_controller/base'
|
||||
require 'action_controller/rescue'
|
||||
require 'action_controller/benchmarking'
|
||||
require 'action_controller/filters'
|
||||
require 'action_controller/layout'
|
||||
require 'action_controller/flash'
|
||||
require 'action_controller/scaffolding'
|
||||
require 'action_controller/helpers'
|
||||
require 'action_controller/dependencies'
|
||||
require 'action_controller/cgi_process'
|
||||
|
||||
ActionController::Base.class_eval do
|
||||
include ActionController::Filters
|
||||
include ActionController::Layout
|
||||
include ActionController::Flash
|
||||
include ActionController::Benchmarking
|
||||
include ActionController::Rescue
|
||||
include ActionController::Scaffolding
|
||||
include ActionController::Helpers
|
||||
include ActionController::Dependencies
|
||||
end
|
||||
|
||||
require 'action_view'
|
||||
ActionController::Base.template_class = ActionView::Base
|
|
@ -0,0 +1,199 @@
|
|||
require 'test/unit'
|
||||
require 'test/unit/assertions'
|
||||
require 'rexml/document'
|
||||
|
||||
module Test #:nodoc:
|
||||
module Unit #:nodoc:
|
||||
# Adds a wealth of assertions to do functional testing of Action Controllers.
|
||||
module Assertions
|
||||
# -- basic assertions ---------------------------------------------------
|
||||
|
||||
# ensure that the web request has been serviced correctly
|
||||
def assert_success(message=nil)
|
||||
response = acquire_assertion_target
|
||||
if response.success?
|
||||
# to count the assertion
|
||||
assert_block("") { true }
|
||||
else
|
||||
if response.redirect?
|
||||
msg = build_message(message, "Response unexpectedly redirect to <?>", response.redirect_url)
|
||||
else
|
||||
msg = build_message(message, "unsuccessful request (response code = <?>)",
|
||||
response.response_code)
|
||||
end
|
||||
assert_block(msg) { false }
|
||||
end
|
||||
end
|
||||
|
||||
# ensure the request was rendered with the appropriate template file
|
||||
def assert_rendered_file(expected=nil, message=nil)
|
||||
response = acquire_assertion_target
|
||||
rendered = expected ? response.rendered_file(!expected.include?('/')) : response.rendered_file
|
||||
msg = build_message(message, "expecting <?> but rendering with <?>", expected, rendered)
|
||||
assert_block(msg) do
|
||||
if expected.nil?
|
||||
response.rendered_with_file?
|
||||
else
|
||||
expected == rendered
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# -- session assertions -------------------------------------------------
|
||||
|
||||
# ensure that the session has an object with the specified name
|
||||
def assert_session_has(key=nil, message=nil)
|
||||
response = acquire_assertion_target
|
||||
msg = build_message(message, "<?> is not in the session <?>", key, response.session)
|
||||
assert_block(msg) { response.has_session_object?(key) }
|
||||
end
|
||||
|
||||
# ensure that the session has no object with the specified name
|
||||
def assert_session_has_no(key=nil, message=nil)
|
||||
response = acquire_assertion_target
|
||||
msg = build_message(message, "<?> is in the session <?>", key, response.session)
|
||||
assert_block(msg) { !response.has_session_object?(key) }
|
||||
end
|
||||
|
||||
def assert_session_equal(expected = nil, key = nil, message = nil)
|
||||
response = acquire_assertion_target
|
||||
msg = build_message(message, "<?> expected in session['?'] but was <?>", expected, key, response.session[key])
|
||||
assert_block(msg) { expected == response.session[key] }
|
||||
end
|
||||
|
||||
# -- flash assertions ---------------------------------------------------
|
||||
|
||||
# ensure that the flash has an object with the specified name
|
||||
def assert_flash_has(key=nil, message=nil)
|
||||
response = acquire_assertion_target
|
||||
msg = build_message(message, "<?> is not in the flash <?>", key, response.flash)
|
||||
assert_block(msg) { response.has_flash_object?(key) }
|
||||
end
|
||||
|
||||
# ensure that the flash has no object with the specified name
|
||||
def assert_flash_has_no(key=nil, message=nil)
|
||||
response = acquire_assertion_target
|
||||
msg = build_message(message, "<?> is in the flash <?>", key, response.flash)
|
||||
assert_block(msg) { !response.has_flash_object?(key) }
|
||||
end
|
||||
|
||||
# ensure the flash exists
|
||||
def assert_flash_exists(message=nil)
|
||||
response = acquire_assertion_target
|
||||
msg = build_message(message, "the flash does not exist <?>", response.session['flash'] )
|
||||
assert_block(msg) { response.has_flash? }
|
||||
end
|
||||
|
||||
# ensure the flash does not exist
|
||||
def assert_flash_not_exists(message=nil)
|
||||
response = acquire_assertion_target
|
||||
msg = build_message(message, "the flash exists <?>", response.flash)
|
||||
assert_block(msg) { !response.has_flash? }
|
||||
end
|
||||
|
||||
# ensure the flash is empty but existant
|
||||
def assert_flash_empty(message=nil)
|
||||
response = acquire_assertion_target
|
||||
msg = build_message(message, "the flash is not empty <?>", response.flash)
|
||||
assert_block(msg) { !response.has_flash_with_contents? }
|
||||
end
|
||||
|
||||
# ensure the flash is not empty
|
||||
def assert_flash_not_empty(message=nil)
|
||||
response = acquire_assertion_target
|
||||
msg = build_message(message, "the flash is empty")
|
||||
assert_block(msg) { response.has_flash_with_contents? }
|
||||
end
|
||||
|
||||
def assert_flash_equal(expected = nil, key = nil, message = nil)
|
||||
response = acquire_assertion_target
|
||||
msg = build_message(message, "<?> expected in flash['?'] but was <?>", expected, key, response.flash[key])
|
||||
assert_block(msg) { expected == response.flash[key] }
|
||||
end
|
||||
|
||||
# -- redirection assertions ---------------------------------------------
|
||||
|
||||
# ensure we have be redirected
|
||||
def assert_redirect(message=nil)
|
||||
response = acquire_assertion_target
|
||||
msg = build_message(message, "response is not a redirection (response code is <?>)", response.response_code)
|
||||
assert_block(msg) { response.redirect? }
|
||||
end
|
||||
|
||||
def assert_redirected_to(options = {}, message=nil)
|
||||
assert_redirect(message)
|
||||
response = acquire_assertion_target
|
||||
|
||||
msg = build_message(message, "response is not a redirection to all of the options supplied (redirection is <?>)", response.redirected_to)
|
||||
assert_block(msg) do
|
||||
if options.is_a?(Symbol)
|
||||
response.redirected_to == options
|
||||
else
|
||||
options.keys.all? { |k| options[k] == response.redirected_to[k] }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# ensure our redirection url is an exact match
|
||||
def assert_redirect_url(url=nil, message=nil)
|
||||
assert_redirect(message)
|
||||
response = acquire_assertion_target
|
||||
msg = build_message(message, "<?> is not the redirected location <?>", url, response.redirect_url)
|
||||
assert_block(msg) { response.redirect_url == url }
|
||||
end
|
||||
|
||||
# ensure our redirection url matches a pattern
|
||||
def assert_redirect_url_match(pattern=nil, message=nil)
|
||||
assert_redirect(message)
|
||||
response = acquire_assertion_target
|
||||
msg = build_message(message, "<?> was not found in the location: <?>", pattern, response.redirect_url)
|
||||
assert_block(msg) { response.redirect_url_match?(pattern) }
|
||||
end
|
||||
|
||||
# -- template assertions ------------------------------------------------
|
||||
|
||||
# ensure that a template object with the given name exists
|
||||
def assert_template_has(key=nil, message=nil)
|
||||
response = acquire_assertion_target
|
||||
msg = build_message(message, "<?> is not a template object", key )
|
||||
assert_block(msg) { response.has_template_object?(key) }
|
||||
end
|
||||
|
||||
# ensure that a template object with the given name does not exist
|
||||
def assert_template_has_no(key=nil,message=nil)
|
||||
response = acquire_assertion_target
|
||||
msg = build_message(message, "<?> is a template object <?>", key, response.template_objects[key])
|
||||
assert_block(msg) { !response.has_template_object?(key) }
|
||||
end
|
||||
|
||||
# ensures that the object assigned to the template on +key+ is equal to +expected+ object.
|
||||
def assert_assigned_equal(expected = nil, key = nil, message = nil)
|
||||
response = acquire_assertion_target
|
||||
msg = build_message(message, "<?> expected in assigns['?'] but was <?>", expected, key, response.template.assigns[key.to_s])
|
||||
assert_block(msg) { expected == response.template.assigns[key.to_s] }
|
||||
end
|
||||
|
||||
# Asserts that the template returns the +expected+ string or array based on the XPath +expression+.
|
||||
# This will only work if the template rendered a valid XML document.
|
||||
def assert_template_xpath_match(expression=nil, expected=nil, message=nil)
|
||||
response = acquire_assertion_target
|
||||
xml, matches = REXML::Document.new(response.body), []
|
||||
xml.elements.each(expression) { |e| matches << e.text }
|
||||
matches = matches.first if matches.length < 2
|
||||
|
||||
msg = build_message(message, "<?> found <?>, not <?>", expression, matches, expected)
|
||||
assert_block(msg) { matches == expected }
|
||||
end
|
||||
|
||||
# -- helper functions ---------------------------------------------------
|
||||
|
||||
# get the TestResponse object that these assertions depend upon
|
||||
def acquire_assertion_target
|
||||
target = ActionController::TestResponse.assertion_target
|
||||
assert_block( "Unable to acquire the TestResponse.assertion_target. Please set this before calling this assertion." ) { !target.nil? }
|
||||
target
|
||||
end
|
||||
|
||||
end # Assertions
|
||||
end # Unit
|
||||
end # Test
|
|
@ -0,0 +1,65 @@
|
|||
require 'test/unit'
|
||||
require 'test/unit/assertions'
|
||||
# active_record is assumed to be loaded by this point
|
||||
|
||||
module Test #:nodoc:
|
||||
module Unit #:nodoc:
|
||||
module Assertions
|
||||
# Assert the template object with the given name is an Active Record descendant and is valid.
|
||||
def assert_valid_record(key = nil, message = nil)
|
||||
record = find_record_in_template(key)
|
||||
msg = build_message(message, "Active Record is invalid <?>)", record.errors.full_messages)
|
||||
assert_block(msg) { record.valid? }
|
||||
end
|
||||
|
||||
# Assert the template object with the given name is an Active Record descendant and is invalid.
|
||||
def assert_invalid_record(key = nil, message = nil)
|
||||
record = find_record_in_template(key)
|
||||
msg = build_message(message, "Active Record is valid)")
|
||||
assert_block(msg) { !record.valid? }
|
||||
end
|
||||
|
||||
# Assert the template object with the given name is an Active Record descendant and the specified column(s) are valid.
|
||||
def assert_valid_column_on_record(key = nil, columns = "", message = nil)
|
||||
record = find_record_in_template(key)
|
||||
record.validate
|
||||
|
||||
cols = glue_columns(columns)
|
||||
cols.delete_if { |col| !record.errors.invalid?(col) }
|
||||
msg = build_message(message, "Active Record has invalid columns <?>)", cols.join(",") )
|
||||
assert_block(msg) { cols.empty? }
|
||||
end
|
||||
|
||||
# Assert the template object with the given name is an Active Record descendant and the specified column(s) are invalid.
|
||||
def assert_invalid_column_on_record(key = nil, columns = "", message = nil)
|
||||
record = find_record_in_template(key)
|
||||
record.validate
|
||||
|
||||
cols = glue_columns(columns)
|
||||
cols.delete_if { |col| record.errors.invalid?(col) }
|
||||
msg = build_message(message, "Active Record has valid columns <?>)", cols.join(",") )
|
||||
assert_block(msg) { cols.empty? }
|
||||
end
|
||||
|
||||
private
|
||||
def glue_columns(columns)
|
||||
cols = []
|
||||
cols << columns if columns.class == String
|
||||
cols += columns if columns.class == Array
|
||||
cols
|
||||
end
|
||||
|
||||
def find_record_in_template(key = nil)
|
||||
response = acquire_assertion_target
|
||||
|
||||
assert_template_has(key)
|
||||
record = response.template_objects[key]
|
||||
|
||||
assert_not_nil(record)
|
||||
assert_kind_of ActiveRecord::Base, record
|
||||
|
||||
return record
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,689 @@
|
|||
require 'action_controller/request'
|
||||
require 'action_controller/response'
|
||||
require 'action_controller/url_rewriter'
|
||||
require 'action_controller/support/class_attribute_accessors'
|
||||
require 'action_controller/support/class_inheritable_attributes'
|
||||
require 'action_controller/support/inflector'
|
||||
|
||||
module ActionController #:nodoc:
|
||||
class ActionControllerError < StandardError #:nodoc:
|
||||
end
|
||||
class SessionRestoreError < ActionControllerError #:nodoc:
|
||||
end
|
||||
class MissingTemplate < ActionControllerError #:nodoc:
|
||||
end
|
||||
class UnknownAction < ActionControllerError #:nodoc:
|
||||
end
|
||||
|
||||
# Action Controllers are made up of one or more actions that performs its purpose and then either renders a template or
|
||||
# redirects to another action. An action is defined as a public method on the controller, which will automatically be
|
||||
# made accessible to the web-server through a mod_rewrite mapping. A sample controller could look like this:
|
||||
#
|
||||
# class GuestBookController < ActionController::Base
|
||||
# def index
|
||||
# @entries = Entry.find_all
|
||||
# end
|
||||
#
|
||||
# def sign
|
||||
# Entry.create(@params["entry"])
|
||||
# redirect_to :action => "index"
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# GuestBookController.template_root = "templates/"
|
||||
# GuestBookController.process_cgi
|
||||
#
|
||||
# All actions assume that you want to render a template matching the name of the action at the end of the performance
|
||||
# unless you tell it otherwise. The index action complies with this assumption, so after populating the @entries instance
|
||||
# variable, the GuestBookController will render "templates/guestbook/index.rhtml".
|
||||
#
|
||||
# Unlike index, the sign action isn't interested in rendering a template. So after performing its main purpose (creating a
|
||||
# new entry in the guest book), it sheds the rendering assumption and initiates a redirect instead. This redirect works by
|
||||
# returning an external "302 Moved" HTTP response that takes the user to the index action.
|
||||
#
|
||||
# The index and sign represent the two basic action archetypes used in Action Controllers. Get-and-show and do-and-redirect.
|
||||
# Most actions are variations of these themes.
|
||||
#
|
||||
# Also note that it's the final call to <tt>process_cgi</tt> that actually initiates the action performance. It will extract
|
||||
# request and response objects from the CGI
|
||||
#
|
||||
# == Requests
|
||||
#
|
||||
# Requests are processed by the Action Controller framework by extracting the value of the "action" key in the request parameters.
|
||||
# This value should hold the name of the action to be performed. Once the action has been identified, the remaining
|
||||
# request parameters, the session (if one is available), and the full request with all the http headers are made available to
|
||||
# the action through instance variables. Then the action is performed.
|
||||
#
|
||||
# The full request object is available in @request and is primarily used to query for http headers. These queries are made by
|
||||
# accessing the environment hash, like this:
|
||||
#
|
||||
# def hello_ip
|
||||
# location = @request.env["REMOTE_ADDRESS"]
|
||||
# render_text "Hello stranger from #{location}"
|
||||
# end
|
||||
#
|
||||
# == Parameters
|
||||
#
|
||||
# All request parameters whether they come from a GET or POST request, or from the URL, are available through the @params hash.
|
||||
# So an action that was performed through /weblog/list?category=All&limit=5 will include { "category" => "All", "limit" => 5 }
|
||||
# in @params.
|
||||
#
|
||||
# It's also possible to construct multi-dimensional parameter hashes by specifying keys using brackets, such as:
|
||||
#
|
||||
# <input type="text" name="post[name]" value="david">
|
||||
# <input type="text" name="post[address]" value="hyacintvej">
|
||||
#
|
||||
# A request stemming from a form holding these inputs will include { "post" # => { "name" => "david", "address" => "hyacintvej" } }.
|
||||
# If the address input had been named "post[address][street]", the @params would have included
|
||||
# { "post" => { "address" => { "street" => "hyacintvej" } } }. There's no limit to the depth of the nesting.
|
||||
#
|
||||
# == Sessions
|
||||
#
|
||||
# Sessions allows you to store objects in memory 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.
|
||||
#
|
||||
# You can place objects in the session by using the <tt>@session</tt> hash:
|
||||
#
|
||||
# @session["person"] = Person.authenticate(user_name, password)
|
||||
#
|
||||
# And retrieved again through the same hash:
|
||||
#
|
||||
# Hello #{@session["person"]}
|
||||
#
|
||||
# Any object can be placed in the session (as long as it can be Marshalled). But remember that 1000 active sessions each storing a
|
||||
# 50kb object could lead to a 50MB memory overhead. In other words, think carefully about size and caching before resorting to the use
|
||||
# of the session.
|
||||
#
|
||||
# == Responses
|
||||
#
|
||||
# Each action results in a response, which holds the headers and document to be sent to the user's browser. The actual response
|
||||
# object is generated automatically through the use of renders and redirects, so it's normally nothing you'll need to be concerned about.
|
||||
#
|
||||
# == Renders
|
||||
#
|
||||
# Action Controller sends content to the user by using one of five rendering methods. The most versatile and common is the rendering
|
||||
# of a template. Included in the Action Pack is the Action View, which enables rendering of ERb templates. It's automatically configured.
|
||||
# The controller passes objects to the view by assigning instance variables:
|
||||
#
|
||||
# def show
|
||||
# @post = Post.find(@params["id"])
|
||||
# end
|
||||
#
|
||||
# Which are then automatically available to the view:
|
||||
#
|
||||
# Title: <%= @post.title %>
|
||||
#
|
||||
# You don't have to rely on the automated rendering. Especially actions that could result in the rendering of different templates will use
|
||||
# the manual rendering methods:
|
||||
#
|
||||
# def search
|
||||
# @results = Search.find(@params["query"])
|
||||
# case @results
|
||||
# when 0 then render "weblog/no_results"
|
||||
# when 1 then render_action "show"
|
||||
# when 2..10 then render_action "show_many"
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# Read more about writing ERb and Builder templates in link:classes/ActionView/Base.html.
|
||||
#
|
||||
# == Redirects
|
||||
#
|
||||
# Redirecting is what actions that update the model do when they're done. The <tt>save_post</tt> method shouldn't be responsible for also
|
||||
# showing the post once it's saved -- that's the job for <tt>show_post</tt>. So once <tt>save_post</tt> has completed its business, it'll
|
||||
# redirect to <tt>show_post</tt>. All redirects are external, which means that when the user refreshes his browser, it's not going to save
|
||||
# the post again, but rather just show it one more time.
|
||||
#
|
||||
# This sounds fairly simple, but the redirection is complicated by the quest for a phenomenon known as "pretty urls". Instead of accepting
|
||||
# the dreadful beings that is "weblog_controller?action=show&post_id=5", Action Controller goes out of its way to represent the former as
|
||||
# "/weblog/show/5". And this is even the simple case. As an example of a more advanced pretty url consider
|
||||
# "/library/books/ISBN/0743536703/show", which can be mapped to books_controller?action=show&type=ISBN&id=0743536703.
|
||||
#
|
||||
# Redirects work by rewriting the URL of the current action. So if the show action was called by "/library/books/ISBN/0743536703/show",
|
||||
# we can redirect to an edit action simply by doing <tt>redirect_to(:action => "edit")</tt>, which could throw the user to
|
||||
# "/library/books/ISBN/0743536703/edit". Naturally, you'll need to setup the .htaccess (or other means of URL rewriting for the web server)
|
||||
# to point to the proper controller and action in the first place, but once you have, it can be rewritten with ease.
|
||||
#
|
||||
# Let's consider a bunch of examples on how to go from "/library/books/ISBN/0743536703/edit" to somewhere else:
|
||||
#
|
||||
# redirect_to(:action => "show", :action_prefix => "XTC/123") =>
|
||||
# "http://www.singlefile.com/library/books/XTC/123/show"
|
||||
#
|
||||
# redirect_to(:path_params => {"type" => "EXBC"}) =>
|
||||
# "http://www.singlefile.com/library/books/EXBC/0743536703/show"
|
||||
#
|
||||
# redirect_to(:controller => "settings") =>
|
||||
# "http://www.singlefile.com/library/settings/"
|
||||
#
|
||||
# For more examples of redirecting options, have a look at the unit test in test/controller/url_test.rb. It's very readable and will give
|
||||
# you an excellent understanding of the different options and what they do.
|
||||
#
|
||||
# == Environments
|
||||
#
|
||||
# Action Controller works out of the box with CGI, FastCGI, and mod_ruby. CGI and mod_ruby controllers are triggered just the same using:
|
||||
#
|
||||
# WeblogController.process_cgi
|
||||
#
|
||||
# FastCGI controllers are triggered using:
|
||||
#
|
||||
# FCGI.each_cgi{ |cgi| WeblogController.process_cgi(cgi) }
|
||||
class Base
|
||||
include ClassInheritableAttributes
|
||||
|
||||
DEFAULT_RENDER_STATUS_CODE = "200 OK"
|
||||
|
||||
DEFAULT_SEND_FILE_OPTIONS = {
|
||||
:type => 'application/octet_stream',
|
||||
:disposition => 'attachment',
|
||||
:stream => true,
|
||||
:buffer_size => 4096
|
||||
}
|
||||
|
||||
|
||||
# Determines whether the view has access to controller internals @request, @response, @session, and @template.
|
||||
# By default, it does.
|
||||
@@view_controller_internals = true
|
||||
cattr_accessor :view_controller_internals
|
||||
|
||||
# All requests are considered local by default, so everyone will be exposed to detailed debugging screens on errors.
|
||||
# When the application is ready to go public, this should be set to false, and the protected method <tt>local_request?</tt>
|
||||
# should instead be implemented in the controller to determine when debugging screens should be shown.
|
||||
@@consider_all_requests_local = true
|
||||
cattr_accessor :consider_all_requests_local
|
||||
|
||||
# When turned on (which is default), all dependencies are included using "load". This mean that any change is instant in cached
|
||||
# environments like mod_ruby or FastCGI. When set to false, "require" is used, which is faster but requires server restart to
|
||||
# be effective.
|
||||
@@reload_dependencies = true
|
||||
cattr_accessor :reload_dependencies
|
||||
|
||||
# Template root determines the base from which template references will be made. So a call to render("test/template")
|
||||
# will be converted to "#{template_root}/test/template.rhtml".
|
||||
cattr_accessor :template_root
|
||||
|
||||
# The logger is used for generating information on the action run-time (including benchmarking) if available.
|
||||
# Can be set to nil for no logging. Compatible with both Ruby's own Logger and Log4r loggers.
|
||||
cattr_accessor :logger
|
||||
|
||||
# Determines which template class should be used by ActionController.
|
||||
cattr_accessor :template_class
|
||||
|
||||
# Turn on +ignore_missing_templates+ if you want to unit test actions without making the associated templates.
|
||||
cattr_accessor :ignore_missing_templates
|
||||
|
||||
# Holds the request object that's primarily used to get environment variables through access like
|
||||
# <tt>@request.env["REQUEST_URI"]</tt>.
|
||||
attr_accessor :request
|
||||
|
||||
# Holds a hash of all the GET, POST, and Url parameters passed to the action. Accessed like <tt>@params["post_id"]</tt>
|
||||
# to get the post_id. No type casts are made, so all values are returned as strings.
|
||||
attr_accessor :params
|
||||
|
||||
# Holds the response object that's primarily used to set additional HTTP headers through access like
|
||||
# <tt>@response.headers["Cache-Control"] = "no-cache"</tt>. Can also be used to access the final body HTML after a template
|
||||
# has been rendered through @response.body -- useful for <tt>after_filter</tt>s that wants to manipulate the output,
|
||||
# such as a OutputCompressionFilter.
|
||||
attr_accessor :response
|
||||
|
||||
# Holds a hash of objects in the session. Accessed like <tt>@session["person"]</tt> to get the object tied to the "person"
|
||||
# key. The session will hold any type of object as values, but the key should be a string.
|
||||
attr_accessor :session
|
||||
|
||||
# Holds a hash of header names and values. Accessed like <tt>@headers["Cache-Control"]</tt> to get the value of the Cache-Control
|
||||
# directive. Values should always be specified as strings.
|
||||
attr_accessor :headers
|
||||
|
||||
# Holds a hash of cookie names and values. Accessed like <tt>@cookies["user_name"]</tt> to get the value of the user_name cookie.
|
||||
# This hash is read-only. You set new cookies using the cookie method.
|
||||
attr_accessor :cookies
|
||||
|
||||
# Holds the hash of variables that are passed on to the template class to be made available to the view. This hash
|
||||
# is generated by taking a snapshot of all the instance variables in the current scope just before a template is rendered.
|
||||
attr_accessor :assigns
|
||||
|
||||
class << self
|
||||
# Factory for the standard create, process loop where the controller is discarded after processing.
|
||||
def process(request, response) #:nodoc:
|
||||
new.process(request, response)
|
||||
end
|
||||
|
||||
# Converts the class name from something like "OneModule::TwoModule::NeatController" to "NeatController".
|
||||
def controller_class_name
|
||||
Inflector.demodulize(name)
|
||||
end
|
||||
|
||||
# Converts the class name from something like "OneModule::TwoModule::NeatController" to "neat".
|
||||
def controller_name
|
||||
Inflector.underscore(controller_class_name.sub(/Controller/, ""))
|
||||
end
|
||||
|
||||
# Loads the <tt>file_name</tt> if reload_dependencies is true or requires if it's false.
|
||||
def require_or_load(file_name)
|
||||
reload_dependencies ? load("#{file_name}.rb") : require(file_name)
|
||||
end
|
||||
end
|
||||
|
||||
public
|
||||
# Extracts the action_name from the request parameters and performs that action.
|
||||
def process(request, response) #:nodoc:
|
||||
initialize_template_class(response)
|
||||
assign_shortcuts(request, response)
|
||||
initialize_current_url
|
||||
|
||||
log_processing unless logger.nil?
|
||||
perform_action
|
||||
close_session
|
||||
|
||||
return @response
|
||||
end
|
||||
|
||||
# Returns an URL that has been rewritten according to the hash of +options+ (for doing a complete redirect, use redirect_to). The
|
||||
# valid keys in options are specified below with an example going from "/library/books/ISBN/0743536703/show" (mapped to
|
||||
# books_controller?action=show&type=ISBN&id=0743536703):
|
||||
#
|
||||
# .---> controller .--> action
|
||||
# /library/books/ISBN/0743536703/show
|
||||
# '------> '--------------> action_prefix
|
||||
# controller_prefix
|
||||
#
|
||||
# * <tt>:controller_prefix</tt> - specifies the string before the controller name, which would be "/library" for the example.
|
||||
# Called with "/shop" gives "/shop/books/ISBN/0743536703/show".
|
||||
# * <tt>:controller</tt> - specifies a new controller and clears out everything after the controller name (including the action,
|
||||
# the pre- and suffix, and all params), so called with "settings" gives "/library/settings/".
|
||||
# * <tt>:action_prefix</tt> - specifies the string between the controller name and the action name, which would
|
||||
# be "/ISBN/0743536703" for the example. Called with "/XTC/123/" gives "/library/books/XTC/123/show".
|
||||
# * <tt>:action</tt> - specifies a new action, so called with "edit" gives "/library/books/ISBN/0743536703/edit"
|
||||
# * <tt>:action_suffix</tt> - specifies the string after the action name, which would be empty for the example.
|
||||
# Called with "/detailed" gives "/library/books/ISBN/0743536703/detailed".
|
||||
# * <tt>:path_params</tt> - specifies a hash that contains keys mapping to the request parameter names. In the example,
|
||||
# { "type" => "ISBN", "id" => "0743536703" } would be the path_params. It serves as another way of replacing part of
|
||||
# the action_prefix or action_suffix. So passing { "type" => "XTC" } would give "/library/books/XTC/0743536703/show".
|
||||
# * <tt>:id</tt> - shortcut where ":id => 5" can be used instead of specifying :path_params => { "id" => 5 }.
|
||||
# Called with "123" gives "/library/books/ISBN/123/show".
|
||||
# * <tt>:params</tt> - specifies a hash that represents the regular request parameters, such as { "cat" => 1,
|
||||
# "origin" => "there"} that would give "?cat=1&origin=there". Called with { "temporary" => 1 } in the example would give
|
||||
# "/library/books/ISBN/0743536703/show?temporary=1"
|
||||
# * <tt>:anchor</tt> - specifies the anchor name to be appended to the path. Called with "x14" would give
|
||||
# "/library/books/ISBN/0743536703/show#x14"
|
||||
# * <tt>:only_path</tt> - if true, returns the absolute URL (omitting the protocol, host name, and port).
|
||||
#
|
||||
# Naturally, you can combine multiple options in a single redirect. Examples:
|
||||
#
|
||||
# redirect_to(:controller_prefix => "/shop", :controller => "settings")
|
||||
# redirect_to(:action => "edit", :id => 3425)
|
||||
# redirect_to(:action => "edit", :path_params => { "type" => "XTC" }, :params => { "temp" => 1})
|
||||
# redirect_to(:action => "publish", :action_prefix => "/published", :anchor => "x14")
|
||||
#
|
||||
# Instead of passing an options hash, you can also pass a method reference in the form of a symbol. Consider this example:
|
||||
#
|
||||
# class WeblogController < ActionController::Base
|
||||
# def update
|
||||
# # do some update
|
||||
# redirect_to :dashboard_url
|
||||
# end
|
||||
#
|
||||
# protected
|
||||
# def dashboard_url
|
||||
# url_for :controller => (@project.active? ? "project" : "account"), :action => "dashboard"
|
||||
# end
|
||||
# end
|
||||
def url_for(options = {}, *parameters_for_method_reference) #:doc:
|
||||
case options
|
||||
when String then options
|
||||
when Symbol then send(options, *parameters_for_method_reference)
|
||||
when Hash then @url.rewrite(rewrite_options(options))
|
||||
end
|
||||
end
|
||||
|
||||
def module_name
|
||||
@params["module"]
|
||||
end
|
||||
|
||||
# Converts the class name from something like "OneModule::TwoModule::NeatController" to "NeatController".
|
||||
def controller_class_name
|
||||
self.class.controller_class_name
|
||||
end
|
||||
|
||||
# Converts the class name from something like "OneModule::TwoModule::NeatController" to "neat".
|
||||
def controller_name
|
||||
self.class.controller_name
|
||||
end
|
||||
|
||||
# Returns the name of the action this controller is processing.
|
||||
def action_name
|
||||
@params["action"] || "index"
|
||||
end
|
||||
|
||||
protected
|
||||
# Renders the template specified by <tt>template_name</tt>, which defaults to the name of the current controller and action.
|
||||
# So calling +render+ in WeblogController#show will attempt to render "#{template_root}/weblog/show.rhtml" or
|
||||
# "#{template_root}/weblog/show.rxml" (in that order). The template_root is set on the ActionController::Base class and is
|
||||
# shared by all controllers. It's also possible to pass a status code using the second parameter. This defaults to "200 OK",
|
||||
# but can be changed, such as by calling <tt>render("weblog/error", "500 Error")</tt>.
|
||||
def render(template_name = nil, status = nil) #:doc:
|
||||
render_file(template_name || default_template_name, status, true)
|
||||
end
|
||||
|
||||
# Works like render, but instead of requiring a full template name, you can get by with specifying the action name. So calling
|
||||
# <tt>render_action "show_many"</tt> in WeblogController#display will render "#{template_root}/weblog/show_many.rhtml" or
|
||||
# "#{template_root}/weblog/show_many.rxml".
|
||||
def render_action(action_name, status = nil) #:doc:
|
||||
render default_template_name(action_name), status
|
||||
end
|
||||
|
||||
# Works like render, but disregards the template_root and requires a full path to the template that needs to be rendered. Can be
|
||||
# used like <tt>render_file "/Users/david/Code/Ruby/template"</tt> to render "/Users/david/Code/Ruby/template.rhtml" or
|
||||
# "/Users/david/Code/Ruby/template.rxml".
|
||||
def render_file(template_path, status = nil, use_full_path = false) #:doc:
|
||||
assert_existance_of_template_file(template_path) if use_full_path
|
||||
logger.info("Rendering #{template_path} (#{status || DEFAULT_RENDER_STATUS_CODE})") unless logger.nil?
|
||||
|
||||
add_variables_to_assigns
|
||||
render_text(@template.render_file(template_path, use_full_path), status)
|
||||
end
|
||||
|
||||
# Renders the +template+ string, which is useful for rendering short templates you don't want to bother having a file for. So
|
||||
# you'd call <tt>render_template "Hello, <%= @user.name %>"</tt> to greet the current user. Or if you want to render as Builder
|
||||
# template, you could do <tt>render_template "xml.h1 @user.name", nil, "rxml"</tt>.
|
||||
def render_template(template, status = nil, type = "rhtml") #:doc:
|
||||
add_variables_to_assigns
|
||||
render_text(@template.render_template(type, template), status)
|
||||
end
|
||||
|
||||
# Renders the +text+ string without parsing it through any template engine. Useful for rendering static information as it's
|
||||
# considerably faster than rendering through the template engine.
|
||||
# Use block for response body if provided (useful for deferred rendering or streaming output).
|
||||
def render_text(text = nil, status = nil, &block) #:doc:
|
||||
add_variables_to_assigns
|
||||
@response.headers["Status"] = status || DEFAULT_RENDER_STATUS_CODE
|
||||
@response.body = block_given? ? block : text
|
||||
@performed_render = true
|
||||
end
|
||||
|
||||
# Sends the file by streaming it 4096 bytes at a time. This way the
|
||||
# whole file doesn't need to be read into memory at once. This makes
|
||||
# it feasible to send even large files.
|
||||
#
|
||||
# Be careful to sanitize the path parameter if it coming from a web
|
||||
# page. send_file(@params['path']) allows a malicious user to
|
||||
# download any file on your server.
|
||||
#
|
||||
# Options:
|
||||
# * <tt>:filename</tt> - suggests a filename for the browser to use.
|
||||
# Defaults to File.basename(path).
|
||||
# * <tt>:type</tt> - specifies an HTTP content type.
|
||||
# Defaults to 'application/octet-stream'.
|
||||
# * <tt>:disposition</tt> - specifies whether the file will be shown inline or downloaded.
|
||||
# Valid values are 'inline' and 'attachment' (default).
|
||||
# * <tt>:streaming</tt> - whether to send the file to the user agent as it is read (true)
|
||||
# or to read the entire file before sending (false). Defaults to true.
|
||||
# * <tt>:buffer_size</tt> - specifies size (in bytes) of the buffer used to stream the file.
|
||||
# Defaults to 4096.
|
||||
#
|
||||
# The default Content-Type and Content-Disposition headers are
|
||||
# set to download arbitrary binary files in as many browsers as
|
||||
# possible. IE versions 4, 5, 5.5, and 6 are all known to have
|
||||
# a variety of quirks (especially when downloading over SSL).
|
||||
#
|
||||
# Simple download:
|
||||
# send_file '/path/to.zip'
|
||||
#
|
||||
# Show a JPEG in browser:
|
||||
# send_file '/path/to.jpeg', :type => 'image/jpeg', :disposition => 'inline'
|
||||
#
|
||||
# Read about the other Content-* HTTP headers if you'd like to
|
||||
# provide the user with more information (such as Content-Description).
|
||||
# http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.11
|
||||
#
|
||||
# Also be aware that the document may be cached by proxies and browsers.
|
||||
# The Pragma and Cache-Control headers declare how the file may be cached
|
||||
# by intermediaries. They default to require clients to validate with
|
||||
# the server before releasing cached responses. See
|
||||
# http://www.mnot.net/cache_docs/ for an overview of web caching and
|
||||
# http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9
|
||||
# for the Cache-Control header spec.
|
||||
def send_file(path, options = {})
|
||||
raise MissingFile unless File.file?(path) and File.readable?(path)
|
||||
|
||||
options[:length] ||= File.size(path)
|
||||
options[:filename] ||= File.basename(path)
|
||||
send_file_headers! options
|
||||
|
||||
if options[:stream]
|
||||
render_text do
|
||||
logger.info "Streaming file #{path}" unless logger.nil?
|
||||
len = options[:buffer_size] || 4096
|
||||
File.open(path, 'rb') do |file|
|
||||
begin
|
||||
while true
|
||||
$stdout.syswrite file.sysread(len)
|
||||
end
|
||||
rescue EOFError
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
logger.info "Sending file #{path}" unless logger.nil?
|
||||
File.open(path, 'rb') { |file| render_text file.read }
|
||||
end
|
||||
end
|
||||
|
||||
# Send binary data to the user as a file download. May set content type, apparent file name,
|
||||
# and specify whether to show data inline or download as an attachment.
|
||||
#
|
||||
# Options:
|
||||
# * <tt>:filename</tt> - Suggests a filename for the browser to use.
|
||||
# * <tt>:type</tt> - specifies an HTTP content type.
|
||||
# Defaults to 'application/octet-stream'.
|
||||
# * <tt>:disposition</tt> - specifies whether the file will be shown inline or downloaded.
|
||||
# Valid values are 'inline' and 'attachment' (default).
|
||||
#
|
||||
# Generic data download:
|
||||
# send_data buffer
|
||||
#
|
||||
# Download a dynamically-generated tarball:
|
||||
# send_data generate_tgz('dir'), :filename => 'dir.tgz'
|
||||
#
|
||||
# Display an image Active Record in the browser:
|
||||
# send_data image.data, :type => image.content_type, :disposition => 'inline'
|
||||
#
|
||||
# See +send_file+ for more information on HTTP Content-* headers and caching.
|
||||
def send_data(data, options = {})
|
||||
logger.info "Sending data #{options[:filename]}" unless logger.nil?
|
||||
send_file_headers! options.merge(:length => data.size)
|
||||
render_text data
|
||||
end
|
||||
|
||||
def rewrite_options(options)
|
||||
if defaults = default_url_options(options)
|
||||
defaults.merge(options)
|
||||
else
|
||||
options
|
||||
end
|
||||
end
|
||||
|
||||
# Overwrite to implement a number of default options that all url_for-based methods will use. The default options should come in
|
||||
# the form of a hash, just like the one you would use for url_for directly. Example:
|
||||
#
|
||||
# def default_url_options(options)
|
||||
# { :controller_prefix => @project.active? ? "projects/" : "accounts/" }
|
||||
# end
|
||||
#
|
||||
# As you can infer from the example, this is mostly useful for situations where you want to centralize dynamic decisions about the
|
||||
# urls as they stem from the business domain. Please note that any individual url_for call can always override the defaults set
|
||||
# by this method.
|
||||
def default_url_options(options) #:doc:
|
||||
end
|
||||
|
||||
# Redirects the browser to an URL that has been rewritten according to the hash of +options+ using a "302 Moved" HTTP header.
|
||||
# See url_for for a description of the valid options.
|
||||
def redirect_to(options = {}, *parameters_for_method_reference) #:doc:
|
||||
if parameters_for_method_reference.empty?
|
||||
@response.redirected_to = options
|
||||
redirect_to_url(url_for(options))
|
||||
else
|
||||
@response.redirected_to, @response.redirected_to_method_params = options, parameters_for_method_reference
|
||||
redirect_to_url(url_for(options, *parameters_for_method_reference))
|
||||
end
|
||||
end
|
||||
|
||||
# Redirects the browser to the specified <tt>path</tt> within the current host (specified with a leading /). Used to sidestep
|
||||
# the URL rewriting and go directly to a known path. Example: <tt>redirect_to_path "/images/screenshot.jpg"</tt>.
|
||||
def redirect_to_path(path) #:doc:
|
||||
redirect_to_url(@request.protocol + @request.host_with_port + path)
|
||||
end
|
||||
|
||||
# Redirects the browser to the specified <tt>url</tt>. Used to redirect outside of the current application. Example:
|
||||
# <tt>redirect_to_url "http://www.rubyonrails.org"</tt>.
|
||||
def redirect_to_url(url) #:doc:
|
||||
logger.info("Redirected to #{url}") unless logger.nil?
|
||||
@response.redirect(url)
|
||||
@performed_redirect = true
|
||||
end
|
||||
|
||||
# Creates a new cookie that is sent along-side the next render or redirect command. API is the same as for CGI::Cookie.
|
||||
# Examples:
|
||||
#
|
||||
# cookie("name", "value1", "value2", ...)
|
||||
# cookie("name" => "name", "value" => "value")
|
||||
# cookie('name' => 'name',
|
||||
# 'value' => ['value1', 'value2', ...],
|
||||
# 'path' => 'path', # optional
|
||||
# 'domain' => 'domain', # optional
|
||||
# 'expires' => Time.now, # optional
|
||||
# 'secure' => true # optional
|
||||
# )
|
||||
def cookie(*options) #:doc:
|
||||
@response.headers["cookie"] << CGI::Cookie.new(*options)
|
||||
end
|
||||
|
||||
# Resets the session by clearsing out all the objects stored within and initializing a new session object.
|
||||
def reset_session #:doc:
|
||||
@request.reset_session
|
||||
@session = @request.session
|
||||
@response.session = @session
|
||||
end
|
||||
|
||||
private
|
||||
def initialize_template_class(response)
|
||||
begin
|
||||
response.template = template_class.new(template_root, {}, self)
|
||||
rescue
|
||||
raise "You must assign a template class through ActionController.template_class= before processing a request"
|
||||
end
|
||||
|
||||
@performed_render = @performed_redirect = false
|
||||
end
|
||||
|
||||
def assign_shortcuts(request, response)
|
||||
@request, @params, @cookies = request, request.parameters, request.cookies
|
||||
|
||||
@response = response
|
||||
@response.session = request.session
|
||||
|
||||
@session = @response.session
|
||||
@template = @response.template
|
||||
@assigns = @response.template.assigns
|
||||
@headers = @response.headers
|
||||
end
|
||||
|
||||
def initialize_current_url
|
||||
@url = UrlRewriter.new(@request, controller_name, action_name)
|
||||
end
|
||||
|
||||
def log_processing
|
||||
logger.info "\n\nProcessing #{controller_class_name}\##{action_name} (for #{request_origin})"
|
||||
logger.info " Parameters: #{@params.inspect}"
|
||||
end
|
||||
|
||||
def perform_action
|
||||
if action_methods.include?(action_name)
|
||||
send(action_name)
|
||||
render unless @performed_render || @performed_redirect
|
||||
elsif template_exists? && template_public?
|
||||
render
|
||||
else
|
||||
raise UnknownAction, "No action responded to #{action_name}", caller
|
||||
end
|
||||
end
|
||||
|
||||
def action_methods
|
||||
action_controller_classes = self.class.ancestors.reject{ |a| [Object, Kernel].include?(a) }
|
||||
action_controller_classes.inject([]) { |action_methods, klass| action_methods + klass.instance_methods(false) }
|
||||
end
|
||||
|
||||
def add_variables_to_assigns
|
||||
add_instance_variables_to_assigns
|
||||
add_class_variables_to_assigns if view_controller_internals
|
||||
end
|
||||
|
||||
def add_instance_variables_to_assigns
|
||||
protected_variables_cache = protected_instance_variables
|
||||
instance_variables.each do |var|
|
||||
next if protected_variables_cache.include?(var)
|
||||
@assigns[var[1..-1]] = instance_variable_get(var)
|
||||
end
|
||||
end
|
||||
|
||||
def add_class_variables_to_assigns
|
||||
%w( template_root logger template_class ignore_missing_templates ).each do |cvar|
|
||||
@assigns[cvar] = self.send(cvar)
|
||||
end
|
||||
end
|
||||
|
||||
def protected_instance_variables
|
||||
if view_controller_internals
|
||||
[ "@assigns", "@performed_redirect", "@performed_render" ]
|
||||
else
|
||||
[ "@assigns", "@performed_redirect", "@performed_render", "@request", "@response", "@session", "@cookies", "@template" ]
|
||||
end
|
||||
end
|
||||
|
||||
def request_origin
|
||||
"#{@request.remote_ip} at #{Time.now.to_s}"
|
||||
end
|
||||
|
||||
def close_session
|
||||
@session.close unless @session.nil? || Hash === @session
|
||||
end
|
||||
|
||||
def template_exists?(template_name = default_template_name)
|
||||
@template.file_exists?(template_name)
|
||||
end
|
||||
|
||||
def template_public?(template_name = default_template_name)
|
||||
@template.file_public?(template_name)
|
||||
end
|
||||
|
||||
def assert_existance_of_template_file(template_name)
|
||||
unless template_exists?(template_name) || ignore_missing_templates
|
||||
full_template_path = @template.send(:full_template_path, template_name, 'rhtml')
|
||||
template_type = (template_name =~ /layouts/i) ? 'layout' : 'template'
|
||||
raise(MissingTemplate, "Missing #{template_type} #{full_template_path}")
|
||||
end
|
||||
end
|
||||
|
||||
def send_file_headers!(options)
|
||||
options.update(DEFAULT_SEND_FILE_OPTIONS.merge(options))
|
||||
[:length, :type, :disposition].each do |arg|
|
||||
raise ArgumentError, ":#{arg} option required" if options[arg].nil?
|
||||
end
|
||||
|
||||
disposition = options[:disposition] || 'attachment'
|
||||
disposition <<= %(; filename="#{options[:filename]}") if options[:filename]
|
||||
|
||||
@headers.update(
|
||||
'Content-Length' => options[:length],
|
||||
'Content-Type' => options[:type],
|
||||
'Content-Disposition' => disposition,
|
||||
'Content-Transfer-Encoding' => 'binary'
|
||||
);
|
||||
end
|
||||
|
||||
def default_template_name(default_action_name = action_name)
|
||||
module_name ? "#{module_name}/#{controller_name}/#{default_action_name}" : "#{controller_name}/#{default_action_name}"
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,49 @@
|
|||
require 'benchmark'
|
||||
|
||||
module ActionController #:nodoc:
|
||||
# The benchmarking module times the performance of actions and reports to the logger. If the Active Record
|
||||
# package has been included, a separate timing section for database calls will be added as well.
|
||||
module Benchmarking #:nodoc:
|
||||
def self.append_features(base)
|
||||
super
|
||||
base.class_eval {
|
||||
alias_method :perform_action_without_benchmark, :perform_action
|
||||
alias_method :perform_action, :perform_action_with_benchmark
|
||||
|
||||
alias_method :render_without_benchmark, :render
|
||||
alias_method :render, :render_with_benchmark
|
||||
}
|
||||
end
|
||||
|
||||
def render_with_benchmark(template_name = default_template_name, status = "200 OK")
|
||||
if logger.nil?
|
||||
render_without_benchmark(template_name, status)
|
||||
else
|
||||
@rendering_runtime = Benchmark::measure{ render_without_benchmark(template_name, status) }.real
|
||||
end
|
||||
end
|
||||
|
||||
def perform_action_with_benchmark
|
||||
if logger.nil?
|
||||
perform_action_without_benchmark
|
||||
else
|
||||
runtime = [Benchmark::measure{ perform_action_without_benchmark }.real, 0.0001].max
|
||||
log_message = "Completed in #{sprintf("%4f", runtime)} (#{(1 / runtime).floor} reqs/sec)"
|
||||
log_message << rendering_runtime(runtime) if @rendering_runtime
|
||||
log_message << active_record_runtime(runtime) if Object.const_defined?("ActiveRecord") && ActiveRecord::Base.connected?
|
||||
logger.info(log_message)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def rendering_runtime(runtime)
|
||||
" | Rendering: #{sprintf("%f", @rendering_runtime)} (#{sprintf("%d", (@rendering_runtime / runtime) * 100)}%)"
|
||||
end
|
||||
|
||||
def active_record_runtime(runtime)
|
||||
db_runtime = ActiveRecord::Base.connection.reset_runtime
|
||||
db_percentage = (db_runtime / runtime) * 100
|
||||
" | DB: #{sprintf("%f", db_runtime)} (#{sprintf("%d", db_percentage)}%)"
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,43 @@
|
|||
require 'cgi'
|
||||
require 'cgi/session'
|
||||
require 'cgi/session/pstore'
|
||||
require 'action_controller/cgi_ext/cgi_methods'
|
||||
|
||||
# Wrapper around the CGIMethods that have been secluded to allow testing without
|
||||
# an instatiated CGI object
|
||||
class CGI #:nodoc:
|
||||
class << self
|
||||
alias :escapeHTML_fail_on_nil :escapeHTML
|
||||
|
||||
def escapeHTML(string)
|
||||
escapeHTML_fail_on_nil(string) unless string.nil?
|
||||
end
|
||||
end
|
||||
|
||||
# Returns a parameter hash including values from both the request (POST/GET)
|
||||
# and the query string with the latter taking precedence.
|
||||
def parameters
|
||||
request_parameters.update(query_parameters)
|
||||
end
|
||||
|
||||
def query_parameters
|
||||
CGIMethods.parse_query_parameters(query_string)
|
||||
end
|
||||
|
||||
def request_parameters
|
||||
CGIMethods.parse_request_parameters(params)
|
||||
end
|
||||
|
||||
def redirect(where)
|
||||
header({
|
||||
"Status" => "302 Moved",
|
||||
"location" => "#{where}"
|
||||
})
|
||||
end
|
||||
|
||||
def session(parameters = nil)
|
||||
parameters = {} if parameters.nil?
|
||||
parameters['database_manager'] = CGI::Session::PStore
|
||||
CGI::Session.new(self, parameters)
|
||||
end
|
||||
end
|
|
@ -0,0 +1,91 @@
|
|||
require 'cgi'
|
||||
|
||||
# Static methods for parsing the query and request parameters that can be used in
|
||||
# a CGI extension class or testing in isolation.
|
||||
class CGIMethods #:nodoc:
|
||||
public
|
||||
# Returns a hash with the pairs from the query string. The implicit hash construction that is done in
|
||||
# parse_request_params is not done here.
|
||||
def CGIMethods.parse_query_parameters(query_string)
|
||||
parsed_params = {}
|
||||
|
||||
query_string.split(/[&;]/).each { |p|
|
||||
k, v = p.split('=')
|
||||
|
||||
k = CGI.unescape(k) unless k.nil?
|
||||
v = CGI.unescape(v) unless v.nil?
|
||||
|
||||
if k =~ /(.*)\[\]$/
|
||||
if parsed_params.has_key? $1
|
||||
parsed_params[$1] << v
|
||||
else
|
||||
parsed_params[$1] = [v]
|
||||
end
|
||||
else
|
||||
parsed_params[k] = v.nil? ? nil : v
|
||||
end
|
||||
}
|
||||
|
||||
return parsed_params
|
||||
end
|
||||
|
||||
# Returns the request (POST/GET) parameters in a parsed form where pairs such as "customer[address][street]" /
|
||||
# "Somewhere cool!" are translated into a full hash hierarchy, like
|
||||
# { "customer" => { "address" => { "street" => "Somewhere cool!" } } }
|
||||
def CGIMethods.parse_request_parameters(params)
|
||||
parsed_params = {}
|
||||
|
||||
for key, value in params
|
||||
value = [value] if key =~ /.*\[\]$/
|
||||
CGIMethods.build_deep_hash(
|
||||
CGIMethods.get_typed_value(value[0]),
|
||||
parsed_params,
|
||||
CGIMethods.get_levels(key)
|
||||
)
|
||||
end
|
||||
|
||||
return parsed_params
|
||||
end
|
||||
|
||||
private
|
||||
def CGIMethods.get_typed_value(value)
|
||||
if value.respond_to?(:content_type) && !value.content_type.empty?
|
||||
# Uploaded file
|
||||
value
|
||||
elsif value.respond_to?(:read)
|
||||
# Value as part of a multipart request
|
||||
value.read
|
||||
elsif value.class == Array
|
||||
value
|
||||
else
|
||||
# Standard value (not a multipart request)
|
||||
value.to_s
|
||||
end
|
||||
end
|
||||
|
||||
def CGIMethods.get_levels(key_string)
|
||||
return [] if key_string.nil? or key_string.empty?
|
||||
|
||||
levels = []
|
||||
main, existance = /(\w+)(\[)?.?/.match(key_string).captures
|
||||
levels << main
|
||||
|
||||
unless existance.nil?
|
||||
hash_part = key_string.sub(/\w+\[/, "")
|
||||
hash_part.slice!(-1, 1)
|
||||
levels += hash_part.split(/\]\[/)
|
||||
end
|
||||
|
||||
levels
|
||||
end
|
||||
|
||||
def CGIMethods.build_deep_hash(value, hash, levels)
|
||||
if levels.length == 0
|
||||
value;
|
||||
elsif hash.nil?
|
||||
{ levels.first => CGIMethods.build_deep_hash(value, nil, levels[1..-1]) }
|
||||
else
|
||||
hash.update({ levels.first => CGIMethods.build_deep_hash(value, hash[levels.first], levels[1..-1]) })
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,124 @@
|
|||
require 'action_controller/cgi_ext/cgi_ext'
|
||||
require 'action_controller/support/cookie_performance_fix'
|
||||
require 'action_controller/session/drb_store'
|
||||
require 'action_controller/session/active_record_store'
|
||||
|
||||
module ActionController #:nodoc:
|
||||
class Base
|
||||
# Process a request extracted from an CGI object and return a response. Pass false as <tt>session_options</tt> to disable
|
||||
# sessions (large performance increase if sessions are not needed). The <tt>session_options</tt> are the same as for CGI::Session:
|
||||
#
|
||||
# * <tt>:database_manager</tt> - standard options are CGI::Session::FileStore, CGI::Session::MemoryStore, and CGI::Session::PStore
|
||||
# (default). Additionally, there is CGI::Session::DRbStore and CGI::Session::ActiveRecordStore. Read more about these in
|
||||
# lib/action_controller/session.
|
||||
# * <tt>:session_key</tt> - the parameter name used for the session id. Defaults to '_session_id'.
|
||||
# * <tt>:session_id</tt> - the session id to use. If not provided, then it is retrieved from the +session_key+ parameter
|
||||
# of the request, or automatically generated for a new session.
|
||||
# * <tt>:new_session</tt> - if true, force creation of a new session. If not set, a new session is only created if none currently
|
||||
# exists. If false, a new session is never created, and if none currently exists and the +session_id+ option is not set,
|
||||
# an ArgumentError is raised.
|
||||
# * <tt>:session_expires</tt> - the time the current session expires, as a +Time+ object. If not set, the session will continue
|
||||
# indefinitely.
|
||||
# * <tt>:session_domain</tt> - the hostname domain for which this session is valid. If not set, defaults to the hostname of the
|
||||
# server.
|
||||
# * <tt>:session_secure</tt> - if +true+, this session will only work over HTTPS.
|
||||
# * <tt>:session_path</tt> - the path for which this session applies. Defaults to the directory of the CGI script.
|
||||
def self.process_cgi(cgi = CGI.new, session_options = {})
|
||||
new.process_cgi(cgi, session_options)
|
||||
end
|
||||
|
||||
def process_cgi(cgi, session_options = {}) #:nodoc:
|
||||
process(CgiRequest.new(cgi, session_options), CgiResponse.new(cgi)).out
|
||||
end
|
||||
end
|
||||
|
||||
class CgiRequest < AbstractRequest #:nodoc:
|
||||
attr_accessor :cgi
|
||||
|
||||
DEFAULT_SESSION_OPTIONS =
|
||||
{ "database_manager" => CGI::Session::PStore, "prefix" => "ruby_sess.", "session_path" => "/" }
|
||||
|
||||
def initialize(cgi, session_options = {})
|
||||
@cgi = cgi
|
||||
@session_options = session_options
|
||||
super()
|
||||
end
|
||||
|
||||
def query_parameters
|
||||
@cgi.query_string ? CGIMethods.parse_query_parameters(@cgi.query_string) : {}
|
||||
end
|
||||
|
||||
def request_parameters
|
||||
CGIMethods.parse_request_parameters(@cgi.params)
|
||||
end
|
||||
|
||||
def env
|
||||
@cgi.send(:env_table)
|
||||
end
|
||||
|
||||
def cookies
|
||||
@cgi.cookies.freeze
|
||||
end
|
||||
|
||||
def host
|
||||
env["HTTP_X_FORWARDED_HOST"] || @cgi.host.split(":").first
|
||||
end
|
||||
|
||||
def session
|
||||
return @session unless @session.nil?
|
||||
begin
|
||||
@session = (@session_options == false ? {} : CGI::Session.new(@cgi, DEFAULT_SESSION_OPTIONS.merge(@session_options)))
|
||||
@session["__valid_session"]
|
||||
return @session
|
||||
rescue ArgumentError => e
|
||||
@session.delete if @session
|
||||
raise(
|
||||
ActionController::SessionRestoreError,
|
||||
"Session contained objects where the class definition wasn't available. " +
|
||||
"Remember to require classes for all objects kept in the session. " +
|
||||
"The session has been deleted."
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def reset_session
|
||||
@session.delete
|
||||
@session = (@session_options == false ? {} : new_session)
|
||||
end
|
||||
|
||||
def method_missing(method_id, *arguments)
|
||||
@cgi.send(method_id, *arguments) rescue super
|
||||
end
|
||||
|
||||
private
|
||||
def new_session
|
||||
CGI::Session.new(@cgi, DEFAULT_SESSION_OPTIONS.merge(@session_options).merge("new_session" => true))
|
||||
end
|
||||
end
|
||||
|
||||
class CgiResponse < AbstractResponse #:nodoc:
|
||||
def initialize(cgi)
|
||||
@cgi = cgi
|
||||
super()
|
||||
end
|
||||
|
||||
def out
|
||||
convert_content_type!(@headers)
|
||||
$stdout.binmode if $stdout.respond_to?(:binmode)
|
||||
print @cgi.header(@headers)
|
||||
if @body.respond_to?(:call)
|
||||
@body.call(self)
|
||||
else
|
||||
print @body
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def convert_content_type!(headers)
|
||||
if headers["Content-Type"]
|
||||
headers["type"] = headers["Content-Type"]
|
||||
headers.delete "Content-Type"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,49 @@
|
|||
module ActionController #:nodoc:
|
||||
module Dependencies #:nodoc:
|
||||
def self.append_features(base)
|
||||
super
|
||||
base.extend(ClassMethods)
|
||||
end
|
||||
|
||||
module ClassMethods
|
||||
def model(*models)
|
||||
require_dependencies(:model, models)
|
||||
depend_on(:model, models)
|
||||
end
|
||||
|
||||
def service(*services)
|
||||
require_dependencies(:service, services)
|
||||
depend_on(:service, services)
|
||||
end
|
||||
|
||||
def observer(*observers)
|
||||
require_dependencies(:observer, observers)
|
||||
depend_on(:observer, observers)
|
||||
instantiate_observers(observers)
|
||||
end
|
||||
|
||||
def dependencies_on(layer) # :nodoc:
|
||||
read_inheritable_attribute("#{layer}_dependencies")
|
||||
end
|
||||
|
||||
def depend_on(layer, dependencies)
|
||||
write_inheritable_array("#{layer}_dependencies", dependencies)
|
||||
end
|
||||
|
||||
private
|
||||
def instantiate_observers(observers)
|
||||
observers.flatten.each { |observer| Object.const_get(Inflector.classify(observer.to_s)).instance }
|
||||
end
|
||||
|
||||
def require_dependencies(layer, dependencies)
|
||||
dependencies.flatten.each do |dependency|
|
||||
begin
|
||||
require_or_load(dependency.to_s)
|
||||
rescue LoadError
|
||||
raise LoadError, "Missing #{layer} #{dependency}.rb"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,279 @@
|
|||
module ActionController #:nodoc:
|
||||
module Filters #:nodoc:
|
||||
def self.append_features(base)
|
||||
super
|
||||
base.extend(ClassMethods)
|
||||
base.class_eval { include ActionController::Filters::InstanceMethods }
|
||||
end
|
||||
|
||||
# Filters enable controllers to run shared pre and post processing code for its actions. These filters can be used to do
|
||||
# authentication, caching, or auditing before the intended action is performed. Or to do localization or output
|
||||
# compression after the action has been performed.
|
||||
#
|
||||
# Filters have access to the request, response, and all the instance variables set by other filters in the chain
|
||||
# or by the action (in the case of after filters). Additionally, it's possible for a pre-processing <tt>before_filter</tt>
|
||||
# to halt the processing before the intended action is processed by returning false. This is especially useful for
|
||||
# filters like authentication where you're not interested in allowing the action to be performed if the proper
|
||||
# credentials are not in order.
|
||||
#
|
||||
# == Filter inheritance
|
||||
#
|
||||
# Controller inheritance hierarchies share filters downwards, but subclasses can also add new filters without
|
||||
# affecting the superclass. For example:
|
||||
#
|
||||
# class BankController < ActionController::Base
|
||||
# before_filter :audit
|
||||
#
|
||||
# private
|
||||
# def audit
|
||||
# # record the action and parameters in an audit log
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# class VaultController < BankController
|
||||
# before_filter :verify_credentials
|
||||
#
|
||||
# private
|
||||
# def verify_credentials
|
||||
# # make sure the user is allowed into the vault
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# Now any actions performed on the BankController will have the audit method called before. On the VaultController,
|
||||
# first the audit method is called, then the verify_credentials method. If the audit method returns false, then
|
||||
# verify_credentials and the intended action is never called.
|
||||
#
|
||||
# == Filter types
|
||||
#
|
||||
# A filter can take one of three forms: method reference (symbol), external class, or inline method (proc). The first
|
||||
# is the most common and works by referencing a protected or private method somewhere in the inheritance hierarchy of
|
||||
# the controller by use of a symbol. In the bank example above, both BankController and VaultController use this form.
|
||||
#
|
||||
# Using an external class makes for more easily reused generic filters, such as output compression. External filter classes
|
||||
# are implemented by having a static +filter+ method on any class and then passing this class to the filter method. Example:
|
||||
#
|
||||
# class OutputCompressionFilter
|
||||
# def self.filter(controller)
|
||||
# controller.response.body = compress(controller.response.body)
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# class NewspaperController < ActionController::Base
|
||||
# after_filter OutputCompressionFilter
|
||||
# end
|
||||
#
|
||||
# The filter method is passed the controller instance and is hence granted access to all aspects of the controller and can
|
||||
# manipulate them as it sees fit.
|
||||
#
|
||||
# The inline method (using a proc) can be used to quickly do something small that doesn't require a lot of explanation.
|
||||
# Or just as a quick test. It works like this:
|
||||
#
|
||||
# class WeblogController < ActionController::Base
|
||||
# before_filter { |controller| return false if controller.params["stop_action"] }
|
||||
# end
|
||||
#
|
||||
# As you can see, the block expects to be passed the controller after it has assigned the request to the internal variables.
|
||||
# This means that the block has access to both the request and response objects complete with convenience methods for params,
|
||||
# session, template, and assigns. Note: The inline method doesn't strictly has to be a block. Any object that responds to call
|
||||
# and returns 1 or -1 on arity will do (such as a Proc or an Method object).
|
||||
#
|
||||
# == Filter chain ordering
|
||||
#
|
||||
# Using <tt>before_filter</tt> and <tt>after_filter</tt> appends the specified filters to the existing chain. That's usually
|
||||
# just fine, but some times you care more about the order in which the filters are executed. When that's the case, you
|
||||
# can use <tt>prepend_before_filter</tt> and <tt>prepend_after_filter</tt>. Filters added by these methods will be put at the
|
||||
# beginning of their respective chain and executed before the rest. For example:
|
||||
#
|
||||
# class ShoppingController
|
||||
# before_filter :verify_open_shop
|
||||
#
|
||||
# class CheckoutController
|
||||
# prepend_before_filter :ensure_items_in_cart, :ensure_items_in_stock
|
||||
#
|
||||
# The filter chain for the CheckoutController is now <tt>:ensure_items_in_cart, :ensure_items_in_stock,</tt>
|
||||
# <tt>:verify_open_shop</tt>. So if either of the ensure filters return false, we'll never get around to see if the shop
|
||||
# is open or not.
|
||||
#
|
||||
# You may pass multiple filter arguments of each type as well as a filter block.
|
||||
# If a block is given, it is treated as the last argument.
|
||||
#
|
||||
# == Around filters
|
||||
#
|
||||
# In addition to the individual before and after filters, it's also possible to specify that a single object should handle
|
||||
# both the before and after call. That's especially usefuly when you need to keep state active between the before and after,
|
||||
# such as the example of a benchmark filter below:
|
||||
#
|
||||
# class WeblogController < ActionController::Base
|
||||
# around_filter BenchmarkingFilter.new
|
||||
#
|
||||
# # Before this action is performed, BenchmarkingFilter#before(controller) is executed
|
||||
# def index
|
||||
# end
|
||||
# # After this action has been performed, BenchmarkingFilter#after(controller) is executed
|
||||
# end
|
||||
#
|
||||
# class BenchmarkingFilter
|
||||
# def initialize
|
||||
# @runtime
|
||||
# end
|
||||
#
|
||||
# def before
|
||||
# start_timer
|
||||
# end
|
||||
#
|
||||
# def after
|
||||
# stop_timer
|
||||
# report_result
|
||||
# end
|
||||
# end
|
||||
module ClassMethods
|
||||
# The passed <tt>filters</tt> will be appended to the array of filters that's run _before_ actions
|
||||
# on this controller are performed.
|
||||
def append_before_filter(*filters, &block)
|
||||
filters << block if block_given?
|
||||
append_filter_to_chain("before", filters)
|
||||
end
|
||||
|
||||
# The passed <tt>filters</tt> will be prepended to the array of filters that's run _before_ actions
|
||||
# on this controller are performed.
|
||||
def prepend_before_filter(*filters, &block)
|
||||
filters << block if block_given?
|
||||
prepend_filter_to_chain("before", filters)
|
||||
end
|
||||
|
||||
# Short-hand for append_before_filter since that's the most common of the two.
|
||||
alias :before_filter :append_before_filter
|
||||
|
||||
# The passed <tt>filters</tt> will be appended to the array of filters that's run _after_ actions
|
||||
# on this controller are performed.
|
||||
def append_after_filter(*filters, &block)
|
||||
filters << block if block_given?
|
||||
append_filter_to_chain("after", filters)
|
||||
end
|
||||
|
||||
# The passed <tt>filters</tt> will be prepended to the array of filters that's run _after_ actions
|
||||
# on this controller are performed.
|
||||
def prepend_after_filter(*filters, &block)
|
||||
filters << block if block_given?
|
||||
prepend_filter_to_chain("after", filters)
|
||||
end
|
||||
|
||||
# Short-hand for append_after_filter since that's the most common of the two.
|
||||
alias :after_filter :append_after_filter
|
||||
|
||||
# The passed <tt>filters</tt> will have their +before+ method appended to the array of filters that's run both before actions
|
||||
# on this controller are performed and have their +after+ method prepended to the after actions. The filter objects must all
|
||||
# respond to both +before+ and +after+. So if you do append_around_filter A.new, B.new, the callstack will look like:
|
||||
#
|
||||
# B#before
|
||||
# A#before
|
||||
# A#after
|
||||
# B#after
|
||||
def append_around_filter(filters)
|
||||
for filter in [filters].flatten
|
||||
ensure_filter_responds_to_before_and_after(filter)
|
||||
append_before_filter { |c| filter.before(c) }
|
||||
prepend_after_filter { |c| filter.after(c) }
|
||||
end
|
||||
end
|
||||
|
||||
# The passed <tt>filters</tt> will have their +before+ method prepended to the array of filters that's run both before actions
|
||||
# on this controller are performed and have their +after+ method appended to the after actions. The filter objects must all
|
||||
# respond to both +before+ and +after+. So if you do prepend_around_filter A.new, B.new, the callstack will look like:
|
||||
#
|
||||
# A#before
|
||||
# B#before
|
||||
# B#after
|
||||
# A#after
|
||||
def prepend_around_filter(filters)
|
||||
for filter in [filters].flatten
|
||||
ensure_filter_responds_to_before_and_after(filter)
|
||||
prepend_before_filter { |c| filter.before(c) }
|
||||
append_after_filter { |c| filter.after(c) }
|
||||
end
|
||||
end
|
||||
|
||||
# Short-hand for append_around_filter since that's the most common of the two.
|
||||
alias :around_filter :append_around_filter
|
||||
|
||||
# Returns all the before filters for this class and all its ancestors.
|
||||
def before_filters #:nodoc:
|
||||
read_inheritable_attribute("before_filters")
|
||||
end
|
||||
|
||||
# Returns all the after filters for this class and all its ancestors.
|
||||
def after_filters #:nodoc:
|
||||
read_inheritable_attribute("after_filters")
|
||||
end
|
||||
|
||||
private
|
||||
def append_filter_to_chain(condition, filters)
|
||||
write_inheritable_array("#{condition}_filters", filters)
|
||||
end
|
||||
|
||||
def prepend_filter_to_chain(condition, filters)
|
||||
write_inheritable_attribute("#{condition}_filters", filters + read_inheritable_attribute("#{condition}_filters"))
|
||||
end
|
||||
|
||||
def ensure_filter_responds_to_before_and_after(filter)
|
||||
unless filter.respond_to?(:before) && filter.respond_to?(:after)
|
||||
raise ActionControllerError, "Filter object must respond to both before and after"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
module InstanceMethods # :nodoc:
|
||||
def self.append_features(base)
|
||||
super
|
||||
base.class_eval {
|
||||
alias_method :perform_action_without_filters, :perform_action
|
||||
alias_method :perform_action, :perform_action_with_filters
|
||||
}
|
||||
end
|
||||
|
||||
def perform_action_with_filters
|
||||
return if before_action == false
|
||||
perform_action_without_filters
|
||||
after_action
|
||||
end
|
||||
|
||||
# Calls all the defined before-filter filters, which are added by using "before_filter :method".
|
||||
# If any of the filters return false, no more filters will be executed and the action is aborted.
|
||||
def before_action #:doc:
|
||||
call_filters(self.class.before_filters)
|
||||
end
|
||||
|
||||
# Calls all the defined after-filter filters, which are added by using "after_filter :method".
|
||||
# If any of the filters return false, no more filters will be executed.
|
||||
def after_action #:doc:
|
||||
call_filters(self.class.after_filters)
|
||||
end
|
||||
|
||||
private
|
||||
def call_filters(filters)
|
||||
filters.each do |filter|
|
||||
if Symbol === filter
|
||||
if self.send(filter) == false then return false end
|
||||
elsif filter_block?(filter)
|
||||
if filter.call(self) == false then return false end
|
||||
elsif filter_class?(filter)
|
||||
if filter.filter(self) == false then return false end
|
||||
else
|
||||
raise(
|
||||
ActionControllerError,
|
||||
"Filters need to be either a symbol, proc/method, or class implementing a static filter method"
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def filter_block?(filter)
|
||||
filter.respond_to?("call") && (filter.arity == 1 || filter.arity == -1)
|
||||
end
|
||||
|
||||
def filter_class?(filter)
|
||||
filter.respond_to?("filter")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,65 @@
|
|||
module ActionController #:nodoc:
|
||||
# The flash provides a way to pass temporary objects between actions. Anything you place in the flash will be exposed
|
||||
# to the very next action and then cleared out. This is a great way of doing notices and alerts, such as a create action
|
||||
# that sets <tt>flash["notice"] = "Succesfully created"</tt> before redirecting to a display action that can then expose
|
||||
# the flash to its template. Actually, that exposure is automatically done. Example:
|
||||
#
|
||||
# class WeblogController < ActionController::Base
|
||||
# def create
|
||||
# # save post
|
||||
# flash["notice"] = "Succesfully created post"
|
||||
# redirect_to :action => "display", :params => { "id" => post.id }
|
||||
# end
|
||||
#
|
||||
# def display
|
||||
# # doesn't need to assign the flash notice to the template, that's done automatically
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# display.rhtml
|
||||
# <% if @flash["notice"] %><div class="notice"><%= @flash["notice"] %></div><% end %>
|
||||
#
|
||||
# This example just places a string in the flash, but you can put any object in there. And of course, you can put as many
|
||||
# as you like at a time too. Just remember: They'll be gone by the time the next action has been performed.
|
||||
module Flash
|
||||
def self.append_features(base) #:nodoc:
|
||||
super
|
||||
base.before_filter(:fire_flash)
|
||||
base.after_filter(:clear_flash)
|
||||
end
|
||||
|
||||
protected
|
||||
# Access the contents of the flash. Use <tt>flash["notice"]</tt> to read a notice you put there or
|
||||
# <tt>flash["notice"] = "hello"</tt> to put a new one.
|
||||
def flash #:doc:
|
||||
if @session["flash"].nil?
|
||||
@session["flash"] = {}
|
||||
@session["flashes"] ||= 0
|
||||
end
|
||||
@session["flash"]
|
||||
end
|
||||
|
||||
# Can be called by any action that would like to keep the current content of the flash around for one more action.
|
||||
def keep_flash #:doc:
|
||||
@session["flashes"] = 0
|
||||
end
|
||||
|
||||
private
|
||||
# Records that the contents of @session["flash"] was flashed to the action
|
||||
def fire_flash
|
||||
if @session["flash"]
|
||||
@session["flashes"] += 1 unless @session["flash"].empty?
|
||||
@assigns["flash"] = @session["flash"]
|
||||
else
|
||||
@assigns["flash"] = {}
|
||||
end
|
||||
end
|
||||
|
||||
def clear_flash
|
||||
if @session["flash"] && (@session["flashes"].nil? || @session["flashes"] >= 1)
|
||||
@session["flash"] = {}
|
||||
@session["flashes"] = 0
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,100 @@
|
|||
module ActionController #:nodoc:
|
||||
module Helpers #:nodoc:
|
||||
def self.append_features(base)
|
||||
super
|
||||
base.class_eval { class << self; alias_method :inherited_without_helper, :inherited; end }
|
||||
base.extend(ClassMethods)
|
||||
end
|
||||
|
||||
# The template helpers serves to relieve the templates from including the same inline code again and again. It's a
|
||||
# set of standardized methods for working with forms (FormHelper), dates (DateHelper), texts (TextHelper), and
|
||||
# Active Records (ActiveRecordHelper) that's available to all templates by default.
|
||||
#
|
||||
# It's also really easy to make your own helpers and it's much encouraged to keep the template files free
|
||||
# from complicated logic. It's even encouraged to bundle common compositions of methods from other helpers
|
||||
# (often the common helpers) as they're used by the specific application.
|
||||
#
|
||||
# module MyHelper
|
||||
# def hello_world() "hello world" end
|
||||
# end
|
||||
#
|
||||
# MyHelper can now be included in a controller, like this:
|
||||
#
|
||||
# class MyController < ActionController::Base
|
||||
# helper :my_helper
|
||||
# end
|
||||
#
|
||||
# ...and, same as above, used in any template rendered from MyController, like this:
|
||||
#
|
||||
# Let's hear what the helper has to say: <tt><%= hello_world %></tt>
|
||||
module ClassMethods
|
||||
# Makes all the (instance) methods in the helper module available to templates rendered through this controller.
|
||||
# See ActionView::Helpers (link:classes/ActionView/Helpers.html) for more about making your own helper modules
|
||||
# available to the templates.
|
||||
def add_template_helper(helper_module)
|
||||
template_class.class_eval "include #{helper_module}"
|
||||
end
|
||||
|
||||
# Declare a helper. If you use this method in your controller, you don't
|
||||
# have to do the +self.append_features+ incantation in your helper class.
|
||||
# helper :foo
|
||||
# requires 'foo_helper' and includes FooHelper in the template class.
|
||||
# helper FooHelper
|
||||
# includes FooHelper in the template class.
|
||||
# helper { def foo() "#{bar} is the very best" end }
|
||||
# evaluates the block in the template class, adding method #foo.
|
||||
# helper(:three, BlindHelper) { def mice() 'mice' end }
|
||||
# does all three.
|
||||
def helper(*args, &block)
|
||||
args.flatten.each do |arg|
|
||||
case arg
|
||||
when Module
|
||||
add_template_helper(arg)
|
||||
when String, Symbol
|
||||
file_name = Inflector.underscore(arg.to_s.downcase) + '_helper'
|
||||
class_name = Inflector.camelize(file_name)
|
||||
begin
|
||||
require_or_load(file_name)
|
||||
rescue LoadError
|
||||
raise LoadError, "Missing helper file helpers/#{file_name}.rb"
|
||||
end
|
||||
raise ArgumentError, "Missing #{class_name} module in helpers/#{file_name}.rb" unless Object.const_defined?(class_name)
|
||||
add_template_helper(Object.const_get(class_name))
|
||||
else
|
||||
raise ArgumentError, 'helper expects String, Symbol, or Module argument'
|
||||
end
|
||||
end
|
||||
|
||||
# Evaluate block in template class if given.
|
||||
template_class.module_eval(&block) if block_given?
|
||||
end
|
||||
|
||||
# Declare a controller method as a helper. For example,
|
||||
# helper_method :link_to
|
||||
# def link_to(name, options) ... end
|
||||
# makes the link_to controller method available in the view.
|
||||
def helper_method(*methods)
|
||||
template_class.controller_delegate(*methods)
|
||||
end
|
||||
|
||||
# Declare a controller attribute as a helper. For example,
|
||||
# helper_attr :name
|
||||
# attr_accessor :name
|
||||
# makes the name and name= controller methods available in the view.
|
||||
# The is a convenience wrapper for helper_method.
|
||||
def helper_attr(*attrs)
|
||||
attrs.flatten.each { |attr| helper_method(attr, "#{attr}=") }
|
||||
end
|
||||
|
||||
private
|
||||
def inherited(child)
|
||||
inherited_without_helper(child)
|
||||
begin
|
||||
child.helper(child.controller_name)
|
||||
rescue LoadError
|
||||
# No default helper available for this controller
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,149 @@
|
|||
module ActionController #:nodoc:
|
||||
module Layout #:nodoc:
|
||||
def self.append_features(base)
|
||||
super
|
||||
base.extend(ClassMethods)
|
||||
base.class_eval do
|
||||
alias_method :render_without_layout, :render
|
||||
alias_method :render, :render_with_layout
|
||||
end
|
||||
end
|
||||
|
||||
# Layouts reverse the common pattern of including shared headers and footers in many templates to isolate changes in
|
||||
# repeated setups. The inclusion pattern has pages that look like this:
|
||||
#
|
||||
# <%= render "shared/header" %>
|
||||
# Hello World
|
||||
# <%= render "shared/footer" %>
|
||||
#
|
||||
# This approach is a decent way of keeping common structures isolated from the changing content, but it's verbose
|
||||
# and if you ever want to change the structure of these two includes, you'll have to change all the templates.
|
||||
#
|
||||
# With layouts, you can flip it around and have the common structure know where to insert changing content. This means
|
||||
# that the header and footer is only mentioned in one place, like this:
|
||||
#
|
||||
# <!-- The header part of this layout -->
|
||||
# <%= @content_for_layout %>
|
||||
# <!-- The footer part of this layout -->
|
||||
#
|
||||
# And then you have content pages that look like this:
|
||||
#
|
||||
# hello world
|
||||
#
|
||||
# Not a word about common structures. At rendering time, the content page is computed and then inserted in the layout,
|
||||
# like this:
|
||||
#
|
||||
# <!-- The header part of this layout -->
|
||||
# hello world
|
||||
# <!-- The footer part of this layout -->
|
||||
#
|
||||
# == Accessing shared variables
|
||||
#
|
||||
# Layouts have access to variables specified in the content pages and vice versa. This allows you to have layouts with
|
||||
# references that won't materialize before rendering time:
|
||||
#
|
||||
# <h1><%= @page_title %></h1>
|
||||
# <%= @content_for_layout %>
|
||||
#
|
||||
# ...and content pages that fulfill these references _at_ rendering time:
|
||||
#
|
||||
# <% @page_title = "Welcome" %>
|
||||
# Off-world colonies offers you a chance to start a new life
|
||||
#
|
||||
# The result after rendering is:
|
||||
#
|
||||
# <h1>Welcome</h1>
|
||||
# Off-world colonies offers you a chance to start a new life
|
||||
#
|
||||
# == Inheritance for layouts
|
||||
#
|
||||
# Layouts are shared downwards in the inheritance hierarchy, but not upwards. Examples:
|
||||
#
|
||||
# class BankController < ActionController::Base
|
||||
# layout "layouts/bank_standard"
|
||||
#
|
||||
# class InformationController < BankController
|
||||
#
|
||||
# class VaultController < BankController
|
||||
# layout :access_level_layout
|
||||
#
|
||||
# class EmployeeController < BankController
|
||||
# layout nil
|
||||
#
|
||||
# The InformationController uses "layouts/bank_standard" inherited from the BankController, the VaultController overwrites
|
||||
# and picks the layout dynamically, and the EmployeeController doesn't want to use a layout at all.
|
||||
#
|
||||
# == Types of layouts
|
||||
#
|
||||
# Layouts are basically just regular templates, but the name of this template needs not be specified statically. Sometimes
|
||||
# you want to alternate layouts depending on runtime information, such as whether someone is logged in or not. This can
|
||||
# be done either by specifying a method reference as a symbol or using an inline method (as a proc).
|
||||
#
|
||||
# The method reference is the preferred approach to variable layouts and is used like this:
|
||||
#
|
||||
# class WeblogController < ActionController::Base
|
||||
# layout :writers_and_readers
|
||||
#
|
||||
# def index
|
||||
# # fetching posts
|
||||
# end
|
||||
#
|
||||
# private
|
||||
# def writers_and_readers
|
||||
# logged_in? ? "writer_layout" : "reader_layout"
|
||||
# end
|
||||
#
|
||||
# Now when a new request for the index action is processed, the layout will vary depending on whether the person accessing
|
||||
# is logged in or not.
|
||||
#
|
||||
# If you want to use an inline method, such as a proc, do something like this:
|
||||
#
|
||||
# class WeblogController < ActionController::Base
|
||||
# layout proc{ |controller| controller.logged_in? ? "writer_layout" : "reader_layout" }
|
||||
#
|
||||
# Of course, the most common way of specifying a layout is still just as a plain template path:
|
||||
#
|
||||
# class WeblogController < ActionController::Base
|
||||
# layout "layouts/weblog_standard"
|
||||
#
|
||||
# == Avoiding the use of a layout
|
||||
#
|
||||
# If you have a layout that by default is applied to all the actions of a controller, you still have the option to rendering
|
||||
# a given action without a layout. Just use the method <tt>render_without_layout</tt>, which works just like Base.render --
|
||||
# it just doesn't apply any layouts.
|
||||
module ClassMethods
|
||||
# If a layout is specified, all actions rendered through render and render_action will have their result assigned
|
||||
# to <tt>@content_for_layout</tt>, which can then be used by the layout to insert their contents with
|
||||
# <tt><%= @content_for_layout %></tt>. This layout can itself depend on instance variables assigned during action
|
||||
# performance and have access to them as any normal template would.
|
||||
def layout(template_name)
|
||||
write_inheritable_attribute "layout", template_name
|
||||
end
|
||||
end
|
||||
|
||||
# Returns the name of the active layout. If the layout was specified as a method reference (through a symbol), this method
|
||||
# is called and the return value is used. Likewise if the layout was specified as an inline method (through a proc or method
|
||||
# object). If the layout was defined without a directory, layouts is assumed. So <tt>layout "weblog/standard"</tt> will return
|
||||
# weblog/standard, but <tt>layout "standard"</tt> will return layouts/standard.
|
||||
def active_layout(passed_layout = nil)
|
||||
layout = passed_layout || self.class.read_inheritable_attribute("layout")
|
||||
active_layout = case layout
|
||||
when Symbol then send(layout)
|
||||
when Proc then layout.call(self)
|
||||
when String then layout
|
||||
end
|
||||
active_layout.include?("/") ? active_layout : "layouts/#{active_layout}" if active_layout
|
||||
end
|
||||
|
||||
def render_with_layout(template_name = default_template_name, status = nil, layout = nil) #:nodoc:
|
||||
if layout ||= active_layout
|
||||
add_variables_to_assigns
|
||||
logger.info("Rendering #{template_name} within #{layout}") unless logger.nil?
|
||||
@content_for_layout = @template.render_file(template_name, true)
|
||||
render_without_layout(layout, status)
|
||||
else
|
||||
render_without_layout(template_name, status)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,99 @@
|
|||
module ActionController
|
||||
# These methods are available in both the production and test Request objects.
|
||||
class AbstractRequest
|
||||
# Returns both GET and POST parameters in a single hash.
|
||||
def parameters
|
||||
@parameters ||= request_parameters.update(query_parameters)
|
||||
end
|
||||
|
||||
def method
|
||||
env['REQUEST_METHOD'].downcase.intern
|
||||
end
|
||||
|
||||
def get?
|
||||
method == :get
|
||||
end
|
||||
|
||||
def post?
|
||||
method == :post
|
||||
end
|
||||
|
||||
def put?
|
||||
method == :put
|
||||
end
|
||||
|
||||
def delete?
|
||||
method == :delete
|
||||
end
|
||||
|
||||
# Determine originating IP address. REMOTE_ADDR is the standard
|
||||
# but will fail if the user is behind a proxy. HTTP_CLIENT_IP and/or
|
||||
# HTTP_X_FORWARDED_FOR are set by proxies so check for these before
|
||||
# falling back to REMOTE_ADDR. HTTP_X_FORWARDED_FOR may be a comma-
|
||||
# delimited list in the case of multiple chained proxies; the first is
|
||||
# the originating IP.
|
||||
def remote_ip
|
||||
if env['HTTP_CLIENT_IP']
|
||||
env['HTTP_CLIENT_IP']
|
||||
elsif env['HTTP_X_FORWARDED_FOR']
|
||||
remote_ip = env['HTTP_X_FORWARDED_FOR'].split(',').reject { |ip|
|
||||
ip =~ /^unknown$|^(10|172\.16|192\.168)\./i
|
||||
}.first
|
||||
|
||||
remote_ip ? remote_ip.strip : env['REMOTE_ADDR']
|
||||
else
|
||||
env['REMOTE_ADDR']
|
||||
end
|
||||
end
|
||||
|
||||
def request_uri
|
||||
env["REQUEST_URI"]
|
||||
end
|
||||
|
||||
def protocol
|
||||
port == 443 ? "https://" : "http://"
|
||||
end
|
||||
|
||||
def path
|
||||
request_uri ? request_uri.split("?").first : ""
|
||||
end
|
||||
|
||||
def port
|
||||
env["SERVER_PORT"].to_i
|
||||
end
|
||||
|
||||
def host_with_port
|
||||
if env['HTTP_HOST']
|
||||
env['HTTP_HOST']
|
||||
elsif (protocol == "http://" && port == 80) || (protocol == "https://" && port == 443)
|
||||
host
|
||||
else
|
||||
host + ":#{port}"
|
||||
end
|
||||
end
|
||||
|
||||
#--
|
||||
# Must be implemented in the concrete request
|
||||
#++
|
||||
def query_parameters
|
||||
end
|
||||
|
||||
def request_parameters
|
||||
end
|
||||
|
||||
def env
|
||||
end
|
||||
|
||||
def host
|
||||
end
|
||||
|
||||
def cookies
|
||||
end
|
||||
|
||||
def session
|
||||
end
|
||||
|
||||
def reset_session
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,94 @@
|
|||
module ActionController #:nodoc:
|
||||
# Actions that fail to perform as expected throw exceptions. These exceptions can either be rescued for the public view
|
||||
# (with a nice user-friendly explanation) or for the developers view (with tons of debugging information). The developers view
|
||||
# is already implemented by the Action Controller, but the public view should be tailored to your specific application. So too
|
||||
# could the decision on whether something is a public or a developer request.
|
||||
#
|
||||
# You can tailor the rescuing behavior and appearance by overwriting the following two stub methods.
|
||||
module Rescue
|
||||
def self.append_features(base) #:nodoc:
|
||||
super
|
||||
base.class_eval do
|
||||
alias_method :perform_action_without_rescue, :perform_action
|
||||
alias_method :perform_action, :perform_action_with_rescue
|
||||
end
|
||||
end
|
||||
|
||||
protected
|
||||
# Exception handler called when the performance of an action raises an exception.
|
||||
def rescue_action(exception)
|
||||
log_error(exception) unless logger.nil?
|
||||
|
||||
if consider_all_requests_local || local_request?
|
||||
rescue_action_locally(exception)
|
||||
else
|
||||
rescue_action_in_public(exception)
|
||||
end
|
||||
end
|
||||
|
||||
# Overwrite to implement custom logging of errors. By default logs as fatal.
|
||||
def log_error(exception) #:doc:
|
||||
if ActionView::TemplateError === exception
|
||||
logger.fatal(exception.to_s)
|
||||
else
|
||||
logger.fatal(
|
||||
"\n\n#{exception.class} (#{exception.message}):\n " +
|
||||
clean_backtrace(exception).join("\n ") +
|
||||
"\n\n"
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
# Overwrite to implement public exception handling (for requests answering false to <tt>local_request?</tt>).
|
||||
def rescue_action_in_public(exception) #:doc:
|
||||
render_text "<html><body><h1>Application error (Rails)</h1></body></html>"
|
||||
end
|
||||
|
||||
# Overwrite to expand the meaning of a local request in order to show local rescues on other occurances than
|
||||
# the remote IP being 127.0.0.1. For example, this could include the IP of the developer machine when debugging
|
||||
# remotely.
|
||||
def local_request? #:doc:
|
||||
@request.remote_addr == "127.0.0.1"
|
||||
end
|
||||
|
||||
# Renders a detailed diagnostics screen on action exceptions.
|
||||
def rescue_action_locally(exception)
|
||||
@exception = exception
|
||||
@rescues_path = File.dirname(__FILE__) + "/templates/rescues/"
|
||||
add_variables_to_assigns
|
||||
@contents = @template.render_file(template_path_for_local_rescue(exception), false)
|
||||
|
||||
@headers["Content-Type"] = "text/html"
|
||||
render_file(rescues_path("layout"), "500 Internal Error")
|
||||
end
|
||||
|
||||
private
|
||||
def perform_action_with_rescue #:nodoc:
|
||||
begin
|
||||
perform_action_without_rescue
|
||||
rescue Exception => exception
|
||||
rescue_action(exception)
|
||||
end
|
||||
end
|
||||
|
||||
def rescues_path(template_name)
|
||||
File.dirname(__FILE__) + "/templates/rescues/#{template_name}.rhtml"
|
||||
end
|
||||
|
||||
def template_path_for_local_rescue(exception)
|
||||
rescues_path(
|
||||
case exception
|
||||
when MissingTemplate then "missing_template"
|
||||
when UnknownAction then "unknown_action"
|
||||
when ActionView::TemplateError then "template_error"
|
||||
else "diagnostics"
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
def clean_backtrace(exception)
|
||||
base_dir = File.expand_path(File.dirname(__FILE__) + "/../../../../")
|
||||
exception.backtrace.collect { |line| line.gsub(base_dir, "").gsub("/public/../config/environments/../../", "").gsub("/public/../", "") }
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,15 @@
|
|||
module ActionController
|
||||
class AbstractResponse #:nodoc:
|
||||
DEFAULT_HEADERS = { "Cache-Control" => "no-cache" }
|
||||
attr_accessor :body, :headers, :session, :cookies, :assigns, :template, :redirected_to, :redirected_to_method_params
|
||||
|
||||
def initialize
|
||||
@body, @headers, @session, @assigns = "", DEFAULT_HEADERS.merge("cookie" => []), [], []
|
||||
end
|
||||
|
||||
def redirect(to_url)
|
||||
@headers["Status"] = "302 Moved"
|
||||
@headers["location"] = to_url
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,183 @@
|
|||
module ActionController
|
||||
module Scaffolding # :nodoc:
|
||||
def self.append_features(base)
|
||||
super
|
||||
base.extend(ClassMethods)
|
||||
end
|
||||
|
||||
# Scaffolding is a way to quickly put an Active Record class online by providing a series of standardized actions
|
||||
# for listing, showing, creating, updating, and destroying objects of the class. These standardized actions come
|
||||
# with both controller logic and default templates that through introspection already know which fields to display
|
||||
# and which input types to use. Example:
|
||||
#
|
||||
# class WeblogController < ActionController::Base
|
||||
# scaffold :entry
|
||||
# end
|
||||
#
|
||||
# This tiny piece of code will add all of the following methods to the controller:
|
||||
#
|
||||
# class WeblogController < ActionController::Base
|
||||
# def index
|
||||
# list
|
||||
# end
|
||||
#
|
||||
# def list
|
||||
# @entries = Entry.find_all
|
||||
# render_scaffold "list"
|
||||
# end
|
||||
#
|
||||
# def show
|
||||
# @entry = Entry.find(@params["id"])
|
||||
# render_scaffold
|
||||
# end
|
||||
#
|
||||
# def destroy
|
||||
# Entry.find(@params["id"]).destroy
|
||||
# redirect_to :action => "list"
|
||||
# end
|
||||
#
|
||||
# def new
|
||||
# @entry = Entry.new
|
||||
# render_scaffold
|
||||
# end
|
||||
#
|
||||
# def create
|
||||
# @entry = Entry.new(@params["entry"])
|
||||
# if @entry.save
|
||||
# flash["notice"] = "Entry was succesfully created"
|
||||
# redirect_to :action => "list"
|
||||
# else
|
||||
# render "entry/new"
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# def edit
|
||||
# @entry = Entry.find(@params["id"])
|
||||
# render_scaffold
|
||||
# end
|
||||
#
|
||||
# def update
|
||||
# @entry = Entry.find(@params["entry"]["id"])
|
||||
# @entry.attributes = @params["entry"]
|
||||
#
|
||||
# if @entry.save
|
||||
# flash["notice"] = "Entry was succesfully updated"
|
||||
# redirect_to :action => "show/" + @entry.id.to_s
|
||||
# else
|
||||
# render "entry/edit"
|
||||
# end
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# The <tt>render_scaffold</tt> method will first check to see if you've made your own template (like "weblog/show.rhtml" for
|
||||
# the show action) and if not, then render the generic template for that action. This gives you the possibility of using the
|
||||
# scaffold while you're building your specific application. Start out with a totally generic setup, then replace one template
|
||||
# and one action at a time while relying on the rest of the scaffolded templates and actions.
|
||||
module ClassMethods
|
||||
# Adds a swath of generic CRUD actions to the controller. The +model_id+ is automatically converted into a class name unless
|
||||
# one is specifically provide through <tt>options[:class_name]</tt>. So <tt>scaffold :post</tt> would use Post as the class
|
||||
# and @post/@posts for the instance variables.
|
||||
#
|
||||
# It's possible to use more than one scaffold in a single controller by specifying <tt>options[:suffix] = true</tt>. This will
|
||||
# make <tt>scaffold :post, :suffix => true</tt> use method names like list_post, show_post, and create_post
|
||||
# instead of just list, show, and post. If suffix is used, then no index method is added.
|
||||
def scaffold(model_id, options = {})
|
||||
validate_options([ :class_name, :suffix ], options.keys)
|
||||
|
||||
require "#{model_id.id2name}" rescue logger.warn "Couldn't auto-require #{model_id.id2name}.rb" unless logger.nil?
|
||||
|
||||
singular_name = model_id.id2name
|
||||
class_name = options[:class_name] || Inflector.camelize(singular_name)
|
||||
plural_name = Inflector.pluralize(singular_name)
|
||||
suffix = options[:suffix] ? "_#{singular_name}" : ""
|
||||
|
||||
unless options[:suffix]
|
||||
module_eval <<-"end_eval", __FILE__, __LINE__
|
||||
def index
|
||||
list
|
||||
end
|
||||
end_eval
|
||||
end
|
||||
|
||||
module_eval <<-"end_eval", __FILE__, __LINE__
|
||||
def list#{suffix}
|
||||
@#{plural_name} = #{class_name}.find_all
|
||||
render#{suffix}_scaffold "list#{suffix}"
|
||||
end
|
||||
|
||||
def show#{suffix}
|
||||
@#{singular_name} = #{class_name}.find(@params["id"])
|
||||
render#{suffix}_scaffold
|
||||
end
|
||||
|
||||
def destroy#{suffix}
|
||||
#{class_name}.find(@params["id"]).destroy
|
||||
redirect_to :action => "list#{suffix}"
|
||||
end
|
||||
|
||||
def new#{suffix}
|
||||
@#{singular_name} = #{class_name}.new
|
||||
render#{suffix}_scaffold
|
||||
end
|
||||
|
||||
def create#{suffix}
|
||||
@#{singular_name} = #{class_name}.new(@params["#{singular_name}"])
|
||||
if @#{singular_name}.save
|
||||
flash["notice"] = "#{class_name} was succesfully created"
|
||||
redirect_to :action => "list#{suffix}"
|
||||
else
|
||||
render "#{singular_name}/new#{suffix}"
|
||||
end
|
||||
end
|
||||
|
||||
def edit#{suffix}
|
||||
@#{singular_name} = #{class_name}.find(@params["id"])
|
||||
render#{suffix}_scaffold
|
||||
end
|
||||
|
||||
def update#{suffix}
|
||||
@#{singular_name} = #{class_name}.find(@params["#{singular_name}"]["id"])
|
||||
@#{singular_name}.attributes = @params["#{singular_name}"]
|
||||
|
||||
if @#{singular_name}.save
|
||||
flash["notice"] = "#{class_name} was succesfully updated"
|
||||
redirect_to :action => "show#{suffix}/" + @#{singular_name}.id.to_s
|
||||
else
|
||||
render "#{singular_name}/edit#{suffix}"
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def render#{suffix}_scaffold(action = caller_method_name(caller))
|
||||
if template_exists?("\#{controller_name}/\#{action}")
|
||||
render_action(action)
|
||||
else
|
||||
@scaffold_class = #{class_name}
|
||||
@scaffold_singular_name, @scaffold_plural_name = "#{singular_name}", "#{plural_name}"
|
||||
@scaffold_suffix = "#{suffix}"
|
||||
add_instance_variables_to_assigns
|
||||
|
||||
@content_for_layout = @template.render_file(scaffold_path(action.sub(/#{suffix}$/, "")), false)
|
||||
self.active_layout ? render_file(self.active_layout, "200 OK", true) : render_file(scaffold_path("layout"))
|
||||
end
|
||||
end
|
||||
|
||||
def scaffold_path(template_name)
|
||||
File.dirname(__FILE__) + "/templates/scaffolds/" + template_name + ".rhtml"
|
||||
end
|
||||
|
||||
def caller_method_name(caller)
|
||||
caller.first.scan(/`(.*)'/).first.first # ' ruby-mode
|
||||
end
|
||||
end_eval
|
||||
end
|
||||
|
||||
private
|
||||
# Raises an exception if an invalid option has been specified to prevent misspellings from slipping through
|
||||
def validate_options(valid_option_keys, supplied_option_keys)
|
||||
unknown_option_keys = supplied_option_keys - valid_option_keys
|
||||
raise(ActionController::ActionControllerError, "Unknown options: #{unknown_option_keys}") unless unknown_option_keys.empty?
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,72 @@
|
|||
begin
|
||||
|
||||
require 'active_record'
|
||||
require 'cgi'
|
||||
require 'cgi/session'
|
||||
|
||||
# Contributed by Tim Bates
|
||||
class CGI
|
||||
class Session
|
||||
# ActiveRecord database based session storage class.
|
||||
#
|
||||
# Implements session storage in a database using the ActiveRecord ORM library. Assumes that the database
|
||||
# has a table called +sessions+ with columns +id+ (numeric, primary key), +sessid+ and +data+ (text).
|
||||
# The session data is stored in the +data+ column in YAML format; the user is responsible for ensuring that
|
||||
# only data that can be YAMLized is stored in the session.
|
||||
class ActiveRecordStore
|
||||
# The ActiveRecord class which corresponds to the database table.
|
||||
class Session < ActiveRecord::Base
|
||||
serialize :data
|
||||
# Isn't this class definition beautiful?
|
||||
end
|
||||
|
||||
# Create a new ActiveRecordStore instance. This constructor is used internally by CGI::Session.
|
||||
# The user does not generally need to call it directly.
|
||||
#
|
||||
# +session+ is the session for which this instance is being created.
|
||||
#
|
||||
# +option+ is currently ignored as no options are recognized.
|
||||
#
|
||||
# This session's ActiveRecord database row will be created if it does not exist, or opened if it does.
|
||||
def initialize(session, option=nil)
|
||||
@session = Session.find_first(["sessid = '%s'", session.session_id])
|
||||
if @session
|
||||
@data = @session.data
|
||||
else
|
||||
@session = Session.new("sessid" => session.session_id, "data" => {})
|
||||
end
|
||||
end
|
||||
|
||||
# Update and close the session's ActiveRecord object.
|
||||
def close
|
||||
return unless @session
|
||||
update
|
||||
@session = nil
|
||||
end
|
||||
|
||||
# Close and destroy the session's ActiveRecord object.
|
||||
def delete
|
||||
return unless @session
|
||||
@session.destroy
|
||||
@session = nil
|
||||
end
|
||||
|
||||
# Restore session state from the session's ActiveRecord object.
|
||||
def restore
|
||||
return unless @session
|
||||
@data = @session.data
|
||||
end
|
||||
|
||||
# Save session state in the session's ActiveRecord object.
|
||||
def update
|
||||
return unless @session
|
||||
@session.data = @data
|
||||
@session.save
|
||||
end
|
||||
end #ActiveRecordStore
|
||||
end #Session
|
||||
end #CGI
|
||||
|
||||
rescue LoadError
|
||||
# Couldn't load Active Record, so don't make this store available
|
||||
end
|
|
@ -0,0 +1,9 @@
|
|||
#!/usr/local/bin/ruby -w
|
||||
|
||||
# This is a really simple session storage daemon, basically just a hash,
|
||||
# which is enabled for DRb access.
|
||||
|
||||
require 'drb'
|
||||
|
||||
DRb.start_service('druby://127.0.0.1:9192', Hash.new)
|
||||
DRb.thread.join
|
|
@ -0,0 +1,31 @@
|
|||
require 'cgi'
|
||||
require 'cgi/session'
|
||||
require 'drb'
|
||||
|
||||
class CGI #:nodoc:all
|
||||
class Session
|
||||
class DRbStore
|
||||
@@session_data = DRbObject.new(nil, 'druby://localhost:9192')
|
||||
|
||||
def initialize(session, option=nil)
|
||||
@session_id = session.session_id
|
||||
end
|
||||
|
||||
def restore
|
||||
@h = @@session_data[@session_id] || {}
|
||||
end
|
||||
|
||||
def update
|
||||
@@session_data[@session_id] = @h
|
||||
end
|
||||
|
||||
def close
|
||||
update
|
||||
end
|
||||
|
||||
def delete
|
||||
@@session_data.delete(@session_id)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,57 @@
|
|||
# Extends the class object with class and instance accessors for class attributes,
|
||||
# just like the native attr* accessors for instance attributes.
|
||||
class Class # :nodoc:
|
||||
def cattr_reader(*syms)
|
||||
syms.each do |sym|
|
||||
class_eval <<-EOS
|
||||
if ! defined? @@#{sym.id2name}
|
||||
@@#{sym.id2name} = nil
|
||||
end
|
||||
|
||||
def self.#{sym.id2name}
|
||||
@@#{sym}
|
||||
end
|
||||
|
||||
def #{sym.id2name}
|
||||
@@#{sym}
|
||||
end
|
||||
|
||||
def call_#{sym.id2name}
|
||||
case @@#{sym.id2name}
|
||||
when Symbol then send(@@#{sym})
|
||||
when Proc then @@#{sym}.call(self)
|
||||
when String then @@#{sym}
|
||||
else nil
|
||||
end
|
||||
end
|
||||
EOS
|
||||
end
|
||||
end
|
||||
|
||||
def cattr_writer(*syms)
|
||||
syms.each do |sym|
|
||||
class_eval <<-EOS
|
||||
if ! defined? @@#{sym.id2name}
|
||||
@@#{sym.id2name} = nil
|
||||
end
|
||||
|
||||
def self.#{sym.id2name}=(obj)
|
||||
@@#{sym.id2name} = obj
|
||||
end
|
||||
|
||||
def self.set_#{sym.id2name}(obj)
|
||||
@@#{sym.id2name} = obj
|
||||
end
|
||||
|
||||
def #{sym.id2name}=(obj)
|
||||
@@#{sym} = obj
|
||||
end
|
||||
EOS
|
||||
end
|
||||
end
|
||||
|
||||
def cattr_accessor(*syms)
|
||||
cattr_reader(*syms)
|
||||
cattr_writer(*syms)
|
||||
end
|
||||
end
|
|
@ -0,0 +1,37 @@
|
|||
# Allows attributes to be shared within an inheritance hierarchy, but where each descendant gets a copy of
|
||||
# their parents' attributes, instead of just a pointer to the same. This means that the child can add elements
|
||||
# to, for example, an array without those additions being shared with either their parent, siblings, or
|
||||
# children, which is unlike the regular class-level attributes that are shared across the entire hierarchy.
|
||||
module ClassInheritableAttributes # :nodoc:
|
||||
def self.append_features(base)
|
||||
super
|
||||
base.extend(ClassMethods)
|
||||
end
|
||||
|
||||
module ClassMethods # :nodoc:
|
||||
@@classes ||= {}
|
||||
|
||||
def inheritable_attributes
|
||||
@@classes[self] ||= {}
|
||||
end
|
||||
|
||||
def write_inheritable_attribute(key, value)
|
||||
inheritable_attributes[key] = value
|
||||
end
|
||||
|
||||
def write_inheritable_array(key, elements)
|
||||
write_inheritable_attribute(key, []) if read_inheritable_attribute(key).nil?
|
||||
write_inheritable_attribute(key, read_inheritable_attribute(key) + elements)
|
||||
end
|
||||
|
||||
def read_inheritable_attribute(key)
|
||||
inheritable_attributes[key]
|
||||
end
|
||||
|
||||
private
|
||||
def inherited(child)
|
||||
@@classes[child] = inheritable_attributes.dup
|
||||
end
|
||||
|
||||
end
|
||||
end
|
|
@ -0,0 +1,10 @@
|
|||
require 'logger'
|
||||
|
||||
class Logger #:nodoc:
|
||||
private
|
||||
remove_const "Format"
|
||||
Format = "%s\n"
|
||||
def format_message(severity, timestamp, msg, progname)
|
||||
Format % [msg]
|
||||
end
|
||||
end
|
|
@ -0,0 +1,121 @@
|
|||
CGI.module_eval { remove_const "Cookie" }
|
||||
|
||||
class CGI #:nodoc:
|
||||
# This is a cookie class that fixes the performance problems with the default one that ships with 1.8.1 and below.
|
||||
# It replaces the inheritance on SimpleDelegator with DelegateClass(Array) following the suggestion from Matz on
|
||||
# http://groups.google.com/groups?th=e3a4e68ba042f842&seekm=c3sioe%241qvm%241%40news.cybercity.dk#link14
|
||||
class Cookie < DelegateClass(Array)
|
||||
# Create a new CGI::Cookie object.
|
||||
#
|
||||
# The contents of the cookie can be specified as a +name+ and one
|
||||
# or more +value+ arguments. Alternatively, the contents can
|
||||
# be specified as a single hash argument. The possible keywords of
|
||||
# this hash are as follows:
|
||||
#
|
||||
# name:: the name of the cookie. Required.
|
||||
# value:: the cookie's value or list of values.
|
||||
# path:: the path for which this cookie applies. Defaults to the
|
||||
# base directory of the CGI script.
|
||||
# domain:: the domain for which this cookie applies.
|
||||
# expires:: the time at which this cookie expires, as a +Time+ object.
|
||||
# secure:: whether this cookie is a secure cookie or not (default to
|
||||
# false). Secure cookies are only transmitted to HTTPS
|
||||
# servers.
|
||||
#
|
||||
# These keywords correspond to attributes of the cookie object.
|
||||
def initialize(name = "", *value)
|
||||
options = if name.kind_of?(String)
|
||||
{ "name" => name, "value" => value }
|
||||
else
|
||||
name
|
||||
end
|
||||
unless options.has_key?("name")
|
||||
raise ArgumentError, "`name' required"
|
||||
end
|
||||
|
||||
@name = options["name"]
|
||||
@value = Array(options["value"])
|
||||
# simple support for IE
|
||||
if options["path"]
|
||||
@path = options["path"]
|
||||
else
|
||||
%r|^(.*/)|.match(ENV["SCRIPT_NAME"])
|
||||
@path = ($1 or "")
|
||||
end
|
||||
@domain = options["domain"]
|
||||
@expires = options["expires"]
|
||||
@secure = options["secure"] == true ? true : false
|
||||
|
||||
super(@value)
|
||||
end
|
||||
|
||||
def __setobj__(obj)
|
||||
@_dc_obj = obj
|
||||
end
|
||||
|
||||
attr_accessor("name", "value", "path", "domain", "expires")
|
||||
attr_reader("secure")
|
||||
|
||||
# Set whether the Cookie is a secure cookie or not.
|
||||
#
|
||||
# +val+ must be a boolean.
|
||||
def secure=(val)
|
||||
@secure = val if val == true or val == false
|
||||
@secure
|
||||
end
|
||||
|
||||
# Convert the Cookie to its string representation.
|
||||
def to_s
|
||||
buf = ""
|
||||
buf += @name + '='
|
||||
|
||||
if @value.kind_of?(String)
|
||||
buf += CGI::escape(@value)
|
||||
else
|
||||
buf += @value.collect{|v| CGI::escape(v) }.join("&")
|
||||
end
|
||||
|
||||
if @domain
|
||||
buf += '; domain=' + @domain
|
||||
end
|
||||
|
||||
if @path
|
||||
buf += '; path=' + @path
|
||||
end
|
||||
|
||||
if @expires
|
||||
buf += '; expires=' + CGI::rfc1123_date(@expires)
|
||||
end
|
||||
|
||||
if @secure == true
|
||||
buf += '; secure'
|
||||
end
|
||||
|
||||
buf
|
||||
end
|
||||
|
||||
# Parse a raw cookie string into a hash of cookie-name=>Cookie
|
||||
# pairs.
|
||||
#
|
||||
# cookies = CGI::Cookie::parse("raw_cookie_string")
|
||||
# # { "name1" => cookie1, "name2" => cookie2, ... }
|
||||
#
|
||||
def self.parse(raw_cookie)
|
||||
cookies = Hash.new([])
|
||||
return cookies unless raw_cookie
|
||||
|
||||
raw_cookie.split(/; /).each do |pairs|
|
||||
name, values = pairs.split('=',2)
|
||||
next unless name and values
|
||||
name = CGI::unescape(name)
|
||||
values ||= ""
|
||||
values = values.split('&').collect{|v| CGI::unescape(v) }
|
||||
unless cookies.has_key?(name)
|
||||
cookies[name] = new({ "name" => name, "value" => values })
|
||||
end
|
||||
end
|
||||
|
||||
cookies
|
||||
end
|
||||
end # class Cookie
|
||||
end
|
|
@ -0,0 +1,78 @@
|
|||
# The Inflector transforms words from singular to plural, class names to table names, modulized class names to ones without,
|
||||
# and class names to foreign keys.
|
||||
module Inflector
|
||||
extend self
|
||||
|
||||
def pluralize(word)
|
||||
result = word.dup
|
||||
plural_rules.each do |(rule, replacement)|
|
||||
break if result.gsub!(rule, replacement)
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
def singularize(word)
|
||||
result = word.dup
|
||||
singular_rules.each do |(rule, replacement)|
|
||||
break if result.gsub!(rule, replacement)
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
def camelize(lower_case_and_underscored_word)
|
||||
lower_case_and_underscored_word.gsub(/(^|_)(.)/){$2.upcase}
|
||||
end
|
||||
|
||||
def underscore(camel_cased_word)
|
||||
camel_cased_word.gsub(/([A-Z]+)([A-Z])/,'\1_\2').gsub(/([a-z])([A-Z])/,'\1_\2').downcase
|
||||
end
|
||||
|
||||
def demodulize(class_name_in_module)
|
||||
class_name_in_module.gsub(/^.*::/, '')
|
||||
end
|
||||
|
||||
def tableize(class_name)
|
||||
pluralize(underscore(class_name))
|
||||
end
|
||||
|
||||
def classify(table_name)
|
||||
camelize(singularize(table_name))
|
||||
end
|
||||
|
||||
def foreign_key(class_name, separate_class_name_and_id_with_underscore = true)
|
||||
Inflector.underscore(Inflector.demodulize(class_name)) +
|
||||
(separate_class_name_and_id_with_underscore ? "_id" : "id")
|
||||
end
|
||||
|
||||
private
|
||||
def plural_rules #:doc:
|
||||
[
|
||||
[/(x|ch|ss)$/, '\1es'], # search, switch, fix, box, process, address
|
||||
[/([^aeiouy]|qu)y$/, '\1ies'], # query, ability, agency
|
||||
[/(?:([^f])fe|([lr])f)$/, '\1\2ves'], # half, safe, wife
|
||||
[/sis$/, 'ses'], # basis, diagnosis
|
||||
[/([ti])um$/, '\1a'], # datum, medium
|
||||
[/person$/, 'people'], # person, salesperson
|
||||
[/man$/, 'men'], # man, woman, spokesman
|
||||
[/child$/, 'children'], # child
|
||||
[/s$/, 's'], # no change (compatibility)
|
||||
[/$/, 's']
|
||||
]
|
||||
end
|
||||
|
||||
def singular_rules #:doc:
|
||||
[
|
||||
[/(x|ch|ss)es$/, '\1'],
|
||||
[/([^aeiouy]|qu)ies$/, '\1y'],
|
||||
[/([lr])ves$/, '\1f'],
|
||||
[/([^f])ves$/, '\1fe'],
|
||||
[/(analy|ba|diagno|parenthe|progno|synop|the)ses$/, '\1sis'],
|
||||
[/([ti])a$/, '\1um'],
|
||||
[/people$/, 'person'],
|
||||
[/men$/, 'man'],
|
||||
[/status$/, 'status'],
|
||||
[/children$/, 'child'],
|
||||
[/s$/, '']
|
||||
]
|
||||
end
|
||||
end
|
|
@ -0,0 +1,28 @@
|
|||
<%
|
||||
base_dir = File.expand_path(File.dirname(__FILE__))
|
||||
|
||||
request_parameters_without_action = @request.parameters.clone
|
||||
request_parameters_without_action.delete("action")
|
||||
request_parameters_without_action.delete("controller")
|
||||
|
||||
request_dump = request_parameters_without_action.inspect.gsub(/,/, ",\n")
|
||||
session_dump = @request.session.instance_variable_get("@data").inspect.gsub(/,/, ",\n")
|
||||
response_dump = @response.inspect.gsub(/,/, ",\n")
|
||||
|
||||
template_assigns = @response.template.instance_variable_get("@assigns")
|
||||
%w( response exception template session request template_root template_class url ignore_missing_templates logger cookies headers params ).each { |t| template_assigns.delete(t) }
|
||||
template_dump = template_assigns.inspect.gsub(/,/, ",\n")
|
||||
%>
|
||||
|
||||
<h2 style="margin-top: 30px">Request</h2>
|
||||
<p><b>Parameters</b>: <%=h request_dump == "{}" ? "None" : request_dump %></p>
|
||||
|
||||
<p><a href="#" onclick="document.getElementById('session_dump').style.display='block'; return false;">Show session dump</a></p>
|
||||
<div id="session_dump" style="display:none"><%= debug(@request.session.instance_variable_get("@data")) %></div>
|
||||
|
||||
|
||||
<h2 style="margin-top: 30px">Response</h2>
|
||||
<b>Headers</b>: <%=h @response.headers.inspect.gsub(/,/, ",\n") %><br/>
|
||||
|
||||
<p><a href="#" onclick="document.getElementById('template_dump').style.display='block'; return false;">Show template parameters</a></p>
|
||||
<div id="template_dump" style="display:none"><%= debug(template_assigns) %></div>
|
|
@ -0,0 +1,22 @@
|
|||
<%
|
||||
base_dir = File.expand_path(File.dirname(__FILE__))
|
||||
|
||||
clean_backtrace = @exception.backtrace.collect { |line| line.gsub(base_dir, "").gsub("/../config/environments/../../", "") }
|
||||
app_trace = clean_backtrace.reject { |line| line[0..6] == "vendor/" || line.include?("dispatch.cgi") }
|
||||
framework_trace = clean_backtrace - app_trace
|
||||
%>
|
||||
|
||||
<h1>
|
||||
<%=h @exception.class.to_s %> in
|
||||
<%=h @request.parameters["controller"].capitalize %>#<%=h @request.parameters["action"] %>
|
||||
</h1>
|
||||
<p><%=h @exception.message %></p>
|
||||
|
||||
<% unless app_trace.empty? %><pre><code><%=h app_trace.collect { |line| line.gsub("/../", "") }.join("\n") %></code></pre><% end %>
|
||||
|
||||
<% unless framework_trace.empty? %>
|
||||
<a href="#" onclick="document.getElementById('framework_trace').style.display='block'; return false;">Show framework trace</a>
|
||||
<pre id="framework_trace" style="display:none"><code><%=h framework_trace.join("\n") %></code></pre>
|
||||
<% end %>
|
||||
|
||||
<%= render_file(@rescues_path + "/_request_and_response.rhtml", false) %>
|
|
@ -0,0 +1,29 @@
|
|||
<html>
|
||||
<head>
|
||||
<title>Action Controller: Exception caught</title>
|
||||
<style>
|
||||
body { background-color: #fff; color: #333; }
|
||||
|
||||
body, 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; }
|
||||
a:visited { color: #666; }
|
||||
a:hover { color: #fff; background-color:#000; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<%= @contents %>
|
||||
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,2 @@
|
|||
<h1>Template is missing</h1>
|
||||
<p><%=h @exception.message %></p>
|
|
@ -0,0 +1,26 @@
|
|||
<%
|
||||
base_dir = File.expand_path(File.dirname(__FILE__))
|
||||
|
||||
framework_trace = @exception.original_exception.backtrace.collect do |line|
|
||||
line.gsub(base_dir, "").gsub("/../config/environments/../../", "")
|
||||
end
|
||||
%>
|
||||
|
||||
<h1>
|
||||
<%=h @exception.original_exception.class.to_s %> in
|
||||
<%=h @request.parameters["controller"].capitalize %>#<%=h @request.parameters["action"] %>
|
||||
</h1>
|
||||
|
||||
<p>
|
||||
Showing <i><%=h @exception.file_name %></i> where line <b>#<%=h @exception.line_number %></b> raised
|
||||
<u><%=h @exception.message %></u>
|
||||
</p>
|
||||
|
||||
<pre><code><%=h @exception.source_extract %></code></pre>
|
||||
|
||||
<p><%=h @exception.sub_template_message %></p>
|
||||
|
||||
<a href="#" onclick="document.getElementById('framework_trace').style.display='block'">Show template trace</a>
|
||||
<pre id="framework_trace" style="display:none"><code><%=h framework_trace.join("\n") %></code></pre>
|
||||
|
||||
<%= render_file(@rescues_path + "/_request_and_response.rhtml", false) %>
|
|
@ -0,0 +1,2 @@
|
|||
<h1>Unknown action</h1>
|
||||
<p><%=h @exception.message %></p>
|
|
@ -0,0 +1,6 @@
|
|||
<h1>Editing <%= @scaffold_singular_name %></h1>
|
||||
|
||||
<%= form(@scaffold_singular_name, :action => "../update" + @scaffold_suffix) %>
|
||||
|
||||
<%= link_to "Show", :action => "show#{@scaffold_suffix}", :id => instance_variable_get("@#{@scaffold_singular_name}").id %> |
|
||||
<%= link_to "Back", :action => "list#{@scaffold_suffix}" %>
|
|
@ -0,0 +1,29 @@
|
|||
<html>
|
||||
<head>
|
||||
<title>Scaffolding</title>
|
||||
<style>
|
||||
body { background-color: #fff; color: #333; }
|
||||
|
||||
body, 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; }
|
||||
a:visited { color: #666; }
|
||||
a:hover { color: #fff; background-color:#000; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<%= @content_for_layout %>
|
||||
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,24 @@
|
|||
<h1>Listing <%= @scaffold_plural_name %></h1>
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<% for column in @scaffold_class.content_columns %>
|
||||
<th><%= column.human_name %></th>
|
||||
<% end %>
|
||||
</tr>
|
||||
|
||||
<% for entry in instance_variable_get("@#{@scaffold_plural_name}") %>
|
||||
<tr>
|
||||
<% for column in @scaffold_class.content_columns %>
|
||||
<td><%= entry.send(column.name) %></td>
|
||||
<% end %>
|
||||
<td><%= link_to "Show", :action => "show#{@scaffold_suffix}", :id => entry.id %></td>
|
||||
<td><%= link_to "Edit", :action => "edit#{@scaffold_suffix}", :id => entry.id %></td>
|
||||
<td><%= link_to "Destroy", :action => "destroy#{@scaffold_suffix}", :id => entry.id %></td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</table>
|
||||
|
||||
<br />
|
||||
|
||||
<%= link_to "New #{@scaffold_singular_name}", :action => "new#{@scaffold_suffix}" %>
|
|
@ -0,0 +1,5 @@
|
|||
<h1>New <%= @scaffold_singular_name %></h1>
|
||||
|
||||
<%= form(@scaffold_singular_name, :action => "create" + @scaffold_suffix) %>
|
||||
|
||||
<%= link_to "Back", :action => "list#{@scaffold_suffix}" %>
|
|
@ -0,0 +1,9 @@
|
|||
<% for column in @scaffold_class.content_columns %>
|
||||
<p>
|
||||
<b><%= column.human_name %>:</b>
|
||||
<%= instance_variable_get("@#{@scaffold_singular_name}").send(column.name) %>
|
||||
</p>
|
||||
<% end %>
|
||||
|
||||
<%= link_to "Edit", :action => "edit#{@scaffold_suffix}", :id => instance_variable_get("@#{@scaffold_singular_name}").id %> |
|
||||
<%= link_to "Back", :action => "list#{@scaffold_suffix}" %>
|
|
@ -0,0 +1,195 @@
|
|||
require File.dirname(__FILE__) + '/assertions/action_pack_assertions'
|
||||
require File.dirname(__FILE__) + '/assertions/active_record_assertions'
|
||||
|
||||
module ActionController #:nodoc:
|
||||
class Base
|
||||
# Process a test request called with a +TestRequest+ object.
|
||||
def self.process_test(request)
|
||||
new.process_test(request)
|
||||
end
|
||||
|
||||
def process_test(request) #:nodoc:
|
||||
process(request, TestResponse.new)
|
||||
end
|
||||
end
|
||||
|
||||
class TestRequest < AbstractRequest #:nodoc:
|
||||
attr_writer :cookies
|
||||
attr_accessor :query_parameters, :request_parameters, :session, :env
|
||||
attr_accessor :host, :path, :request_uri, :remote_addr
|
||||
|
||||
def initialize(query_parameters = nil, request_parameters = nil, session = nil)
|
||||
@query_parameters = query_parameters || {}
|
||||
@request_parameters = request_parameters || {}
|
||||
@session = session || TestSession.new
|
||||
|
||||
initialize_containers
|
||||
initialize_default_values
|
||||
|
||||
super()
|
||||
end
|
||||
|
||||
def reset_session
|
||||
@session = {}
|
||||
end
|
||||
|
||||
def cookies
|
||||
@cookies.freeze
|
||||
end
|
||||
|
||||
def action=(action_name)
|
||||
@query_parameters.update({ "action" => action_name })
|
||||
@parameters = nil
|
||||
end
|
||||
|
||||
def request_uri=(uri)
|
||||
@request_uri = uri
|
||||
@path = uri.split("?").first
|
||||
end
|
||||
|
||||
private
|
||||
def initialize_containers
|
||||
@env, @cookies = {}, {}
|
||||
end
|
||||
|
||||
def initialize_default_values
|
||||
@host = "test.host"
|
||||
@request_uri = "/"
|
||||
@remote_addr = "127.0.0.1"
|
||||
@env["SERVER_PORT"] = 80
|
||||
end
|
||||
end
|
||||
|
||||
class TestResponse < AbstractResponse #:nodoc:
|
||||
# the class attribute ties a TestResponse to the assertions
|
||||
class << self
|
||||
attr_accessor :assertion_target
|
||||
end
|
||||
|
||||
# initializer
|
||||
def initialize
|
||||
TestResponse.assertion_target=self# if TestResponse.assertion_target.nil?
|
||||
super()
|
||||
end
|
||||
|
||||
# the response code of the request
|
||||
def response_code
|
||||
headers['Status'][0,3].to_i rescue 0
|
||||
end
|
||||
|
||||
# was the response successful?
|
||||
def success?
|
||||
response_code == 200
|
||||
end
|
||||
|
||||
# was the URL not found?
|
||||
def missing?
|
||||
response_code == 404
|
||||
end
|
||||
|
||||
# were we redirected?
|
||||
def redirect?
|
||||
(300..399).include?(response_code)
|
||||
end
|
||||
|
||||
# was there a server-side error?
|
||||
def server_error?
|
||||
(500..599).include?(response_code)
|
||||
end
|
||||
|
||||
# returns the redirection location or nil
|
||||
def redirect_url
|
||||
redirect? ? headers['location'] : nil
|
||||
end
|
||||
|
||||
# does the redirect location match this regexp pattern?
|
||||
def redirect_url_match?( pattern )
|
||||
return false if redirect_url.nil?
|
||||
p = Regexp.new(pattern) if pattern.class == String
|
||||
p = pattern if pattern.class == Regexp
|
||||
return false if p.nil?
|
||||
p.match(redirect_url) != nil
|
||||
end
|
||||
|
||||
# returns the template path of the file which was used to
|
||||
# render this response (or nil)
|
||||
def rendered_file(with_controller=false)
|
||||
unless template.first_render.nil?
|
||||
unless with_controller
|
||||
template.first_render
|
||||
else
|
||||
template.first_render.split('/').last || template.first_render
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# was this template rendered by a file?
|
||||
def rendered_with_file?
|
||||
!rendered_file.nil?
|
||||
end
|
||||
|
||||
# a shortcut to the flash (or an empty hash if no flash.. hey! that rhymes!)
|
||||
def flash
|
||||
session['flash'] || {}
|
||||
end
|
||||
|
||||
# do we have a flash?
|
||||
def has_flash?
|
||||
!session['flash'].nil?
|
||||
end
|
||||
|
||||
# do we have a flash that has contents?
|
||||
def has_flash_with_contents?
|
||||
!flash.empty?
|
||||
end
|
||||
|
||||
# does the specified flash object exist?
|
||||
def has_flash_object?(name=nil)
|
||||
!flash[name].nil?
|
||||
end
|
||||
|
||||
# does the specified object exist in the session?
|
||||
def has_session_object?(name=nil)
|
||||
!session[name].nil?
|
||||
end
|
||||
|
||||
# a shortcut to the template.assigns
|
||||
def template_objects
|
||||
template.assigns || {}
|
||||
end
|
||||
|
||||
# does the specified template object exist?
|
||||
def has_template_object?(name=nil)
|
||||
!template_objects[name].nil?
|
||||
end
|
||||
end
|
||||
|
||||
class TestSession #:nodoc:
|
||||
def initialize(attributes = {})
|
||||
@attributes = attributes
|
||||
end
|
||||
|
||||
def [](key)
|
||||
@attributes[key]
|
||||
end
|
||||
|
||||
def []=(key, value)
|
||||
@attributes[key] = value
|
||||
end
|
||||
|
||||
def update() end
|
||||
def close() end
|
||||
def delete() @attributes = {} end
|
||||
end
|
||||
end
|
||||
|
||||
class Test::Unit::TestCase #:nodoc:
|
||||
private
|
||||
# execute the request and set/volley the response
|
||||
def process(action, parameters = nil, session = nil)
|
||||
@request.action = action.to_s
|
||||
@request.parameters.update(parameters) unless parameters.nil?
|
||||
@request.session = ActionController::TestSession.new(session) unless session.nil?
|
||||
@controller.process(@request, @response)
|
||||
end
|
||||
end
|
|
@ -0,0 +1,170 @@
|
|||
module ActionController
|
||||
# Rewrites urls for Base.redirect_to and Base.url_for in the controller.
|
||||
class UrlRewriter #:nodoc:
|
||||
VALID_OPTIONS = [:action, :action_prefix, :action_suffix, :module, :controller, :controller_prefix, :anchor, :params, :path_params, :id, :only_path, :overwrite_params ]
|
||||
|
||||
def initialize(request, controller, action)
|
||||
@request, @controller, @action = request, controller, action
|
||||
@rewritten_path = @request.path ? @request.path.dup : ""
|
||||
end
|
||||
|
||||
def rewrite(options = {})
|
||||
validate_options(VALID_OPTIONS, options.keys)
|
||||
|
||||
rewrite_url(
|
||||
rewrite_path(@rewritten_path, options),
|
||||
options
|
||||
)
|
||||
end
|
||||
|
||||
def to_s
|
||||
to_str
|
||||
end
|
||||
|
||||
def to_str
|
||||
"#{@request.protocol}, #{@request.host_with_port}, #{@request.path}, #{@controller}, #{@action}, #{@request.parameters.inspect}"
|
||||
end
|
||||
|
||||
private
|
||||
def validate_options(valid_option_keys, supplied_option_keys)
|
||||
unknown_option_keys = supplied_option_keys - valid_option_keys
|
||||
raise(ActionController::ActionControllerError, "Unknown options: #{unknown_option_keys}") unless unknown_option_keys.empty?
|
||||
end
|
||||
|
||||
def rewrite_url(path, options)
|
||||
rewritten_url = ""
|
||||
rewritten_url << @request.protocol unless options[:only_path]
|
||||
rewritten_url << @request.host_with_port unless options[:only_path]
|
||||
|
||||
rewritten_url << path
|
||||
rewritten_url << build_query_string(new_parameters(options)) if options[:params] || options[:overwrite_params]
|
||||
rewritten_url << "##{options[:anchor]}" if options[:anchor]
|
||||
return rewritten_url
|
||||
end
|
||||
|
||||
def rewrite_path(path, options)
|
||||
include_id_in_path_params(options)
|
||||
|
||||
path = rewrite_action(path, options) if options[:action] || options[:action_prefix]
|
||||
path = rewrite_path_params(path, options) if options[:path_params]
|
||||
path = rewrite_controller(path, options) if options[:controller] || options[:controller_prefix]
|
||||
return path
|
||||
end
|
||||
|
||||
def rewrite_path_params(path, options)
|
||||
index_action = options[:action] == 'index' || options[:action].nil? && @action == 'index'
|
||||
id_only = options[:path_params].size == 1 && options[:path_params]['id']
|
||||
|
||||
if index_action && id_only
|
||||
path += '/' unless path[-1..-1] == '/'
|
||||
path += "index/#{options[:path_params]['id']}"
|
||||
path
|
||||
else
|
||||
options[:path_params].inject(path) do |path, pair|
|
||||
if options[:action].nil? && @request.parameters[pair.first]
|
||||
path.sub(/\b#{@request.parameters[pair.first]}\b/, pair.last.to_s)
|
||||
else
|
||||
path += "/#{pair.last}"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def rewrite_action(path, options)
|
||||
# This regex assumes that "index" actions won't be included in the URL
|
||||
all, controller_prefix, action_prefix, action_suffix =
|
||||
/^\/(.*)#{@controller}\/(.*)#{@action == "index" ? "" : @action}(.*)/.match(path).to_a
|
||||
|
||||
if @action == "index"
|
||||
if action_prefix == "index"
|
||||
# we broke the parsing assumption that this would be excluded, so
|
||||
# don't tell action_name about our little boo-boo
|
||||
path = path.sub(action_prefix, action_name(options, nil))
|
||||
elsif action_prefix && !action_prefix.empty?
|
||||
path = path.sub(action_prefix, action_name(options, action_prefix))
|
||||
else
|
||||
path = path.sub(%r(#{@controller}/?), @controller + "/" + action_name(options)) # " ruby-mode
|
||||
end
|
||||
else
|
||||
path = path.sub((action_prefix || "") + @action + (action_suffix || ""), action_name(options, action_prefix))
|
||||
end
|
||||
|
||||
if options[:controller_prefix] && !options[:controller]
|
||||
ensure_slash_suffix(options, :controller_prefix)
|
||||
if controller_prefix
|
||||
path = path.sub(controller_prefix, options[:controller_prefix])
|
||||
else
|
||||
path = options[:controller_prefix] + path
|
||||
end
|
||||
end
|
||||
|
||||
return path
|
||||
end
|
||||
|
||||
def rewrite_controller(path, options)
|
||||
all, controller_prefix = /^\/(.*?)#{@controller}/.match(path).to_a
|
||||
path = "/"
|
||||
path << controller_name(options, controller_prefix)
|
||||
path << action_name(options) if options[:action]
|
||||
path << path_params_in_list(options) if options[:path_params]
|
||||
return path
|
||||
end
|
||||
|
||||
def action_name(options, action_prefix = nil, action_suffix = nil)
|
||||
ensure_slash_suffix(options, :action_prefix)
|
||||
ensure_slash_prefix(options, :action_suffix)
|
||||
|
||||
prefix = options[:action_prefix] || action_prefix || ""
|
||||
suffix = options[:action] == "index" ? "" : (options[:action_suffix] || action_suffix || "")
|
||||
name = (options[:action] == "index" ? "" : options[:action]) || ""
|
||||
|
||||
return prefix + name + suffix
|
||||
end
|
||||
|
||||
def controller_name(options, controller_prefix)
|
||||
options[:controller_prefix] = "#{options[:module]}/#{options[:controller_prefix]}" if options[:module]
|
||||
ensure_slash_suffix(options, :controller_prefix)
|
||||
controller_name = options[:controller_prefix] || controller_prefix || ""
|
||||
controller_name << (options[:controller] + "/") if options[:controller]
|
||||
return controller_name
|
||||
end
|
||||
|
||||
def path_params_in_list(options)
|
||||
options[:path_params].inject("") { |path, pair| path += "/#{pair.last}" }
|
||||
end
|
||||
|
||||
def ensure_slash_suffix(options, key)
|
||||
options[key] = options[key] + "/" if options[key] && !options[key].empty? && options[key][-1..-1] != "/"
|
||||
end
|
||||
|
||||
def ensure_slash_prefix(options, key)
|
||||
options[key] = "/" + options[key] if options[key] && !options[key].empty? && options[key][0..1] != "/"
|
||||
end
|
||||
|
||||
def include_id_in_path_params(options)
|
||||
options[:path_params] = (options[:path_params] || {}).merge({"id" => options[:id]}) if options[:id]
|
||||
end
|
||||
|
||||
def new_parameters(options)
|
||||
parameters = options[:params] || existing_parameters
|
||||
parameters.update(options[:overwrite_params]) if options[:overwrite_params]
|
||||
parameters.reject { |key,value| value.nil? }
|
||||
end
|
||||
|
||||
def existing_parameters
|
||||
@request.parameters.reject { |key, value| %w( id action controller).include?(key) }
|
||||
end
|
||||
|
||||
# Returns a query string with escaped keys and values from the passed hash. If the passed hash contains an "id" it'll
|
||||
# be added as a path element instead of a regular parameter pair.
|
||||
def build_query_string(hash)
|
||||
elements = []
|
||||
query_string = ""
|
||||
|
||||
hash.each { |key, value| elements << "#{CGI.escape(key)}=#{CGI.escape(value.to_s)}" }
|
||||
unless elements.empty? then query_string << ("?" + elements.join("&")) end
|
||||
|
||||
return query_string
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,49 @@
|
|||
#--
|
||||
# Copyright (c) 2004 David Heinemeier Hansson
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining
|
||||
# a copy of this software and associated documentation files (the
|
||||
# "Software"), to deal in the Software without restriction, including
|
||||
# without limitation the rights to use, copy, modify, merge, publish,
|
||||
# distribute, sublicense, and/or sell copies of the Software, and to
|
||||
# permit persons to whom the Software is furnished to do so, subject to
|
||||
# the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
#++
|
||||
|
||||
begin
|
||||
require 'rubygems'
|
||||
require 'builder'
|
||||
rescue LoadError
|
||||
# RubyGems is not available, use included Builder
|
||||
$:.unshift(File.dirname(__FILE__) + "/action_view/vendor")
|
||||
require 'action_view/vendor/builder'
|
||||
ensure
|
||||
# Temporary patch until it's in Builder 1.2.2
|
||||
class BlankSlate
|
||||
class << self
|
||||
def hide(name)
|
||||
undef_method name if instance_methods.include?(name) and name !~ /^(__|instance_eval)/
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
require 'action_view/base'
|
||||
require 'action_view/partials'
|
||||
|
||||
ActionView::Base.class_eval do
|
||||
include ActionView::Partials
|
||||
end
|
||||
|
||||
ActionView::Base.load_helpers(File.dirname(__FILE__) + "/action_view/helpers/")
|
|
@ -0,0 +1,264 @@
|
|||
require 'erb'
|
||||
|
||||
module ActionView #:nodoc:
|
||||
class ActionViewError < StandardError #:nodoc:
|
||||
end
|
||||
|
||||
# Action View templates can be written in two ways. If the template file has a +.rhtml+ extension then it uses a mixture of ERb
|
||||
# (included in Ruby) and HTML. If the template file has a +.rxml+ extension then Jim Weirich's Builder::XmlMarkup library is used.
|
||||
#
|
||||
# = ERb
|
||||
#
|
||||
# You trigger ERb by using embeddings such as <% %> and <%= %>. The difference is whether you want output or not. Consider the
|
||||
# following loop for names:
|
||||
#
|
||||
# <b>Names of all the people</b>
|
||||
# <% for person in @people %>
|
||||
# Name: <%= person.name %><br/>
|
||||
# <% end %>
|
||||
#
|
||||
# The loop is setup in regular embedding tags (<% %>) and the name is written using the output embedding tag (<%= %>). Note that this
|
||||
# is not just a usage suggestion. Regular output functions like print or puts won't work with ERb templates. So this would be wrong:
|
||||
#
|
||||
# Hi, Mr. <% puts "Frodo" %>
|
||||
#
|
||||
# (If you absolutely must write from within a function, you can use the TextHelper#concat)
|
||||
#
|
||||
# == Using sub templates
|
||||
#
|
||||
# Using sub templates allows you to sidestep tedious replication and extract common display structures in shared templates. The
|
||||
# classic example is the use of a header and footer (even though the Action Pack-way would be to use Layouts):
|
||||
#
|
||||
# <%= render "shared/header" %>
|
||||
# Something really specific and terrific
|
||||
# <%= render "shared/footer" %>
|
||||
#
|
||||
# As you see, we use the output embeddings for the render methods. The render call itself will just return a string holding the
|
||||
# result of the rendering. The output embedding writes it to the current template.
|
||||
#
|
||||
# But you don't have to restrict yourself to static includes. Templates can share variables amongst themselves by using instance
|
||||
# variables defined in using the regular embedding tags. Like this:
|
||||
#
|
||||
# <% @page_title = "A Wonderful Hello" %>
|
||||
# <%= render "shared/header" %>
|
||||
#
|
||||
# Now the header can pick up on the @page_title variable and use it for outputting a title tag:
|
||||
#
|
||||
# <title><%= @page_title %></title>
|
||||
#
|
||||
# == Passing local variables to sub templates
|
||||
#
|
||||
# You can pass local variables to sub templates by using a hash with the variable names as keys and the objects as values:
|
||||
#
|
||||
# <%= render "shared/header", { "headline" => "Welcome", "person" => person } %>
|
||||
#
|
||||
# These can now be accessed in shared/header with:
|
||||
#
|
||||
# Headline: <%= headline %>
|
||||
# First name: <%= person.first_name %>
|
||||
#
|
||||
# == Template caching
|
||||
#
|
||||
# The parsing of ERb templates are cached by default, but the reading of them are not. This means that the application by default
|
||||
# will reflect changes to the templates immediatly. If you'd like to sacrifice that immediacy for the speed gain given by also
|
||||
# caching the loading of templates (reading from the file systen), you can turn that on with
|
||||
# <tt>ActionView::Base.cache_template_loading = true</tt>.
|
||||
#
|
||||
# == Builder
|
||||
#
|
||||
# Builder templates are a more programatic alternative to ERb. They are especially useful for generating XML content. An +XmlMarkup+ object
|
||||
# named +xml+ is automatically made available to templates with a +.rxml+ extension.
|
||||
#
|
||||
# Here are some basic examples:
|
||||
#
|
||||
# xml.em("emphasized") # => <em>emphasized</em>
|
||||
# xml.em { xml.b("emp & bold") } # => <em><b>emph & bold</b></em>
|
||||
# xml.a("A Link", "href"=>"http://onestepback.org") # => <a href="http://onestepback.org">A Link</a>
|
||||
# xm.target("name"=>"compile", "option"=>"fast") # => <target option="fast" name="compile"\>
|
||||
# # NOTE: order of attributes is not specified.
|
||||
#
|
||||
# Any method with a block will be treated as an XML markup tag with nested markup in the block. For example, the following:
|
||||
#
|
||||
# xml.div {
|
||||
# xml.h1(@person.name)
|
||||
# xml.p(@person.bio)
|
||||
# }
|
||||
#
|
||||
# would produce something like:
|
||||
#
|
||||
# <div>
|
||||
# <h1>David Heinemeier Hansson</h1>
|
||||
# <p>A product of Danish Design during the Winter of '79...</p>
|
||||
# </div>
|
||||
#
|
||||
# A full-length RSS example actually used on Basecamp:
|
||||
#
|
||||
# xml.rss("version" => "2.0", "xmlns:dc" => "http://purl.org/dc/elements/1.1/") do
|
||||
# xml.channel do
|
||||
# xml.title(@feed_title)
|
||||
# xml.link(@url)
|
||||
# xml.description "Basecamp: Recent items"
|
||||
# xml.language "en-us"
|
||||
# xml.ttl "40"
|
||||
#
|
||||
# for item in @recent_items
|
||||
# xml.item do
|
||||
# xml.title(item_title(item))
|
||||
# xml.description(item_description(item)) if item_description(item)
|
||||
# xml.pubDate(item_pubDate(item))
|
||||
# xml.guid(@person.firm.account.url + @recent_items.url(item))
|
||||
# xml.link(@person.firm.account.url + @recent_items.url(item))
|
||||
#
|
||||
# xml.tag!("dc:creator", item.author_name) if item_has_creator?(item)
|
||||
# end
|
||||
# end
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# More builder documentation can be found at http://builder.rubyforge.org.
|
||||
class Base
|
||||
include ERB::Util
|
||||
|
||||
attr_reader :first_render
|
||||
attr_accessor :base_path, :assigns, :template_extension
|
||||
attr_accessor :controller
|
||||
|
||||
# Turn on to cache the reading of templates from the file system. Doing so means that you have to restart the server
|
||||
# when changing templates, but that rendering will be faster.
|
||||
@@cache_template_loading = false
|
||||
cattr_accessor :cache_template_loading
|
||||
|
||||
@@compiled_erb_templates = {}
|
||||
@@loaded_templates = {}
|
||||
|
||||
def self.load_helpers(helper_dir)#:nodoc:
|
||||
Dir.foreach(helper_dir) do |helper_file|
|
||||
next unless helper_file =~ /_helper.rb$/
|
||||
require helper_dir + helper_file
|
||||
helper_module_name = helper_file.capitalize.gsub(/_([a-z])/) { |m| $1.capitalize }[0..-4]
|
||||
|
||||
class_eval("include ActionView::Helpers::#{helper_module_name}") if Helpers.const_defined?(helper_module_name)
|
||||
end
|
||||
end
|
||||
|
||||
def self.controller_delegate(*methods)
|
||||
methods.flatten.each do |method|
|
||||
class_eval <<-end_eval
|
||||
def #{method}(*args, &block)
|
||||
controller.send(%(#{method}), *args, &block)
|
||||
end
|
||||
end_eval
|
||||
end
|
||||
end
|
||||
|
||||
def initialize(base_path = nil, assigns_for_first_render = {}, controller = nil)#:nodoc:
|
||||
@base_path, @assigns = base_path, assigns_for_first_render
|
||||
@controller = controller
|
||||
end
|
||||
|
||||
# Renders the template present at <tt>template_path</tt>. If <tt>use_full_path</tt> is set to true,
|
||||
# it's relative to the template_root, otherwise it's absolute. The hash in <tt>local_assigns</tt>
|
||||
# is made available as local variables.
|
||||
def render_file(template_path, use_full_path = true, local_assigns = {})
|
||||
@first_render = template_path if @first_render.nil?
|
||||
|
||||
if use_full_path
|
||||
template_extension = pick_template_extension(template_path)
|
||||
template_file_name = full_template_path(template_path, template_extension)
|
||||
else
|
||||
template_file_name = template_path
|
||||
template_extension = template_path.split(".").last
|
||||
end
|
||||
|
||||
template_source = read_template_file(template_file_name)
|
||||
|
||||
begin
|
||||
render_template(template_extension, template_source, local_assigns)
|
||||
rescue Exception => e
|
||||
if TemplateError === e
|
||||
e.sub_template_of(template_file_name)
|
||||
raise e
|
||||
else
|
||||
raise TemplateError.new(@base_path, template_file_name, @assigns, template_source, e)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Renders the template present at <tt>template_path</tt> (relative to the template_root).
|
||||
# The hash in <tt>local_assigns</tt> is made available as local variables.
|
||||
def render(template_path, local_assigns = {})
|
||||
render_file(template_path, true, local_assigns)
|
||||
end
|
||||
|
||||
# Renders the +template+ which is given as a string as either rhtml or rxml depending on <tt>template_extension</tt>.
|
||||
# The hash in <tt>local_assigns</tt> is made available as local variables.
|
||||
def render_template(template_extension, template, local_assigns = {})
|
||||
b = binding
|
||||
local_assigns.each { |key, value| eval "#{key} = local_assigns[\"#{key}\"]", b }
|
||||
@assigns.each { |key, value| instance_variable_set "@#{key}", value }
|
||||
xml = Builder::XmlMarkup.new(:indent => 2)
|
||||
|
||||
send(pick_rendering_method(template_extension), template, binding)
|
||||
end
|
||||
|
||||
def pick_template_extension(template_path)#:nodoc:
|
||||
if erb_template_exists?(template_path)
|
||||
"rhtml"
|
||||
elsif builder_template_exists?(template_path)
|
||||
"rxml"
|
||||
else
|
||||
raise ActionViewError, "No rhtml or rxml template found for #{template_path}"
|
||||
end
|
||||
end
|
||||
|
||||
def pick_rendering_method(template_extension)#:nodoc:
|
||||
(template_extension == "rxml" ? "rxml" : "rhtml") + "_render"
|
||||
end
|
||||
|
||||
def erb_template_exists?(template_path)#:nodoc:
|
||||
template_exists?(template_path, "rhtml")
|
||||
end
|
||||
|
||||
def builder_template_exists?(template_path)#:nodoc:
|
||||
template_exists?(template_path, "rxml")
|
||||
end
|
||||
|
||||
def file_exists?(template_path)#:nodoc:
|
||||
erb_template_exists?(template_path) || builder_template_exists?(template_path)
|
||||
end
|
||||
|
||||
# Returns true is the file may be rendered implicitly.
|
||||
def file_public?(template_path)#:nodoc:
|
||||
template_path.split("/").last[0,1] != "_"
|
||||
end
|
||||
|
||||
private
|
||||
def full_template_path(template_path, extension)
|
||||
"#{@base_path}/#{template_path}.#{extension}"
|
||||
end
|
||||
|
||||
def template_exists?(template_path, extension)
|
||||
FileTest.exists?(full_template_path(template_path, extension))
|
||||
end
|
||||
|
||||
def read_template_file(template_path)
|
||||
unless cache_template_loading && @@loaded_templates[template_path]
|
||||
@@loaded_templates[template_path] = File.read(template_path)
|
||||
end
|
||||
|
||||
@@loaded_templates[template_path]
|
||||
end
|
||||
|
||||
def rhtml_render(template, binding)
|
||||
@@compiled_erb_templates[template] ||= ERB.new(template)
|
||||
@@compiled_erb_templates[template].result(binding)
|
||||
end
|
||||
|
||||
def rxml_render(template, binding)
|
||||
@controller.headers["Content-Type"] ||= 'text/xml'
|
||||
eval(template, binding)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
require 'action_view/template_error'
|
|
@ -0,0 +1,171 @@
|
|||
require 'cgi'
|
||||
require File.dirname(__FILE__) + '/form_helper'
|
||||
|
||||
module ActionView
|
||||
class Base
|
||||
@@field_error_proc = Proc.new{ |html_tag, instance| "<div class=\"fieldWithErrors\">#{html_tag}</div>" }
|
||||
cattr_accessor :field_error_proc
|
||||
end
|
||||
|
||||
module Helpers
|
||||
# The Active Record Helper makes it easier to create forms for records kept in instance variables. The most far-reaching is the form
|
||||
# method that creates a complete form for all the basic content types of the record (not associations or aggregations, though). This
|
||||
# is a great of making the record quickly available for editing, but likely to prove lacklusters for a complicated real-world form.
|
||||
# In that case, it's better to use the input method and the specialized form methods in link:classes/ActionView/Helpers/FormHelper.html
|
||||
module ActiveRecordHelper
|
||||
# Returns a default input tag for the type of object returned by the method. Example
|
||||
# (title is a VARCHAR column and holds "Hello World"):
|
||||
# input("post", "title") =>
|
||||
# <input id="post_title" name="post[title]" size="30" type="text" value="Hello World" />
|
||||
def input(record_name, method)
|
||||
InstanceTag.new(record_name, method, self).to_tag
|
||||
end
|
||||
|
||||
# Returns an entire form with input tags and everything for a specified Active Record object. Example
|
||||
# (post is a new record that has a title using VARCHAR and a body using TEXT):
|
||||
# form("post") =>
|
||||
# <form action='create' method='POST'>
|
||||
# <p>
|
||||
# <label for="post_title">Title</label><br />
|
||||
# <input id="post_title" name="post[title]" size="30" type="text" value="Hello World" />
|
||||
# </p>
|
||||
# <p>
|
||||
# <label for="post_body">Body</label><br />
|
||||
# <textarea cols="40" id="post_body" name="post[body]" rows="20" wrap="virtual">
|
||||
# Back to the hill and over it again!
|
||||
# </textarea>
|
||||
# </p>
|
||||
# <input type='submit' value='Create' />
|
||||
# </form>
|
||||
#
|
||||
# It's possible to specialize the form builder by using a different action name and by supplying another
|
||||
# block renderer. Example (entry is a new record that has a message attribute using VARCHAR):
|
||||
#
|
||||
# form("entry", :action => "sign", :input_block =>
|
||||
# Proc.new { |record, column| "#{column.human_name}: #{input(record, column.name)}<br />" }) =>
|
||||
#
|
||||
# <form action='sign' method='POST'>
|
||||
# Message:
|
||||
# <input id="post_title" name="post[title]" size="30" type="text" value="Hello World" /><br />
|
||||
# <input type='submit' value='Sign' />
|
||||
# </form>
|
||||
def form(record_name, options = {})
|
||||
record = instance_eval("@#{record_name}")
|
||||
action = options[:action] || (record.new_record? ? "create" : "update")
|
||||
id_field = record.new_record? ? "" : InstanceTag.new(record_name, "id", self).to_input_field_tag("hidden")
|
||||
|
||||
"<form action='#{action}' method='POST'>" +
|
||||
id_field + all_input_tags(record, record_name, options) +
|
||||
"<input type='submit' value='#{action.gsub(/[^A-Za-z]/, "").capitalize}' />" +
|
||||
"</form>"
|
||||
end
|
||||
|
||||
# Returns a string containing the error message attached to the +method+ on the +object+, if one exists.
|
||||
# This error message is wrapped in a DIV tag, which can be specialized to include both a +prepend_text+ and +append_text+
|
||||
# to properly introduce the error and a +css_class+ to style it accordingly. Examples (post has an error message
|
||||
# "can't be empty" on the title attribute):
|
||||
#
|
||||
# <%= error_message_on "post", "title" %> =>
|
||||
# <div class="formError">can't be empty</div>
|
||||
#
|
||||
# <%= error_message_on "post", "title", "Title simply ", " (or it won't work)", "inputError" %> =>
|
||||
# <div class="inputError">Title simply can't be empty (or it won't work)</div>
|
||||
def error_message_on(object, method, prepend_text = "", append_text = "", css_class = "formError")
|
||||
if errors = instance_eval("@#{object}").errors.on(method)
|
||||
"<div class=\"#{css_class}\">#{prepend_text + (errors.is_a?(Array) ? errors.first : errors) + append_text}</div>"
|
||||
end
|
||||
end
|
||||
|
||||
def error_messages_for(object_name)
|
||||
object = instance_eval("@#{object_name}")
|
||||
unless object.errors.empty?
|
||||
"<div id=\"errorExplanation\">" +
|
||||
"<h2>#{object.errors.count} error#{"s" unless object.errors.count == 1} prohibited this #{object_name.gsub("_", " ")} from being saved</h2>" +
|
||||
"<p>There were problems with the following fields (marked in red below):</p>" +
|
||||
"<ul>#{object.errors.full_messages.collect { |msg| "<li>#{msg}</li>"}}</ul>" +
|
||||
"</div>"
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def all_input_tags(record, record_name, options)
|
||||
input_block = options[:input_block] || default_input_block
|
||||
record.class.content_columns.collect{ |column| input_block.call(record_name, column) }.join("\n")
|
||||
end
|
||||
|
||||
def default_input_block
|
||||
Proc.new { |record, column| "<p><label for=\"#{record}_#{column.name}\">#{column.human_name}</label><br />#{input(record, column.name)}</p>" }
|
||||
end
|
||||
end
|
||||
|
||||
class InstanceTag #:nodoc:
|
||||
def to_tag(options = {})
|
||||
case column_type
|
||||
when :string
|
||||
field_type = @method_name.include?("password") ? "password" : "text"
|
||||
to_input_field_tag(field_type, options)
|
||||
when :text
|
||||
to_text_area_tag(options)
|
||||
when :integer, :float
|
||||
to_input_field_tag("text", options)
|
||||
when :date
|
||||
to_date_select_tag(options)
|
||||
when :datetime
|
||||
to_datetime_select_tag(options)
|
||||
when :boolean
|
||||
to_boolean_select_tag(options)
|
||||
end
|
||||
end
|
||||
|
||||
alias_method :tag_without_error_wrapping, :tag
|
||||
|
||||
def tag(name, options)
|
||||
if object.respond_to?("errors") && object.errors.respond_to?("on")
|
||||
error_wrapping(tag_without_error_wrapping(name, options), object.errors.on(@method_name))
|
||||
else
|
||||
tag_without_error_wrapping(name, options)
|
||||
end
|
||||
end
|
||||
|
||||
alias_method :content_tag_without_error_wrapping, :content_tag
|
||||
|
||||
def content_tag(name, value, options)
|
||||
if object.respond_to?("errors") && object.errors.respond_to?("on")
|
||||
error_wrapping(content_tag_without_error_wrapping(name, value, options), object.errors.on(@method_name))
|
||||
else
|
||||
content_tag_without_error_wrapping(name, value, options)
|
||||
end
|
||||
end
|
||||
|
||||
alias_method :to_date_select_tag_without_error_wrapping, :to_date_select_tag
|
||||
def to_date_select_tag(options = {})
|
||||
if object.respond_to?("errors") && object.errors.respond_to?("on")
|
||||
error_wrapping(to_date_select_tag_without_error_wrapping(options), object.errors.on(@method_name))
|
||||
else
|
||||
to_date_select_tag_without_error_wrapping(options)
|
||||
end
|
||||
end
|
||||
|
||||
alias_method :to_datetime_select_tag_without_error_wrapping, :to_datetime_select_tag
|
||||
def to_datetime_select_tag(options = {})
|
||||
if object.respond_to?("errors") && object.errors.respond_to?("on")
|
||||
error_wrapping(to_datetime_select_tag_without_error_wrapping(options), object.errors.on(@method_name))
|
||||
else
|
||||
to_datetime_select_tag_without_error_wrapping(options)
|
||||
end
|
||||
end
|
||||
|
||||
def error_wrapping(html_tag, has_error)
|
||||
has_error ? Base.field_error_proc.call(html_tag, self) : html_tag
|
||||
end
|
||||
|
||||
def error_message
|
||||
object.errors.on(@method_name)
|
||||
end
|
||||
|
||||
def column_type
|
||||
object.send("column_for_attribute", @method_name).type
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,230 @@
|
|||
require "date"
|
||||
|
||||
module ActionView
|
||||
module Helpers
|
||||
# The Date Helper primarily creates select/option tags for different kinds of dates and date elements. All of the select-type methods
|
||||
# share a number of common options that are as follows:
|
||||
#
|
||||
# * <tt>:prefix</tt> - overwrites the default prefix of "date" used for the select names. So specifying "birthday" would give
|
||||
# birthday[month] instead of date[month] if passed to the select_month method.
|
||||
# * <tt>:include_blank</tt> - set to true if it should be possible to set an empty date.
|
||||
# * <tt>:discard_type</tt> - set to true if you want to discard the type part of the select name. If set to true, the select_month
|
||||
# method would use simply "date" (which can be overwritten using <tt>:prefix</tt>) instead of "date[month]".
|
||||
module DateHelper
|
||||
DEFAULT_PREFIX = "date" unless const_defined?("DEFAULT_PREFIX")
|
||||
|
||||
# Reports the approximate distance in time between to Time objects. For example, if the distance is 47 minutes, it'll return
|
||||
# "about 1 hour". See the source for the complete wording list.
|
||||
def distance_of_time_in_words(from_time, to_time)
|
||||
distance_in_minutes = ((to_time - from_time) / 60).round
|
||||
|
||||
case distance_in_minutes
|
||||
when 0 then "less than a minute"
|
||||
when 1 then "1 minute"
|
||||
when 2..45 then "#{distance_in_minutes} minutes"
|
||||
when 46..90 then "about 1 hour"
|
||||
when 90..1440 then "about #{(distance_in_minutes.to_f / 60.0).round} hours"
|
||||
when 1441..2880 then "1 day"
|
||||
else "#{(distance_in_minutes / 1440).round} days"
|
||||
end
|
||||
end
|
||||
|
||||
# Like distance_of_time_in_words, but where <tt>to_time</tt> is fixed to <tt>Time.now</tt>.
|
||||
def distance_of_time_in_words_to_now(from_time)
|
||||
distance_of_time_in_words(from_time, Time.now)
|
||||
end
|
||||
|
||||
# Returns a set of select tags (one for year, month, and day) pre-selected for accessing a specified date-based attribute (identified by
|
||||
# +method+) on an object assigned to the template (identified by +object+). It's possible to tailor the selects through the +options+ hash,
|
||||
# which both accepts all the keys that each of the individual select builders does (like :use_month_numbers for select_month) and a range
|
||||
# of discard options. The discard options are <tt>:discard_month</tt> and <tt>:discard_day</tt>. Set to true, they'll drop the respective
|
||||
# select. Discarding the month select will also automatically discard the day select.
|
||||
#
|
||||
# NOTE: Discarded selects will default to 1. So if no month select is available, January will be assumed. Additionally, you can get the
|
||||
# month select before the year by setting :month_before_year to true in the options. This is especially useful for credit card forms.
|
||||
# Examples:
|
||||
#
|
||||
# date_select("post", "written_on")
|
||||
# date_select("post", "written_on", :start_year => 1995)
|
||||
# date_select("post", "written_on", :start_year => 1995, :use_month_numbers => true,
|
||||
# :discard_day => true, :include_blank => true)
|
||||
#
|
||||
# The selects are prepared for multi-parameter assignment to an Active Record object.
|
||||
def date_select(object, method, options = {})
|
||||
InstanceTag.new(object, method, self).to_date_select_tag(options)
|
||||
end
|
||||
|
||||
# Returns a set of select tags (one for year, month, day, hour, and minute) pre-selected for accessing a specified datetime-based
|
||||
# attribute (identified by +method+) on an object assigned to the template (identified by +object+). Examples:
|
||||
#
|
||||
# datetime_select("post", "written_on")
|
||||
# datetime_select("post", "written_on", :start_year => 1995)
|
||||
#
|
||||
# The selects are prepared for multi-parameter assignment to an Active Record object.
|
||||
def datetime_select(object, method, options = {})
|
||||
InstanceTag.new(object, method, self).to_datetime_select_tag(options)
|
||||
end
|
||||
|
||||
# Returns a set of html select-tags (one for year, month, and day) pre-selected with the +date+.
|
||||
def select_date(date = Date.today, options = {})
|
||||
select_year(date, options) + select_month(date, options) + select_day(date, options)
|
||||
end
|
||||
|
||||
# Returns a set of html select-tags (one for year, month, day, hour, and minute) preselected the +datetime+.
|
||||
def select_datetime(datetime = Time.now, options = {})
|
||||
select_year(datetime, options) + select_month(datetime, options) + select_day(datetime, options) +
|
||||
select_hour(datetime, options) + select_minute(datetime, options)
|
||||
end
|
||||
|
||||
# Returns a select tag with options for each of the minutes 0 through 59 with the current minute selected.
|
||||
# The <tt>minute</tt> can also be substituted for a minute number.
|
||||
def select_minute(datetime, options = {})
|
||||
minute_options = []
|
||||
|
||||
0.upto(59) do |minute|
|
||||
minute_options << ((datetime.kind_of?(Fixnum) ? datetime : datetime.min) == minute ?
|
||||
"<option selected=\"selected\">#{leading_zero_on_single_digits(minute)}</option>\n" :
|
||||
"<option>#{leading_zero_on_single_digits(minute)}</option>\n"
|
||||
)
|
||||
end
|
||||
|
||||
select_html("minute", minute_options, options[:prefix], options[:include_blank], options[:discard_type])
|
||||
end
|
||||
|
||||
# Returns a select tag with options for each of the hours 0 through 23 with the current hour selected.
|
||||
# The <tt>hour</tt> can also be substituted for a hour number.
|
||||
def select_hour(datetime, options = {})
|
||||
hour_options = []
|
||||
|
||||
0.upto(23) do |hour|
|
||||
hour_options << ((datetime.kind_of?(Fixnum) ? datetime : datetime.hour) == hour ?
|
||||
"<option selected=\"selected\">#{leading_zero_on_single_digits(hour)}</option>\n" :
|
||||
"<option>#{leading_zero_on_single_digits(hour)}</option>\n"
|
||||
)
|
||||
end
|
||||
|
||||
select_html("hour", hour_options, options[:prefix], options[:include_blank], options[:discard_type])
|
||||
end
|
||||
|
||||
# Returns a select tag with options for each of the days 1 through 31 with the current day selected.
|
||||
# The <tt>date</tt> can also be substituted for a hour number.
|
||||
def select_day(date, options = {})
|
||||
day_options = []
|
||||
|
||||
1.upto(31) do |day|
|
||||
day_options << ((date.kind_of?(Fixnum) ? date : date.day) == day ?
|
||||
"<option selected=\"selected\">#{day}</option>\n" :
|
||||
"<option>#{day}</option>\n"
|
||||
)
|
||||
end
|
||||
|
||||
select_html("day", day_options, options[:prefix], options[:include_blank], options[:discard_type])
|
||||
end
|
||||
|
||||
# Returns a select tag with options for each of the months January through December with the current month selected.
|
||||
# The month names are presented as keys (what's shown to the user) and the month numbers (1-12) are used as values
|
||||
# (what's submitted to the server). It's also possible to use month numbers for the presentation instead of names --
|
||||
# set the <tt>:use_month_numbers</tt> key in +options+ to true for this to happen. If you want both numbers and names,
|
||||
# set the <tt>:add_month_numbers</tt> key in +options+ to true. Examples:
|
||||
#
|
||||
# select_month(Date.today) # Will use keys like "January", "March"
|
||||
# select_month(Date.today, :use_month_numbers => true) # Will use keys like "1", "3"
|
||||
# select_month(Date.today, :add_month_numbers => true) # Will use keys like "1 - January", "3 - March"
|
||||
def select_month(date, options = {})
|
||||
month_options = []
|
||||
|
||||
1.upto(12) do |month_number|
|
||||
month_name = if options[:use_month_numbers]
|
||||
month_number
|
||||
elsif options[:add_month_numbers]
|
||||
month_number.to_s + " - " + Date::MONTHNAMES[month_number]
|
||||
else
|
||||
Date::MONTHNAMES[month_number]
|
||||
end
|
||||
|
||||
month_options << ((date.kind_of?(Fixnum) ? date : date.month) == month_number ?
|
||||
"<option value='#{month_number}' selected=\"selected\">#{month_name}</option>\n" :
|
||||
"<option value='#{month_number}'>#{month_name}</option>\n"
|
||||
)
|
||||
end
|
||||
|
||||
select_html("month", month_options, options[:prefix], options[:include_blank], options[:discard_type])
|
||||
end
|
||||
|
||||
# Returns a select tag with options for each of the five years on each side of the current, which is selected. The five year radius
|
||||
# can be changed using the <tt>:start_year</tt> and <tt>:end_year</tt> keys in the +options+. The <tt>date</tt> can also be substituted
|
||||
# for a year given as a number. Example:
|
||||
#
|
||||
# select_year(Date.today, :start_year => 1992, :end_year => 2007)
|
||||
def select_year(date, options = {})
|
||||
year_options = []
|
||||
unless date.kind_of?(Fixnum) then default_start_year, default_end_year = date.year - 5, date.year + 5 end
|
||||
|
||||
(options[:start_year] || default_start_year).upto(options[:end_year] || default_end_year) do |year|
|
||||
year_options << ((date.kind_of?(Fixnum) ? date : date.year) == year ?
|
||||
"<option selected=\"selected\">#{year}</option>\n" :
|
||||
"<option>#{year}</option>\n"
|
||||
)
|
||||
end
|
||||
|
||||
select_html("year", year_options, options[:prefix], options[:include_blank], options[:discard_type])
|
||||
end
|
||||
|
||||
private
|
||||
def select_html(type, options, prefix = nil, include_blank = false, discard_type = false)
|
||||
select_html = "<select name='#{prefix || DEFAULT_PREFIX}"
|
||||
select_html << "[#{type}]" unless discard_type
|
||||
select_html << "'>\n"
|
||||
select_html << "<option></option>\n" if include_blank
|
||||
select_html << options.to_s
|
||||
select_html << "</select>\n"
|
||||
|
||||
return select_html
|
||||
end
|
||||
|
||||
def leading_zero_on_single_digits(number)
|
||||
number > 9 ? number : "0#{number}"
|
||||
end
|
||||
end
|
||||
|
||||
class InstanceTag #:nodoc:
|
||||
include DateHelper
|
||||
|
||||
def to_date_select_tag(options = {})
|
||||
defaults = { :discard_type => true }
|
||||
options = defaults.merge(options)
|
||||
options_with_prefix = Proc.new { |position| options.update({ :prefix => "#{@object_name}[#{@method_name}(#{position}i)]" }) }
|
||||
date = options[:include_blank] ? (value || 0) : (value || Date.today)
|
||||
|
||||
date_select = ""
|
||||
|
||||
if options[:month_before_year]
|
||||
date_select << select_month(date, options_with_prefix.call(2)) unless options[:discard_month]
|
||||
date_select << select_year(date, options_with_prefix.call(1))
|
||||
else
|
||||
date_select << select_year(date, options_with_prefix.call(1))
|
||||
date_select << select_month(date, options_with_prefix.call(2)) unless options[:discard_month]
|
||||
end
|
||||
|
||||
date_select << select_day(date, options_with_prefix.call(3)) unless options[:discard_day] || options[:discard_month]
|
||||
|
||||
return date_select
|
||||
end
|
||||
|
||||
def to_datetime_select_tag(options = {})
|
||||
defaults = { :discard_type => true }
|
||||
options = defaults.merge(options)
|
||||
options_with_prefix = Proc.new { |position| options.update({ :prefix => "#{@object_name}[#{@method_name}(#{position}i)]" }) }
|
||||
datetime = options[:include_blank] ? (value || 0) : (value || Time.now)
|
||||
|
||||
datetime_select = select_year(datetime, options_with_prefix.call(1))
|
||||
datetime_select << select_month(datetime, options_with_prefix.call(2)) unless options[:discard_month]
|
||||
datetime_select << select_day(datetime, options_with_prefix.call(3)) unless options[:discard_day] || options[:discard_month]
|
||||
datetime_select << " — " + select_hour(datetime, options_with_prefix.call(4)) unless options[:discard_hour]
|
||||
datetime_select << " : " + select_minute(datetime, options_with_prefix.call(5)) unless options[:discard_minute] || options[:discard_hour]
|
||||
|
||||
return datetime_select
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,17 @@
|
|||
module ActionView
|
||||
module Helpers
|
||||
# Provides a set of methods for making it easier to locate problems.
|
||||
module DebugHelper
|
||||
# Returns a <pre>-tag set with the +object+ dumped by YAML. Very readable way to inspect an object.
|
||||
def debug(object)
|
||||
begin
|
||||
Marshal::dump(object)
|
||||
"<pre class='debug_dump'>#{h(object.to_yaml).gsub(" ", " ")}</pre>"
|
||||
rescue Object => e
|
||||
# Object couldn't be dumped, perhaps because of singleton methods -- this is the fallback
|
||||
"<code class='debug_dump'>#{h(object.inspect)}</code>"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,182 @@
|
|||
require 'cgi'
|
||||
require File.dirname(__FILE__) + '/date_helper'
|
||||
require File.dirname(__FILE__) + '/tag_helper'
|
||||
|
||||
module ActionView
|
||||
module Helpers
|
||||
# Provides a set of methods for working with forms and especially forms related to objects assigned to the template.
|
||||
# The following is an example of a complete form for a person object that works for both creates and updates built
|
||||
# with all the form helpers. The <tt>@person</tt> object was assigned by an action on the controller:
|
||||
# <form action="save_person" method="post">
|
||||
# Name:
|
||||
# <%= text_field "person", "name", "size" => 20 %>
|
||||
#
|
||||
# Password:
|
||||
# <%= password_field "person", "password", "maxsize" => 20 %>
|
||||
#
|
||||
# Single?:
|
||||
# <%= check_box "person", "single" %>
|
||||
#
|
||||
# Description:
|
||||
# <%= text_area "person", "description", "cols" => 20 %>
|
||||
#
|
||||
# <input type="submit" value="Save">
|
||||
# </form>
|
||||
#
|
||||
# ...is compiled to:
|
||||
#
|
||||
# <form action="save_person" method="post">
|
||||
# Name:
|
||||
# <input type="text" id="person_name" name="person[name]"
|
||||
# size="20" value="<%= @person.name %>" />
|
||||
#
|
||||
# Password:
|
||||
# <input type="password" id="person_password" name="person[password]"
|
||||
# size="20" maxsize="20" value="<%= @person.password %>" />
|
||||
#
|
||||
# Single?:
|
||||
# <input type="checkbox" id="person_single" name="person[single] value="1" />
|
||||
#
|
||||
# Description:
|
||||
# <textarea cols="20" rows="40" id="person_description" name="person[description]">
|
||||
# <%= @person.description %>
|
||||
# </textarea>
|
||||
#
|
||||
# <input type="submit" value="Save">
|
||||
# </form>
|
||||
#
|
||||
# There's also methods for helping to build form tags in link:classes/ActionView/Helpers/FormOptionsHelper.html,
|
||||
# link:classes/ActionView/Helpers/DateHelper.html, and link:classes/ActionView/Helpers/ActiveRecordHelper.html
|
||||
module FormHelper
|
||||
# Returns an input tag of the "text" type tailored for accessing a specified attribute (identified by +method+) on an object
|
||||
# assigned to the template (identified by +object+). Additional options on the input tag can be passed as a
|
||||
# hash with +options+.
|
||||
#
|
||||
# Examples (call, result):
|
||||
# text_field("post", "title", "size" => 20)
|
||||
# <input type="text" id="post_title" name="post[title]" size="20" value="#{@post.title}" />
|
||||
def text_field(object, method, options = {})
|
||||
InstanceTag.new(object, method, self).to_input_field_tag("text", options)
|
||||
end
|
||||
|
||||
# Works just like text_field, but returns a input tag of the "password" type instead.
|
||||
def password_field(object, method, options = {})
|
||||
InstanceTag.new(object, method, self).to_input_field_tag("password", options)
|
||||
end
|
||||
|
||||
# Works just like text_field, but returns a input tag of the "hidden" type instead.
|
||||
def hidden_field(object, method, options = {})
|
||||
InstanceTag.new(object, method, self).to_input_field_tag("hidden", options)
|
||||
end
|
||||
|
||||
# Returns a textarea opening and closing tag set tailored for accessing a specified attribute (identified by +method+)
|
||||
# on an object assigned to the template (identified by +object+). Additional options on the input tag can be passed as a
|
||||
# hash with +options+.
|
||||
#
|
||||
# Example (call, result):
|
||||
# text_area("post", "body", "cols" => 20, "rows" => 40)
|
||||
# <textarea cols="20" rows="40" id="post_body" name="post[body]">
|
||||
# #{@post.body}
|
||||
# </textarea>
|
||||
def text_area(object, method, options = {})
|
||||
InstanceTag.new(object, method, self).to_text_area_tag(options)
|
||||
end
|
||||
|
||||
# Returns a checkbox tag tailored for accessing a specified attribute (identified by +method+) on an object
|
||||
# assigned to the template (identified by +object+). It's intended that +method+ returns an integer and if that
|
||||
# integer is above zero, then the checkbox is checked. Additional options on the input tag can be passed as a
|
||||
# hash with +options+. The +checked_value+ defaults to 1 while the default +unchecked_value+
|
||||
# is set to 0 which is convenient for boolean values. Usually unchecked checkboxes don't post anything.
|
||||
# We work around this problem by adding a hidden value with the same name as the checkbox.
|
||||
#
|
||||
# Example (call, result). Imagine that @post.validated? returns 1:
|
||||
# check_box("post", "validated")
|
||||
# <input type="checkbox" id="post_validate" name="post[validated] value="1" checked="checked" /><input name="post[validated]" type="hidden" value="0" />
|
||||
#
|
||||
# Example (call, result). Imagine that @puppy.gooddog returns no:
|
||||
# check_box("puppy", "gooddog", {}, "yes", "no")
|
||||
# <input type="checkbox" id="puppy_gooddog" name="puppy[gooddog] value="yes" /><input name="puppy[gooddog]" type="hidden" value="no" />
|
||||
def check_box(object, method, options = {}, checked_value = "1", unchecked_value = "0")
|
||||
InstanceTag.new(object, method, self).to_check_box_tag(options, checked_value, unchecked_value)
|
||||
end
|
||||
end
|
||||
|
||||
class InstanceTag #:nodoc:
|
||||
include Helpers::TagHelper
|
||||
|
||||
attr_reader :method_name, :object_name
|
||||
|
||||
DEFAULT_FIELD_OPTIONS = { "size" => 30 } unless const_defined?("DEFAULT_FIELD_OPTIONS")
|
||||
DEFAULT_TEXT_AREA_OPTIONS = { "wrap" => "virtual", "cols" => 40, "rows" => 20 } unless const_defined?("DEFAULT_TEXT_AREA_OPTIONS")
|
||||
|
||||
def initialize(object_name, method_name, template_object, local_binding = nil)
|
||||
@object_name, @method_name = object_name, method_name
|
||||
@template_object, @local_binding = template_object, local_binding
|
||||
end
|
||||
|
||||
def to_input_field_tag(field_type, options = {})
|
||||
html_options = DEFAULT_FIELD_OPTIONS.merge(options)
|
||||
html_options.merge!({ "size" => options["maxlength"]}) if options["maxlength"] && !options["size"]
|
||||
html_options.merge!({ "type" => field_type, "value" => value.to_s })
|
||||
add_default_name_and_id(html_options)
|
||||
tag("input", html_options)
|
||||
end
|
||||
|
||||
def to_text_area_tag(options = {})
|
||||
options = DEFAULT_TEXT_AREA_OPTIONS.merge(options)
|
||||
add_default_name_and_id(options)
|
||||
content_tag("textarea", html_escape(value), options)
|
||||
end
|
||||
|
||||
def to_check_box_tag(options = {}, checked_value = "1", unchecked_value = "0")
|
||||
options.merge!({"checked" => "checked"}) if !value.nil? && ((value.is_a?(TrueClass) || value.is_a?(FalseClass)) ? value : value.to_i > 0)
|
||||
options.merge!({ "type" => "checkbox", "value" => checked_value })
|
||||
add_default_name_and_id(options)
|
||||
tag("input", options) << tag("input", ({ "name" => options['name'], "type" => "hidden", "value" => unchecked_value }))
|
||||
end
|
||||
|
||||
def to_date_tag()
|
||||
defaults = { "discard_type" => true }
|
||||
date = value || Date.today
|
||||
options = Proc.new { |position| defaults.update({ :prefix => "#{@object_name}[#{@method_name}(#{position}i)]" }) }
|
||||
|
||||
html_day_select(date, options.call(3)) +
|
||||
html_month_select(date, options.call(2)) +
|
||||
html_year_select(date, options.call(1))
|
||||
end
|
||||
|
||||
def to_boolean_select_tag(options = {})
|
||||
add_default_name_and_id(options)
|
||||
tag_text = "<select"
|
||||
tag_text << tag_options(options)
|
||||
tag_text << "><option value=\"false\""
|
||||
tag_text << " selected" if value == false
|
||||
tag_text << ">False</option><option value=\"true\""
|
||||
tag_text << " selected" if value
|
||||
tag_text << ">True</option></select>"
|
||||
end
|
||||
|
||||
def object
|
||||
@template_object.instance_variable_get "@#{@object_name}"
|
||||
end
|
||||
|
||||
def value
|
||||
object.send(@method_name) unless object.nil?
|
||||
end
|
||||
|
||||
private
|
||||
def add_default_name_and_id(options)
|
||||
options['name'] = tag_name unless options.has_key? "name"
|
||||
options['id'] = tag_id unless options.has_key? "id"
|
||||
end
|
||||
|
||||
def tag_name
|
||||
"#{@object_name}[#{@method_name}]"
|
||||
end
|
||||
|
||||
def tag_id
|
||||
"#{@object_name}_#{@method_name}"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,212 @@
|
|||
require 'cgi'
|
||||
require 'erb'
|
||||
require File.dirname(__FILE__) + '/form_helper'
|
||||
|
||||
module ActionView
|
||||
module Helpers
|
||||
# Provides a number of methods for turning different kinds of containers into a set of option tags. Neither of the methods provide
|
||||
# the actual select tag, so you'll need to construct that in HTML manually.
|
||||
module FormOptionsHelper
|
||||
include ERB::Util
|
||||
|
||||
def select(object, method, choices, options = {}, html_options = {})
|
||||
InstanceTag.new(object, method, self).to_select_tag(choices, options, html_options)
|
||||
end
|
||||
|
||||
def collection_select(object, method, collection, value_method, text_method, options = {}, html_options = {})
|
||||
InstanceTag.new(object, method, self).to_collection_select_tag(collection, value_method, text_method, options, html_options)
|
||||
end
|
||||
|
||||
def country_select(object, method, priority_countries = nil, options = {}, html_options = {})
|
||||
InstanceTag.new(object, method, self).to_country_select_tag(priority_countries, options, html_options)
|
||||
end
|
||||
|
||||
# Accepts a container (hash, array, enumerable, your type) and returns a string of option tags. Given a container
|
||||
# where the elements respond to first and last (such as a two-element array), the "lasts" serve as option values and
|
||||
# the "firsts" as option text. Hashes are turned into this form automatically, so the keys become "firsts" and values
|
||||
# become lasts. If +selected+ is specified, the matching "last" or element will get the selected option-tag. +Selected+
|
||||
# may also be an array of values to be selected when using a multiple select.
|
||||
#
|
||||
# Examples (call, result):
|
||||
# options_for_select([["Dollar", "$"], ["Kroner", "DKK"]])
|
||||
# <option value="$">Dollar</option>\n<option value="DKK">Kroner</option>
|
||||
#
|
||||
# options_for_select([ "VISA", "Mastercard" ], "Mastercard")
|
||||
# <option>VISA</option>\n<option selected="selected">Mastercard</option>
|
||||
#
|
||||
# options_for_select({ "Basic" => "$20", "Plus" => "$40" }, "$40")
|
||||
# <option value="$20">Basic</option>\n<option value="$40" selected="selected">Plus</option>
|
||||
#
|
||||
# options_for_select([ "VISA", "Mastercard", "Discover" ], ["VISA", "Discover"])
|
||||
# <option selected="selected">VISA</option>\n<option>Mastercard</option>\n<option selected="selected">Discover</option>
|
||||
def options_for_select(container, selected = nil)
|
||||
container = container.to_a if Hash === container
|
||||
|
||||
options_for_select = container.inject([]) do |options, element|
|
||||
if element.respond_to?(:first) && element.respond_to?(:last)
|
||||
is_selected = ( (selected.respond_to?(:include?) ? selected.include?(element.last) : element.last == selected) )
|
||||
if is_selected
|
||||
options << "<option value=\"#{html_escape(element.last.to_s)}\" selected=\"selected\">#{html_escape(element.first.to_s)}</option>"
|
||||
else
|
||||
options << "<option value=\"#{html_escape(element.last.to_s)}\">#{html_escape(element.first.to_s)}</option>"
|
||||
end
|
||||
else
|
||||
is_selected = ( (selected.respond_to?(:include?) ? selected.include?(element) : element == selected) )
|
||||
options << ((is_selected) ? "<option selected=\"selected\">#{html_escape(element.to_s)}</option>" : "<option>#{html_escape(element.to_s)}</option>")
|
||||
end
|
||||
end
|
||||
|
||||
options_for_select.join("\n")
|
||||
end
|
||||
|
||||
# Returns a string of option tags that has been compiled by iterating over the +collection+ and assigning the
|
||||
# the result of a call to the +value_method+ as the option value and the +text_method+ as the option text.
|
||||
# If +selected_value+ is specified, the element returning a match on +value_method+ will get the selected option tag.
|
||||
#
|
||||
# Example (call, result). Imagine a loop iterating over each +person+ in <tt>@project.people</tt> to generate a input tag:
|
||||
# options_from_collection_for_select(@project.people, "id", "name")
|
||||
# <option value="#{person.id}">#{person.name}</option>
|
||||
def options_from_collection_for_select(collection, value_method, text_method, selected_value = nil)
|
||||
options_for_select(
|
||||
collection.inject([]) { |options, object| options << [ object.send(text_method), object.send(value_method) ] },
|
||||
selected_value
|
||||
)
|
||||
end
|
||||
|
||||
# Returns a string of option tags, like options_from_collection_for_select, but surrounds them by <optgroup> tags.
|
||||
#
|
||||
# An array of group objects are passed. Each group should return an array of options when calling group_method
|
||||
# Each group should should return its name when calling group_label_method.
|
||||
#
|
||||
# html_option_groups_from_collection(@continents, "countries", "contient_name", "country_id", "country_name", @selected_country.id)
|
||||
#
|
||||
# Could become:
|
||||
# <optgroup label="Africa">
|
||||
# <select>Egypt</select>
|
||||
# <select>Rwanda</select>
|
||||
# ...
|
||||
# </optgroup>
|
||||
# <optgroup label="Asia">
|
||||
# <select>China</select>
|
||||
# <select>India</select>
|
||||
# <select>Japan</select>
|
||||
# ...
|
||||
# </optgroup>
|
||||
#
|
||||
# with objects of the following classes:
|
||||
# class Continent
|
||||
# def initialize(p_name, p_countries) @continent_name = p_name; @countries = p_countries; end
|
||||
# def continent_name() @continent_name; end
|
||||
# def countries() @countries; end
|
||||
# end
|
||||
# class Country
|
||||
# def initialize(id, name) @id = id; @name = name end
|
||||
# def country_id() @id; end
|
||||
# def country_name() @name; end
|
||||
# end
|
||||
def option_groups_from_collection_for_select(collection, group_method, group_label_method,
|
||||
option_key_method, option_value_method, selected_key = nil)
|
||||
collection.inject("") do |options_for_select, group|
|
||||
group_label_string = eval("group.#{group_label_method}")
|
||||
options_for_select += "<optgroup label=\"#{html_escape(group_label_string)}\">"
|
||||
options_for_select += options_from_collection_for_select(eval("group.#{group_method}"), option_key_method, option_value_method, selected_key)
|
||||
options_for_select += '</optgroup>'
|
||||
end
|
||||
end
|
||||
|
||||
# Returns a string of option tags for pretty much any country in the world. Supply a country name as +selected+ to
|
||||
# have it marked as the selected option tag. You can also supply an array of countries as +priority_countries+, so
|
||||
# that they will be listed above the rest of the (long) list.
|
||||
def country_options_for_select(selected = nil, priority_countries = nil)
|
||||
country_options = ""
|
||||
|
||||
if priority_countries
|
||||
country_options += options_for_select(priority_countries, selected)
|
||||
country_options += "<option>-------------</option>\n"
|
||||
end
|
||||
|
||||
if priority_countries && priority_countries.include?(selected)
|
||||
country_options += options_for_select(COUNTRIES - priority_countries, selected)
|
||||
else
|
||||
country_options += options_for_select(COUNTRIES, selected)
|
||||
end
|
||||
|
||||
return country_options
|
||||
end
|
||||
|
||||
|
||||
private
|
||||
# All the countries included in the country_options output.
|
||||
COUNTRIES = [ "Albania", "Algeria", "American Samoa", "Andorra", "Angola", "Anguilla",
|
||||
"Antarctica", "Antigua And Barbuda", "Argentina", "Armenia", "Aruba", "Australia",
|
||||
"Austria", "Azerbaijan", "Bahamas", "Bahrain", "Bangladesh", "Barbados", "Belarus",
|
||||
"Belgium", "Belize", "Benin", "Bermuda", "Bhutan", "Bolivia", "Bosnia and Herzegowina",
|
||||
"Botswana", "Bouvet Island", "Brazil", "British Indian Ocean Territory",
|
||||
"Brunei Darussalam", "Bulgaria", "Burkina Faso", "Burma", "Burundi", "Cambodia",
|
||||
"Cameroon", "Canada", "Cape Verde", "Cayman Islands", "Central African Republic",
|
||||
"Chad", "Chile", "China", "Christmas Island", "Cocos (Keeling) Islands", "Colombia",
|
||||
"Comoros", "Congo", "Congo, the Democratic Republic of the", "Cook Islands",
|
||||
"Costa Rica", "Cote d'Ivoire", "Croatia", "Cyprus", "Czech Republic", "Denmark",
|
||||
"Djibouti", "Dominica", "Dominican Republic", "East Timor", "Ecuador", "Egypt",
|
||||
"El Salvador", "England", "Equatorial Guinea", "Eritrea", "Espana", "Estonia",
|
||||
"Ethiopia", "Falkland Islands", "Faroe Islands", "Fiji", "Finland", "France",
|
||||
"French Guiana", "French Polynesia", "French Southern Territories", "Gabon", "Gambia",
|
||||
"Georgia", "Germany", "Ghana", "Gibraltar", "Great Britain", "Greece", "Greenland",
|
||||
"Grenada", "Guadeloupe", "Guam", "Guatemala", "Guinea", "Guinea-Bissau", "Guyana",
|
||||
"Haiti", "Heard and Mc Donald Islands", "Honduras", "Hong Kong", "Hungary", "Iceland",
|
||||
"India", "Indonesia", "Ireland", "Israel", "Italy", "Jamaica", "Japan", "Jordan",
|
||||
"Kazakhstan", "Kenya", "Kiribati", "Korea, Republic of", "Korea (South)", "Kuwait",
|
||||
"Kyrgyzstan", "Lao People's Democratic Republic", "Latvia", "Lebanon", "Lesotho",
|
||||
"Liberia", "Liechtenstein", "Lithuania", "Luxembourg", "Macau", "Macedonia",
|
||||
"Madagascar", "Malawi", "Malaysia", "Maldives", "Mali", "Malta", "Marshall Islands",
|
||||
"Martinique", "Mauritania", "Mauritius", "Mayotte", "Mexico",
|
||||
"Micronesia, Federated States of", "Moldova, Republic of", "Monaco", "Mongolia",
|
||||
"Montserrat", "Morocco", "Mozambique", "Myanmar", "Namibia", "Nauru", "Nepal",
|
||||
"Netherlands", "Netherlands Antilles", "New Caledonia", "New Zealand", "Nicaragua",
|
||||
"Niger", "Nigeria", "Niue", "Norfolk Island", "Northern Ireland",
|
||||
"Northern Mariana Islands", "Norway", "Oman", "Pakistan", "Palau", "Panama",
|
||||
"Papua New Guinea", "Paraguay", "Peru", "Philippines", "Pitcairn", "Poland",
|
||||
"Portugal", "Puerto Rico", "Qatar", "Reunion", "Romania", "Russia", "Rwanda",
|
||||
"Saint Kitts and Nevis", "Saint Lucia", "Saint Vincent and the Grenadines",
|
||||
"Samoa (Independent)", "San Marino", "Sao Tome and Principe", "Saudi Arabia",
|
||||
"Scotland", "Senegal", "Seychelles", "Sierra Leone", "Singapore", "Slovakia",
|
||||
"Slovenia", "Solomon Islands", "Somalia", "South Africa",
|
||||
"South Georgia and the South Sandwich Islands", "South Korea", "Spain", "Sri Lanka",
|
||||
"St. Helena", "St. Pierre and Miquelon", "Suriname", "Svalbard and Jan Mayen Islands",
|
||||
"Swaziland", "Sweden", "Switzerland", "Taiwan", "Tajikistan", "Tanzania", "Thailand",
|
||||
"Togo", "Tokelau", "Tonga", "Trinidad", "Trinidad and Tobago", "Tunisia", "Turkey",
|
||||
"Turkmenistan", "Turks and Caicos Islands", "Tuvalu", "Uganda", "Ukraine",
|
||||
"United Arab Emirates", "United Kingdom", "United States",
|
||||
"United States Minor Outlying Islands", "Uruguay", "Uzbekistan", "Vanuatu",
|
||||
"Vatican City State (Holy See)", "Venezuela", "Viet Nam", "Virgin Islands (British)",
|
||||
"Virgin Islands (U.S.)", "Wales", "Wallis and Futuna Islands", "Western Sahara",
|
||||
"Yemen", "Zambia", "Zimbabwe" ] unless const_defined?("COUNTRIES")
|
||||
end
|
||||
|
||||
class InstanceTag #:nodoc:
|
||||
include FormOptionsHelper
|
||||
|
||||
def to_select_tag(choices, options, html_options)
|
||||
add_default_name_and_id(html_options)
|
||||
content_tag("select", add_blank_option(options_for_select(choices, value), options[:include_blank]), html_options)
|
||||
end
|
||||
|
||||
def to_collection_select_tag(collection, value_method, text_method, options, html_options)
|
||||
add_default_name_and_id(html_options)
|
||||
content_tag(
|
||||
"select", add_blank_option(options_from_collection_for_select(collection, value_method, text_method, value), options[:include_blank]), html_options
|
||||
)
|
||||
end
|
||||
|
||||
def to_country_select_tag(priority_countries, options, html_options)
|
||||
add_default_name_and_id(html_options)
|
||||
content_tag("select", add_blank_option(country_options_for_select(value, priority_countries), options[:include_blank]), html_options)
|
||||
end
|
||||
|
||||
private
|
||||
def add_blank_option(option_tags, add_blank)
|
||||
add_blank ? "<option></option>\n" + option_tags : option_tags
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,59 @@
|
|||
require 'cgi'
|
||||
|
||||
module ActionView
|
||||
module Helpers
|
||||
# This is poor man's Builder for the rare cases where you need to programmatically make tags but can't use Builder.
|
||||
module TagHelper
|
||||
include ERB::Util
|
||||
|
||||
# Examples:
|
||||
# * tag("br") => <br />
|
||||
# * tag("input", { "type" => "text"}) => <input type="text" />
|
||||
def tag(name, options = {}, open = false)
|
||||
"<#{name + tag_options(options)}" + (open ? ">" : " />")
|
||||
end
|
||||
|
||||
# Examples:
|
||||
# * content_tag("p", "Hello world!") => <p>Hello world!</p>
|
||||
# * content_tag("div", content_tag("p", "Hello world!"), "class" => "strong") =>
|
||||
# <div class="strong"><p>Hello world!</p></div>
|
||||
def content_tag(name, content, options = {})
|
||||
"<#{name + tag_options(options)}>#{content}</#{name}>"
|
||||
end
|
||||
|
||||
# Starts a form tag that points the action to an url configured with <tt>url_for_options</tt> just like
|
||||
# ActionController::Base#url_for.
|
||||
def form_tag(url_for_options, options = {}, *parameters_for_url)
|
||||
html_options = { "method" => "POST" }.merge(options)
|
||||
|
||||
if html_options[:multipart]
|
||||
html_options["enctype"] = "multipart/form-data"
|
||||
html_options.delete(:multipart)
|
||||
end
|
||||
|
||||
html_options["action"] = url_for(url_for_options, *parameters_for_url)
|
||||
|
||||
tag("form", html_options, true)
|
||||
end
|
||||
|
||||
alias_method :start_form_tag, :form_tag
|
||||
|
||||
# Outputs "</form>"
|
||||
def end_form_tag
|
||||
"</form>"
|
||||
end
|
||||
|
||||
|
||||
private
|
||||
def tag_options(options)
|
||||
if options.empty?
|
||||
""
|
||||
else
|
||||
" " + options.collect { |pair|
|
||||
"#{pair.first}=\"#{html_escape(pair.last)}\""
|
||||
}.sort.join(" ")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,111 @@
|
|||
module ActionView
|
||||
module Helpers #:nodoc:
|
||||
# Provides a set of methods for working with text strings that can help unburden the level of inline Ruby code in the
|
||||
# templates. In the example below we iterate over a collection of posts provided to the template and prints each title
|
||||
# after making sure it doesn't run longer than 20 characters:
|
||||
# <% for post in @posts %>
|
||||
# Title: <%= truncate(post.title, 20) %>
|
||||
# <% end %>
|
||||
module TextHelper
|
||||
# The regular puts and print are outlawed in eRuby. It's recommended to use the <%= "hello" %> form instead of print "hello".
|
||||
# If you absolutely must use a method-based output, you can use concat. It's use like this <% concat "hello", binding %>. Notice that
|
||||
# it doesn't have an equal sign in front. Using <%= concat "hello" %> would result in a double hello.
|
||||
def concat(string, binding)
|
||||
eval("_erbout", binding).concat(string)
|
||||
end
|
||||
|
||||
# Truncates +text+ to the length of +length+ and replaces the last three characters with the +truncate_string+
|
||||
# if the +text+ is longer than +length+.
|
||||
def truncate(text, length = 30, truncate_string = "...")
|
||||
if text.nil? then return end
|
||||
if text.length > length then text[0..(length - 3)] + truncate_string else text end
|
||||
end
|
||||
|
||||
# Highlights the +phrase+ where it is found in the +text+ by surrounding it like
|
||||
# <strong class="highlight">I'm a highlight phrase</strong>. The highlighter can be specialized by
|
||||
# passing +highlighter+ as single-quoted string with \1 where the phrase is supposed to be inserted.
|
||||
# N.B.: The +phrase+ is sanitized to include only letters, digits, and spaces before use.
|
||||
def highlight(text, phrase, highlighter = '<strong class="highlight">\1</strong>')
|
||||
if text.nil? || phrase.nil? then return end
|
||||
text.gsub(/(#{escape_regexp(phrase)})/i, highlighter) unless text.nil?
|
||||
end
|
||||
|
||||
# Extracts an excerpt from the +text+ surrounding the +phrase+ with a number of characters on each side determined
|
||||
# by +radius+. If the phrase isn't found, nil is returned. Ex:
|
||||
# excerpt("hello my world", "my", 3) => "...lo my wo..."
|
||||
def excerpt(text, phrase, radius = 100, excerpt_string = "...")
|
||||
if text.nil? || phrase.nil? then return end
|
||||
phrase = escape_regexp(phrase)
|
||||
|
||||
if found_pos = text =~ /(#{phrase})/i
|
||||
start_pos = [ found_pos - radius, 0 ].max
|
||||
end_pos = [ found_pos + phrase.length + radius, text.length ].min
|
||||
|
||||
prefix = start_pos > 0 ? excerpt_string : ""
|
||||
postfix = end_pos < text.length ? excerpt_string : ""
|
||||
|
||||
prefix + text[start_pos..end_pos].strip + postfix
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
# Attempts to pluralize the +singular+ word unless +count+ is 1. See source for pluralization rules.
|
||||
def pluralize(count, singular, plural = nil)
|
||||
"#{count} " + if count == 1
|
||||
singular
|
||||
elsif plural
|
||||
plural
|
||||
elsif Object.const_defined?("Inflector")
|
||||
Inflector.pluralize(singular)
|
||||
else
|
||||
singular + "s"
|
||||
end
|
||||
end
|
||||
|
||||
begin
|
||||
require "redcloth"
|
||||
|
||||
# Returns the text with all the Textile codes turned into HTML-tags.
|
||||
# <i>This method is only available if RedCloth can be required</i>.
|
||||
def textilize(text)
|
||||
RedCloth.new(text).to_html
|
||||
end
|
||||
|
||||
# Returns the text with all the Textile codes turned into HTML-tags, but without the regular bounding <p> tag.
|
||||
# <i>This method is only available if RedCloth can be required</i>.
|
||||
def textilize_without_paragraph(text)
|
||||
textiled = textilize(text)
|
||||
if textiled[0..2] == "<p>" then textiled = textiled[3..-1] end
|
||||
if textiled[-4..-1] == "</p>" then textiled = textiled[0..-5] end
|
||||
return textiled
|
||||
end
|
||||
rescue LoadError
|
||||
# We can't really help what's not there
|
||||
end
|
||||
|
||||
begin
|
||||
require "bluecloth"
|
||||
|
||||
# Returns the text with all the Markdown codes turned into HTML-tags.
|
||||
# <i>This method is only available if BlueCloth can be required</i>.
|
||||
def markdown(text)
|
||||
BlueCloth.new(text).to_html
|
||||
end
|
||||
rescue LoadError
|
||||
# We can't really help what's not there
|
||||
end
|
||||
|
||||
# Turns all links into words, like "<a href="something">else</a>" to "else".
|
||||
def strip_links(text)
|
||||
text.gsub(/<a.*>(.*)<\/a>/m, '\1')
|
||||
end
|
||||
|
||||
private
|
||||
# Returns a version of the text that's safe to use in a regular expression without triggering engine features.
|
||||
def escape_regexp(text)
|
||||
text.gsub(/([\\|?+*\/\)\(])/) { |m| "\\#{$1}" }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue